From e08038ded4822109658a8330c34604746d874565 Mon Sep 17 00:00:00 2001 From: Francisco Date: Tue, 27 May 2025 15:33:36 +0200 Subject: [PATCH 1/7] first commit --- java/Iceberg/IcebergSQLJSONGlue/README.md | 78 ++++++ java/Iceberg/IcebergSQLJSONGlue/pom.xml | 225 ++++++++++++++++++ .../main/java/GlueTableSQLJSONExample.java | 189 +++++++++++++++ .../src/main/java/StockPrice.java | 60 +++++ .../java/StockPriceGeneratorFunction.java | 24 ++ .../flink-application-properties-dev.json | 16 ++ .../src/main/resources/log4j2.properties | 13 + .../src/main/resources/price.avsc | 23 ++ 8 files changed, 628 insertions(+) create mode 100644 java/Iceberg/IcebergSQLJSONGlue/README.md create mode 100644 java/Iceberg/IcebergSQLJSONGlue/pom.xml create mode 100644 java/Iceberg/IcebergSQLJSONGlue/src/main/java/GlueTableSQLJSONExample.java create mode 100644 java/Iceberg/IcebergSQLJSONGlue/src/main/java/StockPrice.java create mode 100644 java/Iceberg/IcebergSQLJSONGlue/src/main/java/StockPriceGeneratorFunction.java create mode 100644 java/Iceberg/IcebergSQLJSONGlue/src/main/resources/flink-application-properties-dev.json create mode 100644 java/Iceberg/IcebergSQLJSONGlue/src/main/resources/log4j2.properties create mode 100644 java/Iceberg/IcebergSQLJSONGlue/src/main/resources/price.avsc diff --git a/java/Iceberg/IcebergSQLJSONGlue/README.md b/java/Iceberg/IcebergSQLJSONGlue/README.md new file mode 100644 index 0000000..ca05aa6 --- /dev/null +++ b/java/Iceberg/IcebergSQLJSONGlue/README.md @@ -0,0 +1,78 @@ +# Flink Iceberg Sink using SQL API + +* Flink version: 1.20.0 +* Flink API: SQL API +* Iceberg 1.8.1 +* Language: Java (11) +* Flink connectors: [DataGen](https://nightlies.apache.org/flink/flink-docs-release-1.20/docs/connectors/datastream/datagen/) + and [Iceberg](https://iceberg.apache.org/docs/latest/flink/) + +This example demonstrates how to use +[Flink SQL API with Iceberg](https://iceberg.apache.org/docs/latest/flink-writes/) and the Glue Data Catalog. + +For simplicity, the application generates synthetic data, random stock prices, internally. +Data is generated as POJO objects, simulating a real source, for example a Kafka Source, that receives records +that can be converted to table format for SQL operations. + +### Prerequisites + +The application expects the following resources: +* A Glue Data Catalog database in the current AWS region. The database name is configurable (default: "default"). + The application creates the Table, but the Catalog must exist already. +* An S3 bucket to write the Iceberg table. + +#### IAM Permissions + +The application must have IAM permissions to: +* Show and alter Glue Data Catalog databases, show and create Glue Data Catalog tables. + See [Glue Data Catalog permissions](https://docs.aws.amazon.com/athena/latest/ug/fine-grained-access-to-glue-resources.html). +* Read and Write from the S3 bucket. + +### Runtime configuration + +When running on Amazon Managed Service for Apache Flink the runtime configuration is read from Runtime Properties. + +When running locally, the configuration is read from the +[resources/flink-application-properties-dev.json](./src/main/resources/flink-application-properties-dev.json) file. + +Runtime parameters: + +| Group ID | Key | Default | Description | +|-----------|--------------------------|-------------------|---------------------------------------------------------------------------------------------------------------------| +| `DataGen` | `records.per.sec` | `10.0` | Records per second generated. | +| `Iceberg` | `bucket.prefix` | (mandatory) | S3 bucket prefix, for example `s3://my-bucket/iceberg`. | +| `Iceberg` | `catalog.db` | `default` | Name of the Glue Data Catalog database. | +| `Iceberg` | `catalog.table` | `prices_iceberg` | Name of the Glue Data Catalog table. | + +### Checkpoints + +Checkpointing must be enabled. Iceberg commits writes on checkpoint. + +When running locally, the application enables checkpoints programmatically, every 30 seconds. +When deployed to Managed Service for Apache Flink, checkpointing is controlled by the application configuration. + +### Known limitations + +At the moment there are current limitations concerning Flink Iceberg integration: +* Doesn't support Iceberg Table with hidden partitioning +* Doesn't support adding columns, removing columns, renaming columns or changing columns. + +### Schema and schema evolution + +The application uses a predefined schema for the stock price data with the following fields: +* `timestamp`: STRING - ISO timestamp of the record +* `symbol`: STRING - Stock symbol (e.g., AAPL, AMZN) +* `price`: FLOAT - Stock price (0-10 range) +* `volumes`: INT - Trade volumes (0-1000000 range) + +This schema matches the AVRO schema used in the DataStream API example for consistency. +The equivalent AVRO schema definition is available in [price.avsc](./src/main/resources/price.avsc) for reference. + +Schema changes would require updating both the POJO class and the SQL table definition. +Unlike the DataStream approach with AVRO, this SQL approach requires explicit schema management. + +### Running locally, in IntelliJ + +You can run this example directly in IntelliJ, without any local Flink cluster or local Flink installation. + +See [Running examples locally](https://github.com/nicusX/amazon-managed-service-for-apache-flink-examples/blob/main/java/running-examples-locally.md) for details. diff --git a/java/Iceberg/IcebergSQLJSONGlue/pom.xml b/java/Iceberg/IcebergSQLJSONGlue/pom.xml new file mode 100644 index 0000000..1df90e1 --- /dev/null +++ b/java/Iceberg/IcebergSQLJSONGlue/pom.xml @@ -0,0 +1,225 @@ + + + 4.0.0 + + com.amazonaws + iceberg-sql-flink + 1.0 + jar + + + UTF-8 + 11 + ${target.java.version} + ${target.java.version} + + 1.20.0 + 1.11.3 + 2.12 + 3.4.0 + 1.8.1 + 1.2.0 + 2.23.1 + 5.8.1 + + + + + + org.apache.flink + flink-runtime-web + ${flink.version} + provided + + + org.apache.flink + flink-streaming-java + ${flink.version} + provided + + + org.apache.flink + flink-table-runtime + ${flink.version} + provided + + + org.apache.flink + flink-table-api-java-bridge + ${flink.version} + + + org.apache.flink + flink-table-common + ${flink.version} + + + org.apache.flink + flink-metrics-dropwizard + ${flink.version} + + + org.apache.flink + flink-avro + ${flink.version} + + + + + org.apache.flink + flink-table-planner_${scala.version} + ${flink.version} + provided + + + + + com.amazonaws + aws-kinesisanalytics-runtime + ${kda.runtime.version} + provided + + + org.apache.flink + flink-connector-files + ${flink.version} + provided + + + + org.apache.hadoop + hadoop-client + ${hadoop.version} + + + org.apache.avro + avro + + + org.slf4j + slf4j-reload4j + + + + + org.apache.hadoop + hadoop-common + ${hadoop.version} + + + org.apache.hadoop + hadoop-mapreduce-client-core + ${hadoop.version} + + + + + org.apache.iceberg + iceberg-core + ${iceberg.version} + + + + org.apache.iceberg + iceberg-flink + ${iceberg.version} + + + org.apache.iceberg + iceberg-aws-bundle + ${iceberg.version} + + + org.apache.iceberg + iceberg-aws + ${iceberg.version} + + + org.apache.iceberg + iceberg-flink-1.20 + ${iceberg.version} + + + + org.junit.jupiter + junit-jupiter + ${junit5.version} + test + + + + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${target.java.version} + ${target.java.version} + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + package + + shade + + + + + org.apache.flink:force-shading + com.google.code.findbugs:jsr305 + org.slf4j:* + log4j:* + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + GlueTableSQLJSONExample + + + + + + + + + diff --git a/java/Iceberg/IcebergSQLJSONGlue/src/main/java/GlueTableSQLJSONExample.java b/java/Iceberg/IcebergSQLJSONGlue/src/main/java/GlueTableSQLJSONExample.java new file mode 100644 index 0000000..15b0c42 --- /dev/null +++ b/java/Iceberg/IcebergSQLJSONGlue/src/main/java/GlueTableSQLJSONExample.java @@ -0,0 +1,189 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: MIT-0 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this + * software and associated documentation files (the "Software"), to deal in the Software + * without restriction, including without limitation the rights to use, copy, modify, + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import com.amazonaws.services.kinesisanalytics.runtime.KinesisAnalyticsRuntime; +import org.apache.flink.api.common.eventtime.WatermarkStrategy; +import org.apache.flink.api.common.typeinfo.TypeInformation; +import org.apache.flink.api.connector.source.util.ratelimit.RateLimiterStrategy; +import org.apache.flink.configuration.Configuration; +import org.apache.flink.connector.datagen.source.DataGeneratorSource; +import org.apache.flink.streaming.api.datastream.DataStream; +import org.apache.flink.streaming.api.environment.LocalStreamEnvironment; +import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; +import org.apache.flink.table.api.Table; +import org.apache.flink.table.api.TableResult; +import org.apache.flink.table.api.bridge.java.StreamTableEnvironment; +import org.apache.flink.table.catalog.Catalog; +import org.apache.flink.table.catalog.CatalogDatabaseImpl; +import org.apache.flink.table.catalog.CatalogDescriptor; +import org.apache.flink.util.Preconditions; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.flink.CatalogLoader; +import org.apache.iceberg.flink.FlinkCatalog; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; + +public class GlueTableSQLJSONExample { + // Constants + private static final String CATALOG_NAME = "glue"; + private static final String LOCAL_APPLICATION_PROPERTIES_RESOURCE = "flink-application-properties-dev.json"; + private static final Logger LOG = LoggerFactory.getLogger(GlueTableSQLJSONExample.class); + + // Configuration properties + private static String s3BucketPrefix; + private static String glueDatabase; + private static String glueTable; + + public static void main(String[] args) throws Exception { + // 1. Initialize environments - using standard environment instead of WebUI for production consistency + final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); + final StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env); + + // 2. Load properties and configure environment + Map applicationProperties = loadApplicationProperties(env); + Properties icebergProperties = applicationProperties.get("Iceberg"); + + // Configure local development settings if needed + if (isLocal(env)) { + env.enableCheckpointing(30000); + env.setParallelism(2); + } + + // 3. Setup configuration properties with validation + setupIcebergProperties(icebergProperties); + Catalog glueCatalog = createGlueCatalog(tableEnv); + tableEnv.registerCatalog(CATALOG_NAME,glueCatalog); + + // 4. Create data generator source + Properties dataGenProperties = applicationProperties.get("DataGen"); + DataStream stockPriceDataStream = env.fromSource( + createDataGenerator(dataGenProperties), + WatermarkStrategy.noWatermarks(), + "DataGen"); + + + // 5. Convert DataStream to Table and create view + Table stockPriceTable = tableEnv.fromDataStream(stockPriceDataStream); + tableEnv.createTemporaryView("stockPriceTable", stockPriceTable); + + String sinkTableName = CATALOG_NAME + "." + glueDatabase + "." + glueTable; + + // Define and create table with schema matching AVRO schema from DataStream example + String createTableStatement = "CREATE TABLE IF NOT EXISTS " + sinkTableName + " (" + + "`timestamp` STRING, " + + "symbol STRING," + + "price FLOAT," + + "volumes INT" + + ") PARTITIONED BY (symbol) "; + + LOG.info("Creating table with statement: {}", createTableStatement); + tableEnv.executeSql(createTableStatement); + + // 7. Execute SQL operations - Insert data from stock price stream + String insertQuery = "INSERT INTO " + sinkTableName + + " SELECT `timestamp`, symbol, price, volumes FROM stockPriceTable"; + LOG.info("Executing insert statement: {}", insertQuery); + TableResult insertResult = tableEnv.executeSql(insertQuery); + + // Keep the job running to continuously insert data + LOG.info("Application started successfully. Inserting data into Iceberg table: {}", sinkTableName); + + } + + private static void setupIcebergProperties(Properties icebergProperties) { + s3BucketPrefix = icebergProperties.getProperty("bucket.prefix"); + glueDatabase = icebergProperties.getProperty("catalog.db", "default"); + glueTable = icebergProperties.getProperty("catalog.table", "prices_iceberg"); + + Preconditions.checkNotNull(s3BucketPrefix, "You must supply an s3 bucket prefix for the warehouse."); + Preconditions.checkNotNull(glueDatabase, "You must supply a database name"); + Preconditions.checkNotNull(glueTable, "You must supply a table name"); + + // Validate S3 URI format + validateURI(s3BucketPrefix); + + LOG.info("Iceberg configuration: bucket={}, database={}, table={}", + s3BucketPrefix, glueDatabase, glueTable); + } + + private static DataGeneratorSource createDataGenerator(Properties dataGeneratorProperties) { + double recordsPerSecond = Double.parseDouble(dataGeneratorProperties.getProperty("records.per.sec", "10.0")); + Preconditions.checkArgument(recordsPerSecond > 0, "Generator records per sec must be > 0"); + + LOG.info("Data generator: {} record/sec", recordsPerSecond); + return new DataGeneratorSource(new StockPriceGeneratorFunction(), + Long.MAX_VALUE, + RateLimiterStrategy.perSecond(recordsPerSecond), + TypeInformation.of(StockPrice.class)); + } + + /** + * Defines a config object with Glue specific catalog and io implementations + * Then, uses that to create the Flink catalog + */ + private static Catalog createGlueCatalog(StreamTableEnvironment tableEnv) { + + Map catalogProperties = new HashMap<>(); + catalogProperties.put("type", "iceberg"); + catalogProperties.put("io-impl", "org.apache.iceberg.aws.s3.S3FileIO"); + catalogProperties.put("warehouse", s3BucketPrefix); + catalogProperties.put("impl", "org.apache.iceberg.aws.glue.GlueCatalog"); + //Loading Glue Data Catalog + CatalogLoader glueCatalogLoader = CatalogLoader.custom( + CATALOG_NAME, + catalogProperties, + new org.apache.hadoop.conf.Configuration(), + "org.apache.iceberg.aws.glue.GlueCatalog"); + + + FlinkCatalog flinkCatalog = new FlinkCatalog(CATALOG_NAME,glueDatabase, Namespace.empty(),glueCatalogLoader,true,1000); + return flinkCatalog; + } + + private static boolean isLocal(StreamExecutionEnvironment env) { + return env instanceof LocalStreamEnvironment; + } + + /** + * Load application properties from Amazon Managed Service for Apache Flink runtime + * or from a local resource, when the environment is local + */ + private static Map loadApplicationProperties(StreamExecutionEnvironment env) throws IOException { + if (isLocal(env)) { + LOG.info("Loading application properties from '{}'", LOCAL_APPLICATION_PROPERTIES_RESOURCE); + return KinesisAnalyticsRuntime.getApplicationProperties( + Objects.requireNonNull(GlueTableSQLJSONExample.class.getClassLoader() + .getResource(LOCAL_APPLICATION_PROPERTIES_RESOURCE)).getPath()); + } else { + LOG.info("Loading application properties from Amazon Managed Service for Apache Flink"); + return KinesisAnalyticsRuntime.getApplicationProperties(); + } + } + + public static void validateURI(String uri) { + String s3UriPattern = "^s3://([a-z0-9.-]+)(/[a-z0-9-_/]+/?)$"; + Preconditions.checkArgument(uri != null && uri.matches(s3UriPattern), + "Invalid S3 URI format: %s. URI must match pattern: s3://bucket-name/path/", uri); + } +} diff --git a/java/Iceberg/IcebergSQLJSONGlue/src/main/java/StockPrice.java b/java/Iceberg/IcebergSQLJSONGlue/src/main/java/StockPrice.java new file mode 100644 index 0000000..c386c2a --- /dev/null +++ b/java/Iceberg/IcebergSQLJSONGlue/src/main/java/StockPrice.java @@ -0,0 +1,60 @@ +import java.time.Instant; + +public class StockPrice { + private String timestamp; + private String symbol; + private Float price; + private Integer volumes; + + public StockPrice() { + } + + public StockPrice(String timestamp, String symbol, Float price, Integer volumes) { + this.timestamp = timestamp; + this.symbol = symbol; + this.price = price; + this.volumes = volumes; + } + + public String getTimestamp() { + return timestamp; + } + + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + + public String getSymbol() { + return symbol; + } + + public void setSymbol(String symbol) { + this.symbol = symbol; + } + + public Float getPrice() { + return price; + } + + public void setPrice(Float price) { + this.price = price; + } + + public Integer getVolumes() { + return volumes; + } + + public void setVolumes(Integer volumes) { + this.volumes = volumes; + } + + @Override + public String toString() { + return "StockPrice{" + + "timestamp='" + timestamp + '\'' + + ", symbol='" + symbol + '\'' + + ", price=" + price + + ", volumes=" + volumes + + '}'; + } +} diff --git a/java/Iceberg/IcebergSQLJSONGlue/src/main/java/StockPriceGeneratorFunction.java b/java/Iceberg/IcebergSQLJSONGlue/src/main/java/StockPriceGeneratorFunction.java new file mode 100644 index 0000000..38e236b --- /dev/null +++ b/java/Iceberg/IcebergSQLJSONGlue/src/main/java/StockPriceGeneratorFunction.java @@ -0,0 +1,24 @@ +import org.apache.commons.lang3.RandomUtils; +import org.apache.flink.connector.datagen.source.GeneratorFunction; +import java.time.Instant; + +/** + * Function used by DataGen source to generate random records as StockPrice POJOs. + * + * The generator mimics the behavior of AvroGenericStockTradeGeneratorFunction + * from the IcebergDataStreamSink example. + */ +public class StockPriceGeneratorFunction implements GeneratorFunction { + + private static final String[] SYMBOLS = {"AAPL", "AMZN", "MSFT", "INTC", "TBV"}; + + @Override + public StockPrice map(Long sequence) throws Exception { + String symbol = SYMBOLS[RandomUtils.nextInt(0, SYMBOLS.length)]; + float price = RandomUtils.nextFloat(0, 10); + int volumes = RandomUtils.nextInt(0, 1000000); + String timestamp = Instant.now().toString(); + + return new StockPrice(timestamp, symbol, price, volumes); + } +} diff --git a/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/flink-application-properties-dev.json b/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/flink-application-properties-dev.json new file mode 100644 index 0000000..67e0aa1 --- /dev/null +++ b/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/flink-application-properties-dev.json @@ -0,0 +1,16 @@ +[ + { + "PropertyGroupId": "DataGen", + "PropertyMap": { + "records.per.sec": 10.0 + } + }, + { + "PropertyGroupId": "Iceberg", + "PropertyMap": { + "bucket.prefix": "s3://iceberg-performance-026090544291/iceberg", + "catalog.db": "iceberg", + "catalog.table": "prices_iceberg" + } + } +] \ No newline at end of file diff --git a/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/log4j2.properties b/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/log4j2.properties new file mode 100644 index 0000000..a6cccce --- /dev/null +++ b/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/log4j2.properties @@ -0,0 +1,13 @@ +# Log4j2 configuration +status = warn +name = PropertiesConfig + +# Console appender configuration +appender.console.type = Console +appender.console.name = ConsoleAppender +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{HH:mm:ss,SSS} %-5p %-60c %x - %m%n + +# Root logger configuration +rootLogger.level = INFO +rootLogger.appenderRef.console.ref = ConsoleAppender \ No newline at end of file diff --git a/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/price.avsc b/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/price.avsc new file mode 100644 index 0000000..6303e0d --- /dev/null +++ b/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/price.avsc @@ -0,0 +1,23 @@ +{ + "type": "record", + "name": "Price", + "namespace": "com.amazonaws.services.msf.avro", + "fields": [ + { + "name": "timestamp", + "type": "string" + }, + { + "name": "symbol", + "type": "string" + }, + { + "name": "price", + "type": "float" + }, + { + "name": "volumes", + "type": "int" + } + ] +} \ No newline at end of file From fd81e7f359bb20a4c9fde3bc83368fe27c5e9921 Mon Sep 17 00:00:00 2001 From: Francisco Date: Tue, 27 May 2025 15:35:39 +0200 Subject: [PATCH 2/7] updating properties --- .../src/main/resources/flink-application-properties-dev.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/flink-application-properties-dev.json b/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/flink-application-properties-dev.json index 67e0aa1..79ec4f1 100644 --- a/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/flink-application-properties-dev.json +++ b/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/flink-application-properties-dev.json @@ -8,7 +8,7 @@ { "PropertyGroupId": "Iceberg", "PropertyMap": { - "bucket.prefix": "s3://iceberg-performance-026090544291/iceberg", + "bucket.prefix": "s3:///iceberg", "catalog.db": "iceberg", "catalog.table": "prices_iceberg" } From 68c7ce6f2051072fb62f8dc609d9baed50998c85 Mon Sep 17 00:00:00 2001 From: Francisco Date: Thu, 10 Jul 2025 16:22:20 +0200 Subject: [PATCH 3/7] Adding Hadoop Utils --- java/Iceberg/IcebergSQLJSONGlue/README.md | 25 ++- java/Iceberg/IcebergSQLJSONGlue/pom.xml | 129 ++++------- .../msf}/GlueTableSQLJSONExample.java | 206 ++++++++---------- .../services/msf/pojo}/StockPrice.java | 4 +- .../source}/StockPriceGeneratorFunction.java | 5 +- .../flink/runtime/util/HadoopUtils.java | 120 ++++++++++ .../flink-application-properties-dev.json | 4 +- .../src/main/resources/price.avsc | 23 -- 8 files changed, 284 insertions(+), 232 deletions(-) rename java/Iceberg/IcebergSQLJSONGlue/src/main/java/{ => com/amazonaws/services/msf}/GlueTableSQLJSONExample.java (70%) rename java/Iceberg/IcebergSQLJSONGlue/src/main/java/{ => com/amazonaws/services/msf/pojo}/StockPrice.java (92%) rename java/Iceberg/IcebergSQLJSONGlue/src/main/java/{ => com/amazonaws/services/msf/source}/StockPriceGeneratorFunction.java (86%) create mode 100644 java/Iceberg/IcebergSQLJSONGlue/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java delete mode 100644 java/Iceberg/IcebergSQLJSONGlue/src/main/resources/price.avsc diff --git a/java/Iceberg/IcebergSQLJSONGlue/README.md b/java/Iceberg/IcebergSQLJSONGlue/README.md index ca05aa6..045e9bc 100644 --- a/java/Iceberg/IcebergSQLJSONGlue/README.md +++ b/java/Iceberg/IcebergSQLJSONGlue/README.md @@ -1,8 +1,8 @@ # Flink Iceberg Sink using SQL API -* Flink version: 1.20.0 +* Flink version: 1.20.1 * Flink API: SQL API -* Iceberg 1.8.1 +* Iceberg 1.9.1 * Language: Java (11) * Flink connectors: [DataGen](https://nightlies.apache.org/flink/flink-docs-release-1.20/docs/connectors/datastream/datagen/) and [Iceberg](https://iceberg.apache.org/docs/latest/flink/) @@ -57,7 +57,21 @@ At the moment there are current limitations concerning Flink Iceberg integration * Doesn't support Iceberg Table with hidden partitioning * Doesn't support adding columns, removing columns, renaming columns or changing columns. -### Schema and schema evolution +### Hadoop Library Availability Challenge + +When integrating Flink with Iceberg, there's a common configuration challenge that affects most Flink deployments: + +#### The Challenge +* When using Flink SQL's `CREATE CATALOG` statements, Hadoop libraries must be available on the system classpath +* However, standard Flink distributions use shaded dependencies that can create class loading conflicts with Hadoop's expectations +* This is particularly relevant for TaskManagers (which is the case for most generic Flink clusters, except EMR) + +#### Solution Approaches +1. **For SQL Applications (This Example)** + * If Hadoop is not pre-installed in the cluster, you'll need to create a custom HadoopUtils class and properly configure Maven dependencies + * This example includes the necessary configuration to handle these dependencies + +### Schema The application uses a predefined schema for the stock price data with the following fields: * `timestamp`: STRING - ISO timestamp of the record @@ -65,11 +79,6 @@ The application uses a predefined schema for the stock price data with the follo * `price`: FLOAT - Stock price (0-10 range) * `volumes`: INT - Trade volumes (0-1000000 range) -This schema matches the AVRO schema used in the DataStream API example for consistency. -The equivalent AVRO schema definition is available in [price.avsc](./src/main/resources/price.avsc) for reference. - -Schema changes would require updating both the POJO class and the SQL table definition. -Unlike the DataStream approach with AVRO, this SQL approach requires explicit schema management. ### Running locally, in IntelliJ diff --git a/java/Iceberg/IcebergSQLJSONGlue/pom.xml b/java/Iceberg/IcebergSQLJSONGlue/pom.xml index 1df90e1..627ec0a 100644 --- a/java/Iceberg/IcebergSQLJSONGlue/pom.xml +++ b/java/Iceberg/IcebergSQLJSONGlue/pom.xml @@ -14,116 +14,71 @@ 11 ${target.java.version} ${target.java.version} - - 1.20.0 - 1.11.3 + 1.20 + 1.20.1 2.12 - 3.4.0 - 1.8.1 + 1.9.1 1.2.0 2.23.1 5.8.1 + + + + + com.amazonaws + aws-java-sdk-bom + + 1.12.782 + pom + import + + + + - - - org.apache.flink - flink-runtime-web - ${flink.version} - provided - org.apache.flink flink-streaming-java ${flink.version} provided - - org.apache.flink - flink-table-runtime - ${flink.version} - provided - org.apache.flink flink-table-api-java-bridge ${flink.version} + org.apache.flink - flink-table-common - ${flink.version} - - - org.apache.flink - flink-metrics-dropwizard + flink-table-planner_${scala.version} ${flink.version} + provided org.apache.flink - flink-avro + flink-clients ${flink.version} - org.apache.flink - flink-table-planner_${scala.version} + flink-connector-datagen ${flink.version} provided - com.amazonaws aws-kinesisanalytics-runtime ${kda.runtime.version} provided - - org.apache.flink - flink-connector-files - ${flink.version} - provided - - - - org.apache.hadoop - hadoop-client - ${hadoop.version} - - - org.apache.avro - avro - - - org.slf4j - slf4j-reload4j - - - - - org.apache.hadoop - hadoop-common - ${hadoop.version} - - - org.apache.hadoop - hadoop-mapreduce-client-core - ${hadoop.version} - - - - org.apache.iceberg - iceberg-core - ${iceberg.version} - - org.apache.iceberg - iceberg-flink + iceberg-flink-runtime-${flink.major.version} ${iceberg.version} @@ -131,25 +86,15 @@ iceberg-aws-bundle ${iceberg.version} + + - org.apache.iceberg - iceberg-aws - ${iceberg.version} - - - org.apache.iceberg - iceberg-flink-1.20 - ${iceberg.version} - - - - org.junit.jupiter - junit-jupiter - ${junit5.version} - test + org.apache.flink + flink-s3-fs-hadoop + ${flink.version} - + org.apache.logging.log4j log4j-slf4j-impl @@ -184,7 +129,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.1 + 3.6.0 package @@ -213,9 +158,21 @@ - GlueTableSQLJSONExample + com.amazonaws.services.msf.GlueTableSQLJSONExample + + + + org.apache.hadoop.conf + shaded.org.apache.hadoop.conf + + + org.apache.flink.runtime.util.HadoopUtils + shadow.org.apache.flink.runtime.util.HadoopUtils + + diff --git a/java/Iceberg/IcebergSQLJSONGlue/src/main/java/GlueTableSQLJSONExample.java b/java/Iceberg/IcebergSQLJSONGlue/src/main/java/com/amazonaws/services/msf/GlueTableSQLJSONExample.java similarity index 70% rename from java/Iceberg/IcebergSQLJSONGlue/src/main/java/GlueTableSQLJSONExample.java rename to java/Iceberg/IcebergSQLJSONGlue/src/main/java/com/amazonaws/services/msf/GlueTableSQLJSONExample.java index 15b0c42..dfe64f9 100644 --- a/java/Iceberg/IcebergSQLJSONGlue/src/main/java/GlueTableSQLJSONExample.java +++ b/java/Iceberg/IcebergSQLJSONGlue/src/main/java/com/amazonaws/services/msf/GlueTableSQLJSONExample.java @@ -1,4 +1,4 @@ -/* +package com.amazonaws.services.msf;/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 * @@ -17,10 +17,11 @@ */ import com.amazonaws.services.kinesisanalytics.runtime.KinesisAnalyticsRuntime; +import com.amazonaws.services.msf.pojo.StockPrice; +import com.amazonaws.services.msf.source.StockPriceGeneratorFunction; import org.apache.flink.api.common.eventtime.WatermarkStrategy; import org.apache.flink.api.common.typeinfo.TypeInformation; import org.apache.flink.api.connector.source.util.ratelimit.RateLimiterStrategy; -import org.apache.flink.configuration.Configuration; import org.apache.flink.connector.datagen.source.DataGeneratorSource; import org.apache.flink.streaming.api.datastream.DataStream; import org.apache.flink.streaming.api.environment.LocalStreamEnvironment; @@ -28,18 +29,11 @@ import org.apache.flink.table.api.Table; import org.apache.flink.table.api.TableResult; import org.apache.flink.table.api.bridge.java.StreamTableEnvironment; -import org.apache.flink.table.catalog.Catalog; -import org.apache.flink.table.catalog.CatalogDatabaseImpl; -import org.apache.flink.table.catalog.CatalogDescriptor; import org.apache.flink.util.Preconditions; -import org.apache.iceberg.catalog.Namespace; -import org.apache.iceberg.flink.CatalogLoader; -import org.apache.iceberg.flink.FlinkCatalog; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Properties; @@ -50,71 +44,60 @@ public class GlueTableSQLJSONExample { private static final String LOCAL_APPLICATION_PROPERTIES_RESOURCE = "flink-application-properties-dev.json"; private static final Logger LOG = LoggerFactory.getLogger(GlueTableSQLJSONExample.class); - // Configuration properties - private static String s3BucketPrefix; - private static String glueDatabase; - private static String glueTable; - - public static void main(String[] args) throws Exception { - // 1. Initialize environments - using standard environment instead of WebUI for production consistency - final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); - final StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env); + private static boolean isLocal(StreamExecutionEnvironment env) { + return env instanceof LocalStreamEnvironment; + } - // 2. Load properties and configure environment - Map applicationProperties = loadApplicationProperties(env); - Properties icebergProperties = applicationProperties.get("Iceberg"); + private static void validateURI(String uri) { + String s3UriPattern = "^s3://([a-z0-9.-]+)(/[a-z0-9-_/]+/?)$"; + Preconditions.checkArgument(uri != null && uri.matches(s3UriPattern), + "Invalid S3 URI format: %s. URI must match pattern: s3://bucket-name/path/", uri); + } - // Configure local development settings if needed + private static Map loadApplicationProperties(StreamExecutionEnvironment env) throws IOException { if (isLocal(env)) { - env.enableCheckpointing(30000); - env.setParallelism(2); + LOG.info("Loading application properties from '{}'", LOCAL_APPLICATION_PROPERTIES_RESOURCE); + return KinesisAnalyticsRuntime.getApplicationProperties( + Objects.requireNonNull(GlueTableSQLJSONExample.class.getClassLoader() + .getResource(LOCAL_APPLICATION_PROPERTIES_RESOURCE)).getPath()); + } else { + LOG.info("Loading application properties from Amazon Managed Service for Apache Flink"); + return KinesisAnalyticsRuntime.getApplicationProperties(); } + } - // 3. Setup configuration properties with validation - setupIcebergProperties(icebergProperties); - Catalog glueCatalog = createGlueCatalog(tableEnv); - tableEnv.registerCatalog(CATALOG_NAME,glueCatalog); - - // 4. Create data generator source - Properties dataGenProperties = applicationProperties.get("DataGen"); - DataStream stockPriceDataStream = env.fromSource( - createDataGenerator(dataGenProperties), - WatermarkStrategy.noWatermarks(), - "DataGen"); - + private static DataGeneratorSource createDataGenerator(Properties dataGeneratorProperties) { + double recordsPerSecond = Double.parseDouble(dataGeneratorProperties.getProperty("records.per.sec", "10.0")); + Preconditions.checkArgument(recordsPerSecond > 0, "Generator records per sec must be > 0"); - // 5. Convert DataStream to Table and create view - Table stockPriceTable = tableEnv.fromDataStream(stockPriceDataStream); - tableEnv.createTemporaryView("stockPriceTable", stockPriceTable); + LOG.info("Data generator: {} record/sec", recordsPerSecond); + return new DataGeneratorSource(new StockPriceGeneratorFunction(), + Long.MAX_VALUE, + RateLimiterStrategy.perSecond(recordsPerSecond), + TypeInformation.of(StockPrice.class)); + } - String sinkTableName = CATALOG_NAME + "." + glueDatabase + "." + glueTable; + private static String createCatalogStatement(String s3BucketPrefix) { + return "CREATE CATALOG " + CATALOG_NAME + " WITH (" + + "'type' = 'iceberg', " + + "'catalog-impl' = 'org.apache.iceberg.aws.glue.GlueCatalog'," + + "'io-impl' = 'org.apache.iceberg.aws.s3.S3FileIO'," + + "'warehouse' = '" + s3BucketPrefix + "')"; + } - // Define and create table with schema matching AVRO schema from DataStream example - String createTableStatement = "CREATE TABLE IF NOT EXISTS " + sinkTableName + " (" + + private static String createTableStatement(String sinkTableName) { + return "CREATE TABLE IF NOT EXISTS " + sinkTableName + " (" + "`timestamp` STRING, " + "symbol STRING," + "price FLOAT," + "volumes INT" + ") PARTITIONED BY (symbol) "; - - LOG.info("Creating table with statement: {}", createTableStatement); - tableEnv.executeSql(createTableStatement); - - // 7. Execute SQL operations - Insert data from stock price stream - String insertQuery = "INSERT INTO " + sinkTableName + - " SELECT `timestamp`, symbol, price, volumes FROM stockPriceTable"; - LOG.info("Executing insert statement: {}", insertQuery); - TableResult insertResult = tableEnv.executeSql(insertQuery); - - // Keep the job running to continuously insert data - LOG.info("Application started successfully. Inserting data into Iceberg table: {}", sinkTableName); - } - private static void setupIcebergProperties(Properties icebergProperties) { - s3BucketPrefix = icebergProperties.getProperty("bucket.prefix"); - glueDatabase = icebergProperties.getProperty("catalog.db", "default"); - glueTable = icebergProperties.getProperty("catalog.table", "prices_iceberg"); + private static IcebergConfig setupIcebergProperties(Properties icebergProperties) { + String s3BucketPrefix = icebergProperties.getProperty("bucket.prefix"); + String glueDatabase = icebergProperties.getProperty("catalog.db", "default"); + String glueTable = icebergProperties.getProperty("catalog.table", "prices_iceberg"); Preconditions.checkNotNull(s3BucketPrefix, "You must supply an s3 bucket prefix for the warehouse."); Preconditions.checkNotNull(glueDatabase, "You must supply a database name"); @@ -125,65 +108,68 @@ private static void setupIcebergProperties(Properties icebergProperties) { LOG.info("Iceberg configuration: bucket={}, database={}, table={}", s3BucketPrefix, glueDatabase, glueTable); - } - private static DataGeneratorSource createDataGenerator(Properties dataGeneratorProperties) { - double recordsPerSecond = Double.parseDouble(dataGeneratorProperties.getProperty("records.per.sec", "10.0")); - Preconditions.checkArgument(recordsPerSecond > 0, "Generator records per sec must be > 0"); - - LOG.info("Data generator: {} record/sec", recordsPerSecond); - return new DataGeneratorSource(new StockPriceGeneratorFunction(), - Long.MAX_VALUE, - RateLimiterStrategy.perSecond(recordsPerSecond), - TypeInformation.of(StockPrice.class)); + return new IcebergConfig(s3BucketPrefix, glueDatabase, glueTable); } - /** - * Defines a config object with Glue specific catalog and io implementations - * Then, uses that to create the Flink catalog - */ - private static Catalog createGlueCatalog(StreamTableEnvironment tableEnv) { - - Map catalogProperties = new HashMap<>(); - catalogProperties.put("type", "iceberg"); - catalogProperties.put("io-impl", "org.apache.iceberg.aws.s3.S3FileIO"); - catalogProperties.put("warehouse", s3BucketPrefix); - catalogProperties.put("impl", "org.apache.iceberg.aws.glue.GlueCatalog"); - //Loading Glue Data Catalog - CatalogLoader glueCatalogLoader = CatalogLoader.custom( - CATALOG_NAME, - catalogProperties, - new org.apache.hadoop.conf.Configuration(), - "org.apache.iceberg.aws.glue.GlueCatalog"); - - - FlinkCatalog flinkCatalog = new FlinkCatalog(CATALOG_NAME,glueDatabase, Namespace.empty(),glueCatalogLoader,true,1000); - return flinkCatalog; - } + private static class IcebergConfig { + final String s3BucketPrefix; + final String glueDatabase; + final String glueTable; - private static boolean isLocal(StreamExecutionEnvironment env) { - return env instanceof LocalStreamEnvironment; + IcebergConfig(String s3BucketPrefix, String glueDatabase, String glueTable) { + this.s3BucketPrefix = s3BucketPrefix; + this.glueDatabase = glueDatabase; + this.glueTable = glueTable; + } } - /** - * Load application properties from Amazon Managed Service for Apache Flink runtime - * or from a local resource, when the environment is local - */ - private static Map loadApplicationProperties(StreamExecutionEnvironment env) throws IOException { + public static void main(String[] args) throws Exception { + // 1. Initialize environments - using standard environment instead of WebUI for production consistency + final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); + final StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env); + + // 2 If running local, we need to enable Checkpoints. Iceberg commits data with every checkpoint if (isLocal(env)) { - LOG.info("Loading application properties from '{}'", LOCAL_APPLICATION_PROPERTIES_RESOURCE); - return KinesisAnalyticsRuntime.getApplicationProperties( - Objects.requireNonNull(GlueTableSQLJSONExample.class.getClassLoader() - .getResource(LOCAL_APPLICATION_PROPERTIES_RESOURCE)).getPath()); - } else { - LOG.info("Loading application properties from Amazon Managed Service for Apache Flink"); - return KinesisAnalyticsRuntime.getApplicationProperties(); + env.enableCheckpointing(60000); } - } - public static void validateURI(String uri) { - String s3UriPattern = "^s3://([a-z0-9.-]+)(/[a-z0-9-_/]+/?)$"; - Preconditions.checkArgument(uri != null && uri.matches(s3UriPattern), - "Invalid S3 URI format: %s. URI must match pattern: s3://bucket-name/path/", uri); + // 3. Setup configuration properties with validation + Map applicationProperties = loadApplicationProperties(env); + Properties icebergProperties = applicationProperties.get("Iceberg"); + IcebergConfig config = setupIcebergProperties(icebergProperties); + + // 4. Create data generator source + Properties dataGenProperties = applicationProperties.get("DataGen"); + DataStream stockPriceDataStream = env.fromSource( + createDataGenerator(dataGenProperties), + WatermarkStrategy.noWatermarks(), + "DataGen"); + + // 5. Convert DataStream to Table and create view + Table stockPriceTable = tableEnv.fromDataStream(stockPriceDataStream); + tableEnv.createTemporaryView("stockPriceTable", stockPriceTable); + + String sinkTableName = CATALOG_NAME + "." + config.glueDatabase + "." + config.glueTable; + + // Create catalog and configure it + tableEnv.executeSql(createCatalogStatement(config.s3BucketPrefix)); + tableEnv.executeSql("USE CATALOG " + CATALOG_NAME); + tableEnv.executeSql("CREATE DATABASE IF NOT EXISTS " + config.glueDatabase); + tableEnv.executeSql("USE " + config.glueDatabase); + + // Create table + String createTableStatement = createTableStatement(sinkTableName); + LOG.info("Creating table with statement: {}", createTableStatement); + tableEnv.executeSql(createTableStatement); + + // 7. Execute SQL operations - Insert data from stock price stream + String insertQuery = "INSERT INTO " + sinkTableName + + " SELECT `timestamp`, symbol, price, volumes FROM default_catalog.default_database.stockPriceTable"; + LOG.info("Executing insert statement: {}", insertQuery); + TableResult insertResult = tableEnv.executeSql(insertQuery); + + // Keep the job running to continuously insert data + LOG.info("Application started successfully. Inserting data into Iceberg table: {}", sinkTableName); } } diff --git a/java/Iceberg/IcebergSQLJSONGlue/src/main/java/StockPrice.java b/java/Iceberg/IcebergSQLJSONGlue/src/main/java/com/amazonaws/services/msf/pojo/StockPrice.java similarity index 92% rename from java/Iceberg/IcebergSQLJSONGlue/src/main/java/StockPrice.java rename to java/Iceberg/IcebergSQLJSONGlue/src/main/java/com/amazonaws/services/msf/pojo/StockPrice.java index c386c2a..3b4f923 100644 --- a/java/Iceberg/IcebergSQLJSONGlue/src/main/java/StockPrice.java +++ b/java/Iceberg/IcebergSQLJSONGlue/src/main/java/com/amazonaws/services/msf/pojo/StockPrice.java @@ -1,4 +1,4 @@ -import java.time.Instant; +package com.amazonaws.services.msf.pojo; public class StockPrice { private String timestamp; @@ -50,7 +50,7 @@ public void setVolumes(Integer volumes) { @Override public String toString() { - return "StockPrice{" + + return "com.amazonaws.services.msf.pojo.StockPrice{" + "timestamp='" + timestamp + '\'' + ", symbol='" + symbol + '\'' + ", price=" + price + diff --git a/java/Iceberg/IcebergSQLJSONGlue/src/main/java/StockPriceGeneratorFunction.java b/java/Iceberg/IcebergSQLJSONGlue/src/main/java/com/amazonaws/services/msf/source/StockPriceGeneratorFunction.java similarity index 86% rename from java/Iceberg/IcebergSQLJSONGlue/src/main/java/StockPriceGeneratorFunction.java rename to java/Iceberg/IcebergSQLJSONGlue/src/main/java/com/amazonaws/services/msf/source/StockPriceGeneratorFunction.java index 38e236b..1cf5554 100644 --- a/java/Iceberg/IcebergSQLJSONGlue/src/main/java/StockPriceGeneratorFunction.java +++ b/java/Iceberg/IcebergSQLJSONGlue/src/main/java/com/amazonaws/services/msf/source/StockPriceGeneratorFunction.java @@ -1,9 +1,12 @@ +package com.amazonaws.services.msf.source; + +import com.amazonaws.services.msf.pojo.StockPrice; import org.apache.commons.lang3.RandomUtils; import org.apache.flink.connector.datagen.source.GeneratorFunction; import java.time.Instant; /** - * Function used by DataGen source to generate random records as StockPrice POJOs. + * Function used by DataGen source to generate random records as com.amazonaws.services.msf.pojo.StockPrice POJOs. * * The generator mimics the behavior of AvroGenericStockTradeGeneratorFunction * from the IcebergDataStreamSink example. diff --git a/java/Iceberg/IcebergSQLJSONGlue/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java b/java/Iceberg/IcebergSQLJSONGlue/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java new file mode 100644 index 0000000..b177d06 --- /dev/null +++ b/java/Iceberg/IcebergSQLJSONGlue/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java @@ -0,0 +1,120 @@ +package org.apache.flink.runtime.util; + +import org.apache.flink.api.java.tuple.Tuple2; +import org.apache.flink.util.FlinkRuntimeException; +import org.apache.flink.util.Preconditions; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.io.Text; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.hadoop.security.token.Token; +import org.apache.hadoop.security.token.TokenIdentifier; +import org.apache.hadoop.util.VersionInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; + + +/** + * This class is a copy of org.apache.flink.runtime.util.HadoopUtils with the getHadoopConfiguration() method replaced to + * return an org.apache.hadoop.conf.Configuration instead of org.apache.hadoop.hdfs.HdfsConfiguration. + * + * This class is then shaded, along with org.apache.hadoop.conf.*, to avoid conflicts with the same classes provided by + * org.apache.flink:flink-s3-fs-hadoop, which is normally installed as plugin in Flink when S3. + * + * Other methods are copied from the original class. + */ +public class HadoopUtils { + private static final Logger LOG = LoggerFactory.getLogger(HadoopUtils.class); + + static final Text HDFS_DELEGATION_TOKEN_KIND = new Text("HDFS_DELEGATION_TOKEN"); + + /** + * This method has been re-implemented to always return a org.apache.hadoop.conf.Configuration + */ + public static Configuration getHadoopConfiguration( + org.apache.flink.configuration.Configuration flinkConfiguration) { + return new Configuration(false); + } + + public static boolean isKerberosSecurityEnabled(UserGroupInformation ugi) { + return UserGroupInformation.isSecurityEnabled() + && ugi.getAuthenticationMethod() + == UserGroupInformation.AuthenticationMethod.KERBEROS; + } + + + public static boolean areKerberosCredentialsValid( + UserGroupInformation ugi, boolean useTicketCache) { + Preconditions.checkState(isKerberosSecurityEnabled(ugi)); + + // note: UGI::hasKerberosCredentials inaccurately reports false + // for logins based on a keytab (fixed in Hadoop 2.6.1, see HADOOP-10786), + // so we check only in ticket cache scenario. + if (useTicketCache && !ugi.hasKerberosCredentials()) { + if (hasHDFSDelegationToken(ugi)) { + LOG.warn( + "Hadoop security is enabled but current login user does not have Kerberos credentials, " + + "use delegation token instead. Flink application will terminate after token expires."); + return true; + } else { + LOG.error( + "Hadoop security is enabled, but current login user has neither Kerberos credentials " + + "nor delegation tokens!"); + return false; + } + } + + return true; + } + + /** + * Indicates whether the user has an HDFS delegation token. + */ + public static boolean hasHDFSDelegationToken(UserGroupInformation ugi) { + Collection> usrTok = ugi.getTokens(); + for (Token token : usrTok) { + if (token.getKind().equals(HDFS_DELEGATION_TOKEN_KIND)) { + return true; + } + } + return false; + } + + /** + * Checks if the Hadoop dependency is at least the given version. + */ + public static boolean isMinHadoopVersion(int major, int minor) throws FlinkRuntimeException { + final Tuple2 hadoopVersion = getMajorMinorBundledHadoopVersion(); + int maj = hadoopVersion.f0; + int min = hadoopVersion.f1; + + return maj > major || (maj == major && min >= minor); + } + + /** + * Checks if the Hadoop dependency is at most the given version. + */ + public static boolean isMaxHadoopVersion(int major, int minor) throws FlinkRuntimeException { + final Tuple2 hadoopVersion = getMajorMinorBundledHadoopVersion(); + int maj = hadoopVersion.f0; + int min = hadoopVersion.f1; + + return maj < major || (maj == major && min < minor); + } + + private static Tuple2 getMajorMinorBundledHadoopVersion() { + String versionString = VersionInfo.getVersion(); + String[] versionParts = versionString.split("\\."); + + if (versionParts.length < 2) { + throw new FlinkRuntimeException( + "Cannot determine version of Hadoop, unexpected version string: " + + versionString); + } + + int maj = Integer.parseInt(versionParts[0]); + int min = Integer.parseInt(versionParts[1]); + return Tuple2.of(maj, min); + } +} \ No newline at end of file diff --git a/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/flink-application-properties-dev.json b/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/flink-application-properties-dev.json index 79ec4f1..14f4d2a 100644 --- a/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/flink-application-properties-dev.json +++ b/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/flink-application-properties-dev.json @@ -8,9 +8,9 @@ { "PropertyGroupId": "Iceberg", "PropertyMap": { - "bucket.prefix": "s3:///iceberg", + "bucket.prefix": "s3://iceberg-performance-026090544291/iceberg", "catalog.db": "iceberg", - "catalog.table": "prices_iceberg" + "catalog.table": "prices_iceberg_test_2" } } ] \ No newline at end of file diff --git a/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/price.avsc b/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/price.avsc deleted file mode 100644 index 6303e0d..0000000 --- a/java/Iceberg/IcebergSQLJSONGlue/src/main/resources/price.avsc +++ /dev/null @@ -1,23 +0,0 @@ -{ - "type": "record", - "name": "Price", - "namespace": "com.amazonaws.services.msf.avro", - "fields": [ - { - "name": "timestamp", - "type": "string" - }, - { - "name": "symbol", - "type": "string" - }, - { - "name": "price", - "type": "float" - }, - { - "name": "volumes", - "type": "int" - } - ] -} \ No newline at end of file From f358b768be7523bd361b29b094e6eb15f68b8bdb Mon Sep 17 00:00:00 2001 From: Lorenzo Nicora Date: Fri, 11 Jul 2025 09:32:19 +0100 Subject: [PATCH 4/7] Update README. Rename example --- .gitignore | 5 +- .../pom.xml | 18 +++--- .../services/msf/IcebergSQLSinkJob.java} | 13 +++-- .../README.md | 56 +++++++++++++------ .../services/msf/pojo/StockPrice.java | 0 .../source/StockPriceGeneratorFunction.java | 0 .../flink/runtime/util/HadoopUtils.java | 0 .../flink-application-properties-dev.json | 4 +- .../src/main/resources/log4j2.properties | 0 java/Iceberg/S3TableSink/README.md | 6 +- .../flink-application-properties-dev.json | 2 +- java/pom.xml | 1 + 12 files changed, 67 insertions(+), 38 deletions(-) rename java/Iceberg/{IcebergSQLJSONGlue => IcebergSQLSink}/pom.xml (94%) rename java/Iceberg/{IcebergSQLJSONGlue/src/main/java/com/amazonaws/services/msf/GlueTableSQLJSONExample.java => IcebergSQLSink/src/main/java/com/amazonaws/services/msf/IcebergSQLSinkJob.java} (95%) rename java/Iceberg/{IcebergSQLJSONGlue => S3TableSQLSink}/README.md (68%) rename java/Iceberg/{IcebergSQLJSONGlue => S3TableSQLSink}/src/main/java/com/amazonaws/services/msf/pojo/StockPrice.java (100%) rename java/Iceberg/{IcebergSQLJSONGlue => S3TableSQLSink}/src/main/java/com/amazonaws/services/msf/source/StockPriceGeneratorFunction.java (100%) rename java/Iceberg/{IcebergSQLJSONGlue => S3TableSQLSink}/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java (100%) rename java/Iceberg/{IcebergSQLJSONGlue => S3TableSQLSink}/src/main/resources/flink-application-properties-dev.json (63%) rename java/Iceberg/{IcebergSQLJSONGlue => S3TableSQLSink}/src/main/resources/log4j2.properties (100%) diff --git a/.gitignore b/.gitignore index 94085cc..9883146 100644 --- a/.gitignore +++ b/.gitignore @@ -13,8 +13,9 @@ venv/ .java-version /pyflink/ __pycache__/ - +/.vscode/ +/.docker/ /.run/ clean.sh -smudge.sh \ No newline at end of file +smudge.sh diff --git a/java/Iceberg/IcebergSQLJSONGlue/pom.xml b/java/Iceberg/IcebergSQLSink/pom.xml similarity index 94% rename from java/Iceberg/IcebergSQLJSONGlue/pom.xml rename to java/Iceberg/IcebergSQLSink/pom.xml index 627ec0a..5be621a 100644 --- a/java/Iceberg/IcebergSQLJSONGlue/pom.xml +++ b/java/Iceberg/IcebergSQLSink/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.amazonaws - iceberg-sql-flink + iceberg-sql-sink 1.0 jar @@ -15,7 +15,7 @@ ${target.java.version} ${target.java.version} 1.20 - 1.20.1 + 1.20.0 2.12 1.9.1 1.2.0 @@ -48,6 +48,7 @@ org.apache.flink flink-table-api-java-bridge ${flink.version} + provided @@ -60,6 +61,7 @@ org.apache.flink flink-clients ${flink.version} + provided @@ -95,11 +97,11 @@ - - org.apache.logging.log4j - log4j-slf4j-impl - ${log4j.version} - + + + + + org.apache.logging.log4j log4j-api @@ -158,7 +160,7 @@ - com.amazonaws.services.msf.GlueTableSQLJSONExample + com.amazonaws.services.msf.IcebergSQLSinkJob + + + + + + + - com.amazonaws - aws-java-sdk-bom - - 1.12.782 + software.amazon.awssdk + bom + 2.28.29 pom import @@ -97,11 +104,11 @@ - - - - - + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + org.apache.logging.log4j log4j-api @@ -158,8 +165,10 @@ - - + + com.amazonaws.services.msf.IcebergSQLSinkJob diff --git a/java/Iceberg/IcebergSQLSink/src/main/java/com/amazonaws/services/msf/IcebergSQLSinkJob.java b/java/Iceberg/IcebergSQLSink/src/main/java/com/amazonaws/services/msf/IcebergSQLSinkJob.java index ea91e59..ba0dbec 100644 --- a/java/Iceberg/IcebergSQLSink/src/main/java/com/amazonaws/services/msf/IcebergSQLSinkJob.java +++ b/java/Iceberg/IcebergSQLSink/src/main/java/com/amazonaws/services/msf/IcebergSQLSinkJob.java @@ -134,7 +134,9 @@ public static void main(String[] args) throws Exception { // 2 If running local, we need to enable Checkpoints. Iceberg commits data with every checkpoint if (isLocal(env)) { - env.enableCheckpointing(60000); + // For development, we are checkpointing every 30 second to see files being committed faster + // In production, committing every 30 second may generate small files. + env.enableCheckpointing(30000); } // 3. Setup configuration properties with validation @@ -167,9 +169,9 @@ public static void main(String[] args) throws Exception { tableEnv.executeSql(createTableStatement); // 7. Execute SQL operations - Insert data from stock price stream - String insertQuery = "INSERT INTO " + sinkTableName + - " SELECT `timestamp`, symbol, price, volumes FROM default_catalog.default_database.stockPriceTable"; - LOG.info("Executing insert statement: {}", insertQuery); + String insertQuery = "INSERT INTO " + sinkTableName + " " + + "SELECT `timestamp`, symbol, price, volumes " + + "FROM default_catalog.default_database.stockPriceTable"; TableResult insertResult = tableEnv.executeSql(insertQuery); // Keep the job running to continuously insert data diff --git a/java/Iceberg/IcebergSQLSink/src/main/java/com/amazonaws/services/msf/pojo/StockPrice.java b/java/Iceberg/IcebergSQLSink/src/main/java/com/amazonaws/services/msf/pojo/StockPrice.java new file mode 100644 index 0000000..3b4f923 --- /dev/null +++ b/java/Iceberg/IcebergSQLSink/src/main/java/com/amazonaws/services/msf/pojo/StockPrice.java @@ -0,0 +1,60 @@ +package com.amazonaws.services.msf.pojo; + +public class StockPrice { + private String timestamp; + private String symbol; + private Float price; + private Integer volumes; + + public StockPrice() { + } + + public StockPrice(String timestamp, String symbol, Float price, Integer volumes) { + this.timestamp = timestamp; + this.symbol = symbol; + this.price = price; + this.volumes = volumes; + } + + public String getTimestamp() { + return timestamp; + } + + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + + public String getSymbol() { + return symbol; + } + + public void setSymbol(String symbol) { + this.symbol = symbol; + } + + public Float getPrice() { + return price; + } + + public void setPrice(Float price) { + this.price = price; + } + + public Integer getVolumes() { + return volumes; + } + + public void setVolumes(Integer volumes) { + this.volumes = volumes; + } + + @Override + public String toString() { + return "com.amazonaws.services.msf.pojo.StockPrice{" + + "timestamp='" + timestamp + '\'' + + ", symbol='" + symbol + '\'' + + ", price=" + price + + ", volumes=" + volumes + + '}'; + } +} diff --git a/java/Iceberg/IcebergSQLSink/src/main/java/com/amazonaws/services/msf/source/StockPriceGeneratorFunction.java b/java/Iceberg/IcebergSQLSink/src/main/java/com/amazonaws/services/msf/source/StockPriceGeneratorFunction.java new file mode 100644 index 0000000..1cf5554 --- /dev/null +++ b/java/Iceberg/IcebergSQLSink/src/main/java/com/amazonaws/services/msf/source/StockPriceGeneratorFunction.java @@ -0,0 +1,27 @@ +package com.amazonaws.services.msf.source; + +import com.amazonaws.services.msf.pojo.StockPrice; +import org.apache.commons.lang3.RandomUtils; +import org.apache.flink.connector.datagen.source.GeneratorFunction; +import java.time.Instant; + +/** + * Function used by DataGen source to generate random records as com.amazonaws.services.msf.pojo.StockPrice POJOs. + * + * The generator mimics the behavior of AvroGenericStockTradeGeneratorFunction + * from the IcebergDataStreamSink example. + */ +public class StockPriceGeneratorFunction implements GeneratorFunction { + + private static final String[] SYMBOLS = {"AAPL", "AMZN", "MSFT", "INTC", "TBV"}; + + @Override + public StockPrice map(Long sequence) throws Exception { + String symbol = SYMBOLS[RandomUtils.nextInt(0, SYMBOLS.length)]; + float price = RandomUtils.nextFloat(0, 10); + int volumes = RandomUtils.nextInt(0, 1000000); + String timestamp = Instant.now().toString(); + + return new StockPrice(timestamp, symbol, price, volumes); + } +} diff --git a/java/Iceberg/IcebergSQLSink/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java b/java/Iceberg/IcebergSQLSink/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java new file mode 100644 index 0000000..b177d06 --- /dev/null +++ b/java/Iceberg/IcebergSQLSink/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java @@ -0,0 +1,120 @@ +package org.apache.flink.runtime.util; + +import org.apache.flink.api.java.tuple.Tuple2; +import org.apache.flink.util.FlinkRuntimeException; +import org.apache.flink.util.Preconditions; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.io.Text; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.hadoop.security.token.Token; +import org.apache.hadoop.security.token.TokenIdentifier; +import org.apache.hadoop.util.VersionInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; + + +/** + * This class is a copy of org.apache.flink.runtime.util.HadoopUtils with the getHadoopConfiguration() method replaced to + * return an org.apache.hadoop.conf.Configuration instead of org.apache.hadoop.hdfs.HdfsConfiguration. + * + * This class is then shaded, along with org.apache.hadoop.conf.*, to avoid conflicts with the same classes provided by + * org.apache.flink:flink-s3-fs-hadoop, which is normally installed as plugin in Flink when S3. + * + * Other methods are copied from the original class. + */ +public class HadoopUtils { + private static final Logger LOG = LoggerFactory.getLogger(HadoopUtils.class); + + static final Text HDFS_DELEGATION_TOKEN_KIND = new Text("HDFS_DELEGATION_TOKEN"); + + /** + * This method has been re-implemented to always return a org.apache.hadoop.conf.Configuration + */ + public static Configuration getHadoopConfiguration( + org.apache.flink.configuration.Configuration flinkConfiguration) { + return new Configuration(false); + } + + public static boolean isKerberosSecurityEnabled(UserGroupInformation ugi) { + return UserGroupInformation.isSecurityEnabled() + && ugi.getAuthenticationMethod() + == UserGroupInformation.AuthenticationMethod.KERBEROS; + } + + + public static boolean areKerberosCredentialsValid( + UserGroupInformation ugi, boolean useTicketCache) { + Preconditions.checkState(isKerberosSecurityEnabled(ugi)); + + // note: UGI::hasKerberosCredentials inaccurately reports false + // for logins based on a keytab (fixed in Hadoop 2.6.1, see HADOOP-10786), + // so we check only in ticket cache scenario. + if (useTicketCache && !ugi.hasKerberosCredentials()) { + if (hasHDFSDelegationToken(ugi)) { + LOG.warn( + "Hadoop security is enabled but current login user does not have Kerberos credentials, " + + "use delegation token instead. Flink application will terminate after token expires."); + return true; + } else { + LOG.error( + "Hadoop security is enabled, but current login user has neither Kerberos credentials " + + "nor delegation tokens!"); + return false; + } + } + + return true; + } + + /** + * Indicates whether the user has an HDFS delegation token. + */ + public static boolean hasHDFSDelegationToken(UserGroupInformation ugi) { + Collection> usrTok = ugi.getTokens(); + for (Token token : usrTok) { + if (token.getKind().equals(HDFS_DELEGATION_TOKEN_KIND)) { + return true; + } + } + return false; + } + + /** + * Checks if the Hadoop dependency is at least the given version. + */ + public static boolean isMinHadoopVersion(int major, int minor) throws FlinkRuntimeException { + final Tuple2 hadoopVersion = getMajorMinorBundledHadoopVersion(); + int maj = hadoopVersion.f0; + int min = hadoopVersion.f1; + + return maj > major || (maj == major && min >= minor); + } + + /** + * Checks if the Hadoop dependency is at most the given version. + */ + public static boolean isMaxHadoopVersion(int major, int minor) throws FlinkRuntimeException { + final Tuple2 hadoopVersion = getMajorMinorBundledHadoopVersion(); + int maj = hadoopVersion.f0; + int min = hadoopVersion.f1; + + return maj < major || (maj == major && min < minor); + } + + private static Tuple2 getMajorMinorBundledHadoopVersion() { + String versionString = VersionInfo.getVersion(); + String[] versionParts = versionString.split("\\."); + + if (versionParts.length < 2) { + throw new FlinkRuntimeException( + "Cannot determine version of Hadoop, unexpected version string: " + + versionString); + } + + int maj = Integer.parseInt(versionParts[0]); + int min = Integer.parseInt(versionParts[1]); + return Tuple2.of(maj, min); + } +} \ No newline at end of file diff --git a/java/Iceberg/IcebergSQLSink/src/main/resources/flink-application-properties-dev.json b/java/Iceberg/IcebergSQLSink/src/main/resources/flink-application-properties-dev.json new file mode 100644 index 0000000..b8474f1 --- /dev/null +++ b/java/Iceberg/IcebergSQLSink/src/main/resources/flink-application-properties-dev.json @@ -0,0 +1,16 @@ +[ + { + "PropertyGroupId": "DataGen", + "PropertyMap": { + "records.per.sec": 10.0 + } + }, + { + "PropertyGroupId": "Iceberg", + "PropertyMap": { + "bucket.prefix": "s3://msf-examples-output/iceberg", + "catalog.db": "iceberg", + "catalog.table": "sqlsink_prices" + } + } +] \ No newline at end of file diff --git a/java/Iceberg/IcebergSQLSink/src/main/resources/log4j2.properties b/java/Iceberg/IcebergSQLSink/src/main/resources/log4j2.properties new file mode 100644 index 0000000..97c21b9 --- /dev/null +++ b/java/Iceberg/IcebergSQLSink/src/main/resources/log4j2.properties @@ -0,0 +1,13 @@ +# Log4j2 configuration +status = warn +name = PropertiesConfig + +# Console appender configuration +appender.console.type = Console +appender.console.name = ConsoleAppender +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{HH:mm:ss,SSS} %-5p %-60c %x - %m%n + +# Root logger configuration +rootLogger.level = INFO +rootLogger.appenderRef.console.ref = ConsoleAppender \ No newline at end of file From d0ce61c0e874af398aa8ad5151e7812df826b57b Mon Sep 17 00:00:00 2001 From: Lorenzo Nicora Date: Sat, 12 Jul 2025 19:09:04 +0200 Subject: [PATCH 6/7] Rename the example. Minor README fixes --- .../README.md | 62 +++++++++---------- java/Iceberg/IcebergSQLSink/pom.xml | 31 ++++++---- .../services/msf/IcebergSQLSinkJob.java | 19 +++--- .../services/msf/pojo/StockPrice.java | 0 .../source/StockPriceGeneratorFunction.java | 0 .../flink/runtime/util/HadoopUtils.java | 0 .../flink-application-properties-dev.json | 2 +- .../src/main/resources/log4j2.properties | 2 +- 8 files changed, 63 insertions(+), 53 deletions(-) rename java/Iceberg/{S3TableSQLSink => IcebergSQLSink}/README.md (78%) rename java/Iceberg/{S3TableSQLSink => IcebergSQLSink}/src/main/java/com/amazonaws/services/msf/pojo/StockPrice.java (100%) rename java/Iceberg/{S3TableSQLSink => IcebergSQLSink}/src/main/java/com/amazonaws/services/msf/source/StockPriceGeneratorFunction.java (100%) rename java/Iceberg/{S3TableSQLSink => IcebergSQLSink}/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java (100%) rename java/Iceberg/{S3TableSQLSink => IcebergSQLSink}/src/main/resources/flink-application-properties-dev.json (86%) rename java/Iceberg/{S3TableSQLSink => IcebergSQLSink}/src/main/resources/log4j2.properties (86%) diff --git a/java/Iceberg/S3TableSQLSink/README.md b/java/Iceberg/IcebergSQLSink/README.md similarity index 78% rename from java/Iceberg/S3TableSQLSink/README.md rename to java/Iceberg/IcebergSQLSink/README.md index cb8dcf4..f0a656f 100644 --- a/java/Iceberg/S3TableSQLSink/README.md +++ b/java/Iceberg/IcebergSQLSink/README.md @@ -1,4 +1,4 @@ -# Iceberg ink to Amazon S3 Table using SQL +## Iceberg Sink (Glue Data Catalog) using SQL * Flink version: 1.20.0 * Flink API: SQL API @@ -37,12 +37,18 @@ When running locally, the configuration is read from the Runtime parameters: -| Group ID | Key | Default | Description | -|-----------|--------------------------|-------------------|--------------------------------------------------------| -| `DataGen` | `records.per.sec` | `10.0` | Records per second generated. | -| `Iceberg` | `bucket.prefix` | (mandatory) | S3 bucket prefix, for example `s3://mybucket/iceberg`. | -| `Iceberg` | `catalog.db` | `default` | Name of the Glue Data Catalog database. | -| `Iceberg` | `catalog.table` | `prices_iceberg` | Name of the Glue Data Catalog table. | +| Group ID | Key | Default | Description | +|-----------|--------------------------|-------------------|--------------------------------------------------------------------------------------------| +| `DataGen` | `records.per.sec` | `10.0` | Records per second generated. | +| `Iceberg` | `bucket.prefix` | (mandatory) | S3 bucket and path URL prefix, starting with `s3://`. For example `s3://mybucket/iceberg`. | +| `Iceberg` | `catalog.db` | `default` | Name of the Glue Data Catalog database. | +| `Iceberg` | `catalog.table` | `prices_iceberg` | Name of the Glue Data Catalog table. | + +### Running locally, in IntelliJ + +You can run this example directly in IntelliJ, without any local Flink cluster or local Flink installation. + +See [Running examples locally](https://github.com/nicusX/amazon-managed-service-for-apache-flink-examples/blob/main/java/running-examples-locally.md) for details. ### Checkpoints @@ -51,21 +57,31 @@ Checkpointing must be enabled. Iceberg commits writes on checkpoint. When running locally, the application enables checkpoints programmatically, every 30 seconds. When deployed to Managed Service for Apache Flink, checkpointing is controlled by the application configuration. -### Known limitations +### Sample Data Schema + +The application uses a predefined schema for the stock price data with the following fields: +* `timestamp`: STRING - ISO timestamp of the record +* `symbol`: STRING - Stock symbol (e.g., AAPL, AMZN) +* `price`: FLOAT - Stock price (0-10 range) +* `volumes`: INT - Trade volumes (0-1000000 range) + +### Known limitations of the Flink Iceberg sink At the moment there are current limitations concerning Flink Iceberg integration: * Doesn't support Iceberg Table with hidden partitioning * Doesn't support adding columns, removing columns, renaming columns or changing columns. -### Hadoop Library Clash +--- -When integrating Flink with Iceberg, there's a common configuration challenge that affects most Flink deployments: +### Known Flink issue: Hadoop library clash -#### Problem +When integrating Flink with Iceberg, there's a common issue affecting most Flink setups -* When using Flink SQL's `CREATE CATALOG` statements, Hadoop libraries must be available on the system classpath -* However, standard Flink distributions use shaded dependencies that can create class loading conflicts with Hadoop's expectations -* This is particularly relevant for TaskManagers (which is the case for most generic Flink clusters, except EMR) +When using Flink SQL's `CREATE CATALOG` statements, Hadoop libraries must be available on the system classpath. +However, standard Flink distributions use shaded dependencies that can create class loading conflicts with Hadoop's +expectations. +Flink default classloading, when running in Application mode, prevents from using some Hadoop classes even if +included in the application uber-jar. #### Solution @@ -90,20 +106,4 @@ into the `maven-shade-plugin` configuration. shadow.org.apache.flink.runtime.util.HadoopUtils -``` - - -### Sample Data Schema - -The application uses a predefined schema for the stock price data with the following fields: -* `timestamp`: STRING - ISO timestamp of the record -* `symbol`: STRING - Stock symbol (e.g., AAPL, AMZN) -* `price`: FLOAT - Stock price (0-10 range) -* `volumes`: INT - Trade volumes (0-1000000 range) - - -### Running locally, in IntelliJ - -You can run this example directly in IntelliJ, without any local Flink cluster or local Flink installation. - -See [Running examples locally](https://github.com/nicusX/amazon-managed-service-for-apache-flink-examples/blob/main/java/running-examples-locally.md) for details. +``` \ No newline at end of file diff --git a/java/Iceberg/IcebergSQLSink/pom.xml b/java/Iceberg/IcebergSQLSink/pom.xml index 5be621a..a79b73e 100644 --- a/java/Iceberg/IcebergSQLSink/pom.xml +++ b/java/Iceberg/IcebergSQLSink/pom.xml @@ -26,11 +26,18 @@ + + + + + + + + - com.amazonaws - aws-java-sdk-bom - - 1.12.782 + software.amazon.awssdk + bom + 2.28.29 pom import @@ -97,11 +104,11 @@ - - - - - + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + org.apache.logging.log4j log4j-api @@ -158,8 +165,10 @@ - - + + com.amazonaws.services.msf.IcebergSQLSinkJob diff --git a/java/Iceberg/IcebergSQLSink/src/main/java/com/amazonaws/services/msf/IcebergSQLSinkJob.java b/java/Iceberg/IcebergSQLSink/src/main/java/com/amazonaws/services/msf/IcebergSQLSinkJob.java index ea91e59..c296926 100644 --- a/java/Iceberg/IcebergSQLSink/src/main/java/com/amazonaws/services/msf/IcebergSQLSinkJob.java +++ b/java/Iceberg/IcebergSQLSink/src/main/java/com/amazonaws/services/msf/IcebergSQLSinkJob.java @@ -128,28 +128,29 @@ private static class IcebergConfig { } public static void main(String[] args) throws Exception { - // 1. Initialize environments - using standard environment instead of WebUI for production consistency + // 1. Initialize environments final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); final StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env); - // 2 If running local, we need to enable Checkpoints. Iceberg commits data with every checkpoint + // 2. If running local, we need to enable Checkpoints. Iceberg commits data with every checkpoint if (isLocal(env)) { - env.enableCheckpointing(60000); + // For development, we are checkpointing every 30 second to have data commited faster. + env.enableCheckpointing(30000); } - // 3. Setup configuration properties with validation + // 3. Parse and validate the configuration for the Iceberg sink Map applicationProperties = loadApplicationProperties(env); Properties icebergProperties = applicationProperties.get("Iceberg"); IcebergConfig config = setupIcebergProperties(icebergProperties); - // 4. Create data generator source + // 4. Create data generator source, using DataStream API Properties dataGenProperties = applicationProperties.get("DataGen"); DataStream stockPriceDataStream = env.fromSource( createDataGenerator(dataGenProperties), WatermarkStrategy.noWatermarks(), "DataGen"); - // 5. Convert DataStream to Table and create view + // 5. Convert DataStream to a Table and create view Table stockPriceTable = tableEnv.fromDataStream(stockPriceDataStream); tableEnv.createTemporaryView("stockPriceTable", stockPriceTable); @@ -167,9 +168,9 @@ public static void main(String[] args) throws Exception { tableEnv.executeSql(createTableStatement); // 7. Execute SQL operations - Insert data from stock price stream - String insertQuery = "INSERT INTO " + sinkTableName + - " SELECT `timestamp`, symbol, price, volumes FROM default_catalog.default_database.stockPriceTable"; - LOG.info("Executing insert statement: {}", insertQuery); + String insertQuery = "INSERT INTO " + sinkTableName + " " + + "SELECT `timestamp`, symbol, price, volumes " + + "FROM default_catalog.default_database.stockPriceTable"; TableResult insertResult = tableEnv.executeSql(insertQuery); // Keep the job running to continuously insert data diff --git a/java/Iceberg/S3TableSQLSink/src/main/java/com/amazonaws/services/msf/pojo/StockPrice.java b/java/Iceberg/IcebergSQLSink/src/main/java/com/amazonaws/services/msf/pojo/StockPrice.java similarity index 100% rename from java/Iceberg/S3TableSQLSink/src/main/java/com/amazonaws/services/msf/pojo/StockPrice.java rename to java/Iceberg/IcebergSQLSink/src/main/java/com/amazonaws/services/msf/pojo/StockPrice.java diff --git a/java/Iceberg/S3TableSQLSink/src/main/java/com/amazonaws/services/msf/source/StockPriceGeneratorFunction.java b/java/Iceberg/IcebergSQLSink/src/main/java/com/amazonaws/services/msf/source/StockPriceGeneratorFunction.java similarity index 100% rename from java/Iceberg/S3TableSQLSink/src/main/java/com/amazonaws/services/msf/source/StockPriceGeneratorFunction.java rename to java/Iceberg/IcebergSQLSink/src/main/java/com/amazonaws/services/msf/source/StockPriceGeneratorFunction.java diff --git a/java/Iceberg/S3TableSQLSink/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java b/java/Iceberg/IcebergSQLSink/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java similarity index 100% rename from java/Iceberg/S3TableSQLSink/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java rename to java/Iceberg/IcebergSQLSink/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java diff --git a/java/Iceberg/S3TableSQLSink/src/main/resources/flink-application-properties-dev.json b/java/Iceberg/IcebergSQLSink/src/main/resources/flink-application-properties-dev.json similarity index 86% rename from java/Iceberg/S3TableSQLSink/src/main/resources/flink-application-properties-dev.json rename to java/Iceberg/IcebergSQLSink/src/main/resources/flink-application-properties-dev.json index 2d36a58..cbbe0b0 100644 --- a/java/Iceberg/S3TableSQLSink/src/main/resources/flink-application-properties-dev.json +++ b/java/Iceberg/IcebergSQLSink/src/main/resources/flink-application-properties-dev.json @@ -10,7 +10,7 @@ "PropertyMap": { "bucket.prefix": "s3:///iceberg", "catalog.db": "iceberg", - "catalog.table": "prices_iceberg2" + "catalog.table": "sqlsink_prices" } } ] \ No newline at end of file diff --git a/java/Iceberg/S3TableSQLSink/src/main/resources/log4j2.properties b/java/Iceberg/IcebergSQLSink/src/main/resources/log4j2.properties similarity index 86% rename from java/Iceberg/S3TableSQLSink/src/main/resources/log4j2.properties rename to java/Iceberg/IcebergSQLSink/src/main/resources/log4j2.properties index a6cccce..97c21b9 100644 --- a/java/Iceberg/S3TableSQLSink/src/main/resources/log4j2.properties +++ b/java/Iceberg/IcebergSQLSink/src/main/resources/log4j2.properties @@ -10,4 +10,4 @@ appender.console.layout.pattern = %d{HH:mm:ss,SSS} %-5p %-60c %x - %m%n # Root logger configuration rootLogger.level = INFO -rootLogger.appenderRef.console.ref = ConsoleAppender \ No newline at end of file +rootLogger.appenderRef.console.ref = ConsoleAppender \ No newline at end of file From 446b3607efb343a3bc7ace90233352d94c5613da Mon Sep 17 00:00:00 2001 From: Lorenzo Nicora Date: Sun, 13 Jul 2025 16:21:42 +0200 Subject: [PATCH 7/7] Merge branch 'main' into iceberg-sql-glue --- .gitattributes | 1 - .github/workflows/check-arns.yml | 24 + .gitignore | 5 +- CONTRIBUTING.md | 18 +- README.md | 87 +- infrastructure/AutoScaling/.gitignore | 5 + infrastructure/AutoScaling/README.md | 229 +- .../AutoScaling/cdk/bin/kda-autoscaling.d.ts | 2 + .../AutoScaling/cdk/bin/kda-autoscaling.js | 20 + .../AutoScaling/cdk/bin/kda-autoscaling.ts | 19 + infrastructure/AutoScaling/cdk/cdk.json | 53 + infrastructure/AutoScaling/cdk/jest.config.js | 8 + .../cdk/lib/kda-autoscaling-stack.d.ts | 5 + .../cdk/lib/kda-autoscaling-stack.js | 17 + .../cdk/lib/kda-autoscaling-stack.ts | 520 +++ .../AutoScaling/cdk/package-lock.json | 4152 +++++++++++++++++ infrastructure/AutoScaling/cdk/package.json | 27 + .../cdk/resources/scaling/scaling.py | 129 + .../cdk/test/kda-autoscaling.test.d.ts | 0 .../cdk/test/kda-autoscaling.test.js | 17 + .../cdk/test/kda-autoscaling.test.ts | 17 + infrastructure/AutoScaling/cdk/tsconfig.json | 31 + .../AutoScaling/generateautoscaler.sh | 68 + .../AutoScaling/img/generating-autoscaler.png | Bin 0 -> 166373 bytes infrastructure/README.md | 1 + infrastructure/scripts/README.md | 51 + infrastructure/scripts/task_status.sh | 47 + .../flink-application-properties-dev.json | 6 +- .../flink-application-properties-dev.json | 4 +- .../FlinkCDCSQLServerSource/README.md | 150 + .../docker/docker-compose.yml | 77 + .../docker/mysql-init/init.sql | 15 + .../docker/postgres-init/init.sql | 10 + .../docker/sqlserver-init/init.sql | 56 + java/FlinkCDC/FlinkCDCSQLServerSource/pom.xml | 191 + .../msf/FlinkCDCSqlServer2JdbcJob.java | 157 + .../flink-application-properties-dev.json | 22 + .../src/main/resources/log4j2.properties | 7 + java/FlinkCDC/README.md | 9 + java/FlinkDataGenerator/README.md | 144 + java/FlinkDataGenerator/images/dashboard.png | Bin 0 -> 400863 bytes java/FlinkDataGenerator/pom.xml | 185 + .../services/msf/DataGeneratorJob.java | 225 + .../services/msf/MetricEmitterNoOpMap.java | 52 + .../services/msf/domain/StockPrice.java | 70 + .../domain/StockPriceGeneratorFunction.java | 59 + .../flink-application-properties-dev.json | 22 + .../src/main/resources/log4j2.properties | 16 + .../services/msf/DataGeneratorJobTest.java | 150 + .../StockPriceGeneratorFunctionTest.java | 77 + .../services/msf/domain/StockPriceTest.java | 64 + ...ink-application-properties-kafka-only.json | 15 + ...k-application-properties-kinesis-only.json | 15 + ...flink-application-properties-no-sinks.json | 8 + .../tools/dashboard-cfn.yaml | 578 +++ java/Iceberg/IcebergDataStreamSink/README.md | 21 +- java/Iceberg/IcebergDataStreamSink/pom.xml | 4 +- .../msf/iceberg/IcebergSinkBuilder.java | 11 +- .../flink-application-properties-dev.json | 3 +- .../Iceberg/IcebergDataStreamSource/README.md | 2 +- java/Iceberg/IcebergDataStreamSource/pom.xml | 4 +- .../flink-application-properties-dev.json | 2 +- java/Iceberg/README.md | 12 + java/Iceberg/S3TableSink/README.md | 34 +- .../amazonaws/services/msf/StreamingJob.java | 22 +- .../msf/iceberg/IcebergSinkBuilder.java | 12 +- .../flink-application-properties-dev.json | 8 +- java/KafkaConfigProviders/README.md | 19 +- .../flink-application-properties-dev.json | 4 +- java/KinesisSourceDeaggregation/README.md | 31 + .../flink-app/README.md | 55 + .../flink-app/pom.xml | 199 + .../amazonaws/services/msf/StreamingJob.java | 99 + ...gregatingDeserializationSchemaWrapper.java | 105 + .../services/msf/model/StockPrice.java | 47 + .../flink-application-properties-dev.json | 17 + .../src/main/resources/log4j2.properties | 8 + .../kpl-producer/README.md | 20 + .../kpl-producer/pom.xml | 79 + .../kds/producer/KplAggregatingProducer.java | 182 + .../kds/producer/model/StockPrice.java | 37 + .../main/resources/simplelogger.properties | 8 + java/KinesisSourceDeaggregation/pom.xml | 16 + java/S3AvroSink/README.md | 54 + java/S3AvroSink/pom.xml | 212 + .../amazonaws/services/msf/StreamingJob.java | 112 + .../datagen/StockPriceGeneratorFunction.java | 20 + .../src/main/resources/avro/stockprice.avdl | 9 + .../flink-application-properties-dev.json | 9 + .../src/main/resources/log4j2.properties | 14 + java/S3AvroSource/README.md | 57 + java/S3AvroSource/pom.xml | 224 + .../msf/AvroSpecificRecordBulkFormat.java | 44 + .../amazonaws/services/msf/JsonConverter.java | 37 + .../amazonaws/services/msf/StreamingJob.java | 109 + .../src/main/resources/avro/stockprice.avdl | 9 + .../flink-application-properties-dev.json | 16 + .../src/main/resources/log4j2.properties | 14 + java/S3ParquetSink/README.md | 2 + java/S3ParquetSource/README.md | 76 + java/S3ParquetSource/pom.xml | 400 ++ .../amazonaws/services/msf/JsonConverter.java | 37 + .../services/msf/S3ParquetToKinesisJob.java | 108 + .../flink/runtime/util/HadoopUtils.java | 120 + .../src/main/resources/avro/stockprice.avdl | 9 + .../flink-application-properties-dev.json | 17 + .../src/main/resources/log4j2.properties | 14 + .../flink-application-properties-dev.json | 2 +- java/Serialization/README.md | 8 + .../flink-application-properties-dev.json | 4 +- java/pom.xml | 6 + python/GettingStarted/README.md | 9 + python/GettingStarted/pom.xml | 2 +- python/IcebergSink/README.md | 249 + .../IcebergSink/application_properties.json | 19 + python/IcebergSink/assembly/assembly.xml | 25 + python/IcebergSink/main.py | 237 + python/IcebergSink/pom.xml | 189 + .../flink/runtime/util/HadoopUtils.java | 14 + python/PackagedPythonDependencies/.gitignore | 1 + python/PackagedPythonDependencies/README.md | 196 + .../application_properties.json | 17 + .../assembly/assembly.xml | 44 + python/PackagedPythonDependencies/main.py | 263 ++ python/PackagedPythonDependencies/pom.xml | 115 + .../requirements.txt | 1 + python/PythonDependencies/README.md | 53 +- python/README.md | 42 +- 128 files changed, 12020 insertions(+), 197 deletions(-) delete mode 100644 .gitattributes create mode 100644 .github/workflows/check-arns.yml create mode 100644 infrastructure/AutoScaling/.gitignore create mode 100644 infrastructure/AutoScaling/cdk/bin/kda-autoscaling.d.ts create mode 100644 infrastructure/AutoScaling/cdk/bin/kda-autoscaling.js create mode 100644 infrastructure/AutoScaling/cdk/bin/kda-autoscaling.ts create mode 100644 infrastructure/AutoScaling/cdk/cdk.json create mode 100644 infrastructure/AutoScaling/cdk/jest.config.js create mode 100644 infrastructure/AutoScaling/cdk/lib/kda-autoscaling-stack.d.ts create mode 100644 infrastructure/AutoScaling/cdk/lib/kda-autoscaling-stack.js create mode 100644 infrastructure/AutoScaling/cdk/lib/kda-autoscaling-stack.ts create mode 100644 infrastructure/AutoScaling/cdk/package-lock.json create mode 100644 infrastructure/AutoScaling/cdk/package.json create mode 100644 infrastructure/AutoScaling/cdk/resources/scaling/scaling.py create mode 100644 infrastructure/AutoScaling/cdk/test/kda-autoscaling.test.d.ts create mode 100644 infrastructure/AutoScaling/cdk/test/kda-autoscaling.test.js create mode 100644 infrastructure/AutoScaling/cdk/test/kda-autoscaling.test.ts create mode 100644 infrastructure/AutoScaling/cdk/tsconfig.json create mode 100755 infrastructure/AutoScaling/generateautoscaler.sh create mode 100644 infrastructure/AutoScaling/img/generating-autoscaler.png create mode 100644 infrastructure/scripts/README.md create mode 100755 infrastructure/scripts/task_status.sh create mode 100644 java/FlinkCDC/FlinkCDCSQLServerSource/README.md create mode 100644 java/FlinkCDC/FlinkCDCSQLServerSource/docker/docker-compose.yml create mode 100644 java/FlinkCDC/FlinkCDCSQLServerSource/docker/mysql-init/init.sql create mode 100644 java/FlinkCDC/FlinkCDCSQLServerSource/docker/postgres-init/init.sql create mode 100644 java/FlinkCDC/FlinkCDCSQLServerSource/docker/sqlserver-init/init.sql create mode 100644 java/FlinkCDC/FlinkCDCSQLServerSource/pom.xml create mode 100644 java/FlinkCDC/FlinkCDCSQLServerSource/src/main/java/com/amazonaws/services/msf/FlinkCDCSqlServer2JdbcJob.java create mode 100644 java/FlinkCDC/FlinkCDCSQLServerSource/src/main/resources/flink-application-properties-dev.json create mode 100644 java/FlinkCDC/FlinkCDCSQLServerSource/src/main/resources/log4j2.properties create mode 100644 java/FlinkCDC/README.md create mode 100644 java/FlinkDataGenerator/README.md create mode 100644 java/FlinkDataGenerator/images/dashboard.png create mode 100644 java/FlinkDataGenerator/pom.xml create mode 100644 java/FlinkDataGenerator/src/main/java/com/amazonaws/services/msf/DataGeneratorJob.java create mode 100644 java/FlinkDataGenerator/src/main/java/com/amazonaws/services/msf/MetricEmitterNoOpMap.java create mode 100644 java/FlinkDataGenerator/src/main/java/com/amazonaws/services/msf/domain/StockPrice.java create mode 100644 java/FlinkDataGenerator/src/main/java/com/amazonaws/services/msf/domain/StockPriceGeneratorFunction.java create mode 100644 java/FlinkDataGenerator/src/main/resources/flink-application-properties-dev.json create mode 100644 java/FlinkDataGenerator/src/main/resources/log4j2.properties create mode 100644 java/FlinkDataGenerator/src/test/java/com/amazonaws/services/msf/DataGeneratorJobTest.java create mode 100644 java/FlinkDataGenerator/src/test/java/com/amazonaws/services/msf/domain/StockPriceGeneratorFunctionTest.java create mode 100644 java/FlinkDataGenerator/src/test/java/com/amazonaws/services/msf/domain/StockPriceTest.java create mode 100644 java/FlinkDataGenerator/src/test/resources/flink-application-properties-kafka-only.json create mode 100644 java/FlinkDataGenerator/src/test/resources/flink-application-properties-kinesis-only.json create mode 100644 java/FlinkDataGenerator/src/test/resources/flink-application-properties-no-sinks.json create mode 100644 java/FlinkDataGenerator/tools/dashboard-cfn.yaml create mode 100644 java/Iceberg/README.md create mode 100644 java/KinesisSourceDeaggregation/README.md create mode 100644 java/KinesisSourceDeaggregation/flink-app/README.md create mode 100644 java/KinesisSourceDeaggregation/flink-app/pom.xml create mode 100644 java/KinesisSourceDeaggregation/flink-app/src/main/java/com/amazonaws/services/msf/StreamingJob.java create mode 100644 java/KinesisSourceDeaggregation/flink-app/src/main/java/com/amazonaws/services/msf/deaggregation/KinesisDeaggregatingDeserializationSchemaWrapper.java create mode 100644 java/KinesisSourceDeaggregation/flink-app/src/main/java/com/amazonaws/services/msf/model/StockPrice.java create mode 100644 java/KinesisSourceDeaggregation/flink-app/src/main/resources/flink-application-properties-dev.json create mode 100644 java/KinesisSourceDeaggregation/flink-app/src/main/resources/log4j2.properties create mode 100644 java/KinesisSourceDeaggregation/kpl-producer/README.md create mode 100644 java/KinesisSourceDeaggregation/kpl-producer/pom.xml create mode 100644 java/KinesisSourceDeaggregation/kpl-producer/src/main/java/com/amazonaws/services/kds/producer/KplAggregatingProducer.java create mode 100644 java/KinesisSourceDeaggregation/kpl-producer/src/main/java/com/amazonaws/services/kds/producer/model/StockPrice.java create mode 100644 java/KinesisSourceDeaggregation/kpl-producer/src/main/resources/simplelogger.properties create mode 100644 java/KinesisSourceDeaggregation/pom.xml create mode 100644 java/S3AvroSink/README.md create mode 100644 java/S3AvroSink/pom.xml create mode 100644 java/S3AvroSink/src/main/java/com/amazonaws/services/msf/StreamingJob.java create mode 100644 java/S3AvroSink/src/main/java/com/amazonaws/services/msf/datagen/StockPriceGeneratorFunction.java create mode 100644 java/S3AvroSink/src/main/resources/avro/stockprice.avdl create mode 100644 java/S3AvroSink/src/main/resources/flink-application-properties-dev.json create mode 100644 java/S3AvroSink/src/main/resources/log4j2.properties create mode 100644 java/S3AvroSource/README.md create mode 100644 java/S3AvroSource/pom.xml create mode 100644 java/S3AvroSource/src/main/java/com/amazonaws/services/msf/AvroSpecificRecordBulkFormat.java create mode 100644 java/S3AvroSource/src/main/java/com/amazonaws/services/msf/JsonConverter.java create mode 100644 java/S3AvroSource/src/main/java/com/amazonaws/services/msf/StreamingJob.java create mode 100644 java/S3AvroSource/src/main/resources/avro/stockprice.avdl create mode 100644 java/S3AvroSource/src/main/resources/flink-application-properties-dev.json create mode 100644 java/S3AvroSource/src/main/resources/log4j2.properties create mode 100644 java/S3ParquetSource/README.md create mode 100644 java/S3ParquetSource/pom.xml create mode 100644 java/S3ParquetSource/src/main/java/com/amazonaws/services/msf/JsonConverter.java create mode 100644 java/S3ParquetSource/src/main/java/com/amazonaws/services/msf/S3ParquetToKinesisJob.java create mode 100644 java/S3ParquetSource/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java create mode 100644 java/S3ParquetSource/src/main/resources/avro/stockprice.avdl create mode 100644 java/S3ParquetSource/src/main/resources/flink-application-properties-dev.json create mode 100644 java/S3ParquetSource/src/main/resources/log4j2.properties create mode 100644 java/Serialization/README.md create mode 100644 python/IcebergSink/README.md create mode 100644 python/IcebergSink/application_properties.json create mode 100644 python/IcebergSink/assembly/assembly.xml create mode 100644 python/IcebergSink/main.py create mode 100644 python/IcebergSink/pom.xml create mode 100644 python/IcebergSink/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java create mode 100644 python/PackagedPythonDependencies/.gitignore create mode 100644 python/PackagedPythonDependencies/README.md create mode 100644 python/PackagedPythonDependencies/application_properties.json create mode 100644 python/PackagedPythonDependencies/assembly/assembly.xml create mode 100644 python/PackagedPythonDependencies/main.py create mode 100644 python/PackagedPythonDependencies/pom.xml create mode 100644 python/PackagedPythonDependencies/requirements.txt diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index b858c86..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -flink-application-properties-dev.json filter=arn-filter diff --git a/.github/workflows/check-arns.yml b/.github/workflows/check-arns.yml new file mode 100644 index 0000000..e858080 --- /dev/null +++ b/.github/workflows/check-arns.yml @@ -0,0 +1,24 @@ +name: Check for Exposed ARNs + +on: + pull_request: + +jobs: + check-arns: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Check for exposed ARNs + run: | + # Find files containing ARN patterns with actual account IDs + # Exclude .git directory, markdown files, and this workflow file itself + if grep -r --include="*" --exclude="*.md" --exclude-dir=".git" --exclude=".github/workflows/check-arns.yml" -E 'arn:aws:[^:]+:[^:]+:[0-9]{12}:' .; then + echo "ERROR: Found unsanitized ARNs in the repository" + echo "Please replace account IDs with a placeholder such as " + echo "Files with exposed ARNs:" + grep -r --include="*" --exclude="*.md" --exclude-dir=".git" --exclude=".github/workflows/check-arns.yml" -l -E 'arn:aws:[^:]+:[^:]+:[0-9]{12}:' . + exit 1 + fi + + echo "All files checked - no exposed ARNs found" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9883146..49ecf15 100644 --- a/.gitignore +++ b/.gitignore @@ -13,9 +13,8 @@ venv/ .java-version /pyflink/ __pycache__/ -/.vscode/ -/.docker/ +.vscode/ /.run/ clean.sh -smudge.sh +smudge.sh \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b2ce7d9..361a68d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,6 +59,7 @@ The AWS team managing the repository reserves the right to modify or reject new versions, external dependencies, permissions, and runtime configuration. Use [this example](https://github.com/aws-samples/amazon-managed-service-for-apache-flink-examples/tree/main/java/KafkaConfigProviders/Kafka-SASL_SSL-ConfigProviders) as a reference. * Make sure the example works with what explained in the README, and without any implicit dependency or configuration. +* Add an entry for the new example in the top-level [README](README.md) or in the README of the subfolder, if the example is in a subfolder such as `java/FlinkCDC` or `java/Iceberg` #### AWS authentication and credentials @@ -66,10 +67,19 @@ The AWS team managing the repository reserves the right to modify or reject new * Any permissions must be provided from the IAM Role assigned to the Managed Apache Flink application. When running locally, leverage the IDE AWS plugins. #### Dependencies in PyFlink examples - * Use the pattern illustrated by [this example](https://github.com/aws-samples/amazon-managed-service-for-apache-flink-examples/tree/main/python/GettingStarted) - to provide JAR dependencies and build the ZIP using Maven. - * If the application also requires Python dependencies, use the pattern illustrated by [this example](https://github.com/aws-samples/amazon-managed-service-for-apache-flink-examples/tree/main/python/PythonDependencies) - leveraging `requirements.txt`. + +* Use the pattern illustrated by [this example](https://github.com/aws-samples/amazon-managed-service-for-apache-flink-examples/tree/main/python/GettingStarted) + to provide JAR dependencies and build the ZIP using Maven. +* If the application also requires Python dependencies used for UDF and data processing in general, use the pattern illustrated by [this example](https://github.com/aws-samples/amazon-managed-service-for-apache-flink-examples/tree/main/python/PythonDependencies) + leveraging `requirements.txt`. +* Only if the application requires Python dependencies used during the job initialization, in the main(), use the pattern + illustrated in [this other example](https://github.com/aws-samples/amazon-managed-service-for-apache-flink-examples/tree/main/python/PackagedPythonDependencies), + packaging dependencies in the ZIP artifact. + +#### Top POM-file for Java examples + +* Add the new Java example also to the `pom.xml` file in the `java/` folder + ## Reporting Bugs/Feature Requests diff --git a/README.md b/README.md index 4a86784..39ad86a 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,89 @@ Example applications in Java, Python, Scala and SQL for Amazon Managed Service for Apache Flink (formerly known as Amazon Kinesis Data Analytics), illustrating various aspects of Apache Flink applications, and simple "getting started" base projects. -* [Java examples](./java) -* [Python examples](./python) -* [Scala examples](/scala) -* [Operational utilities and infrastructure code](./infrastructure) +## Table of Contents -## Security +### Java Examples -See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. +#### Getting Started +- [**Getting Started - DataStream API**](./java/GettingStarted) - Skeleton project for a basic Flink Java application using DataStream API +- [**Getting Started - Table API & SQL**](./java/GettingStartedTable) - Basic Flink Java application using Table API & SQL with DataStream API + +#### Connectors +- [**Kinesis Connectors**](./java/KinesisConnectors) - Examples of Flink Kinesis Connector source and sink (standard and EFO) +- [**Kinesis Source Deaggregation**](./java/KinesisSourceDeaggregation) - Handling Kinesis record deaggregation in the Kinesis source +- [**Kafka Connectors**](./java/KafkaConnectors) - Examples of Flink Kafka Connector source and sink +- [**Kafka Config Providers**](./java/KafkaConfigProviders) - Examples of using Kafka Config Providers for secure configuration management +- [**DynamoDB Stream Source**](./java/DynamoDBStreamSource) - Reading from DynamoDB Streams as a source +- [**Kinesis Firehose Sink**](./java/KinesisFirehoseSink) - Writing data to Amazon Kinesis Data Firehose +- [**SQS Sink**](./java/SQSSink) - Writing data to Amazon SQS +- [**Prometheus Sink**](./java/PrometheusSink) - Sending metrics to Prometheus +- [**Flink CDC**](./java/FlinkCDC) - Change Data Capture examples using Flink CDC + +#### Reading and writing files and transactional data lake formats +- [**Iceberg**](./java/Iceberg) - Working with Apache Iceberg and Amazon S3 Tables +- [**S3 Sink**](./java/S3Sink) - Writing JSON data to Amazon S3 +- [**S3 Avro Sink**](./java/S3AvroSink) - Writing Avro format data to Amazon S3 +- [**S3 Avro Source**](./java/S3AvroSource) - Reading Avro format data from Amazon S3 +- [**S3 Parquet Sink**](./java/S3ParquetSink) - Writing Parquet format data to Amazon S3 +- [**S3 Parquet Source**](./java/S3ParquetSource) - Reading Parquet format data from Amazon S3 + +#### Data Formats & Schema Registry +- [**Avro with Glue Schema Registry - Kinesis**](./java/AvroGlueSchemaRegistryKinesis) - Using Avro format with AWS Glue Schema Registry and Kinesis +- [**Avro with Glue Schema Registry - Kafka**](./java/AvroGlueSchemaRegistryKafka) - Using Avro format with AWS Glue Schema Registry and Kafka + +#### Stream Processing Patterns +- [**Serialization**](./java/Serialization) - Serialization of record and state +- [**Windowing**](./java/Windowing) - Time-based window aggregation examples +- [**Side Outputs**](./java/SideOutputs) - Using side outputs for data routing and filtering +- [**Async I/O**](./java/AsyncIO) - Asynchronous I/O patterns with retries for external API calls\ +- [**Custom Metrics**](./java/CustomMetrics) - Creating and publishing custom application metrics + +#### Utilities +- [**Fink Data Generator (JSON)**](java/FlinkDataGenerator) - How to use a Flink application as data generator, for functional and load testing. + +### Python Examples + +#### Getting Started +- [**Getting Started**](./python/GettingStarted) - Basic PyFlink application Table API & SQL + +#### Handling Python dependencies +- [**Python Dependencies**](./python/PythonDependencies) - Managing Python dependencies in PyFlink applications using `requirements.txt` +- [**Packaged Python Dependencies**](./python/PackagedPythonDependencies) - Managing Python dependencies packaged with the PyFlink application at build time + +#### Connectors +- [**Datastream Kafka Connector**](./python/DatastreamKafkaConnector) - Using Kafka connector with PyFlink DataStream API +- [**Kafka Config Providers**](./python/KafkaConfigProviders) - Secure configuration management for Kafka in PyFlink +- [**S3 Sink**](./python/S3Sink) - Writing data to Amazon S3 using PyFlink +- [**Firehose Sink**](./python/FirehoseSink) - Writing data to Amazon Kinesis Data Firehose +- [**Iceberg Sink**](./python/IcebergSink) - Writing data to Apache Iceberg tables + +#### Stream Processing Patterns +- [**Windowing**](./python/Windowing) - Time-based window aggregation examples with PyFlink/SQL +- [**User Defined Functions (UDF)**](./python/UDF) - Creating and using custom functions in PyFlink + +#### Utilities +- [**Data Generator**](./python/data-generator) - Python script for generating sample data to Kinesis Data Streams +- [**Local Development on Apple Silicon**](./python/LocalDevelopmentOnAppleSilicon) - Setup guide for local development of Flink 1.15 on Apple Silicon Macs (not required with Flink 1.18 or later) + + +### Scala Examples + +#### Getting Started +- [**Getting Started - DataStream API**](./scala/GettingStarted) - Skeleton project for a basic Flink Scala application using DataStream API + +### Infrastructure & Operations + +- [**Auto Scaling**](./infrastructure/AutoScaling) - Custom autoscaler for Amazon Managed Service for Apache Flink +- [**Scheduled Scaling**](./infrastructure/ScheduledScaling) - Scale applications up and down based on daily time schedules +- [**Monitoring**](./infrastructure/monitoring) - Extended CloudWatch Dashboard examples for monitoring applications +- [**Scripts**](./infrastructure/scripts) - Useful shell scripts for interacting with Amazon Managed Service for Apache Flink control plane API + +--- + +## Contributing + +See [Contributing Guidelines](CONTRIBUTING.md#security-issue-notifications) for more information. ## License Summary diff --git a/infrastructure/AutoScaling/.gitignore b/infrastructure/AutoScaling/.gitignore new file mode 100644 index 0000000..4fe961d --- /dev/null +++ b/infrastructure/AutoScaling/.gitignore @@ -0,0 +1,5 @@ +node_modules +cdk.out +/cdk.context.json +Autoscaler-*.yaml +/.DS_Store diff --git a/infrastructure/AutoScaling/README.md b/infrastructure/AutoScaling/README.md index aead3c5..381e289 100644 --- a/infrastructure/AutoScaling/README.md +++ b/infrastructure/AutoScaling/README.md @@ -1,73 +1,202 @@ -# Automatic Scaling for Amazon Managed Service for Apache Flink Applications +# Amazon Managed Service for Apache Flink - Custom autoscaler -* Python 3.9 for Lambda Function +This tool helps creating a custom autoscaler for your Amazon Managed Service for Apache Flink application. +The custom autoscaller allows you defining using metrics other than `containerCPUUtilization`, defining custom thresholds, max and min scale, and customize the autoscale cycle. -**IMPORTANT:** We strongly recommend that you disable autoscaling within your Amazon Managed Service for Apache Flink application if using the approach described here. -This sample illustrates how to scale your Amazon Managed Service for Apache Flink application using a different CloudWatch Metric from the Apache Flink Application, Amazon MSK, Amazon Kinesis Data Stream. Here's the high level approach: +Setting up the autoscaler is a two-steps process: +1. Generate a CloudFormation (CFN) template for an autoscaler for on a specific metric and statistic +2. Use the generated CFN template to deploy a CFN Stack which control autoscaling of a specific Managed Service for Apache Flink application. -- Using Amazon CloudWatch Alarms for the select metric in order to trigger Scaling Step Function Logic -- Step Functions that triggers the AWS Lambda Scaling Function, which verifies if the alarm after metric has gone into OK status -- AWS Lambda Function to Scale Up/Down the Managed Flink Application, as well as verifying the Application doesn't go above or below maximum/minimum KPU. -## Deploying the AutoScaling for Managed Flink Applications +## Process overview -Follow the instructions to deploy the autoscaling solution in your AWS Account -1. Clone this repository -2. Go to CloudFormation -3. Click Create stack -4. Select `Upload a template file` -5. Upload the template from this repository -6. This deployment takes the following CFN Parameters - 1. **Amazon Managed Service for Apache Flink AutoScaling Configuration:** +1. Decide metric and statistic for your autoscaler (see [Supported metric](#supported-metrics)). +2. Run the `generateautoscaler.sh` script to generate the CFN template. The generate template is named `Autoscaler-*.yaml` (see [Generate CFN template](#generate-the-cloudformation-template-using-the-script-recommended)) +3. Use the CFN template to create the autoscaler Stack. When you create the stack specify all remaining [autoscaler parameters](#autoscaler-parameters), including the application name (see [Deploying the autoscaler template](#deploying-the-autoscaler-template)) - 1. *Amazon Managed Service for Apache Flink Application Name*: The name of the Amazon Managed Apache Flink Application you would want to Auto Scale - 2. *Auto Scale Metric*: Available metrics to use for Autoscaling. - 3. *Custom Metric Name*: If you choose custom metric to do scaling, please provide its name. Remember that it will only work if you add as dimension to the Metric group **kinesisAnalytics** - 3. *Maximum KPU*: Maximum number of KPUs you want the Managed Flink Application to Scale - 4. *Minimum KPU*: Minimum number of KPUs you want the Managed Flink Application to Scale - 2. [**CloudWatch Alarm Configuration:**](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html#alarm-evaluation) - 5. *Evaluation Period for Metric*: Period to be used in minutes for the evaluation for scaling in or out (Example: 5) - 6. *Number of Data points to Trigger Alarm*: Number of Data Points (60 seconds each data point) during Evaluation Period where Metric has to be over threshold for rule to be in Alarm State. (Example: 2 This would mean that alarm would trigger if during a 5 minute window, 2 data points are above threshold) - 7. Waiting time for Application to finish updating (Seconds) - 7. *Grace Period for Alarm*: Time given in seconds to application after scaling to have alarms go to OK status (Example: 120) - 3. **Scale In/Out Configuration:** +Notes +* A generated CFN Template hardwires a specific metric and statistics. +* An autoscaler Stack controls a single Managed Flink application. +* If you want to create autoscaler Stacks for multiple applications, using the same metric and statistics, you can reuse the same CFN Template. - 8. *Scale Out/In Operation*: Scale Out/In Operation (Multiply/Divide or Add/Substract) - 9. *Scale In Factor*: Factor by which you want to reduce the number of KPUs in your Flink Application - 10. *Threshold for Metric to Scale Down*: Choose the threshold for when the Scale In Rule should be in Alarm State - 11. *Scale Out Factor*: Factor by which you want to increase the number of KPUs in your Flink Application - 12. *Threshold for Metric to Scale Up*: Choose the threshold for when the Scale Out Rule should be in Alarm State - 4. **Kafka Configuration:** +The CFN Stack creates two CloudWatch Alarms, a StepFunction and a Lambda function which automatically control the scaling of your Managed Flink application. - 13. *Amazon MSK Cluster Name*: If you choose topic metrics (MaxOffsetLag, SumOffsetLag or EstimatedMaxTimeLag) as metric to scale, you need to provide the name of the MSK Cluster for monitoring - 14. *Kafka Topic Name*: If you choose topic metrics (MaxOffsetLag, SumOffsetLag or EstimatedMaxTimeLag) as metric to scale, you need to provide the Kafka Topic for monitoring - 15. *Kafka Consumer Group*: If you choose topic metrics (MaxOffsetLag, SumOffsetLag or EstimatedMaxTimeLag) as metric to scale, you need to provide the Consumer Group name for monitoring - 5. **Kinesis Configuration:** - 16. *Kinesis Data Streams Name*: If you choose MillisBehindLatest as metric to scale, you need to provide the Kinesis Data Stream Name for monitoring +![Process of generating the CFN template and creating the autoscaler](img/generating-autoscaler.png) -## Scaling logic -As alluded to above, the scaling logic is a bit more involved than simply calling `UpdateApplication` on your Amazon Managed Service for Apache Flink application. Here are the steps involved: +> ⚠️ Neither the autoscaler nor CloudWatch Alarm validate the metric name. +> If you type a wrong metric name or select a metric type inconsistent with the metric, the CFN template will create the autoscaler stack but the CloudWatch Alarms controlling the scale will never trigger. -The Solution will trigger an alarm based on the threshold set in the parameter of the CloudFormation Template for the selected metric. This will activate an AWS Step Functions Workflow which will -* Scale the Managed Flink Application In or Out using an increase factor defined in the CloudFormation Template. -* Once the application has finished updating, it will verify if it has reached the minimum or maximum value for KPUs that the application can have. If it has it will finish the scaling event. -* If the application hasn't reached the max/min values of allocated KPU’s, the workflow will wait for a given period of time, to allow the metric to fall within threshold and have the Amazon CloudWatch Rule from ALARM status to OK. -* If the rule is still in ALARM status, the workflow will scale again the application. If the rule is now in OK, it will finish the scaling event. +--- +## Step 1: Generate the CloudFormation template using the script (recommended) -NOTE: In this sample, we assume that the parallelism/KPU is 1. For more background on parallelism and parallelism/KPU, please see [Application Scaling in Amazon Managed Service for Apache Flink](https://docs.aws.amazon.com/kinesisanalytics/latest/java/how-scaling.html). +1. Ensure you have installed AWS CDK in the current directory: `npm install aws-cdk-lib` +2. Decide which metric and statistics you want to use. See [Supported metrics](#supported-metrics) and [Supported statistics](#supported-statistics) +3. Execute the generator script to generate the YAML file with the CloudFormation template -## References +``` +generateautoscaler.sh type= stat= +``` -- [Amazon Managed Service for Apache Flink developer guide](https://docs.aws.amazon.com/kinesisanalytics/latest/java/what-is.html). -- [Application Scaling in Amazon Managed Service for Apache Flink](https://docs.aws.amazon.com/kinesisanalytics/latest/java/how-scaling.html). -- [KinesisAnalyticsV2 boto3 reference](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/kinesisanalyticsv2.html). \ No newline at end of file +The script generates a CFN template file named `Autoscaler--.yaml` in the same directory. + +Script parameters: +* `type` [`KinesisAnalytics`, `MSK`, `Kinesis`, `KinesisEFO`]: determines the metric type (default: `KinesisAnalytics`). +* `metric`: (mandatory) name of the metric. See [Supported metric](#supported-metrics). +* `stat` [`Average`, `Minimum`, `Maximum`, `Sum`] : metric statistics (default: `Maximum`). See [Supported statistics](#supported-statistics). + + +Examples of valid generator commands: + +``` +./generateautoscaler.sh type=KinesisAnalytics metric=containerCPUUtilization stat=Average + +./generateautoscaler.sh type=MSK metric=MaxOffsetLag stat=Maximum + +./generateautoscaler.sh type=Kinesis metric=GetRecords.IteratorAgeMilliseconds stat=Maximum + +./generateautoscaler.sh type=KinesisEFO metric=SubscribeToShardEvent.MillisBehindLates stat=Maximum +``` + +### Supported metrics + +When you generate the CFN template you specify +1. Metric type (see below) +2. Metric name +3. Statistic (Maximum, Average etc) + +The metric type determines the Namespace and Dimensions of the CloudWatch metric to use for autoscaling. + +> The name *KinesisAnalytics* refers to metrics exposed by Amazon Managed Service for Apache Flink. This is due to the CloudWatch Namespace that is called `AWS/KinesisAnalytics` for legacy reasons. + + +| Metric Type | Supported Metrics | Namespace | Dimensions | +|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------|-------------------------------------------| +| `KinesisAnalytics` | Any metrics exposed by Managed Service for Apache Flink which have only the `Applicaton` dimension. This also includes custom metrics when *Monitoring metrics level* is set to *Application*. | `AWS/KinesisAnalytics` | `Application` | +| `MSK` | MSK Consumer Group metrics, such as `EstimatedMaxTimeLag`, `EstimatedTimeLag`, `MaxOffsetLag`, `OffsetLag`, and `SumOffsetLag`. | `AWS/Kafka` | `Cluster Name`, `Consumer Group`, `Topic` | +| `Kinesis` | Any basic stream-level metrics exposed by Kinesis Data Streams with `StreamName` dimension only (i.e. excluding metric related to EFO consumers. | `AWS/Kinesis` | `StreamName` | +| `KinesisEFO` | Any basic stream-level metrics exposed by Kinesis Data Streams for EFO consumers, with `StreamName` and `ConsumerName` dimensions. For example `SubscribeToShardEvent.MillisBehindLatest`. | `AWS/Kinesis` | `StreamName`, `ConsumerName` | + +Reference docs +* [Metrics and dimensions in Managed Service for Apache Flink](https://docs.aws.amazon.com/managed-flink/latest/java/metrics-dimensions.html) +* [Use custom metrics with Amazon Managed Service for Apache Flink](https://docs.aws.amazon.com/managed-flink/latest/java/monitoring-metrics-custom.html) +* MSK: [Monitoring consumer lags](https://docs.aws.amazon.com/msk/latest/developerguide/consumer-lag.html) +* [Monitor the Amazon Kinesis Data Streams service with Amazon CloudWatch](https://docs.aws.amazon.com/streams/latest/dev/monitoring-with-cloudwatch.html#kinesis-metrics-stream) + + +### Supported statistics + +The autoscaler supports the following metric statistics: +* `Average` +* `Maximum` +* `Minumum` +* `Sum` + +The period of calculation of the statistic is defined when you create the stack (default: 60 sec). + +### Requirements for template generation + +To generate the CFN template you need: +1. AWS CLI +2. AWS CDK (Node.js). See [Getting started with AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html). + +This code has been tested with the following versions: +* Node v22.15.0 +* aws-cdk@2.1001.0 +* aws-cdk-lib@2.181.1 (constructs@10.4.2) +* aws-cli 2.24.14 + +The script will install any required Node dependency in the `./cdk` directory, using `npm install`. + +--- + +## Step 2: Deploying the autoscaler template + +You can use the generated autocaler CFN template to create an autoscaler stack that uses the metric and stats you specified when you generated the script, with any Managed Flink application. + +To deploy it (to create the stack) you can either use the AWS console or [CloudFormation CLI](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cloudformation/create-stack.html). + +> ⚠️ The provided CDK code is not designed to deploy the autoscaler stack. It only generates the CFN YAML template, locally. + + +When you deploy the CFN Template you need to specify additional parameters, to specify the application to control and other variables of the autoscaler, such as thesholds and scaling factors. The parameters depends on the metric type you chose, when you generated the template. + +### Paramers requested for all metric types + +Paramters you probably want to customize, for each application: + +| Parameter | Parameter name | Default | Description | +|-----------------|----------------|---------|--------------| +| Application Name | `ApplicationName` | (none) | Name of the Managed Flink application to control. | +| Maximum KPU | `MaxKPU` | 10 | Maximum number of KPU the autoscaler may scale out to. | +| Minimum KPU | `MinKPU` | 1 | Minimum number of KPU the autoscaler may scale in to. | +| Scale operation | `ScaleOperation` | (none) | Operation to calculate the new parallelism, when scaling out/in. `Multiply/Divide` to multiply or divide the current parallelism by the scaling factor, `Add/Subtract` to add to or substract from the current parallelism. | +| Scale-out factor | `ScaleOutFactor` | 2 | Factor added to (Add) or multiplied by (Multiply) the current parallelism, on a scale-out event. | +| Scale-out metric threshold | `ScaleOutThreshold` | 80 | Upper threshold for the metric to trigger the scaling-out alarm. | +| Scale-in factor | `ScaleInFactor` | 2 | Factor subtracted to (Subtract) or divided by (Divide) the current parallelism, on a scale-in event. | +| Scale-in metric threshold | `ScaleInThreshold` | 20 | Lower threshold for the metric to trigger the scaling-in alarm. | +| Cooling down period (grace period) after scaling | `ScalingCoolingDownPeriod` | 300 (seconds) | Cool-down time, after the application has scaled in or out, before reconsidering the scaling alarm. | +| Scaling Alarm evaluation datapoints | `ScalingEvaluationPeriod` | 5 | Number of datapoints (metric periods) considered to evaluate the scaling alarm. This is in terms of datapoints. The actual duration depends on the metric period. | +| Number of datapoints to trigger the alarm | `DataPointsToTriggerAlarm` | 5 | Number of datapoints (metric periods) beyond threshold that trigger the scaling alarm. | + +Notes +* *Cooling down period (grace period) after scaling* should be long enough to let the application stabilize after scaling in or out. + If the grace period is too short (and the scaling factor is too small), the backlog accumulated in the downtime while + the application is scaling may cause consecutive "bounce up" when the application scales up on a workload peal, + or "bouncing down and up" when the application scales down +* If you choose *Scale operation* = *Add/Subtract* and you are too conservative in the Scale-in or Scale-out factors, + the autoscaler may trigger many consecutive scaling events increasing the overall downtime. + + +Parameters you seldom want to change: + +| Parameter | Parameter name | Default | Description | +|-----------------|----------------|---------|--------------| +| Waiting time while updating | `UpdateApplicationWaitingTime` | 60 (seconds) | This is the polling interval during the scaling operation, to check whether the application is still in `UPDATING` state. We recommend not to change this unless your application takes very long time to scale, due to a very big state. | +| Metric data point duration (metric period) | `MetricPeriod` | 60 (seconds) | Duration of each individual metric datapoints stats used for the alarm. This is the period over which the statistics is calculated. You probably do not want to change the default 60 seconds. | + + +### Parameters requested for `MSK` metric type only + +These parameters are requested only if you selected metric type `MSK` at template generation. + +| Parameter | Parameter name | Default | Description | +|-----------------|----------------|---------|--------------| +| MSK cluster name | `MSKClusterName`| (none) | Name of the MSK cluster. | +| Kafka Consumer Group name | `KafkaConsumerGroupName` | (none) | Name of the Kafka Consumer Group. | +| Kafka topic name | `KafkaTopicName` | none) | Name of the topic. | + +### Parameters requested for `Kinesis` and `KinesisEFO` metric types only + +These parameters are requested only if you selected metric type `Kinesis` or `KinesisEFO` at template generation. + +| Parameter | Parameter name | Metric type | Default | Description | +|-----------------|----------------|-------------|---------|--------------| +| Kinesis Data Stream name | `KinesisDataStreamName` | `Kinesis` and `KinesisEFO` | (none) | Name of the Kinesis stream. | +| Kinesis EFO Consumer name | `KinesisConsumerName` | `KinesisEFO` | (none) | Consumer Name, for metrics like `SubscribeToShardEvent.MillisBehindLatest`. | + + +--- + +## Known limitation + +The autoscaler only supports for Managed Service for Apache Flink metrics with `Application` dimension. + +If your application uses *Monitoring metrics level* = *Application* all metrics are published to CloudWatch with `Application` +dimension only, and the autoscaler can use any metrics. +However, when *Monitoring metrics level* is set to higher than *Application*, the autoscaler can only use metrics exposed +at Application level, such as `containerCPUUtilization`, `containerMemoryUtilization`, or `heapMemoryUtilization`. + +When *Monitoring metrics level* is set to higher than *Application*, [custom metrics]((https://docs.aws.amazon.com/managed-flink/latest/java/monitoring-metrics-custom.html)) +are also published to CloudWatch with additional dimensions and cannot be used by the autoscaler. + +The autoscaler does not support defining autoscaling based on math expression. Only simple statistics are supported. diff --git a/infrastructure/AutoScaling/cdk/bin/kda-autoscaling.d.ts b/infrastructure/AutoScaling/cdk/bin/kda-autoscaling.d.ts new file mode 100644 index 0000000..c5f71d1 --- /dev/null +++ b/infrastructure/AutoScaling/cdk/bin/kda-autoscaling.d.ts @@ -0,0 +1,2 @@ +#!/usr/bin/env node +import 'source-map-support/register'; diff --git a/infrastructure/AutoScaling/cdk/bin/kda-autoscaling.js b/infrastructure/AutoScaling/cdk/bin/kda-autoscaling.js new file mode 100644 index 0000000..7b3a51a --- /dev/null +++ b/infrastructure/AutoScaling/cdk/bin/kda-autoscaling.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +require("source-map-support/register"); +const cdk = require("aws-cdk-lib"); +const kda_autoscaling_stack_1 = require("../lib/kda-autoscaling-stack"); +const app = new cdk.App(); +new kda_autoscaling_stack_1.KdaAutoscalingStack(app, 'KdaAutoscalingStack', { +/* 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 line to specialize this stack for the AWS Account + * and Region that are implied by the current CLI configuration. */ +// env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, +/* Uncomment the next line if you know exactly what Account and Region you + * want to deploy the stack to. */ +// env: { account: '123456789012', region: 'us-east-1' }, +/* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ +}); +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoia2RhLWF1dG9zY2FsaW5nLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsia2RhLWF1dG9zY2FsaW5nLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUNBLHVDQUFxQztBQUNyQyxtQ0FBbUM7QUFDbkMsd0VBQW1FO0FBRW5FLE1BQU0sR0FBRyxHQUFHLElBQUksR0FBRyxDQUFDLEdBQUcsRUFBRSxDQUFDO0FBQzFCLElBQUksMkNBQW1CLENBQUMsR0FBRyxFQUFFLHFCQUFxQixFQUFFO0FBQ2xEOztpRUFFaUU7QUFFakU7bUVBQ21FO0FBQ25FLDZGQUE2RjtBQUU3RjtrQ0FDa0M7QUFDbEMseURBQXlEO0FBRXpELDhGQUE4RjtDQUMvRixDQUFDLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIjIS91c3IvYmluL2VudiBub2RlXG5pbXBvcnQgJ3NvdXJjZS1tYXAtc3VwcG9ydC9yZWdpc3Rlcic7XG5pbXBvcnQgKiBhcyBjZGsgZnJvbSAnYXdzLWNkay1saWInO1xuaW1wb3J0IHsgS2RhQXV0b3NjYWxpbmdTdGFjayB9IGZyb20gJy4uL2xpYi9rZGEtYXV0b3NjYWxpbmctc3RhY2snO1xuXG5jb25zdCBhcHAgPSBuZXcgY2RrLkFwcCgpO1xubmV3IEtkYUF1dG9zY2FsaW5nU3RhY2soYXBwLCAnS2RhQXV0b3NjYWxpbmdTdGFjaycsIHtcbiAgLyogSWYgeW91IGRvbid0IHNwZWNpZnkgJ2VudicsIHRoaXMgc3RhY2sgd2lsbCBiZSBlbnZpcm9ubWVudC1hZ25vc3RpYy5cbiAgICogQWNjb3VudC9SZWdpb24tZGVwZW5kZW50IGZlYXR1cmVzIGFuZCBjb250ZXh0IGxvb2t1cHMgd2lsbCBub3Qgd29yayxcbiAgICogYnV0IGEgc2luZ2xlIHN5bnRoZXNpemVkIHRlbXBsYXRlIGNhbiBiZSBkZXBsb3llZCBhbnl3aGVyZS4gKi9cblxuICAvKiBVbmNvbW1lbnQgdGhlIG5leHQgbGluZSB0byBzcGVjaWFsaXplIHRoaXMgc3RhY2sgZm9yIHRoZSBBV1MgQWNjb3VudFxuICAgKiBhbmQgUmVnaW9uIHRoYXQgYXJlIGltcGxpZWQgYnkgdGhlIGN1cnJlbnQgQ0xJIGNvbmZpZ3VyYXRpb24uICovXG4gIC8vIGVudjogeyBhY2NvdW50OiBwcm9jZXNzLmVudi5DREtfREVGQVVMVF9BQ0NPVU5ULCByZWdpb246IHByb2Nlc3MuZW52LkNES19ERUZBVUxUX1JFR0lPTiB9LFxuXG4gIC8qIFVuY29tbWVudCB0aGUgbmV4dCBsaW5lIGlmIHlvdSBrbm93IGV4YWN0bHkgd2hhdCBBY2NvdW50IGFuZCBSZWdpb24geW91XG4gICAqIHdhbnQgdG8gZGVwbG95IHRoZSBzdGFjayB0by4gKi9cbiAgLy8gZW52OiB7IGFjY291bnQ6ICcxMjM0NTY3ODkwMTInLCByZWdpb246ICd1cy1lYXN0LTEnIH0sXG5cbiAgLyogRm9yIG1vcmUgaW5mb3JtYXRpb24sIHNlZSBodHRwczovL2RvY3MuYXdzLmFtYXpvbi5jb20vY2RrL2xhdGVzdC9ndWlkZS9lbnZpcm9ubWVudHMuaHRtbCAqL1xufSk7Il19 \ No newline at end of file diff --git a/infrastructure/AutoScaling/cdk/bin/kda-autoscaling.ts b/infrastructure/AutoScaling/cdk/bin/kda-autoscaling.ts new file mode 100644 index 0000000..740debc --- /dev/null +++ b/infrastructure/AutoScaling/cdk/bin/kda-autoscaling.ts @@ -0,0 +1,19 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import {KdaAutoscalingStack} from '../lib/kda-autoscaling-stack'; +import {DefaultStackSynthesizer} from "aws-cdk-lib"; + +const app = new cdk.App(); + +const synthDate = new Date().toISOString().split('T')[0]; + +new KdaAutoscalingStack(app, 'KdaAutoscalingStack', { + description: `MSF autoscaler (${synthDate})`, + + synthesizer: new DefaultStackSynthesizer({ + generateBootstrapVersionRule: false + }) +}); + + diff --git a/infrastructure/AutoScaling/cdk/cdk.json b/infrastructure/AutoScaling/cdk/cdk.json new file mode 100644 index 0000000..b518399 --- /dev/null +++ b/infrastructure/AutoScaling/cdk/cdk.json @@ -0,0 +1,53 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/kda-autoscaling.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "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-iam:standardizedServicePrincipals": 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 + } +} diff --git a/infrastructure/AutoScaling/cdk/jest.config.js b/infrastructure/AutoScaling/cdk/jest.config.js new file mode 100644 index 0000000..08263b8 --- /dev/null +++ b/infrastructure/AutoScaling/cdk/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + } +}; diff --git a/infrastructure/AutoScaling/cdk/lib/kda-autoscaling-stack.d.ts b/infrastructure/AutoScaling/cdk/lib/kda-autoscaling-stack.d.ts new file mode 100644 index 0000000..f5b4bf5 --- /dev/null +++ b/infrastructure/AutoScaling/cdk/lib/kda-autoscaling-stack.d.ts @@ -0,0 +1,5 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +export declare class KdaAutoscalingStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps); +} diff --git a/infrastructure/AutoScaling/cdk/lib/kda-autoscaling-stack.js b/infrastructure/AutoScaling/cdk/lib/kda-autoscaling-stack.js new file mode 100644 index 0000000..a31c4d2 --- /dev/null +++ b/infrastructure/AutoScaling/cdk/lib/kda-autoscaling-stack.js @@ -0,0 +1,17 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.KdaAutoscalingStack = void 0; +const cdk = require("aws-cdk-lib"); +// import * as sqs from 'aws-cdk-lib/aws-sqs'; +class KdaAutoscalingStack extends cdk.Stack { + constructor(scope, id, props) { + super(scope, id, props); + // The code that defines your stack goes here + // example resource + // const queue = new sqs.Queue(this, 'KdaAutoscalingQueue', { + // visibilityTimeout: cdk.Duration.seconds(300) + // }); + } +} +exports.KdaAutoscalingStack = KdaAutoscalingStack; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoia2RhLWF1dG9zY2FsaW5nLXN0YWNrLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsia2RhLWF1dG9zY2FsaW5nLXN0YWNrLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFBLG1DQUFtQztBQUVuQyw4Q0FBOEM7QUFFOUMsTUFBYSxtQkFBb0IsU0FBUSxHQUFHLENBQUMsS0FBSztJQUNoRCxZQUFZLEtBQWdCLEVBQUUsRUFBVSxFQUFFLEtBQXNCO1FBQzlELEtBQUssQ0FBQyxLQUFLLEVBQUUsRUFBRSxFQUFFLEtBQUssQ0FBQyxDQUFDO1FBRXhCLDZDQUE2QztRQUU3QyxtQkFBbUI7UUFDbkIsNkRBQTZEO1FBQzdELGlEQUFpRDtRQUNqRCxNQUFNO0lBQ1IsQ0FBQztDQUNGO0FBWEQsa0RBV0MiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBjZGsgZnJvbSAnYXdzLWNkay1saWInO1xuaW1wb3J0IHsgQ29uc3RydWN0IH0gZnJvbSAnY29uc3RydWN0cyc7XG4vLyBpbXBvcnQgKiBhcyBzcXMgZnJvbSAnYXdzLWNkay1saWIvYXdzLXNxcyc7XG5cbmV4cG9ydCBjbGFzcyBLZGFBdXRvc2NhbGluZ1N0YWNrIGV4dGVuZHMgY2RrLlN0YWNrIHtcbiAgY29uc3RydWN0b3Ioc2NvcGU6IENvbnN0cnVjdCwgaWQ6IHN0cmluZywgcHJvcHM/OiBjZGsuU3RhY2tQcm9wcykge1xuICAgIHN1cGVyKHNjb3BlLCBpZCwgcHJvcHMpO1xuXG4gICAgLy8gVGhlIGNvZGUgdGhhdCBkZWZpbmVzIHlvdXIgc3RhY2sgZ29lcyBoZXJlXG5cbiAgICAvLyBleGFtcGxlIHJlc291cmNlXG4gICAgLy8gY29uc3QgcXVldWUgPSBuZXcgc3FzLlF1ZXVlKHRoaXMsICdLZGFBdXRvc2NhbGluZ1F1ZXVlJywge1xuICAgIC8vICAgdmlzaWJpbGl0eVRpbWVvdXQ6IGNkay5EdXJhdGlvbi5zZWNvbmRzKDMwMClcbiAgICAvLyB9KTtcbiAgfVxufVxuIl19 \ No newline at end of file diff --git a/infrastructure/AutoScaling/cdk/lib/kda-autoscaling-stack.ts b/infrastructure/AutoScaling/cdk/lib/kda-autoscaling-stack.ts new file mode 100644 index 0000000..1848ccd --- /dev/null +++ b/infrastructure/AutoScaling/cdk/lib/kda-autoscaling-stack.ts @@ -0,0 +1,520 @@ +import * as cdk from 'aws-cdk-lib'; +import {Aws, aws_events_targets, CfnCondition, CfnJson, CfnParameter, Fn, Token} from 'aws-cdk-lib'; +import {Construct} from 'constructs'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; +import * as events from 'aws-cdk-lib/aws-events'; +import * as sfn from 'aws-cdk-lib/aws-stepfunctions'; +import * as tasks from 'aws-cdk-lib/aws-stepfunctions-tasks'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import {readFileSync} from 'fs'; + +function removePrefix(str: string, prefix: string): string { + const regex = new RegExp(`^${prefix}`); + return str.replace(regex, ''); +} + +enum MetricStats { + Maximum = 'Maximum', + Minimum = 'Minimum', + Average = 'Average', + Sum = 'Sum', +} + +enum MetricType { + KinesisAnalytics = "KinesisAnalytics", + MSK = "MSK", + Kinesis = "Kinesis", + KinesisEFO = "KinesisEFO" +} + + +export class KdaAutoscalingStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + /// CDK CLI parameters (at synth) + + const metricTypeStr: string = this.node.tryGetContext("type") || "KinesisAnalytics"; + if (!Object.values(MetricType).includes(metricTypeStr as MetricType)) { + console.error(`Invalid metric type: "${metricTypeStr}". Expected one of ${Object.values(MetricType).join(", ")}`); + process.exit(1); + } + const metricType: MetricType = metricTypeStr as MetricType; + + const autoscaleMetricName_: string | undefined = this.node.tryGetContext("metric"); + const autoscaleMetricStat_: string = this.node.tryGetContext("stat") || "Maximum"; + const triggerMetricPeriod_: number = Number(this.node.tryGetContext('period') ?? 60); + + /// Validate CDK parameters + + if (autoscaleMetricName_ === undefined) { + console.error("Metric name not defined") + process.exit(1) + } + + + if (!(autoscaleMetricStat_ in MetricStats)) { + console.error(`Invalid metric stat. Must be one of ${Object.values(MetricStats).join(", ")}`) + process.exit(1) + } + + + /// Base CFN parameters, always present + + const flinkApplicationName = new CfnParameter(this, 'ApplicationName', { + type: "String", + description: 'The name of the Amazon Managed Apache Flink Application to control' + }); + + const maxKPU = new CfnParameter(this, 'MaxKPU', { + type: "Number", + description: 'Maximum number of KPUs the Managed Flink Application may scale-out to', + default: '10' + }); + + const minKPU = new CfnParameter(this, 'MinKPU', { + type: "Number", + description: 'Minimum number of KPUs the Managed Flink Application may scale-in to', + default: '1' + }); + + const scaleMetricPeriod = new CfnParameter(this, 'MetricPeriod', { + type: "Number", + description: "Duration of each individual data point for an alarm (Seconds)", + default: 60, + allowedValues: ["1", "5", "10", "30", "60", "120", "180", "240", "300"], + }); + + const scaleInMetricThreshold = new CfnParameter(this, 'ScaleInThreshold', { + type: "Number", + description: "Lower threshold for the metric to trigger the scaling-in alarm", + default: 20 + }); + + const scaleOutMetricThreshold = new CfnParameter(this, 'ScaleOutThreshold', { + type: "Number", + description: "Upper threshold for the metric to trigger the scaling-out alarm", + default: 80 + }); + + const autoscaleEvaluationPeriod = new CfnParameter(this, 'ScalingEvaluationPeriod', { + type: "Number", + description: "Number of datapoints (metric periods) considered to evaluate the scaling alarm", + default: 5 + }); + + const autoscaleDataPointsAlarm = new CfnParameter(this, 'DataPointsToTriggerAlarm', { + type: "Number", + description: "Number of datapoints (metric periods) beyond threshold that trigger the scaling alarm", + default: 5 + }); + + const autoscaleCoolingDownPeriod = new CfnParameter(this, 'ScalingCoolingDownPeriod', { + type: "Number", + description: "Cool-down time, after the application has scaled in or out, before reconsidering the scaling alarm (in seconds)", + default: 300 + }); + + const updateWaitingTime = new CfnParameter(this, 'UpdateApplicationWaitingTime', { + type: "Number", + description: "Time given to the application to complete updating, in seconds", + default: 60 + }); + + const scaleInFactor = new CfnParameter(this, 'ScaleInFactor', { + type: "Number", + description: "Factor subtracted to (Subtract) or divided by (Divide) the current parallelism, on a scale-in event", + default: 2 + }); + + const scaleOutFactor = new CfnParameter(this, 'ScaleOutFactor', { + type: "Number", + description: "Factor added to (Add) or multiplied by (Multiply) the current parallelism, on a scale-out event", + default: 2 + }); + + const scaleOperation = new CfnParameter(this, "ScaleOperation", { + type: "String", + description: "Operation to calculate the new parallelism (Multiply/Divide or Add/Subtract) when scaling out/in", + allowedValues: ["Multiply/Divide", "Add/Subtract"] + }); + + // Define CFN template interface with the base parameter, always present + this.templateOptions.metadata = { + 'AWS::CloudFormation::Interface': { + ParameterGroups: [ + { + Label: {default: `Amazon Managed Flink application scaling (metric type: ${metricType}, metric: ${autoscaleMetricName_}, stat: ${autoscaleMetricStat_})`}, + Parameters: [flinkApplicationName.logicalId, maxKPU.logicalId, minKPU.logicalId] + }, + { + Label: {default: 'CloudWatch Scaling Alarm'}, + Parameters: [scaleMetricPeriod.logicalId, autoscaleEvaluationPeriod.logicalId, autoscaleDataPointsAlarm.logicalId] + }, + { + Label: {default: 'Autoscaling cycle'}, + Parameters: [autoscaleCoolingDownPeriod.logicalId, updateWaitingTime.logicalId] + }, + { + Label: {default: 'Scaling parameters'}, + Parameters: [scaleOperation.logicalId, scaleOutFactor.logicalId, scaleOutMetricThreshold.logicalId, scaleInFactor.logicalId, scaleInMetricThreshold.logicalId] + }, + ], + ParameterLabels: { + ApplicationName: {default: 'Application Name'}, + MaxKPU: {default: 'Maximum KPU'}, + MinKPU: {default: 'Minimum KPU'}, + MetricPeriod: {default: 'Metric data point duration (sec)'}, + ScalingEvaluationPeriod: {default: 'Scaling alarm evaluation datapoints'}, + DataPointsToTriggerAlarm: {default: 'Number of datapoints beyond threshold to trigger the alarm'}, + ScalingCoolingDownPeriod: {default: 'Cooling down period (grace period) after scaling (sec)'}, + UpdateApplicationWaitingTime: {default: 'Waiting time while updating (sec)'}, + ScaleInFactor: {default: 'Scale-in factor'}, + ScaleOutFactor: {default: 'Scale-out factor'}, + ScaleOperation: {default: 'Scale operation (mandatory)'}, + ScaleInThreshold: {default: 'Scale-in metric threshold'}, + ScaleOutThreshold: {default: 'Scale-out metric threshold'}, + } + } + } + + const applicationName = flinkApplicationName.valueAsString.trim(); + let triggerMetricNamespace; + let kinesisDataStreamName: cdk.CfnParameter + let kinesisConsumerName: cdk.CfnParameter + let triggerMetricDimensions: cdk.aws_cloudwatch.DimensionsMap; + + // Conditionally add additional CFN parameters and labels, and generate the metric parameters, depending on the metric type + switch (metricType) { + + case MetricType.KinesisAnalytics: + // Metric parameters + triggerMetricNamespace = "AWS/KinesisAnalytics"; + triggerMetricDimensions = { + Application: applicationName + } + + break; + + case MetricType.Kinesis: + /// Conditionally add KinesisStreamName CFN Parameter + + // CFN parameters + kinesisDataStreamName = new CfnParameter(this, "KinesisDataStreamName", { + type: "String", + description: "Name of the Kinesis Data Stream" + }); + + + // CFN parameter group + this.templateOptions.metadata['AWS::CloudFormation::Interface'].ParameterGroups.push({ + Label: {default: 'Kinesis metrics'}, + Parameters: [kinesisDataStreamName.logicalId] + }); + + // CFN parameter labels + this.templateOptions.metadata['AWS::CloudFormation::Interface'].ParameterLabels.KinesisDataStreamName = { + default: 'Kinesis Data Stream name' + }; + + // Metric parameters + triggerMetricNamespace = "AWS/Kinesis"; + triggerMetricDimensions = { + StreamName: kinesisDataStreamName.valueAsString.trim() + } + break; + + case MetricType.KinesisEFO: + /// Conditionally add KinesisStreamName and ConsumerName CFN Parameters + + // CFN parameters + kinesisDataStreamName = new CfnParameter(this, "KinesisDataStreamName", { + type: "String", + description: "Name of the Kinesis Data Stream" + }); + kinesisConsumerName = new CfnParameter(this, "KinesisConsumerName", { + type: "String", + description: "EFO Consumer Name" + }); + + // CFN parameter group + this.templateOptions.metadata['AWS::CloudFormation::Interface'].ParameterGroups.push({ + Label: {default: 'Kinesis EFO consumer metrics'}, + Parameters: [kinesisDataStreamName.logicalId, kinesisConsumerName.logicalId] + }); + + // CFN parameter labels + this.templateOptions.metadata['AWS::CloudFormation::Interface'].ParameterLabels.KinesisDataStreamName = { + default: 'Kinesis Data Stream name' + }; + this.templateOptions.metadata['AWS::CloudFormation::Interface'].ParameterLabels.KinesisConsumerName = { + default: 'Kinesis EFO Consumer name (optional)' + }; + + + // Kinesis EFO consumer metric parameters + triggerMetricNamespace = "AWS/Kinesis"; + triggerMetricDimensions = { + StreamName: kinesisDataStreamName.valueAsString.trim(), + ConsumerName: kinesisConsumerName.valueAsString.trim() + } + break; + + case MetricType.MSK: + // Conditionally add CFN parameters and labels for MSK Consumer Group metrics + const mskClusterName = new CfnParameter(this, "MSKClusterName", { + type: "String", + description: "If you choose topic metrics (MaxOffsetLag, SumOffsetLag or EstimatedMaxTimeLag) as metric to scale, you need to provide the name of the MSK Cluster for monitoring" + }); + const kafkaTopicName = new CfnParameter(this, "KafkaTopicName", { + type: "String", + description: "If you choose topic metrics (MaxOffsetLag, SumOffsetLag or EstimatedMaxTimeLag) as metric to scale, you need to provide the Kafka Topic for monitoring" + }); + const kafkaConsumerGroup = new CfnParameter(this, "KafkaConsumerGroupName", { + type: "String", + description: "If you choose topic metrics (MaxOffsetLag, SumOffsetLag or EstimatedMaxTimeLag) as metric to scale, you need to provide the Consumer Group name for monitoring" + }); + + this.templateOptions.metadata['AWS::CloudFormation::Interface'].ParameterGroups.push({ + Label: {default: 'MSK Consumer Group metrics'}, + Parameters: [mskClusterName.logicalId, kafkaTopicName.logicalId, kafkaConsumerGroup.logicalId,] + }); + + this.templateOptions.metadata['AWS::CloudFormation::Interface'].ParameterLabels.MSKClusterName = { + default: 'MSK cluster name' + }; + this.templateOptions.metadata['AWS::CloudFormation::Interface'].ParameterLabels.KafkaConsumerGroupName = { + default: 'Kafka consumer group name' + }; + this.templateOptions.metadata['AWS::CloudFormation::Interface'].ParameterLabels.KafkaTopicName = { + default: 'Kafka topic name' + }; + + // MSK Consumer Group metric parameters + triggerMetricNamespace = "AWS/Kafka"; + triggerMetricDimensions = { + "Cluster Name": mskClusterName.valueAsString, + "Consumer Group": kafkaConsumerGroup.valueAsString, + "Topic": kafkaTopicName.valueAsString + } + break; + + default: + throw new Error("Invalid metric type: " + metricType); + + } + + + /// Metric + const triggerMetric: cdk.aws_cloudwatch.Metric = new cloudwatch.Metric({ + namespace: triggerMetricNamespace, + metricName: autoscaleMetricName_, + statistic: autoscaleMetricStat_, + period: cdk.Duration.seconds(triggerMetricPeriod_), + dimensionsMap: triggerMetricDimensions + } + ); + + + /// Alarms + const scaleOutAlarm: cdk.aws_cloudwatch.Alarm = new cloudwatch.Alarm(this, `Scale-out alarm on ${autoscaleMetricName_}`, { + comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, + alarmName: `ScaleOutAlarm-${applicationName}`, + threshold: scaleOutMetricThreshold.valueAsNumber, + evaluationPeriods: autoscaleEvaluationPeriod.valueAsNumber, + datapointsToAlarm: autoscaleDataPointsAlarm.valueAsNumber, + metric: triggerMetric + }); + const scaleInAlarm: cdk.aws_cloudwatch.Alarm = new cloudwatch.Alarm(this, `Scale-in alarm on ${autoscaleMetricName_}`, { + comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD, + alarmName: `ScaleInAlarm-${applicationName}`, + threshold: scaleInMetricThreshold.valueAsNumber, + evaluationPeriods: autoscaleEvaluationPeriod.valueAsNumber, + datapointsToAlarm: autoscaleDataPointsAlarm.valueAsNumber, + metric: triggerMetric + }); + + + /// Rule + const scaleRule: cdk.aws_events.Rule = new events.Rule(this, `Rule on ${autoscaleMetricName_}`, { + ruleName: `ScalingRule-${applicationName}`, + eventPattern: { + source: ["aws.cloudwatch"], + detailType: ["CloudWatch Alarm State Change"], + resources: [scaleOutAlarm.alarmArn, scaleInAlarm.alarmArn], + detail: { + state: { + "value": ["ALARM"] + } + } + }, + }); + + + /// Lambda + + // Allow Lambda to log to CW + const accessCWLogsPolicy: cdk.aws_iam.PolicyDocument = new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + resources: [`arn:aws:logs:${this.region}:${this.account}:log-group:/aws/lambda/*`], + actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], + }), + ], + }); + + // Allow Lambda to describe and update kinesisanalytics application + const kdaAccessPolicy: cdk.aws_iam.PolicyDocument = new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + resources: [`arn:aws:kinesisanalytics:${this.region}:${this.account}:application/${applicationName}`], + actions: ['kinesisanalytics:DescribeApplication', 'kinesisAnalytics:UpdateApplication'] + }), + ], + }); + + // Lambda IAM role + const lambdaRole: cdk.aws_iam.Role = new iam.Role(this, 'Scaling Function Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + description: 'Lambda Scaling Role', + inlinePolicies: { + AccessCWLogsPolicy: accessCWLogsPolicy, + AccessKDA: kdaAccessPolicy, + }, + }); + + // Create Lambda function + const lambdaCode = readFileSync("resources/scaling/scaling.py", "utf-8") + const lambdaFunction: cdk.aws_lambda.Function = new lambda.Function(this, 'Scaling Function', { + runtime: lambda.Runtime.PYTHON_3_9, + handler: "index.handler", + role: lambdaRole, + code: lambda.Code.fromInline(lambdaCode), + environment: { + flinkApplicationName: applicationName, + scaleInFactor: scaleInFactor.valueAsString, + scaleOutFactor: scaleOutFactor.valueAsString, + scaleOperation: scaleOperation.valueAsString, + maxKPU: maxKPU.valueAsString, + minKPU: minKPU.valueAsString + } + }) + + + /// Step Functions + + // IAM Policy to be added to StepFunction to allow KinesisAnalytics DescribeApplication. + // Note that CallAwsService is supposed to add the correct IAM permissions automatically, based on `service`, + // `action`, and `iamResources`. However, because the service API is "kinesisanalyticsv2" while the IAM action + // prefix is "kinesisanalytics" this does not work properly + const kdaDescribeApplicationPolicyStatement: cdk.aws_iam.PolicyStatement = new iam.PolicyStatement({ + resources: [`arn:aws:kinesisanalytics:${this.region}:${this.account}:application/${applicationName}`], + actions: ['kinesisanalytics:DescribeApplication'] + }); + + const describeKdaApplicationTask: cdk.aws_stepfunctions_tasks.CallAwsService = new tasks.CallAwsService(this, "Describe Application", { + service: 'kinesisanalyticsv2', // API is "v2" + action: 'describeApplication', + iamResources: [`arn:aws:kinesisanalytics:${this.region}:${this.account}:application/${applicationName}`], + additionalIamStatements: [kdaDescribeApplicationPolicyStatement], // ensure IAM policy has the correct permissions + parameters: { + "ApplicationName": applicationName + }, + resultPath: "$.TaskResult" + }) + + const scalingChoice: cdk.aws_stepfunctions.Choice = new sfn.Choice(this, 'Still updating?'); + const lambdaChoice: cdk.aws_stepfunctions.Choice = new sfn.Choice(this, 'MinMax KPU Reached?'); + const alarmChoice: cdk.aws_stepfunctions.Choice = new sfn.Choice(this, 'Still in alarm?'); + + const waitUpdate: cdk.aws_stepfunctions.Wait = new sfn.Wait(this, "Waiting for application to finish updating", { + time: sfn.WaitTime.duration(cdk.Duration.seconds(updateWaitingTime.valueAsNumber)) + }); + const waitCoolingPeriod: cdk.aws_stepfunctions.Wait = new sfn.Wait(this, "Cooling Period for Alarm", { + time: sfn.WaitTime.duration(cdk.Duration.seconds(autoscaleCoolingDownPeriod.valueAsNumber)) + }); + + // Add conditions with .when() + const successState: cdk.aws_stepfunctions.Pass = new sfn.Pass(this, 'SuccessState'); + + const lambdaTask: cdk.aws_stepfunctions_tasks.LambdaInvoke = new tasks.LambdaInvoke(this, 'Invoke Lambda Function', { + lambdaFunction: lambdaFunction, + resultPath: "$.TaskResult" + }) + + const describeKdaApplicationAfterScalingTask: cdk.aws_stepfunctions_tasks.CallAwsService = new tasks.CallAwsService(this, "Describe Application after scaling", { + service: 'kinesisanalyticsv2', // API is "v2" + action: 'describeApplication', + iamResources: [`arn:aws:kinesisanalytics:${this.region}:${this.account}:application/${applicationName}`], + additionalIamStatements: [kdaDescribeApplicationPolicyStatement], // ensure IAM policy has the correct permissions + parameters: { + "ApplicationName": applicationName + }, + resultPath: "$.TaskResult", + }) + + + const describeCwAlarmTask: cdk.aws_stepfunctions_tasks.CallAwsService = new tasks.CallAwsService(this, "Describe Alarm after waiting", { + service: 'cloudwatch', + action: 'describeAlarms', + iamResources: [`arn:aws:cloudwatch:${this.region}:${this.account}:alarm:*`], + parameters: { + "AlarmNames.$": "States.Array($.detail.alarmName)", + "AlarmTypes": ['CompositeAlarm', 'MetricAlarm'] + }, + resultPath: "$.TaskResult" + }) + + const definition: sfn.Chain = describeKdaApplicationTask + .next(lambdaTask) + .next(lambdaChoice + .when(sfn.Condition.numberEquals('$.TaskResult.Payload.body', 1), successState) + .otherwise(waitUpdate + .next(describeKdaApplicationAfterScalingTask) + .next(scalingChoice + .when(sfn.Condition.stringEquals('$.TaskResult.ApplicationDetail.ApplicationStatus', 'UPDATING'), waitUpdate) + .otherwise(waitCoolingPeriod + .next(describeCwAlarmTask.next(alarmChoice + .when(sfn.Condition.stringEquals('$.TaskResult.MetricAlarms[0].StateValue', 'ALARM'), describeKdaApplicationTask) + .otherwise(successState))))))); + + const stateMachine: sfn.StateMachine = new sfn.StateMachine(this, 'StateMachine', { + definitionBody: sfn.DefinitionBody.fromChainable(definition) + }); + + const stepFunctionPolicy: cdk.aws_iam.PolicyDocument = new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + resources: [stateMachine.stateMachineArn], + actions: ['states:StartExecution'] + }), + ], + }); + + const eventBridgeRole: cdk.aws_iam.Role = new iam.Role(this, 'EventBridge Role', { + assumedBy: new iam.ServicePrincipal('events.amazonaws.com'), + description: 'EventBridge Role', + inlinePolicies: { + AccessStepFunctionsPolicy: stepFunctionPolicy + }, + }); + + scaleRule.addTarget(new aws_events_targets.SfnStateMachine(stateMachine, { + role: eventBridgeRole + })); + + + /// CFN Outputs + new cdk.CfnOutput(this, "Autoscaler Metric Name", {value: autoscaleMetricName_}); + new cdk.CfnOutput(this, "Autoscaler Metric Statistic", {value: autoscaleMetricStat_}); + new cdk.CfnOutput(this, "Application name", {value: applicationName}); + new cdk.CfnOutput(this, "CW Metric Namespace", {value: triggerMetricNamespace}); + new cdk.CfnOutput(this, "CW Metric Dimensions", {value: JSON.stringify(triggerMetricDimensions)}); + + new cdk.CfnOutput(this, "Scale-in alarm name", {value: scaleInAlarm.alarmName}); + new cdk.CfnOutput(this, "Scale-out alarm name", {value: scaleOutAlarm.alarmName}); + + } +} + diff --git a/infrastructure/AutoScaling/cdk/package-lock.json b/infrastructure/AutoScaling/cdk/package-lock.json new file mode 100644 index 0000000..c6fb845 --- /dev/null +++ b/infrastructure/AutoScaling/cdk/package-lock.json @@ -0,0 +1,4152 @@ +{ + "name": "kda-autoscaling", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "kda-autoscaling", + "version": "0.1.0", + "dependencies": { + "aws-cdk-lib": "^2.192.0", + "constructs": "^10.4.2", + "source-map-support": "^0.5.21" + }, + "bin": { + "kda-autoscaling": "bin/kda-autoscaling.js" + }, + "devDependencies": { + "@types/jest": "^29.5.1", + "@types/node": "20.1.7", + "aws-cdk": "^2.1001.0", + "jest": "^29.5.0", + "ts-jest": "^29.1.0", + "ts-node": "^10.9.1", + "typescript": "~5.0.4" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.233", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.233.tgz", + "integrity": "sha512-OH5ZN1F/0wwOUwzVUSvE0/syUOi44H9the6IG16anlSptfeQ1fvduJazZAKRuJLtautPbiqxllyOrtWh6LhX8A==" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", + "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "41.2.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-41.2.0.tgz", + "integrity": "sha512-JaulVS6z9y5+u4jNmoWbHZRs9uGOnmn/ktXygNWKNu1k6lF3ad4so3s18eRu15XCbUIomxN9WPYT6Ehh7hzONw==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.1" + }, + "engines": { + "node": ">= 14.15.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.1", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz", + "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.1.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.7.tgz", + "integrity": "sha512-WCuw/o4GSwDGMoonES8rcvwsig77dGCMbZDrZr2x4ZZiNW4P/gcoZXe/0twgtobcTkmg9TuKflxYL/DuwDyJzg==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "node_modules/aws-cdk": { + "version": "2.1012.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1012.0.tgz", + "integrity": "sha512-C6jSWkqP0hkY2Cs300VJHjspmTXDTMfB813kwZvRbd/OsKBfTBJBbYU16VoLAp1LVEOnQMf8otSlaSgzVF0X9A==", + "dev": true, + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 14.15.0" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.192.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.192.0.tgz", + "integrity": "sha512-Y9BAlr9a4QsEsamKc2cOGzX8DpVSOh94wsrMSGRXT0bZaqmixhhmT7WYCrT1KX4MU3gYk3OiwY2BbNyWaNE8Fg==", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "dependencies": { + "@aws-cdk/asset-awscli-v1": "^2.2.229", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^41.0.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.0", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^3.1.2", + "punycode": "^2.3.1", + "semver": "^7.7.1", + "table": "^6.9.0", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "constructs": "^10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.17.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.11", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.0.6", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.7.1", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001715", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", + "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/constructs": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", + "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.144", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.144.tgz", + "integrity": "sha512-eJIaMRKeAzxfBSxtjYnoIAw/tdD6VIH6tHBZepZnAbE3Gyqqs5mGN87DvcldPUbVkIljTK8pY0CMcUljP64lfQ==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-jest": { + "version": "29.3.2", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.2.tgz", + "integrity": "sha512-bJJkrWc6PjFVz5g2DGCNUo8z7oFEYaz1xP1NpeDU7KNLMWPpEyV8Chbpkn8xjzgRDpQhnGMyvyldoL7h8JXyug==", + "dev": true, + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.1", + "type-fest": "^4.39.1", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.1.tgz", + "integrity": "sha512-9YvLNnORDpI+vghLU/Nf+zSv0kL47KbVJ1o3sKgoTefl6i+zebxbiDQWoe/oWWqPhIgQdRZRT1KA9sCPL810SA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/infrastructure/AutoScaling/cdk/package.json b/infrastructure/AutoScaling/cdk/package.json new file mode 100644 index 0000000..7942d36 --- /dev/null +++ b/infrastructure/AutoScaling/cdk/package.json @@ -0,0 +1,27 @@ +{ + "name": "kda-autoscaling", + "version": "0.1.0", + "bin": { + "kda-autoscaling": "bin/kda-autoscaling.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk" + }, + "devDependencies": { + "@types/jest": "^29.5.1", + "@types/node": "20.1.7", + "aws-cdk": "^2.1001.0", + "jest": "^29.5.0", + "ts-jest": "^29.1.0", + "ts-node": "^10.9.1", + "typescript": "~5.0.4" + }, + "dependencies": { + "aws-cdk-lib": "^2.192.0", + "constructs": "^10.4.2", + "source-map-support": "^0.5.21" + } +} diff --git a/infrastructure/AutoScaling/cdk/resources/scaling/scaling.py b/infrastructure/AutoScaling/cdk/resources/scaling/scaling.py new file mode 100644 index 0000000..a1fe7e8 --- /dev/null +++ b/infrastructure/AutoScaling/cdk/resources/scaling/scaling.py @@ -0,0 +1,129 @@ +import boto3 +import json +import os + +client_kda = boto3.client('kinesisanalyticsv2') +client_ssm = boto3.client('ssm') +client_cloudwatch = boto3.client('cloudwatch') +client_cloudformation = boto3.client('cloudformation') +client_aas = boto3.client('application-autoscaling') +client_iam = boto3.resource('iam') + +def update_parallelism(context, desiredCapacity, resourceName, appVersionId, currentParallelismPerKPU): + try: + # Compute the new parallelism based on the desired capacity and parallelismPerKPU + newParallelism = desiredCapacity * currentParallelismPerKPU + newParallelismPerKPU = currentParallelismPerKPU # Assume no change for now + + # Call KDA service to update parallelism + response = client_kda.update_application( + ApplicationName=resourceName, + CurrentApplicationVersionId=appVersionId, + ApplicationConfigurationUpdate={ + 'FlinkApplicationConfigurationUpdate': { + 'ParallelismConfigurationUpdate': { + 'ConfigurationTypeUpdate': 'CUSTOM', + 'ParallelismUpdate': int(newParallelism), + 'ParallelismPerKPUUpdate': int(newParallelismPerKPU), + 'AutoScalingEnabledUpdate': False + } + } + } + ) + + print("In update_parallelism; response: ") + print(response) + scalingStatus = "InProgress" + + except Exception as e: + print(e) + scalingStatus = "Failed" + + return scalingStatus + + +def response_function(status_code, response_body): + return_json = { + 'statusCode': status_code, + 'body': response_body, + 'headers': { + 'Content-Type': 'application/json', + }, + } + # log response + print(return_json) + return return_json + + +def handler(event, context): + print(event) + resourceName = os.environ['flinkApplicationName'] + scaleInFactor = int(os.environ['scaleInFactor']) + scaleOutFactor = int(os.environ['scaleOutFactor']) + scaleOperation = os.environ['scaleOperation'] + alarm_status = event['detail']['state']['value'] + alarm_name = event['detail']['alarmName'] + minKPU = int(os.environ['minKPU']) + maxKPU = int(os.environ['maxKPU']) + stop_scale = 0 + + # get details for the KDA app in question + appVersion = event["TaskResult"]["ApplicationDetail"]["ApplicationVersionId"] + applicationStatus = event["TaskResult"]["ApplicationDetail"]["ApplicationStatus"] + parallelism = event["TaskResult"]["ApplicationDetail"]["ApplicationConfigurationDescription"][ + "FlinkApplicationConfigurationDescription"]["ParallelismConfigurationDescription"]["Parallelism"] + parallelismPerKPU = event["TaskResult"]["ApplicationDetail"]["ApplicationConfigurationDescription"]["FlinkApplicationConfigurationDescription"]["ParallelismConfigurationDescription"]["ParallelismPerKPU"] + actualCapacity = (parallelism + parallelismPerKPU - 1) // parallelismPerKPU + print(f"Actual Capacity: {actualCapacity}") + + if applicationStatus == "UPDATING": + scalingStatus = "InProgress" + elif applicationStatus == "RUNNING": + scalingStatus = "Successful" + + # Scaling out scenario (ScaleOut) + if "ScaleOut" in alarm_name and alarm_status == 'ALARM' and applicationStatus == 'RUNNING': + if actualCapacity < maxKPU: + if scaleOperation == 'Multiply/Divide': + desiredCapacity = actualCapacity * scaleOutFactor + print(f"Scaling out, desired capacity: {desiredCapacity}") + else: + desiredCapacity = actualCapacity + scaleOutFactor + print(f"Scaling out, desired capacity: {desiredCapacity}") + if desiredCapacity < maxKPU: + update_parallelism(context, desiredCapacity, resourceName, appVersion, parallelismPerKPU) + else: + update_parallelism(context, maxKPU, resourceName, appVersion, parallelismPerKPU) + print("Application will be set to Max KPU") + stop_scale = 1 + else: + desiredCapacity = actualCapacity + print("Application is already equal or above to Max KPU") + stop_scale = 1 + + # Scaling in scenario (ScaleIn) + elif "ScaleIn" in alarm_name and alarm_status == 'ALARM' and applicationStatus == 'RUNNING': + if actualCapacity > minKPU: + if scaleOperation == 'Multiply/Divide': + desiredCapacity = int(actualCapacity / scaleInFactor) + print(f"Scaling in, desired capacity: {desiredCapacity}") + else: + desiredCapacity = actualCapacity - scaleInFactor + print(f"Scaling in, desired capacity: {desiredCapacity}") + if desiredCapacity > minKPU: + update_parallelism(context, desiredCapacity, resourceName, appVersion, parallelismPerKPU) + else: + update_parallelism(context, minKPU, resourceName, appVersion, parallelismPerKPU) + print("Application will go below Min KPU") + stop_scale = 1 + else: + desiredCapacity = actualCapacity + print("Application is already below or equal to Min KPU") + stop_scale = 1 + + else: + desiredCapacity = actualCapacity + print("Scaling still happening or not required") + stop_scale = 0 + + return response_function(200, stop_scale) diff --git a/infrastructure/AutoScaling/cdk/test/kda-autoscaling.test.d.ts b/infrastructure/AutoScaling/cdk/test/kda-autoscaling.test.d.ts new file mode 100644 index 0000000..e69de29 diff --git a/infrastructure/AutoScaling/cdk/test/kda-autoscaling.test.js b/infrastructure/AutoScaling/cdk/test/kda-autoscaling.test.js new file mode 100644 index 0000000..cbb15bb --- /dev/null +++ b/infrastructure/AutoScaling/cdk/test/kda-autoscaling.test.js @@ -0,0 +1,17 @@ +"use strict"; +// import * as cdk from 'aws-cdk-lib'; +// import { Template } from 'aws-cdk-lib/assertions'; +// import * as KdaAutoscaling from '../lib/kda-autoscaling-stack'; +// example test. To run these tests, uncomment this file along with the +// example resource in lib/kda-autoscaling-stack.ts +test('SQS Queue Created', () => { + // const app = new cdk.App(); + // // WHEN + // const stack = new KdaAutoscaling.KdaAutoscalingStack(app, 'MyTestStack'); + // // THEN + // const template = Template.fromStack(stack); + // template.hasResourceProperties('AWS::SQS::Queue', { + // VisibilityTimeout: 300 + // }); +}); +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoia2RhLWF1dG9zY2FsaW5nLnRlc3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJrZGEtYXV0b3NjYWxpbmcudGVzdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUEsc0NBQXNDO0FBQ3RDLHFEQUFxRDtBQUNyRCxrRUFBa0U7QUFFbEUsdUVBQXVFO0FBQ3ZFLG1EQUFtRDtBQUNuRCxJQUFJLENBQUMsbUJBQW1CLEVBQUUsR0FBRyxFQUFFO0lBQy9CLCtCQUErQjtJQUMvQixjQUFjO0lBQ2QsOEVBQThFO0lBQzlFLGNBQWM7SUFDZCxnREFBZ0Q7SUFFaEQsd0RBQXdEO0lBQ3hELDZCQUE2QjtJQUM3QixRQUFRO0FBQ1IsQ0FBQyxDQUFDLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBpbXBvcnQgKiBhcyBjZGsgZnJvbSAnYXdzLWNkay1saWInO1xuLy8gaW1wb3J0IHsgVGVtcGxhdGUgfSBmcm9tICdhd3MtY2RrLWxpYi9hc3NlcnRpb25zJztcbi8vIGltcG9ydCAqIGFzIEtkYUF1dG9zY2FsaW5nIGZyb20gJy4uL2xpYi9rZGEtYXV0b3NjYWxpbmctc3RhY2snO1xuXG4vLyBleGFtcGxlIHRlc3QuIFRvIHJ1biB0aGVzZSB0ZXN0cywgdW5jb21tZW50IHRoaXMgZmlsZSBhbG9uZyB3aXRoIHRoZVxuLy8gZXhhbXBsZSByZXNvdXJjZSBpbiBsaWIva2RhLWF1dG9zY2FsaW5nLXN0YWNrLnRzXG50ZXN0KCdTUVMgUXVldWUgQ3JlYXRlZCcsICgpID0+IHtcbi8vICAgY29uc3QgYXBwID0gbmV3IGNkay5BcHAoKTtcbi8vICAgICAvLyBXSEVOXG4vLyAgIGNvbnN0IHN0YWNrID0gbmV3IEtkYUF1dG9zY2FsaW5nLktkYUF1dG9zY2FsaW5nU3RhY2soYXBwLCAnTXlUZXN0U3RhY2snKTtcbi8vICAgICAvLyBUSEVOXG4vLyAgIGNvbnN0IHRlbXBsYXRlID0gVGVtcGxhdGUuZnJvbVN0YWNrKHN0YWNrKTtcblxuLy8gICB0ZW1wbGF0ZS5oYXNSZXNvdXJjZVByb3BlcnRpZXMoJ0FXUzo6U1FTOjpRdWV1ZScsIHtcbi8vICAgICBWaXNpYmlsaXR5VGltZW91dDogMzAwXG4vLyAgIH0pO1xufSk7XG4iXX0= \ No newline at end of file diff --git a/infrastructure/AutoScaling/cdk/test/kda-autoscaling.test.ts b/infrastructure/AutoScaling/cdk/test/kda-autoscaling.test.ts new file mode 100644 index 0000000..6c06efa --- /dev/null +++ b/infrastructure/AutoScaling/cdk/test/kda-autoscaling.test.ts @@ -0,0 +1,17 @@ +// import * as cdk from 'aws-cdk-lib'; +// import { Template } from 'aws-cdk-lib/assertions'; +// import * as KdaAutoscaling from '../lib/kda-autoscaling-stack'; + +// example test. To run these tests, uncomment this file along with the +// example resource in lib/kda-autoscaling-stack.ts +test('SQS Queue Created', () => { +// const app = new cdk.App(); +// // WHEN +// const stack = new KdaAutoscaling.KdaAutoscalingStack(app, 'MyTestStack'); +// // THEN +// const template = Template.fromStack(stack); + +// template.hasResourceProperties('AWS::SQS::Queue', { +// VisibilityTimeout: 300 +// }); +}); diff --git a/infrastructure/AutoScaling/cdk/tsconfig.json b/infrastructure/AutoScaling/cdk/tsconfig.json new file mode 100644 index 0000000..aaa7dc5 --- /dev/null +++ b/infrastructure/AutoScaling/cdk/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} diff --git a/infrastructure/AutoScaling/generateautoscaler.sh b/infrastructure/AutoScaling/generateautoscaler.sh new file mode 100755 index 0000000..93a8876 --- /dev/null +++ b/infrastructure/AutoScaling/generateautoscaler.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +print_help_and_exit() { + echo "Usage: sythautoscaler.sh [type=] metric= stat= " + echo "Parameters:" + echo " type=[KDA|MSK|Kinesis|KinesisEFO]: determines the metric type, default = KDA (AWS/KinesisAnalytics namespace metrics)" + echo " metric=: the name of the metric" + echo " stat=[Average|Sum|Minimum|Maximum]: determines the metric stat" + exit 1 +} + + +# Validate parameters +METRIC_FOUND=false +for ARG in "$@"; do + if [[ ! "$ARG" =~ ^[a-zA-Z0-9]+=[^:]*$ ]]; then + echo "Error: Invalid argument '$ARG'. Must be in the form =" + print_help_and_exit + fi + if [[ "$ARG" =~ ^metric= ]]; then + METRIC_FOUND=true + METRIC_NAME="${ARG#metric=}" + fi + if [[ "$ARG" =~ ^stat= ]]; then + STAT_FOUND=true + STAT_NAME="${ARG#stat=}" + fi +done + +if [ "$METRIC_FOUND" = false ]; then + echo "Missing mandatory 'metric' parameter" + print_help_and_exit +fi +if [ "$STAT_FOUND" = false ]; then + echo "Missing mandatory 'stat' parameter" + print_help_and_exit +fi + +# Move to subdir +CURRDIR=$(pwd) +cd cdk + +# Construct the command +CMD="npx cdk synth" +for ARG in "$@"; do + CMD+=" -c $ARG" +done + +# Install node dependencies if node_modules doesn't exist or package.json has changed +if [ ! -d "node_modules" ] || [ package.json -nt node_modules ]; then + echo "Installing dependencies..." >&2 + npm install --no-fund && echo_success "Dependencies installed" +fi + +# CFN template file name +CFN_TEMPLATE_FILE="Autoscaler-${METRIC_NAME}-${STAT_NAME}.yaml" + +# Execute synth command using local node dependencies +echo "Generating autoscaler CFN template..." +$CMD > "${CURRDIR}/${CFN_TEMPLATE_FILE}" +if [ $? -eq 0 ]; then + echo "Autoscaler CFN template generated: ${CFN_TEMPLATE_FILE}" +else + echo "Error generating autoscaler CFN template" +fi + +# Move back +cd ${CURRDIR} \ No newline at end of file diff --git a/infrastructure/AutoScaling/img/generating-autoscaler.png b/infrastructure/AutoScaling/img/generating-autoscaler.png new file mode 100644 index 0000000000000000000000000000000000000000..04c5f171d7f2e3e538debb623f59afc699d8f84a GIT binary patch literal 166373 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%0D-EFpd~sagPxzB9pi?5h>M*+> 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≀#NBxN72%7 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 WI4=Ea^V|#v_)$_&lP{7r4){MGn%{5$ literal 0 HcmV?d00001 diff --git a/infrastructure/README.md b/infrastructure/README.md index 353833c..2ede617 100644 --- a/infrastructure/README.md +++ b/infrastructure/README.md @@ -5,3 +5,4 @@ This folder contains a collection of operational utilities and infrastructure-as * [Custom autoscaling](./AutoScaling/): automatically scale Amazon Managed Service for Apache Flink applications based on any CloudWatch metric. You can also customize metric, threshold and scale up/scale down factor. * [Scheduled scaling](./ScheduledScaling/): scale Amazon Managed Service for Apache Flink applications up and down, based on a daily time schedule. * [CloudWatch Dashboard example](./monitoring/): Example of extended CloudWatch Dashboard to monitor an Amazon Managed Service for Apache Flink application. +* [scripts](./scripts): contains some useful shell script to interact with the Amazon Managed Service for Apache Flink control plane API diff --git a/infrastructure/scripts/README.md b/infrastructure/scripts/README.md new file mode 100644 index 0000000..fdaf9bd --- /dev/null +++ b/infrastructure/scripts/README.md @@ -0,0 +1,51 @@ +## Useful shell script + +This folder contains some useful shell script to interact with the Amazon Managed Service for Apache Flink API via AWS CLI. + +All scripts have the following prerequisites: +* [AWS CLI v2](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) +* [jq](https://jqlang.org/) + +All scripts assume the default profile is authenticated to the AWS account hosting the Managed Flink application with sufficient +permissions, and the default region matches the region of the application. +You can modify the script to pass AWS profile or region explicitly. + +These scripts are for demonstration purposes only. + +### Retrieve the status of the tasks + +[`task_status.sh`](task_status.sh) + +This script returns the status of each task in the Flink job. + +This is useful for example to automate operation, to check whether an update has been successfully deployed or is stuck +in a fail-and-restart loop due to some problem at runtime. + +The job is up-and-running and processing data when all the tasks are `RUNNING`. + +When the application is not `RUNNING`, the script always returns `UNKNOWN` + +Example 1: the job has 3 tasks, is healthy and processing data + +```shell +> ./task_status.sh MyApplication +RUNNING +RUNNING +RUNNING +``` + +Example 2: the job has 2 tasks, failing and restarting + +```shell +> ./task_status.sh MyApplication +FAILED +CANCELED +``` + +Example 3: the application is not running + +```shell +> ./task_status.sh MyApplication +UNKNOWN +``` + diff --git a/infrastructure/scripts/task_status.sh b/infrastructure/scripts/task_status.sh new file mode 100755 index 0000000..e5a2f97 --- /dev/null +++ b/infrastructure/scripts/task_status.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# This script returns the status of all tasks (called "vertices" in the API) of the Flink job. +# The script expects the application name as only parameter. It assumes the AWS default profile +# and Region are correctly set for the Managed Flink application. + +# Validate parameters +if [ $# -eq 0 ]; then + echo "Error: Application name required" + echo "Usage: $0 " + exit 1 +fi + +# Generate the pre-signed URL for the application +output=$(aws kinesisanalyticsv2 create-application-presigned-url \ + --application-name "$1" \ + --url-type FLINK_DASHBOARD_URL 2>&1) + +# Check if the output contains ResourceInUseException. It will happen when the application +# is not RUNNING, and the pre-signed URL is not available +if echo "$output" | grep -q "An error occurred (ResourceInUseException)"; then + echo "UNKNOWN" + exit 0 +fi + +# Parse the pre-signed URL +presigned_url=$(echo "$output" | jq -r '.AuthorizedUrl') +base_url=$(echo "$presigned_url" | grep -o 'https://[^/]*\.amazonaws\.com/flinkdashboard') +auth_token=$(echo "$presigned_url" | grep -o 'authToken=[^&]*' | cut -d'=' -f2) + +# Jobs endpoint URL +jobs_url="${base_url}/jobs?authToken=${auth_token}" + +# GET jobs status. Extract the Job ID assuming a single job is running +jobs_response=$(wget -qO- "${jobs_url}") +job_id=$(echo "$jobs_response" | jq -r '.jobs[0].id') + +# Job detail endpoint URL +job_details_url="${base_url}/jobs/${job_id}?authToken=${auth_token}" + +# GET Job details +job_details=$(wget -qO- "${job_details_url}") + +# Extract statuses of all vertices and join with space +vertices_statuses=$(echo "$job_details" | jq -r '.vertices[].status') + +echo "$vertices_statuses" diff --git a/java/AsyncIO/src/main/resources/flink-application-properties-dev.json b/java/AsyncIO/src/main/resources/flink-application-properties-dev.json index afdb1be..c32ab03 100644 --- a/java/AsyncIO/src/main/resources/flink-application-properties-dev.json +++ b/java/AsyncIO/src/main/resources/flink-application-properties-dev.json @@ -3,7 +3,7 @@ "PropertyGroupId": "OutputStream0", "PropertyMap": { "aws.region": "us-east-1", - "stream.arn": "ExampleOutputStream-ARN-GOES-HERE", + "stream.arn": "arn:aws:kinesis:us-east-1::stream/ExampleOutputStream", "flink.stream.initpos": "LATEST" } }, @@ -11,8 +11,8 @@ "PropertyGroupId": "EndpointService", "PropertyMap": { "aws.region": "us-east-1", - "api.url": "API-GATEWAY-URL", - "api.key": "API-GATEWAY-KEY" + "api.url": "", + "api.key": "" } } ] diff --git a/java/DynamoDBStreamSource/src/main/resources/flink-application-properties-dev.json b/java/DynamoDBStreamSource/src/main/resources/flink-application-properties-dev.json index 81730be..90f76d7 100644 --- a/java/DynamoDBStreamSource/src/main/resources/flink-application-properties-dev.json +++ b/java/DynamoDBStreamSource/src/main/resources/flink-application-properties-dev.json @@ -2,14 +2,14 @@ { "PropertyGroupId": "InputStream0", "PropertyMap": { - "stream.arn": "arn:aws:dynamodb:us-east-1:012345678901:table/my-ddb-table/stream/2024-11-07T17:14:13.766", + "stream.arn": "arn:aws:dynamodb:us-east-1::table/my-ddb-table/stream/2024-11-07T17:14:13.766", "flink.stream.initpos": "TRIM_HORIZON" } }, { "PropertyGroupId": "OutputStream0", "PropertyMap": { - "stream.arn": "arn:aws:kinesis:us-east-1:012345678900:stream/ExampleOutputStream", + "stream.arn": "arn:aws:kinesis:us-east-1::stream/ExampleOutputStream", "aws.region": "us-east-1" } } diff --git a/java/FlinkCDC/FlinkCDCSQLServerSource/README.md b/java/FlinkCDC/FlinkCDCSQLServerSource/README.md new file mode 100644 index 0000000..b165832 --- /dev/null +++ b/java/FlinkCDC/FlinkCDCSQLServerSource/README.md @@ -0,0 +1,150 @@ +# FlinkCDC SQL Server source example + +This example shows how to capture data from a database (SQL Server in this case) directly from Flink using a Flink CDC source connector. + +* Flink version: 1.20 +* Flink API: SQL +* Language: Java (11) +* Flink connectors: Flink CDC SQL Server source (3.4), JDBC sink + +The job is implemented in SQL embedded in Java. +It uses the [Flink CDC SQL Server source connector](https://nightlies.apache.org/flink/flink-cdc-docs-release-3.4/docs/connectors/flink-sources/sqlserver-cdc/) +to capture changes from a database. +Changes are propagated to the destination database using [JDBC Sink connector](https://nightlies.apache.org/flink/flink-docs-release-1.20/docs/connectors/table/jdbc/). + +### Source database + +This example uses Ms SQL Server as source database. To use a different database as CDC source you need to switch to a different Flink CDC Source connector. + +Different Flink CDC Sources require different configurations and support different metadata fields. To switch the source to a different database you need to modify the code. + +See [Flink CDC Sources documentation](https://nightlies.apache.org/flink/flink-cdc-docs-release-3.4/docs/connectors/flink-sources/sqlserver-cdc) for further details. + + +### Destination database + +Note that the JDBC sink is agnostic to the actual destination database technology. +This example is tested with both MySQL and PostgreSQL but can be easily adjusted to different databases. + +The `url` property in the `JdbcSink` configuration group decides the destination database (see [Runtime configuration](#runtime-configuration), below). +The correct JDBC driver must be included in the `pom.xml`. This example includes both MySQL and PostgreSQL drivers. + +### Testing with local databases using Docker Compose + +This example can be run locally using Docker. + +A [Docker Compose file](./docker/docker-compose.yml) is provided to run local SQL Server, MySQL and PostgreSQL databases. +The local databases are initialized by creating users, databases and tables. Some initial records are also inserted into the source table. + +You can run the Flink application inside your IDE following the instructions in [Running in IntelliJ](#running-in-intellij). +The default local configuration connects to the local PostgreSQL db defined in Docker Compose. + +To start the local databases run `docker compose up -d` in the `./docker` folder. + +Use `docker compose down -v` to shut them down, also removing the data volumes. + + +### Database prerequisites + +When running on Amazon Managed Service for Apache Flink and with databases on AWS, you need to set up the databases manually, ensuring you set up all the following: + +> YYou can find the SQL scripts that set up the dockerized databases by checking out the init scripts for +> [SQL Server](docker/sqlserver-init/init.sql), [MySQL](docker/mysql-init/init.sql), +> and [PostgreSQL](docker/postgres-init/init.sql). + +1. **Source database (Ms SQL Server)** + 1. SQL Server Agent must be running + 2. Native (user/password) authentication must be enabled + 3. The login used by Flink CDC (e.g. `flink_cdc`) must be `db_owner` for the database + 4. The source database and table must match the `database.name` and `table.name` you specify in the source configuration (e.g. `SampleDataPlatform` and `Customers`) + 5. The source table must have this schema: + ```sql + CREATE TABLE [dbo].[Customers] + ( + [CustomerID] [int] IDENTITY (1,1) NOT NULL, + [FirstName] [nvarchar](40) NOT NULL, + [MiddleInitial] [nvarchar](40) NULL, + [LastName] [nvarchar](40) NOT NULL, + [mail] [varchar](50) NULL, + CONSTRAINT [CustomerPK] PRIMARY KEY CLUSTERED ([CustomerID] ASC) + ) ON [PRIMARY]; + ``` + 6. CDC must be enabled both on the source database. On Amazon RDS SQL Server use the following stored procedure: + ```sql + exec msdb.dbo.rds_cdc_enable_db 'MyDB' + ``` + On self-managed SQL server you need to call a different procedure, while in the database: + ```sql + USE MyDB; + EXEC sys.sp_cdc_enable_db; + ``` + 7. CDC must also be enabled on the table: + ```sql + EXEC sys.sp_cdc_enable_table + @source_schema = N'dbo', + @source_name = N'Customers', + @role_name = NULL, + @supports_net_changes = 0; + ``` +2. **Destination database (MySQL or PostgreSQL)** + 1. The destination database name must match the `url` configured in the JDBC sink + 2. The destination table must have the following schema + ```sql + CREATE TABLE customers ( + customer_id INT PRIMARY KEY, + first_name VARCHAR(40), + middle_initial VARCHAR(40), + last_name VARCHAR(40), + email VARCHAR(50), + _source_updated_at TIMESTAMP, + _change_processed_at TIMESTAMP + ); + ``` + 3. The destination database user must have SELECT, INSERT, UPDATE and DELETE permissions on the destination table + +### Running in IntelliJ + +You can run this example directly in IntelliJ, without any local Flink cluster or local Flink installation. +Run the databases locally using Docker Compose, as described [above](#testing-with-local-databases-using-docker-compose). + +See [Running examples locally](../../running-examples-locally.md) for details about running the application in the IDE. + + +### Running on Amazon Managed Service for Apache Flink + +To run the application in Amazon Managed Service for Apache Flink make sure the application configuration has the following: +* VPC networking +* The selected Subnets can route traffic to both the source and destination databases +* The Security Group allows traffic from the application to both source and destination databases + + +### Runtime configuration + +When running on Amazon Managed Service for Apache Flink the runtime configuration is read from *Runtime Properties*. + +When running locally, the configuration is read from the [`resources/flink-application-properties-dev.json`](resources/flink-application-properties-dev.json) file located in the resources folder. + +Runtime parameters: + +| Group ID | Key | Description | +|-------------|-----------------|----------------------------------------------------------------------------------------------------------------------------| +| `CDCSource` | `hostname` | Source database DNS hostname or IP | +| `CDCSource` | `port` | Source database port (normally `1433`) | +| `CDCSource` | `username` | Source database username. The user must be `dbo_owner` of the database | +| `CDCSource` | `password` | Source database password | +| `CDCSource` | `database.name` | Source database name | +| `CDCSource` | `table.name` | Source table name. e.g. `dbo.Customers` | +| `JdbcSink` | `url` | Destination database JDBC URL. e.g. `jdbc:postgresql://localhost:5432/targetdb`. Note: the URL includes the database name. | +| `JdbcSink` | `table.name` | Destination table. e.g. `customers` | +| `JdbcSink` | `username` | Destination database user | +| `JdbcSink` | `password` | Destination database password | + +### Known limitations + +Using the SQL interface of Flink CDC Sources greatly simplifies the implementation of a passthrough application. +However, schema changes in the source table are ignored. + +## References + +* [Flink CDC SQL Server documentation](https://nightlies.apache.org/flink/flink-cdc-docs-release-3.4/docs/connectors/flink-sources/sqlserver-cdc) +* [Debezium SQL Server documentation](https://debezium.io/documentation/reference/1.9/connectors/sqlserver.html) \ No newline at end of file diff --git a/java/FlinkCDC/FlinkCDCSQLServerSource/docker/docker-compose.yml b/java/FlinkCDC/FlinkCDCSQLServerSource/docker/docker-compose.yml new file mode 100644 index 0000000..ca6abb4 --- /dev/null +++ b/java/FlinkCDC/FlinkCDCSQLServerSource/docker/docker-compose.yml @@ -0,0 +1,77 @@ +services: + + # Ms SQL Server + init + sqlserver: + image: mcr.microsoft.com/mssql/server:2022-latest + container_name: mssql-server-2022 + environment: + - ACCEPT_EULA=Y + - SA_PASSWORD=YourStrong@Passw0rd + - MSSQL_PID=Developer + - MSSQL_AGENT_ENABLED=true + ports: + - "1433:1433" + volumes: + - sqlserver_data:/var/opt/mssql + - ./sqlserver-init/init.sql:/tmp/init.sql + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P YourStrong@Passw0rd -Q 'SELECT 1' -C"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + sqlserver-init: + image: mcr.microsoft.com/mssql/server:2022-latest + depends_on: + sqlserver: + condition: service_healthy + volumes: + - ./sqlserver-init/init.sql:/tmp/init.sql + command: > + bash -c " + echo 'Waiting for SQL Server to be ready...' && + sleep 5 && + echo 'Running initialization script...' && + /opt/mssql-tools18/bin/sqlcmd -S sqlserver -U sa -P YourStrong@Passw0rd -i /tmp/init.sql -C && + echo 'Initialization completed!' + " + + # MySQL database + mysql: + image: mysql:8.0 + container_name: mysql_db + restart: always + environment: + MYSQL_ROOT_PASSWORD: R00tpwd! + MYSQL_DATABASE: targetdb + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + - ./mysql-init:/docker-entrypoint-initdb.d + command: --default-authentication-plugin=mysql_native_password + + # PostgreSQL database + postgres: + image: postgres:15 + container_name: postgres_db + restart: always + environment: + POSTGRES_DB: targetdb + POSTGRES_USER: flinkusr + POSTGRES_PASSWORD: PassW0rd! + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./postgres-init:/docker-entrypoint-initdb.d + +volumes: + sqlserver_data: + driver: local + mysql_data: + driver: local + postgres_data: + driver: local diff --git a/java/FlinkCDC/FlinkCDCSQLServerSource/docker/mysql-init/init.sql b/java/FlinkCDC/FlinkCDCSQLServerSource/docker/mysql-init/init.sql new file mode 100644 index 0000000..211e432 --- /dev/null +++ b/java/FlinkCDC/FlinkCDCSQLServerSource/docker/mysql-init/init.sql @@ -0,0 +1,15 @@ +CREATE USER 'flinkusr'@'%' IDENTIFIED BY 'PassW0rd!'; +GRANT SELECT, INSERT, UPDATE, DELETE, SHOW DATABASES ON *.* TO 'flinkusr'@'%'; + +FLUSH PRIVILEGES; + +-- Create customer table +CREATE TABLE customers ( + customer_id INT PRIMARY KEY, + first_name VARCHAR(40), + middle_initial VARCHAR(40), + last_name VARCHAR(40), + email VARCHAR(50), + _source_updated_at TIMESTAMP, + _change_processed_at TIMESTAMP +); diff --git a/java/FlinkCDC/FlinkCDCSQLServerSource/docker/postgres-init/init.sql b/java/FlinkCDC/FlinkCDCSQLServerSource/docker/postgres-init/init.sql new file mode 100644 index 0000000..05e9051 --- /dev/null +++ b/java/FlinkCDC/FlinkCDCSQLServerSource/docker/postgres-init/init.sql @@ -0,0 +1,10 @@ +-- Create customer table +CREATE TABLE customers ( + customer_id INT PRIMARY KEY, + first_name VARCHAR(40), + middle_initial VARCHAR(40), + last_name VARCHAR(40), + email VARCHAR(50), + _source_updated_at TIMESTAMP, + _change_processed_at TIMESTAMP +); diff --git a/java/FlinkCDC/FlinkCDCSQLServerSource/docker/sqlserver-init/init.sql b/java/FlinkCDC/FlinkCDCSQLServerSource/docker/sqlserver-init/init.sql new file mode 100644 index 0000000..4fb43a9 --- /dev/null +++ b/java/FlinkCDC/FlinkCDCSQLServerSource/docker/sqlserver-init/init.sql @@ -0,0 +1,56 @@ +-- Create SampleDataPlatform database +CREATE DATABASE SampleDataPlatform; +GO + +-- Use the SampleDataPlatform database +USE SampleDataPlatform; +GO + +-- Create login for flink_cdc +CREATE LOGIN flink_cdc WITH PASSWORD = 'FlinkCDC@123'; +GO + +-- Create user in SampleDataPlatform database +CREATE USER flink_cdc FOR LOGIN flink_cdc; +GO + +-- Grant necessary permissions for CDC operations +ALTER ROLE db_owner ADD MEMBER flink_cdc; +GO + +-- Enable CDC on the SampleDataPlatform database +USE SampleDataPlatform; +EXEC sys.sp_cdc_enable_db; +GO + +-- Create Customers table with the specified schema +CREATE TABLE [dbo].[Customers] +( + [CustomerID] [int] IDENTITY (1,1) NOT NULL, + [FirstName] [nvarchar](40) NOT NULL, + [MiddleInitial] [nvarchar](40) NULL, + [LastName] [nvarchar](40) NOT NULL, + [mail] [varchar](50) NULL, + CONSTRAINT [CustomerPK] PRIMARY KEY CLUSTERED ([CustomerID] ASC) + WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] +) ON [PRIMARY]; +GO + +-- Enable CDC on the Customers table +EXEC sys.sp_cdc_enable_table + @source_schema = N'dbo', + @source_name = N'Customers', + @role_name = NULL, + @supports_net_changes = 0; +GO + +-- Insert some sample data +INSERT INTO [dbo].[Customers] ([FirstName], [MiddleInitial], [LastName], [mail]) +VALUES ('John', 'A', 'Doe', 'john.doe@example.com'), + ('Jane', NULL, 'Smith', 'jane.smith@example.com'), + ('Bob', 'R', 'Johnson', 'bob.johnson@example.com'), + ('Alice', 'M', 'Williams', 'alice.williams@example.com'), + ('Charlie', NULL, 'Brown', 'charlie.brown@example.com'); +GO + +PRINT 'Database initialization completed successfully!'; diff --git a/java/FlinkCDC/FlinkCDCSQLServerSource/pom.xml b/java/FlinkCDC/FlinkCDCSQLServerSource/pom.xml new file mode 100644 index 0000000..ca8f112 --- /dev/null +++ b/java/FlinkCDC/FlinkCDCSQLServerSource/pom.xml @@ -0,0 +1,191 @@ + + + 4.0.0 + + com.amazonaws + flink-cdc-sqlserver-sql-source + 1.0 + jar + + + UTF-8 + ${project.basedir}/target + ${project.name}-${project.version} + 11 + ${target.java.version} + ${target.java.version} + 1.20.0 + 3.4.0 + 3.3.0-1.20 + 9.3.0 + 42.7.2 + 1.2.0 + 2.23.1 + + + + + + + + org.apache.flink + flink-streaming-java + ${flink.version} + provided + + + org.apache.flink + flink-clients + ${flink.version} + provided + + + org.apache.flink + flink-runtime-web + ${flink.version} + provided + + + org.apache.flink + flink-json + ${flink.version} + provided + + + org.apache.flink + flink-table-runtime + ${flink.version} + provided + + + org.apache.flink + flink-table-planner-loader + ${flink.version} + provided + + + + + com.amazonaws + aws-kinesisanalytics-runtime + ${kda.runtime.version} + provided + + + + + org.apache.flink + flink-connector-base + ${flink.version} + provided + + + org.apache.flink + flink-connector-sqlserver-cdc + ${flink.cdc.version} + + + org.apache.flink + flink-connector-jdbc + ${flink.jdbc.connector.version} + + + + + + com.mysql + mysql-connector-j + ${mysql.jdbc.driver.version} + + + org.postgresql + postgresql + ${postgresql.jdbc.driver.version} + + + + + + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + + + ${buildDirectory} + ${jar.finalName} + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${target.java.version} + ${target.java.version} + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + + package + + shade + + + + + org.apache.flink:force-shading + com.google.code.findbugs:jsr305 + org.slf4j:* + log4j:* + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + com.amazonaws.services.msf.FlinkCDCSqlServer2JdbcJob + + + + + + + + + \ No newline at end of file diff --git a/java/FlinkCDC/FlinkCDCSQLServerSource/src/main/java/com/amazonaws/services/msf/FlinkCDCSqlServer2JdbcJob.java b/java/FlinkCDC/FlinkCDCSQLServerSource/src/main/java/com/amazonaws/services/msf/FlinkCDCSqlServer2JdbcJob.java new file mode 100644 index 0000000..1fbc14f --- /dev/null +++ b/java/FlinkCDC/FlinkCDCSQLServerSource/src/main/java/com/amazonaws/services/msf/FlinkCDCSqlServer2JdbcJob.java @@ -0,0 +1,157 @@ +package com.amazonaws.services.msf; + +import com.amazonaws.services.kinesisanalytics.runtime.KinesisAnalyticsRuntime; +import org.apache.flink.cdc.common.utils.Preconditions; +import org.apache.flink.streaming.api.environment.LocalStreamEnvironment; +import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; +import org.apache.flink.table.api.EnvironmentSettings; +import org.apache.flink.table.api.bridge.java.StreamStatementSet; +import org.apache.flink.table.api.bridge.java.StreamTableEnvironment; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Map; +import java.util.Properties; + +public class FlinkCDCSqlServer2JdbcJob { + private static final Logger LOG = LoggerFactory.getLogger(FlinkCDCSqlServer2JdbcJob.class); + + // Name of the local JSON resource with the application properties in the same format as they are received from the Amazon Managed Service for Apache Flink runtime + private static final String LOCAL_APPLICATION_PROPERTIES_RESOURCE = "flink-application-properties-dev.json"; + + private static final int DEFAULT_CDC_DB_PORT = 1433; + + private static boolean isLocal(StreamExecutionEnvironment env) { + return env instanceof LocalStreamEnvironment; + } + + /** + * Load application properties from Amazon Managed Service for Apache Flink runtime or from a local resource, when the environment is local + */ + private static Map loadApplicationProperties(StreamExecutionEnvironment env) throws IOException { + if (isLocal(env)) { + LOG.info("Loading application properties from '{}'", LOCAL_APPLICATION_PROPERTIES_RESOURCE); + return KinesisAnalyticsRuntime.getApplicationProperties( + FlinkCDCSqlServer2JdbcJob.class.getClassLoader() + .getResource(LOCAL_APPLICATION_PROPERTIES_RESOURCE).getPath()); + } else { + LOG.info("Loading application properties from Amazon Managed Service for Apache Flink"); + return KinesisAnalyticsRuntime.getApplicationProperties(); + } + } + + public static void main(String[] args) throws Exception { + // set up the streaming execution environment + final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); + final StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env, EnvironmentSettings.newInstance().build()); + + final Map applicationProperties = loadApplicationProperties(env); + LOG.warn("Application properties: {}", applicationProperties); + + // Enable checkpoints and set parallelism when running locally + // On Managed Flink, checkpoints and application parallelism are managed by the service and controlled by the application configuration + if (isLocal(env)) { + env.setParallelism(1); // Ms SQL Server Flink CDC is single-threaded + env.enableCheckpointing(30000); + } + + + // Create CDC source table + Properties cdcSourceProperties = applicationProperties.get("CDCSource"); + tableEnv.executeSql("CREATE TABLE Customers (" + + " CustomerID INT," + + " FirstName STRING," + + " MiddleInitial STRING," + + " LastName STRING," + + " mail STRING," + + // Some additional metadata columns for demonstration purposes + " `_change_processed_at` AS PROCTIME()," + // The time when Flink is processing this record + " `_source_updated_at` TIMESTAMP_LTZ(3) METADATA FROM 'op_ts' VIRTUAL," + // The time when the operation was executed on the db + " `_table_name` STRING METADATA FROM 'table_name' VIRTUAL," + // Name of the table in the source db + " `_schema_name` STRING METADATA FROM 'schema_name' VIRTUAL, " + // Name of the schema in the source db + " `_db_name` STRING METADATA FROM 'database_name' VIRTUAL," + // name of the database + " PRIMARY KEY(CustomerID) NOT ENFORCED" + + ") WITH (" + + " 'connector' = 'sqlserver-cdc'," + + " 'hostname' = '" + Preconditions.checkNotNull(cdcSourceProperties.getProperty("hostname"), "missing CDC source hostname") + "'," + + " 'port' = '" + Preconditions.checkNotNull(cdcSourceProperties.getProperty("port", Integer.toString(DEFAULT_CDC_DB_PORT)), "missing CDC source port") + "'," + + " 'username' = '" + Preconditions.checkNotNull(cdcSourceProperties.getProperty("username"), "missing CDC source username") + "'," + + // For simplicity, we are passing the db password as a runtime configuration unencrypted. This should be avoided in production + " 'password' = '" + Preconditions.checkNotNull(cdcSourceProperties.getProperty("password"), "missing CDC source password") + "'," + + " 'database-name' = '" + Preconditions.checkNotNull(cdcSourceProperties.getProperty("database.name"), "missing CDC source database name") + "'," + + " 'table-name' = '" + Preconditions.checkNotNull(cdcSourceProperties.getProperty("table.name"), "missing CDC source table name") + "'" + + ")"); + + + // Create a JDBC sink table + // Note that the definition of the table is agnostic to the actual destination database (e.g. MySQL or PostgreSQL) + Properties jdbcSinkProperties = applicationProperties.get("JdbcSink"); + tableEnv.executeSql("CREATE TABLE DestinationTable (" + + " customer_id INT," + + " first_name STRING," + + " middle_initial STRING," + + " last_name STRING," + + " email STRING," + + " _source_updated_at TIMESTAMP(3)," + + " _change_processed_at TIMESTAMP(3)," + + " PRIMARY KEY(customer_id) NOT ENFORCED" + + ") WITH (" + + " 'connector' = 'jdbc'," + + " 'url' = '" + Preconditions.checkNotNull(jdbcSinkProperties.getProperty("url"), "missing destination database JDBC URL") + "'," + + " 'table-name' = '" + Preconditions.checkNotNull(jdbcSinkProperties.getProperty("table.name"), "missing destination database table name") + "'," + + " 'username' = '" + Preconditions.checkNotNull(jdbcSinkProperties.getProperty("username"), "missing destination database username") + "'," + + " 'password' = '" + Preconditions.checkNotNull(jdbcSinkProperties.getProperty("password"), "missing destination database password") + "'" + + ")"); + + // When running locally we add a secondary sink to print the output to the console. + // When the job is running on Managed Flink any output to console is not visible and may cause overhead. + // It is recommended not to print any output to the console when running the application on Managed Flink. + if( isLocal(env)) { + tableEnv.executeSql("CREATE TABLE PrintSinkTable (" + + " CustomerID INT," + + " FirstName STRING," + + " MiddleInitial STRING," + + " LastName STRING," + + " mail STRING," + + " `_change_processed_at` TIMESTAMP_LTZ(3)," + + " `_source_updated_at` TIMESTAMP_LTZ(3)," + + " `_table_name` STRING," + + " `_schema_name` STRING," + + " `_db_name` STRING," + + " PRIMARY KEY(CustomerID) NOT ENFORCED" + + ") WITH (" + + " 'connector' = 'print'" + + ")"); + } + + // Note that we use a statement set to add the two "INSERT INTO..." statements. + // When tableEnv.executeSQL(...) is used with INSERT INTO on a job running in Application mode, like on Managed Flink, + // the first statement triggers the job execution, and any code which follows is ignored. + StreamStatementSet statementSet = tableEnv.createStatementSet(); + statementSet.addInsertSql("INSERT INTO DestinationTable (" + + "customer_id, " + + "first_name, " + + "middle_initial, " + + "last_name, " + + "email, " + + "_source_updated_at, " + + "_change_processed_at" + + ") SELECT " + + "CustomerID, " + + "FirstName, " + + "MiddleInitial, " + + "LastName, " + + "mail, " + + "`_source_updated_at`, " + + "`_change_processed_at` " + + "FROM Customers"); + if( isLocal(env)) { + statementSet.addInsertSql("INSERT INTO PrintSinkTable SELECT * FROM Customers"); + } + + + // Execute the two INSERT INTO statements + statementSet.execute(); + } +} diff --git a/java/FlinkCDC/FlinkCDCSQLServerSource/src/main/resources/flink-application-properties-dev.json b/java/FlinkCDC/FlinkCDCSQLServerSource/src/main/resources/flink-application-properties-dev.json new file mode 100644 index 0000000..8974b2f --- /dev/null +++ b/java/FlinkCDC/FlinkCDCSQLServerSource/src/main/resources/flink-application-properties-dev.json @@ -0,0 +1,22 @@ +[ + { + "PropertyGroupId": "CDCSource", + "PropertyMap": { + "hostname": "localhost", + "port": "1433", + "username": "flink_cdc", + "password": "FlinkCDC@123", + "database.name": "SampleDataPlatform", + "table.name": "dbo.Customers" + } + }, + { + "PropertyGroupId": "JdbcSink", + "PropertyMap": { + "table.name": "customers", + "url": "jdbc:postgresql://localhost:5432/targetdb", + "username": "flinkusr", + "password": "PassW0rd!" + } + } +] \ No newline at end of file diff --git a/java/FlinkCDC/FlinkCDCSQLServerSource/src/main/resources/log4j2.properties b/java/FlinkCDC/FlinkCDCSQLServerSource/src/main/resources/log4j2.properties new file mode 100644 index 0000000..3546643 --- /dev/null +++ b/java/FlinkCDC/FlinkCDCSQLServerSource/src/main/resources/log4j2.properties @@ -0,0 +1,7 @@ +rootLogger.level = INFO +rootLogger.appenderRef.console.ref = ConsoleAppender + +appender.console.name = ConsoleAppender +appender.console.type = CONSOLE +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{HH:mm:ss,SSS} %-5p %-60c %x - %m%n diff --git a/java/FlinkCDC/README.md b/java/FlinkCDC/README.md new file mode 100644 index 0000000..196b083 --- /dev/null +++ b/java/FlinkCDC/README.md @@ -0,0 +1,9 @@ +# Flink CDC Examples + +Examples demonstrating Change Data Capture (CDC) using [Flink CDC source connectors](https://nightlies.apache.org/flink/flink-cdc-docs-release-3.4/docs/connectors/flink-sources/overview/) +in Amazon Managed Service for Apache Flink. + +## Table of Contents + +### Database Sources +- [**Flink CDC SQL Server Source**](./FlinkCDCSQLServerSource) - Capturing changes from SQL Server database and writing to JDBC sink \ No newline at end of file diff --git a/java/FlinkDataGenerator/README.md b/java/FlinkDataGenerator/README.md new file mode 100644 index 0000000..84376f9 --- /dev/null +++ b/java/FlinkDataGenerator/README.md @@ -0,0 +1,144 @@ +# Flink JSON Data Generator to Kinesis or Kafka + +This example demonstrates how you can use Apache Flink's as a data generator for load testing. + +* Flink version: 1.20 +* Flink API: DataStream API +* Language: Java (11) +* Flink connectors: DataGen, Kafka Sink, Kinesis Sink + +The application generates random stock prices at fixed rate. +Depending on runtime configuration it will send generated records, as JSON, either to a Kinesis Data Stream +or an MSK/Kafka topic (or both). + +The application can easily scale to generate high throughput. For example, with 3 KPU you can generate more than 64,000 records per second. +See [Using the data generator for load testing](#using-the-data-generator-for-load-testing). + +It can be easily modified to generate different type of records, changing the implementation of the record class +[StockPrice](src/main/java/com/amazonaws/services/msf/domain/StockPrice.java), and the function generating data [StockPriceGeneratorFunction](src/main/java/com/amazonaws/services/msf/domain/StockPriceGeneratorFunction.java) + +### Prerequisites + +The data generator application must be able to write to the Kinesis Stream or the Kafka topic +* Kafka/MSK + * The Managed Flink application must have VPC networking. + * Routing and Security must allow the application to reach the Kafka cluster. + * Any Kafka/MSK authentication must be added to the application (this application writes unauthenticated) + * Kafka ACL or IAM must allow the application writing to the topic +* Kinesis Data Stream + * The Managed Flink application IAM Role must have permissions to write to the stream + * Ensure the Kinesis Stream has sufficient capacity for the generated throughput + * If the application has VPC networking, you must also create a VPC Endpoint for Kinesis to be able to write to the Stream + + + +### Runtime configuration + +The application reads the runtime configuration from the Runtime Properties, when running on Amazon Managed Service for Apache Flink, +or, when running locally, from the [`src/main/resources/flink-application-properties-dev.json`](src/main/resources/flink-application-properties-dev.json) file. +All parameters are case-sensitive. + +The presence of the configuration group `KafkaSink` enables the Kafka sink. +Likewise, `KinesisSink` enables the Kinesis sink. + + +| Group ID | Key | Description | +|---------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `DataGen` | `records.per.second` | Number of records per second generated across all subtasks. | +| `KinesisSink` | `stream.arn` | ARN of the Kinesis Stream | +| `KinesisSink` | `aws.region` | Region of the Kinesis Stream | +| `KinesisSink` | (any other parameter) | Any other parameters in this group is passed to the Kinesis sink connector as KinesisClientProperties. See [documentation](https://nightlies.apache.org/flink/flink-docs-release-1.20/docs/connectors/datastream/kinesis/#kinesis-streams-sink) | +| `KafkaSink` | `bootstrap.servers` | Kafka bootstrap servers. | +| `KafkaSink` | `topic` | Name of the Kafka topic. | +| `KafkaSink` | (any other parameter) | Any other parameters in this group is passed to the Kafka sink connector as KafkaProducerConfig. See [documentation](https://nightlies.apache.org/flink/flink-docs-release-1.20/docs/connectors/datastream/kafka/#kafka-sink) | + + +> Renaming `KafkaSink` or `KinesisSink` groups to something different, for example `KinesisSink-DISABLE` prevents +> the generator creating that particular sink. + +### Running in IntelliJ + +You can run this example directly in IntelliJ, without any local Flink cluster or local Flink installation. + +See [Running examples locally](../running-examples-locally.md) for details. + +--- + +## Data Generation + +This example generates random stock price records similar to the following: + +```json +{ + "event_time": "2024-01-15T10:30:45.123", + "ticker": "AAPL", + "price": 150.25 +} +``` + +The data generation can be easily customized to match your specific records, modifying two components: + +* The class [StockPrice](src/main/java/com/amazonaws/services/msf/domain/StockPrice.java) representing the record. + You can use [Jackson annotations](https://github.com/FasterXML/jackson-annotations/wiki/Jackson-Annotations) to customize the generated JSON. +* The class [StockPriceGeneratorFunction](src/main/java/com/amazonaws/services/msf/domain/StockPriceGeneratorFunction.java) + contains the logic for generating each record. + +### Partitioning + +Records published in Kinesis or Kafka are partitioned by the `ticker` field. + +If you customize the data object you also need to modify the `PartitionKeyGenerator` and `SerializationSchema` +extracting the key in the Kinesis and Kafka sink respectively. + +## Using the data generator for load testing + +This application can be used to load test other applications. + +Make sure the data generator application has sufficient resources to generate the desired throughput. + +Also, make sure the Kafka/MSK cluster or the Kinesis Stream have sufficient capacity to ingest the generated throughput. + +⚠️ If the destination system or the data generator Flink application are underprovisioned, you may generate a throughput lower than expected. + +For reference, the following configuration allows generating ~64,000 records/sec to either Kinesis or Kafka: +* `Parallelism = 3`, `Parallelism-per-KPU = 1` (`3 KPU`) +* `DataGen` `records.per.second` = `64000` + +> We recommend to overprovision the data generator to ensure the required throughput can be achieved. +> Use the provided [CloudWatch dashboard](#cloudwatch-dashboard) to monitor the generator. + +### Monitoring the data generator + +The application exposes 3 custom metrics to CloudWatch: +* `generatedRecordCount`: count of generated record, per parallelism +* `generatedRecordRatePerParallelism`: generated records per second, per parallelism +* `taskParallelism`: parallelism of the data generator + +> ⚠️ Flink custom metrics are not global. Each subtask maintains its own metrics. +> Also, for each metric Amazon Managed Service for Apache Flink exports to CloudWatch 4 datapoints per minute, per subtask. +> That considered, to calculate the total generated record and rate, across the entire application, you need to apply +> the following maths: +> - Total generatedRecordCount = `SUM(generatedRecordCount) / 4`, over 1 minute +> - Total generatedRecordsPerSec = `AVG(generatedRecordRatePerParallelism) * AVG(taskParallelism)`, over 1 minute + +#### CloudWatch Dashboard + +The CloudFormation template [dashboard-cfn.yaml](tools/dashboard-cfn.yaml) provided can be used to create a CloudWatch Dashboard +to monitor the data generator + +![Flink Data Generator dashboard](images/dashboard.png). + +When creating the CloudFormation stack you need to provide: +* The name of the Managed Flink application +* The Region +* The name of the Kinesis Stream, if publishing to Kinesis +* The name of the MSK cluster and topic, if publishing to MSK + +> Note: the dashboard assumes an MSK cluster with up to 6 brokers. +> If you have a cluster with more than 6 brokers you need to adjust the *Kafka output* widget + +### Known limitations and possible extensions + +* Only JSON serialization is supported. +* Data generation is stateless. The logic generating each record does not know about other records previously generated. +* Fixed record rate only. No ramp up or ramp down. diff --git a/java/FlinkDataGenerator/images/dashboard.png b/java/FlinkDataGenerator/images/dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..248e36cb5988ed53132afe10668335bd41789d53 GIT binary patch literal 400863 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%0AE5L9 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

+ * The code of this class is copied from + * https://github.com/awslabs/kinesis-aggregation/blob/master/java/KinesisDeaggregatorV2/src/main/java/com/amazonaws/kinesis/deagg/RecordDeaggregator.java + * which is not available in Maven Central. + * See https://github.com/awslabs/kinesis-aggregation/issues/120 + */ + private static class RecordDeaggregator implements Serializable { + + /** + * Method to deaggregate a single Kinesis Record into a List of UserRecords + * + * @param inputRecord The Kinesis Record provided by AWS Lambda or Kinesis SDK + * @return A list of Kinesis UserRecord objects obtained by deaggregating the + * input list of KinesisEventRecords + */ + public List deaggregate(T inputRecord) throws Exception { + return new AggregatorUtil().deaggregate(convertType(Arrays.asList(inputRecord))); + } + + @SuppressWarnings("unchecked") + private List convertType(List inputRecords) throws Exception { + List records = null; + + if (!inputRecords.isEmpty() && inputRecords.get(0) instanceof Record) { + records = new ArrayList<>(); + for (Record rec : (List) inputRecords) { + records.add(KinesisClientRecord.fromRecord(rec)); + } + } else { + if (inputRecords.isEmpty()) { + return new ArrayList<>(); + } else { + throw new Exception("Input Types must be a Model Records"); + } + } + + return records; + } + } +} diff --git a/java/KinesisSourceDeaggregation/flink-app/src/main/java/com/amazonaws/services/msf/model/StockPrice.java b/java/KinesisSourceDeaggregation/flink-app/src/main/java/com/amazonaws/services/msf/model/StockPrice.java new file mode 100644 index 0000000..4e423d7 --- /dev/null +++ b/java/KinesisSourceDeaggregation/flink-app/src/main/java/com/amazonaws/services/msf/model/StockPrice.java @@ -0,0 +1,47 @@ +package com.amazonaws.services.msf.model; + +import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.annotation.JsonProperty; + +public class StockPrice { + // This annotation as well as the associated jackson2 import is needed to correctly map the JSON input key to the + // appropriate POJO property name to ensure event_time isn't missed in serialization and deserialization + @JsonProperty("event_time") + private String eventTime; + private String ticker; + private float price; + + public StockPrice() {} + + public String getEventTime() { + return eventTime; + } + + public void setEventTime(String eventTime) { + this.eventTime = eventTime; + } + + public String getTicker() { + return ticker; + } + + public void setTicker(String ticker) { + this.ticker = ticker; + } + + public float getPrice() { + return price; + } + + public void setPrice(float price) { + this.price = price; + } + + @Override + public String toString() { + return "StockPrice{" + + "event_time='" + eventTime + '\'' + + ", ticker='" + ticker + '\'' + + ", price=" + price + + '}'; + } +} diff --git a/java/KinesisSourceDeaggregation/flink-app/src/main/resources/flink-application-properties-dev.json b/java/KinesisSourceDeaggregation/flink-app/src/main/resources/flink-application-properties-dev.json new file mode 100644 index 0000000..880a58f --- /dev/null +++ b/java/KinesisSourceDeaggregation/flink-app/src/main/resources/flink-application-properties-dev.json @@ -0,0 +1,17 @@ +[ + { + "PropertyGroupId": "InputStream0", + "PropertyMap": { + "stream.arn": "arn:aws:kinesis:us-east-1::stream/ExampleInputStream", + "source.init.position": "LATEST", + "aws.region": "us-east-1" + } + }, + { + "PropertyGroupId": "OutputStream0", + "PropertyMap": { + "stream.arn": "arn:aws:kinesis:us-east-1::stream/ExampleOutputStream", + "aws.region": "us-east-1" + } + } +] diff --git a/java/KinesisSourceDeaggregation/flink-app/src/main/resources/log4j2.properties b/java/KinesisSourceDeaggregation/flink-app/src/main/resources/log4j2.properties new file mode 100644 index 0000000..b4d75f2 --- /dev/null +++ b/java/KinesisSourceDeaggregation/flink-app/src/main/resources/log4j2.properties @@ -0,0 +1,8 @@ +rootLogger.level = INFO +rootLogger.appenderRef.console.ref = ConsoleAppender + +appender.console.name = ConsoleAppender +appender.console.type = CONSOLE +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{HH:mm:ss,SSS} %-5p %-60c %x - %m%n + diff --git a/java/KinesisSourceDeaggregation/kpl-producer/README.md b/java/KinesisSourceDeaggregation/kpl-producer/README.md new file mode 100644 index 0000000..d906df1 --- /dev/null +++ b/java/KinesisSourceDeaggregation/kpl-producer/README.md @@ -0,0 +1,20 @@ +## Simple KPL aggregating data generator + +This module contains a simple random data generator which publishes stock prices records to a Kinesis Data Streams +using KPL aggregation. + +1. Compile: `mvn package` +2. Run: `java -jar target/kpl-producer-1.0.jar --streamName --streamRegion --sleep 10` + +Runtime parameters (all optional) + +* `streamName`: default `ExampleInputStream` +* `stramRegion`: default `us-east-1` +* `sleep`: default 10 (milliseconds between records) + + +### Data example + +``` +{'event_time': '2024-05-28T19:53:17.497201', 'ticker': 'AMZN', 'price': 42.88} +``` \ No newline at end of file diff --git a/java/KinesisSourceDeaggregation/kpl-producer/pom.xml b/java/KinesisSourceDeaggregation/kpl-producer/pom.xml new file mode 100644 index 0000000..e5fc504 --- /dev/null +++ b/java/KinesisSourceDeaggregation/kpl-producer/pom.xml @@ -0,0 +1,79 @@ + + 4.0.0 + com.amazonaws + kpl-producer + 1.0 + + + 11 + ${target.java.version} + ${target.java.version} + 1.0.0 + 2.0.17 + 2.23.1 + + + + + software.amazon.kinesis + amazon-kinesis-producer + ${kpl.version} + + + + commons-cli + commons-cli + 1.9.0 + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-simple + ${slf4j.version} + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${target.java.version} + ${target.java.version} + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + + package + + shade + + + + + com.amazonaws.services.kds.producer.KplAggregatingProducer + + + + + + + + + + diff --git a/java/KinesisSourceDeaggregation/kpl-producer/src/main/java/com/amazonaws/services/kds/producer/KplAggregatingProducer.java b/java/KinesisSourceDeaggregation/kpl-producer/src/main/java/com/amazonaws/services/kds/producer/KplAggregatingProducer.java new file mode 100644 index 0000000..8db2458 --- /dev/null +++ b/java/KinesisSourceDeaggregation/kpl-producer/src/main/java/com/amazonaws/services/kds/producer/KplAggregatingProducer.java @@ -0,0 +1,182 @@ +package com.amazonaws.services.kds.producer; + +import com.amazonaws.services.kds.producer.model.StockPrice; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import org.apache.commons.cli.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.kinesis.producer.KinesisProducer; +import software.amazon.kinesis.producer.KinesisProducerConfiguration; +import software.amazon.kinesis.producer.UserRecordResult; + +import java.nio.ByteBuffer; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Simple KPL producer publishing random StockRecords as JSON to a Kinesis stream + */ +public class KplAggregatingProducer { + private static final Logger LOG = LoggerFactory.getLogger(KplAggregatingProducer.class); + + private static final String[] TICKERS = {"AAPL", "AMZN", "MSFT", "INTC", "TBV"}; + private static final Random RANDOM = new Random(); + private static final DateTimeFormatter ISO_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS") + .withZone(ZoneOffset.UTC); + + private final String streamName; + private final String streamRegion; + private final long sleepTimeBetweenRecords; + + private final AtomicLong queuedRecordCount = new AtomicLong(); + private final AtomicLong sentRecordCount = new AtomicLong(); + + private static final String DEFAULT_STREAM_REGION = "us-east-1"; + private static final String DEFAULT_STREAM_NAME = "ExampleInputStream"; + private static final long DEFAULT_SLEEP_TIME_BETWEEN_RECORDS = 50L; + + public static void main(String[] args) throws Exception { + Options options = new Options() + .addOption(Option.builder() + .longOpt("streamName") + .hasArg() + .desc("Stream name (default: " + DEFAULT_STREAM_NAME + ")") + .build()) + .addOption(Option.builder() + .longOpt("streamRegion") + .hasArg() + .desc("Stream AWS region (default: " + DEFAULT_STREAM_REGION + ")") + .build()) + .addOption(Option.builder() + .longOpt("sleep") + .hasArg() + .desc("Sleep duration in seconds (default: " + DEFAULT_SLEEP_TIME_BETWEEN_RECORDS + ")") + .build()); + + CommandLineParser parser = new DefaultParser(); + HelpFormatter formatter = new HelpFormatter(); + + try { + CommandLine cmd = parser.parse(options, args); + String streamNameValue = cmd.getOptionValue("streamName", DEFAULT_STREAM_NAME); + String streamRegionValue = cmd.getOptionValue("region", DEFAULT_STREAM_REGION); + long sleepTimeBetweenRecordsMillis = Long.parseLong(cmd.getOptionValue("sleep", String.valueOf(DEFAULT_SLEEP_TIME_BETWEEN_RECORDS))); + + LOG.info("StreamName: {}, region: {}", streamNameValue, streamNameValue); + LOG.info("SleepTimeBetweenRecords: {} ms", sleepTimeBetweenRecordsMillis); + + KplAggregatingProducer instance = new KplAggregatingProducer(streamNameValue, streamRegionValue, sleepTimeBetweenRecordsMillis); + instance.produce(); + + } catch (ParseException e) { + System.err.println("Error parsing command line arguments: " + e.getMessage()); + formatter.printHelp("KplAggregatingProducer", options); + System.exit(1); + } catch (NumberFormatException e) { + System.err.println("Error: sleep parameter must be a valid integer"); + formatter.printHelp("KplAggregatingProducer", options); + System.exit(1); + } + } + + public KplAggregatingProducer(String streamName, String streamRegion, long sleepTimeBetweenRecords) { + this.streamName = streamName; + this.streamRegion = streamRegion; + this.sleepTimeBetweenRecords = sleepTimeBetweenRecords; + } + + public void produce() { + KinesisProducerConfiguration config = new KinesisProducerConfiguration() + .setRegion(streamRegion) + .setAggregationEnabled(true) + .setRecordMaxBufferedTime(100) + .setMaxConnections(4) + .setRequestTimeout(60000); + + KinesisProducer producer = new KinesisProducer(config); + ExecutorService callbackThreadPool = Executors.newCachedThreadPool(); + + // Setup shutdown hook for cleanup + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + LOG.info("Shutting down..."); + producer.flushSync(); + producer.destroy(); + callbackThreadPool.shutdown(); + try { + callbackThreadPool.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + LOG.warn("Thread pool shutdown interrupted", e); + } + })); + + try { + while (true) { + StockPrice stockPrice = generateRandomStockPrice(); + sendStockPriceToKinesis(stockPrice, streamName, producer, callbackThreadPool); + Thread.sleep(sleepTimeBetweenRecords); // Avoid flooding the stream + } + } catch (InterruptedException e) { + LOG.warn("Producer interrupted" + e); + } + } + + private StockPrice generateRandomStockPrice() { + String ticker = TICKERS[RANDOM.nextInt(TICKERS.length)]; + double price = RANDOM.nextDouble() * 100.0; + String timestamp = LocalDateTime.now().format(ISO_FORMATTER); + + return new StockPrice(timestamp, ticker, price); + } + + private void sendStockPriceToKinesis(StockPrice stockPrice, String streamName, KinesisProducer producer, + ExecutorService callbackThreadPool) { + try { + String jsonPayload = String.format( + "{\"event_time\": \"%s\", \"ticker\": \"%s\", \"price\": %.2f}", + stockPrice.getEventTime(), + stockPrice.getTicker(), + stockPrice.getPrice() + ); + + byte[] bytes = jsonPayload.getBytes(); + LOG.trace("Sending stock price: {}", jsonPayload); + + + ListenableFuture future = producer.addUserRecord( + streamName, + stockPrice.getTicker(), // Use ticker as partition key + ByteBuffer.wrap(bytes)); + long queued = queuedRecordCount.incrementAndGet(); + if (queued % 1000 == 0) { + long sent = sentRecordCount.get(); + LOG.info("Queued {} records. Sent {} records", queued, sent); + } + + + // Handle success/failure asynchronously + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(UserRecordResult result) { + sentRecordCount.incrementAndGet(); + LOG.trace("Successfully sent record: {}", result.getSequenceNumber()); + } + + @Override + public void onFailure(Throwable t) { + LOG.error("Failed to send record" + t); + } + }, callbackThreadPool); + } catch (Exception e) { + LOG.error("Error serializing or sending stock price", e); + } + } +} diff --git a/java/KinesisSourceDeaggregation/kpl-producer/src/main/java/com/amazonaws/services/kds/producer/model/StockPrice.java b/java/KinesisSourceDeaggregation/kpl-producer/src/main/java/com/amazonaws/services/kds/producer/model/StockPrice.java new file mode 100644 index 0000000..7fc7dab --- /dev/null +++ b/java/KinesisSourceDeaggregation/kpl-producer/src/main/java/com/amazonaws/services/kds/producer/model/StockPrice.java @@ -0,0 +1,37 @@ +package com.amazonaws.services.kds.producer.model; + +public class StockPrice { + private String eventTime; + private String ticker; + private double price; + + public StockPrice(String eventTime, String ticker, double price) { + this.eventTime = eventTime; + this.ticker = ticker; + this.price = price; + } + + public String getEventTime() { + return eventTime; + } + + public void setEventTime(String eventTime) { + this.eventTime = eventTime; + } + + public String getTicker() { + return ticker; + } + + public void setTicker(String ticker) { + this.ticker = ticker; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } +} diff --git a/java/KinesisSourceDeaggregation/kpl-producer/src/main/resources/simplelogger.properties b/java/KinesisSourceDeaggregation/kpl-producer/src/main/resources/simplelogger.properties new file mode 100644 index 0000000..7d6c710 --- /dev/null +++ b/java/KinesisSourceDeaggregation/kpl-producer/src/main/resources/simplelogger.properties @@ -0,0 +1,8 @@ +# Configuration for SLF4J Simple Logger + +org.slf4j.simpleLogger.defaultLogLevel=info +org.slf4j.simpleLogger.showDateTime=false +org.slf4j.simpleLogger.showLogName=true +org.slf4j.simpleLogger.showShortLogName=true +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.levelInBrackets=true diff --git a/java/KinesisSourceDeaggregation/pom.xml b/java/KinesisSourceDeaggregation/pom.xml new file mode 100644 index 0000000..080082d --- /dev/null +++ b/java/KinesisSourceDeaggregation/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + com.amazonaws + kinesis-source-deaggregation-example + 1.0 + pom + + kpl-producer + flink-app + + + \ No newline at end of file diff --git a/java/S3AvroSink/README.md b/java/S3AvroSink/README.md new file mode 100644 index 0000000..85947d2 --- /dev/null +++ b/java/S3AvroSink/README.md @@ -0,0 +1,54 @@ +# S3 Avro Sink + +* Flink version: 1.20 +* Flink API: DataStream API +* Language Java (11) +* Connectors: FileSystem Sink (and DataGen connector) + +This example demonstrates how to write AVRO files to S3. + +The example generates random stock price data using the DataGen connector and writes to S3 as AVRO files with +a bucketing in the format `year=yyyy/month=MM/day=dd/hour=HH/` and rotating files on checkpoint. + +Note that FileSystem sink commit the writes on checkpoint. For this reason, checkpoint are programmatically enabled when running locally. +When running on Managed Flink checkpoints are controlled by the application configuration and enabled by default. + +This application can be used in combination with the [S3AvroSource](../S3AvroSource) example application which read AVRO data with the same schema from S3. + +## Prerequisites + +* An S3 bucket for writing data. The application IAM Role must allow writing to the bucket + + +## Runtime Configuration + +The application reads the runtime configuration from the Runtime Properties, when running on Amazon Managed Service for Apache Flink, +or, when running locally, from the [`resources/flink-application-properties-dev.json`](resources/flink-application-properties-dev.json) file located in the resources folder. + +All parameters are case-sensitive. + +| Group ID | Key | Description | +|----------------|---------------|------------------------------------| +| `OutputBucket` | `bucket.name` | Name of the destination S3 bucket. | +| `OutputBucket` | `bucket.path` | Base path withing the bucket. | + +To configure the application on Managed Service for Apache Flink, set up these parameter in the *Runtime properties*. + +To configure the application for running locally, edit the [json file](resources/flink-application-properties-dev.json). + +### Running in IntelliJ + +You can run this example directly in IntelliJ, without any local Flink cluster or local Flink installation. + +See [Running examples locally](../running-examples-locally.md) for details. + +## AVRO Specific Record usage + +The AVRO schema definition (`avdl` file) is included as part of the source code in the `./src/main/resources/avro` folder. +The AVRO Maven Plugin is used to generate the Java object `StockPrice` at compile time. + +If IntelliJ cannot find the `StockPrice` class when you import the project: +1. Run `mvn generate-sources` manually once +2. Ensure that the IntelliJ module configuration, in Project settings, also includes `target/generated-sources/avro` as Sources. If IntelliJ does not auto-detect it, add the path manually + +These operations are only needed once. diff --git a/java/S3AvroSink/pom.xml b/java/S3AvroSink/pom.xml new file mode 100644 index 0000000..92e20b2 --- /dev/null +++ b/java/S3AvroSink/pom.xml @@ -0,0 +1,212 @@ + + + 4.0.0 + + com.amazonaws + s3-avro-sink + 1.0 + + + UTF-8 + ${project.basedir}/target + ${project.name}-${project.version} + UTF-8 + 11 + ${target.java.version} + ${target.java.version} + + 1.20.0 + 1.2.0 + 1.11.3 + 1.15.1 + + 2.23.1 + 5.8.1 + + + + + + com.amazonaws + aws-java-sdk-bom + + 1.12.782 + pom + import + + + + + + + + + org.apache.flink + flink-streaming-java + ${flink.version} + provided + + + org.apache.flink + flink-clients + ${flink.version} + provided + + + org.apache.flink + flink-runtime-web + ${flink.version} + provided + + + + + com.amazonaws + aws-kinesisanalytics-runtime + ${kda.runtime.version} + provided + + + + + org.apache.flink + flink-connector-files + ${flink.version} + provided + + + org.apache.flink + flink-s3-fs-hadoop + ${flink.version} + provided + + + + + org.apache.flink + flink-avro + ${flink.version} + + + org.apache.avro + avro + ${avro.version} + + + + + org.junit.jupiter + junit-jupiter + ${junit5.version} + test + + + + + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + runtime + + + + + + + + + org.apache.avro + avro-maven-plugin + ${avro.version} + + + generate-sources + + idl-protocol + + + ${project.basedir}/src/main/resources/avro + ${project.basedir}/src/test/resources/avro + private + String + true + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${target.java.version} + ${target.java.version} + true + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + + package + + shade + + + + + org.apache.flink:force-shading + com.google.code.findbugs:jsr305 + org.slf4j:* + org.apache.logging.log4j:* + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + com.amazonaws.services.msf.StreamingJob + + + + + + + + + + + \ No newline at end of file diff --git a/java/S3AvroSink/src/main/java/com/amazonaws/services/msf/StreamingJob.java b/java/S3AvroSink/src/main/java/com/amazonaws/services/msf/StreamingJob.java new file mode 100644 index 0000000..7a12365 --- /dev/null +++ b/java/S3AvroSink/src/main/java/com/amazonaws/services/msf/StreamingJob.java @@ -0,0 +1,112 @@ +package com.amazonaws.services.msf; + +import com.amazonaws.services.kinesisanalytics.runtime.KinesisAnalyticsRuntime; +import com.amazonaws.services.msf.avro.StockPrice; +import com.amazonaws.services.msf.datagen.StockPriceGeneratorFunction; +import org.apache.flink.api.common.eventtime.WatermarkStrategy; +import org.apache.flink.api.common.typeinfo.TypeInformation; +import org.apache.flink.api.connector.source.util.ratelimit.RateLimiterStrategy; +import org.apache.flink.connector.datagen.source.DataGeneratorSource; +import org.apache.flink.connector.file.sink.FileSink; +import org.apache.flink.core.fs.Path; +import org.apache.flink.formats.avro.AvroWriters; +import org.apache.flink.streaming.api.datastream.DataStream; +import org.apache.flink.streaming.api.environment.LocalStreamEnvironment; +import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; +import org.apache.flink.streaming.api.functions.sink.filesystem.OutputFileConfig; +import org.apache.flink.streaming.api.functions.sink.filesystem.bucketassigners.DateTimeBucketAssigner; +import org.apache.flink.streaming.api.functions.sink.filesystem.rollingpolicies.OnCheckpointRollingPolicy; +import org.apache.flink.util.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Map; +import java.util.Properties; + +public class StreamingJob { + private static final Logger LOGGER = LoggerFactory.getLogger(StreamingJob.class); + + // Name of the local JSON resource with the application properties in the same format as they are received from the Amazon Managed Service for Apache Flink runtime + private static final String LOCAL_APPLICATION_PROPERTIES_RESOURCE = "flink-application-properties-dev.json"; + + private static boolean isLocal(StreamExecutionEnvironment env) { + return env instanceof LocalStreamEnvironment; + } + + /** + * Load application properties from Amazon Managed Service for Apache Flink runtime or from a local resource, when the environment is local + */ + private static Map loadApplicationProperties(StreamExecutionEnvironment env) throws IOException { + if (isLocal(env)) { + LOGGER.info("Loading application properties from '{}'", LOCAL_APPLICATION_PROPERTIES_RESOURCE); + return KinesisAnalyticsRuntime.getApplicationProperties( + StreamingJob.class.getClassLoader() + .getResource(LOCAL_APPLICATION_PROPERTIES_RESOURCE).getPath()); + } else { + LOGGER.info("Loading application properties from Amazon Managed Service for Apache Flink"); + return KinesisAnalyticsRuntime.getApplicationProperties(); + } + } + + private static DataGeneratorSource getStockPriceDataGeneratorSource() { + long recordPerSecond = 100; + return new DataGeneratorSource<>( + new StockPriceGeneratorFunction(), + Long.MAX_VALUE, + RateLimiterStrategy.perSecond(recordPerSecond), + TypeInformation.of(StockPrice.class)); + } + + private static FileSink getParquetS3Sink(String s3UrlPath) { + return FileSink + .forBulkFormat(new Path(s3UrlPath), AvroWriters.forSpecificRecord(StockPrice.class)) + // Bucketing + .withBucketAssigner(new DateTimeBucketAssigner<>("'year='yyyy'/month='MM'/day='dd'/hour='HH/")) + // Part file rolling - this is actually the default, rolling on checkpoint + .withRollingPolicy(OnCheckpointRollingPolicy.build()) + .withOutputFileConfig(OutputFileConfig.builder() + .withPartSuffix(".avro") + .build()) + .build(); + } + + public static void main(String[] args) throws Exception { + // set up the streaming execution environment + StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); + + // Local dev specific settings + if (isLocal(env)) { + // Checkpointing and parallelism are set by Amazon Managed Service for Apache Flink when running on AWS + env.enableCheckpointing(30000); + env.setParallelism(2); + } + + // Application configuration + Properties applicationProperties = loadApplicationProperties(env).get("OutputBucket"); + String bucketName = Preconditions.checkNotNull(applicationProperties.getProperty("bucket.name"), "Bucket for S3 not defined"); + String bucketPath = Preconditions.checkNotNull(applicationProperties.getProperty("bucket.path"), "Path in S3 not defined"); + + // Build S3 URL. Strip any initial fwd slash from bucket path + String s3UrlPath = String.format("s3a://%s/%s", bucketName.trim(), bucketPath.trim().replaceFirst("^/+", "")); + LOGGER.info("Output URL: {}", s3UrlPath); + + // Source (data generator) + DataGeneratorSource source = getStockPriceDataGeneratorSource(); + + // DataStream from source + DataStream stockPrices = env.fromSource( + source, WatermarkStrategy.noWatermarks(), "data-generator").setParallelism(1); + + // Sink (Parquet files to S3) + FileSink sink = getParquetS3Sink(s3UrlPath); + + stockPrices.sinkTo(sink).name("avro-s3-sink"); + + // Also print the output + // (This is for illustration purposes only and used when running locally. No output is printed when running on Managed Flink) + stockPrices.print(); + + env.execute("Sink Avro to S3"); + } +} diff --git a/java/S3AvroSink/src/main/java/com/amazonaws/services/msf/datagen/StockPriceGeneratorFunction.java b/java/S3AvroSink/src/main/java/com/amazonaws/services/msf/datagen/StockPriceGeneratorFunction.java new file mode 100644 index 0000000..2166969 --- /dev/null +++ b/java/S3AvroSink/src/main/java/com/amazonaws/services/msf/datagen/StockPriceGeneratorFunction.java @@ -0,0 +1,20 @@ +package com.amazonaws.services.msf.datagen; + +import com.amazonaws.services.msf.avro.StockPrice; +import org.apache.commons.lang3.RandomUtils; +import org.apache.flink.connector.datagen.source.GeneratorFunction; + +import java.time.Instant; + +public class StockPriceGeneratorFunction implements GeneratorFunction { + private static final String[] TICKERS = {"AAPL", "AMZN", "MSFT", "INTC", "TBV"}; + + @Override + public StockPrice map(Long aLong) { + return new StockPrice( + TICKERS[RandomUtils.nextInt(0, TICKERS.length)], + RandomUtils.nextFloat(10, 100), + RandomUtils.nextInt(1, 10000), + Instant.now().toEpochMilli()); + } +} \ No newline at end of file diff --git a/java/S3AvroSink/src/main/resources/avro/stockprice.avdl b/java/S3AvroSink/src/main/resources/avro/stockprice.avdl new file mode 100644 index 0000000..3292400 --- /dev/null +++ b/java/S3AvroSink/src/main/resources/avro/stockprice.avdl @@ -0,0 +1,9 @@ +@namespace("com.amazonaws.services.msf.avro") +protocol In { + record StockPrice { + string symbol; + float price; + int volume; + long timestamp; + } +} \ No newline at end of file diff --git a/java/S3AvroSink/src/main/resources/flink-application-properties-dev.json b/java/S3AvroSink/src/main/resources/flink-application-properties-dev.json new file mode 100644 index 0000000..e159802 --- /dev/null +++ b/java/S3AvroSink/src/main/resources/flink-application-properties-dev.json @@ -0,0 +1,9 @@ +[ + { + "PropertyGroupId": "OutputBucket", + "PropertyMap": { + "bucket.name": "", + "bucket.path": "avrostockprices" + } + } +] \ No newline at end of file diff --git a/java/S3AvroSink/src/main/resources/log4j2.properties b/java/S3AvroSink/src/main/resources/log4j2.properties new file mode 100644 index 0000000..b7e6ea5 --- /dev/null +++ b/java/S3AvroSink/src/main/resources/log4j2.properties @@ -0,0 +1,14 @@ +appender.console.name = ConsoleAppender +appender.console.type = CONSOLE +appender.console.layout.type = PatternLayout +#appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1} - %m%n +appender.console.layout.pattern = %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + +rootLogger.level = INFO +rootLogger.appenderRef.console.ref = ConsoleAppender + +#logger.verbose.name = org.apache.flink.connector.file +#logger.verbose.level = DEBUG +#logger.verbose.additivity = false +#logger.verbose.appenderRef.console.ref = ConsoleAppender \ No newline at end of file diff --git a/java/S3AvroSource/README.md b/java/S3AvroSource/README.md new file mode 100644 index 0000000..c4ed97a --- /dev/null +++ b/java/S3AvroSource/README.md @@ -0,0 +1,57 @@ +# S3 Avro Source + + +* Flink API: DataStream API +* Language Java (11) +* Connectors: FileSystem Source and Kinesis Sink + +This example demonstrates how to read AVRO files from S3. + +The application reads AVRO records written by the [S3AvroSink](../S3AvroSink) example application, from an S3 bucket and publish them to Kinesis as JSON. + +## Prerequisites + +* An S3 bucket containing the data. The application IAM Role must allow reading from the bucket +* A Kinesis Data Stream to output the data. The application IAM Role must allow publishing to the stream + +## Runtime Configuration + +The application reads the runtime configuration from the Runtime Properties, when running on Amazon Managed Service for Apache Flink, +or, when running locally, from the [`resources/flink-application-properties-dev.json`](resources/flink-application-properties-dev.json) file located in the resources folder. + +All parameters are case-sensitive. + +| Group ID | Key | Description | +|-----------------|----------------|-------------------------------| +| `InputBucket` | `bucket.name` | Name of the source S3 bucket. | +| `InputBucket` | `bucket.path` | Base path within the bucket. | +| `OutputStream0` | `stream.arn` | ARN of the output stream. | +| `OutputStream0` | `aws.region` | Region of the output stream. | + +Every parameter in the `OutputStream0` is passed to the Kinesis client of the sink. + +To configure the application on Managed Service for Apache Flink, set up these parameter in the *Runtime properties*. + +To configure the application for running locally, edit the [json file](resources/flink-application-properties-dev.json). + +### Running in IntelliJ + +You can run this example directly in IntelliJ, without any local Flink cluster or local Flink installation. + +See [Running examples locally](../running-examples-locally.md) for details. + +### Generating data + +You can use the [S3AvroSink](../S3AvroSink) example application to write AVRO data into an S3 bucket. +Both examples use the same AVRO schema. + +## AVRO Specific Record usage + +The AVRO reader's schema definition (`avdl` file) is included as part of the source code in the `./src/main/resources/avro` folder. +The AVRO Maven Plugin is used to generate the Java object `StockPrice` at compile time. + +If IntelliJ cannot find the `StockPrice` class when you import the project: +1. Run `mvn generate-sources` manually once +2. Ensure that the IntelliJ module configuration, in Project settings, also includes `target/generated-sources/avro` as Sources. If IntelliJ does not auto-detect it, add the path manually + +These operations are only needed once. diff --git a/java/S3AvroSource/pom.xml b/java/S3AvroSource/pom.xml new file mode 100644 index 0000000..576f4a2 --- /dev/null +++ b/java/S3AvroSource/pom.xml @@ -0,0 +1,224 @@ + + + 4.0.0 + + com.amazonaws + s3-avro-source + 1.0 + + + UTF-8 + ${project.basedir}/target + ${project.name}-${project.version} + UTF-8 + 11 + ${target.java.version} + ${target.java.version} + + 1.20.0 + 1.2.0 + 1.11.3 + 1.15.1 + 5.0.0-1.20 + + 2.23.1 + 5.8.1 + + + + + + com.amazonaws + aws-java-sdk-bom + + 1.12.782 + pom + import + + + + + + + + + org.apache.flink + flink-streaming-java + ${flink.version} + provided + + + org.apache.flink + flink-clients + ${flink.version} + provided + + + org.apache.flink + flink-runtime-web + ${flink.version} + provided + + + org.apache.flink + flink-json + ${flink.version} + provided + + + + + com.amazonaws + aws-kinesisanalytics-runtime + ${kda.runtime.version} + provided + + + + + org.apache.flink + flink-connector-files + ${flink.version} + provided + + + org.apache.flink + flink-s3-fs-hadoop + ${flink.version} + provided + + + org.apache.flink + flink-connector-aws-kinesis-streams + ${aws.connector.version} + + + + + org.apache.flink + flink-avro + ${flink.version} + + + org.apache.avro + avro + ${avro.version} + + + + + org.junit.jupiter + junit-jupiter + ${junit5.version} + test + + + + + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + runtime + + + + + + + + + org.apache.avro + avro-maven-plugin + ${avro.version} + + + generate-sources + + idl-protocol + + + ${project.basedir}/src/main/resources/avro + ${project.basedir}/src/test/resources/avro + private + String + true + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${target.java.version} + ${target.java.version} + true + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + + package + + shade + + + + + org.apache.flink:force-shading + com.google.code.findbugs:jsr305 + org.slf4j:* + org.apache.logging.log4j:* + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + com.amazonaws.services.msf.StreamingJob + + + + + + + + + + + \ No newline at end of file diff --git a/java/S3AvroSource/src/main/java/com/amazonaws/services/msf/AvroSpecificRecordBulkFormat.java b/java/S3AvroSource/src/main/java/com/amazonaws/services/msf/AvroSpecificRecordBulkFormat.java new file mode 100644 index 0000000..357f484 --- /dev/null +++ b/java/S3AvroSource/src/main/java/com/amazonaws/services/msf/AvroSpecificRecordBulkFormat.java @@ -0,0 +1,44 @@ +package com.amazonaws.services.msf; + +import org.apache.avro.generic.GenericData; +import org.apache.avro.generic.GenericRecord; +import org.apache.avro.specific.SpecificData; +import org.apache.flink.api.common.typeinfo.TypeInformation; +import org.apache.flink.connector.file.src.FileSourceSplit; +import org.apache.flink.formats.avro.AbstractAvroBulkFormat; + +import java.util.function.Function; + +/** + * Simple BulkFormat to read AVRO files into SpecificRecord + * @param + */ +public class AvroSpecificRecordBulkFormat + extends AbstractAvroBulkFormat { + + private final TypeInformation producedTypeInfo; + private final org.apache.avro.Schema avroReaderSchema; + + + public AvroSpecificRecordBulkFormat(Class recordClass, org.apache.avro.Schema avroSchema) { + super(avroSchema); + avroReaderSchema = avroSchema; + producedTypeInfo = TypeInformation.of(recordClass); + } + + @Override + protected GenericRecord createReusedAvroRecord() { + return new GenericData.Record(avroReaderSchema); + } + + @Override + protected Function createConverter() { + return genericRecord -> (O) SpecificData.get().deepCopy(avroReaderSchema, genericRecord); + } + + @Override + public TypeInformation getProducedType() { + return producedTypeInfo; + } + +} diff --git a/java/S3AvroSource/src/main/java/com/amazonaws/services/msf/JsonConverter.java b/java/S3AvroSource/src/main/java/com/amazonaws/services/msf/JsonConverter.java new file mode 100644 index 0000000..d8eda3e --- /dev/null +++ b/java/S3AvroSource/src/main/java/com/amazonaws/services/msf/JsonConverter.java @@ -0,0 +1,37 @@ +package com.amazonaws.services.msf; + +import org.apache.avro.Schema; +import org.apache.avro.io.EncoderFactory; +import org.apache.avro.io.JsonEncoder; +import org.apache.avro.specific.SpecificDatumWriter; +import org.apache.flink.api.common.functions.OpenContext; +import org.apache.flink.api.common.functions.RichMapFunction; + +import java.io.ByteArrayOutputStream; + +/** + * Simple converter for Avro records to JSON. Not designed for production. + */ +public class JsonConverter extends RichMapFunction { + + private final Schema avroSchema; + private transient SpecificDatumWriter writer; + + public JsonConverter(Schema avroSchema) { + this.avroSchema = avroSchema; + } + + @Override + public void open(OpenContext openContext) throws Exception { + this.writer = new SpecificDatumWriter<>(avroSchema); + } + + @Override + public String map(T record) throws Exception { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonEncoder encoder = EncoderFactory.get().jsonEncoder(record.getSchema(), outputStream); + writer.write(record, encoder); + encoder.flush(); + return outputStream.toString(); + } +} diff --git a/java/S3AvroSource/src/main/java/com/amazonaws/services/msf/StreamingJob.java b/java/S3AvroSource/src/main/java/com/amazonaws/services/msf/StreamingJob.java new file mode 100644 index 0000000..5ef854f --- /dev/null +++ b/java/S3AvroSource/src/main/java/com/amazonaws/services/msf/StreamingJob.java @@ -0,0 +1,109 @@ +package com.amazonaws.services.msf; + +import com.amazonaws.services.kinesisanalytics.runtime.KinesisAnalyticsRuntime; +import com.amazonaws.services.msf.avro.StockPrice; +import org.apache.avro.Schema; +import org.apache.avro.specific.SpecificRecord; +import org.apache.flink.api.common.eventtime.WatermarkStrategy; +import org.apache.flink.api.common.serialization.SerializationSchema; +import org.apache.flink.api.common.serialization.SimpleStringSchema; +import org.apache.flink.api.common.typeinfo.TypeInformation; +import org.apache.flink.connector.file.src.FileSource; +import org.apache.flink.connector.kinesis.sink.KinesisStreamsSink; +import org.apache.flink.core.fs.Path; +import org.apache.flink.formats.json.JsonSerializationSchema; +import org.apache.flink.streaming.api.datastream.DataStream; +import org.apache.flink.streaming.api.environment.LocalStreamEnvironment; +import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; +import org.apache.flink.util.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.time.Duration; +import java.util.Map; +import java.util.Properties; + +public class StreamingJob { + private static final Logger LOGGER = LoggerFactory.getLogger(StreamingJob.class); + + // Name of the local JSON resource with the application properties in the same format as they are received from the Amazon Managed Service for Apache Flink runtime + private static final String LOCAL_APPLICATION_PROPERTIES_RESOURCE = "flink-application-properties-dev.json"; + + private static boolean isLocal(StreamExecutionEnvironment env) { + return env instanceof LocalStreamEnvironment; + } + + /** + * Load application properties from Amazon Managed Service for Apache Flink runtime or from a local resource, when the environment is local + */ + private static Map loadApplicationProperties(StreamExecutionEnvironment env) throws IOException { + if (isLocal(env)) { + LOGGER.info("Loading application properties from '{}'", LOCAL_APPLICATION_PROPERTIES_RESOURCE); + return KinesisAnalyticsRuntime.getApplicationProperties( + StreamingJob.class.getClassLoader() + .getResource(LOCAL_APPLICATION_PROPERTIES_RESOURCE).getPath()); + } else { + LOGGER.info("Loading application properties from Amazon Managed Service for Apache Flink"); + return KinesisAnalyticsRuntime.getApplicationProperties(); + } + } + + + private static KinesisStreamsSink createKinesisSink(Properties outputProperties, final SerializationSchema serializationSchema) { + final String outputStreamArn = outputProperties.getProperty("stream.arn"); + return KinesisStreamsSink.builder() + .setStreamArn(outputStreamArn) + .setKinesisClientProperties(outputProperties) + .setSerializationSchema(serializationSchema) + .setPartitionKeyGenerator(element -> String.valueOf(element.hashCode())) + .build(); + } + + private static FileSource createAvroFileSource(Properties sourceProperties, Class avroRecordClass, Schema avroSchema) { + String bucketName = Preconditions.checkNotNull(sourceProperties.getProperty("bucket.name"), "Bucket for S3 not defined"); + String bucketPath = Preconditions.checkNotNull(sourceProperties.getProperty("bucket.path"), "Path in S3 not defined"); + + // Build S3 URL. Strip any initial fwd slash from bucket path + String s3UrlPath = String.format("s3a://%s/%s", bucketName.trim(), bucketPath.trim().replaceFirst("^/+", "")); + LOGGER.info("Input URL: {}", s3UrlPath); + + // A custom BulkFormat is required to read AVRO files + AvroSpecificRecordBulkFormat bulkFormat = new AvroSpecificRecordBulkFormat<>(avroRecordClass, avroSchema); + + return FileSource.forBulkFileFormat(bulkFormat, new Path(s3UrlPath)) + .monitorContinuously(Duration.ofSeconds(10)) + .build(); + } + + public static void main(String[] args) throws Exception { + // set up the streaming execution environment + StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); + + // Application configuration + Map applicationPropertiesMap = loadApplicationProperties(env); + + // Source reading AVRO files into a SpecificRecord + FileSource avroFileSource = createAvroFileSource(applicationPropertiesMap.get("InputBucket"), StockPrice.class, StockPrice.SCHEMA$); + + // DataStream from source + DataStream stockPrices = env.fromSource( + avroFileSource, WatermarkStrategy.noWatermarks(), "avro-source", TypeInformation.of(StockPrice.class)).setParallelism(1); + + // Convert the AVRO record into a String containing JSON + DataStream jsonStockPrices = stockPrices.map(new JsonConverter<>(StockPrice.SCHEMA$)); + + // Output the Strings to Kinesis + // (You cannot use JsonSerializationSchema to convert AVRO specific records into JSON, directly) + KinesisStreamsSink kinesisSink = createKinesisSink(applicationPropertiesMap.get("OutputStream0"), new SimpleStringSchema()); + + // Attach the sink + jsonStockPrices.sinkTo(kinesisSink); + + // Also print the output + // (This is for illustration purposes only and used when running locally. No output is printed when running on Managed Flink) + jsonStockPrices.print(); + + env.execute("Source Avro from S3"); + } +} diff --git a/java/S3AvroSource/src/main/resources/avro/stockprice.avdl b/java/S3AvroSource/src/main/resources/avro/stockprice.avdl new file mode 100644 index 0000000..3292400 --- /dev/null +++ b/java/S3AvroSource/src/main/resources/avro/stockprice.avdl @@ -0,0 +1,9 @@ +@namespace("com.amazonaws.services.msf.avro") +protocol In { + record StockPrice { + string symbol; + float price; + int volume; + long timestamp; + } +} \ No newline at end of file diff --git a/java/S3AvroSource/src/main/resources/flink-application-properties-dev.json b/java/S3AvroSource/src/main/resources/flink-application-properties-dev.json new file mode 100644 index 0000000..a70379c --- /dev/null +++ b/java/S3AvroSource/src/main/resources/flink-application-properties-dev.json @@ -0,0 +1,16 @@ +[ + { + "PropertyGroupId": "InputBucket", + "PropertyMap": { + "bucket.name": "", + "bucket.path": "avrostockprices" + } + }, + { + "PropertyGroupId": "OutputStream0", + "PropertyMap": { + "stream.arn": "arn:aws:kinesis:us-east-1::stream/", + "aws.region": "us-east-1" + } + } +] \ No newline at end of file diff --git a/java/S3AvroSource/src/main/resources/log4j2.properties b/java/S3AvroSource/src/main/resources/log4j2.properties new file mode 100644 index 0000000..b7e6ea5 --- /dev/null +++ b/java/S3AvroSource/src/main/resources/log4j2.properties @@ -0,0 +1,14 @@ +appender.console.name = ConsoleAppender +appender.console.type = CONSOLE +appender.console.layout.type = PatternLayout +#appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1} - %m%n +appender.console.layout.pattern = %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + +rootLogger.level = INFO +rootLogger.appenderRef.console.ref = ConsoleAppender + +#logger.verbose.name = org.apache.flink.connector.file +#logger.verbose.level = DEBUG +#logger.verbose.additivity = false +#logger.verbose.appenderRef.console.ref = ConsoleAppender \ No newline at end of file diff --git a/java/S3ParquetSink/README.md b/java/S3ParquetSink/README.md index 21027f6..24aa367 100644 --- a/java/S3ParquetSink/README.md +++ b/java/S3ParquetSink/README.md @@ -6,6 +6,7 @@ * Connectors: FileSystem Sink (and DataGen connector) This example demonstrates how to write Parquet files to S3. +See the [S3 Parquet Source](../S3ParquetSource) example to read Parquet files from S3. The example generates random stock price data using the DataGen connector and writes to S3 as Parquet files with a bucketing in the format `year=yyyy/month=MM/day=dd/hour=HH/` and rotating files on checkpoint. @@ -13,6 +14,7 @@ a bucketing in the format `year=yyyy/month=MM/day=dd/hour=HH/` and rotating file Note that FileSystem sink commit the writes on checkpoint. For this reason, checkpoint are programmatically enabled when running locally. When running on Managed Flink checkpoints are controlled by the application configuration and enabled by default. + ## Prerequisites * An S3 bucket for writing data. The application IAM Role must allow writing to the bucket diff --git a/java/S3ParquetSource/README.md b/java/S3ParquetSource/README.md new file mode 100644 index 0000000..b9f721d --- /dev/null +++ b/java/S3ParquetSource/README.md @@ -0,0 +1,76 @@ +# S3 Parquet Source + +* Flink API: DataStream API +* Language Java (11) +* Connectors: FileSystem Source and Kinesis Sink + +This example demonstrates how to read Parquet files from S3. + +The application reads records written by the [S3ParquetSink](../S3ParquetSink) example application, from an S3 bucket and publish them to Kinesis as JSON. + +The records read from Parquet are deserialized into AVRO specific objects. + +## Important note about reading Parquet + +Flink relies on Hadoop libraries to read Parquet files from S3. +Because of the [Flink classloading system](https://nightlies.apache.org/flink/flink-docs-release-1.20/docs/ops/debugging/debugging_classloading/) introduced after Flink 1.5, +and because Flink S3 File System is installed in the cluster to support checkpointing, reading Parquet files normally causes a class not found exception in some Hadoop classes, +even if you include these classes in the uber-jar. + +This examples demonstrates a workaround to this issue. The `org.apache.flink.runtime.util.HadoopUtils` class is replaced +by a custom implementation, and some Hadoop classes are shaded (remapped) by the `maven-shade-plugin` used to build the uber-jar. + +Note that this workaround works in this specific case and may not help in other cases of Hadoop class conflict you may encounter. + +## Prerequisites + +* An S3 bucket containing the data. The application IAM Role must allow reading from the bucket +* A Kinesis Data Stream to output the data. The application IAM Role must allow publishing to the stream + +## Runtime Configuration + +The application reads the runtime configuration from the Runtime Properties, when running on Amazon Managed Service for Apache Flink, +or, when running locally, from the [`resources/flink-application-properties-dev.json`](resources/flink-application-properties-dev.json) file located in the resources folder. + +All parameters are case-sensitive. + +| Group ID | Key | Description | +|-----------------|--------------------------------|---------------------------------------------------------------------------------| +| `InputBucket` | `bucket.name` | Name of the destination S3 bucket. | +| `InputBucket` | `bucket.path` | Base path within the bucket. | +| `InputBucket` | `discovery.interval.seconds` | Inteval the bucket path is scanned for new files, in seconds (default = 30 sec) | +| `OutputStream0` | `stream.arn` | ARN of the output stream. | +| `OutputStream0` | `aws.region` | Region of the output stream. | + +Every parameter in the `OutputStream0` is passed to the Kinesis client of the sink. + +To configure the application on Managed Service for Apache Flink, set up these parameter in the *Runtime properties*. + +To configure the application for running locally, edit the [json file](resources/flink-application-properties-dev.json). + +### Running in IntelliJ + +You can run this example directly in IntelliJ, without any local Flink cluster or local Flink installation. + +See [Running examples locally](../running-examples-locally.md) for details. + +Note: when running locally, the application also prints the records to the console. +Records starts appearing about 30 seconds after the application is fully initialized and starts reading from S3. + +### Generating data + +You can use the [S3ParquetSink](../S3ParquetSink) example application to write Parquet data into an S3 bucket. +Both examples use the same AVRO schema. + +## AVRO Specific Record usage + +The records read from Parquet are deserialized into AVRO specific objects. + +The AVRO reader's schema definition (`avdl` file) is included as part of the source code in the `./src/main/resources/avro` folder. +The AVRO Maven Plugin is used to generate the Java object `StockPrice` at compile time. + +If IntelliJ cannot find the `StockPrice` class when you import the project: +1. Run `mvn generate-sources` manually once +2. Ensure that the IntelliJ module configuration, in Project settings, also includes `target/generated-sources/avro` as Sources. If IntelliJ does not auto-detect it, add the path manually + +These operations are only needed once. diff --git a/java/S3ParquetSource/pom.xml b/java/S3ParquetSource/pom.xml new file mode 100644 index 0000000..f8eb5ae --- /dev/null +++ b/java/S3ParquetSource/pom.xml @@ -0,0 +1,400 @@ + + + 4.0.0 + + com.amazonaws + s3-parquet-source + 1.0 + + + UTF-8 + ${project.basedir}/target + ${project.name}-${project.version} + UTF-8 + 11 + ${target.java.version} + ${target.java.version} + + 1.20.0 + 1.2.0 + 1.11.3 + 1.12.3 + 3.3.4 + 5.0.0-1.20 + 2.23.1 + 5.8.1 + + + + + + com.amazonaws + aws-java-sdk-bom + + 1.12.782 + pom + import + + + + + + + + + org.apache.flink + flink-streaming-java + ${flink.version} + provided + + + org.apache.flink + flink-clients + ${flink.version} + provided + + + org.apache.flink + flink-runtime-web + ${flink.version} + provided + + + org.apache.flink + flink-json + ${flink.version} + provided + + + + + com.amazonaws + aws-kinesisanalytics-runtime + ${kda.runtime.version} + provided + + + + + org.apache.flink + flink-connector-aws-kinesis-streams + ${aws.connector.version} + + + org.apache.flink + flink-connector-files + ${flink.version} + provided + + + + + org.apache.flink + flink-s3-fs-hadoop + ${flink.version} + provided + + + + + org.apache.hadoop + hadoop-client + ${hadoop.version} + + + + + org.apache.hadoop + hadoop-yarn-api + + + org.apache.hadoop + hadoop-yarn-client + + + org.apache.hadoop + hadoop-mapreduce-client-jobclient + + + jdk.tools + jdk.tools + + + com.jcraft + jsch + + + com.sun.jersey + jersey-core + + + com.sun.jersey + jersey-servlet + + + com.sun.jersey + jersey-json + + + com.sun.jersey + jersey-server + + + org.apache.avro + avro + + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + jetty-util + + + org.eclipse.jetty + jetty-servlet + + + org.eclipse.jetty + jetty-webapp + + + javax.servlet + javax.servlet-api + + + javax.servlet.jsp + jsp-api + + + org.apache.kerby + kerb-simplekdc + + + org.apache.curator + curator-client + + + org.apache.curator + curator-framework + + + org.apache.curator + curator-recipes + + + org.apache.zookeeper + zookeeper + + + commons-net + commons-net + + + commons-cli + commons-cli + + + commons-codec + commons-codec + + + com.google.protobuf + protobuf-java + + + com.google.code.gson + gson + + + org.apache.httpcomponents + httpclient + + + org.apache.commons + commons-math3 + + + com.nimbusds + nimbus-jose-jwt + + + net.minidev + json-smart + + + + + ch.qos.reload4j + reload4j + + + org.slf4j + * + + + org.slf4j + slf4j-log4j12 + + + log4j + log4j + + + + + + + org.apache.flink + flink-parquet + ${flink.version} + + + org.apache.parquet + parquet-avro + ${parquet.avro.version} + + + + + org.apache.flink + flink-avro + ${flink.version} + + + org.apache.avro + avro + ${avro.version} + + + + + org.junit.jupiter + junit-jupiter + ${junit5.version} + test + + + + + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + runtime + + + + + ${jar.finalName} + + + + + org.apache.avro + avro-maven-plugin + ${avro.version} + + + generate-sources + + idl-protocol + + + ${project.basedir}/src/main/resources/avro + ${project.basedir}/src/test/resources/avro + private + String + true + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${target.java.version} + ${target.java.version} + true + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + + package + + shade + + + + + org.apache.flink:force-shading + com.google.code.findbugs:jsr305 + org.slf4j:* + org.apache.logging.log4j:* + + + + + *:* + + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + com.amazonaws.services.msf.S3ParquetToKinesisJob + + + + + + + org.apache.hadoop.conf + shaded.org.apache.hadoop.conf + + + org.apache.flink.runtime.util.HadoopUtils + shadow.org.apache.flink.runtime.util.HadoopUtils + + + + + + + + + + + \ No newline at end of file diff --git a/java/S3ParquetSource/src/main/java/com/amazonaws/services/msf/JsonConverter.java b/java/S3ParquetSource/src/main/java/com/amazonaws/services/msf/JsonConverter.java new file mode 100644 index 0000000..d8eda3e --- /dev/null +++ b/java/S3ParquetSource/src/main/java/com/amazonaws/services/msf/JsonConverter.java @@ -0,0 +1,37 @@ +package com.amazonaws.services.msf; + +import org.apache.avro.Schema; +import org.apache.avro.io.EncoderFactory; +import org.apache.avro.io.JsonEncoder; +import org.apache.avro.specific.SpecificDatumWriter; +import org.apache.flink.api.common.functions.OpenContext; +import org.apache.flink.api.common.functions.RichMapFunction; + +import java.io.ByteArrayOutputStream; + +/** + * Simple converter for Avro records to JSON. Not designed for production. + */ +public class JsonConverter extends RichMapFunction { + + private final Schema avroSchema; + private transient SpecificDatumWriter writer; + + public JsonConverter(Schema avroSchema) { + this.avroSchema = avroSchema; + } + + @Override + public void open(OpenContext openContext) throws Exception { + this.writer = new SpecificDatumWriter<>(avroSchema); + } + + @Override + public String map(T record) throws Exception { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonEncoder encoder = EncoderFactory.get().jsonEncoder(record.getSchema(), outputStream); + writer.write(record, encoder); + encoder.flush(); + return outputStream.toString(); + } +} diff --git a/java/S3ParquetSource/src/main/java/com/amazonaws/services/msf/S3ParquetToKinesisJob.java b/java/S3ParquetSource/src/main/java/com/amazonaws/services/msf/S3ParquetToKinesisJob.java new file mode 100644 index 0000000..5bd00c6 --- /dev/null +++ b/java/S3ParquetSource/src/main/java/com/amazonaws/services/msf/S3ParquetToKinesisJob.java @@ -0,0 +1,108 @@ +package com.amazonaws.services.msf; + +import com.amazonaws.services.kinesisanalytics.runtime.KinesisAnalyticsRuntime; +import com.amazonaws.services.msf.avro.StockPrice; +import org.apache.avro.specific.SpecificRecordBase; +import org.apache.flink.api.common.eventtime.WatermarkStrategy; +import org.apache.flink.api.common.serialization.SerializationSchema; +import org.apache.flink.api.common.serialization.SimpleStringSchema; +import org.apache.flink.connector.file.src.FileSource; +import org.apache.flink.connector.file.src.reader.StreamFormat; +import org.apache.flink.connector.kinesis.sink.KinesisStreamsSink; +import org.apache.flink.core.fs.Path; +import org.apache.flink.formats.json.JsonSerializationSchema; +import org.apache.flink.formats.parquet.avro.AvroParquetReaders; +import org.apache.flink.streaming.api.datastream.DataStream; +import org.apache.flink.streaming.api.environment.LocalStreamEnvironment; +import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; +import org.apache.flink.util.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.time.Duration; +import java.util.Map; +import java.util.Properties; + +public class S3ParquetToKinesisJob { + private static final Logger LOGGER = LoggerFactory.getLogger(S3ParquetToKinesisJob.class); + + // Name of the local JSON resource with the application properties in the same format as they are received from the Amazon Managed Service for Apache Flink runtime + private static final String LOCAL_APPLICATION_PROPERTIES_RESOURCE = "flink-application-properties-dev.json"; + + private static boolean isLocal(StreamExecutionEnvironment env) { + return env instanceof LocalStreamEnvironment; + } + + /** + * Load application properties from Amazon Managed Service for Apache Flink runtime or from a local resource, when the environment is local + */ + private static Map loadApplicationProperties(StreamExecutionEnvironment env) throws IOException { + if (isLocal(env)) { + LOGGER.info("Loading application properties from '{}'", LOCAL_APPLICATION_PROPERTIES_RESOURCE); + return KinesisAnalyticsRuntime.getApplicationProperties( + S3ParquetToKinesisJob.class.getClassLoader() + .getResource(LOCAL_APPLICATION_PROPERTIES_RESOURCE).getPath()); + } else { + LOGGER.info("Loading application properties from Amazon Managed Service for Apache Flink"); + return KinesisAnalyticsRuntime.getApplicationProperties(); + } + } + + private static FileSource createParquetS3Source(Properties applicationProperties, final Class clazz) { + String bucketName = Preconditions.checkNotNull(applicationProperties.getProperty("bucket.name"), "Bucket for S3 not defined"); + String bucketPath = Preconditions.checkNotNull(applicationProperties.getProperty("bucket.path"), "Path in S3 not defined"); + int discoveryIntervalSec = Integer.parseInt(applicationProperties.getProperty("bucket.discovery.interval.sec", "30")); + + // Build S3 URL. Strip any initial fwd slash from bucket path + String s3UrlPath = String.format("s3a://%s/%s", bucketName.trim(), bucketPath.trim().replaceFirst("^/+", "")); + LOGGER.info("Input URL: {}", s3UrlPath); + LOGGER.info("Discovery interval: {} sec", discoveryIntervalSec); + + return FileSource + .forRecordStreamFormat(AvroParquetReaders.forSpecificRecord(clazz), new Path(s3UrlPath)) + .monitorContinuously(Duration.ofSeconds(discoveryIntervalSec)) + .build(); + } + + private static KinesisStreamsSink createKinesisSink(Properties outputProperties, final SerializationSchema serializationSchema) { + final String outputStreamArn = outputProperties.getProperty("stream.arn"); + return KinesisStreamsSink.builder() + .setStreamArn(outputStreamArn) + .setKinesisClientProperties(outputProperties) + .setSerializationSchema(serializationSchema) + .setPartitionKeyGenerator(element -> String.valueOf(element.hashCode())) + .build(); + } + + public static void main(String[] args) throws Exception { + // set up the streaming execution environment + StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); + + final Map applicationProperties = loadApplicationProperties(env); + LOGGER.info("Application properties: {}", applicationProperties); + + FileSource source = createParquetS3Source(applicationProperties.get("InputBucket"), StockPrice.class); + KinesisStreamsSink sink = createKinesisSink(applicationProperties.get("OutputStream0"),new SimpleStringSchema()); + + // DataStream from source + DataStream stockPrices = env.fromSource( + source, WatermarkStrategy.noWatermarks(), "parquet-source").setParallelism(1); + + // Convert to JSON + // (We cannot use JsonSerializationSchema on the sink with an AVRO specific object) + DataStream jsonPrices = stockPrices + .map(new JsonConverter<>(StockPrice.getClassSchema())).uid("json-converter"); + + // Sink JSON to Kinesis + jsonPrices.sinkTo(sink).name("kinesis-sink"); + + // Also print to stdout for local testing + // (Do not print records to stdout in a production application. This adds overhead and is not visible when deployed on Managed Flink) + jsonPrices.print().name("stdout-sink"); + + env.execute("Source Parquet from S3"); + } + + +} diff --git a/java/S3ParquetSource/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java b/java/S3ParquetSource/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java new file mode 100644 index 0000000..bc8cd8c --- /dev/null +++ b/java/S3ParquetSource/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java @@ -0,0 +1,120 @@ +package org.apache.flink.runtime.util; + +import org.apache.flink.api.java.tuple.Tuple2; +import org.apache.flink.util.FlinkRuntimeException; +import org.apache.flink.util.Preconditions; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.io.Text; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.hadoop.security.token.Token; +import org.apache.hadoop.security.token.TokenIdentifier; +import org.apache.hadoop.util.VersionInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; + + +/** + * This class is a copy of org.apache.flink.runtime.util.HadoopUtils with the getHadoopConfiguration() method replaced to + * return an org.apache.hadoop.conf.Configuration instead of org.apache.hadoop.hdfs.HdfsConfiguration. + * + * This class is then shaded, along with org.apache.hadoop.conf.*, to avoid conflicts with the same classes provided by + * org.apache.flink:flink-s3-fs-hadoop, which is normally installed as plugin in Flink when S3. + * + * Other methods are copied from the original class. + */ +public class HadoopUtils { + private static final Logger LOG = LoggerFactory.getLogger(HadoopUtils.class); + + static final Text HDFS_DELEGATION_TOKEN_KIND = new Text("HDFS_DELEGATION_TOKEN"); + + /** + * This method has been re-implemented to always return a org.apache.hadoop.conf.Configuration + */ + public static Configuration getHadoopConfiguration( + org.apache.flink.configuration.Configuration flinkConfiguration) { + return new Configuration(false); + } + + public static boolean isKerberosSecurityEnabled(UserGroupInformation ugi) { + return UserGroupInformation.isSecurityEnabled() + && ugi.getAuthenticationMethod() + == UserGroupInformation.AuthenticationMethod.KERBEROS; + } + + + public static boolean areKerberosCredentialsValid( + UserGroupInformation ugi, boolean useTicketCache) { + Preconditions.checkState(isKerberosSecurityEnabled(ugi)); + + // note: UGI::hasKerberosCredentials inaccurately reports false + // for logins based on a keytab (fixed in Hadoop 2.6.1, see HADOOP-10786), + // so we check only in ticket cache scenario. + if (useTicketCache && !ugi.hasKerberosCredentials()) { + if (hasHDFSDelegationToken(ugi)) { + LOG.warn( + "Hadoop security is enabled but current login user does not have Kerberos credentials, " + + "use delegation token instead. Flink application will terminate after token expires."); + return true; + } else { + LOG.error( + "Hadoop security is enabled, but current login user has neither Kerberos credentials " + + "nor delegation tokens!"); + return false; + } + } + + return true; + } + + /** + * Indicates whether the user has an HDFS delegation token. + */ + public static boolean hasHDFSDelegationToken(UserGroupInformation ugi) { + Collection> usrTok = ugi.getTokens(); + for (Token token : usrTok) { + if (token.getKind().equals(HDFS_DELEGATION_TOKEN_KIND)) { + return true; + } + } + return false; + } + + /** + * Checks if the Hadoop dependency is at least the given version. + */ + public static boolean isMinHadoopVersion(int major, int minor) throws FlinkRuntimeException { + final Tuple2 hadoopVersion = getMajorMinorBundledHadoopVersion(); + int maj = hadoopVersion.f0; + int min = hadoopVersion.f1; + + return maj > major || (maj == major && min >= minor); + } + + /** + * Checks if the Hadoop dependency is at most the given version. + */ + public static boolean isMaxHadoopVersion(int major, int minor) throws FlinkRuntimeException { + final Tuple2 hadoopVersion = getMajorMinorBundledHadoopVersion(); + int maj = hadoopVersion.f0; + int min = hadoopVersion.f1; + + return maj < major || (maj == major && min < minor); + } + + private static Tuple2 getMajorMinorBundledHadoopVersion() { + String versionString = VersionInfo.getVersion(); + String[] versionParts = versionString.split("\\."); + + if (versionParts.length < 2) { + throw new FlinkRuntimeException( + "Cannot determine version of Hadoop, unexpected version string: " + + versionString); + } + + int maj = Integer.parseInt(versionParts[0]); + int min = Integer.parseInt(versionParts[1]); + return Tuple2.of(maj, min); + } +} diff --git a/java/S3ParquetSource/src/main/resources/avro/stockprice.avdl b/java/S3ParquetSource/src/main/resources/avro/stockprice.avdl new file mode 100644 index 0000000..3292400 --- /dev/null +++ b/java/S3ParquetSource/src/main/resources/avro/stockprice.avdl @@ -0,0 +1,9 @@ +@namespace("com.amazonaws.services.msf.avro") +protocol In { + record StockPrice { + string symbol; + float price; + int volume; + long timestamp; + } +} \ No newline at end of file diff --git a/java/S3ParquetSource/src/main/resources/flink-application-properties-dev.json b/java/S3ParquetSource/src/main/resources/flink-application-properties-dev.json new file mode 100644 index 0000000..b4b8fef --- /dev/null +++ b/java/S3ParquetSource/src/main/resources/flink-application-properties-dev.json @@ -0,0 +1,17 @@ +[ + { + "PropertyGroupId": "InputBucket", + "PropertyMap": { + "bucket.name": "", + "bucket.path": "parquetstockprices", + "discovery.interval.seconds": "30" + } + }, + { + "PropertyGroupId": "OutputStream0", + "PropertyMap": { + "stream.arn": "arn:aws:kinesis:us-east-1::stream/", + "aws.region": "us-east-1" + } + } +] \ No newline at end of file diff --git a/java/S3ParquetSource/src/main/resources/log4j2.properties b/java/S3ParquetSource/src/main/resources/log4j2.properties new file mode 100644 index 0000000..2faa1c8 --- /dev/null +++ b/java/S3ParquetSource/src/main/resources/log4j2.properties @@ -0,0 +1,14 @@ +appender.console.name = ConsoleAppender +appender.console.type = CONSOLE +appender.console.layout.type = PatternLayout +#appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1} - %m%n +appender.console.layout.pattern = %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + +rootLogger.level = INFO +rootLogger.appenderRef.console.ref = ConsoleAppender + +logger.verbose.name = org.apache.flink.connector.file +logger.verbose.level = DEBUG +logger.verbose.additivity = false +logger.verbose.appenderRef.console.ref = ConsoleAppender \ No newline at end of file diff --git a/java/SQSSink/src/main/resources/flink-application-properties-dev.json b/java/SQSSink/src/main/resources/flink-application-properties-dev.json index 287ed36..4fd412d 100644 --- a/java/SQSSink/src/main/resources/flink-application-properties-dev.json +++ b/java/SQSSink/src/main/resources/flink-application-properties-dev.json @@ -2,7 +2,7 @@ { "PropertyGroupId": "OutputQueue0", "PropertyMap": { - "sqs-url": "https://sqs.us-east-1.amazonaws.com/012345678901/MyTestQueue", + "sqs-url": "https://sqs.us-east-1.amazonaws.com//MyTestQueue", "aws.region": "us-east-1" } } diff --git a/java/Serialization/README.md b/java/Serialization/README.md new file mode 100644 index 0000000..dfa39ac --- /dev/null +++ b/java/Serialization/README.md @@ -0,0 +1,8 @@ +# Serialization Examples + +Examples demonstrating data serialization patterns and custom type handling in Amazon Managed Service for Apache Flink. + +## Table of Contents + +### Custom Serialization +- [**Custom TypeInfo**](./CustomTypeInfo) - Using custom TypeInformation to avoid Kryo serialization fallback diff --git a/java/SideOutputs/src/main/resources/flink-application-properties-dev.json b/java/SideOutputs/src/main/resources/flink-application-properties-dev.json index 098cd56..174a8d1 100644 --- a/java/SideOutputs/src/main/resources/flink-application-properties-dev.json +++ b/java/SideOutputs/src/main/resources/flink-application-properties-dev.json @@ -3,7 +3,7 @@ "PropertyGroupId": "ProcessedOutputStream", "PropertyMap": { "aws.region": "us-east-1", - "stream.arn": "ExampleOutputStream-ARN-GOES-HERE", + "stream.arn": "arn:aws:kinesis:us-east-1::stream/ExampleOutputStream", "flink.stream.initpos": "LATEST" } }, @@ -11,7 +11,7 @@ "PropertyGroupId": "DLQOutputStream", "PropertyMap": { "aws.region": "us-east-1", - "stream.arn": "DLQStream-ARN-GOES-HERE", + "stream.arn": "arn:aws:kinesis:us-east-1::stream/DLQStream", "flink.stream.initpos": "LATEST" } } diff --git a/java/pom.xml b/java/pom.xml index eb260a0..08543f2 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -30,14 +30,20 @@ KafkaConfigProviders/Kafka-mTLS-Keystore-Sql-ConfigProviders KafkaConnectors KinesisConnectors + KinesisSourceDeaggregation DynamoDBStreamSource KinesisFirehoseSink S3ParquetSink + S3ParquetSource S3Sink Windowing Serialization/CustomTypeInfo SideOutputs PrometheusSink SQSSink + S3AvroSink + S3AvroSource + FlinkCDC/FlinkCDCSQLServerSource + FlinkDataGenerator \ No newline at end of file diff --git a/python/GettingStarted/README.md b/python/GettingStarted/README.md index 65e2a81..ec12619 100644 --- a/python/GettingStarted/README.md +++ b/python/GettingStarted/README.md @@ -8,6 +8,15 @@ Sample PyFlink application reading from and writing to Kinesis Data Stream. * Language: Python This example provides the basic skeleton for a PyFlink application. +It shows how to correctly package JAR dependencies such as Flink connectors. + + +> This example does not include external Python dependencies. +> To learn **how to package Pythion dependencies** check out these two examples: +> 1. [PythonDependencies](../PythonDependencies): how to download Python dependencies at runtime using `requirements.txt` +> 2. [PackagedPythonDependencies](../PackagedPythonDependencies): how to package dependencies with the application artifact + +--- The application is written in Python, but operators are defined using SQL. This is a popular way of defining applications in PyFlink, but not the only one. You could attain the same results diff --git a/python/GettingStarted/pom.xml b/python/GettingStarted/pom.xml index 322d1d3..4bea630 100644 --- a/python/GettingStarted/pom.xml +++ b/python/GettingStarted/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.amazonaws - managed-flink-pyfink-getting-started + managed-flink-pyflink-getting-started 1.0.0 diff --git a/python/IcebergSink/README.md b/python/IcebergSink/README.md new file mode 100644 index 0000000..720b09c --- /dev/null +++ b/python/IcebergSink/README.md @@ -0,0 +1,249 @@ +## Example of writing to Apache Iceberg with PyFlink + +Example showing a PyFlink application writing to Iceberg table in S3. + +* Flink version: 1.20 +* Flink API: Table API & SQL +* Flink Connectors: Apache Iceberg & Flink S3 +* Language: Python + +This application demonstrates settings up Apache Iceberg table as sink. + +The application is written in Python, but operators are defined using SQL. This is a popular way of defining applications in PyFlink, but not the only one. You could attain the same results using Table API ar DataStream API, in Python. + +The job can run both on Amazon Managed Service for Apache Flink, and locally for development. +--- + +### Dependency Shading + +This project uses Maven Shade Plugin to handle dependency conflicts: + +```xml + + + org.apache.hadoop.conf + shaded.org.apache.hadoop.conf + + + org.apache.flink.runtime.util.HadoopUtils + shadow.org.apache.flink.runtime.util.HadoopUtils + + +``` + +#### Why this matters: + +* Prevents classpath conflicts with Hadoop/Flink internals +* Ensures our bundled dependencies don't clash with AWS Managed Flink's runtime +* Required for stable operation with Iceberg connector + +--- + +## Excluded Java Versions in maven-shade-plugin + +Apache Flink 1.20 [only supports Java 11 as non-experimental](https://nightlies.apache.org/flink/flink-docs-release-1.20/docs/deployment/java_compatibility/). +Amazon Managed Service for Apache Flink currently runs on Java 11. +Any Java code must be compiled with a target java version 11. + +We have excluded certain Java versions to avoid errors caused by [multi-release JARs](https://openjdk.org/jeps/238) where the META-INF/versions/XX directories contain **Java version-specific class files**. + +Example: A dependency might include optimized code for Java 21+ in META-INF/versions/21/.... + +### Why Exclude Specific Versions + +* Avoid Compatibility Issues - If you include JAR dependencies compiled for Java 21/22 you may face errors like *java.lang.UnsupportedClassVersionError: Unsupported major.minor version 65* +* Prevent Conflicts - Some libraries include multi-release JARs that conflict with Flink’s dependencies when merged into a fat JAR. + +### Requirements + +#### Development and build environment requirements + +* Python 3.11 +* PyFlink library: `apache-flink==1.20.0` +* Java JDK 11+ and Maven + +> ⚠️ As of 2024-06-27, the Flink Python library 1.19.x may fail installing on Python 3.12. +> We recommend using Python 3.11 for development, the same Python version used by Amazon Managed Service for Apache Flink +> runtime 1.20. + +> JDK and Maven are used to download and package any required Flink dependencies, e.g. connectors, and + to package the application as `.zip` file, for deployment to Amazon Managed Service for Apache Flink. + +> This code has been tested and compiled using OpenJDK 11.0.22 and Maven 3.9.6 + +#### External dependencies + +The application requires Amazon S3 bucket to write the Iceberg table data. The application also needs appropriate permissions for Amazon S3 and AWS Glue as [discussed below](#iam-permissions). + +The target Iceberg table parameters are defined in the configuration (see [below](#runtime-configuration)). + +#### IAM permissions + +The application must have sufficient permissions to read and write to AWS Glue Data Catalog where catalog details will be stored and Amazon S3 bucket where Iceberg table data and metadata will be stored. + +When running locally, you need active valid AWS credentials that allow reading and writing catalog to Glue and data to S3 bucket. + +### Runtime configuration + +* **Local development**: uses the local file [application_properties.json](./application_properties.json) - **Edit the file with your Iceberg table details which includes catalog name, warehouse location, database name, table name, and AWS region** +* **On Amazon Managed Service for Apache Fink**: define Runtime Properties, using Group ID and property names based on the content of [application_properties.json](./application_properties.json) + +For this application, the configuration properties to specify are: + +Runtime parameters: + +| Group ID | Key | Mandatory | Example Value (default for local) | Notes | +|-----------------|-----------------|-----------|-----------------------------------|--------------------------------------------------| +| `IcebergTable0` | `catalog.name` | Y | `glue_catalog` | Catalog name to defined | +| `IcebergTable0` | `warehouse.path`| Y | `s3://my_bucket/my_warehouse` | Warehouse path for catalog | +| `IcebergTable0` | `database.name` | Y | `my_database` | Database name for Iceberg table | +| `IcebergTable0` | `table.name` | Y | `my_table` | Table name to write the data | +| `IcebergTable0` | `aws.region` | Y | `us-east-1` | Region for the output Iceberg table and catalog. | + + +In addition to these configuration properties, when running a PyFlink application in Managed Flink you need to set two +[Additional configuring for PyFink application on Managed Flink](#additional-configuring-for-pyfink-application-on-managed-flink). + +> If you forget to edit the local `application_properties.json` configuration to point your Iceberg table and warehouse path, the application will fail to start locally. + +#### Additional configuring for PyFink application on Managed Flink + +To tell Managed Flink what Python script to run and the fat-jar containing all dependencies, you need to specific some +additional Runtime Properties, as part of the application configuration: + +| Group ID | Key | Mandatory | Value | Notes | +|---------------------------------------|-----------|-----------|--------------------------------|---------------------------------------------------------------------------| +| `kinesis.analytics.flink.run.options` | `python` | Y | `main.py` | The Python script containing the main() method to start the job. | +| `kinesis.analytics.flink.run.options` | `jarfile` | Y | `lib/pyflink-dependencies.jar` | Location (inside the zip) of the fat-jar containing all jar dependencies. | + +> ⚠️ If you forget adding these parameters to the Runtime properties, the application will not start. +--- + +### How to run and build the application + +#### Local development - in the IDE + +1. Make sure you have created the Kinesis Streams and you have a valid AWS session that allows you to publish to the Streams (the way of doing it depends on your setup) +2. Run `mvn package` once, from this directory. This step is required to download the jar dependencies - the Kinesis connector in this case +3. Set the environment variable `IS_LOCAL=true`. You can do from the prompt or in the run profile of the IDE +4. Run `main.py` + +You can also run the python script directly from the command line, like `python main.py`. This still require running `mvn package` before. + +If you are using Virtual Environments, make sure the to select the venv as a runtime in your IDE. + +If you forget the set the environment variable `IS_LOCAL=true` or forget to run `mvn package` the application fails on start. + +> 🚨 The application does not log or print anything. +> If you do not see any output in the console, it does not mean the application is not running. +> The output is sent to the Iceberg table. You can inspect the content of the table using Amazon Athena or Trino/Spark on EMR. + +Note: if you modify the Python code, you do not need to re-run `mvn package` before running the application locally. + +##### Troubleshooting the application when running locally + +By default, the PyFlink application running locally does not send logs to the console. +Any exception thrown by the Flink runtime (i.e. not due to Python error) will not appear in the console. +The application may appear to be running, but actually continuously failing and restarting. + +To see any error messages, you need to inspect the Flink logs. +By default, PyFlink will send logs to the directory where the PyFlink module is installed (Flink home). +Use this command to find the directory: + +``` +$ python -c "import pyflink;import os;print(os.path.dirname(os.path.abspath(pyflink.__file__))+'/log')" +``` + + +#### Deploy and run on Amazon Managed Service for Apache Flink + +1. Make sure you have the S3 bucket location for Iceberg warehouse path +2. Create a Managed Flink application +3. Modify the application IAM role to allow writing to Glue Data catalog and S3 location for the Iceberg table +4. Package the application: run `mvn clean package` from this directory +5. Upload to an S3 bucket the zip file that the previous creates in the [`./target`](./target) subdirectory +6. Configure the Managed Flink application: set Application Code Location to the bucket and zip file you just uploaded +7. Configure the Runtime Properties of the application, creating the Group ID, Keys and Values as defined in the [application_properties.json](./application_properties.json) +8. Start the application +9. When the application transitions to "Ready" you can open the Flink Dashboard to verify the job is running, and you can inspect the data published to the Iceberg table from Athena or Trino/Spark on EMR. + +##### Troubleshooting Python errors when the application runs on Amazon Managed Service for Apache Flink + +Amazon Managed Service for Apache Flink sends all logs to CloudWatch Logs. +You can find the name of the Log Group and Log Stream in the configuration of the application, in the console. + +Errors caused by the Flink engine are usually logged as `ERROR` and easy to find. However, errors reported by the Python +runtime are **not** logged as `ERROR`. + +Apache Flink logs any entry reported by the Python runtime using a logger named `org.apache.flink.client.python.PythonDriver`. + +The easiest way to find errors reported by Python is using CloudWatch Insight, and run the following query:] + +``` +fields @timestamp, message +| sort @timestamp asc +| filter logger like /PythonDriver/ +| limit 1000 +``` + +> 🚨 If the Flink jobs fails to start due to an error reported by Python, for example a missing expected configuration +> parameters, the Amazon Managed Service for Apache Flink may report as *Running* but the job fails to start. +> You can check whether the job is actually running using the Apache Flink Dashboard. If the job is not listed in the +> Running Job List, it means it probably failed to start due to an error. +> +> In CloudWatch Logs you may find an `ERROR` entry with not very explanatory message "Run python process failed". +> To find the actual cause of the problem, run the CloudWatch Insight above, to see the actual error reported by +> the Python runtime. + + +#### Publishing code changes to Amazon Managed Service for Apache Flink + +Follow this process to make changes to the Python code + +1. Modify the code locally (test/run locally, as required) +2. Re-run `mvn clean package` - **if you skip this step, the zipfile is not updated**, and contains the old Python script. +3. Upload the new zip file to the same location on S3 (overwriting the previous zip file) +4. In the Managed Flink application console, enter *Configure*, scroll down and press *Save Changes* + * If your application was running when you published the change, Managed Flink stops the application and restarts it with the new code + * If the application was not running (in Ready state) you need to click *Run* to restart it with the new code + +> 🚨 by design, Managed Flink does not detect the new zip file automatically. +> You control when you want to restart the application with the code changes. This is done saving a new configuration from the +> console or using the [*UpdateApplication*](https://docs.aws.amazon.com/managed-flink/latest/apiv2/API_UpdateApplication.html) +> API. + +--- + +### Application structure + +The application generates synthetic data using the [DataGen](https://nightlies.apache.org/flink/flink-docs-release-1.20/docs/connectors/table/datagen/) connector. +No external data generator is required. + +Generated records are written to a destination Iceberg table which cataloged in AWS Glue and data is written to S3. + +Data format is Parquet, and partitioning is by the `sensor_id`. + +Note that the Iceberg connector writes to S3 on checkpoint. For this reason, when running locally checkpoint is set up programmatically by the application every minute. When deployed on Amazon Managed Service for Apache Flink, the checkpoint +configuration is configured as part of the Managed Flink application configuration. By default, it's every minute. + +If you disable checkpoints (or forget to set it up when running locally) the application runs but never writes any data to S3. + +--- + +### Application packaging and dependencies + +This examples also demonstrate how to include jar dependencies - e.g. connectors - in a PyFlink application, and how to +package it, for deploying on Amazon Managed Service for Apache Flink. + +Any jar dependencies must be added to the `` block in the [pom.xml](pom.xml) file. +In this case, you can see we have included `iceberg-flink-runtime`, `iceberg-aws-bundle`, `flink-s3-fs-hadoop`, `flink-hadoop-fs`, and AWS SDK for Glue & S3 for Iceberg sink to work with Flink. + +Executing `mvn package` takes care of downloading any defined dependencies and create a single "fat-jar" containing all of them. +This file, is generated in the `./target` subdirectory and is called `pyflink-dependencies.jar` + +> The `./target` directory and any generated files are not supposed to be committed to git. + +When running locally, for example in your IDE, PyFlink will look for this jar file in `./target`. + +When you are happy with your Python code and you are ready to deploy the application to Amazon Managed Service for Apache Flink, +run `mvn package` **again**. The zip file you find in `./target` is the artifact to upload to S3, containing both jar dependencies and your Python code. \ No newline at end of file diff --git a/python/IcebergSink/application_properties.json b/python/IcebergSink/application_properties.json new file mode 100644 index 0000000..15adccc --- /dev/null +++ b/python/IcebergSink/application_properties.json @@ -0,0 +1,19 @@ +[ + { + "PropertyGroupId": "kinesis.analytics.flink.run.options", + "PropertyMap": { + "python": "main.py", + "jarfile": "lib/pyflink-dependencies.jar" + } + }, + { + "PropertyGroupId": "IcebergTable0", + "PropertyMap": { + "catalog.name": "glue_catalog", + "warehouse.path": "s3:///my_warehouse", + "database.name": "my_database", + "table.name": "my_table", + "aws.region": "us-east-1" + } + } +] \ No newline at end of file diff --git a/python/IcebergSink/assembly/assembly.xml b/python/IcebergSink/assembly/assembly.xml new file mode 100644 index 0000000..12665c1 --- /dev/null +++ b/python/IcebergSink/assembly/assembly.xml @@ -0,0 +1,25 @@ + + my-assembly + + zip + + false + + + ${project.basedir} + / + + main.py + + + + ${project.build.directory} + lib + + *.jar + + + + \ No newline at end of file diff --git a/python/IcebergSink/main.py b/python/IcebergSink/main.py new file mode 100644 index 0000000..4473f4e --- /dev/null +++ b/python/IcebergSink/main.py @@ -0,0 +1,237 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" +# -*- coding: utf-8 -*- +""" +main.py +~~~~~~~~~~~~~~~~~~~ +This module: +FIXME + 1. Creates a execution environment + 2. Set any special configuration for local mode (e.g. when running in the IDE) + 3. Retrieve the runtime configuration + 4. Creates a source table to generate data using DataGen connector + 5. Create a catalog in AWS Glue data catalog + 6. Create a sink table writing to an Apache Iceberg table on Amazon S3 + 7. Insert into the sink table (Iceberg S3) +""" + +import os +import json +import pyflink +from pyflink.table import EnvironmentSettings, TableEnvironment + +####################################### +# 1. Creates the execution environment +####################################### + +env_settings = EnvironmentSettings.in_streaming_mode() +table_env = TableEnvironment.create(env_settings) + +table_env.get_config().get_configuration().set_string( + "execution.checkpointing.mode", "EXACTLY_ONCE" +) + +table_env.get_config().get_configuration().set_string( + "execution.checkpointing.interval", "1 min" +) + +# Location of the configuration file when running on Managed Flink. +# NOTE: this is not the file included in the project, but a file generated by Managed Flink, based on the +# application configuration. +APPLICATION_PROPERTIES_FILE_PATH = "/etc/flink/application_properties.json" + +# Set the environment variable IS_LOCAL=true in your local development environment, +# or in the run profile of your IDE: the application relies on this variable to run in local mode (as a standalone +# Python application, as opposed to running in a Flink cluster). +# Differently from Java Flink, PyFlink cannot automatically detect when running in local mode +is_local = ( + True if os.environ.get("IS_LOCAL") else False +) + +############################################## +# 2. Set special configuration for local mode +############################################## + +if is_local: + # Load the configuration from the json file included in the project + APPLICATION_PROPERTIES_FILE_PATH = "application_properties.json" + + # Point to the fat-jar generated by Maven, containing all jar dependencies (e.g. connectors) + CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) + table_env.get_config().get_configuration().set_string( + "pipeline.jars", + # For local development (only): use the fat-jar containing all dependencies, generated by `mvn package` + # located in the target/ subdirectory + "file:///" + CURRENT_DIR + "/target/pyflink-dependencies.jar", + ) + + # Show the PyFlink home directory and the directory where logs will be written, when running locally + print("PyFlink home: " + os.path.dirname(os.path.abspath(pyflink.__file__))) + print("Logging directory: " + os.path.dirname(os.path.abspath(pyflink.__file__)) + '/log') + +# Utility method, extracting properties from the runtime configuration file +def get_application_properties(): + if os.path.isfile(APPLICATION_PROPERTIES_FILE_PATH): + with open(APPLICATION_PROPERTIES_FILE_PATH, "r") as file: + contents = file.read() + properties = json.loads(contents) + return properties + else: + print('A file at "{}" was not found'.format(APPLICATION_PROPERTIES_FILE_PATH)) + +# Utility method, extracting a property from a property group +def property_map(props, property_group_id): + for prop in props: + if prop["PropertyGroupId"] == property_group_id: + return prop["PropertyMap"] + +def main(): + + ##################################### + # Default configs + ##################################### + + # Default catalog, database, and input/output tables + catalog = "default_catalog" + database = "default_database" + input_table = f"{catalog}.{database}.sensor_readings" + print_output_table = f"{catalog}.{database}.sensor_output" + + + ##################################### + # 3. Retrieve runtime configuration + ##################################### + + props = get_application_properties() + + # Iceberg table configuration + iceberg_table_properties = property_map(props, "IcebergTable0") + iceberg_catalog_name = iceberg_table_properties["catalog.name"] + iceberg_warehouse_path = iceberg_table_properties["warehouse.path"] + iceberg_database_name = iceberg_table_properties["database.name"] + iceberg_table_name = iceberg_table_properties["table.name"] + iceberg_table_region = iceberg_table_properties["aws.region"] + + ################################################# + # 4. Define input table using datagen connector + ################################################# + + # In a real application, this table will probably be connected to a source stream, using for example the 'kinesis' + # connector. + table_env.execute_sql(f""" + CREATE TABLE {input_table} ( + sensor_id INT, + temperature NUMERIC(6,2), + measurement_time TIMESTAMP(3) + ) + PARTITIONED BY (sensor_id) + WITH ( + 'connector' = 'datagen', + 'fields.sensor_id.min' = '10', + 'fields.sensor_id.max' = '20', + 'fields.temperature.min' = '0', + 'fields.temperature.max' = '100' + ) + """) + + ################################################# + # 5. Define catalog for iceberg table + ################################################# + + table_env.execute_sql(f""" + CREATE CATALOG {iceberg_catalog_name} WITH ( + 'type' = 'iceberg', + 'property-version' = '1', + 'catalog-impl' = 'org.apache.iceberg.aws.glue.GlueCatalog', + 'io-impl' = 'org.apache.iceberg.aws.s3.S3FileIO', + 'warehouse' = '{iceberg_warehouse_path}', + 'aws.region' = '{iceberg_table_region}' + ) + """) + + ################################################# + # 6. Use the catalog and create database + ################################################# + + # Start by using the catalog + table_env.execute_sql(f"USE CATALOG `{iceberg_catalog_name}`;") + + # Create database if not exists + table_env.execute_sql(f"CREATE DATABASE IF NOT EXISTS `{iceberg_database_name}`;") + + # Use database + table_env.execute_sql(f"USE `{iceberg_database_name}`;") + + ################################################# + # 7. Define sink table for Iceberg table + ################################################# + + table_env.execute_sql(f""" + CREATE TABLE IF NOT EXISTS `{iceberg_catalog_name}`.`{iceberg_database_name}`.`{iceberg_table_name}` ( + sensor_id INT NOT NULL, + temperature NUMERIC(6,2) NOT NULL, + `time` TIMESTAMP_LTZ(3) NOT NULL + ) + PARTITIONED BY (sensor_id) + WITH ( + 'type' = 'iceberg', + 'write.format.default' = 'parquet', + 'write.parquet.compression-codec' = 'snappy', + 'format-version' = '2' + ) + """) + + # table_env.execute_sql(f""" + # CREATE TABLE {print_output_table}( + # sensor_id INT NOT NULL, + # temperature NUMERIC(6,2) NOT NULL, + # `time` TIMESTAMP_LTZ(3) NOT NULL + # ) + # PARTITIONED BY (sensor_id) + # WITH ( + # 'connector' = 'print' + # ) + # """) + + # In a real application we would probably have some transformations between the input and the output. + # For simplicity, we will send the source table directly to the sink table. + + ########################################################################################## + # 8. Insert into the sink table + ########################################################################################## + + table_result = table_env.execute_sql(f""" + INSERT INTO `{iceberg_catalog_name}`.`{iceberg_database_name}`.`{iceberg_table_name}` + SELECT sensor_id, temperature, measurement_time as `time` + FROM {input_table}""") + + ## Uncomment below when using "print" + # table_result = table_env.execute_sql(f""" + # INSERT INTO sensors_output + # SELECT sensor_id, temperature, measurement_time as `time` + # FROM {input_table}""") + + + # When running locally, as a standalone Python application, you must instruct Python not to exit at the end of the + # main() method, otherwise the job will stop immediately. + # When running the job deployed in a Flink cluster or in Amazon Managed Service for Apache Flink, the main() method + # must end once the flow has been defined and handed over to the Flink framework to run. + if is_local: + table_result.wait() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/python/IcebergSink/pom.xml b/python/IcebergSink/pom.xml new file mode 100644 index 0000000..f793604 --- /dev/null +++ b/python/IcebergSink/pom.xml @@ -0,0 +1,189 @@ + + 4.0.0 + + com.amazonaws + managed-flink-pyfink-iceberg-sink-example + 1.0.0 + + + UTF-8 + ${project.basedir}/target + ${project.name}-${project.version} + pyflink-dependencies + + 11 + ${target.java.version} + ${target.java.version} + + 1.20.0 + 1.20 + 1.9.0 + 2.31.48 + 1.2.0 + + + + + + software.amazon.awssdk + bom + ${awssdk.version} + pom + import + + + + + + + + + software.amazon.awssdk + glue + + + + software.amazon.awssdk + s3 + + + org.apache.iceberg + iceberg-flink-runtime-${flink.major.version} + ${iceberg.version} + + + org.apache.iceberg + iceberg-aws-bundle + ${iceberg.version} + + + org.apache.flink + flink-s3-fs-hadoop + ${flink.version} + + + org.apache.flink + flink-hadoop-fs + ${flink.version} + + + + + ${buildDirectory} + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${target.java.version} + ${target.java.version} + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + + package + + shade + + + + ${project.build.outputDirectory} + + + + org.apache.flink:force-shading + com.google.code.findbugs:jsr305 + org.slf4j:* + log4j:* + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + META-INF/versions/21/** + META-INF/versions/22/** + + + + + + + + + + + + org.apache.hadoop.conf + shaded.org.apache.hadoop.conf + + + org.apache.flink.runtime.util.HadoopUtils + shadow.org.apache.flink.runtime.util.HadoopUtils + + + + + + + + + + maven-assembly-plugin + 3.3.0 + + + assembly/assembly.xml + + ${zip.finalName} + ${buildDirectory} + false + + + + make-assembly + package + + single + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + ${jar.finalName} + + + + + \ No newline at end of file diff --git a/python/IcebergSink/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java b/python/IcebergSink/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java new file mode 100644 index 0000000..9ffaae1 --- /dev/null +++ b/python/IcebergSink/src/main/java/org/apache/flink/runtime/util/HadoopUtils.java @@ -0,0 +1,14 @@ +// This utility is used by Flink to dynamically load Hadoop configurations at runtime. +// Why Needed: Required for Flink to interact with Hadoop-compatible systems (e.g., S3 via s3a:// or s3:// paths). +// Returns an empty Hadoop Configuration object (new Configuration(false)). + +package org.apache.flink.runtime.util; + +import org.apache.hadoop.conf.Configuration; + +public class HadoopUtils { + public static Configuration getHadoopConfiguration( + org.apache.flink.configuration.Configuration flinkConfiguration) { + return new Configuration(false); + } + } \ No newline at end of file diff --git a/python/PackagedPythonDependencies/.gitignore b/python/PackagedPythonDependencies/.gitignore new file mode 100644 index 0000000..f36bcce --- /dev/null +++ b/python/PackagedPythonDependencies/.gitignore @@ -0,0 +1 @@ +/dep/ diff --git a/python/PackagedPythonDependencies/README.md b/python/PackagedPythonDependencies/README.md new file mode 100644 index 0000000..ecb9c69 --- /dev/null +++ b/python/PackagedPythonDependencies/README.md @@ -0,0 +1,196 @@ +## Packaging Python dependencies with the ZIP + +Example showing how you can package Python dependencies with the ZIP file you upload to S3. + +* Flink version: 1.20 +* Flink API: Table API & SQL +* Flink Connectors: Kinesis Connector +* Language: Python + +This example shows how you can package Python dependencies within the ZIP and make them available to the application. + +> This method is alternative to what illustrated in the [Python Dependencies](../PythonDependencies) example which relies on the +`requirements.txt` file for installing the dependencies at runtime. + +The approach shown in this example has the following benefits: + +* It works with any number of Python libraries +* It supports Python libraries which include **native dependencies**, such as SciPy or Pydantic specific to the CPU architecture (note that Pandas, NumPy, and PyArrow also have native dependencies, but are already available as transitive dependencies +of `apache-flink` and should not be added as additional dependencies). +* It allows to run the application locally, in your machine, and in Managed Service for Apache Flink, with no code changes. +* Dependencies are available both during job initialization, in the `main()` method, and for data processing, for example in a User Defined Function (UDF). + +Drawbacks: +* You need to use a virtual environment for the Python dependencies when running locally, because the CPU architecture of your machine may differ from the architecture used by Managed Service for Apache Flink +* Python dependencies are included in the ZIP file slowing down a bit operations + +For more details about how packaging dependencies works, see [Packaging application and dependencies](#packaging-application-and-dependencies), below. + + + +The application includes [SciPy](https://scipy.org/) used in a UDF. The actual use is not important. +It also shows how the same library can be used during the job initialization. + +The application generates random data using [DataGen](https://nightlies.apache.org/flink/flink-docs-release-1.20/docs/connectors/table/datagen/) +and send the output to a Kinesis Data Streams. + +--- + +### How to run and build the application + +#### Prerequisites + +We recommend to use a Virtual environment (venv) on the development machine. + +Development and build environment requirements: +* Python 3.11 +* PyFlink library: `apache-flink==1.20.0` +* Java JDK 11+ and Maven + + +#### Setting up the local environment + +To run the application locally, from the command line or in your IDE, you need to install the following Python dependencies: + +* `apache-flink==1.20.0` +* Any additional Python dependency is defined in `requirements.txt` + +Assuming you use virtualenv: + +1. Create the Virtual Environment in the project directory: `virtualenv venv` +2. Activate the Virtual Environment you just created: `source venv/bin/activate` +3. Install PyFlink library: `pip install apache-flink==1.20.0` +4. Define the additional dependencies + * Add JAR-dependencies in the `pom.xml` file + * Add Python dependencies in the `requirements.txt` (do not include any Flink dependency!) +5. Install Python from `requirements.txt` into the venv, for local development: `pip install -r requirements.txt` +6. Download the Python dependencies for the target architecture: `pip install -r requirements.txt --target=dep/ --platform=manylinux2014_x86_64 --only-binary=:all:` +7. Run `mvn package` to download and package the JAR dependencies and build the ZIP artifact + +> ⚠️ The Flink Python library 1.20.0 may fail installing on Python 3.12. We recommend using Python 3.11 for development, the same Python version used by Amazon Managed Service for Apache Flink runtime 1.20. + +> JDK and Maven are uses to download and package any required Flink dependencies, e.g. connectors, and to package the application as .zip file, for deployment to Amazon Managed Service for Apache Flink. + +### Runtime configuration + +* **Local development**: uses the local file [application_properties.json](./application_properties.json) +* **On Amazon Managed Service for Apache Fink**: define Runtime Properties, using Group ID and property names based on the content of [application_properties.json](./application_properties.json) + +For this application, the configuration properties to specify are: + + +| Group ID | Key | Mandatory | Example Value (default for local) | Notes | +|------------------|---------------|-----------|-----------------------------------|-------------------------------| +| `OutputStream0` | `stream.name` | Y | `ExampleOutputStream` | Output stream name. | +| `OutputStream0` | `aws.region` | Y | `us-east-1` | Region for the output stream. | + + + +To tell Managed Flink what Python script to run, the fat-jar containing all dependencies, and the Python dependencies, you need to specific some +additional Runtime Properties, as part of the application configuration: + +| Group ID | Key | Mandatory | Value | Notes | +|---------------------------------------|-----------|-----------|--------------------------------|------------------------------------------------------------------------------------| +| `kinesis.analytics.flink.run.options` | `python` | Y | `main.py` | The Python script containing the main() method to start the job. | +| `kinesis.analytics.flink.run.options` | `jarfile` | Y | `lib/pyflink-dependencies.jar` | Location (inside the zip) of the fat-jar containing all jar dependencies. | +| `kinesis.analytics.flink.run.options` | `pyFiles` | Y | `dep/` | Relative path of the subdirectory (inside the zip) containing Python dependencies. | + +Note that these properties are ignored when running locally. + +--- + +### Local development - in the IDE + +1. Make sure you have created the Kinesis Streams and you have a valid AWS session that allows you to publish to the Streams (the way of doing it depends on your setup) +2. Make sure your IDE uses the venv you created. Follow the documentations of your IDE (PyCharm, Visual Studio Code) +3. Run `mvn package` once, from this directory. This step is required to download the jar dependencies - the Kinesis connector in this case +4. Set the environment variable `IS_LOCAL=true`. You can do from the prompt or in the run profile of the IDE +5. Run `main.py` + +You can also run the python script directly from the command line, like python main.py. This still require running mvn package before. + +If you forget the set the environment variable `IS_LOCAL=true` or forget to run `mvn package` the application fails on start. + +> 🚨 The application does not log or print anything. If you do not see any output in the console, it does not mean the application is not running. The output is sent to the Kinesis streams. You can inspect the content of the streams using the Data Viewer in the Kinesis console + + + +### Deploy and run on Amazon Managed Service for Apache Flink + +1. Make sure you have the required Kinesis Streams +2. Create a Managed Flink application +3. Modify the application IAM role to allow writing to the Kinesis Stream +4. If you haven't done already, download the Python dependencies for the target architecture: `pip install -r requirements.txt --target=dep/ --platform=manylinux2014_x86_64 --only-binary=:all:` +5. Package the application: run `mvn clean package` from this directory +6. Upload to an S3 bucket the zip file that the previous creates in the ./target subdirectory +7. Configure the Managed Flink application: set Application Code Location to the bucket and zip file you just uploaded +8. Configure the Runtime Properties of the application, creating the Group ID, Keys and Values as defined in the [`application_properties.json`](application_properties.json) (a) +9. Start the application +10. When the application transitions to "RUNNING" you can open the Flink Dashboard to verify the job is running, and you can inspect the data published to the Kinesis Streams, using the Data Viewer in the Kinesis console. + + + +### Publishing code changes to Amazon Managed Service for Apache Flink + +Follow this process to make changes to the Python code or the dependencies + +1. Modify the code locally (test/run locally, as required) +2. Re-run `mvn clean package` - if you skip this step, the zipfile is not updated, and contains the old Python script. +3. Upload the new zip file to the same location on S3 (overwriting the previous zip file) +4. In the Managed Flink application console, enter Configure, scroll down and press Save Changes + * If your application was running when you published the change, Managed Flink stops the application and restarts it with the new code + * If the application was not running (in Ready state) you need to click Run to restart it with the new code + + +> 🚨 by design, Managed Flink does not detect the new zip file automatically. You control when you want to restart the application with the code changes. This is done saving a new configuration from the console or using the UpdateApplication API. + +--- + +### Packaging application and dependencies + + +This example also demonstrates how to include both jar dependencies - e.g. connectors - and Python libraries in a PyFlink application. It demonstrates how to package it for deploying on Amazon Managed Service for Apache Flink. + +The [`assembly/assembly.xml`](assembly/assembly.xml) file instructs Maven for including the correct files in the ZIP-file. + +#### Jar dependencies + +Any jar dependencies must be added to the `` block in the [pom.xml](pom.xml) file. +In this case, you can see we have included `flink-sql-connector-kinesis` + +Executing `mvn package` takes care of downloading any defined dependencies and create a single "fat-jar" containing all of them. +This file, is generated in the `./target` subdirectory and is called `pyflink-dependencies.jar` + +> The `./target` directory and any generated files are not supposed to be committed to git. + +When running locally, for example in your IDE, PyFlink will look for this jar file in `./target`. + +When you are happy with your Python code and you are ready to deploy the application to Amazon Managed Service for Apache Flink, +run `mvn package` **again**. The zip file you find in `./target` is the artifact to upload to S3, containing +both jar dependencies and your Python code. + +#### Python 3rd-party libraries + +Any additional 3rd-party Python library (i.e. Python libraries not provided by PyFlink directly) must also be available +when the application runs. + +There are different approaches for including these libraries in an application deployed on Managed Service for Apache Flink. +The approach demonstrated in this example is the following: + +1. Define a `requirements.txt` with all additional Python dependencies - **DO NOT include any PyFlink dependency** +2. Download the dependencies for the target architecture (`manylinux2014_x86_64`) into the `dep/` sub-folder +3. Package the `dep/` sub-folder in the ZIP file +4. At runtime, register the dependency folder. There are two **alternative** methods (use one of the following, not both): + 1. (recommended) Use the Managed Flink application configuration + * Group ID: `kinesis.analytics.flink.run.options` + * Key: `pyFiles` + * Value: `dep/` + 2. Alternatively, you can programmatically register the directory but only when not running locally + ```python + if not is_local: + python_source_dir = str(pathlib.Path(__file__).parent) + table_env.add_python_file(file_path="file:///" + python_source_dir + "/dep") + ``` + +> This approach differs from what shown in the [Python Dependencies](../PythonDependencies) example because the Python dependencies +> are packaged within the ZIP. The `requirements.txt` file is NOT used to download the dependencies at runtime. diff --git a/python/PackagedPythonDependencies/application_properties.json b/python/PackagedPythonDependencies/application_properties.json new file mode 100644 index 0000000..9a673b9 --- /dev/null +++ b/python/PackagedPythonDependencies/application_properties.json @@ -0,0 +1,17 @@ +[ + { + "PropertyGroupId": "OutputStream0", + "PropertyMap": { + "stream.name": "ExampleOutputStream", + "aws.region": "us-east-1" + } + }, + { + "PropertyGroupId": "kinesis.analytics.flink.run.options", + "PropertyMap": { + "python": "main.py", + "jarfile": "lib/pyflink-dependencies.jar", + "pyFiles": "dep/" + } + } +] \ No newline at end of file diff --git a/python/PackagedPythonDependencies/assembly/assembly.xml b/python/PackagedPythonDependencies/assembly/assembly.xml new file mode 100644 index 0000000..f3d6486 --- /dev/null +++ b/python/PackagedPythonDependencies/assembly/assembly.xml @@ -0,0 +1,44 @@ + + my-assembly + + zip + + false + + + + ${project.basedir} + / + + **/*.py + + + + dep/** + + requirements.txt + + + + + + ${project.basedir}/dep + dep + + ** + + + + + + ${project.build.directory} + lib + + *.jar + + + + + \ No newline at end of file diff --git a/python/PackagedPythonDependencies/main.py b/python/PackagedPythonDependencies/main.py new file mode 100644 index 0000000..d87b721 --- /dev/null +++ b/python/PackagedPythonDependencies/main.py @@ -0,0 +1,263 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" + +""" +main.py +~~~~~~~~~~~~~~~~~~~ +This module: + 1. Creates the execution environment and specify 3rd-party Python dependencies + 2. Sets any special configuration for local mode (e.g. when running in the IDE) + 3. (Optional) Register the Python dependencies + 4. Retrieves the runtime configuration + 5. Defines and register a UDF that use the python library + 6. Creates a source table to generate data using DataGen connector + 7. Creates a view from a query that uses the UDF + 8. Creates a sink table to Kinesis Data Streams and inserts into the sink table from the view +""" + +from pyflink.table import EnvironmentSettings, TableEnvironment, DataTypes +from pyflink.table.udf import udf +import os +import json +import logging +import pyflink +import pathlib + +################################################################################# +# 1. Creates the execution environment and specify 3rd-party Python dependencies +################################################################################# + +env_settings = EnvironmentSettings.in_streaming_mode() +table_env = TableEnvironment.create(env_settings) + +############################################## +# 2. Set special configuration for local mode +############################################## + +# Location of the configuration file when running on Managed Flink. +# NOTE: this is not the file included in the project, but a file generated by Managed Flink, based on the +# application configuration. +APPLICATION_PROPERTIES_FILE_PATH = "/etc/flink/application_properties.json" + +# Set the environment variable IS_LOCAL=true in your local development environment, +# or in the run profile of your IDE: the application relies on this variable to run in local mode (as a standalone +# Python application, as opposed to running in a Flink cluster). +# Differently from Java Flink, PyFlink cannot automatically detect when running in local mode +is_local = ( + True if os.environ.get("IS_LOCAL") else False +) + +if is_local: + # Load the configuration from the json file included in the project + APPLICATION_PROPERTIES_FILE_PATH = "application_properties.json" + + # Point to the fat-jar generated by Maven, containing all jar dependencies (e.g. connectors) + CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) + table_env.get_config().get_configuration().set_string( + "pipeline.jars", + # For local development (only): use the fat-jar containing all dependencies, generated by `mvn package` + "file:///" + CURRENT_DIR + "/target/pyflink-dependencies.jar", + ) + + # Show the PyFlink home directory and the directory where logs will be written, when running locally + print("PyFlink home: " + os.path.dirname(os.path.abspath(pyflink.__file__))) + print("Logging directory: " + os.path.dirname(os.path.abspath(pyflink.__file__)) + '/log') + + +########################################################################################## +# 3. (Optional) Register the additional Python dependencies - alt. to specifying pyFiles +########################################################################################## + +# Alternatively to specifying the runtime property kinesis.analytics.flink.run.options : pyFiles, you can +# programmatically register the sub-folder containing the Python dependencies. +# IMPORTANT: you must either specify pyFiles OR registering the dependencies programmatically. NOT both. +# Also, important: when running locally for development you should install the Python dependencies in a venv and not +# register them programmatically. The reason is that the dependencies downloaded in the dep/ subdirectory must match +# the target architecture used by Managed Flink (linux x86_64), which can differ from the architecture of the machine +# you are using for development. + +# Uncomment the following code as alternative to specifying +# the runtime property kinesis.analytics.flink.run.options : pyFiles = dep/ +# DO NOT use both runtime property and programmatic registration. +# if not is_local: +# # Only register the Python dependencies when running locally +# python_source_dir = str(pathlib.Path(__file__).parent) +# table_env.add_python_file(file_path="file:///" + python_source_dir + "/dep") + + +# Utility method, extracting properties from the runtime configuration file +def get_application_properties(): + if os.path.isfile(APPLICATION_PROPERTIES_FILE_PATH): + with open(APPLICATION_PROPERTIES_FILE_PATH, "r") as file: + contents = file.read() + properties = json.loads(contents) + return properties + else: + print('A file at "{}" was not found'.format(APPLICATION_PROPERTIES_FILE_PATH)) + + +# Utility method, extracting a property from a property group +def property_map(props, property_group_id): + for prop in props: + if prop["PropertyGroupId"] == property_group_id: + return prop["PropertyMap"] + + +##################################### +# 4. Retrieve runtime configuration +##################################### + +props = get_application_properties() + +# Get name and region of the Kinesis stream from application configuration +output_stream_name = property_map(props, "OutputStream0")["stream.name"] +output_stream_region = property_map(props, "OutputStream0")["aws.region"] +logging.info(f"Output stream: {output_stream_name}, region: {output_stream_region}") + + +############################################################# +# 5. Defines and register a UDF that uses the Python library +############################################################# + + +@udf(input_types=[DataTypes.FLOAT(), DataTypes.FLOAT(), DataTypes.FLOAT(), DataTypes.FLOAT()], + result_type=DataTypes.FLOAT()) +def determinant(element1, element2, element3, element4): + import numpy as np + from scipy import linalg + a = np.array([[element1, element2], [element3, element4]]) + det = linalg.det(a) + return det + + +# Register the UDF +table_env.create_temporary_system_function("determinant", determinant) + + +def main(): + # Demonstrate the Python dependency is also available in the main() method + # This piece of code is not doing anything useful. The goal is just to shows that the registered dependencies + # are also available job initialization, in the main() method. + # A more realistic case would be, for example, using boto3 to fetch some resources you need to initialize the job. + import numpy as np + from scipy import linalg + matrix = np.array([[42, 43], [44, 43]]) + det = linalg.det(matrix) + print(f"Check dependency in main(): determinant({matrix}) = {det}") + + + ################################################# + # 6. Define input table using datagen connector + ################################################# + + # In a real application, this table will probably be connected to a source stream, using for example the 'kinesis' + # connector. + + table_env.execute_sql(""" + CREATE TABLE random_numbers ( + seed_time TIMESTAMP(3), + element1 FLOAT, + element2 FLOAT, + element3 FLOAT, + element4 FLOAT + ) + PARTITIONED BY (seed_time) + WITH ( + 'connector' = 'datagen', + 'rows-per-second' = '1', + 'fields.element1.min' = '0', + 'fields.element1.max' = '100', + 'fields.element2.min' = '0', + 'fields.element2.max' = '100', + 'fields.element3.min' = '0', + 'fields.element3.max' = '100', + 'fields.element4.min' = '0', + 'fields.element4.max' = '100' + ) + """) + + ################################################### + # 7. Creates a view from a query that uses the UDF + ################################################### + + table_env.execute_sql(""" + CREATE TEMPORARY VIEW determinants + AS + SELECT seed_time, + element1, element2, element3, element4, + determinant(element1, element2, element3, element4) AS determinant + FROM random_numbers + """) + + ################################################# + # 8. Define sink table using kinesis connector + ################################################# + + table_env.execute_sql(f""" + CREATE TABLE output ( + seed_time TIMESTAMP(3), + element1 FLOAT, + element2 FLOAT, + element3 FLOAT, + element4 FLOAT, + determinant FLOAT + ) + WITH ( + 'connector' = 'kinesis', + 'stream' = '{output_stream_name}', + 'aws.region' = '{output_stream_region}', + 'sink.partitioner-field-delimiter' = ';', + 'sink.batch.max-size' = '5', + 'format' = 'json', + 'json.timestamp-format.standard' = 'ISO-8601' + ) + """) + + # For local development purposes, you might want to print the output to the console, instead of sending it to a + # Kinesis Stream. To do that, you can replace the sink table using the 'kinesis' connector, above, with a sink table + # using the 'print' connector. Comment the statement immediately above and uncomment the one immediately below. + # table_env.execute_sql(""" + # CREATE TABLE output ( + # seed_time TIMESTAMP(3), + # element1 FLOAT, + # element2 FLOAT, + # element3 FLOAT, + # element4 FLOAT, + # determinant FLOAT + # ) + # WITH ( + # 'connector' = 'print' + # ) + # """) + + # Executing an INSERT INTO statement will trigger the job + table_result = table_env.execute_sql(""" + INSERT INTO output + SELECT seed_time, element1, element2, element3, element4, determinant + FROM determinants + """) + + # When running locally, as a standalone Python application, you must instruct Python not to exit at the end of the + # main() method, otherwise the job will stop immediately. + # When running the job deployed in a Flink cluster or in Amazon Managed Service for Apache Flink, the main() method + # must end once the flow has been defined and handed over to the Flink framework to run. + if is_local: + table_result.wait() + + +if __name__ == "__main__": + main() diff --git a/python/PackagedPythonDependencies/pom.xml b/python/PackagedPythonDependencies/pom.xml new file mode 100644 index 0000000..9203b5c --- /dev/null +++ b/python/PackagedPythonDependencies/pom.xml @@ -0,0 +1,115 @@ + + 4.0.0 + + com.amazonaws + managed-flink-pyflink-packaged-dependencies-example + 1.0.0 + + + UTF-8 + ${project.basedir}/target + ${project.name}-${project.version} + pyflink-dependencies + 1.19.1 + 4.3.0-1.19 + 1.2.0 + + + + + + org.apache.flink + flink-sql-connector-kinesis + ${aws.connector.version} + + + + + ${buildDirectory} + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + + package + + shade + + + + ${project.build.outputDirectory} + + + + org.apache.flink:force-shading + com.google.code.findbugs:jsr305 + org.slf4j:* + log4j:* + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + + + + + + maven-assembly-plugin + 3.3.0 + + + assembly/assembly.xml + + ${zip.finalName} + ${buildDirectory} + false + + + + make-assembly + package + + single + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + ${jar.finalName} + + + + + \ No newline at end of file diff --git a/python/PackagedPythonDependencies/requirements.txt b/python/PackagedPythonDependencies/requirements.txt new file mode 100644 index 0000000..9c61c73 --- /dev/null +++ b/python/PackagedPythonDependencies/requirements.txt @@ -0,0 +1 @@ +scipy \ No newline at end of file diff --git a/python/PythonDependencies/README.md b/python/PythonDependencies/README.md index 7819bc1..9a8ec27 100644 --- a/python/PythonDependencies/README.md +++ b/python/PythonDependencies/README.md @@ -1,25 +1,34 @@ -## Packaging Python dependencies +## Downloading Python dependencies at runtime -Examples showing how to include Python libraries in your PyFlink application. +Examples showing how to include Python libraries in your PyFlink application, downloading them at runtime. * Flink version: 1.20 * Flink API: Table API & SQL * Flink Connectors: Kinesis Connector * Language: Python -This example demonstrate how to include in your PyFlink application additional Python libraries. +This example demonstrate how to include in your PyFlink application additional Python libraries, using `requirements.txt` +to have Managed Service for Apache Flink downloading the dependencies at runtime -There are multiple ways of adding Python dependencies to an application deployed on Amazon Managed Service for Apache Flink. -The approach demonstrated in this example has several benefits: +> This approach is alternative to what illustrated by the [Packaging Python dependencies with the ZIP](../PackagedPythonDependencies) +> which includes the Python dependencies in the ZIP file, as opposed to downloading them at runtime. + + +The approach shown in this example has the following benefits: * It works with any number of Python libraries +* It keeps the ZIP artifact small * It allows to run the application locally, in your machine, and in Managed Service for Apache Flink, with no code changes * It supports Python libraries not purely written in Python, like PyArrow for example, that are specific to a CPU architecture. Including these libraries may be challenging because they architecture of your development machine may be different from the architecture of Managed Service for Apache Flink. -As many other examples, this example also packages any JAR dependencies required for the application. In this case the Kinesis -connector. +Drawbacks: +* The dependencies downloaded at runtime are only available for data processing, for example in UDF. They are NOT available during the job initialization, in the `main()` method. + If you need to usa a Python dependency during job initialization, you must use the approach illustrated by the [Packaging Python dependencies with the ZIP](../PackagedPythonDependencies) + +For more details about how packaging dependencies works, see [Packaging application and dependencies](#packaging-application-and-dependencies), below. + The application is very simple, and uses _botocore_ and _boto3_ Python libraries. These libraries are used to invoke Amazon Bedrock to get a fun fact about a random number. @@ -82,8 +91,18 @@ For this application, the configuration properties to specify are: | `OutputStream0` | `stream.name` | Y | `ExampleOutputStream` | Output stream name. | | `OutputStream0` | `aws.region` | Y | `us-east-1` | Region for the output stream. | -In addition to these configuration properties, when running a PyFlink application in Managed Flink you need to set two -[Additional configuring for PyFink application on Managed Flink](#additional-configuring-for-pyfink-application-on-managed-flink). + +#### Additional configuring for PyFink application on Managed Flink + +To tell Managed Flink what Python script to run and the fat-jar containing all dependencies, you need to specific some +additional Runtime Properties, as part of the application configuration: + +| Group ID | Key | Mandatory | Value | Notes | +|---------------------------------------|-----------|-----------|--------------------------------|---------------------------------------------------------------------------| +| `kinesis.analytics.flink.run.options` | `python` | Y | `main.py` | The Python script containing the main() method to start the job. | +| `kinesis.analytics.flink.run.options` | `jarfile` | Y | `lib/pyflink-dependencies.jar` | Location (inside the zip) of the fat-jar containing all jar dependencies. | + +These parameters are ignored when running locally. --- @@ -169,11 +188,15 @@ libraries, boto3 and botocore in this case. --- -### Application packaging and dependencies +### Packaging application and dependencies This example also demonstrates how to include both jar dependencies - e.g. connectors - and Python libraries in a PyFlink application. It demonstrates how to package it for deploying on Amazon Managed Service for Apache Flink. + +The [`assembly/assembly.xml`](assembly/assembly.xml) file instructs Maven for including the correct files in the ZIP-file. + + #### Jar dependencies Any jar dependencies must be added to the `` block in the [pom.xml](pom.xml) file. @@ -209,13 +232,3 @@ The approach demonstrated in this example is the following: With this approach the Python library are **not packaged** with the application artifact you deploy to Managed Service for Apache Flink. They are installed by the runtime on the cluster, when the application starts. -#### Additional configuring for PyFink application on Managed Flink - -To tell Managed Flink what Python script to run and the fat-jar containing all dependencies, you need to specific some -additional Runtime Properties, as part of the application configuration: - -| Group ID | Key | Mandatory | Value | Notes | -|---------------------------------------|-----------|-----------|--------------------------------|---------------------------------------------------------------------------| -| `kinesis.analytics.flink.run.options` | `python` | Y | `main.py` | The Python script containing the main() method to start the job. | -| `kinesis.analytics.flink.run.options` | `jarfile` | Y | `lib/pyflink-dependencies.jar` | Location (inside the zip) of the fat-jar containing all jar dependencies. | - diff --git a/python/README.md b/python/README.md index 8ce35b4..fcaf57c 100644 --- a/python/README.md +++ b/python/README.md @@ -1,39 +1,43 @@ -## Flink Python examples +## Flink Python Examples This folder contains examples of Flink applications written in Python. --- -### Packaging dependencies for running in Amazon Managed Service for Apache Flink +### Packaging Dependencies for Amazon Managed Service for Apache Flink -There multiple ways of packaging a PyFlink application with multiple dependencies, Python libraries or JAR dependencies, like connectors. [Amazon Managed Service for Apache Flink](https://aws.amazon.com/managed-service-apache-flink/) expects a specific packaging and runtime configuration for PyFlink applications. +There are multiple ways to package a PyFlink application with dependencies, including Python libraries and JAR dependencies like connectors. [Amazon Managed Service for Apache Flink](https://aws.amazon.com/managed-service-apache-flink/) expects specific packaging and runtime configuration for PyFlink applications. -#### JAR dependencies +#### JAR Dependencies Amazon Managed Service for Apache Flink expects **a single JAR file** containing all JAR dependencies of a PyFlink application. These dependencies include any Flink connector and any other Java library your PyFlink application requires. All these dependencies must be packaged in a single *uber-jar* using [Maven](https://maven.apache.org/) or other similar tools. -The [Getting Started](./GettingStarted/) example, and most of the other example in this directory, show a project set up that allows you to add any number of JAR dependencies to your PyFlink project. It requires Java JDK and [Maven](https://maven.apache.org/) to develop and to package the PyFlink application, and uses Maven to build the *uber-jar* and to package the PyFlink application in the `zip` file for deploymenbt on Managed Service for Apache Flink. This set up also allows you to run your application in your IDE, for debugging and development, and in Managed Service for Apache Flink **without any code changes**. +The [Getting Started](./GettingStarted) example, and most of the other examples in this directory, show a project setup that allows you to add any number of JAR dependencies to your PyFlink project. +It requires Java JDK and [Maven](https://maven.apache.org/) to develop and package the PyFlink application, and uses Maven to build the *uber-jar* and package the PyFlink application in the `zip` file for deployment on Managed Service for Apache Flink. +This setup also allows you to run your application in your IDE for debugging and development, and in Managed Service for Apache Flink **without any code changes**. +**No local Flink cluster is required** for development. +#### Python Dependencies -#### Python dependencies +Apache Flink supports multiple ways of adding Python libraries to your PyFlink application. +Check out these two examples to learn how to correctly package dependencies: -In Apache Flink supports multiple ways of adding Python libraries to your PyFlink application. +1. [PythonDependencies](./PythonDependencies): How to download Python dependencies at runtime using `requirements.txt` +2. [PackagedPythonDependencies](./PackagedPythonDependencies): How to package dependencies with the application artifact + +> The patterns shown in these examples allow you to run the application locally and on Amazon Managed Service +> for Apache Flink **without any code changes**, regardless of the machine architecture you are developing with. -Thre [Python Dependencies](./PythonDependencies/) example shows the most general way of adding any number of Python libraries to your application, using the `requriement.txt` file. This method works with any type of Python library, and does not require packaging these dependencies into the `zip` file deployed on Managed Service for Apache Flink. This also allows you to run the application in your IDE, for debugging and development, and in Managed Service for Apache Flink **without any code changes**. --- -### Python and Flink versions for local development +### Python and Flink Versions for Local Development -There are some known issues with some specific Python and PyFlink versions, for local development +There are some known issues with specific Python and PyFlink versions for local development: -* We recommend using **Python 3.11** to develop Python Flink 1.20.0 applications. +* We recommend using **Python 3.11** to develop PyFlink 1.20.0 applications. This is also the runtime used by Amazon Managed Service for Apache Flink. - Installation of the Python Flink 1.19 library on Python 3.12 may fail. -* Installation of the Python Flink **1.15** library on machines based on **Apple Silicon** fail. - We recommend upgrading to the Flink 1.20, 1.19 or 1.18. Versions 1.18+ work correctly also on Apple Silicon machines. - If you need to maintain a Flink 1.15 application using a machine based on Apple Silicon, you can follow [the guide to develop Flink 1.15 on Apple Silicon](LocalDevelopmentOnAppleSilicon). - - -> None of these issues affects Python Flink applications running on Amazon Managed Service for Apache Flink. -> The managed service uses Python versions compatible with the Flink runtime version you choose. + Installation of the PyFlink 1.19 library on Python 3.12 may fail. +* If you are using Flink **1.15 or earlier** and developing on a machine based on **Apple Silicon**, installing PyFlink locally may fail. + This is a know issue not affecting the application deployed on Amazon Managed Service for Apache Flink. + To develop on Apple Silicon follow [the guide to develop Flink 1.15 on Apple Silicon](LocalDevelopmentOnAppleSilicon).

44IQmq=ErM(OhbR9#Cc6&vjo|<%s3CwRvwtCAesqjKE zM&cMuHgX=0Ky!w)Tynlkn6_x|q7s_xLsDKadi&4t^!cZvJM8G!k`5gu%#)+a^T;bT z5{r3Su6r}g-n}|$N5V6)nf}Zx!&zpxT=ECt0Go@X^p#zly*?^_t$;KfmHw0iFf@J{ zY*h^vjVEJxCC_QK{VeN^e0#?>otjNMPYhw0X$Yvd>Spv*t+&wBqxlNIp{!cxOTyK_ zj@&ovWd?@Zv!{0fR`f{{e^E6SG$}w8Y*oRq5aX_PHtcw|c%xp9zw`RvPY{#lvdple6K{v@3GC7sZm!{F>1)x<8Z1d^4)T=dDLDtm}eA0kvP7l2ZQU9GJst(sA# zNExHt8}E1j6+2G!8(6I2tdqIC%sjx25$C$j(d!qv^F;dQ|8 zK#6kdu-ozBVfA)Zf#Z(_f7i{VXX}r69YY}Ta`c9Z=7i9(dBsxm+wq9#s-U3<+XK2s zug+=MLxbYzSdh$%+5G}{A8PQK6%dSPQqbm>>WuN*7mAE1tX^Z0h)<|JD7^B zzvF8k`QlV3jd$=M^NSR$^;=dGkIvFn6ZtBPL}{Q!wpXyFy%sfhce5_@Q&FKgYvZZV zv+*p8zpe((i2F)vc9vn^Or*DJm*9qz@{n?KoS$mxD|2t4ZHUSutiySwIS{Dz{OcW->hXD*&(5`ld8)22o*6h}^N3#cBCFOoP+ZHkPvvFgt!+w&b$IWOT zXNvU2W3umFHZohBbYnbQ8Hf=uvP5UM?`@Zm*w_l0jLB8fadB}De-zoawgmFvmjx==+5=G8eV*s9g+X1zC|Bbtl4CEWL6 z7SPc)k`F5Ena#cXDY{(YFN&y*)p1Ez)jPH=r~QP9CFB{`KSfZuj`i*ku&UP zdQ0k6azTF2#t`4m`!=)E0t?CS$rVX=BLn-mn?(x`Mm<|Sb5m%{m3Wo5t}T8sie$+N z79NRw`zyA{Rb6g1nWkja5tTF6-nFIouMuHnk{-)+rsx$0Zc5G_`1^vDX}ulm-145& z^$?lYcIUHeB#|ZKenHXF|ESh8>WYW==}8~8xCEq#MtIa=D2zKtQ`R66-k|_bG}b+8Emu_D@`%I zWAqD^W0`8Zwix9yRAP=&loK+5C7a8L_ZvRcZ2><^j;(6*#T@Hn^?BfjJR35>7#^Sb zFS>$IEx&b_*tjbRJGchPV0tlsq)0aH{txfH*Ezx(2B1r|x?e-$VxCF&oT6JJq0Ii$ z!f=?aBhYcDf%f8%n8-04>}w-wyf64iI_kTu5q+RQ&&s_m%nXlwrFeS5w|$RlL^BRepjhs!!5kuH9gKh!tLaRS zTi(h^9g`19x^Q54XT4n0u=lmg4}aNx0}|gTo~v*0tCQj;RzZDCIQ#4KITD@10+wJ0 zvVB`PM;BkVNsLVPZ@_VyfD&H()V7y}Fp}KQXIEnCHMx?;F~zH~?yTWwQc^KBuz8J4 zTWkE+H@K^F5aap>`Ck+c6$e5kv3O$ck#m33B&tVSIEek+OcLKWy;)33w;)?YJHZhg ztG$SEhcAOgWqwwy@O@6r(m}f5a|?IfMu2_2-m`@!sA?$b|5N4(oc0Yld$DL@xL-)}cr&dxLA^sIFx;0(jS%4kK*laFB2xFCMKA4Ol4lfB`S%(%lPL)palu00wx%^HJW+a4Ra zo~w$fchn|deij@H>-L)l%X(A`^!$w zS=P404F!crqrHg$L^b-o#ckj?=AXXMs<693o--h{U}$;xI@#}~8R0t~st2Ik?}>zU zcgG|t<@}r-S4w1O_FQX6hy7I~c0W`~7|mqh3R+wEJ!8w|r$~HFxzCe={ELDGAX+b; zKfh+Xm8AsZVrf zJ@&4LnM;gl^?h42wtve@q*ZfE?86OpbYvb(WKNpsXzR46&uvy`KJVy2Wn{It;$a+< z_49ao?K+JcQ*OsQZbFvRUDhkfV^1252{)lx4@=vw3D(<7gPe9tstm&YSYVhcvCF7A zr-ITJZa2eRSZ6kQ+pI5ds`gmjhg%7QO#>XrTQAVgK>N^2`%eGqGU>ST+$v`u0asj~ zf`Eif4bv!EKqRDGp4emY;`}iYd$tbYvP9p}P_vVF%&nsGx!JcTI@jiE7@R+4!?NTA z^DUG*0=F3{!HBdjVfLOhjX|sC>7a6dWGOz1WA;kr#%#NHHLO94EvEG$8kQqxWLINh zw7WKeV2)?5AV`w%w>t_?)R&g1F0C|Zj*?F*+EA6?lQJ+FEidL$x8>Y=I(w(LD;{Rc z62ogYaE%<^s&P}sNOx?$R+en%U5CsyI^HofMzF^r-zD=gMZuuFjs<&Ki=pO@8n_wqmHAl!D%nLgQ{4 zWshum@g+WZm!^b-3NH;IDqVp-1G$8RyH3d0?56h|9inHz`TO`8WqGzm&M(B;eqi1p z?FD&8{v-usiRB2x*abG?!tC)87=ujX(m_YdPW8@%J%FVP*%9R?h6R$MF+8TBmv8gm zTY0|2EYI^>QOE+;n*kEOql{USnUT_qG14Q6<|8HMQCud)rPAiP7p^#0!P>rlFvtn> zq=6~klc8Oc72N;oK>ztFIg>(~ENo11^C!`T8mhFs*lx<5WUK55P%^*7V#4{2IUmxg~wl^t`+%T5d8tr&-dP#c^O3zYSoM z!|`rm-y~SLOd?tF1|!D&v~_Mf507-yd@^kCl`9|~`(Ut4>GG|w)P zeo3r`6RyM7Y3U4D;}`Xw)vT2Atm|}_{$Bi5DY$Tx9oCuMLQiCG8W(ru0(k4u1sdF*TK?6-PI5DTv77zJh;f8 z#$52=(01P0ldPy-Qf)tbi{9e9Vot-E(55e88ZJ@%*=`p|x(j`yhe~5skM8l63)POf z9lJ36vl;8wn=Zc&^8A8H_fTKj1>Y5%Z_AJtObl_YN=YF#r9V$H135lRcF7C{aBfoE?6qFF;Alu;ZT@`fH{H!kZ6xD(IJ=7 z3`RA%TX^jte+YKXPkclych>g#+)E|1-84A-_oGu$CYZD=u2Yvy^K#*?>551ui%#{3 zgG(+&US5YI5NvF}bxY>ifEoqC@uNOjqpDAm1+um;Iq@eDF+wT%`S%35%lxo*-cue> zt$d~BZVb#n=OK!<(iO}DFPoT`z=~n$dbmOmU+fbbM?3pO-3(Jr+$8R?;LX?6mO?hO zwRbem^k92Flr!FxnMAo)Tb*3KrXLqLPHLC^O{-jcpig@@+**;8i0oGBDVLDbE;R5p zYBWuh+==iT0nLU_*Ecp|R1)kKtlucgJx~GOpN^fK-SOh`bzwk+qG2^Zr;qpIjsro7 z2|pV4WC7#^+SK|9l7wvMe>q=L(TxjXRuP=@i{&qf|Lx`HoD$r?9;ui~^Ts%_P*y68 z&9~tp;?wcL6h$3V*ImTQI#1PY$cUMN5f|$&fahYDncfNtLiD9!hHc?@pjN8Fc3RT; zCI!b1LK%FKgZzzhLl+8ZALlWLIY|(s#1J%WMRbMt*$$u_EXpR%bCB=5Hlv1qTLz`W zer8#WmeZ9x+g2QTh!auRW}5Z#$smv_mJ7)>{|ZKO6JbTr$H!zL<(%bO@STZr)U{(C z>-Lk)B11&{Swj)#_|;;}>i60&^#Nvr(C2C?4V3(-6F;s#k#bkZ4-Br%ksTt$&)Ekm$9f2XWzA?PKat=F)c-OJL z(u=%?eEwj`ab+>tUn(^oJ>uwF#TEB-_Fl_L3gBf`tpv(W`ToITkk{&w!Lxc#zGJSH zxlN5@rQe^!M6y-an){1Nltc`p1}34vs(!XTDrP@hyQ+LrXnarBR);o_Kf_TRObMbO zT2S;Bpbu)hJ(_0a7f}rcx?c3fmwrI)*(v*gI;L6nzFvuGwC*IejT!AZLC2ARJCCDp zF?1&^{bUOyh)P0J>kM&!R)ogEez8~`EHtd2ZVqOq&ZE!loZ>F=svM;+uQ5gDc1?A| zUrzS4ydKcoj@U>pWSt$VuwV6S$aVDJis&?aqWj#JPYAYny$YX{l7UOC8W(k&Nb>F% zW(pFA_QA9c_2hr~%SF~3{pVquBt85JsMK$;WG;Si4fVg~Im)j|j^ver_?MYl=W1@J zp=w-Q)6+Ht??&6AGkSZOqYv3~42`7_AuSzbCZfYMmz$mKDHrUiAIzwWP#NSgAKZa5)YP2eN@99XEap~e z_BH3Z(p-{PYNno?ul6#2(Yae9=oaRg@eecT5f5^2$e3MiAhj`}%&e%{c1vaRUqR~A zpQVdy$?=o3bW}E!X)L*OItK)TjFx_mkUB|>y;CP3sbnCfF|C2vJUn?wC;JdiZ`Wrk zF{T*2CZU^VD@QLZLmvtYBiQj8FA%UQCru|Wn-y2&MgG*;nU=-w7ttN*Fw-rRg{W}_hoio;EKq|{M;G+__+2uNi!4Uzd2HRKiO>V+F)}&qT>9n z`SEDr9z59ZhpbWm2K>Dx+5L2y6DRr*$8LF4=XbYL7PE!x`r$lfR6Au#`$4~%P&O(sL<=cHSn@;@jWTCVDukASo#+ZJh-#amw{f z9b@G5vi<1EF`_@(IwX9>u07e@)8{F9jyEIfChCd!63^!pn8l<*%>Yk``RsjTb05@X zq_o8WQ9HDK0PdIW{H@@F!49Rv_R1)k4Yf?7oUO}f?zb&4g!i_Mk84Irf$k=}$56QI z`ku1m<}RS_3``An*qqOPNgxjrCncsRGIpnq*>c~`=7ikXpNWx<9l>kPpy#d-^4mI| z$e@U8X6pq20C{dRVyM6*F|LpqU0MBR;`^=hJ7HIfJ6fgpal{T2P4(wEIo`X<_e(!S zC3vQ+nC{hQcgxW1SUqPpEV3dY2iYy@}d)5Hj$6)4%r~&<37kipb zTNX4alYu@y}T3>KQ*s5|ieeN>~@v zV2^DS`iaWm4P3tm#aQU5pOL-U$X6#RY$`GwVbyQ&NqaY)=L8PLRhHBnO88X@9KW|_ zZp4}Uk$>Y?VBy~WQklv?Dp59tEkiG}-ReC=VgIPmo_nUr)FlbICUw1^ugB33sY;q8aWTRiJS!_WMD#`Rxo9Zr3==j6!oyT8J>P`nvAyKj)|+3DKR z87*MLB<*b45((FSEMb~!=2h7u;K(YoWbml={JE$}%`IC7x-`znkO~F0;0OhQ{!i$q zg_Ay%{;4TJrY`Q%WSw9v?>(tNlid7=$YI1{uu z94O85wQVF%j(wr!gJS1qqa)flOI_q=vx#33XBW5AHkKh0w8llHKg}`or^RmqG6+)? zuX#`tpLt4ryeRc}K`3xg43)05R-)ASpTHyvj#nfPER)}lF(Q*B@S(!t?*-*F-4pDA zNLi`rut!%PY(8H==EW764Hk6kejg)|YzXBs?SD%9s?OMLxWqi=;rsKibUtm|39!MM zN!iffN&Y5Vv2hVC^`pyQak$n+_JP__05fgPTWRpBP6Uu^P1iku7icZQLFuhzvKGu2 zE?4)F^5iE)k5q{It&R=ZyQP}<^1oi_P9a?Q5&}4_kX7TW&*a4FJbCb0QqRgyPpQvk zY{0drF4io7D#Nc$d_Hu9T+iZ!U6PG^QeEpOOT@<3siERMu*`s=J6B5x{3vg3ixK|Z zXs7oLVu3#u<|KymU{J~}xH=WB$pb$)LT^O(;NMnN%xTqo7L7vU`o6l!uFVoi&`jG+ zu=+C{w%GfdIdZ+^Db~9U>3NSpzgM0<)uC&*)$Ilg^bv(g9Xf#J5{34Wt82$4v`6u^ z%1e;b1!6llx3*?&-U1?p=rZno0rQXZ{<2~C%PR-8HZv#+=0Kb{GaD~rn8h8UTqfB+ zq{ChGJl@h!G4oJqs#RQr!@k2IsHm1xmGb?FFEAbO!9@qS^w6d#W!j)?tfW~+HIdst zl=|gvBvYu@NzVsM;oj)>%b(O#h>`ZQmmyb(F^z^>j0&dtmjNb=7&qRG=$X+=Rh-hW zL7dv-KOWQA#Mxu?$Or0^*ZT)HVm}2xL94WHW*OA@H>48fvV|tfvbM3E#;{cG?#UH8 zeCxzo4C4J|k9Dv6Ds3`|h=|Os%67g*Mt+!$uKDo~$Yc3lDhu}i0Vc0t8xz@nh9O8l z|2i#bP`q=^!NF77mzJZZ!a2CI%BafHSVo(oH>dbAvkAK|S?>mGa26RD7;-mI-h9SS zXeFR&z6joOEqxTA!D@ZMxA(|l@tQ5DdXkXDcyKUDL89 zcJ+q(YYiX#yNuzf3)u1NP9<|;N<_qDfMzVOrO)dPCcU7+Ph7wxUbY&@S=@XM~St1 zja@Z*a5u-j+HO8g3s*0q7U6VW zOruwCBzxXE^HK$a?+qD~en|=a{;r6KlN@2bMR&3u02q4CPioQB0sA*2eA5^Y z?B8}So%!@>QU$0|VHS?RR@eq_7#EATs{3gancX9sQy9k$iazNX_J}0dHPL}fYvn7C z=~SzYX;nAxP&4eX1rRwFg(d`cg`;N37d1Zl;`V-m3|xax@NE@!HjTR|6IMw*e25q9 zAcz0>`e*pHi5zs@z2R?>Pwz=kmExFrYe*U)mghqHPfbG9?jkm{S{;Fy^H9DMz=2hQ z)Aov%8wkR@`%JXrhKO*9KrhToe~WF9H$uJ2GDwxzrcHC9I%93=#sb(O;D_qw4Ei~u z?Pu0^*FuEs+SAXN4^#Qy4=Z6v%X#B!ySJV&8bhhI9d!nbOH5{6s`TU5kvbeDe`~}( zP(hcT2}XW_OAiEY4*`!>-re&-%q`NgzSQ@a~Es4c5YSTqqYJeIZZf0}S+= z*m=Z*Au}-=O&_wn3S&NR|M?(eA_xwyfChcSHUiOFdZ0#{muvLi!2DIAjD~JK zf(7%@p8xaF#!*f`vx0sbMzven*h{Q0-@N>l%j@D~Oa8gDI~Jw74Lx%OGR!mg7$MGQ zwSYAyKz3Oub@riDPS_`Q*})|$1?&>rV>Gzo88C%8X~FUWzk=wNY2jGlZ>H~hLuDz? z$vsGh|0Qj^sKD`>$xkND`(DYm2yEGT( zNjVBfC8OeTiS>?b#l$9Nw_^5xEH`1zi>k$Gu=u9{ znwI$Ff(O1nz@@tO&fPqZ`yy8Yytz#64zBgZ6NszQQK#+pvZ`8P4p0OBi;Xx`>lA-Klou5y5b)ZfBj*vJH{?a|6`ZsQy_A% zK@X;Eo#FcftcO!tgpq}FB=K~(ddcqQ=WKwtYL%MvrpQy!e&q(<`Kjx}ef$!U$;#>S zTigMLFnr+}PrdT>!KJ8XD=qbwPTiqa`5dx(=-~`fV{9<)f4^}!xJUWR(Y)Bl$1nJzTf62;!f|%z5x8ha8Flauv5<`9X1!+A zkfhe1%2Q3klM9!U!!sBA$AbyysZnmwmlKD{!*UN+r|w`sNbj)mW2;()6e6*U{m&o< zLH`-K!TLiSyFiHw)Oq}25YzA6oi=9@5v+ROl`aT<95uIRco4^v9z5b~p|fKVdiK|z z!+t~z*g_g`H9as%^?w^2=`ShycpUJf(+LT4DG``BR4 z7GoWucr0p#T4U-(4^!K;Xezc{$yLw|R~DPUOkJEQs$NbueAQESXeLUfg8lA%wKnDd zIK@_p_1+5`|8zHx4NQqz9e3(5_ta~>_t7pT3dnGHkkk>2?GdF^-*&nG4PfBd{_xWG9i zz$Wysa_g4==%|!vFeZWCE z?7%A@1ZfjE<$*7~Kx=U8r>5zudc^ww>D~wKV4!Bu4=u$?kgp63Qvi^F6-nI`aQ`-OfRAij zrA&esBYGFhMr4es=cajp%3O9}9!T!kBLJOfac9e!_R$@ZOyl*P+AaiMmk<*n7@oPk zWa^h1(I|^3r(Wh7#LFGrcDkt!JIWT$GluOhdIj@*GXL{@OcOuMT2+X*POd+iGuQ{+ zbET`CIe!q8Q{#lR8H}r#04h}sXhDL*+=KUtJm&oS1kKD#lv;)u?@xus;WaycmMSK$ z8(V0S#p2^bKf&kApZi?a%cn{6_8t)RJYGWQz1sKJWS^@&Dy;Yyo-d{w;oNEr|Ncnm z41zq0Fc0bcHOxbzdKv64L;>^flzvnwV;@-lXX?+l6Q`p~uJ#yE%A2?}7-uMu`*Z{R zAdeA(@RFA?Us;ClTDq2vm3F-^FA4G(Rd3Bt-0m)#cvY=G01ED#F-NF!7GzC(xmN_2 zrQ2MJc+*#>vSWfCML#t{gfP52dr+d~I8a5T&=-4OY#Z!r`I5Ed{z08)JuykSXTDtY z^$7%d$_#=DuXk`EDoKUcekxM=h5EH+({P{U9rW^QySvHP`pLp@#G9$3B7PewFN;#4s=643@r$_FH=(DW_w>}qJQb@g;YW`0+B1CPOBj6Mct(F= z|CgK35>m!EL#@Bgkmkf0I>nw-ecGnN+a zn?6C{Ssm7yRX#VxdQ}fk{rUn`#aGPrRL+vPqMU7$Cn8;S}Qle>R-x++y1k$)47$7Rq|A+2K zVKvio>j)QB%;OWCeTKVe`$Y)6)la}1*c{-HyrZoDpvch*+ zjIRKE%NZj(eH>idp)WYF3m(^&sE`JBMw54t=RiYlUfdYi**X_d{xYVr^mTBXW#7DxSqs5l-luHepSd4K#6sH)RM^W|8hhW`HKmG#AFo`RZ#s4^66 zwNvr@?a>zq{?sa4i(!5!au$uZ!&sMk_y1bga6F1v(XCwPLiLlX_#$%^pT;s8_ zSPO%{9dYRoF&`=RpO1t!Ci`?o4|lMiVV=j*wG+-oz7wYmu=pv~L&8XVIZ|fFXxS?g zBGe!be4KeD4O5Alj^`mGRBercJgH3>Y7BK*B=^y8SR>ymWbWyVju{&Y9i(A4&}oRl;D2khseqXl^-?6;xJQw6U+Hr$SK(bpkw+FZu@rx|fMm zOYA;ZZcTu&I8U*9pim^&?g|PSLI6m!9BNqB(cX+0XpsO))G%QZ^O-xyz#Sl-a~z(P z)!vpeVmnnlcJ2Y{b#hR^Mno~XobMl@#s?$R7>524YPRuCgqmtY58^u@3k-xBpnvpE zJ=TX(ZlYL`W%mJR-usor%85#9ub$(t(h(9!kRUVUo_&1vH|ux6(wjsv2G96E27fp- zyK=X`D?Bc`oh2!WFGB9Qow$4M(5GnLYkjd;t{@@rc>E>}6pAv1fsTktalb<%kX#BW;HQaF?xJ)U?D$P@T z$tyvH*W)pR{1uuvQ@)_#MIV6iTbvQ{dLaDUiPv?@N5U`h>N0@DC_QUl(+ZMuZPta>2TLf;xeBdQrNQkBoh1~FPr z-iC_Q6(EdJ-9JY*rEDJ!T-@6ZEWE{B8&?NPF(xN!AY0=zMLGe4(5R2;DG6^>Qen2 z@|PDl?-_zf#kkHf4chulc*xSBzzJ){#XY4T$a8D5F^0i+!mNR^H z;&DLBq0!Z?G+jghaqc@&J_gh)(H*CeJTOz~Q?$gh_ZI|cDUMf0^fk&SN=bk81*^^s z9wX+`uH6`@++dCnbc<=zF6If+E95C3t-5zV=%?mLmT&Y()K9X~ORcziNl@r)`bzgg zo-g@^_D-Z`V;Vh7z@GQwTcSuA>%tNx2{X3EUTU%^nW&Q&l&|gHQ57=&h|C30GG8BG zP2JgnxaO!{%pr;4~ z)`>}6s82vYG1Ltk2u_;jCfCcWi_=OQ5(Qe;}2Q9^oLk;Jym%p zfz5!&P+WTs+xjtNy?x(6s;vQ-ZpHXM$b3(yglSvbX}zh#>8^ChXV2El@{c1K?q-L~ zIZYWPcC4aK^&jHW7~ot^u!023c_rsScwn9gz_Xs2kG;$&khMhLVgZ1r<^F7cEbxc@ zL2@#rUvW*W$b({urn?@BL|+wH?)IECXjwJDAF)qw=z{;2M5$=4R+ zi3yM(?Jl~4!HE~(gq2HmkG$Ed13)^hD}f(Oa!LUOQrg676?Qq5ptpw8a_s(Kr9+`D zs$XV*3&j9>F#{gI*nK4F-~g}O9x^SkU3j5Z>(y8q?x}3-z8A)`Q-pv3#BWtNP3Gy2HT*={N*A5>m-KV zj(6at_AQ4alV1h1ya82R&Gt3u(Obka^t&_cZX|{6*+zuxVEzo<6;kedQG^J?j#yT( z-a2k+1m08HCU#ScHcZo6(v}jh9dg*|Fptxuxi@^}G`-+E*l;#CN6$|*)2hzv2J;h< zV^lI;{V`~~3o>Z~ol}fl25nDOOF-@DI>@9qE+MgJp@~%m;I9i`F7M7%VutoFq=eMKvgg_9k)Z)#6p6-1)YkeigWt zt>e_}}a zELfm%UBOH7G;q+m);cJx*K}=fE6;72MP<7~&UKwgt=J^QCbsVV1u0OaAN|CHe4<_2 zUwA*A>qS&n&(hqoz6hqP5fIJ8iAGZ6GvZP-x>^YlBh8Y2SOKX=?L{D zVwQk+@30t|n7n>;elQ@14N5!afTg)A+pFWNW?;@lVRXj$8#Yp(0C$glpBNop>(ZD?Ht*^vxJ+XNUM?Cu^1`8_8#GNHE$lA3Qy?MKUDoM88} zz?)yg$HY!x%t6hus{<%JjQK$?7t6w`QQ{Lj35+?W@^=FnVt=DUgpw;!y(qwR0%}SE zyJ6&@Sw*1y!+T|~QRL`OWK(tzRG8?}#{-O+nwW(!p98Tq1+yoPI{c5gs+TItWK{>I zg)xJX>;^@5GHAK|$%T}Kk9KwEY${w`K|^0)D9=MR2A7J~_=oQkKZCvH%!v86@(HL+ zf=HIXZV-3=ehJV&y6RkZDB_2<+aAro-FlbDA*n##&rNf2+9Lx@kR?xO?rt8wT@zt9fF0~AU#&j_rfw6Ub z`$MP}!pJZ-Ehc7_TwA||zN|3Y`ARIL~P#0x3`K}^I?*8u& zm7o~#MVc%*RE0LvEycbC&Q;Jh-6ov=su7llJ!dnXVYJn!129%%ZAI3^WvtSn%Vr$( z(^|JMrtvq@Ju|WkL#imC<`JL-UFK~Ll`x)M!&PkSC$*z$W|8+N7GER&}KKmJ^*HT$rpu1CDmc`4@A}3A( zG{0pHDi?nfiW8I&y#@jBjyhl$^ucAnEarmfq^7~_HGAF&d4#y%?S-gL=N0%|3z!MP z2n{Kj&q}Nc_M5MKU4L<*C(R#h*l~l7{Sb^&`AWiNz^||4xIkyzn;uxW6xMk#@tLwk zhHrFw2??G?b*F>ALZ)y?jJFG}A{_+ENP1w=gF2i?W@l;obJ(2EsUkY6plNDi<8~NF z2KsJ**e=VHpcWjy<;$KV?i&P^&?PaKnj&QZS23Eav>@Hl(6WHf5Oe5++lEDO8wY_| z1#VWnG7+3&>+9=3V>+FYTK3HVu2z<60231X|44fas4UlRYgiExML|SC8l@xz=?+Ce zL`pV=zwh2-4|$&Z zzOEH>%{dpr7YA4H9+(7MB!@_c38US|w5OYJCBJ$>HAlehM-E3<A4WA3FY0)`Gg95SG- zo_zJ2jtU|F*@C-MLy}A^>g6Ekm8(DMg!GtR=CKAM>)98f2ny7az5R>9~s)iQkHA+9ae*^ z485;9JYwtz-rhW6)G_uwaMF*|(mE2XbS#N^t=}?`#LexKQn>%RX|xNDSm9l1>NPZ+ zW~hXKhM0=f)g4%_wn!uN#%ahU%Whhh%_^K&$dwb)iqif`(s9lG$V}jSe5TeJd${Z6 za~*_fc>@fyK>0eT$Z6=$=V${B>~P9X0~HJIpm_|;x1~;(lKrUNxG}<7NSwGUYC0N(D(CH&`ry!jIxoefy}&ci%$xRcpzk zD!q=`jOKK`Cc9^5X6Z#XXDr1_Sn5+p_uFbWr{1|^gRYpo4rz$rB z)by8cDLpqH8POELnpWP^mz9DW%^6gK`rbo$EG zilwSb{Zo5xp|SfyBb(Ek-TU~zw%@3h?>FzGg!Uh-6*@sBgktdMW1*<0pO%rUzHMDjShc-Z>bOhoBm03uvlu+`!3UxFCLH}zT%Ww0&+ z@A1-LatDE&;1wwS^)+s-pEF(n{`%<>0}!cnp)^texWLsF-lAR?-nNXEO1AtO^HgMV2} z7qCPX2Kgd~{KFFbsqLfk|@<~+>wM7h0?nF8* z4K2`oH@=@O%YPqL_O4H;kRYaM?Lm0-YJtA{WwuxNVymi;zSZ{NQ)_Dv-zId=3dWBi zuGI7r+St{>^qS@y*^k=U58J2EZ}!5A)4tmx5+J-D85JdC`N- ztg>j0NSQsN!&TJ+Png#{m9H&;tBb$ehB*tzNiT694VXK%SO*^eBOYJ%-_dg2EM29?MtY?9xaaEVE7tix71$B&<5L7=Gii zyZ3IhTia`)#Lsng_J=Hf=^A)_jz8a~<;*wlZI^O12A!zJ=55lj-%|F>{rGp3021R- zAkA<~{Ra7T_mC(?aLGX6Fb3#*i@s|qMVu88jg+0~$GY!n>R#KED@anZvTnnCBy*?B z1Uy1@FT}^4liExC@;HD4c<-*nY`RZ=5)rrXO_NQZ-Rg>1@?0;rF^c82dA9>?hC`lQ zEO%3M?8GFr1TK(?;w(U8A4a(|a!U4IB56)@9}59#XzTn9rL7mizb!wU(}lhg6DncL zRXR>q7S^D=U6`fZ$b9g?_mCd|B`5Adjnr@B^@$dv>kQ@ z%rv`4+W{aqs7=v5WEhMaK3YRqKeO8>)$J%Vh91+N|9Bk@DtjWy8wp$%Knq_6kJ)2TEFg+x`NZ@d;Ru6S(Na zS^!(SCb-BGWawo~Tk+#aIgLkH;;s3J-#j^$eZuD=0_qTO{r=ZbgkC{4?}*R1b@GD8 z;*Z;7mzZ$(M1-Q%WDl}$7z2r7bzF&iI3dQTD0U#VNbSao=1xMUBSrh2#(G+W$+ z;m!+S22f*Sy8_MDG-?d??tzG|kj9lCDe_Mf{W~*v736M2`g##9b8El{Hf6VrbRs%} zav@X@Gaf{vv!w|SZ~)^QN;eB^m;U|6yCQ6X^W+)DRW{7z4zdVNKD8Un+axZL&_W;aKA zD+G{+nQ1q4{I>&ehVqp1gl#A8^p<;-J8Pdv3uum+um%I|+ma?sOiba3ajUM@DP;YV zN;6o#|C~vdd$UMG3Q~wRTkxUc&qzuC_%RfWy~KtP!-xS`HGCb~0f|4C*B{f}nR{jp zP=e4f#4A=g;m!AoB?w>w%_|$-m9HKY*<^k%_=o`6{W}U`SE8z&9s+N`I+txJ&_9TW z2RA*?(poH06)(T+G9Jvs|J!@f4N9;{1cn3kffQK8#I=keF=2h!OPc~RJv3i0?9K>T zL7j@4{E?y@NPI3wIYVn1S$XBs|2$rD_-ZzyUKjwcdMXfBI7JeG6N&)A4xlF(m%9z2 z-cepvy^RnLs1o>UD38S)k;+tQ4~})J>sjNo7}4}Pd(Sq(bJ{>_@hVf zv5*0LNQhJlJ(Ei|52j^#OMOI6rCY1Z`V-!__&!((z!GKOxVtt_dyupWkrQDvJ3w0X zrD1XIkGqxt$J==GVVYV19rtVeHv9expA(Y+@fj#1`W>&s*&H(JNb!gZao?ARZ@gKb!x zaM4o%NJL`r%(YfrhGT&kX`vb(Md|wU$|)(4?O0ZZSuBsL253EqPyD2a>z#K7ObapJ zWJ_sQ*SfcyZCL_Nc@q#Qo-fiTY*L8>uI!V0gj@lxOp+gl0Lm5r0Lu0_=t-XJdffOH zwtk^^@NqjvsrpZ^nwHgNRv?d7VLC3MOLuxG-Bkgbt;=^z{=y4jAS0may}gm|2beTS#dC5 zIdK%?bdp6wTT1B?RENXB8h}-jG(C|Xshw>{Dvr~)sSt!X#6Sy*v_LPRy}QY`%jrBU6<>9#1T)Fo59?%Jm+52iS%%BN{8%F1Fpq z8eRATXKY+pn7jdjUKjsDuY$+b#~)sZsdB^O{Ri&0JeVuYl-X%OjlnKUg+7*s8e*T$ z!%6$#O?RV3B})N{aijYR_`~0ZB)P+3`dA12OzB4re!oxlAi%PICUPSCY2ZYfCF*%X z{h4JmE-`(n9Wq5uj^$vVG*Nchqbe+uws%j%JIS)C&oSkWhWi=XNd4<7>tcT08&H4v zkS?ZYe!Uk_cD-Z!3YPsW00Tro=oDCaeP%QD!R_`vL;}dAQ3L19N@j-KY4RI`4l;F1ZUAo2^#|mh(1Iy=|U$ANd0GlGH z%LFEasiMw8*C4wEOfa)e1DW^CivJal=2bJ({R@#xflhQ5g&@VzbO zU#}N+Pw)$KeQGE?>N??pilZk}r~hLzB|1`6b63BTzF(K>h%eR;*ki(@lZSWge68`p ziy`(<|3rqdhSRfgm-<4?g4d$)ag0JL>yg+qK-owuDR$4kr=2sDUstnkOzm5o8}>+? z{Jp|%(P6*Zb8zU}s&TbQ1*S?Dxe7z%WM*wGGvGp}=br;D1bhonQ^lbNBxSw-p8-i8 zdO)&%*7{Hm0ypk(lG@=hT_B91%A%uJ0W-|(kcreuhcWd^)x8@Xf|%^Lb=s2OH1r2@ z*QVHY*D7zs~Z1shn~ace@_3$6!m{`sJ)*|1r^gi!1h5A^{g309~KgepVP`H5F#K@JttrU<-VQ` znH-IXftF?AKL=$0@SyddAA94*R8GLX1w0`VVu=5WPH5`q=_bJw2iYI8>l*d}2O-|b zI}-l!4hBq`FzZZU#?gn-L1Yyg_91p+ctzTy#G9&6xq`@!T(@DhOPEm;tE*F`wat6-RS_KlxvVid6DSM{=37@<*TO7zW|N zN6H(Sx-Sk7{$L2+zw=GO33HxEC3h6ZWxUD(bVz2CE48(aLk#6wrbrD-^c~LB1)>tCHm|{?ZzuN&Gk;(KSgt{;@#%r#%)}RC`)TO(2o2yGfuk|12qzMa|KK zIL#v-1TaZ1Pbakt*Z<`SVt2-)DeP>2DeNTqfEQ`2l;37)d^0m3FM$YtV*auQG!Iz= zMJ=6#5EpL3K~fW)x`0hJg59WAC=RR)s(l`}VD}Xnan#+6kS2Xmm>B@p z+sYtgOot<)*TF>c;M$4uVwp~A@T{b=SE_fD#$S?Cw7GY^J#a79%P2zk9R z>i5L)Mc;CtbR6Qi=2}QMZYUmAdY~)w?)n6>6q=08@_a;I;w>6fQDjjZ*YB5j*gLDo#nl$4GEB9L`POM4cHy__(ArV zjg~!Di%}=uAX+zPN|wBEC>snrV16E{3;xHq27o^I5Z5fyN1+ej#=#3(Th|9l;Y5LG zRzQ7SWP^=t%nMOj?2>GK-&f3oG>TIZTY32(k-+rf!`ZoEEj1pKkpp}I1}W%1%jm=5 zoB1D)w9ZtVNGK4DY98!L4wi*cTDr9MMk@Dc!*G|zP=x89*c5#zz~ES-&%H%uhH38c zLE+$|#gN_=gLo)0GJa0GmXtC*%1birb-srxVYZy|UoGNRPWWD&`Zq)`g1jhGG{soy zUy*xd5D76;+mR=uLDfJ@<)UDUc)M5pM3LXU0}p~EYO<~X8RC3C{} zNinlzE4sY-e!!@@QdQE{5U5{nCnHmPS*)%fSO*Tem4}!;qhrAn$&;6z>y~wOG$uJ1 z{v5;LVnui3K}R}@=iX^HmW&qfnU8)IBJ)S+%YA>fCus-Ln1O$!F)RnKt#_5I6+#ZS z6r{CKD!y|V#4MQ}-NHbERAtJqMb*9-wui&=p}>f=?SLR7>UzMrJz{kKk1r^5CH^Kt zc6|LgDoCW?MK8UKPnn)v=kBQ(-M?hF^R?y&A0^sD`x809(8x#Oth7_bG?mh+ZVzVX zSmvN*C&?au&{_Btt@XQ<8^tX{x`C#(A_pCPVm=>{fd9u2^#49FP+^O?9^-`vLG!B) z97=^Y`n%jBj4)#^~(TM?uW;H&j14p#1XvQLj7=Qrhb}W)+}(*k;KJ@-D@2 zTaRi$)Um#oVy{xOTP<7Eh7MUwK0(vA@lw=ljt(+Ann#r;bVUBW1a08JgT6beN9Pz? zp#!lEcsqk+_oD}}=2DEbwD`4iyc|12Y}n`-iM{gl!z}Jis-Wr4=yY=j6iWyR{tANt z1m`FO&Ymom>(9Qa19df!6R}(jnn@T$e#vNr!}(>baWJ7ZBI}Ul&ov=CI?G7N2Su#k z;gdTkiVoA%%z1<@>2RP{f{zqtnPMb+%Widtm*nUI)^Z-69Y^|4;3cO21Tr154e3Wr z42IL@J!LhZYXyDb^5|@f#|v_{19={3ThVG}Bu5yc!*JSB82<0mn5lL}Tr27zX}w=# zdi48}lyTn1lmf`04>E6sB)ZJV#-js5A`m;(wZG5pC5j#D)yeL%8;s%VLSlNt0#ZGG zkm^aGrFzp8z*YjOo|+$=YPxfH2TXhTBL&c>x~uIUYCBi6%!lBAr0VBClA|ND`T>Kz zkH~tRG)Vx0bHNW_YxHxe5k?V7Sn1SlWPi7a{W#3+dJN~ExK4baKjNddq>K=y0^Z-* z*Q=H^1efU*>D-o>uZvNYVn|EpH>guuhIUV-ztn_ilyYwzYgK>4=Bce3%y?5WP0&%s>;Feu^XVh1eg9;t&&p#p!#FH*keU-jiyq3&e^2B4!y2+^@p3n0xgf$jbgF#O z?8Jk282!CzF=(ZE>Vs-q%s3%jin>OGeaqv8VFLJ<1T#&s_iP(#DDgUNJKM&^OIL=c( zY6L2!Gt$7gc17bA+Qrn10Q`|LA}LmJi?G&4GCv4S2!<3pkCyRs9X%Fkk^bkxXaPt6 zK4e-Xh2-ux+KL|?`L}cj?Emfw&CvmDDk>m)y7q@jJ(Nv4i=dx0tnzE1D&hA^L(g-A zf@=#xva>PHid_nCuNQ4_Zz0Z z%YYi9@@k&q{4EB!Kr#%$sZhf56g}uG`cI-iSCqSUKYA(rQ-ygX4{8;Mc))cTgfE9Vu3YaPr)t|0`WAe<|+}U4^6oZ`~sQm z``TxX{EQrj{T3q;eyV@u-JU@|jh26#Kiyb=>;_KUvz7RO0qNVuB zE9o{Cn6{!7)ygC@7Hl`OLEV#j<{tJF>R;7g9qxm?>gRwJ2|@xo6c{H#)MEO|jhqS} zGBlD?cl5CS`=e#ST~n;>{`)ByE#3v6&JT9e%|I#j=$I#orsTC21u`pjo68FdAsSZJ z42kqAscgv#p~sndjB$zB{g?A&%2W|~xzl4nRgh#6T89Cgz-929YAEyAMtBbGIVK(V z(k;IhfvwrYKgkYrT|fwuAGf*x_4&p;TGTV|TL*v^wp>NEx2n3;V?#}tM9=Rf<%W!; zP9l08EgjVvT*T7B#o|zbhY>v69Wh$t@Ty~r@Xk_5(^2_+>^QXMzqo2*UAUco#c%^F z$tMQd%;q#c{N}%Toa+1oq$$)8=^q;`bz83o<|q9oc#Ki0=b5uL*-wdpwb~>QksSFK z^&D?Mv@BhH7!C7GC%eWigog)&O4UYg%{G1SFM2ig;1Uc1J6H^Z5J?-_Bq_{v^F-Esl@ecgH`FxEgk;mbFhtn}O7 ziL-6{oh80XKzA?Hb9d{zxt!x(;>*FvK4(ZZS9S z$xQgkx0{4GO`YQzph6aoYM|_4mu+q03t`4IwcKTaoQL@*5yO=9L+TzOa@IW{c}}J) zwA{PUQ)pC_vV`>uba^$0{Wopd3A4OqzCWU8Oeq59e45^e!2G79BL|# z&aL-BSCHF#aVcHp%Pr0T=RyAc{9Q1pSes{nV--yLbY4!lzNr~SS=?ZFn)I<9UF zf^4~Pl0Pv+6DEou6%r-b9cCX83p!`^ek1ewC`w1mDamBec1y`4+j7`G#M1(wLFquF zMa6UyC?Pa4%o;!5OSQ-etv<+k9a1X@Y_e!!4_Eux;m6fifU~*MKUplJ_Y6!b8fL5l~$#C|J`sM-S0nsA)hZ0OT~wf;+P(t&PYrQX>^%|E6}i*EsCrV?lPMVRAxu@&xc!5lU%!4WurnrOx1Fecy`b&6 z`60(@Sj%ytZ_<4KJFDjv;6=oTlcibZs}|aRD1nVjvc>(mW<+-8{0|QTg)7w}H*&S+c9tr^rC>WHLv?QF95Qx+14p3L&MJ zCbuNvYFYIoMoBS5NUU%nPCt4_5YIj%z(k=X_xIvH{C!~d1v<9#SoC#rwndl2ekxm- z%JVjC-8JyA$Z_Kbw*90po^U8rdciFUuN|B9^d}8lv$Ocr61>}1#S7fXR$0AzF!SDR z`!)l^6fI;Z`+7H(DpU$!Aq3oC5kUN(kPr6*f1|g>lg?|a;|)sj=d-%44|mqz*H!3M z2iXWuyGiPF(U>^h|Lt?))C6232g=1_^P#gGIu*x|#g1{96WkVkyar6t#-1>BMLjXS zz5%Q7q-sLQJzACqH(-RTfRHzgvO|Q^66KASFQS`=@FhkEF)AEPC_y9Qo=h=2Zy4VS zJ_C{_Z-@)BL)!SnFJ0;+hRrUmn^S{qpStQQ;JqTuf1C94cf^s}p;Uq!i4N5e9Xd;p zhPbK0`dTPGNv+vfcALrY zfze<&GOU6L)V;R-mLV5!btMfCuK=o#J@T&EK98~Qe(@2~l?!r_Q6vbS{PFs-bf zkHvTnvliLU*#S4?)M$_Mb4GP9*c-FFpp*v5aVY$Vn$cNT6C2ecn{dyM$S^4s)77w% zzw_ zc6`pnMeKX-4he$G0~&>TJNHye8b9^o#VOBG3E(YI5BI!JP95G&p?u{*9Dnq&wJ==h z%RD{O41Bt+cv_^qG4K4>pV>fBg%u48C9~)ypp}oqWxUgEK0o5#rE0{0L6lK6`+1S8 z5pjK=gzb^$VMGq?xn|1KWXDSNYC_IES52N~E!(&?JLp5zgH87^NK}ZAQ#tkJr3d%? zr_Ox+V0wY-IPzm$iOEtt&z>*99^>4pAXx{?Am0?`1`=KmgAGw_5u`v#zW4gZm2BLL znXY4GG4NX%^VK=)nN_oHN5XPjvTCdnA1tePHw$1b@ZCv0oFBX#OYTv)OBwGtzUJe{C&H@u6Sx!K*%e;U4(cOyg71Os}tK&bv4#-8%9DC<*GmLp=a1LD^mZ z7>keSI&fAmn!`WlGiwyc7unBTg**2!G+j$LIQu$vg}+ZTimW${iV1FF4 zH~9REZUH$99DnA>BxqXMu?2Zu@`B>H+AXe^ja>`lQr?`+CBAu+ zF6{6L-o#v%b>E#;Z@|bHwnR?kMX|RprwhNIe@l4yO_Af_^A7tWSBsLHT9vy~{VN8` zFBBGjzRR7T6l0kO)afxMb@g}|Q3$yeJhG$0jGAI-5#RY~Yc*k5w=3a53$)3MLwY6zHrb1@4Ov%Bv(NA6;N^m21#F&wpyVf0(%$DM`UT6u$x>YL3@ zW#L;EjFetGU%;GDMayl0>lCuAGzx<2)ML}>t2P3hh;M5|hpfPLl^VvB)UhdQu%>+r zPKS9kc8~#Tq3&B~2J71U>*whlE%J_A#a1r1keX-u$fWn$pr5f>mqF@lOtn)!5~v$y zEj^SJKU-5DT+X-0U~P0&ra%cwF2>(b9NKrb$)0kYZ>C$ z+Y}c5&K1la+DR{^&^LcS*6KYS*hP7*u>|%#DuU?g#am(CwIQeu!X6AG1bJlhE}DMm zI-oh*Z~bBhyOb4!Ag>qd9Lx|L zZ0B>FTBjCH@zNq;fFH`(zP7U_97fF-d7X55IG(`ja5W0Ic{V3Oz3Q{7ZVS1CY+AeS zAj>G$+dttD5A<{vm*t7Nm$Df4#%p!Ck5~BZj4f}EN#Qm~Z3g!k_T4*t!D+#wVZJtm z6b0p!9EL%u5TBPJxz_<({VF)^zif;b>i^`ExQwhbPr0z-3*6+ky7`ioW?Ojmucpp_ zIZ4gl!O!UhWT5sW8PXYec2Wd=*5CnMo8V8O=1R2!{}Z;5KRi@7Qh8TUC%`+ud4vql z?W+&*3SN%*G+R#^37$?D|3utUdqQECP|X&kl-Wp8^j#a|4)S|h(Tz#r^(bDDdsPNf zR1j5orCA08^_D|qoZnicmRUO4t4CK4gYAQG(V>70D?Mqgk=6csyZ?O9R6bj@@|}Xp z>OJ?Ti>>bnPlSpM%!RjgL9w8nUb@!T7;6Py%tTpD-2Jsc!z5#8-hu79v(3lfiTUHQ zEWk$1PzbWsBMV86ap}v{gEHfq8{)Q=fAUKJDf7_sBMxOb`b(W#mJi93- z+Ls*l#g5}EoLNNr+NA$EI@GC5+JDM)?ZVtupNDte;?p#PRbOHzRkx;kc&3pQKWQ4{H{_?DuF6Za6@9wF19a7c4*Flz0lb$U<2` zj1er@l6Dr~l=fdipjP_rN0;=j;R|pk_5H*+FdP}T*UR^>L)rh1o6imjT@oXmLBD<` zz9=G(wGo6Op>Th1+vb{8>1t$Z;Z)3`iiq&$1c9o{WX6y_ul-N?=o^n7BbC6H=Dg?B zISrXpO8oF7nIJU_F@Ah=Sg=9zfc`-)L}*Q=y9d_Mi6G?wx5 zN)1S^k^}Ay5zGa-x86Ke2ZX<9)rdH!9{=m3s8WcZydKtl!mw&OW(G;0lTqb6AEe(1 zx@1A}D~P_wMX-zxp-o!4nh-+IiBr`*;$tXJAd%Knr}2@>D5J#_;gMV<6Hyf2b~nl9 zPR}(x)$5{SwOJji)t*sf+O!;fl!;a)kow9>Qd$EpckTb}a?_(PcVGKdF?2&^WOj{g z=`KBS^RP9RX)d?U`o?9Xx&{XM%nq!j*N;$oXj6lTcqf1inkRtEhppl8L&+8bS@90w zFlxAhzeJrH@h%)YIEh+NS5dE#Sb*m#y2#kNnhC)=pLzu%1?mwXBz}Ju`t$W^+9iWY zCY#+>XK7*Ky$Yj|VM*OdJf%Df(sDi>eB35vP4Uhze-V4`u}6(i0yL4cYL%$Ct}k8( zsq6WkJzx;>xV73r@`?g7^0&IGNTA0RkE?kc-06ox)vic@j28{7vc2y)$Pi6Fidc67fNV?L7I*rZcCx5sOHR||L8FN?< zASHnwI@hOJNUaPaZ71T--p$ENJ`52DS97RR;19zM?Bxi?Y3qfwFW0c9MTizbdGo2V5bjpUSli|YkOqa zEs7jQj4S?Ac^-auYdly@l*sB$n8{-r5^AyzbA78Twg%`l*&)&8tFt*$Ee;fBBY}Du z1TnnR!-%9Bx4KK+YJn^=;_Of+A^{YNfJiOT1={cSLLu*!~gZH$F)HIMq>3y-zF~;q~HjuFK z7a~J(i5||awy=$!K=30}%|>UI@z;jEyb2eK<_2u8iyWNx1}(ion$|PeoMTMEIqJei zzpdAAb2b0=J>lt`>4V!8iS@!vW+b7`PJcd!CyI~5YgI4Bs^ynwoWaVTrhc-F8;+x) z;bD2;BSC|HWx2HQ$}EsqCOUbVb6GHd*RSBrFN#yO9#JVm(L z0m5J!koBEVy+Dum+#CCLsh5oHyqqEjtzhjP@)#Q+4cFg1MgBgS#S>WL`fyFo5Bd~I zO?A^73!sXT^)dD7rfTRqH>FP|H3(CU5?$xAwk4=a-AGr)U~Tr4gCbWQid^ZT?Vs6( zhUo(dY4hAA8RjAd{%5t^nhZDZ{e6Da=;xRHzkhz;Ltx$bpHS}W+{hN=NvL(~*m(x5 zeeEnML_FOy8-a=Iwh4d|eqj|m8BwO&3XrQ~U)5N)fQF{@EP0>(N-W(iL6cm}RF3pp zA19$^aFH;Lb-O+oKICiHT+FJYNeqrK4PDKUwljwfes5aUovl zhrG7)ZgB@-0fI)aOvPNvOgBzPlWKL-*((PT1gn$>w&F&Mc$Jywv`M@SpQOnK2+3pJ z{zjgR)o__d81D+aZ`<>CVtDP^Q70G!U&)O4$`KZ8zJB9sQYQF&Z0Bv+!m0IFFHaQJ z{giQ&isf!NY492tkPxRK!2xjSAU8j@D&{=Mum?+L)uZcma|qd{pDAc(rA!>a!sg=f>Lq`c#QVtEq>Qw8%GxP8+gEqSK zCT`u9cEiI;^5a<_*hi>5|OTT8nLo{;}B`Aw3Efl_g|&dQeiPv}YU1A&W1NdRt+PORA+fy}uQ=DLKwj zt*(Cu5ZFj4nT7H9SiLr-*Z-Km8)5eWfkR=y=Xw^E%nBKmRl$o+^F3|nE{foZ5LRyQ zL;S-3+2)<>+`>B^^i7$-15u~KBDG>=CYcz{PXV~*m&zc&*CRk!rP-^?P~APh63S^a z^}g_Ob7xq{F;o;~9KZ6Sv#UgA#YnC|-7imwL3~W)u}bZ*9Z+=Hjp0Z1E_WGd;A|-m}i35>bT;VZjmgEl45sDf#3O zgitLgwbh2d8BRMpEW(z{Qo0#yehGfZgshc&*pCKAe<;IpSd05~GZWATJJ=}~hhM5@ zn~H^Pru+2Jl-P!yi4rktCaawD866PO{f1r1MR*2Tv!?;RF^KdnGE0FnJwK48=*~lJ zkJbD9c>jS zmxOiHf%uqXW-79GHF|FM4;RHnWJg#IwgVt{Zmu;*ylWw^KfQBqLqQ*AK@4${Bit z2_haRBL&1E_rBWCV}WJPYOj4TX9Gm3%i8a3(#oLtOdj43ViMP>evCKiV>Dg6F@vK& z8{`zDmn_%BQ>_cYwJ>({mk) za*$thNq0X1A#DS!7@`J%AcT&M&Zmf~wRUBo=-3_Vzpt86_849Xq~vti!Sdo;*Hf48 zSfu9XnG&O27Kc-WTArqX^fvydE6LPFXBq2(*zim{F2}5P`33RWcT@nZhcE|)qD-)OlhAH6ES+;% z@4~gfifQTcP^1&A(SA+;LXd~&qge{l&YUYQ8hkC_XiW6?=5u2}^hnpK(S3sW233Bw z9{Z+Pz%2+_{jrNL*1)=+vKp^W8O&53yY0Y6MD`y9#o2~UVE|V@REp3oVK;_M&l0D{ zGfcVrQ}Tly*+jJ6D-Ish`!1K{koJ;b&cufZuX#-JyogSX0dJgHMTn{M_cSbC!X*G^ zF#KJD2GjET>YF$~)prwdN|IhNARD*1DRn$!GApF&veD%Rm@3%|t)t?do`Su$_ywYZ(Bu7t)&C_O%vVSV(lC zv{@7(d(KIUE(k$JbXN)x|Adj_P@Q2xg==_Gd-q#1yD@`o4*qnS&0N-9KisCfkB_DT%xp)5Pp9Je+^!-t)+x?+ZvpKh(*ZIY$sW zR@|!4Mbh@5%|H!;0>=qBIbQ8i*a9zw>n@Ab_|4~#|F}W2G)iMxJGu6QbQHgTKmY-K zk$Z^*QXRTXgirLGOaV|>Hj)x@_$0xzSl*6=Lie)H)>jqT8Kty%tadtOb~|TPbDT{& z8V!uh(%Wx35_hRPlC?{3t!t$H#R}@Ftzl_sJ!p0X5Wf_iN-m3yDV}l$a5^m}SSOe5 zn7tFWjSS+Df<5jsnb42lY7G$_)AHmAyq1<1!!jh)5cEbJAph;W(H%$?kyh^Gq?TpR zR%$5RQtN!v@3?J@`Va0bKOv9}t!F8wevA2? zH3^gTpQ7V+zJ);(8irKS$(+UM-^RKOezbD3Gd8^b*B}2j4|6$vjvFPGt@&CA^#7P5 z@2D=0LOxL2$@C?E@%u#0d~fy#&Eowq>)kR zoYkt9)1bpLXfk{1kgG{lvKQrpdFQPw<`~^~82D(30Y$KTJ#6QAeaWq9Ew^tG3`af!|mp0BQ|QHjj85 zrK{3SR54IQ57ie^PQA_R(ZyNkep9gQOwKh`;p}!^t1d`0aaV`(*Z0?g5a~Veed590 z;dFX10Om#h-fYTve4L5F)+!RE2|VX2AG!IHJ6by+6AMJC&z17x^kJ8HgcEFHkCbWh zbW{qyg^ALa=g&*LGX4m5I~hw<{R+J?wd>jH|B8*ZQr-6a&(?XxF{Gg)0fVN-{xzICf<|mNF7&W72w4?$ll}APb$>0B}o4HCBgJ^jR2UA)jVDqf3 zN&dA3wGs?v?ajcGdyo;dvajluoKe$m3KTae!R^88Iz9?ju47>_t z&$d_Rrimu`PIn6VNVtD^c`H{$*uH1${LGEn@{FA%vF!W0DF2Eo#i`oYCMy$V0i)!i z{u#rn5`UJiCh{y?k5e`)v%3?|uj+?BTvYMiWhb$$^b5i4W9(^p^LNWSKn6<$+(w@g z=~tcOxu?XE7CLxk^J8L27<3aNDmf@VN%_&NZ|@5g9u(e9o`#8lNY0j}Hs(k&Fl<4uyH0`f;)cQmvR9->^hV=rJ z9$ojE?k@D#TUKq?uKBvvhdOkbN48)d;7j!vdO)f^5K>@mTsHC;XP|WNE>jwf3brPI zEsrDS(wl3aWYeQqr=hKR%`{@casun@oz?UDcM3fO8lk(MK=CDN9oJ|^Q9s~m51W!u zm}HELN3N$*25!?@ZUeGBi_E&>c-hRS=22rjf43(v+fkNyK|!v*2!(e96s65|-+ly= z*L~SuhDq{C+y?F*a+CrODi$pEmL&K&=L){N6RlV|Xyx($FwsRSZy?0f82)~ZX?ho# z%5j~nssD=*&ptjzAq)Gyh2pL|6Py`(7IyJ@fmcSCU^hSKKBVFZ;G_(2Eg^_)RvtR65mCl^SgSH=#~sH039VWL>}2YfG7wJKlmJ@@O4{{~Y(68eImc<0C*Nfs!01gU{HeMLd=Sw7|+pgUt2T9>SZF6WM*B^h#Gy39kkRs(YX1(>=)B)evs! z`Zx;9LZYCW3ZvInqXK}K^_fm_up118G!2pyNR?JdfJ0sP2qWlOOUc``EP zUF_LAW(%f|umya6tbN6Or)7WV#ZLMD`o4z3{9Z{V7AdEq9(l}aocM>YcqJXD(AKAnlc=J?sVmsO*-1-8H~0Dck6FfdVL*e@q%Bl zOM^^DZl70Kj3&MP%m0SE$fj+tHbZ0F)rQ^a6y-+g7nRGCJS{8+zr8W(O144-e=n=s zE@fL`Jv#d{H^w}xnIvnWqPabrfb!PqTe|3x)_WT7xX?gsm;%jeX%q3~3YT2RvI~2k z3FulaK%0|}7P4XDdx!(2+x!UGX2UAw?5NR7-Gv%a2TD$sM#sLox1ODzTUXYBhIm)UN?!8M6WqO^YNo#^8S`K>?e z$3yixn8waH`uL6e__d0a#rVHo(UA77`5T7e{{+h-BaoKk>wSNRwyHf9(>&#y!K{RW ztm}KFG3a`$n05S-0u+C|H}?QSFL3GD_&pwHw+{H*;E8Dd;dX~2s%K|t2x0i&F>6zn zk_XLU*T(>-j(dc?@*u)Bw>1hu7R%#+M|! zA|1*65(sLAoV?eA$OXE{=T1#1G~=Ny^k6Wt7A`%l@$-7Z$ju1s3bqrYsoBk+?+1TZ z67!!LQ^~`Uu^ZaFO%azTAMdfN>GEi8yOM6HM9Qf*YhR7syi~T*{g5}I4|9=QOHl4E zEUv6x%Fe`WP5?MltgelqIY#9hl21?x&e-?KB}c8d!r$!N=I%45o}SpOXUf2K%>3#b zg)N0g!Jm*JVhHum;wr4GRGQ{IqlpZMe=fGDcq6OErNx(<`R<-+yxCXylHvZ3Dg@}O zb=4SoTD1g542#uQrsAQZv=$&rSy0w;`9NmhdegO!xti<(ZjJRt(F(J~-&;R9M*)Ph z4>=e5w95UJSgEi|pc!nM(17apaa_y!8!T+XTpttdpn_-5)4ERZd~|*XoL#72O}6v4 z1mCU-I)iUK;bQqTRQ~$k4$cSjdCc?{>?qknrq;Aehf`}#m+_CROK+m=_O+T^(wAC; z9FGu}LOR5K*cKQ?g?cx7Jk|#X*~nwau{>cwGC`vgXOs5)T8##iV!-(R?(n{YDQoe3 z*4>X8&tw~JJSTn0+ z0_J+?e3zuTiR8JhakcCNYZ{5n=2|tmn4YruGjh%;D@ptmPBZP|Q!Q^OA3P;b&-@kS zEw0Agb@ij~6HR7E;s#*^vYq}DWXDW1uJjdK3b#r286}OpI7TqcPkXKQ zfX(DnMC(nSA}g|MZ}diUo(!Bb2Y_a{I9dy()~D#*0we!ywxQX_-7~t={Uyiw{a}P` z`?h)j!|v=8+bOa#A@Rmw_c-x*q?qLHgmi61-MoMz!rK^gXie~r>jEevv>ju2Z+nRq z(B8D4kKI28$yD_WK>pLXP!9-!NLKF$J>Gf5?n%h}tol5Qg|!ya(rbl%zZoXRp+EBN zT(a@}e31W>-9+OA#^Ceq=GD@@ZCK3R`=IxA?urpe26>Eji3nx`?|Y18o~~*&}#ie41i# z`TKRtg}e)7pE!h$^Aehl^c9Bb4E)3#A$Tn6-@7#@wd-|$b^qNSbCNhsEC1;U&F(Za z|0!_1s_iq&(418$`P}OV`6xN(?F;c1**N|>2yad81XB;WXPrmCKBbjBHveu1_*!8K9Fa0Er_F~Qe^A4t@TLq5O|wtI}&=6tL!e+%86P%ehZVQO}@NuVd2_d>J;(pl8df}N$$(N zVTLmEonK1l*;8N47f8wVzMY=^J9TYU!K84Rc-c;>f&eCU2pZjeY(CO$^@notIA94y z`Gp(Tt9%2Uz40Fbvw8RT6 zL<{t`eaPRkP5X8DG`svxdIvMguL0m$CjHCk#8f_sz3>Zv=ftTz#=y%0;p=nVEwsIJ z&Go=WQdhSma#6X0SA;YEfIP_2`8IOnWq{S>pY z_&J$QBmJNYwu+I4dVI*kNQs><2WNp>OTdmvs$wm$D;s)^nMdNf>dECH+Aq(1%2B#7 z!n^&sf9|EP@NIlJ6)`9B9=e+|HIiYG)?iU&e5mig>Q@A2H#f?WcX9Kmr0Dtn zuci$t8$yGR4c#q*ZnNl?a|7fi4WK7xF-TpBgF@lL%D^@MDe&EDSh&9nHGIJ-{@whF zpYCx2oXJ0}pXcym^!S7o%SCpl!Sy!%d`gnZOpdQ6f`Wb)g+-c(dbEi(XzQA3Yp_sM z*mdEDe~h-J`5d#Zr14+?k>l^}%z+^jzR5requ{$qL%b?(v!gzKYQ2gnuA~lR9gBVE zk?`J)+k`j@A`b44DT!`90t4d5l!WI)z_T{LB7hwAtKI-}J|>9r+K%Qsjj1d==03-{ zuBL?>PbqCPsqOu7$vJ^4(;>rChv&5+ri>+f4u(Q80-gYLNy`;MSHEa!&s(Cro2jK&QRmO2mknH;rD~kx@N&8GV z=T5Diok8P@RedRe(MeJP^WA3lcV0xqr6#O1S!yff1inuXCwi^&nAReRcrsrLRf&9| zYhZ*o^Tn(I_$eNhs0u%TF}nFIbtToeTdDO!v64H$ud?sz%bFf?BL0yF&voK%HALGP zKNW8deqcm9hExTS5;n%wv97;DS*vlTa$le5_ddCy7s6Lf`>1;Y@cru>52@6#1fGuL z;Z4jw7Fte_B>E$UyVd1FEWmG-S&7w5JO)G2{MlGC5E*@Ew;8)_RNVZse7$rDp`*vv zZqggmPkqHMBF$lixc8A34J?F_&KJ-5tIYaPHFR<2B^@6$X6Qkhx z+(#du_tiglp5Zo0y?;G?;k0#A%8phVkN?!&Ms+43S!nk5cvB;=`khS0KE!P~5BK((%a`mVf>>6ttgCXc?$Fj#OUt!$G05oq$ zobMZtPK8wW+*GYPq$;#4@{M?(n1GbyFx_*LBqR6hTuD3-o_xlu9-qX~plIfS`tv>? znF^^*$oGLaI}`MeyB~kzSc6uLu6QAjG>i5V`C%;)@%n~xgF8Q`a-jY0kCkQA`ebEY z_GJ9&?1=?DA})^1sxu7@i_}~7YozO;GF_S}e}Q0_E#|2<#`@Ca&&+O^guM!ifv(p|tZA8c84pS{#)X#5`@(iWK7K!+{yucl~y8c!o%Q zw4LbFIH%4P)KzGVv%cB@w5+F5!G}bqC>SFNB>R9#WzYYi?8~E}ZsWI~N{f`Gg=9-A zvS#0x2-(TLD+Xf>4MtgpN~Mw|YxeBB!I-g+ij2r^1~Z}>lVwcGzUKXoo}T4*e&>Ck z_nhx>{K>t1mg{p}*G=1u=4p^z(EdJ+wswF$;trn45ob}p=jzlrs4vpsznd4b_U%Dr zIGeE|<&?bdGHwZ=Mmz4r-N({t?B8zQY}W2{I(*l34L}?2+1y(HcBfT@Zv6NBjN~oO zrK0rCHny`YM_<1Q0(4#9KHvI)td1B55E_f%DvoKFHjVu&*XVG`O8X1UUql>>nAd>! zzT5iu&jm-XthG1Z5c+h}ejFv>GSL7$Ez#uq!Q;ncvxm)V3+R{AgE>L*RFqg9_t#rt zbS#r#R>I32F*xerD}^7=kKw!1nS1krsp1G8#MPPFz{g;k9R+gVQS3T(&T$Cl^pWiTtXMBk6}UK?Wr zfGX!=`M`j)HvW=;8(??o<1q#FdSeBB+3J&uyfSQ3g12vnfVHzm)*wS-MVb06m%SRJB{}K==9@v&=Py?~#4w|8pZ*4u8(bAM0LSbCj3>%f!NB$A z+1_HgMCIu-fMs%iCf8VS8R8efZNl8sIQos`+4p%W3?RB*2Mh=G-`|xUcq!?sKMgxf zx67vVh5tmItTtZ1D-w?|7@$8jp}sZNci!@*4`LFr#^`N)63`{wY{EJ+Hh#JmeK;O^ zqz4IAi~S2Q`&G5RWvKf4uu0!FU2I5X3~alw{<4 z>!Mq}mE)6mx3pt%xhuBpDn1VTS3^p!*-#+ z-*g2&4vck?4+}Q$Kcn8i-h13Ch7KIeGWO>vs+Q>_ht3roT$~wh@Y@=ao&qMO-h{-f z2<7)r!W^f#(w7slj#l8m0M!n`?*Q}k=vO9S9u-n0c~|RbATGMGufyRcupvow=TF-W zx-8Ofc|cblb`7meRRc@_FHXD9FF$YC7npzi5nW5CI#d5O4f43=;zEW1bzI0MDCxu! zFu+`zd!MrbxIeQ#XKX&*$?{U-E!(~(1!UFRqeeA9R$moY4_>=H#@O-dn+$fYv`h*2 z831j@6!o@h=pKY0xyQ2ayp`Mq=G6&kg8pvcQMhME+Z$kf0ljgWSGeOPp^kIRaNab= zF{h#p)ieFq#1H`6=y;pTz7H_+gua&nd7t(C>*x1mMaOXhpL#^^YB6a}Y^~XS@c(g$ zTcye%(*0Zi{2|{12p6NH`G4nTmw+S!%@ zr=hWJEoDbsU|JW#Xr+sgmo<)BwDm&9+WA#Y$`j+ya|cyS zsBF$Fyf=3u#miP&w%F40L#dbc1}Hpf360#ySp+b8&j%)!dbuS_91>3RPJF7sOF2RG z>P0t}_nWMvnejpFt;B4=GZ6)OeQzJ#($e|Dk8OaYeFvHARO|WTl~c2t!W{nX(=H*6 z{iiRPKD9n;IXV@;S+7^!Qu2N&(W>MG@9@Ld<#lR)O_J);i2H^?Mh5Kr?z^OnK6u`Jp zHM-oehYmpD(R($x<2fflcpq}y1x}A6-HntXW^nWc#}eoESY|-_Wm!h;4~owl8tm7~7Qi$iGmGw72{?PUUeAnslpxFf zN`B(5>l;Y12-}qj*vA|!&@(nT_76GvCsjo%>76@HuWbs!e4xJ?Ts!%FAHdc2Fu(>i z{XWpp^alAzCl)*eC=I1mywgoyTx8GvfjyUF1)hs#d-RkRD74!!2Px>Rnfk?mY*?6k z=xIXE>H!kZH!bthO1xo2$c$khiSM?{W3qte#f~TFI?uxfBXYRqDcgze12p;1Kd9hN>N_c2J8* zchaG3Si{b(R+GhX_0fe`70S+PzADnP1x(B1X1u4d$8%H+qi^E52lra^R(w_FkDF(u zW1k48I_%qTK5;iB^}v!t12vU%@kj2(9xs~~*N`!|jqe!jy2M!3$luMn#(wP+>j3@% zdUP5a?0E?Jnl^%Telcg?TnQB3e1;ImPpSZOfQbYQ(q<#Ez zS!ih|iE(&IT=LooYVZp9GP5KQFbZy-zUazHh|uy+%%tR#T3B(+tK1h5HPIHGoniCWR%?s`$y(E$Ogk6)pmRl zcgP+fAI2rzYhnEE`$xRboS)VETL;=+Mc*-hyLe0P4HGiLNCaoS<`vc>x1l#V=0;+@i!V->bi8 z{A1?uYxwZ0iw-lEj#a?1Wi;R6(L4F?=dOUdx2U#yhVyo3+)&9TgU&tMehP_@BDlNe z?i==pI$y!5>tu!?ic*~IjsoTwIiM(MX{AV2*PgBa&aod1-p<7*^IMovp(!B8kUDr< zmneLr;%M}O-}1(0kdrz?&2v;RFNZC0-{xxLu^ngqLF=iNWDgmeEmPiI0q^v#7N-hv zd^VxCU*YK%U$T{NyhFvDQBBmwg-?JaVX{CAWfD)HlRi4(xorJ|m@XYczUiA5@W4t) zLAYD{(?AKmH(Z*^Vw8@cL{^T_N6n)2QM049v!qbl?}ekQY4)hKPneq3*yytlH~#*a zrEyVcf#`1tmY$tJjC3`gh~lK>iRjZ?F*3G1{@aUzsv=LX8TdV^=GJ7k*Awk}Uq^j9 zqdxk;6slZ%T)WS{Oww4itzOc5OOcAs4gGSfavyF0oW?OH4IC&!KeRz+B)WpjDoL7M zlib9OFuMGsJnP6PRT-RNYYWwCdCml}zoG0BGPshI-`6@(<3Q@F%orsW!AWvSBnc~y z`471g%R6jq>gC+bm~v3=EWX&9ddqM>7*J{fG9*Q@n_Vve$oIVI@U0})(_edkJ&mUEr^qbzkmxz+~lt~p+q1JjbR`be(ZjN^ryGbkM7fuAW z7=TcTBT_4@_RL`J=1&jv_bpNwnt)NW(CCA>ju^kWXkq*1W?32I8ii2pG4PNWuyCGR z4W3*svH0v2yILA}78ht}V=SlIyAQO8prWz%+LDCH_d%U+u*sF@DND6Io{_ihw>Y~^M$ zl{7b)92wx{Buf^h-E-7i)Z6&Ek{l3i7pQ#&wK|J-a^nfxs*RCBc+pHZ>UVDnoBKGF z1$U#Kb&$V)8Gb+)W1};d94`*oR6F`V zpyD2y~RlpHw;k0{N_LFlXGJ(9AxU2#mzcsIJ}m(Dzck zwT2GfTN0HE^LHhPhekbU#X7q;w?DDKKO@Z^@SG&v9B^XsG;nYxV%#Y{lpGw!yNlQr-8xwOl%P;U8ceYmIy!yKI>s67iIpE)>@B6?XZKitemVS zh-T40ySuxI4pHR#u&QI^%|Rlx`vRWK5LJQWVDBQ7QmdVjq4^D&#>&YFLhqMxXXu@^OWqhk{8*0^1JjUWn zc)}cEbTzfmGS8_35Fg|NF`F)Nt9^J`7r3lZ8UrW4I_M;tbxt(T^so(&aWPDsN!+K9 zJ42Vyt{J6?k)2)&vKVEZOeEflsI1vI9Dyz=U%F8#gEd~DVS-cs~*~l|qS$xw23)Cmdb%2{!{gbk`uVb5oi!~Go z+#C0BH}hl)N6pBp`eH);)}jy@6>)4ipON5{WKwgLa%O~MA1NhA&9JMkqPixHcd#S~ z|GWYU_O6+ft{U2HJyj$T(DEo?bQL#?@&m_K?07YyMUF?*H#%?qPN zN?O&93xnIDa7bz}u%^HjcXPJdh-1!wlu=s#?SgXdGQm99GvivcG<7_Qa9OndO6iVO zc;az0P{LpZPn;1XJbt1$l$7i>%mLEPTCSJ$!5O42oXl39f7;}uxftebi_s&u6U#(H z5ox{kSjJHp=bTo0Y;t?Fv@`o6f97>$;CE;gXbZwjBX3p>X;$vaLA6tl9mIZbWV6->bZz*WHED~M%@sE%L zw|xJ|9V~-73yiFd4$JS&20os2Ka^fQ8w{c2DQ?SB9$$SR<)?mA!s(?Q@Q5{#eaye? zN18mO7nHU~WHwdg4eT!){LQkvU}{TOrs4H_?<8GweaByvDB3F%w&Bt~}`$x(>=UU1hzd-tfyIBteRT-=jH$1gqs{JVuu~f|+aR2s|#UuLF{0 zmK;`5CjgSmTwDgeV&&3Gwpaq5HydK->*BT9TOjaVM36ESElHFbhhcwB6PCBm$_ zVsgP8a_2;Z%w~n59U{y=Cof~wj_XSdlw?`?zg~X_hG$tn{XS(4hspMBuwgG{sYbSa-Pru%25soC-U<8O>IzjQe2vR0t}R|IzpZ^-@>eKq6B=xU=dXGVKPtNWLXo#QwZ@nD*UN1+>SP*{N@w7tVct#;>++&xK z0td}cZOJ3^aVJy0jwx8MZ-D2}!R08nK}4j0hbr7!ay3 zw{YM=e_*P9q-sX~P?@9iew|hgzmW9)Jar@?zz1qQmj4q4iubn8d_t=qSQe$A4aIIj znqq<`V?;;pfr^8_)RQ(Xd8@@-sj=j_SizaAXEi4l7_49NSqieAl998Rr$Ls%FAw<<3ZZf+5YiB>?!o%v3y z4USTXsc9HoVZW&iuT|R*h>-$3eGsmX#DI|7sc~-Zl8~@!#2mYXQrPDHps5CVO79Qf z&iDdnImp86U-f9s(4`i)%*ryEZjb$c_j~Jm>#O?T*4O-K#y4Pn<2q71lJ7ji{(Ry$ zSR-Wy8XAVIjLJ$IAu;Ag#bPj874r=W(HjnK5=&4}uKgAqqvd_mH!-1jVLS;gsxxb; zL&&;dE0!RK*(n6Y&)3mZLGY_DGNiHI~W| zu5y)ckoAlA?fu)uBKvb-H*pVX5LuPd*b4Dbq(rJ@j!%a+&;(`DYy0F!ep=pXp8$F7-|ehB^j{^%Q#G zUAx^?nb8+}Io~T?2RjDQ(2W+6pc5XE1jhh}sb<-B_EjClr$kwcP7qI&S#S>18)CwR zEz76N*;txTs#~jA0)aRD!;EZpA$GZAtO$i*j6XWLBJ z&X-ugPU;ONjCUki!_#3@^5&MESTs-_b6`hi`?v83#>$JguRUq1oL@O?yEFNOT+aWk z>4LZfR5b^7zsAh9ZG>8PdV^9sGQy`+M;`iW-RNY`5&J*OFChMA0P*+L2Te2itE(OD z6Gu{P9oyJj=_CkwXcpi$WL$MT-M8OTZt_*s^NHHJa_r*KtDL_*hUjVS1U>jVOZhd~3$#VA}CK@g{A`ZrS*P z-fL#msGP;5>mq&=b9H^IzMj#;F^6l#E|BZSV#xVnoqLhu8;Nh-Sw^F8ZOUxDb2<^c0|>M?k4Yc@o1`GuO=tgrpVV&`m;yU(qCf8M9 zAa-&ed3BR|=AMv|sbgq#d1E=kHbx8jw2VY7UdjJlQ6qz*S3j2yCbl3EZC(roH;d^8Qf8)?xShk zN7NyO8AXwO;PjOxuQ4s=c09#DqH-Xftn%m6Oawo~0#=~p8_@%j_m}$Xt2XQoRjZ3T z*gdWjv~kqt^8iPpLgte$3L?<&Fv@z_p!<;hi$}~xbgw%nmJ?5MkQNT_Dq%3QyORoz zIVwQ=QZ&HfB@&Q-8A9y}c8KY*rRJ(ICK~+ecq#E7K|o)@ZuEL$oygTx&sCIsZ0cjI z+R6Ej?O_9^so4|`hV;#7qu}=dzR4i@4>tCxJxj8>`~PvZ%?`g5g_vTRp=igUn%Px_ z(sJTW5IF`BQW0UIXlw|BZw3Je7~NL6TT|-j+#n}o%XDAI7*tr|7<#o_d%i+A+pRdi)~~XlenA&vKcN)LQ^m(0jWx9&N`@y*ZCA z2ds_9@&j=clG;`0(uAP1>EvapSO#GLVximWCCwb416*;0*o6GvI^3{(cOc~CE#LVs zJ{hH+PTFO`@QK5VpHL{!_VS~}x8--z@%Tz8*O==y=P@5LuM|K|`iqaa2bF67^40nD zpSuondk$QPT6`<>nPx=|1&ZA}-J^tr5q=;!#mHzYoUv6N;Z?mIBg!(jzuN* zL@CiDNltAs5TGUeCcJQr9RfUQyChdkwyZ4p*qD@(Q)owh@|@x*Rj{-L+opz|(cauuZAB?OCb zdVqD)0tIK6kT2?Y_nEEmtInOh=45gGOI9LZ!AK!#l}sc+92dal<>3y-w$9ec{v^RO zZ2_LWnz1!_007*xELuB76#$G8h4!yrYMi^`LzL4HBaB!exHrFBSU|U53j#EzgN7u{+C|)WF#S{Mqy@j5rYSF!=BxKQoS9ibkk{QCPmzT7eWF*v+Tv9}?MI>2?x3kis$v#0hn_r3BcYJMn z4&v*w>yyRQ2%kLbOz4$ywOEm^WZtJd;T=sZx{g8=O93S}-vO)eiY;{m<-uAY0*6Pp z@31^6WtqiD+|Igm0cO%=Vjc-q?}$)3oaa5b)*uFHjS^PXnO|qHUS`uQpu&^BM=(g8 z_!TmA`cDA>A<#U}8$D^sLCZ7h$xB6R%%@tjcL|t(?aCy3bOK6hPxr-p6Ohi|NY1OhNhlQ?UA@l1B;}j-LJA7IU)vsU1O0qT4EcuG_OQ8nyTg^=9rk52*%tplv%q z41z-!g1LqBHZ3}0p`1y_3pOZol<`&Au1kHvm-6QF`v!wOlfcrNwQ2SuinlUJ+L4! zqmg#pY_t3rkO$B8C>(wNup^bHNvW_uzTc+AiD}o~oEYf&G_DzHEi~!(0~wk8tv_Q} zjq+@?h7rMGJ9a*nX48J>MVR_nm{^V4-1+-{0Qg1s)#IKM1t6KY>#sT){cnLzvsTI9 zUhU|axG*zSxF2tQjk1&A-aQKT?TSd4W?1o^XpQp5T(zdtL2dI~xBi{>*!N`YL!z1%LzXycN-`bd8ACz$cs{H74D6 z)-~|+jDe*0nEQZV68*qaS|8BZ{L$n`#=g$8r*70 zC8{FXWH`8yaR18@TfEyxZa(3ya%*>UWJUNuyWw07~?nD)Nkl>A+ zj5GAikOjCe#XMbaVmypci+T?8vR|B8l96-FvszBq>Fu7CwM7sKt1?pHoW~)nsjVJd0svz#<-%nuEv}l}^a%P@B zJFIBZ2c1X4-&iQO1#^LM+tAB;WWsEC(jgqmqlJ|A#OWvFn*0_6F zTh(HDBZh?4Ev2%ctak)cdxSmI+3p)r0;|$n;q?76GlQ7xl4D0rtZD1R4*97HBca2J zJS?Rq)It9a^hFn30lGabdo;{4)l^+4nn}5GnP{nse?ElKg6hYcbghwi-sw^ehB$=N zPuY_+N3L`gy?9!NlgJ*)84Kgc!Rkl9U=44L{X*Dzf1N)?JIOO5P#3 zjjK_w7^l;+HsIkm3HWdBITfueK6v$1nR)iIz~R6ixSQQ!1oG!o=VM7Ef7^zY?!&bZyK~z3vI-fPUnm!8wl{-;vMAD zN4Z)p!74@rs4u_PnRVO9r9HO6-7`YrFuW}~D9q$s*(!|PUJJTHLra0{^FkdspYye3 zK^=2opsoH@)TGX2s_lx`?yMr2$X11Luc+GbX_dF=SZyk_bu^=! zAI*J(@Mi;3apyx7<$~$y{n0;#C;E?H6{--vVr~T>Z7M#1)}Xb~LEb(X1L zfFWgoo#H=O0>EPG?p4b6zsrjM_i#v|dg1zkNWO=)5VNq2T4kok+?Yj$_G?Luvq(3j zjk32Yf23A6=hTbGripEq#Gn32R-f{UgCNCxWTHC-ZL7z8GY&-&=0{@AF-M=CoMoQY zbA9VqwV9Xk%>tkJMBsW(`cvjdC z>XW%}`y$>g(2jqSh!pdfb#!o)_W?Ls{7;n4k^HdY;{DQ-LSAnj;BBpy{l+%SkL41r zM7C-tM!q7>v8X8PW~E{^3VVXB6AWM^3aOJ9pjiV>4RN1$lu`zR{z`g)a`M+X7WjRR z|KCGQL5#5wflZKRm*|X5WA1^;bx zXVEp2#8<(rf@8kaksn~VHr|6;Oo4ee>y9Pp5EftGaY=&=B=6G-@yWEUaLfs-9*1Ii z$k>>R(wFL~T(hgnBwz%x$uMt&a((S&Vx=QiyOG8&EAG8vx7vc%f-h6`vL}Kor;J16Zn^w)W)u$F$=QMwz2@V5k-G zyTtDflI z-OdyFl4I;=eIt3%|L7ECTB#Dga z1MCllwknQWMf%l%gT1MB!Fhvd2i14Bm8X??H79k$n%;;%Rkm4|8}*-gUB9MP?Ci}) z427iN52VVtwk3at!HYTTg=2!S|r&s7_9bMh2Fg)QkZ12WZTn1p|%$DeWf&7b+l>8ICzrc8U|r9k?4iQEn|7Q&jdaznO1rLZVAu7hp^0FFR5 zQpBRS@EzWA*e-3Z+89JQq>8~n&fbD!Yrs}kOIl!z;W(_goEYt=$7}{`?0#>~6+ik8 z&vsU@l!LMj9CSRDUQ?4h;**BR<%@Q`#7b~j1_6vGy1-{v+<=yk;M;Jm0MZcrOpC1A zr3S1%`O5IO3p$J+^v~T9ko942eUpCi#PV>*W~c z&uFNF`NqQPqzL-0EVTM6GQnvnu-V2DuV4%ePA=-HiBGn&CnkyIfpa$FI0VZ;n$;Dg z)jWxGt6qtsX)FS~@@OizBZgpkjr(jQzPRoPsU#2nXZJ$omhhHkw1~ck9h?E&!piJ2 zwXIGyvN0B%wgtMb<~!=JCdt@rtj495O{?qI=w9KNiQ*=2)O_qe<|cOM_a;&Y|33oF zDi64v0_L{ZUgX#1C^L`MYOD~awikf56-va*`tICagllJ(W$LYtHkOOGP87?kRU8H- z%++kdys3oE+_@j!9hq8(e=0p!QZN>MS+qn@uiWwOe<5un6nEy`#bNhTb&I9qy=+FH z_>TB4a7GFesEEQ$b>9`sl{Q1m7Oq7}o;n(~Bg~P+J80HjTOMeqlS36AUeFuYk{b=a zGI1{A6Q77L$g_K6)mGWU3F(_Yonmg=ffx7`%jgh0W6!X)G4zKMFb}JxVEHbrWN04$NSRg9EDOJXSs?DXX8FZ$O~S5^Pp(0m9NmUIm45Y zb4Lma2DCX(6d(GtaLK!4bC|LA)_$d%fHn(z_4{i8_?WK^dZ2KM?&y*(vBgD4n`-s9 z{@t3t1JJ7P7*4MO5(P}HJOtP9W4R(cL-Pk9E=&(-yb&5<6C~POEJeOEpg8@uftb~mK z@cKAK5Vp3(x&zvJYZo^xfyr2HO0Ek$*As?&mo&6kaZxAy6UzwyPcOD@$@^GMKK{{# z!>uNGtF4x3APOyQ2VZgbVk<`g9a))?2|yu;_3|H=by;~+s9*~z3g_Mo`wqIz3v#_K zm-v;$lxmvhEN9_6f!S;%_iDi(4t@!^63b8ufS+{m2|s_4pbZcKZZl}gpU<(&73KaUnlh>yRMdU+iQuM-LX(w9ArS_m8J?V z7fIRiP6xrSq4|YB`dKIs4nv{QR0w`{Eyxb1*_s99M+FZBe3VV*{ybp0<@i?eXh&eQ z73#>xx+RP*w8V-V6mGGjx0n;!ZMpS!j-~o&v~uDb8#|e=N3B(cC1CIkC0hNlF&C(} z5iU6aWMOm&jC(=zg=DMumS^;orG(Y3z5|jg{gCGoy|+H@szX-pJ7<&=&pgxGIUBE) z=IoJ}k)2h5hO3f`Do2|A6~EC4m)}%&GO`I)Ldp#SVNIBLe7JD9Kx4pWr78Yvd*pM7vJUekgr zcazX%|B-OHzmQsumW?*gpPcrzPx~|y0egfQeT5-nJkyd;8^z8m?a`L4VZi$VPGu4n z*%eJzIM`~Ww|+cJblwH#9Bf^1Igj4X3F{Q(kdr#$(6BXf2V`sUWptWVepSS*b?#U+ z%5;(_BNjEfpbngI?+^&yY7|0iYd8ccE8N)oTa^mQ%NrE4L;UUBv-}%Eww<^~q7-i~ zP43(|#xW+_kR5V(JYrqD;Ss>&wh%A%N#`8|Ezw}bzz*Q6Uu{ZO0e03fv{Txc;f2KK|&M<|}C`0Y{9BEa(irn=G(fJZWI>9zYjP`g()_?bTb zQyW3guKjj<^qJ8AUb~-xc^>boQ|Z;aSQ^?n_HKkL{%KjaMC?)=vq*|%b~2iJJB@u> z<*UC2XFmTrv$jbjDT%AaLF_3xK|l8s<0%(S>4-86_&BHIolt47X^{^ z^46}_owTnMjy$u&J`xKHA~54Ya8iwJrBPYlKrG@L%(XQhiVmDvi=^>AKd_tjNHu5o zBsOFB;}>d$Ta#uZVk?)J14@|U_h@YjMTB7&k4TQxSY?MJymmF@E{{U4%%1HgNvQEb zvnxn-=D46I_r+4&P;hUHu5Uwk!T`B)np; zH0q1`J>LOF!Y9506h?!8QyBN&onhajFbc%@Q)<)3fYTUX>QDxmD_m}d6 z7t4!mN#2>T7&*+bDPSu9NqP0!*c8>Y=u**z&jQc1EP@|2-oT(wKZPCBe}q-*4=RIu z>LmBINqF>;UtWtoO>!^mzHvHP?L^WwM1ti{1x>&9mYCQPU*80C_ZB3v#qSYU_1KiO_+qvm^&^p870|~m}DkL zU^YTga$}$k`P5E6Qa`xcqhtiLFL)k(&tLr-Y?%TOxRZZvqnI`?K;MnixOZ=F|B71p zZJOOm#^u2nm7j07LCR8n$A`~LdAZQLiAtcGNFvr9@4E8aJe1Dyk>0DQIDc1E{}%{C z@{5q}SqNf0&B7um+_8JE4QA0G$}DLHL^~L!jHll<0#Tuy5iFzL`vzC14HsC=^!Mh1 zW+AmQQRPrGoLO_;VBYfjEWxVBph=u@!*oL-#xAl0W?1giJ%F)YIcMzGA3f*+U&mo8 zFYMUK$y|bdOpZ0GgFdW`%aU1r*i`}rca?cNa9is-G{^+O3^gg_4%R6xsBx*luv&Uy zpr`ZWxqF83P+`}VS;<@W9?8_fIibaR;_B#TEl-z2U)kx%j_28j?BgE)q0DyW5bOL3 zlyrJ-r1;0wDa+4oe09ZA!_2Z5^?3bY4yITuz#tt>6M&{i@BAdFS)~?-t!%vFWe{O; zJj3+K#dd2N=qVIxSU|`G-#C38uVzjo~ucQvHb+H&sTT60WCI1d@jP5LZ1XW z48lRAa>PrENNDzIN`)@19}@}e*!9?Cqmv{hbfpYM_@XPx9MH(AkI8}6iM9HC_V&xf zA44t&23cPp=3mbARGeG)d+Qv&9&DMCR5Deq{>{|7n8!;iUS;Y_{i{&+$giyGlZTeg z(pcfN$6c3JqR`z=d~zRG>o8nkA`ZiB6v6!~78L;Xv)x;Osu=Qbs-ogv zFR8glRczOz8hL6bE*)LW?Xo|78eVh1`EfB&M%eXsfZfR6k>X`-a~;){ts9U4EO&1O zZ=yfAJMhLeQ`K*7W4amGdKIw+91LnWci1^4+=o^j1kQL8FtLe4XT!B6nO#^k(-z~F z8h{tKOtbp-E5$u#JsT-brpBt2i)JevE|*y;&WtwdC^mPHm?UB*Xq9?<(BN5U*xA1-^2$y8UMJbS@&Yar1&eUdBb8|lTW z-Ew!5KkuAtw-%0PkahX2t}^Sqg27o1tP`F>zk@X+jaoEQ^X=7jmTe937^Z+o?ld#; z_K!%DUEhapIRfHtmrq(E=jwLck+J!HcD%5&t7eTWUnvBl9xtk$KsQ;2PD; zf&^R9(q^~Ksbx)9WG3!j+zQQDajp69c&CA!WGIfqc`} z~1RC3C%;Pj5PQoR5dR+JLbg<(+qdOykKIz(aXa1lxLo0MOgk=NkBo^81#W0C2 z4lEYKstk%9-W?yA_D88`j_+(*s9h+Nd;Fz*#M+{zl(d@Oq6YNDLD5iUnd8+lTS5qc z5r9-GcS`r|E-CNW)~8B zD8%}76!bK}{l;GuvlFTL`G81yBCR7bGw6kOeAeZX%EsV!lu`N=!wJn3akgK-|6IHC zc5QVl%y$9Rr?}93XR>7>gwRErp5Nh856KQ$=-p`9dMglq*ZpCEk(`5iGq`o^Guj_> zJcL$-e*%L)xp(BR&qA<2fJ{}L*@qO&g|M;i`}cR>iPaO59W{!cJoxmd=SGmZ?Y^TN zakuX~>#O5ggiC%{s4v%3Uf!slJ{muIXgX;zA^ry}U5&##=T&L+T?IL136$+cb>CRz z&XOJ`FuOi;Zu_Ee5*ZuWz7(O8suD2yIHMI%zd$neTi*%{4uG?r{3$N=fDZiou#bIYsLq zJP9{@U3plxYTz!&I?II>oB6z1gPw@&I|<~qgG=v6?y4>acf_;s*2#86z7G!K^(BuZ z?NOZF2e5vNPU?4#8(+eSes84hTvfXSk-MSm<&@sFlt$SSIPKfTGW|yCF4Q$-K60gg z())I0U=P{5uVei+Bx9wTn~DmZ&uI9r@HJs;K7-;KrobH2M;cg}I^Q4ATK3@Ud$e(4 z#gQi(2lj7wc=PG-ws@MIX0X74va}~BtZ{FncRSp_ zvbj=rEGf_K4iwJ2jMtUG|7aNAkvywj1r_^NXTkm9=-TmRTioW|3^FS0rvdxBm8dl# zk_%$}I{L%AAF088!@WrI^zpves*Q0Fon|0*Z&1(3bSI#PYF=ejrH0Jir zniiq-S<~~~CQua)uGgr)&(C(D_QOdbr zD+)pN2&~6;zly<*U5-+B?_LrUd8n1)*&TYdqs}Oxv|Mrh$Hw>Ir(-22Obl6mmd_&ZB31r&S1pas$bFy ze@w08I!G>KiMpb^D9Tn8wpU({WM*~b1Gnq3F`t{y!xCF!6Gn@T$HHxSZ^tLdD~dl^ z9RVT9)j?}Nzc+FwXg{x(0|{P&Agb3cBvXRWVIOFo+f|5BgLf|$edTNp%iTl@%VXYT zhqy=(f_dyszOo#?^%{)^1&u1;>}8pUh9pNB(!SlX>{l73YK}eC=wozg8%G}OSt}1y zQQ1leKQ_eMigwu;z)f-@A3jjlNk~ zPygPmSCeZEaKHbtStsb5CG=~v%Gd|jb5Dkn9re{OvIslGy*((Rs5)bxUh_sOu=kyq z(3OXVr@26zRby&n%U#}%gP#axq79*qj3#%i|$T@mGN)1!5ESEZlL&&{M) z)NDK*Tii%HE}p1$Gks;NtuJT#Glkap)Y#T|BC;=%9Q$kA!n(Jw76SLkaANX~rFXkq zS_kgOZPCjiC1LG}Up|BxvLm6EQ~XSPmZU^l=_a&mV>=bjqy8b=S^Z`N?gw$ufVujv zsMVzNGTOA?8;MwrWohnxcemPsYdQ=4ZvXB}mQ}K%&2x9)iLyH1Z4iPwO>I2bqo%fG zHrhNt897E78Pa^c^Bp!lWt6$FFWhA5<@+x`ufKyxl|;Y|A5W{UPIyX9sN--AY2`d; zJe;2dRkVN4zTDbY@YwlXfZxwhUbRaE5Ailts#gu0XXB*h)g?Fk%U#wC*UlMyYM6FD zGA-`JF5U{Vte@&Q{drPuE4FGRF@#A~#8VA;?5}CWd}0+}HEX=(0oB)Fxc1lQvdoa? z6WZdT`f24_fY|?xjOY8A_Csm@Y)z|8=LY!1IKtL*S$oe}1>7QE-bqyP_PcuYnY*`W z(P0F{T+XJW*IbflbDGOx=A;PW*@i|o+QBpdmBb|<@>2-dJ!`pM*9@)G?(70o>AKQQ z_dG+nT-`fi6iefHDJ`(8!sCd>hHc_bEn zqAk2|n>pod*t;+`k+mNlhlkwwsu*JGzuv7tin`Af^Mvj4l{^@{)6NhsRTy`SS4c_KMD7<6AOZ z4jNA$xKP(3v$e=Cl$%?nw%p6SD{oC>i4zHH_TF`D_59BoY?v6f%L9urJK$v8* z*O(W&foq1gL9pU(W%F9Z7`yZzPv25uZ(xRljN`zDY2XDkr>}w^QOi|xbx{?Q19GGe=ha6FoGY<~i`9GY!XH=63 z*EKw&A}9(fQj`+QEeKKsH1uE@WkxBZw9rcgR5}qPKwuCNr9}s&NVAL}HH1hY5IQ74 zLV}|LA@q_6p@$Yi@Vme|@A}^7p6C5DD{IXUL(X;1*?XUT&UL6?Mxk>=yIXX?jIxdM zgcAu302&xHAjBI5Pb^6Y4&KE*FW;%*Bu(P&zJ`#2dC%QN>3ZK32{U$ow>o(e9F$rt z3srLz42b@{6(yClZ;p5#gQql4a#P_{MlrdN_JkgH@-OSq&SS2Mz!2 z8EURe*}d9w?A2Dh1sP6Vwp2Eojxty&XG6m(%DY8ZoullQA{Qwm82D10Oxt9M{-nQL zCs33FZ7ASkSiqjo1#uJ#YP-5E9Gvp63Yb`~W4yP*xWJPhy zA$r}ol=IIm4|j8~G)rE4n=_A{%P>fc89CMW*r9VS`j?!Ttvss}*WK~?tN8AAloJf% zPHtVJ4{=WDjvIZb`1M%peYX|DB~!~$WY%$e)5S)2YJ`tWP@8Sy&jI+*{Q-Dft4vz^ z<18n`+3b;rf5Qw3{i!>kqhW`V?WM7q?+(QICvIJObONO=*}_fc_M}FD2L0p9n%|MG zjy+{%Irxm+hvnVnW`EK>Ak|l| z{3uhe`G|Z~=cP4|HdFf$Z#3-#^MeQzXQgo~;;q7FFhf#>TUNVuZnw!Fj#9m+C>{A`aJsq`yd_5}Y$fOB?Kpjr~P%I<;k8O~%sQ3Q{ z^{=wbj@LZ607^<&Jgaq@D4>YhlBOh}?iCehGnmeJ?3JG#IF%uM&2{dnu`)9snL{%& zMv?dk8##P8J%v8zs-9)&PT{*npHswJMA$egPWLBAgx>_45ECCKxR!7l85-Q)D?~_6 zdb%h&=}#6E;S(_T*JQn~k4%7@3h5`E#JxxpTSqj(3Xap3Pdw(PMmqc|IVUg_nvcU0&R-19BwY`|1~EV12}4^=ydzf{mvReaI<0ReVMK zP8x`?q>qljSu~Ui%2o)7azVrR8RZV?yY-%tN2IX+l=4i*UQXsy(Iho#tUqL;^ES+3wn)Yu5YWMEJ0E?8^7n+PTTj9C6zv@Ep~>?t%~bWLCaMxJqWzx76Rgxp zeSknLueu@5b~5H^W_x4G^p`tW$u=hiljm4EHI6lxObe4rXE;RoFo1lRYpeyATz~^S zBx7Rii@i0B+>>Ap>aU%+g)Y2v(mVIKCA;Fc1Kd z8tDSbdD!KJcT17MWpZK05>bov$mPQ@m|%cQBD5g-3{z0k-SI$KPb0=2nboqJ2-EHL zz^C}vr_$r9!B`dP<*xgUJ*Dc&BN>H;%#<No^4D7cNikoNk9P1%&XEqW zFGTL3^(X7zN~N_`09U41_lYf5b&1&BPr*xgr*nEwERiB#YRD$455)o0n3s?F7KMt4ZVHz+ z-e_t=hHt?uc^yi>X(VQ+Jd;tq9~Ou9q7d70vdQJL>Q0!{okLFzU2&dIuU{Z|_4A=N z{>NESbt{1nNc_sScHzk~dUEp!Y4V_;)JkePfHf(EL3B|kNw_L6TpE?&5AuRakL;6X zL2z!X>rdSZ0_Nc!#sxBYTv%;M)S{h)+BG(=U!EjjH|yJ_jPfVbwHIDIOGC_sFvFW# z5l! z``#tbY+FTbz|3o3VW#r&|6IC2js8zC*fM7(+uJ+1B~@Y#8bUP@m7=uH!>D zePVxgUPg^3w%n+u-E$g!CEoghbm>jOl+U>&c-!Q-=HAr(+p?TE=d^Fn&HHgxPBb=a z72apA<@dSo7oZ3;+jph=ya~6+DNGs&o7(7OZf{P>f2U$FqYd@S=r2J+*8Soi!|)lk zgly)IYuA_jN!rYx!ZY}9710u?i0-lnTC5aL#y0(7Q?zOAgB2U}+9x6Ye(EN$OtyIK^BLz@<421J2E;shm zWAD+2o;ILTd%t@%mEKRf&A~AHfF*6w!edw(Zt^KXgJf$QxS0 zD8dj|d=J#7MqjO@ad{^SUfUY5%aSRuWb_V59hj@(eHy9vmc!_<^MDDB9JZq0ZR8ct zgMx|2z*Hn9en7Bf{c5RY*K;QiFIsl)GznUBA4PgtTW+`fG!2@y)+OvLygv05R#!1@ z0}0&uwAzVtC8)%SYTy<#=90o0Jbh5wD7&W>_(?Y;t0do|3WWC^sJ*D z|E6aJ_zOt_F+zz?JRT>Fw)^D&E&zd#$XhcdgDMhQt>?A^Z$C>#4zRKpholRJwt8FQ z^XK!r>7ri#L&+$RMjoQMqHIIo^*So9MfMZ1Z*kXv((?4X6Em^BNRsk|$c#G7XSp}q zqFo@wGT@G~Z6Y~9iDLtHU&>!uUf*N%A$ie8kW-Hgr1I@^r?XSrrBX5ss8KK!J(jzG z?00;Ep9Np->JPf)qs-IUz66_GgMK)7wfi@5<$3m#R$S};8n}06U=yw+CpBU5bo=x1 z%F)ySmQ1?G49D3HqdpCzOG(CabOH7cQQbdC9EEcPsw~G2p}?m_-MI5o^D)&qDw?h0 zr{I)H8v99Kc?T!lTcj<~Vp?%snHE%^z=!GML{8=e&40!8Hb>hmwuPMg%)~x1!oa&n zBaO0^Y7)5l4C@%rPs@?7)3hcvPso(=z(c3uxP(Nl@de+Wl|$~h+3kHz#kYQ>V-tXM zGPmgXLr3zShO<}xsaFkdh>OA+pV^qb%%7qCO&8Rx4!H`LSn1zsWQQV7+`}AYB?(`3 z6*#3>b^mB5F^stCTG?qWVXh>Hh6M!Iu;S{w(Gl{gCweKZJni3|n?hz$71|2YBlt0l z#q)=)YmK>`kQ9D{3tM8KWhcmT!35W~<|iVdxAGL?TI@>V@v@^y_dsfpzRJd|%sb2^ ziEz9&ivRlNz%6t5UThr1*N{XbHzt-@uBD7)i{9(7vAdX2yon4Rp4unrrAJgI-Qf&& z!*Jc!6TJ*hmtwUN-@7M&SWUiBeYOI_fnmA=Kh6@Kp`1K|dl`z-QAIdLZ{-5M(Xewr zD%9#>q^S+#mP0aq0S2*>L+cUY%|Q<}8I9WLtqVYlbk-m?B&O<|@ZSZY@ z!&A4EZ}Z8Zy4=i{Ld{fvzq6ML|G1Z(X8C|IN%Ue4 zpV3!D#^=s2Kuknv%WXUJt}YGI%KK63Ls1Bf({zk}Fw?aKqsg~8i{-YBTR$0+Q4Zq7 zZS@57=9!r|xacmeVnWTy({Fb^)$zzO8+Azb^(hJ#aTa1&+Ah)H`>Ik3x}%I$eOsD) zxH9IzLY)G&97&sB)i4dtU#8I!#m1JeY!@@HW zHW9KTO~|kaX#9$O9XB*R5RF_5lGXE)OWQ&A^Xi;oWw`lI^KDJ@qWG)5<}k&?)T449 zwU(|Ph)hwr&%j@_-}d*5M^5h9=2JVf!Pa;UI$izJav^JJdv&}|1zyq)SfBJ;0cJG4 zDZZy&3GE{5hdM_a&!=NrlD~`k#_pS1a@tod`Nshc@cE-aByI_R-+|}w@FPm18B~+{ zABk$UlNgre@jrhXZ>*u8Kl@uDia)l~bxjkiH1Pf)HZAVuP2;RM@y_lcq!mA>@iD-( zY1JU7GmkeKUJSx`{ER9|Rm(^R$uYk`B;DxJ&M($#!5XSw$IWHS!FM!Wd{Zcxee|y5 zq%-q)-KEwS18ivG@jtMk9q3P`!333BD~d-N#Pw2HIK)^4e)aUg<0&Of?kyeFBn+Z$ zENz~I^Q--9EueOpg|@*>Ba}KTgPyj9%Dcy~`i4hd#!JH^MaZ0r+JTVz!>l9{j5}M= zcQh!WkrlHJWwU;oIj>HaN9yn(q6FMu3sAYaS$!7qIZTB&xZce^N-j0*#KM=HmdnWK zE5IbsE?d@q*<8Zxu>D93YM>YvGvCT72D8zna@9`2+j-;QwCFA`FUoS7EE~HO z-ZU8(DH&n$Aag7b68vQ<5bSyOz=Jab`+|o*^S2m)zlj5yvfC+C<8lv}aI^eCbGf>i zjc?y;V@7892o0pPu)P=TO&9wnQAVDM%#n8Tz4BG>`Zg;4{}CwThEe&^ryiD17XGpA zf`%e8xtwKFvq9m`C|mCt$xh?mG3_4eWW2%HPZl~cHJ>Cc)6D>I#62i!h^DkrjS8|| zC-@~YsJV~@&O@b=;f#Jw*m$azB|*<;^Xe%_#rakdkXH^h@Hlj zjEOJv>`2{h`IB&VchZkyW!>npjdAI=&*PjcYgeqg8`IA8uNMB%6l*>C>o{*%BB`h# zYRjQ_Wl`S5oynb-c7JNsl_;0RYj@v?FeP1hU(<4L3Xa*mme$vcHDG2WE#eW5c`Mz5 ztefj?^QY+{g$)Gk#jia?CQRc65}PpAB! zA3|FKL8s!bbXyxmyeZAvLDOUHG<4J95YF`5_NwV8LRz@m`PAJD>2KWTQVp`CyIE_9 z3>(a@g#d5W zDF#OPt$U$cbx6)ZD%*bZT=l7^cT<*y?*@x!U7;EeuLC?yx-&z7wYdD zgq}P5tIeo=VFRt;>Z-CR&))A2WNAFkD(uSB@*6eqnDQI*_Rgco;Lvp)S<;kuV=_t% zkv;{4lyX_zW=4T{)E&jsc8XJZA~qTTF{yPDe(IZE>9ayfHh*;Ut=#FERKNGVw?)~F z7ZL?V3*DIb(?1#b)@JW~suH8Sd>|mP%%mG5+gZ-*s_410SSGw|IoghO+{Fe1h`3och4 zKAL`Fd$fuVJ$WqgG+D!hrbA1edhF{cok!Sf?n8**K@tpAL?kD)r;S@biUbXWNPu%I zK+5uZe(jZc!8SP)I&cqJ&X91V4kUSOu?60!v7W(a&bPL%>dekFrDfVvny*hzmB97< z44XT>4nG4W-TR`awC}s};z&p67Eed1of~7ia5y>NZ|-m2i=SoRFyj3Ni{CZgL0}u) zrHepM4$PQ_V#lJo8a9&9PaMMDiqc04e>cn zbvck>{b-RA25}dr$rVt{sjTayG>4m+ha#xEkrZu7;akouR=VkqB^1iQgR)yeWliID zAg!QBOJJphlkeHZDEE|6mv+qRG8O;iwmO@BlTS#q&*Uk)b)ma@`C`~cJmcv(dx8~I zt+}3cpO+a3V|mfShQrB4GpQ;AU8m%IbH~e;eS=cf0^E3*GB%4o`aDiYD>0~v+0H(n z`@2*W$Z~CtaC93GU(i&gvt4o(-IG1YQvlyq90>Ow3?BmI>*0OUMj+cnQKXkBvNOju z+V}|~+N;owClMuE0a`oF&K-73%V(+Az+2e?`Ad>YGWw6r-#`ffCjx)aR^b?**j zUN(wfu566(EA^DYh$cvxpZHGfk94SR6kJgBzk)m$&fpuwt~CnC+pqljw+9zmzauu? zd7#Jsk+3V6o=|g0u%+WC!bnYKjQU4daX?b)xBW_`%ahJ~%NM#EDfJ zePj;2Qc_*X0!nOXo7H4sJ7ehF<)m_b%sPw%?w84589@jNCuYKl6=lked^gtQ31Yc% zxmV$(tPDYt<10{&c?iaT;rUqa+Z5NjQeaTXZ7?ca)JflkX1wRub4in(<0Dc~T3C1e z+$_?4M2Zsb<{N2}{(Wz_4SVNQx;?D4uPq^8a|X6I_5$vBqK+WqLn z$vNE`Ap!ODqRn7}omPgCsooGmjriH2Uz^GNhh^aAShujlC~@{*iAtr8#Q7n_;2n8# zJ8@Q7!)mq}-#o7#@o9lA8T+ty9aGDeFgY5N5e`g;Hba`2m#@SpC-v+d$hILpOy^r$8TArT2or4>T!t`7vJ!eIkd9Hec_l^+9RHD0xP@)!pf`_v#&m zZV$mHd!K%5Mn}799W#Fg3fFP(j7xYas)9bd(A=4tfblZEhlCrKqc0 zmbwbDij{lfL#63f_naJV`woSojA)9s`%leaN8XftQhnF3QE?Yid`?Zzo%a7|_{Ye3 z6g_`6`?Sg0C8O`k&M(f|K#C3wS)KbP?gR?GeufuO`Nv4`@up$``m7inA1<_=tZ!Dz zaJiH{h8!|3eAwbunpn=xPI<0fE&RX5*v%k$NN0Fmmu1PqCARC9QY6?1Ia#G*i|)X}y3#hbi&9P{ zAJPxiSKi7aFkMIFB}caMEy%cTqf0e6BgCr|)0Fu4qhRjU-P)@i^9O%%iapG5&+Z^6 zoLBv@%6mIHTj>xK(d7CYw`CD7qRi%VJfFAHgD3lo4odUdtC^hYgXt%5ZtHwCM;pTZ z4`JLp?(7l&QJ)Z9dC-@sQ%%{6_zTYhx7;oaK9x~>4YBf}jNCAzH$a(0?anQhOw6c7 zYKNh{8wu6~im2I&=lWn{Jrg2SQb_$#OuhKoBiQ+GG`ttj{+r6rj)Te_oX-*P3~^(Zy6 z_S*O1Y?l30j|f!-EQ(JJShdfPIC0BXy16G?IWFAYD*70SlXXD4!MGc1UKcjf#*Sk@ z_`RcdcOkq%Cm=$}iK-6vD0%cZDcFY1s?Mirq=UgvQ=QYh9*p^7=vPvXGFppFR&dGi z!F63Q6h&hUQ^4$ul=jdz(|A42QvY;?5$~8x-koOX&luFPvD$=ikgu{_a-O2!#89YT4b~m$;Z$dSP#6OS?bMg-Z zC#o-{yS4&;u5fa{hcWmzdKEF7eVL%PQVvd5SoN|ejRJz{rnj@)>M=&>&ACBQGeb$L z13};3pPj#8-qZ!bh2u4K$E?m+Nuk!x6o((TDq*`#K3%kl z5zf*xO>A(hV%dk4=xN9k#@~G9nILO;bwZ`69hrj<_S%1u@ zYc}yGVcDTx?SA@scbG4kvSNu(c|H{%ah8+mewO&+AxSeRill8c?SV-CZ4q0XY5jx~ z5+s|x40SRtFZ4a7Ua!gk1k^BEfFXKEjUb}>)h)URmAx9{_yM<@YKcVPQ+=m7^MIvA zY$U5_5g`9y!JuwJ_aj$YI1KF&9dOw&*c8T1l}?VdOCAfn=kJ-`M8yTSm-flCEK_sB z;Olq-d?Zh&JfmAQz*yb5d8a|G2xjSImS;7xiWr%isz_{`k23y{40F!Z_$2S4K9~io z&c|Q|dJkUPem@QwVyBbH6xEPG%eEYo6y|;4I`8{zz;G@WbUU^Mc5$%094%5?<9`8Q zq04tA9i67W)f?u-S`+{I56xg_AWuZ1X|m5)+IK<|Ls0WLppT& zOtGV?cudM=ah+GII-S>7f!Wh*tq~OO%vF`p-4?D^cpwW^`k2G#b@w(DlCY z#f7-o&QHINr`yw0%Ny6!iJnni#<9m-YrH2ahcxX>RVdx%nlqSo!t6+M?rpT0?TQ-^ zVy|p|kbt9po)T+uPx_1*_Y(KF#n39{l;N>JKiTwhC}^|*r%$p3ly?Ve9LansRS$ms z;N`Z6iS=Qrf{gNdd~9~AF-2?nEJ^#IwH^=&13NZg95L<}`IyUWXZ+##ymua@R1+*) zZ$Ofh*22rP?|3(_G(==3dnY$H2TbZU-yhp+mMDxmFX&Bij6F;aC@gsrdtk(}r+Zb) zplQFf8W|mHmOLbbucg#tN|wdR1E3BLfBh++*j=ixd7r+F1ICx5^zFpLgoKx&H2n^TJNm%GceA_elYl$F?4D>hKz9TbqMJpw5^rEa+LyTjWWGl;oREn<}qbq0weg z#?fW0KFjVy#HDQHUsRaqk-&(*yqZ=SCG}h^Dt~#`fC8~4@x4q5GDFjP!6tp4reR5C zI9oKE8Ov+UbYK>Rf(T%BIcy{HAr{>1t*>J-C!T)WQ42Fyx|Vu&G6V5Ss(IlmGHl>+ zwU57?e!1dBWzhj0RecfJH|n8zYc|QS=pwLG>F(|_33YYJ_>?ihw0sp-i|#Bajs}gl zKC4NROy3$uCwHF~8QnW_T|`o>(0C!t25_jY!FR_hOGQ$-a&URP95Y zEN1GgePl&?1KB0c{gl~16S~%kUKV~o>2h~Vj{ns^s9yY|BJj*%Ly(2+{8!e{|4X2P zUr`iB@oo+aT?i_1by5$AQeAdg9>Io4O|wfg71v2@4oDQ=j*)G;N{kuPfy~6QM1$bm zS6)~M6+b9~)Hk#oj-=4EGWShp%UV&qE>MnN$JGE3_QB~!k&m_>2h^OK5=^`9(T!D zM_Qk{Y(i&CunNedyYNTk=^C+;p+G&Lxb7G#?uJr&F)^7U#FOl<(H=-I6bs|Imc`QHqUo3XjN(Wfw5 z{?y>7hS51irROA@Ja4fja^EL3(hjlVHGqXP%G)xb>SnRYsOwybYLU0F`7KmFw$|BJ>|da;SkPix{cUqqt&^w-c1#9&S$G?Wz9Vxh#ej9X z8jlmQ7H%QWg3SR_@lpdh-y}56s7oySi>9;Qt8{L!6nsl<#M-#ubRfCW*VjqB7^sP_ zYme#XKMjwS6O`)Y$Vl{KK8&Qc;HX zcH}uAjA=9(I8M)APe&;3D)5{eC${y%gsx$TrzSrv`Nu{*!cXslfu)Vd1p55gDI1 zpu`bj1UcDeXs3cWE~*)l3&AqT>*jOLgd(swx||jub-5biLQpnr`OB8s9Da2kzn;&| z}J$xa_6>L;;OfLj3Ap@};enbVio$jkMjSiub=zF`4rOa2n1eOtdrn zZM!2_H(cr=nvI=mecDP%Y?;S`)oV;t26y)X+&+9mQb8wh~vLkzk#yDHhR1P=%b*{-8BidoFCutIih?;-pxJvA|gWY{S)b z9Hr4~bPYx@Mqg4c%x-(Yi5W&vCI7pn_MlNB0k#!;1Lw`i~W(ELTfXzMvdId z)55hA`_&BrCFkg*Psw`7Pzx8^I27tFxkR6HJ4#gW1YA#1_17W4*59-JzP_?2a$%DM0yNt&v=#$QkC* z+IgwnB2>EEs&-bd_N@*LGhEU{j9kxS`+QJHjoWsq#;5hCX;C1}1Kq*2_}$O+K3UJb6->IcG;`jT;YawG<|W560`R)1Bf|P_$xmPBQS| z++R$v0)f+nSA+XrpcqmyP!RiZCbqbBY%PU85A*h)4}y0o*7k}@vPq#5>G6P=N@F(X?A_lTDq|S~q{ovmM z2y*bbQf2sXhLNVIm*56EqHWz#KOs7n)ZN5lX&HX4zIK0B7UAp_J@_<&MT~Jo3U5=Rai1x|;yD%~EEUB_H zg(F(6q{8aD3l|5er@deE)9=8Bs+EbD2UUQA+A1&un9(}_C(ST*9%31NHeBCBRL0di zv%+^1vB$U9-b$Ao*qb!EiR=JC9>w*zk$!{`yp z5Q0<6xaEWBMROg?%mQdehEe=c6c(~<-FjXl>I>?KDie8}AhheK-{MpOU{C7@Q3(^% zCCupOF0ddBX;P&zT+d7xovo)y2xGTjj4ywyetd(zt<_)fB^-MGL(d`tCQ_gln*Q5b z*z}Qc+OzX6!>39GS_S+!TG2cwzW+7$4-d1v7h(#I_QajPwk^Qs@hrRhWSIsa)y!S} zpZ%eboI3+Usy>cu-svERt+DU#z~U~|d{4LT-~-u@efKLNN2l@8=@;T8>ibH-xxNiD z11H@oo%KEvzh)lRe=351eq$;nEUs$LyyGt2q@^W!M{T`@u7y$30HiaUu2r}_RE|qr z?H0`LdK4|k>?=m}&92QNfl)7+t&wt@D#3Cq)d^J|xcd?nGi5n9vq&xmnj9sF4t;`% zsZlp>_sGxY+{r9Oz_{}4-H?EdRLjb~U3jcz)xRdU%-juN<{tlK=DRnUxhcTR zulK9k;gn@UCow~XSn5Id(fdaeg$7oLS`DUpQ>O|ePDviu|4lrz$qT+&Q(^P=54TA7 z9#LgSDNEu`At0Uq5lMWoh(CD`wkQ)!@%r#UZw!6OSUC!Sgpfo9u*lLwR`OVyV3r5& zN{2l&8#b3g21NffS@M`+`1<-R$E0ih@473T#2rTM$lG1b*;(ro)7)0!TA8I(3=S%t zIcexVFq+>|m*s2IMGWpI6gpt_haW69-!flSv{``lPd#O+l$kzD@iYn@z#`q9`7#RR zp^UMFqbyeqXJM{AdC3^eGW0-^big$r8+T=D0Yz^u^s8ZCGFo%p zM{u$-P}{F-9H^%@E*z82^?RNczh_J>S^_IVVecBpkc6cP6b@X4ujG}T)IoRU~Dli^v6f8-N!5T;#9 zFcE*Le^SkZajl@GPM-Wi)T>%0K1CdgF`)Ko-ccFISx7rTVl8r?B5i9?=_D9fCAstt zQ;f!SuJ9jQj3ztoKcy)qd?fk&-rhJv;N1NEMT|%`){82+02PFIE1#xykjpYcKR~a7 zbuK2g(m6FIq}9p6(@wW6fVU4s2qZZ?dK)l(FdxyN^NJ}e@2xMM>){Gfazhf{l= z3f_eRV)pz-j>6TYGelXUy!&WlQDDiNjWz4ctHiYpp9;nJFSU-q>bqT~W@uAPb0g&E z&A?)s?ZG1_U4(jrY3GL;Hms;6Wbr=$&FUtgQTXqG=1MJcC|a_Csy8Du4ZH{b9Jfi#Y?p5YkI@*yE|R2bkHpWu zMTNilm<>UOl=w!a>jo}3$z8i}EHF;L;!M+){1>$56FE^02lP;~YsR~p6@jYdW8>7P zO;G7nJ3)<-BY7cP1V-Fc;HK)uN4A2L<2EujD_w!sI7wUuOmpgVXm%H;t56Kq%&F69 zOhjT?D9P-swR`TW*ZZ-YDT+d1&8sw21~O?(3T{uwWnz(4iDS-uZKzyn;2LScMejp0 zT}}Ej{d`0c^cmYOv5^?S8APTh0#UK^Xg<>_it}*Q`E)Gj#rX^;2JWSZUZxuav5T-0 zxE<-9?xquzY_tn7dia>*pzd#2`Y9Ept6-aC_&t4;os+8J$7Wj~*5dCTES|aK9v;z-cej&gvz`FfE!HH9nC440M6nhoR!r1KdkrLUG|t^{ zH5yJ^w1rdA6>aG^tOl^tQ!M6F}2mmz=}$aS`Z<8n&Q_o zt$C-c8nS)NY;ILllxQi?UhU{Gul99ve3A}Omz5CHAI6Sn>8#FHn|}-(VdppYDqg$; z=8Y5OtPHqm@VoSWi{n{-4LgQLlg#AYM>*M_!nD^{M&fEf{EtRMvNOS?k~w)vtB-I0 zdU#Lxm;8@dLUY;)r6CX3v}CDZzI6U}+~XhDH}W)OMlb5p^N1=D}MC;q>(Wy z(i~3P2<#Dv)6QkhWn{G`%NAybd>RA-?3?c9O(Az3Sg!21?qfTK`Jkm=j&^+(!m(Bl zg%);1It|>4sCdJf$)J|2lM)enHOiC$LQKpF&um1UV;|B(9$V5$f+PdpV;S8&emmk^}N1F++0)O6uo1~PLJq^ylXx2>gPY^=`0L&MyD`#v(x-`t?Vechn>W)}GT z;f&g*qBFUn=+ctJ*@a?H3VLot%h|VFwM1px zSiS`%=b>6!!CUX)X?BLaSJ8!qVi4p6tgoI2S4qF8GsYnCXo_T~ z*sY?|iTFG2pH$k7+V$jRPA~UGAD9`~iCZa2?U0N^xdCRI0V4c@jq(7I0HwGIO1^HM z=gf^x)>~6mM}eQ-NZHgkW-Q%L8Ef6V5i4GBK>(F*lE7ldj0}b0Yj;{Fd+6m=Kp{c3 zxeNJAtofxN?JVdT-rra4cKgy(CT*tN(mS|TB6pGg_CLNj$#-hIP{V=@bg2lt(v^o< zfsX|85!?*^`{4WmhM5C4+-oX+nV%G=@WklJ`bI+SOSyLO%Vg=-ScffZ?!shXqiDwun&?T$rdi|G*Q)t# z>-Y1AXOi1`dRRw-Hay{uyh$wW-fnRBv)@Cys`|xiczrV$cgx)F^&fg{ZmLL>3!AMk z`Cw&tVJtt$_U+Xq50f=Yek_Dc+8G)~Q1)Hgz!|S!%uEeEodRPTSFK*dqIK3x2-0%% zjulF_$-a%*^uhB2?VdMIvbsY?=b(`v{wy#R&aM~_jmCWSQNgCOKCP_IY%Bp(hav-v zd{Bw{j2RJm(~DIi`5)o>COa1iHBPr7W%iM_4xuI4SYbSnT5s>dro(B?0KB1QfjoGa z!09og(9CpDV$96tX>=*XrlyrJ*6HgYf+tiPBxdWJNhB4F3RYW?m!1}HRlQE-ZxI-t z4TmoalUWr_#G&#V(*Sa4$zhSRseZW%;k$*Yhnow6W7qc?uGuJaU3oHOLje zJOK|?mRKD7yg;u*v)Xs$r0TW}LDX+<_lbry>j5!f1*$I@Uw-lGU5FXtl7zHwTll*RHLs zkPIE;RU9f$O_p^zUcu*Q4)7-Ghw@Zz?=bVlz%8Gg^NG`=O|)6RMW3i0YP@|nL9ViT z)@5mGkX<<^v0c}2g7~w+o5D}CiALK&xu_FbB2{^vJWfF16t=pXFh;D7NM6=gt@Wp! z3oAG5?=J{MlNVrff#Q8T7!StBwEH+QdW{BVYrZalJd@iyBj#e40r$#nZd4V(4}*9n zK{7azLpS`L+h5RnpkexQdbH-5KmsA*XtL6Jb&XS(PK09?i`4nshnGGwI#q#F2n5zC z<|T^6;A9ZO`q()c?A6FHRUpt^vdg@#-N{j)WK_a=5fyTmjc*6742XBQDMV{}YOL5O zJ>p$3A*J%LI&MnKRabAonqe@HrL{&ixBmaJ|SbFXmfh+!}Z{rHl2K6gMr%HgQJVLVk5_J|6+(A$LO0D(2X2fLpev zGif3*QyA|JH`Uv@2mLz7MMHr5GjFY*4u3O^r|#Mav4QmeBE-#ck}=378{Sk^_DYjN z!vlpZsnfF`(BY@U2DgHa6IClezVKTaiZL3K3tt(!UOThK)?9D8J#M?r_VCH6HfxC> zIqlg_tI33vM~%4f^V)AXYJt!>j_CAS$W7??eMqtRx*I35MV5p`M5|XWpMI%w-G>L~ zZEHO3H}u(eJ(zrF--O^6z|$!g4(ITP?i^db&}iVCGCuT&L%GPtW}jN$t*Pva*q!HJ(SOg8Kp+T@PP@1}Y{EQy zr$UC`Id$B%);~{rP%~a?jpVrug<~5% zSzeM$snyNEEwqK?GlkC(?`~%u%%M2)q=94Frv2ZAap#S@40DUOzHVFn53R6zDa|8m zhqOx9|FWhCMAz?<{AD=TC&F!Z50k6fN(P8lnm1+GGCmQF!+eM@de*TmqdC{(; zw}3YFQbAou<@?1C1JiAC;lpXp1H`Zo-USW7L`QaUwX(WgIA*fOMEeEDn-Ueg3$WM& zw|5Q|n~weG>kr!DyV+W@zP1)%>FB=!ay3Nj<%SWIp`3`}+N|H(3z8RhuZ?G~-&mp3 z!mZ+qY_Rf$RnwJM@Zs#RiM-r~cV>pN;Qru|lKSndWDFl15qmpeZ}5DpLXuKlh>e`k}GNBVwtPY=*)-p~=;A^XZJwS^eeN z&yiJD5D9ASdX3+rY(b+6Sx(Nyg}?X(pg}wsB?G4K0FDfE75n`xrzs0`LE&qlPU>tj zC*7}s`X4Yrcqs9J$KCRp#iHn84?`Z^Z*7hwsyUxI6Q)2|uAWg@1thFDeiU`mF6kMT zu=uHE&ZOc}>%}tye0FkZ=t*!&Z2vDl|KTer+emIaSvXjMK|n*qVm5T{&s?7m;T1& zQ1shLneTqo%>?`52GD!o?1**l?0ibCzOqvd;rI2Pt{RE{C0zN2$a>dlZCJ8z3$q=} zP%gB`Yj_dFM7#!n`U!IJw_na$%nhyySc#wNa#H(yPo(4C-y^S~TgYgk_X^K=Wf099 z_s@uLK3(F#3xjhuB`C2Q65^K!_EaoIfP^FmN{(b877d1gyHrwpmwm7}p$+Gcqtt8rn?{+r=okcwgIetsV422eI7&G6KL zNFFFeBcm$VreZ@GUA%1|l>pl)cP^Q}*KodWotWghXZjzJ>;3Eou}}V!B{%vOGIQ@K zVwVqe<_iCVlzrX?{$bnVzt~&|)l)b|!&@VeY%!4YE*FCakju0DnFvDwg$^1>UOmex z$?J?3GQPg?w{*OFIrxjS6p$i~ejm?mvsE~Gz_7er#6N!bW|AY~6)rM)*>&75HPMtl zf(BGtZ)w0SMBDj`1Q=(cb^pO@-=Bb|3h|Tt6~%84&%&mMF1zBtd(owkylxmO`q<^< zyU(9LF9Gs6S_Rkbx^Pk9wE%Dgq~pOA=#f2#pKWOs+8_9!ZribEyudrA@eK#Hwvq z&u)C-S*FE-Z_5x1fd5n4DLe1Kf8pQlV*c3ql-==Xq0mC|A3OgON_OhM@Js~w2?MWh z?1eh-`3zbSd#s}ZZvHa($WUJzFe56jE#W?7MijgNJC-j84Gs2r>>nV?i2u$K+Ojdy z2T+Xb%@5z2>Y*(orW+MfF#q3H$aCJZTa>OBpTAssq{9vf?W9TsLK0ZHx>eix{1`y< zBhUWlvrF=<{qD9riO7Log2vivj`&bv0WhGR;?EZ~tYzlL90gDY!@KM^i;I~zfG_q; z%c^7TZ`3+Cv*O~9<}2IHQ>9zK-BtN-=Bwv8k)MJ5-}ojURel!#bt$=)5ClGVd~CML5*4#~ePRRgbE-uX9%h&J>jHCFy}Q8fV9s%Oxx>#Ae=|A* zEZzIGJt_I>qOaQq{E;JU^KaklTf_PM;fLFtH`Dh>9Xz;`U5GUmvm6H=-1v<~F|x~k z`rF@m^9zdcet-3&^%k0bxjc~2h>GD(s+I<@6L%`%Ja8mbBxEeUul)5J1_UH&3`_1$HKYXe0aOZ0?3Hj}_k*7N2G*?b?OIIKF%%&Bc zQZTcd78LuGukOv+-6*}3CJU%mRVDAh={y$))Sq?F))axRiwZdWtHXCS!FF@u$KmUg z6L>tf|BoY=-+ye!pD&$uT>h+XOpg_|E~oB=a%Yn^)*XO7vXT|5;LC7{X+1reN)mX( z!uKGt04uxk6amK(rV0gZepn)Vy9wqXUv&-OQHUy~6(wQ+aT$boZ)1tIyL#n_02Q9O^BueK(uo|4)?ywQ=dyAJ_Rd3P`1N9oZL3cmYxyya73C}+@^}y zMFV$#gpGi63zXiQNw4E|Nhx)Q3_b#Yc%@ht@r}{#`FYb#&-j+rK(+ZBuzL3Y!|HiG zd-Gw*`4IWziYCY;h^UlAzq{5{-ceC=PCZ2#Y%ThyVA1a7>b z1w6NvtNpVSxaY5wj^yE~=3JNI{Q=ggV+yyyG>-?P^Htzp(0XR~JZbKk$XuIm?15YMgvV}s8@qTz-O z55=?Mh)^kmsC&E;uvcy2T3O6?mV%_Sk{yWmW|2^{gzxxtOeKU<03nZ_LwD8XX zcq={XAW<(rJsnjD23n4Q;Bgm?;rZ^gVX{pX3M{`q(IxOW9hPc^5!4*HWCHFfg9qeE z_8XBxSJp?&pju_429m1U@TDGydANIf5E8K=TsH_g(0>QT zFLu~4^Z!@Wwbdu&gf>Gjpe^5W)3ofNgV5W-!=%XuGgU{$_B4E23g6qKC3=x7X^ln? z=gCFkn_XPtt8Rs$KvY7Po6ELG2*T%Ok%0%k(3?539EEhfO)|&6XW5w6>0Za(_B~H? z-^;Kzax&>PxFy(C9K}^PJZZSwv%^FsU?MVWjwgKG;ME-tlSSuo(Tzb|4Z3qlBaZ-~ zWzb_f2ZWGw5q~j{A+Y}ZUO%$uFJ!Te<#vCy(V7fM&+2@Hl5Z_;yK_mm-ozFCH zB>Pt)1Xxl4Pi#GZ%zZvl-NfYu{u@l^EH3lCSs=4@!YKdCgJPQ!uidiFpo#&0D-A3u zQU?jp$I*oCGGm!*cvpwzA1Vq|ibC313+XreN_2bq7)pQ&S;o**AO`>^&8>OqLwwb!x5>!Y$|dgu+>?FmsI~ zvpzzLF*xYEJyM0$e zL_9EdnkaYkx<-#2wjDdg_s)9>rfQNLxLZj4ac(naH|=|KxGhwTj+h#BKF(s++%}D1I zaE#OM%oy43w-kIIt58g+n?@Qx;#qJrKKhv08_!CSGe{^C&jMaSP)X=|w%P#W83nLw zu`9OG)m&nSma|D2T_n#NgDM}T>yFOf_z{)`GZ5gt7mDF55kg?i#0;>~6SP{c*`$}< z?u2ks3b%8z`Tp!Mw#4hnoX^ePJc@feqaOBdyu==AThcB5N-#~)CL5&0ikFvxoiO=V6=d(;a6zzYcRvf7iSwr;aoTnhTbM+mixx*`}9ZMCBN*{zirn?a|T;Go8`qE zSFj4EJb(Ek)!p+iq6wY$aSWn?1y*$X5zxCusJCn3iFtl7e>-~n6evS1>~(feo2So@ z3tX+n4C{W+nSE-oAL05TGdK-s<%aBD!|o@`z0XASy*3_*>kArp5A6t_f6NSsgieR} z`Z|s9i7)c*bY#xPiLkGib^UC6GIZ5(Om)M9+_@rhd6O9|(&&3MIPmu?KhWs><*|~`^-ycO1@~2X-OxdqB zF?f=4xPY|aWGr}hI?6bE5KDQUYw9`0U|c#{Ah_yk5pvOzYWJMNf7LPlM#t6Wep5>1 z@v~wKY(SdCW;V{Fv%LO^Ov1>(y^#x4*=Tp%fA7(&`hd1uNtU}C&I&*4_zRlFuV2n( zGMIT#yOEYTpbx(z>{@j+5O0l+x(m~oxNV_4&LDN53xP8eZ-(9-pbDhlpXLVh>p$j} zWpy6rj2IyFc3xQo@9+!Srn!cBNDrn75^u#iLoGJRq+T@mZGKpAkoXQ&fO(Nft z^!M*ZA;aQ}-<@S_BU3M=DI~S^P@;RB+{EYssJM1I4<-|Es#i{4<$NzL0htRLv0RE7 z6H;_S!WGyDiwU-i^$Tak*dDKm&{GG}Cy2T48xOoo1kNK|8;qwkN?2YC9j^{uu-18R zKKRH&qVyx|%X*BDQ~!{p%)yRY$g#jrrUU>&gY#zWo27R&XLBRp9Xlm z@b3A2;mz|-(DmeWBa+F_bITg7QiZ38tADInK>TSU1Q}w|iSapB9Qk z=x|_nEl*qgLGrHi%G(#WKS`u?2Ev`J3Bpu*+@7JavA5cgb+h{iJAo>M zMIik-iF9#+Ej-2ttk7fD`X(=`2LulZgL7dakt3#Rj>X0wG9h7d0f6~0?{ zXl0T53q2M?eC|)uT}9GoeMw5yLBzftBv2YSt&&7{iuNM5hpR9DI_cjiiyd<6 zF7Utt0o~Yl`=Us$#Lx~QJ5TWq&aaJr=Oz;?&gL51!Mr^}o;{M!4xHhwX`fc0tqaim zi`Kc)bI?Jz+fzDd_#D6?VM+jEp33y!8}ZzGWHhFW0A;$h&AAz)H4_y(52VVwnNu02 zJw2Et&uwxLcwV&2o79ncp^l{r6r3RiNU2Csee+)OJF}aj_*P7jhN-zJB%YgxtHXGl zla-xKDyS=8p+poX5j}+;CJryDy#5jOy1I@a$=3pju#O; zJ>#$=_2{J^h#+96aA;u-mgfWE5Xjt99}ibmz>*;#k#>^RlKw-Z_)&vXY8Tqhs;EVk zjjtGo&1=iaMbm%bsl;o5woi+yr;u_$7aez$kxmAbqN1I5m^bz3=^sH_e4n*ffI zf)}z6k~v@SLeKP}tdaU6A&ik#g{lOsG)G)MHl6OyScFu z?5rYa9$Ms#fTbl}&u>PiEIs8dEI=42fg~-^n_j)`@7@5Z*YHc7e(=&zAo3VBThdKs zcf7_7E5)z7@$G(lAQK*xQ04(;I3pd|z*o!Fw?5*jlb%FOrmRK1;| z@2w>OP_Wr8H0;-;f-W!qcIVtuJ{^DvTRZsM5tjgoc|2iGNMiF{Y07OST$|EBUM~(Rpg4gNEIF<+F)O)~jyuU)qb;uRmFD@YLT3Z!sSTGFlH` znRm7dkQ0+{|Cfp}(5G+qX9t9{E3Qz%Z7-j==jQP_-9LYo@XYu{>EIdk5EH6MJMEr< zYaJHZN#QH#bpL?C$tSaH*5<|!m8B-5boeT=_%|qtZ`?JVKPrzA$Ka!N`tUJ}8W2#= z%Wd`M$9gw2lP@}V_8po|vsnY>e_9w8oY;&l)i2)JZsI9mR@fZ5-e)oGf@am$Yfoo~ zc6_S~$(sWauLD({v``>Ll3ZkTu>vC5mnKyIa-(J?0yQ-_es~N<+FUM%tDuWkC5>O+ zstMdR)#azHO8meDVn^|y+}|&RYNThI@wP1yw!28v@qGd7tegJ*PN>T=LVYiX zqg%J}e6?YTU1-w6^+Yv+@wu!bcE69xv(4KGG}q{_nvo8xUi^q@cKLdH;*qrvZ=V|**EZ?5j zC%Mhr_Fnrb!(eN>FrMQy_JSt<5`{f`I1vXQ58dR(dG2Q?q%t_{ekLA2XzNqJ0sAc z&=TU~G)4El=P1s%-XvkFSHDVZ%DH2PdVMQdj+i?K75fhwwwJK(5FPH($N!KL{THx^ z_5$=Iyg?B>2$WsD+q1pTs|J6$tUgMTTL)Y~QhlNLYSRl>&J84mLEY>PouRO0%jV!q z(7cSda1`N+R?+JDDc>{4_d3^idmogb9SI47@640$-kO(0!UTqDn82{&*}pS>kjjU{ z_zU_2Nm$WI7qE%U6FCCGIiZre!AgDQ2`U7e-|`rVpvga%oP!bRtoeF0aq zxv4Hmp#;JBue6ui;+2`gC{BG= z>zmPe7NJhOoA}A^p@R#xe%j9(eQ%3mVpJrG6;Pxpc{TdEtwJ7YPo?UYh>)H&34VH$`yE{otG>Rp#*5ub^Yp~&jrz1) zj*9(*>poe}1lxnVFy5EB1Rpkhs~*T4mSHG}Su|#g({;)_yE}<69^N%)L2i)?crn zdP?&7A(<1te(qetFK3)@Q*P4IDB~si`Y!xlm-Z*Zj<25C&ha|_WI6bZH<-6jkcJvy zMsL_A1_u%S>!;-Z538%<9`KnvJnqtOI0NG)$UW+F5 z3$OjWRDSZo&2mu_$pH(Q1fKPxY%=hLAh({PqN*xwaIMds$6)>XR*(M%m$fV2YBw$X z{DwsL=X_ilwL$l>*aw3JF|^Cz_>?F!3J!u6GN$>vo6PzjTh0}-ghYr{*m^eT!jWK6 zIJ9Bo*?CedjO421$}0=%wfAV>S9)!+o{{o94w3$Dp9` zxLl1?rISG(LO1fQRU-?(6|(nq95w-%#U6yQS8Y^!A}9 zXNriD|2zlSKV-C^d=(h(#bsTv6aMtxe`C%oQ|GK}!5WF65E- zMpZ{gw!tWEc8$$-U#IT4hq2!)H3mf805?}cjE{t@IT&R8U5>hB!RT>Pk>t#?mw;O4 z+0NG;&s}e{pt63=mWl77-Epy;V8?eaF1X}3ZG)7=yln5ds-5+PLYb5H=IeR-(}ZT# z;Lde^&R#hf2&J4IIkjw$7mR+3sQa%rNU2B#xgBy0a9b?K;?y3 zWOv-zO%w&{r1RRZt3zTAvo6#h0yvY!x&rG)8f2c%`*Vp?d_)My5}*9`9hbQ$SetL7 zfwTlkY#(Ghc(lp239fRS(eQKcLxj3Plt|sqP*Xv(y0^dF!=>=Hz;`gD%P2ayKbsV$ z8E(dB6z06t$1?1VV-E7Oy%#v?<3QgLIeKose#Ezda$w;gDVHl$)*dk(pU~yHC+=p- z-CL+t`6{mVWI&+*yYtRO?F24)f+?|Kjde(Ksni{c61f?dvYGaLFnqrHywb^Pr zySPxE#Uh67o@Zu#L=~7zpo`F4V1KT5l!?ZLyJC$B0YQm6<0hph+~M%o&vi#C7pNm6 z4v(BVhG2=qCu$m0lucoJqcZ<1>ql8et=Fm7$#zZ8iffi1Y5TA{!R}V>1;G{UZfP@4 zRaol^D`oS9I4gSZJHknFO{+vCvw zAH>Lhq6U!nhIp~43h`jxGpycQ-=HPa7%K$MA7me;#U^i+=+8nNt_KULZ^rW_oA1s^ zoOh=_wMj%4IuA-!T|bkGG5CM z#rrCUWy{>u7t_nSd`up;hD)v73n6}@Sdd4L9wH6f$fgfD8b?vWLlEfUgm?~R;NZ*s zL`}UHBbKvIWCqoYgPRIoN?dd;rnu#@VjQ{jF7p{1-u}^xOs_o#^1$+ZYjM-{n?yQ- zVigdaS8LA(SG!|Eh4>t?P$#9CCaTS52b|Xu_#Ekuyo7?Q7k!ckgf4Wn>q(dFWP#|C zFH89Gyx&gY8R9261S-&fmBJg`UiIXSIs5&6Pd)PMZ_4eOOtw1zVlpjJ3GZoSq$POE zzQPb%&;St0Y+*E>Edp}_1%4dMWS#|puL*Y)NZd)P0S`J>~9FZ8Z@ zr*f*!neiwR3F&pyA3tqj8MHm)!hM z#tUHpC&b()_>5?2JaM8>lSahk$g}>DHF7`d zMZQ`=PINT6JN8$thVHkOmZL!#6xg^`B zE3?D~wr$*E5TA&RQW-RzCKt2+>V|eBrpY4!_d5e*McA>WT<9H}19%TH-`#L@hNxFF_X?8hFX+ zdODaKduH@OjoYlL1_gcLk;Ttyqy2%3Y{0m{R1-qON{`))MyH{!BvUbG+t-*xdf_kh+c_=ApMAD- z>T{xVX1=lg99BZ}7lGc6{_;#b)48W7KqCTin+NnC(+8xZ#twLRm)qDIKgM);u5mgS zic3#&iwO^TM~PK+gXv+58MtPH+ry*_?`zsMS$y_d7mx4Bv)2nW7uPRz%y~goqOWekPUe=`Vl+pt zYPs&My|y6zfQL|Tt49;pq3g3W+zqd|7-;izIR8AHA#eG@m9+!Cp5nmtM3X9PH@%`$ zdklmHkS|IQ5fRtza_x<_W-^^w0)2DUer;&-ZmTN6P+fBgcUXuY9J0iFzYzu8tx{XC z7sofCoSa#+lOQIx)+$&+HdL?Fy)dhk1@jHeYi2o_F!51DO zpFDKX6iCZ%vEJ_b2BG6xeqYes`#m97K4ldG*Wy9o1$wMC@&Tt=T?M+8jy0dfgtSYv zZ0FTlygpi4qeY9)JY9P9S@Jip>~t;X*Qt)<@;R##^$W-$dp7+jBs)U3LK}z;(|9dk zHiG&yIFpN?bwsC#PVI|q)^?W=zw#56#Pv(l9&Q`fufoX-JD62!hy(B89=4g^&O86o zg}@G#5$)_In4Hk8X|h`sUIBbOW--XEtR4U>L)|*(9i90VYx-A5rLw0l zLWxZf{bg}~NZ93|WO-w~OogNKP%#gBOd0hEzCf%ZIbK6gPQ+<*i0DqVaVL_LZ^W#x z&l`B$Jd>U7Sl7nxmgcxK`Sglo{=to&6KR@a9T*eV0p-E^>N3ZT0o5qe?}f$7JE|Yl z1c>no!ab>f)MI=LRoM|5Tu5;<|6+}5IPBPJrkG?iNxUL0OKE*UaXQyFiCRmE1p(nv zOi?O*`c+1Gqn*?dKf~SFx9aTzcFKm}1gh=A=d` z?OErv6CSo|I~%An02C4d00*~Z8E?3}4km8!)coSN!G%q>>M#2C&%>A5F+$A6l_%qK zbGr1F^9ff%fQVA+KtB5<<@;y{G9n{V+y23F|8o6qLnOD88FvCL6EYkra{m$p`N9M0 z4}0m-L{z=!3jABN{MVaVG3P&>+a`vODSGLuhBHADP}!TQjG3q;zMHkfb5}E?ofs{9I6a}RG zogZ7+m*YmyZ)W{TpOcf5@r_$3MJYc(Tn-lGw5zPCo^6Tq@$o5(7znlP33(4?h@E!P zM85R)TihvlUG#>ee99xHZQJ`ARMJD7ktc{F1onBaQf41rlVVd0S+)kDpN^~R4=YKL zNrpe>`l40&=4wvHqrXU}R*QS+OFJ}sT=DbS-1mp%%RPY1Gon23!b#WK1QWvYnhF?4 zL)KpcB?p1=(ydP=}?yWNOO&rjnaad-4ZC zam*BP0*1gY8;2ax8)+f0`8sFXhNmhH4rPg*^dJ`28<`t2Ffq~IPne=Jr61DC0UfS_ zZiN&=EhBothb@1eXER&%cJAuxiqmH1`+Ky*2D-KYS*$||(G7Rg*+L+OeQsk(rgd2f z+mZsPXx6SdHHq8i&H9m`3^*ZE9p0s{pkmnBw}MN?S3~ZjpNN0}Age0!=61cN%@Aqq zMni9;f58!zan6kOX+r4ffxDx-hDtCNyH6;vY%fp+^G&DdP)EsvWWnE z^Qlfd%5^dICm|^^dTqgLRf6J!+3Lu){m!=z^^Qx8)ZZA9Nc2KeL$-EGn&Up62l8lb zp@~+0q(L^p1+ak@|Dl_nN@id=C04rMz)6p@vV+l9J4$W@sUX8`Q%7@E7XVqvm;mw- zKI~tr4T@?Lqh!Ku#3;i;^nWD>huau?eQbP4^#nX(lpawCB7y_*?bSwl4$oOQ2~SVx z7hcIvyU#_{kDR#!QS|7s+ZaEHg48Jc_;VBV{^t9eaR#k=m!g!fW|6aC>OA7~(sT`s zPEho=lTCbg+nBF+bv<*VC=ty4SQNJvtFHH{-s(uA71q5E%FS6YBA%Ktw_QGW8%clc z?pkOZ!S|xa2Xwut#4@PO<+sV5^!%I=wX}TNcs8M}Y<3@RzzS_(Bs{Si&&LM^CZCCA zhd~uGBq(=T1BK6J=IQ4FfzTsc6X2}9UB%{!4bbK1(oyh2Jn*C@{-UBFaX*BR&%c91 zhCKhlDx{8r2|(Hp?7t+GYu00wL z4uox(B(xVoCH8~%$3vG%ecwh-#g!s1Y-O4Zpcr`m1d0LjGQK1wpciy#uh3hgw6h5C z@oSe!*$m*Ew?^9gl7&3E`%?ub!|3^!-Ud_nxP76n667{mhgwaee&9Gf?*me7l+DMr zBJJuC?~8dU5^igIg7_mkxul^0OySM$JU%sy{MPqzyk}Gi=|Q> zN++w{y+c^u%TA@Drw@h^Cw@?nO4YP%=Wu{#(_o#>Ic;XLAe#iiozC^yOBMi5xkUR5 zz}6Q?|MQ8=;=-q)$&TYj=LRVuLqtWB;nHMmobr$nh-v&b*o`fQ*?=pBAa61>4Nw`p zo0NteeW1fSJ#Yc%Fuj=&{+SOpS=*);K7DckdZr`^eN4D!SATDVOUMF%!#_AaI0oI9|$Mk9#a}W~hi-W~nxdOOOx5j@eG zz;L;VXKuO=e>jDRuqHkS6dYvyonymoC{liK=n`^RiovxrDyj-Ubl9j3%H&>JX| zwbSH%qgFOZu*gvbN&5#f3k|9VP@in%a>lmxf+Ml^I1EY2WIEw^QDI3)$1`i z+58beT}afc(5|+X%aV%ZzL~1BAvxO`m3pjMk$G~s)Y{+Rq;T5=9f?Ic`&-=v9_F)E z`vz^u6(5E4?|=Q5^!bh!sE{ogd|d@WP5=I>AnuTwS1Q?`w$=0Ehh2a&cZX?~4u(99 zk$lBt9GFQ^$m31Ll(Unkaa!U40fI@%L3=?>UH3_N?#_6Dxkp$PLCtEk%-2lL-XG<( zsNfN@M@EztI;QwZr%=xT1Z8R8Txb+H+n*OmeEHpckbKLKA9zxo^Ue!*cX#QXns8&V zdxz6yS3@&CykpvJ?(yeLuH44+8LJ#AD=Xqu$L0eOGGaxfzpu6Bcf<)t0)gHi7zW(G z{)k3FoX&KKIFa1Phj{+=Bk-Gk<_L8kN-7?}7%HXSH1eg5BEQWHr&f(URhd6C2`l44 z--m*6+`|m9fIOhfGW4Ex1vLV{%5ZzpT^uVx?df8>*wom!Kb(aG^clvS&G#s{Uma|1 zZCM&`k9(h8O}sBJXE%CafkCKa508(Br%_=%;LOXUS^j>?B3WEX=~KVBlPnUHwhzQB z&Ixi}`;5~FvJ$4;PLfX0GAh42p4j>h5q0xd0A}3AcI3FI=l$)KYQ2k1Z0yAF!3`_3 zp(63$2Z2Bb3p7FS|F4{awRv|h9^Gh~ZG!o`&FpJB*R7GjhiF8iHMI70S&zQ+DkC5mO>R`=0@32V-m_8TBxgq{hKGU$3#Qh?Se%e8mz&JQyPzRK$nk z4#L_8c_3KZ@nMcacn3bI2{ze34_t=L_tuX zcr*MZup;D?@@_~Rv-_0KaeW9@(pL$>1v%P$d%A)iCd4~|;QLacLw;~{-~&JDe(eT1 z*eO)K%BvuFB%xJpn_8jy)}zua5ya0IV6Zp##cb&6E6xZY@N7?(r0+(SkU8>!O;V-} z4GlWS>XVLr3D2A+G|k()a$JX)0rA3u`_~`jXuskn z)PQx$lE>E%|MesAn}@P67~|siCyXJ1Z$^K0bGjuCB3Cw$4@btnspvWYE*AR==v6?5 zrsQ6BM%DK|!`B5>cXqJ1LocV=w7T5Zvc0|Csh7FBs}Kd3ZU|P0SAne$kS7!v^6+mroJ%Z?9?* zg#nsy#Q#^}<;~b1T8$oD;0>Q(7J2VO1+eUqS%;hRJvr8%fp@M8?)0F++><@Ak%Iy> zMpjY7GLUg6hW2Brse(fl1)(SCQMG7D=LqvaM|8KntFT{s4gh$P?Ax6H4!|r~JT_8Y zO^DCy=E{tv+|za=F_6@90UtUAzNZL8i9XvZ>uGBAhKs#fDFBXmVMwX(XfCs`V&~ye?`c^m=d)jA3ZVFTa5IRRIP~yuzko=>PNn-lfO-FE0tU4qWj#H; zFhiyaF0=sP&h*bZPa66bLQ3kk88p{#PX==hTZ6hCchUz^1x7)=^o?Bf#;)cifWDak z43C0>E5m3I8u9aZoww4m(}^7BBNc#2;Mde6o<-NVE(kxpDa~j zJ{Z?hp1TTq-bUyyL9dLhhbcMVTUsE)cEk;0bEm*Ik(pcb!W`v=OcHyaFFA6gO#0^L~qJcZA36O?^ETUx8?Dk>WEDi91W z{U9$&cUwl8=8Q}aZz=K@q6fax%5Dh;#J|+?;9zGbhBGwoYB)r?swTep z@1Lfjz)&v++s_?ZoF?9|L=I0mfZOJqyamp7r&S(#pN*@lM3M_xKQLfz+x?j+^pp@E zUkT83>#JR|ghtj1s<$WW{g%sZp;fo4)BtPpFzYwzoVP!0sjaOwI|B%1d!i^gtm0zr z_STCdvXNpi;hA{Hbn^G)V2cX-6kIY4tI6W*O!43z0wfI5m#O^D<&^^ue^Gd!evd`|2~s<5Q8z&pC2E7 z5~Dz3Qovqp{fl8Am^4Mic+svGn^42CuKtkZKDG?<6 zkg?#YMgQ>!$_nHL=)JGp13+bEzfnavs=Q;h34~rW*QC>oOkgnfeEe~^bdk_yReYte z&=&G+Qr9(=NvCFoWCRSSFvulwVyCRc>v>268esWGXz4xfSD=`=Yq^2Rh}EA+6r4vO zzs9`?f4tU>%WVW8#7LGDW=x~c-St~A;PCQjxqS^V!&ra=v|bl#7>Bt_Q?Ht@+Pxb3mK0=)Xy99(&KY1m z8$ma7^&B-oKscl)eTf{V)$=YKHeXj*aVPcpPmRc6r{@1I!f&ynAcF$;j2;2{5Hi9w zpjODcphmli^Y?-Nb-JS5*dhE2cOp1R749=r-2Zm)e#q}7DYt)27Nz4huuxwS%Ez{) zyt%p+7@4!RYzrY`3`;PrhK-*6?SfF9L*i&X~l3dwU@0I!W_B+??t z1Rft-2R_0#>G}Nh*_ILl;23dW`Q!kj))xsBmF5>Zpc^0T>!L^R0g=HPxlK>PZv!Ug zrU|VW6Ceg7q7uAp_PJ|#EfdRNdWr}JjQ}76<)Nfs00Xark3wp2OsQOzlQG0+07RY1jKfVE=f&2S6 zs>so2zV{~-mH=zW-o4rm4?RVBPsN#~fk)bOz0Fv=6hyKLy6{6SYdy}kM?kswn^IN~ zp$O=bAZ9c0Luviu2vTpDNRLAp36Ff5_xUtYiXw5>D9@r-h13^r^sig2DwFz2W5!&+ z?WK?=YQUAMiO2NLNg19x3d`Rq+_91>0x-I|fAzxOClMtF`4ZtOZ85N}X*~OX`5f!9 zf8GQAYIym>x)2kVl#r-^xVz&hub5iLcS#&_f82p;?;7Zh`mF0REeV1<4cK%iWrq1j zhKr6ir8@Mm6|;QN=SDjFRV>0=Gdli97{HhcypGxlmA}5khEJ@OO=NHC@xKi7K`7|? z)&r`+RE)0+qtbn@yER6Nb?d9Bd~>zz6t86UeZ}_J(_r_cNQ4z~z0l@b&|-h?juway zdpvxK|KRZ$f7z7iH!sMRj~8mH%rP6|k|o8pw~^4{sU-T1)~Nw6C(?AY zFTjDK+w9~06ikjysD{ooe~Igz@h>-|#zOG_;|}L*G}vxMjOtCfxC{GQ+T^ZpK*$3@20gI$88x=`1S!AI~yv~3<#cxl2Nl?Ya} z1?yGFHeL+V6yDQ*&UqVKYMqZv>v^{%zEadS$adlk$hsgm95rL%=Hk3qpnF)ZzH1wg~H!n8A=BaWumyX?IwJZq=zK#>@KpjNKm z0Usv~tkeV@W|iuOFsuB^LmFvqos0PC&92}PCu%aQ9$6B%&#fEi-3jvonf>t-t~X;* zGAy+c#kK26H7j%4wzcgIAAN83s`WJ8Kx-%+0MW4k6|vI^>rv*DcgKd(O%gczcEKp%j zrAIC&Q{|hjCEq*x1fe!awERx_tdrLjU`KWUCd1iTI%+ou3H96zM^@%c_Ay zBAdV_<}zzjogi51;cz}}-V0LSV_Z8Vkca5wbzKt4)^;NErN-Viy<&?-UPVR#7ajxn zg)SR02`vcwB~(Th{B%(RB2X`MIwwQ_N7@*jF?n~4Z))A@*us%bNMJ6(U!qeB!$FKB zJhrD8h5#glp{($QZkQ5<<|Eyh5;-LX1GliIg1N~GR>gpSA#*j^{XA80U=P5T?IT2m4N|F?L}(dk|Y?KKUWp{5{D}Ohul|= z(NIxU2#&6=TcprR7{@A5($K9DzP14#G9{DF@0_1e5)?Zq0@VTiEciU0WB2%#Iy$Ba zFCpb&qMvDnr)MZ*TJb z?M?mvkvCP|ZDvJ2qO+oM-X8k|6p2L4kp##)wJ?zP5;$cU&NhczFV7C%IV? z0|<39(&*`G*Rl!Zp11%E{;pMRAo@#3=K6p^vxf2nCI;O)r-ogeN?%E4LUm#=*TO^t zqQ`U7(|VALi1(zzhz!$eK`4s_x2yo`sQ@)aE=w3w0E&Od<;tDpc)%SydA=+Sf&}F( zvIN;>ormMQNlDtXQn{2et5LUj*{q|aNp$)TlTI~arh9^7KD(4oKM@HNOqiBiIfxGW z0Ymx(IJX`Fi;l&J==YYJN@iHe4&bkOiAtN=2X2@Ryj>A50Tk^G70Qi%kQQn;ozCJ8Io1 zIuJy3;X)1+HyyFU97QgFJy^lK4#3x7t_KG2er4{r*-|zVD?r}D!_3wrYf%UcM6YAP z*st*+!itVpb<4iQjGZAQ3mgmzCRaXr?E>rk1+56>yW^T8TaCq-IX4@cKlDbo004_} z|7{d<|Cv$D16?Xn#JB0khlelwKF&ThSP3cE znb+Y9c9IdJ4p6d7#r`%1HHA1Dgv1wcxK*H2n5W-w?((>owHKV-KG(ailObUxUap}H#HjiSNwZ(cqe~Z#V?6bi-q#4Uv zR0AzmUjybY=Nt#y!yLtY#@Y`xOxVo&5)>_#@2HoZnRdd|aK+pnfyl^>tE;q%u3Mw9hDo?Me3hSRAram2x zc;1E7J8&I=uKc7hw_x>Ev#R$dj=zpL<(uXz=D}=<>>Q}G z?+MvqFHN#$ZGY-HVSlm21lhfy=Clhw5;hG6ahI%bpdfgSFxT>TSZ6}mdc2VzM+&4} z*q{H6t0sf6cMv_4(qHk~Z^j2m*N79bRNR`=!jNQ0VCXNvm{6G<@?!HrQ?3G-9(Y5D zatZ}{lX9^RAFDUO2^~hSVK;k%ml2G-N{K+J1Z3uEiyZGZwNW~*0Zv+Al!iSYTRos4wWo9A)pz9lA) z3BEAjhu@CL3v_rr@v;n8k4|U?`mHub>+Y60ne%TzzXDM-P0z~J3}9M< zYW@&J#`1O$*%|N1D(aEJ4dwG?ZnYda0GM%EeqxjH|obYkAXL%XuG%q!)nH`S(#p6+0Ba>G}^y9%7!2M)qSsr-m;9 z_&KI~w=1quVHU2MSK3OA-PK3QWjyk-dY4Slt+cHC1bpc(Snb%`b0U2Wh{Ov`Tvo*O zk5@Xs;#bkCYBu{c?gE(=M=dgM?YnoK1SKIh+|qJC&m!j?hIfYo&`Y1}fvF5>Fsbl8 z#bf)M&vXOtspBbONWGgsc=S+mYOE0O=xLD(49LfKV~ z8G;dr#Yv-G&<&FfzP=s_U}0==PqP57PsxQ7lBR;Q%DimHjY>Mol90*(Ww~RpK;$LR z0_4(<3s6w8N=b-TVa)?CX{AJ@eEkjR**uRBZnFIYeI+Pa5Wb!E!n!)N{rmzEkwFLK zT$p3M^9~({`2eR)7Apbc>%Nm4A>gb^QEBlknN#RHq2(*%CAL$7ZWnKRGYa)nCZvDg zHs$Zz?gY2}Dd}c%yJT$;k;7!R{Ob7P8b6a^HromRsq!82)0w=iT`j}TcXf)0iJ||X z_M#EghyD5J?2ZGYdT0I@(cHY0l=1*kONc%ZEtMAW3iQ-jDqP$@d8m2xe8 z15~SG0jTO0Ln5Pv^+DT#?w~2;pT!b&Sqb{1fRRZw z809Az9I4Y^C^PQtfvH6?5F;rtcXC(07W(nG{*VPn1Bd`s)SKp8fYkcp>NPZeJ$k7G z3skUiOk3qNdsU$+))UcgWY}e+{l09re_Zwx+*uPMITb>#))M!zlB1b&GP}+v$5wTF zUy9k@0>F;U)cD-MS(9+H&NOy&?z^Q*MHlkw1Xx#5JMGn~LF5H0e^)t{Ca_F4>arCi z4kuw2wIBTnS3yuo)H;VY(r5jV&uD}lyX)Yb z-2v@N$(%(OdgagnUFWt{&$T6ya-ij_x(#Z}Q{-Fy??J7MdA>vgQqsfqXU!P*j?*8nd4H+)2asY)Gk4-SYCH;Yrbw5y70e0$+v*roNt2hWC zE8|jrTs^4wTOwc040fA2tV#OA-q#V9dI6E)2Hcs442L=3)V+r}>-dF~gW% zwCkv0;f`owxOG3INB%}oClvF?^L_tImBn2sHDJcO-2}$Yh`pqxy{XF|Q=ZxR`j`5y9 z_b~R7b;q37yspm;ysG<*vNkR0rN#>@siq1GMzo*sWenQD^VBxNCy(xDu&6O88{|{j zob1{(jB(($D$A%7D)ekEwaxSZ;V)(Nv_lf#mkmvw!)k|${S_!mY%4Dt-}HuS7i%70 zwP|_ZD26d~Mn;HHGT*v?imXCxVNBfnV5h%cffO&%ey) zluO%4+^^it`0SGVQu*-aW~lD9p|@(-PGlIQ@$%>fObpVP!%0|mR23rfc|n#ikUlI( zz`<8dS2t@O_=vi9{8+c$?v*i1j2gbu;&b<6Q1XWuPOE&S8LM79g&ly2ehI>HW5tH& z@|V7yfTOgz8gJUb^Am`ONqJKD93piOIp4z5FoQM1+C*i8o(1Y$NAF!yJk7`q>k)U| zdn4{1KR^Ad3;70juEqvTWsEpZ4NIdt!AD>~5ox&rTbu&|)7wpP=p9c8GM9bDE`K#v zdOAVEIYR`jNY@!7Ly;nPkZ*VgzXIZk{)SvmwFI2_-f=7VzAN~(LR9!jqg;t2SelKE z$s=Udm-F&|rRA$fy{yz@kM+v`d;ZA77W>?32)V?N4?N5bUx~eO2E{$ zjZbdZI+D7hOKfE2vu@+hi5w=lKt*V(S6vZj#CpZEodRjOUme0+*!@#Yp^46I3L z70$AE-Tt6;74H2wTTzLZqa16-w$!eU_j&>fnjiKjsO{6_9;21IJq{atTW{o`kn+mu z7yhxQ_Xo*qbHlp#w0Yw55)Zr?2Lmmu#RFN%450{erhARZ3PTG{&V38kf(=u`9}ygauxW@C;6xCP z7B^>cJu0&LUW>-}rF7w8d2IOvB<6+Z7sod!;7kcWk<=P%5joVsI7uYi`;as&uOXAk zdd{u;r+~;xxbwuKz=7*NJ}0aD^M(>i|KT9SuklVinr(w>^BY9uW-tXo&zCx2T3w2zXI!ZsAR+k?icI{%XEl4lkI&Nel{<;a zIxOeHZ{L^FaDP@Y@W%ci(;vFia3KZ*AXPjT+$Rg@x=bX{FyOvlc{s zS0I>A)G#L8^ALy}LY3m|?+*_muE%#WP~BQE!7=4c7`b`MnHfgQ7o`;H>jY(~n7-P) zvM&9rm@M_Y=Bz3_)U^g`_xzpwg7^xM6ihkdu0i-=7v^klmBPq5^q}j;5i`^Q2k6Wb z9)L?RF4rtgD~f+$y!mZ+wpyGeh8v%VQMzC6lrJ^*uv1?zNmk(l7RD^veBBXBuTXzc z9sFX9m*S3TN$mFG$g#|qBc#rTdFKiUD$*dZ%Mi9ohW%!2fu%iT=ez;!qvx}npI-RN zP#biSXf%+)s)aKggy6=L*=W7-_HKG0OCV;2TuEYwy)oICVIaT6Xe20!~+aC=NeU(c<8;_vIoDazTvw40l)Zc55^*7 zRe8JLT^GA!{PqJlCv;m&+(}Jii2RDqoa1?;F;pwuh4eBEgXyGx`j^MxR5@$Qic^E> zkGPvHaH#gIzdYPjmn`1$ntY-hF?v|1M%kiZ^yM@^O1>7ZcA|-}>ND7BTWaVKEpF^Q z)whrO8YqX%O!2To?Y<7=8eaJk(4j`}xQ7$~myrTs*z^~6Zn(;UKnh7QDtnSjW8Im| zveBmBHUGE$;ohd}CUTz2{mX2^<|hY}%C}y-Ytx77%`L^@9}a5i?w7k?@7|WKSNypW zg+4;gU%r#o_2|#)@+s~1ar-uTkD&dF+cAkhgi**gDRj)!4SVTCbJF#g`)1~eL z%a1mWnBagiYo_HTD7Ve3l&g^bwFg^<|-^Ka>3y2E8bTY5vJTnLw%===MdUqRWU zs|!826eCzdX!rF*n5G_7V>!eN=9NK)sa%)FLs`vg0||nYBYx2IEBpq-ng9|{ibeG1 zTSkzIA91aqi37K1U7&F!p^_yjFEi+?RlY0w*=Rv|xYTA5sB{yH6mkF1cQz& z5qe5n-&lI&CyvSw4n6lZx?6lNNVI@Qr#M^vVbKUG#8qB14xk>a4 zADL3uA=(ym>zc&Z$xYVY*SzTQpm^0717iWg#cI?uJLzv)dFv$Q;>PwgxSzn8#N0`* zmsPj(Ob^$&x&J%qX5B+$Fr~GMJ@_4HKxFH;X(YOWwO}+wLsRYB1TNlm7~c6?a$!HL zE1QPTWR^{;5CW&jPa2$B`*)l_3yWZx%4wG-1FQPtZ zFE|tPaL<@p2sB|5wpA1M(YfYn)#!6L5GqcFJK6mbcRc3<1}cl^)(o;aE(>?}p+}?@ zz3a7lxx+#4L!WH+hc}m6*Q@>6iE4lC7`<2JO_lBMnS|0z(KOLw zpTqbx!J?4D~%JRd# z5Se9Tfv-|=OOw6^XR7ONU7aqC{q62}3D~Aww?y|&rGz!B7R*p@f;;Y5^ryVE*!Slv zJT?1gQ|`O>rJv>b%$74D9&A!U1m`?o!ZmsQO;HvV0-fbSXdt#2qZ zn(NpmWT9RBt3pv21!wGw8UO~{?S<|U(DzFI;dj5noAxqEPsa@Vjn_z7m-VMMs6#5+ z9b@TtT4Q}A=_@w6(z5t%q(eb2xa#^h|6@;f1)OT)%j?NDvl#lZEmjhCu(i)9(tR-d z{b>>WePb+$=G%+dxL`Nlz;8pL{fMwj$NUMU-`qq>*)XgD)A~@dhWp3Xk`Y&l=|q}h zt_pIVWkWIgZxCEn`mMxh>yd-&de?i`eEANxKRN!Ld$;L4Zp*145S6!3H_(~u(~Q&z zj!6}9qZFc|Lj=>=dzR#aaPyS^EcB1nSab3R$nqj_1Bgp zFKI=Ke0fznmQ0tbDBWUej8V_w&(W>3u#a-rD*Iilb?@*OEy)+#=3GxRnzco+S}cCK z;X-ueXQe#r4EC0EBtPzjatJBZOTwMFh zugh#I{rmwj#?oX>;ouetyB)Q&6Bu2xx7wGp3-G_EKxuIgAWvtXgPW=I;hRg#b}wf+ zkwQ0}{=}a|g7}lLL*nzYCE~`?R_DeOunP$F@y8cmHPn3-CJEQJt7#lZnn<*A!Ni|G z32XkOifIZzRXJa?c8*7O!2jwkQg3~sLh^+8kFi zv$+%}s59c6(~oLm*WFt-PgK!o)U1Molu0e}`q5RF10rshJ+Q{o&(wS?&FvKi+v!!Z z$8bJxcHP=1kd2u{S_vG`hFh}AuXp}^Yn^_N1*mL z%6^h&&^q(_)%hpnhXib*6+^%F3_(cCr;Dn9bm0>}EWBEqNkFCu_Uf*BTg5WkrRj0x z`k?)E)Y9|P3Dkx_Y7*^}-6PLV97=!qBguY=`34+WTQXwtGtBM2Lt+lm)j2r*m#KTV z9A$6&75xGi?Y&Oc>R@&e&XZk6SHHcE*z={;j1KM%6qu(l_S<2UpOITzXG{q73imXA zz8HQ^?o-fMf?=`koe8`dBJC*V{x3A!_>+psQO>y)YpN1p{Ln~IA7%Zn@?wT{X{bLE z!{6C|UrOBR#d4m9-NjUig!L5sThgp~D=M!j`*G;lCeC-fRB8;jB&6z`HKDAyZ!CBQ ze{@NK6)SY}`NHnyn?s3WBfnPehI`}N=6}Q63C(+P?E?DGOk6*&-MQT={Hj%jmx&$? z5r<=Ub%4y1F^iIWc<-@);n~hfxvr9u+Ra zv1%(^ydn9(qDmd_arVS$AhOkj+_lF;Fz8*+R~Kr_a+L1`%_7yGl-J!dD^Pbs<%&;S z3!BqOQ{X^jDo(i*--=n4F_6GqjK-ya zT0)*E;0abm;jEZT3I6`!jVq(eBIg|4ExX;NNDj`zKJ5B4GNMFAMo#wfC=t{4v)b2g z%iX}aN%@o~_0GI+kxq;vs(+ROn@`6dzYTo+Nz9xGy`fOfMlUir{tmnLwPf+{F;9x@ z(F)$s38|A(y)piIdXz}+6b>1o2}O^3;3i%zMK-gd++N_OspwdMI8B$P$tFmte4y$p zJs`3sULOQwbq+E}791ThPM~AEfzM;&FLFoQ6{iP)3O+gci zU3~@Tf<7v)2A9{%>H<8s;Gu_HndB;PH_&$pu+)(zQWzX>xuk377RkqylHt{q{{H?Z zWVLRMt(^jU=8Sgz=A(8c;8G=%hK^Q~V6K%O8nkiT9}1LA`jpT<&m{Ri{+`!Fz?A-% zXE9e`^_<0qJLpx2>*%TcEdV9NJY`pV$k}dB5_k=*+!rMNP?lKD>RLu_|F*nz<28w? zIGSs{@VvnI@aMcjjHXvH2NlY=Pwsz)rVL4!j~#tvGyM6^p~~O)_2Zy3F^GSRW<$tt z(dT@)Cu+&%1yiFicp6+6F#0+N?VcF33s9C!wX+|f35QxCE zQm)ynLxjy~(r)DE_~|hCNjrEPh_zOTtRZnX;3A(Zr4c|sb#hb58rB-xE?lzyNT7$@1LvP zQ!Hz!%k_VN^h2}CD1!<89}yDNzUM}{2gDJACZ@_*r+;zaGx%wP&7z#K*C+?G zpyZ8Ul#vW(TP%KZKA9(VfW6E55T%#();cLY3p?Yr6RgMbU3sf(TZ083&({sI@mwx9 zp*UriZL8oT)K{e&jZ;HCDXh0J|FyDrvqbJi;Mca-2KjbQZw-$X^iWnv!;*0ajT;p7 zP2J$wDjjH&n1u7B&jsvRc6E$Mn?x0yy`@R&Hi-8b5wN3a92uwRZ|~Cz+~oL{Hx`%J2F1EN|gsE^*%-7TqNB-hp{ah;~>8l zRIMpYB_)55QdDw106%h1t|#cXqxl?T*hT+#&Mp*$N`nWiX^$>F+G_$|6#o5)-;FNHnYAtM10v=6VE1L?(9pG?S07XilJ4OzE%uw&QC1uqZ{v8T-iTYb zF%1l|2FQ;%>7S+LknF>V^WT=%>&}y)0wz*$lN}3iptZ(y$O#V;Ez0RgsM#z+pC;H* z519>`=#l={3PUC0RQAe%f0!)0;1J)M6}tJ`p0{QBT%**>y_>yTtm=q2Oy(p-G3Zc{ zrNTBdO_wT>vW5B%Koptag%^f_)2J_UM8Q)kkF0{4qo#mM|Fa8-)8t5YPt(bt1f&T}x9jwIufR>qlG zy?TyP!0CfZST{qjor7?c(Dm3X1&{6Q z?of+c)9J?~fRD>IZAHI1+LMirQ1aE4CyPF)%{i6}p6`e>LgtmNu1!~j)SOjX=FY9q zM8J}{vu3ZP&cFEW{-wq3VW*D6u%DUza&?#+&w_nw`07)cs`p2r6APC==x1~N{3w|4 zFE=7}>$=9r|88eHmOPoJy+&LC<4oEGLYj68uv&1_gg9Va6HzheW)N4?sE;Pk3ktNL zCr0$QzYnt*228%xQt)yGe+qIFIf$#bEw_^IUWp0@P`5Pp>Rg{lY_%+MX+A#jS)BcI zY37h6=0}#wX!G4ENY@&@7J(x{qwQkQ%&*{ld~_%?AI_>Awb8_b;+)uhvj)=ZH+Xp@ zwFc=`%4?k=Mbr(4X^f2pMMddjT#D^e!nwLDx_f* zbyTw-ZnwI-$M0|Lr}r{V`Y%M!Tfj&;<*P%?FUK?f*uCnG8j7~_$*$h}(y#e?5}N0k z?1sd%_)rf54#GUC-x{Hi<#FK77(7eAT)Cg~)9D-UG}~RLW_BlV2hsVEGK++EMkpBX zt3u=DJ<4jg#!Izow2D0m&jGDw7thNyu`L`Fz7lq=nAdT#@&3vB-L-R9>r2mRp<4N6 zChtItNfOTyJN4Bwe&=3jwLIgs;h~<82zFD{d-`bUwQbr&(fsGb9qm(DzmV3)GE$HF zH_8wM19L5(g$IHjYi3F}PZRtZi%~x%^bL{!=iPEr6P;f;>DfzKlRldg)@NvtILFcy zlU9dZpk)7K#h~Ap>*yeFp{*cXyAzr<%O=A8;h#3i7)ea{h@?+e1KzT227Jm+w}`w& zty|(y`}z;=IxPb2SD2`)aE5{Y|eOhmG0w+K&D9H=8n>-5(q>BM-VYx`5Qk zJ6>8Gq>G-!D#c%eG#^bG%R&FSQ|VNXwEOi2`+(I86R^j&m0cAc&d|p^t~ObO;H?Iu ztyGTWK4sU(-%oI}5`d%K5o-r=i#3>FB)Ow+@xUzeyaFfBRr`^O0rVr>=D~QBLg944 z?W@1Bb#OW+U~;Ag!AudZoFqF1XDu(s8z)5)?Ft052U$p>1b2T3f;zew|vzPeQ!P)2_4e#KUdMHK~e zgtxz2jadhO%ix|Y&{RUx&XWHnGOYYVcLd+UlOGha-^A~-$oxlbA>GW=He6E`&b?ifegiA4N5*{mTwLiL^M_o zF*+2{HvGb!%;CN0@eb-3rvdZl1I;PNpjeAiy^|LYI28@{(LuH7nPzCE z;5N$ma~qw^1?5ilQReXLfc*2ut8){ilp~{rp=g?D>EixI7kH+q3Ar{n8+X3X#I1ON zvAJ$~8(UVh%Ca($MP_HjXP}o|89_BekE&CZe3Y=c&&%!@TrQj^oQ76!W zyP&kPGZCHS95BN(7lKWuKtYzM@oUj%bq|i0!bkiV@*=J4V)344(rdga-R>!JdF}+; zcPRH6DL)BZU9T;Q=Dr1sf#SiXC%E|^c}!DV!JbvZTC*3eY}ISK+U_Z>UR3caX~7xK zg-!BqmFrJ&(!RdzJEmzazplff5_H>VCm`F9HBxisuc~D1rOiF)$NqgyoE#}TIw$@i z|F4J)SYgc0&H9z2p!)u?h`~f$g>CuDJjpKT+hpA+f;K@1fs;S-=bG_LC%wUvK;<8= z#?Hl^Fu);XL#Kb3n!WeqGQ-F)sdoM$oiZZQ_nfSUqOp_pFf8zkXxH=60$Q|`&i`L? zL5JX%7a-3Y`(WwkK;Y9Nm4OTmdPTaxO*2xf;e8jlBp@aAye0rI>7sn`ZSsPTwlP5# zlNf$e;M3UV5%6EYv3X3&z9Gkn9$YU>vlUu8C?qgvSB(=0oC5z?{~hV7kK0h*od@IA zYS&mkuSD&`4N!6{yB04Rg_Q!7LU0N)+0J_#vmA_@Y2JFZnb4dsiAKR@X^ za43XJ=rR9;&);wjZZ*9t8Pl&D&*Q%Mgrqk(oRWLm{OZ;qreiiFNi(E*0>=-^98wC(sKc3jUUjePv1+}CVq+M4w26<=h(}TIG;q4y5rGV8?)wm8S>40^h>)+bY}2& zc!Pr2L7rDGVLqw$&4c{T9XR@XtPJ~>D;n#1K0kf=BvabWKT7Pbr%K@~<*jw;mffOU zw+g(Mko*Ztw0wvm|7X65>Exfu@voZY@0B$+8ih1q;q*uYwidiva}!yQxw7e5OgS1R zy?NwNCCB;{{|-@?it>gV>tE~H&jG(A&_eynppwN9$y$_}{gvdW4|{TrE4nk82K$d3 z1_i0%e-?JK9EX0LEXTvcw>1Ukzgj0x2ep3jtlQG8(5MP5y~XTsLE>zrFE*x$(9!_V z&M#sQ1BPPo)3UDB5sV?Eg;wBxb5Ee<&IY2`A!ey4`0-yOo7h`oNv znx~E>6-+V>Y1!l=KD$%YewUk&bemjpLm{C_tZFKWin#+=ta-rY5|3DiG08$r;F)LM zMP>-hXEHmivG!a4%>wW{73p=jzrEeHR5Eb}&cpm>%h+C!F(9+G8rq)MPNca6yh8Iz z4<2-0HAAc}3%2zJO!UQ+z7G^yBDZBkrRp5ud!ye1>?&gsoI?0bSKNzh6$Iw0S=hd| zn+@(hKec8;`zH8`5Z& z-+gsaf~;OZUkYXb0%cy{x+;K(Y_}og9>r)ytYXFz0|2(&#Xz#hCr~d+(sfQL_vVhU z%i$aQ&Py4uWSedlg7C%{6&UpfyYz-tEx-aqt^)k9RX8JVC)$5p4p5NlRIRQ;_k?>Jo zKYkauYHPk=KUFu(CrD`~OdhQL&1)QWj!<=_-Pf}p26>a9Ew8DJZ#gq`pm@}4 zWylFNS1N+d_MpWiCrflZzJ&NR5RjA=)$g%^pzTD+1D#GeBt7XeuLovk{+!`&K?L>j z1poOJ2xFb(aFNK-(u}w)fQBmb>vNY)wmfggt?z@1Pmn^(g%Trqy(TD z(w8F=qwSZVyR1-Ca9(<)cTeTt+u~oN_Mwxzzh(lF;YI{F`nBSkFm)z=+$I-Z`(W7o ztto&c#+8PN`3F}(?{$U3|BT`{vyPD`%5z{4;04mM3!C~6pUGnSSn3Vk#K_Fphkox9 z9YpTlNeim`rv*JK+OruD&%fmqq%b|Qo|ai)6QoPv{7zwvPS7Nf*_h*AJ-Y~u-77z~mhMOJL?^9OOcqWeAeqGU#n=4SDu+9WPaUEu_q6Z6!ImYp(}P3 zQEVngIN8bblaj={oA=G{#WIU1QdiA0xtw9Tq;H!hGA_4pn>!ajfTj;Hv+ix4Ishmk zx-hD=b5K(z()3jP%m!u5yp|F1aOJdIdc)@1-tWj_epG}>BD*KfqcGG+z@3q2q1yZi zyYfzvJRigAl`Cgo_LLX(WTUzMTESPOr#jymIqFbQX7T4=15c}17d-c{o~$gc zVhgp(j0{nRydB3rqHdtaf@ic?GH;~%=O@}NXT*T2Opi1x1F5X<5h_P>ej_7&TVg z%zRA}RQk697y8(jDfLcRtAjk3AEA)Yfx~LI_~x5OJ3oS$mYwO|t#V);!-20te=cLu3O}-dNu#4agcc?nHBue zE>o`DjU=)Mc|5wGRC-kNbR5d@I>^vxikm~w4C`R0d|~NpN6^dhaL}ciTqOIqf^C;z zQ#?4Osw}Ph_^@Poru>sJSt3qHnS%otN!M7#IU1)f!_5$tA3@->q9HbH#?#SYHp1v@ zE=&4uVTMNKHCRuJo9UuEBZyGfWC5xu=u*dJ1PuR3c58G3G;E>$D1;VanPI;izT3r+9=h`T^xG=Ku9J zX$MG)=Hs^6nx#iKY3CHa45nBc%`mmox4R&*6e};W0Gw`cD2RXuWg_Iq1`^003q+=I zq@+0Vm)vk3;XLZ%cy2zor#yrf_q{aF$hq=$Gbi0n=X$U+h2JW_7uT)P1FnxPE|M9= z`n~ocQ^!h_Gu!K(OY~THXX1d1Av`^T{pkC3JR%xYUZ6eU(s4;H0?AWi9YxH+hBp+1 zmyX@F<)6-SUnJ`XD?OFEZ}o@G4@GW)>+_6Zo*Te+F@NA&=^d=LqAv^l)Cy0!CC7V? zOdT51Sr2Z;Wm?TJrMyB5V0L{}`nrVW-T(<&dO@nh zSP7W#U-p!L+KQaW{`yhtQkLz>(%Wu!Ouem+DL*UyHjrkk0yczy&_nlz%&|NE=cHCz z>+Nac{2{)IKuU1trElr|vn)9|f2*sgkSQaz05WB)4;HjeH;F}$^@(^%2U;Zu3p)CV z2M#lJk>YXF7wOMLlP2|K8<#Y_31qGq$~5dA zL%cI8Bqy8VBC;vI|I`rP|JKVso3CCvMtb%!kAcW}p&wHw zI~y*AUi%6GEuXQbs*OoY~G5(kf5QWkgvcH zq?DDO=we2d0Ta1vEc2Otz#Psl?oL1i9+5oyBhdT?Bx?8q?z2{7TF*8N+H)Elz26Bg z&2X?oF@@TA?~LnpL19c~O7`PY&iOv7m18hx5$&GtGtTJnyZa47u)$SkxQzq@%0wICaUA{G~>h5U=0-faD==Nzw8W0?SEUNshcaOeAP93=Z0DG*tzh zwMk2?Tf7p!xSQ(^+PkQjmwsF)v(DsJXV1Ju@*Q_crX=YAD6KYbKq&uE_>$P49!x^4ovG+g`}D7S z89Dv@uupWIA5KVM|6&BmU9Oe+MwXq;kO!M}U(I6Xx%_CY_Y@h5R}24Vt&cXHn)I0+ zx$ps9h?^9DF@uHh;yZ5DElRRTQ_=46*;0SqbvN)8QYs;zEC-DA>}ZQ&J{X zV^$r-oRJM}fg&e+(!Ati=AFYODknA(di-7Gf7c~`DoDAGE-WBu&0#WT7pe=gqVXnA z^ur9opJX~&$qmR#&Mfu$c%*xzFQ8b=o5b&vYe@E?z(7^!3CoY@n6yaAmK@4^L0om} ze_tU^w0`%s?P3y~pS6S-=02gc5ew2RQY|HSa{sv2RXK2^S!j~ya(E+Nkm66{an|ZD z!KCxu%a;GJC6Xdv(2>l^_k!7_jF*=!q(54 zkf7}%-9N33C<$yZG8GHr$=~4>qQ0P69~l2B!kwMhhM{&vQAFd=Jy$XLF{mda)E| zyk%pOe<%nOwB@@C{{!;nr$rj7Y^AL!+|;sk{c5YKQ87VVr}D(+qZ=-4sdt{dR>STJ zIMIiR|1a|N`F8*9&;z?*B+|+aokChU6Ql^pCP)7%0(wP;gbMfKL70Y@B%IX)K6Nr+ z;bHy#Eh4AcX^iKc`Y<~0kSrum{J~WiEU(%t&dwLxnS6GkRUNx4_~fIJ{;z#Bo&!|r zd23onl#rsxwXnA0ENqMVnq1=&Xx)XUHlqVA{_~4@stjlH^9#2)f;+R>EJ&aIh~Ab8 zw4?Thab54WoQ7iAqVfww4*N$<-1$H4A6rmgko|SBgKp^0miy0xq4Q(`5<8qMz~nPf zDYU{UM0nCNsfS2m9im#MbZn^-xMy(xUMa9_V$LG+|AX1C;1Z&>0`eE~ntYn+q0HRn zN*2W2HxmM<)5k0=SYm`ub{Pw@%l;`7%l)-SQU6ZUHx&P4xbUxE7s0%VS80!R;_4)R zr(lKaamT0lwnb8px`H#$2oc!N|MgobvB$|0<*#XL7M6u}GpV$$)B(yUzOcGL3H8Sj zgZsbpDTV?qG~|;H6)S?}wSx`ATIc*bZDSKf4*+{*Rn(vn5@gX+>bj_#38T(9o;yMr zM*g%>9gG0JO*=b-!gv9 zQ;p{Wz{B@XJ$GWb^I^lhRa5enP=MefEAp7NjUGK?jkKYEKkoa)EGL?Hh?|*$QMKHE#fQxGwZOyX1@2iz3Ptqk=i z@JMnK5~pVoZ->rW6}UKSzda#E(gLo|FBQfzz+g&cfamj!3-_nP({c^I1I*HF@RN0L z_8Fm^(<%m_Cd441Kq#Y`BCic<+?Ex=f4Ts%S7s3JBP2Lg>WY4XV-!3Jw<0P-30X^0 zAb(HJk=2Yfj#QCTNlZ$Ms}qgiz!uL1_0j5@uLHv9o`rcsS2%v>He&P4<#Tt4K%R|P zPsWu~Vgj)OH!|Gy3aDd zQH&!b((%8#Z3FT!^TW7T=C5~)Feq8!T2os)EE;~GN|YC35WN?d&u4p1_U4(XynZUamCO$$aNrGMmjN0{12Z;9%ezVknwIMSQGh!o95WrRyLM~ z^#+UWl2AGhPFjy{vf{so*UFwlBg9qW_woWljy$hjd+Ymkcz*#hOPLR$Hlv%GuU*@L z=KfLl0gF6(`A61s!>9KdayCIzyKtS(0ee<2tinUcHd^kOPQoc2K|S&hh@EL9EU4l| zygPRSjT$qV%HTxxP@@a<=1-pFUp{i2>e8>cHl^q|O5%0bGTLZZyh`kAM|n+0@}W0uz_hDrJ26 zgYBmK^7rfk?L)l@wRa1Eyek}7IsoB_LLO#Tzlcr_!kNf>%9XkcgWl)B&6Cn{MkYVc zvf{Jjh!vc<_Qp|w%-E3bjS4u!*c)bCM5kR;r}|+p^<2zHc}d&>*(YWJofQveHA^8_ zq&SPG)`c|Oar}mtzNf-z${|t(#k~`pr6H2`3`7KNsn;Zgk;QF{4MMfNe(}}TVkR!G zeA>S&!au!D@&D2-k#5djG%b``st|Nq^UW1v1i<8VHJe0w#aAm!yG^PIAU{`N@H7$h z%TII$p-gM%=qEIO;OFVMmMGHvZCTkWIKB^39WB5GT_a_d-@?Y|r9w|H4ivtl#!>hV zX7`C8gAULN$(kPv%wc3!znD39y}|9Y)g>gxPrh7b>6;ML4e{p@xT7o zS7Q0qQMVs`4_33ny+d2y4XDMNavW|r^}{eBDF0sh>%s4kqaHXdq{|>Qd^u6Xngu0r z3`~y#*j4~^O*zYdS2~)~*l^1AO3vLcx;f=blFm!RUhm!D3>W;4a(QFeFJx4G$^%^b zi|0*)Xtf(gQt2F^=(W(`L#3kiPs#neLJRj$DmL)&%!w0{Lr z_?P0JE`0`FV1TB08)y-Sb)gg*$c8j#a^pkVP40o4^X}kJ!r8qId}t&5cXYT{d)V*T z=zdrN;|d>~G*Ow;@J4$aT#N(t7=$r4AlxrZ4FDH%3PKW_wPDhHnP+EafQGszprJK# z?RrS1s?{a7M7deb2_AZWoNzc^X zp|W!V4B|KN*j1Q_>mZ<3NtZcyN4SjywOhZ0E~Y@e@eS@1+qBNAn;jr3!SHE1`5mN; zE{wT5T(a|LQdzuyX*WHV`=Yp66R8Jqq)b>phZs>ZZqp4t7Hdab7@3VOzo31zZd;mMZBma#1t8fgz%DLcLr=ui#Km>d5y$(G`P>n4)!~J2K7%|q-UB>I7w^X} zN=z;<)=wQD-p4E5^$G5eSlQaCl7Dcd_Lyo{Hq*e7+OD<)zczIoI4}{_mRY2cs_`GOjcEg{1?K%`c=3p7V8=q$-<1(kkPt*Y< zZ!;jj2|A)g0{W9xj9$`Gy_QGoNWf74EqGMv)LS${D)U;1&jE@&@!cZw`W~yZ%{a{6 zc;J!9P9uG>b9>0U6v0|V;jQqFP1I1%OMHb@sC|sHxdOa!F}GxZ*^zuO*7p$1tb5Pete@7`-5d&%Ey) zuc`JjDwFqlQPwNXb3A{Iv1gUXo=ZimXLhA&NUG&)f6@F95cGGaa-esjOspcxM3by> z4CC!F9cbV86!&Jn`NZGXwimfW?6-TrGvSb+4Q0sEaYl1})Tzn4+?t|6Be;{{Rww5v z@(i0h{l%RJpzO9l2%qK_`K0M{2I8md5On_HmG=N2)0)PSw=S&}utIO4-MYI9x914y zRnc)^$c5+kXRppW41KbuP`nsyKF$4E&#KrQ=ysUnpLv1S`VroDCvc6+9VO|8nNSOO z^=Fb~Z<=Ln9>RKwWH-}`He0!VTNb?vPR^IZ6@g!M3EL8cm2uB7yqBLC)UG!rcf~b& zOaCnOwwwjyC9jg6+*jLr-SEgJD@-hVv&of8E&;>Hl*jWkjEeTQmzU!S*Cts~Y1xjU zUlG4z&cM_(gY0ZzAFz^wFdf8S{MMqVDfw}b#aQZcXlWGjCMzZm210JxL?a}PwDQ=Q z>oEzhr0Kr+FR~9YJ;+J+Q5wN~e{P*X3&#c5K@Vqkyx#x*~UbJFWYB&-Ny+)I0#T3umsP4!IC3M}xrksOSO! zz!2PdJm5r(k*t`k) ud-SB^wHq9Jw#m~kCV?cKO8et)j?BC14Lo7Tk1IzhN9$Vs zHj{@k2ej=_lD9#Awq(#7#+&#KO?!ZG~#oXKZ|C$E}_fJG#r zx}r{X{ftl{v*>*$YDP^vH#j1|N(HWwskI+E`Gk+*I(EDO!zXK-p7%G`0CDKCSU1kY z(@}=tViQEHHPtsxn5|oc)qBfejigdU@flWDu|AeUec>w1{g*ju<%+v|3WM~{ODB;*t z0o>NHF!;2kuHpGrxaOuLnDX?!S_K=~dIZnyJ=*xL?2)ab_zp6{FXpGAqam*_1%YzF zt8e923;R#j4!AXw8uv<`xqJ+s@Fc+R%u`?Fgw2KxIOwR0GPNAS8r z_v@Z4o$H;AT(J+y$5!JmwgImV}^eLzGy47x&Y>Kn_rekv}$KOd_+4dZ!9*d+1&PRIIjzHo)V7#y$tNw zOqfo@K*{?Jg7{2jw6+EjSJCPZEH$e-Pal30K7t{uKfwchjx9)HkNZfl)P#^I1Ye`$ zENFRKTcE4zP4U*isK#NZ)OwSTMd}a=1_Tp12pH@_bGzzsG%6KgfbPyEu zLNjAW)dc{|Ug_8p8s7aG_C8TW4~-5-gt+5CL5VcXDoFL1PrsG573zH_z@3ZpDw%!lk&of^p(7C z6epiCormYv!UYu1&>B!!@k4u8&7hVOdln~89?1nwQ0&rhdE?m-^HF&8wZ5KtOm`@V z-2^=s$%%|vnHvT@9L@kq$U-@2%Tjj~+EM(bGL%Uc=oIQH5M8)InAV-j6uJsU>yPQk zApr72?#L2*`D!uJE$L&y#aHkqlHGW_?91d+waHfDj)>+E-&Z(xG4DF0mZhybU;FVJ zZ^JvvYg9CE&IBS1GI!H@cJ2Kq+oq=%h~N;~h07%#3fPAC^cG}_>KK2-t2+wS(VavE z;fJ@J3Yk)AmpylwHAW9buGMXMGM~9MQTU)qVu+`)RKoL~lWsn8ETR4Z_$hh~&U=On zt`BNxO2I8&(NWIX#hHAj6>2b3xYNV#80qy>-5Qs|pB{#n@7O0AqqpS+ytFu`i)%lp z&`Z2oV{%gTy!UU%<$>N`SCFDkt60{>rXbdlCY8n5ho7G%Qche?%jn9JIj>^Q($!nT zAtDQ#8NnL$H(PJc6UuNL>4)pODkh2Dzc@=s&3D!|H>=n82_AS;Xj0tA!zwAhxXNr; z;$kGN^a6Ra6doF@2$p*gJSC*F@`O?ES8wL6Ms>})4+B;5wzc$4bSS$Wf;Hq_tTkBA z^j%fcHOk~M>_(4>dwb3|Gso~L2`Mt{kVvG%_dkTdeK-=EbxL6#_H zJIHe1=hH*a>Emw_AU=Ko24NU&pRt#K$J!)Ktp!{zt^0Y5f+GQ7th`J7kdMPDzq;=k z5cpIeV!imD<{0iu9PI0Q0+(t^dJL4@K}~B$$`VWBjaM$ccRW;@{SsaV!Xw25*(5{c zpYX{Eufi04#@r5k8jRx~pOT#BOZSLClmGoITv;NrQU83?M=G71lpmr9=OSAAkU3NP;c-c62niG4{fd0kyR?LkuntlPXbEL8EBmM%(%|_}r^uU}hmxiFo4@$iE!`yZXYSG7S<0~0!;&ECcIV1H*)LaJX1i16 zugqO}n?Q4Rk}j2&s#E~=`SaIJERhAXWW#~lvmpedko4PaQtOqpGMncGj3oW?6oX2j0YB>T%fa60<=5z9B7!(tg{p445R<9)qi;bh|G-Q~q+_sOov+w%ut5g@pUe)SogcZ6PRhS{QQvK>O|NzM5_r2W6(QORN=I+61n55Yg|M|cW! z3MobLBHrWiqto0{0oDvu|3!p9B?|z!_kVc^uS?q9LlPw`f3yH)e zsZ`63fr2ncgg?iNo2SA91g8sd0#^~LaFh_mZ0T!$f%^0WUW8UcL?cig^?3d|UW#$H z^Zo@tXXESRzjXKEmLm2z2xX1JXM0lKS7A8Z$lx5I?4|4@37v*X!LQ`;pJkX%(%BEP z0v|QDUsfpGUqpRp@sP1Nc{##QUk*i2>Kb zt1y+*|bE&vVm&*&h69o#9^{-bCo`S7)2N!ou;Oq~82kO@Dy)Iq!V0ckv0 zi*$e*$~f=o_4k4%Po>3jA`-f7^l}R-bPA&P0s?#BEsgxjhwiIIA+VXg>|@=&kuqAQ z*Wj#6aO44Eh_OZ&hqUoi2C7h<+~6PLf=uFhX|xnig6@oyjle*_NPA+a{xJf>l4o;} zKLBdPd9ZetG7BpY(@E;>__sO^TE9z}j?K@0cI^=;DihbUP+EG@bwoUoY0Yrj(n{= z+|nyt!HwmEg1#glJK_DMjAN;jUGaVVU%O&}(C4W}(r5dQWT`-liVl-i_G0TIob4Bo z796_S5M6V!ir5FsTni{(>Con2 z&fj_wf!j=xZmYh+JWTZxAT1z;;?$=rt<7cSK9))NxEtVV;>6oyf;q$}W#i!jZqD*i zcR}inueJJact}l3V^XTi(L3>YCIbt3yP(+(e3OZegCO*qrhNZE;(GfO8P6SBP@rF5 z)3-!%Pn0<~AHVPBXbX@tj%E&^q8|nm(;8f59$dd3u|`a->a&TMrtu3=wp7>N!y2|(1O;d%ml(M{dj6U&n1J%;Cw>y>Zo_Xgh5?2xjKLNHo{rm}+ z9{`*lQYEi#IVzcl0AxPdD<%eh%~Ek17}ivQjwf6*J=07{=hY?VRyj zzPI(6hu*aOcw!&R6`zoN;eDjrrGGPwMKD5+A{FI>>a#eyDIIrv~=@^#!)iQwG_k#PvhUo)!gEXtfSJ%O@|J5Znu)E}k zC)Y+SPDuFaX5Qr-2XbWtYUr}~;r4|@moo0o&V_g%3WdY!>~#T8{UWV@dBi;q+HHp+ zWA*p#{_yJSH}VnF6LK~mw!r+d{70h>l~;ZpwP{baKb~qQPrVvG)5pzkPE4tm$-2C_uZ835*TlGXli)hqR(!-QFrR%VZ?~ zyo%%{@#Q4^VW?<)YolwTrM&UJhGHQoQGFdS{e0-wXbSXL&2;opE^ps2H5*M{5%Yb& zwrfA;!&pwINnG98QV)#Jl%dx8iEZ?C-Vd3To@vq9CRU@>3fn+vxNd(3reEXA9(=&x z#wi6uwqD8DA94X7yGKr|i-;oPY*XIWs!_w>NgDA+R!dMn%u z@%>dmxR17rF`(ID*N)AXOnQqfCuMo=KF3|tpuOZFsSC7~|r zIj}sdH`6d7vpb8ILQuC})ZukuXf|5jTIlAAtm)Kq1~B4yZU0S^C9-NI^L>+Z0%Grd89t^1racmjwp1Pd_RJD za_A<&1^(pwpZ{Tp@VmziCMfu?0newE>3L#dw1}y3{Bokyf|cvV(oMob>dGlVERtTv zg)n{0XJfct_44jJAA_e@1ka|*>j3OsNQoL^fEqo-;HbLfyiPL^#mO^WV|k~E+{7xq zZ{$ll;RO0wEm@h`d(rE!b^4cBvhgq6YH2Z11M>EJinQ;oZw7FcV5Gm`x#jAEhpl@# zgx=;}Rbpoqt;%X|LIJ?-x=s*eHq8baIpvyF4}qg$yi|$l-cgRs!%<9To=Apv`C9_{ zjqufnPJ8!|msKvHTbB&@qTT6PNCdnOl-dVmvDX`cr{3b>68sz8L4^C*x;CD~*?RleYr_t! z&ge93dvpnuJ`i-`EU@HJwU4Rd`jW{1f36%$W^_@|4haS-h zi`n)k4}U~p9LWn0?PuU1?}$Kl`vC_WubPx4x02zd!W8_y8^@7bropr8noHgvH#^SiIKd=_rw5u2Zr{RQiA|Auwg<^0z!%T-a=pU&Ns(8kfO6$E{CE})(NT;SW5 zC_UC}3=lloU3iYKpDPt2tdnPG6;L*7TTw+%F_CvFS3$s_$hkadjPZH+mXVpxc~%SJ zN@X7N?8T#^{OE^ra?~`~`9Y+3C1_XAX_elJwn_|KY>Xt!WE0(?R6GT#Cz(%b?q+f1Z0^%>tR~ zLEK1;mCz2(0?oqZ>?2}>sS2Z30R#$2(nwSFz(Xt_>5{QGkcP)NiG$SM5 z*Pmy)ZDWE0Y&sk7%hd4gH9B#W6E7o}Z6|lvn^EP9NPCNsOvbR5S%yiNOVOjN9_%B~ zo4YU;$Ki+VgNe-M3?~kDU$()pQJx>5Gu2UENm?9TD%%wE{`CR(yq|go>bx>)*_qgR zY7ZRT#wFCcWuS6vAyfVP0F=TY@0;8NESxhsSa9n$=|y;b0O$u6nh;Uj30IM)fb3u^ zC`8}(e@#539riJ0pC+sXH~ePLX-pQ2*yMJ=1b10{mZk8@4r$0}q1*E|B@3bW4Esa-nBicol%I*t|z@~{pnLa$(-Aovu_6DF1UZt1n`f_HY ziLjPMLvmO#jpYarM&quQUq^>F0Vx?jGn`C@;kDsg!s-rqJEIDWNJLM;)3kV3qjp$y zmVm+q|aSR)*_G#wj4`@b#%}rkF*{gcaQ& zsYT0JAY|~TZd2gEf#H>Ohg|gKy9FdmSlecTSCWx1GS(1I*;?lQFosd)cRz?v9j&N1 z%o&EH+B=H*y&b9o@wPxEJu-p90);mc4iBuD`eYzA>_a1Sq|CgDS2ji+P60sK5iYX@ z$P06>z@*$I77O){G~$a34U3_%7FJCUf^NQIm#E3y;hR~FR1<+6xZ5ZU9YIy2&ZuLD z+8$!Jh<+|4;!ZfxI~v;4-GMBSsZ$yjL}owU;8T6DV`Efg(MSEbd^wFZgVZ@rY9rCY zo6~5a6_!#_fHCk)&&A}L50@v6DCW>--NeJ&ohmeMYxr6fEcQ7SWHMLXNT|YYP}{g) z)9F@hYaMsF(5>?r2r9^<_JzB5IY8RR`Os=`u?yB1r?^JZ$KTv8@G~D>=Tum^Z}Ujh zQ^$TaDRN}|xpQ7sq+7Ss`R;63iVdsE#@*OfE$v!^Q!Y|uGd-OE%DP~X@%jKXQ_r^Z z-Y(~9Wb6?!MX^Vbh7?}wHFlvyy9bG*4(LXcLgxs1-d3fJaZ{pF16 zsLc;EB;pKBHq-kMZeeWo!9&h7)vT}Y6-bY}0X@MkUqq=e=onsZu2Z|C>s=3lnFb!? zwuRu-^5%wIr)4ULs2s0X<1pX19_MP;zo=rWVBO2ijfl7+_J+1=Jvc=F+pOiHf7>&8 zHwsS?YYpNId-nJ1x%@j9$47+L-^Q&Axs0d=BZiVD20I zjD|aM3(}x)k~6W7aIc2Bw*-ns+U{-2m+2Q}1`AWSDv=(Q)ahtw4~Osc@+d5C&f@pP znLQ~%b@uk!M8L==KMO`p^A^sjtq>3b7Q2lj!NEjryP8-6G%|on-uu1rV~(*a(Sd@* zx5#6KWvU_mXXh49@P?R)zyaRYgX4&l1Z`0muT8g{a+JVYGSmCRwhKe_fpM{)AKA$a4Wqj)9MgkXrH?T_7V+2#}{!WOl_#yj}sBqFEgcX=3nx|Bpy@$*OXh6V%j z0=-B3MH8)4D6`f5_eLTmO(HbiIhV7wjAF3;LY-qN-S%BqB{LW5F@&p(jY>U+my?&@ zxYaDC+6fSeTF(d%lB{w&!p;vHmb*nhf9~dv$qMfS#S1c!Y=s+7_gt){*-*L-jv`_+ z^GrpV2_jl2t|)n0a2DF3eX7L%E^oxz;uMjTFdu2Ab|^cg?Egaa82OX&4)~=oU{*S9SW;v$$9acDC#TkmiH(%T#VqkhCaO> zkaU4`x!i!|ebq(f&g(~A$%@jVCS8peje~Opx@P5!H^^Xe8(ubCpC-LtzYks*5a~b2 zS^83rI}&jdmy$dK(%b6@;V?g(>?kiOV1XWtlpH^|6KNWHJ@|b$JVut(iS4BwJ^BE7 z5-N;)Z}za|)wImYuyy@3gC2xjuW@Dve~p0<9F5XbgNv7%_86%L?vsf5Z4(h#61K-e zV&+b(ZzLR|O6tr(qsTCNW9a6>bJKu@l-Ar-tT%ygJ5Gi2laE}vb|9$onr3_l|6pqO zsFfPK){sySFSt-e{Z|GbI|`Vw1&D|RjW5I4He}fz64*8Qt=OcWj!xgX z`07+zXLtMnb|Fl*HJfVfzD!41SIv<1EPof9mE7-(W=~fhAn@c$#2Xzdk2|mPrM3${?N>!?qnA6jCjDq6zD+;T@(!(!Ru+E$mlsm3pQg-8|?V zAKKk|=<%StrZ}b(Beh3`>%b?!eFe_G9Q`iSbz0-h`q3}1nDW;v;wM@xbNX=6=SZ@B zR-MtX*o2If**wb^`JSlrD&)G^3T-Zjkp*m_v?wCTQCX_QVrkBB7GqaL-dgCuR|Y1d z)XF(CfiY^lRgV7o`$UKCL&~OsCEt&;IWlS%`qW5=vt%eYnuEC zXPHm#BV+>^H_4lnJslZU(Tnho><*6%!yrYBOlv8hpCc@wjSXdbW#fAz>^_87Au&Q$ zjTilRlw%Cvye?W>2Sy8&@EOM@B9J2~XBd`xlMkiLsN^CY9eO>xNcv{tM+b8pXsOYD zzY-W9m$$J<*soSd3mdXwq#`lEaFb0V&(|lf9S2^s+twa)b~e|nUw?4k9`z=eo~jSS zi7OXLy#~$eeh`rjP2csAr^|X8eG+BD(P*qs{)RN@vUkjxX>+<44Vhn+jO%JsT0~Tp z(I=MH{~P$)>j)f}Mv! zsff`p;xk2pXyYUI+>>BL`v8|)-nm{srfJ=^6XdS7qj!PYGg)f*Xp>8vHyY@6)9>D7 zzh_@5U22+iY2npRr0-d1%nNBF-PO2E)`nzF`hmZUCy8JqRBeu>#S-%58e7AIZX(O! zA2N5WkC4j#Mhje;dph^GQW7e1k}xPR)=I}Bq-~hsEqeW^@KgNnK2CLfw30tx8Tp4m zC8E7>2@y)_y{K8ZB!bOPyeZPA`pwI4Ntsah5!R7Z#@#Ad^pE^(e2y8jw2JT%4nGxkDa_|+1lg9< zWWIb#l6k!mA;XN?KG+OPkrj^co=6HgT{7F9Khx~$RtYUacEKrPYmK`F>zBJfc5f%F zh!(K(@wQ^_K<$xAu1jGYY$UKM)7*f48ngy^<)zg9>=)Rt`Ns~_fN7O`5NfirZx zk$gc}xz+mWqaf>|pBlShm|fIj+Gu5@=6u+RKp-s3#M0elA$(o`4R6u8A8LwBl)d#; zl?>d`^2a`ESir|fmBACc1F>bEk8&QRGNID8mrq8ZHBJ_LA;H|cn=ir(K5%+#`4o*zK;qdoe`UX3GRwLE)O zKjMJ|7e9_epZl{HJs^Yv$@tvfb_yzGW9hGqH{`^iQ8o%{koZ5ZD8dOhU0+OQrb z$xo?pV_*I72~x6h_K;HH-f1uUdA*Y&}ZLI#(lG=$)APF<#7N>@MgSWp|FJq za#*RhrLRT#30pkluoLx+;cnx|s8ygRz{snJYC{J1vqJjmljOJ4SqTtRE`W&y@2((BQ z2yjs{4(Zmk*)NcuHe3>GQP>uW1xniSP%&tETacAlKQ^|$jOuBU;r_KXjv75dBi^?`Hc=F5t43=5+(fmpUbAG3SyO#aQ4KO+c$ZQHn0#z;Ld1&jcYDZ*j!MG17&J3h- zxXPT9p(RB`$GT~%goi05yJ_l7t~>f8VEM9`B)M8xV6fjEnQ;7QlZ}9=dd9Aixmc4? zlv%A%>P${q80~G6b;-P*U9t)fvXyEX)t@g9iKCuJ$&2E&oMYoa;ql7)_y-TlP zE{_(04;_0Y{_}RYQ_$DVJ?t-H3)iKzI(G6sV{9IxIX>o)gR7&lIpb?p&w;CqLK$wY z4HvTM(Tw)1l3#Zi~ z4VZZ_&8U>#=UV!h%s{}pXu(GE=S-W|g)_Ua?#PV|S%!lQju!E+y_8OGym~Dv@|Kw$ z;K@X&>L1r0BHsrWU+bZ=1eD%voCUN>4E>em4ld=U$ zyn=7`o#t%CTZX7Uv!NiwIy?7pYo0WZS8H|N>!)IoTi6(Dd4a0{* z$Oiz6od8#2Uq5W(Th#X;dTGU;=ntAXQ6{6w2qhaq|B(fLzFe~0rz6fhFFtzn*S?ib zW|tZMK~Yy5<zMgcMzPIXBr$~+(Vefa*z(7% zu_(OoU)E%kuy1wyB-T9>n?3Eh>DtQi0dnA8feP3_F7RgV91bq_j`d*Sb5b}Q%gCob zC1;#OnV@@0$*rXLCLr`U_C*nJqf91Di~_tauG2I=s>P~w8`_2C&epaw7akZlM*QY6 zgF?}bh5Iq@O%~`)`x|5=Agk?81tSA8j#b=@MGF;m)}6vSY-L%AEav4MPV7VTpw7mR za^FYpA(|EBWZShy8>yV()EGyJ6FxIG9A>Rt$OG)j*7>xpExS&~j@>HxuLh)|j)^cJT&-0~bNzNXj`E}VA@*8P_wGQG#vHZ7 zVKQbqQVBwrfMU4uIU(KSy7yyGW;cZy(HJzJzr&Ok$izKN2m>)#<||j#o^virJ#bZ>i+;CQrYj(dk>uG&C5p}O0G*lB)1Kv=4zN~ zS{wB+G{du}!|t_wnWQ4%;)rBf3mV;3exdDeI!g6sSYl-QGYua;)*yLIG4UQ>);&6N zQ!mui5xjDlU9*yU_;tWY@tX8!26NuRdX@4LqNTgcZmGmyYX%)~bau$t%G{gMNe&I1 z8OaUO8+A3_W>2T5>UP&0sEca)n-Uy78L@7s)D|pkN>`~*A?U}0N)>jj`-I>TP|S3U{QuI67F z*zQqgNNpUdkPWrjV-o{Y7Qe6*Tk@lR1A!^lp7YL*w)8=wMKYP!_3IepBIF8J?ludc=(D` z9KTTe?za_zv!Au~5hG}Ls~Ua1JvLm}%NEE2WkWzVvPiQlXJPa@f1&Fo7{&k?VunA)ig;Rl-WOcetT6o8<83itI{v z^t?qI(dbV$sb!XScO5wHMBguTp#&3Hwjx|3(;=Xr78)06FyO*RosW>|@M^u~{@VW~ zR_ZF;^(57%=Q7HYUi;UMd+M&gsII?HcKvHq>5}YFP6UP+F4Yx;p>Cpwj%qotHpk;S zUaG@Rcei2CcA0P>lxPj+jgebFpBZn+aBYjQE4!>nJa;|XrkJhA7dVN`xX&b${n8=3 zpnlTDwk#rYbGC&f1tY72l?bvB$H}pOY1oHbDWZaP8A&BJWO?jFGj;tXkrETbwQ*96K&%P5 zELYgnebw-+S^F2~OpX2%VC6u7t*5Nor|xu7N_BD!#fPG31}%O*WQ~RHa8he_=6njZ zO;ieBL%4wfL8f_YcI1IbgQ;{N-qn3WEX-KFG|5+IvpH@{g0`3ByvA z?)JIoUP-qieFVn0^b-g8+sy&JuIVocJ$6-AM>6Gm(Otbi@}iTL{NrqB&FI;-C~2`1 zl+~qPXdv$pVV&+y#towH`znz>3)TlH7H6lseXpgOqSJk*3sLYRn(-i%!fbBTQO zQCcqS3iZIT2!g$U-P;SAZ)Zb8$n?~^53e6eOuN78nZy1m&|XQ%sbupDOWVyzl`{g` zE2PuNpo>@StBJ02^7*pEimk%iusNnGLRUGuN#yYQb27 z){m3aGzk{&i37r5gpjuAb-?gCwH{nQel&d~n&o9v7Wb&XF;wQxUy`-!Z!5{~a{mMrq}+ zCk=LWR#v??`&J-YptA>bPWC=Y5Iy2)zeIVU3qHEC}NCbeuox zqc@kye?+HIj{9@PR+IhL1s3i@m%u=&B9tUR-h@7&_bv7Ashuz?o8#$lxY}#bwBE(K zdgmhp!m`Dwsb0$3bfb)10s;E<2oGUi7sO>LMz&FxEK|w}SXvYFl~ZKO(W-pd`r6pV zx~y1e`^pm>a_M(J8lccFJ29i+`H~l(u`oZHkFPp)?_9AJYG1p5XppWG2=TtG&RxF5`{-Wy8utlM)D+h&Q;pDA zzY*R@ZUB1AQz0SNEHn?h&86#19{73{8mcwHp7Hzv4#Tl`+$c`pZu%6IUm%qewK$%SUl^X>)9vk9S z2L`FcH}nm?v6)T;SJ=(t6P5DIa}L>Y=qR6B45Ff(6=@SycU*#5uK20igUc|gzQb@< z1%HmNx%(6)lJ==T{Huc7ZY^xk9U!`WE&7T-6$GrL*w+R|X`!ecGAiQ1LxY!w-;6yx zX|Tncd`5A7Y0&oGuf&fn>~;rZ2MYwd*U-R(*?UhWI!q87P+J;#YCT z+$rYoXs$`F;2v=;tX8RJWCoM5#l+M@of5zEq=rLRucx?2kIgy*#sU{NRkz9dEMiD3 zYZ{0oo15}$1eR9e&VjC10hZB0N`6S3!tm`PpnpKi5I`040V{>WZRW9Z>yKb(Vhv)5 zY!rsP)D-*Oln@Ur0f^`Y{|A5|7&VlF+bP4+4)eRx*!dt5uOYmiWze-!+8|=>6Z(ei zd}CZq1sf3g>|2Z%N?3RwLT-LLd8};pC7NbBbL`wxp3IYBJNnq~@uWX0PbQ+5k}rmp zJ=1ByWl{g)pH^6BTzR4(WrwEUVe!Nn(E@tE1ZP7gIyuFN+wbq*{;7d2Q8UD1lJ_aUWNR5_ ztyiU1zazl;Mr2krWMC;rX}K*y|eyx&3sbg!}lovl|WWD zawMrRNIHvs@ct_zGS@ck_s~$C){~bOGOM32^aBb~y3i8^21g50kcz1VyRg_FdnbN^ zlNp?lB;F3w7If|3bdBHtetATll}#cMB%LT#iP*C7`jDBOa?;OI-bhQC_9Y8rk&^(T zygvHl%r>1wPj-}9|4DZsoR%3n{7o{7@$6&)Z3fRh*@v{%u7|*s5 za9TBvO{s_88{n;i~h^awzJ=c$K|_b$V0kPmcCa-WqKq?v07TwSq=?6fs@p1LG%;lN&gHr98vXI{SA zdLE|`?h1w$cVMB%#>l!Z10F{pZD9~EzAZkDJzM3&!Ehp<<>-r_L1N+erdG)C@qN`< zNphxfZ2~04J9&RJ$zW(VF<^8Ov1v0b1>r|B-($&dzv_4EyA$aCamL~BTzyX`b2ib? zdan})G~pSt3bhXy@~_Q7OxMsEGX!S0Ggw`twT+F5wXcgE@g^MW%HF=p<3cXx>8?8@ zxEB@-^JKyB{oXSTV7aNW6qHXeBSwD9l{C~ry8R#~nRP9G5GEkY8}kG0RkFEdD@4Ng zCiEBQnX3&FZ}d9$)k}xhUymvAnXuG1Gi5H@Tid1%FmX&~(gc!_-h!vty%uR~o9g0` zjDIzR<0bMaEY5uP6sc*ZI^9&Lj%HQ)=|fKmeKds`H964fT zJ}O_wikTy+P79t;GDx@!b)sxC(=^zx&OAcymBZbFrZ)9~`}iO=bX1RUhr0~aWE7J% z`-KR8v`-u12UDF-oYlwy8({bO;KLjz)A{G>e3x+Va7}t1q6{>f!IW=14ZYq<-vso# zV3;46m2J*T(2R2KHZ1V`mA_^_hOh36Bh83R|U8l(2#*I+~EnS8b6bE5at7H&U#d^J@6T=M~A%~5-n-+{p zsdqPX9(B9A3%c{U3vMj++ei*eX0WbZlN=At*77W$r%0Wk{e|lA{>lcwe;v{#rgTP8 z6>O;+Z)8l)iI6v~K9DZdOYCo8s}6L0h^FO?81dIqnZ0WrIYH7ndSX;irxwee#ozE7 ze$^XZpDkt^YAVz#{^Hi-`{Q`ORjDNYP*5lkccD|z_YF;NzS67?Da1`Bl9(&^n_H~l z^BLVLw)!dBdX|}83$@z!Pka+c0_A6kwtQ>*QljS@vIyI|HbUdJjtlCuMj4cKs_Jfy z)dvSB*xa6}2(hy^gsn7{hAUc14C`*6Ed`tdK|c?x?n3ND8%F;N(A?q7%6ycKnX0vT z?L}38D;S&`Y7G$2H2T?(;2sOoy1W|dV)Cs?B$RX|XKt^mBBUt=z90?x)Y5k*F>ZQ+ zf-g+FBj$YaCP3<2%=a@7U8m#C26M<%I6FGs5Fzvv;#=CimMdyuOXN<&RR6H>@O<79 zFipY|vYX_S*5)k`?KwmuPZIKA&U=Rj=4~dl{SO(x(WT=Gr03@Es#-r(+F^YnfXvu& zFIo^iy!s`GCLvSZKb1yzzuPRuV95HIf7Hcn`cNT_5@;!DN!*`pxz@?8oOi%)GTnbG z734o%>r7L;9iscMp>%@8hUT>Cghr39(&M{@oWdg(Jl1Nf7I43>^kr(OMuW?M8gpEc zQd*+OJu9T__0nk|A%iEK$4X>p8oM-6yKI}DpPfSUBP2c zx%0+S#6!KrFnsTrK_NX>I5!7pJv2BZd@#$AbR)z&uVH&zp4}F+11t=EC`* zM2#UQ5g2l7mjgLFj5wn~rT8bFxWmbA7*vw9Ikigs@Q@7n26Q;m%U*Eo57sWrJcuF6OTR$z1P7#j$ZAWO|0BJw`dj#;!vdX^i*2k zpHXj7N5$9FDE}9+?f2~&%ID+mYr1iQ@5ZGxa|w~$tGO>Lt2nVaT0%%ev6Qc-TtcuK zgOfEdDfX>qDb27B6gD{a>~pB!Nuwp~*_T=EIYmfWj6Jf4-C~^g(6kV`mvXWsQjY8A zit~W`pxU^0^Z1a+w`VwT1ACFly56)kqI-f1#O_C_P+RuRD#IBJg?~63EkDAWE(qyn zOm^&MHh+{hKeNcsvFzK6}a#`d!#`p6`iA8|I-|S^KHq(Yu z;eo54F_|+BwzrLJVfjLC6AI?F=k6&`GGeZXjMc^#!rf{@CquM(%_ds*@-b)p#P}#X z%sSJ87f$dKgRYpAVvq9}-#rS3E-`kDjfCB{XujcXlJai^cfkA|!8`kTF$h-T}!f~8C3S{ucnZVUhn320%Nj`o=&_* z4>nI<%U5;mXiF!msfJ`Rm>;(1GIkcPge|I#1{y6oOWo)ysmC%T`y3$rjWwSHo;N4D zBXtH;eAJAtLzFF_{Oq!2-OJkwYOkDkoHgle&R-Nd&oh%x$wldGH>`+ABfo!DQkS>kS9bJT3msN;C8cP5CH3rR$;CAl zNW>*U&{ee9f2 z^SQCPBG8H)u<2Upx&6FHL`o71-j2of&6Cy;ny6tbwR-2C-g)||X^3t{zrL_sz=gW} z@SMr`QTyI3h5nQ6{YzpRjMKHXAp$O>N?QDLpCa>z=(iTRyB@UI$@dV0wY5DK4?w;< zWEVLg6l){b$}<`+uf1S)h`l-%f1=dUdI>ExLG@>US#iOWUsh}<3J&c;I+UZC&qVb& z^n$yUX7lpGZmOtze!G!MupeZN@6r`R04Bk{ zBDwYsH$F>8SETOwH6_ceU#y3cOV-25{u=xOZ`-x3@dZgIrN@v1F2+q8SM{o%n(xl0 zV;W{%kQtflbq>cfAGY6H*0=0j_V8~x*3TD$^5-LXXH)QX7Zqog!idWFdNncH%IjbE z;C(zhvRDK6a;6X2>RJX5^F;lMt0p3q+$^i-(`qgHoxMhswfo&->$xrU6$$bHaERy4 zWKnA}`YHI(p~i4(N(icCtgDQAU>#Z8edlpsxG?_sI%xdFi4xwE^Ha@Wqfiq|<=FBL z|D0~R>dAMvy<_m%;90D~m!aZzbME3>K{vO)Ei`uH*Oe%7mrQM}ffTAVz~!oevdPp2==LD383*!*0sd@_tU zwPXv;4BoJKhgO}+7ZO$q2KP-M+w^~3#TfqYnAWo80BuBSoMoS5qTSnLhS)jGsG}X27-z>LQOV;y zfFpF}^aw;9F!wh^9cZs!Yl_~LK!GkGX?*5Yda;1R_UU9%57)bWkE^_ER28+bR_U#U zN=euQ3|=-8uHcT~O4h_1RZ5hGUb!2@v^7&ik;HAU@&b8CgYD9(1z5N;90qypHi6h0gKz^}j+Wblxf;KP=PIRlfmI}dRldfYlVIL=xyjiL>ca-s)ERlx5$i*#NZ$l!>8r-}8KBZ#ZM(yXPTLrLT_K zw;%1P4IIALbrI*(E;-7t`@T0QZ9cf(1cVcM95!SsIEv2VJ@9xCBEuh?c4Xi=O*oGS z&wK-Yc%52(%$M2oUA{cy_5b+h$w$X2gCN(K&xoxgn=~Rpoo_s?h+&Gmd6pm!7AL_| zBv8_7wP*097en-!N@P}*66MC)9RFfO*Z;wY77g#?YK6!RtV)?GueEa^BotLd{VCMV z?|fBd`1R(;f<9eXseK{+=^RO}t?;}&>))U%TgIcOlS1})gN$fwK3A^3 z#zW)0oShu; zOvUk+?(=Uz8(uklqG2u7vMady`3J{{7IYwo(#z?2dYl};xP&9J|F|vfpW036{Yv95 z|DVXPh9C|`QL4nj4Bc_PEjOIo6gO>1-&@tHtPeYBs-o@_d`D*9&o%jVPxq-8&(`hc z>yh~li3UIIZ+_j`(O;kO{WKseC}d0w69V@n6-EB&i;fWvDoCMGYXjlpt{}(r)Rdv{ z>`zRj{fG*MZbt*D3mnH#{(0x+h)p(2i9bL2lJ4t_b_Yr{B>zAozZ53<3ABs?@aaH#Pil@K_XtckzH^zAR^C^u=qhdqFOMkC zt6~700?e0Q|0X6~c)y(_*SOBdR-x@iqXk{={XS5poHPvd?b9K)29FfahgV8`=!o}C z)l>D~JH>j*?f!AKHBKF#ajTUzq~0c~a_;N%z>tQouj`Wbrj zm=OCr(8>0k>wmuaDn3()H~Ikm;uAysV$!Pw5JQ#bp=9 zc9*M!l&E~ByZK;3ryUH9A~NkyR^3tG|1k@&jB6t)Bznrf?P$0fV;#9rU7lpe^4qRR z(D++#hqUxCkU=-R_w4PLCF8BSn$ZVjB-Q@@hGHe~L?_oARLf@wuXgkUbv`evFv)&l z*ZuX%&sZ#Xujc?e$xsA^o#9{3?Pb(Q(A`M(suj8R^GD54*WiU{5a?&b1PVc}FSH+g z0HR3kU^+G)t`iU%30h6mDSr|)Vf!C;Lk;@5=6!wFfEp?rt6npIIBA_~Jx$GvSMk|v z815(Ei>hb?zJK(&#=XDn@k0Ge~(s(v~!bY)h1n>gWDf$Z;l#nH8i zeSmndKTxa`kZIC(n&%-)a0AtX-ABXeX%Y4Rlq4>%T5NZsvzrJjLiffs0BfcHcAor6 zQ(wJtzpd&N6L`^Pm?qt(fY`>ts>Gxw_LP!DTzLBg58!%6J{ zUh?oiPR2hU>$65XOzhQ=oeku6N(_4AEeyUye{Ba)`kb!GxXZtV=6@GH!|C&V%q;L| z0YzQ<{$(5I>lYU?G0|CRWLf9~P1ELKe@?nL#yIU&%v8(Ww@fN!8RGOE{hV*}vn=}o zQx5YlLG0g;O$`$_I=wtM0`{Uf1Psko8rmhY$6b7ZJkLlR^!}dbKda_d1lcj8f0S4z zQ6RPs-Tp@1lRUXELjw^izH4(u0PkpQr_rgAZG(;wVy%bR*j5PzYzKbTvs1ln;P(AIX{mz@M$oO?9XVtf>N=mXz% zYc>93TM{m}#nmLfVnT*GD>=n_xhvupkb%Rw^s%bBYcG%g`?&?QjR_c={O~D2m;)@v zO#RQ#o?(Jq)oO!#!~L&){@GgVM0tpue)@xz{nE1^o6_$y`P<>p*f~3JBdj1TFfv2-`@3$5Q_y8~5YBFi)%;0X%QYjK5|2sp6+ zJs~4UCtMn7y=Nfsa`pxIfACJ5WC3T%P~ft-j0MT78$-T=xd;D;}gh zKh?9G>#}V5za1myUrQi_s$>Y#xgRge8WbeB8s{2fTfXJq5dQn2|MASyY?Lvmyo5PQ z>u0=&+q__*vC&zxctz*~*eT`_|6%6bmu8OKV6tIL5w8PgUQ+mfG4oJy`qTN`>z8BY zdTt|7+M4g*SZJDM@JJqQCy=Z)l)U)NrI=~3p^}Nz<$JR}oRfR~ z|Fxh5ah}fpuZQ*#P5*}KzrOm?U8{j|RGUt1cFIGgOko}%=*uQ!p=Vi^lLM}AN~#(e z|057cWk=wPYoaD}cDmO4-<8jB+$NuDV@6+rPjoX&*n3cFU&gcFFCT42Y6<%?eVWyZ z0{r5_#G0R`Nv9V3Hwe8cc5cwM+v~eUpf>Z3(jBKY`60=QLUTs z-%tJX9}px>2h%AS6qAVs9oLC@r(SP`%9`<(?%79lRF|L+9JrW2`g_t}W?WjkXFz;g zqEvROp(#H!;lDqmw~w-8&tEJJiLnj47s9JHuLMqf5Jx0q18Zjp}s4#J{HV&-aOw(Q`U%gmc<11y${bV|A>HD;ezBC#fc8H$B+q z{m(PVEDHjAZ`F9r47*R;#o3NPXa8fM$E|lf*>--<5sas`8ee;2&Xbg7=@S3H`+?&7 zJ}-|ym|%i%@?+W_qFTeywO7`(Ryq9cSrmVT*#C3{w2yIo1nk#<^gdaW1zp`u)Z%=xRl z>jQVUT8y#y@Y)7DiKA+|iI4R@^O> z%^c<0WHI=>nJ@4dBHvu4d=;BwA)zTDs5 z1RA?dRrR>!EF)cSbyFBl!JW`-Au;?qJV6s|kSA9Z23w9)pp+pCj1)+y?AzwA|L!Qn zlIbp{{D8&I{cRf3wY#0LCPJgUMBnkr`m(R|<^Mg6TaNM*bHtJCvcJsubtGgoAc$F|_TPl)%Of##^s+rRfzOm%8vVvo^SWmo+R{JN z&xi&TwdlZ|Fh^Da5lPMRq0?L5=!#`aP!ZQ(Kltx%0vt>55s%Su{PwJmLKDa>Nx1G1 zb8`aqiPDUiGAaSEsUBO86T$V%vFZzgmwr>C>?v|=xS;fb6}r62u5EtzZ_ei)b)V{` zX$vrw2=DJlGgpthlN!h(o&F5(CI@I!wYCh;p3?pM6M>S3*!^S=uN49aN$k`gsQ}9G zCuyg7xGN|VSb6!!;KnLFYvt?_)%9Or?%aq3wzw}Ig_X@ca&wumuwriOjXm3cQ`UCb zWUh;;9F*Ans)5YjJEYGOaqCrgbyU5I>?2+&-sN+&QEJxx@XvYFUyrFw9trvmj7Lbf z(Vw?Zg2emr+qVrXL;wa=X@fYTNIRv6y+1vaNel=hCMT=8>u{lDHc(|1m!H)t%KWCb zAjHbTajnep(Dj2Mdly=S}QL4VP-~_hn+gp6icC(1LZm zKhXPxd`VfeCq8%{h5z0spih#b(NLcPHgi(|i!DQiv34CB!u3?Hs3^qz++WMWbRR2S z_THJFfs+L4~_V_yF{C?oLxa=d!AN`2feUT`< zXX|KRn!8#e9h;#kDO_G~;6EJG!)1{DDEfvkRMQ3tFVyJc{w%wHB#?-E>cd4XyXRWj zfegQ-eqHodQ+IUMUURzcG z;Z*uJZ%wRLQ0Qt@s8(L)KHFbSk6}Gmp|+s`d(qOV+r&FOS2xk$l#@(>?ZB$(65O9Z zQzSVXHGuR=-EKLd2HL$C-%#xdmlWFjB&nd)hc70fd{`)~JI+WXk)c+57!%~Z!6Ism zhw|v%l|&PF_i&3>H);Kb6DFa6nE@+~`>;$h@MSj7Rs4aw?Ek-R3Qn$H&;)tXgy9Oo zWjLjD+U>}co9x~N42JWV^{nqZe|@wcrHEnhA9GNEruPI4e_o7g+-(xwAPaJO4TwzU zEH@UbzFQ84UQBX-u@s(_N-!sg1QBkEIGyDu81iM9AeUT>)nXcYrQ5T4UYeIQ8qF6qg?(Ik z!b~jZ!#v)zP#M;bTG10BH4V=H26+Eicu^B`^dn{<4!>{M2K~ z@Wt7h$8#Hx_Wd}T37G+U3y zPy5}o*0jXoxPPS_r@WfdQ0lLT+Bc4h855)5HI2!Ptz3Oy8k?PC$u50C$O9yVboswI z0))>MWI%-B$ikrAzj}=SxOQ+~&m}%1cg2U=qsGQzx+E&_U9N_2pG|1oM&nnDr+Kyc zd@6G5o=Z({dlB3rcS+{ckIr157|JK#VDSL_Sl@2mPjbjP){{?TLX)d8ADH}fjQ_Gj zeL9e#nbIjqo&4@V9wI(ur&|~2xxgh0a{z39@yVfaRhNOI8NSVolX(-mi81=o2@tJ@ zNKaK;E$5jxVBS2`oIyM&KrD$XRj~1w@v%vLn|374i7J(E=@zj8O}8m9At}5`zEalS z3m~5FV0*6|zEbOP&^yk^at_bZ!8`_N_a;wN>_*@% zSddx}ez_hizcN4?&*}saV|jOk(cE&GP;GF(e#^T+t+pZ=D?Dik(U}B#0iHkr>GA$b zKLx;jXRVaT&qYKULN58)voac)g^Y)z&m$QT zL%|exBEt)n_XN;xX&jrs-u^x}cri>gNgpt+e^9iu9nti8n!c4aOpodI6^Dy1{?Xj6 z*&$ym`J=T8r%dxbIv!8{sVM0wb^S$OnnZlBB(JY08#5Q1woSn<`xFg1%FD21d(eZ% zK!_N~oiGdS<>VF+ngVc9>Y+il@8yx}6$e0rGccb;so5ywbHCS5^S1;fu-?k0x@??x zA%#E>7<2fIQNCbo1n4yi#Tw@0bC{}DA=>a|1_OlgS_#&}sJBR+%tyhTgvfZn?Hfs#e-gZ}X+4dP|M)_-9>Y0>7zHvBqmnthrG2phkL zSsS3uob=c>%83(4Ozrz?oRimqHu~72DhmQ3dA!Kv8e?PyI{@oe;`jPTRANfZn<3(! zU_Y#M8W;IEO4U-*4(M2|jvM|sQ7`*+ z3gFJJ(3o0X>I&>JmWzprUBr4Ac2lmjl6AIpW;z!Czv6cgIT8vO?M6BS@TIP*Q z*7I*yyE-#Z9yOM<$8=a-XcwY4*@~mf6ckjRAL-F}?`=6%Pjjn`T89la#hjUH`%^u~ z;8`!iM=cLe0P8=Mv<37!q)CTha)b^o$HWgJU}-gbXjGq$5)0I{J|lKPeE=IxX@_X} z)t*UP;mv2zU0RU7f13`KW3j9-{w`89xdJcES|K-uGFoCn=%36#6by+{X`il?9Z#n(C1cKQGqa?|K zpe~NYGH3kp2FKM3C;B9p)9;UMP1W8@edPy?gtOo;@vGecG>_=bg(7GB9*s_IvJ+zA zs^lw(TbTuV4N}tbUuz=C^e=%P8q&vnX7VO1=Q_IlSPxzD$7flifa_e>4!Eo)AWiWO z!ehI6ABq`BR{91k%@pcvQT0@IG1iJe*gVF;7)v04RDfTMd`3I$InqZjoj{81F}l2k`0gWc8NdWn04n!O$w0aEW`vR(B z?lO4g-4kM2mJpMa;c)@~sgFM?WFn{-sOSv&i#Y?vS{-rLB36TeAw?C2^Y5~*QWJx7 zBLLM$A$AA7$!7$0df;~SWLsuOrK@~jyzKdUuyPNW#4WxU;Q!4v{COWgXEb5Ixk=XR z?ub(YzlbpHfLwk8(7HEOOqj+6swzJhVu`vg#L}pn7n{07zvawuzBP|MXMdigMb2`z z-@WWN==7Prsj;{=Kkk|O@GdY;d~-yKiPT;c|czqCyxE?I>{gBlLD zCRsMmx|D z>PBYS42A5Blf4(+_Ist$*Uc6Cbg=Gf)YYq_6xXsQFQ21IscJQPH+7%nLO|A2iKeH6 z#6QN}xyHL8{&2%#ty7IwwC^0ib-aB@9B9QF61c4m@j)AlntN%RwiAKSDp4j&T?1;Z zcoKU6ki=gg9$$%o*bSIZLCzkqiZmYqHprW)OcYN2yj$#>ed3#l43Yqy$~HJpC0&T%QFm63fgeps!B!ezcLdUhlIP6aRZ~Y}5u)9ga`8{#F$zb< zi^}DIq#m!>)&Xm?1F-=zmIXJ^(me#g4*KhNPR8Eg0Xa^`x61blm0Vym-AY= zm2M9(L~S>K{l+Ey)glQ;G=O)j448g&Wqy_oU~F!U@a|lw#Km)0XRcRF1u}l)p$7Ej zs?X*2@=lVx_*_^DgcQ4cdyH>&;G~~yCI#Phu3^&a=uE_=*B5&x{;8jJtuIBG^ZTc; zMsVTxIRrpxm_@8Rz4^Lc1M&)Xzo!^{`+a%loVERMOy=0EY~4T{R;&A--YA4&-k-NU z&={deuSCK*Ce4MPJuTB|E|5Hfo4H_P^1lN*tkzXvdY<}%J*FoT;n_EVm|_()gh+D* z?0y-d)wBP=5s561C7D-+@v) z?!h^3733;E5GoCpf`LTN3k=SAH{Td?Ig_}aP0X~OL4z=&QF(nk1FG__?N!af?k`KCh?Hj4vvNpIn`;6^f@6MEE!oUvqwE3{+=& zE^p3WBG`I~rcOeo89Odzj`MfdnfH3>4~0WCax69=cPXB6%qNZl%>a|)1-c`FwlYUg zM;w`Ss9$wvbtN2=agT^ro44zm!BP@SMM3#9@IzA^M8`}jiPsot@E*bN_|MwAT%0X6 zB28Q;6jz)g3kJVmUCdk!O(4`AcTT$art6)3IA`je&(;T*AbWekIs%JtD+1|SIa1-J z>&wcKKYIax*~zbe3Q{2^8K{2T-oG)bG9*MN*cFmXW$5x9raXHrz&k`n;mYOQ_PFTC z4}BP~RBLx#RP$q$3b+Y6OutK@A3QLf-t& zr_CtcvH}w=5fLjqVPV)SE5)2d?h zdqrre4ObCqNgijPxJGosxeF|&3%Oj%6f%Lqq3)B(OIOj>0 zopkQdulNQKbgLjBBh;MrMM>2{RFPLXAUS59EOG)AGS~Y{eHqVyhEy>(Atchb%ve#P z@0o}i`I;7DLbZTLUmmaCcOPT>fK()Q(n{6m)7!)zOrQIf5&^iIN0L3*7iVDqFd&!i z48YQfTGpVQfea%k77TKE%+c{+9s($VC!3)5-vflomO%avOGIIdE5d4>d_N>|IYtSH$Tw0 zwmgSHU=zEIu+XmKsYD?D$IM-sp0d{h*b@!>sbs$!3zq3&cVPP_^pK{D-Cd4>ewz-E zSP-#f)V7_nR{F+aHvBT-!@ARRE(X!J#{DrJNqCHoLRJslE$Nnx?%ewSRXO~h)dCp6 zpu4b%z=Ipusm|9kxP@XDSF(#8M~#&S@?~mkzqmB;X_pqkYjW9NYpnV`_0UDGJ=Gwu2G+_V@Fk$8q?K75h zLDUapW#J_q*)@pXdz{&hevX4LK5$a;zoNvBwZ7z(0Zoyg$4 z0D?G87%tl%IIr@}oay6%@s4e%(+1i_ws$=q2N~GASTmQsxLM!`bl9E8pMga`VDXG( z(Ue$OV~D`8UDSJ6UjV#f5`&Wb zg2`FM;rFy)e!{n*U6{C}puFGV`r^dweH?YRfG9&^5UYNs-zpa=ImSZ{tMv(6U* z+0i>~6Q;EcWP?3==S!Ro!DTYqM}h3)v@@m*HWQ`2^&#K?VBv^!WqR3DaPH8G7VCJl z8S{M`$fgp2#)@44gu+{Q7+rERQthx&Pto#Dj}m%Vay8*g>u0|SU5FIReTfXTPpn>y zaQzx5{>aHhl5e5{FLo-LU6xhNc01Ph4=12MC|)JGlMkVh0-7-Ogm}J#(v)g>_(E3C zqIcWGCB#PUKq21p5=1B!*2M+7tCL>Wo6SLgZd$j*sOLE&Xv;<0KkdwoA||PtUO0+x zt;>Mnk^e!F)nZz`7-_7%SVg|q<~b-53u2Yy^#+qvAy?2qmn)OrH}XFM`N@$Y zSVTh%Wvit~B#1i6;ArPZB937-C@1a(ejSe(p#W$U&2x7?TiC472Rr4h+DXobsI6VY zi*cuC3=G7nt_5j$l6Y9G6Bf_HO}p6BKU{-k4g|~)^}2!dE#0K2I*7ItH-Re#Q zjZhD#B-7o6j-Bb!8O#a04D#+?Jzk6zD6>vyxbc(J9Ev)#e8qxfu0U~qX4da8vRE)8 zP9Z~3CjR!z{TwvxdV0lGes@=JLpPFPJ50&As6HA8 z(eyb>I(qTpQxs|WD|{~P81t$xtyqfp0FyvVK(&YsG@9Yb9;h5h*(c4LqCaTefKS#! zwR~6nCCom%UE3i=lFXw+hV#chZdCGwE%@`2MXMR4N0jsfWm5I~^?M0hA{NGPnis%z z;`0Gv`C?=0)IcSO@J_Sg@}RH@$izXGPdC0pwA-o*zZBTcD4O zp4vn{@9TDAq%Aqo z|4678@Fq+}3h;)#=C!a2eGxf??0gzD5&8w>`hJj!Bp`%%^P{gOr+#0u zM|5i-5gudRcB64kg3UOcSg(4SzMKs)iz@vl1vc9Zh;ciW?#T_{jWIJM+$V84o^?po zLT-M~y6s_uMe#l(4T%_&AMnxN)H;q(Qs)v|-2??6uMM;7UC<=?4Agd5iJ9%Z?I(Z; zY!eXbomJ9Vu4b~oS(G#eoYuxMPsw5#T`#p~7cYG`rfgFKm}gex3_TA6bXw%|+HWw@ zLZV~JB!#R}pgotz={@vMt+JcL0X5gp_xaC%NmfKuo^s$8`?P{^OEW!Qfo^ zZOm;|a;d)L-xyUEAJWD|W$+xCh$H~9_lVq8Wu(|t=Fh8|yKPZ@XgT?w%do=*aM!c! zk1AGPbUSoCvPy-^2v3bYPmaVhj&00+pt1%zs; zBmi+*#I{Y1W7nM+=$~-WxD}Kkp?@vZR^$T^M<+kxQM3ZF;ndW76lnD zX+RUP_dJfBK^5>#9eBc_x`-Ae0KS=dW6N-Uq3Pv9D8hgYJ^=5M&i(XGR?iEnFzz})VqleH~gh}*3R}en2?WhL<^)0BH^K(f3rrJyFdiN@? z_<*+(+Md`;EfbHQcK?huO2ftegX5SP6YJKN=GWagO*MJd=P&gD4-`t*d1AF58fuQS z-nE`$L~(?`NRzdj!r2`UC%aj;y^JAysIgnHN=EUz-|vC77> zQf=p}AA#{)y_)J0zu`#G*70FXJd4IrFl%0uv9oIX`Rqi`%mG-3WM#o(SGX<3elU|7UGDE)5V(H6$wG-@$EaEoW>vC9H1Tu+M zd!{D{0r@6LnuL2J6Qm;D(Dv{6!H^pSUmkn6n*rZ#Q4f4xsV>~_qdnK`U7mGJm(ziy zR;%6w9*%WTcUq97Pf{VaI^)in|?ur95VXR&V zH3wFu8}=ueQ`U4Ix#_3q;4;UGL??Cpl%Li+OAAg+TQEd;VAQn8D3ci zEs?nBv5JZEBZVcCGOASO30{ROv*|kfg8~=nAkg_`W-%|QJ1XC92$s!i^YyY!WnK2` z`O)#4_;!CWkCswjOkZbix(P#ocfH~25?$uQ3ib|-cabT8Oj^{3B#$LIf=mKfNW}Hm zl;42AYB=BRnxzpqk?gMYeLMej31iXcdZSQt6zKfuovSVQt*A-#t=m74Q3pxV`b$yS z69z24UX37usXJZG9b@Q_=;VLH?F;?m zPZr)d{*h---mpUaK|3Ix-rk0@+pKQ3wJzb;VV!GuDlVO=A+eG-pq5N)E8F^nQN_6l509Vy2 z9-AAEuo3>pas<0*`sB~2a?oNe>@sB%AQ{i&AU+rKUu{4OjN+|2U0#V)66Yg}u7=5n z?*BF!s2c=E1D>3bSx{X=*DxEfQU+Dnkl`Yd zi>EYj@q_2L34vtWi8*K!w5r)m+b?f}%DDMNTU(iD30UZT7ceJk6HS$m3mL9(?B4{m zUAUN9kw!CT(?^Y9vKRl|?kmS6h>|;x!45iOX1q(1B?T+?;;`p5|2X5^lBhp!i`yMU zzvuSt4qvjM5CRndnt1ne5)6J@7b|vsY_B)K5mgIup~_K^7o`=&zdvU`4vdBjoq&#XyB`B&{gn1woJoc9y)0M2Eau*W6lYd%4`8F^ zB{LM@2`L8@bz5@?^9Ddi1C}SZXPf5t;GQkeNl57K#F0)FND`ldo1X(PeL)|j%@nyb zF)xt*^VBDeYJTy!;eJsFJAA`q?NsmubDsBoNb+?rf?mEvE*lw@-f-)|mUl&z0=nTJ z^heGQ!&=Wu;)Ad{<_A|T<;~-%4XHbK0ebVvh_n7%$Kv5ZP^(8F!&}se)3M!KA^ICG z1^u&XX*OzUe{CoIY6iKXl(`_4FKsEl>f;5_-tGPcgRvD=`usr(3N3XmmUZ(Z;~Q3M z4GRxb{%w+F&yk=JOFaom5RTq%=whVkqE2|?wd-f1GeuPL1{*GnBi#tdLR;O%JDxoT z$-eV%A27TD&65D`#@`7C;26j=0C4Qc6sxOKt*R|A4CCVIvS()q+>tGl?Lt8-A|52QT0CR#oQq=RQ_6UObM$B>wEOxQ< zALxo)E=0}86eZDCiqy7XDw1Qi83Old$kpKpsQ5e+9*-eWL~VTLnb0r$N*&5fzS~%vCCU3`)OtUXVvNIh^s8ULqKK5L`Lz zsN`17)t51*ME3mK6W<8QxR1p2JP@spW9XRivJ(m%!cAk6nPAf?rm8ow(w|nEx(Fud zbnn-O8LS%+)ej;U9cx;Aczq;shC%F}+HT|f@iK!kXA^Hd__bU2G4Z~C)@)FiNHpu* z3)AZW&6{^^ADs*?D$06jTpXp7WU+~r$o(wKgH)iE$Z#(dgIbiAk$=os=fiv1p5{*h zSf=5BVVQj)g89d_uOO`0?tn5tpqusUBPf-4J1+nRKVuCnK$raI%EI3dLNJ%+vFl~y zcE9*QN#ha#(djfpb9K^g4s*Lz%q`0+YqmNWL0BDy#yA>I;P}r6I+a(;=$gA142I8p z8^7T3-^X>T;bikiBz|IMWe%fu$DNvQw{YIYc9i0IT@eYe1UT+|*j{bs%09DJsaL2v z2`7G}eC{(5 z{!DBn-tKd=9yDWnfs6BHfnX#Z={O^)_e(S9OB_2KVfR><$9;~F+m8CnV1V`51v>9b z@>~zSR8?o&LsvSEeR0ehWDmMCZGZCL$i5jbvj+3L-92^3jfhA}>xAWm*kqhMr^7M4 zCy?mnVS34TB5;E?Y5#N8El->`pxf}2%o-m;`$16p$SS|xq-Caj_ct~Yk|bz#T8o3n zs(^aDS&RM7*EhO&-0(s;i62%HQE(!spZjmG2Ng7;a{I$zIFBm^>aB1cJ!7;l<%2X~ zYc1C(K|9n%ucXyCI0jZKFx z@;1cewfU{UPw-cTpaW1!aZpOHvTa#rnpZL`wIa_X%To6~Ak1)fTCk z1_&FID9%M*fNppjFV6eDW$6k?TDk?N>Ng!r7k+(^B8&n6P#AaTC@D9m^=dd+A60O^dM$ch;{rVdTnkJ<>@J<|S?dXx{jN3}#x(L0=FZbU zCV@D;T3~g{$u9U|&`RdFB(v8}H*2?SYBMeZug&;W^_a`pt)UehfQe-c$*Q&Zw+$lo zhJ>59V=uyDS@h~h?zp4GStLb=mV6PuALv(Wv$0ZsX-*^Gci%KMFBln#0<+MjfJ8^V z#P%S%n(7)B+5djECko1p)oS9fnoU#)+fO!{($+%fGpwmzh7j% zn4He10S}%{8ltR!9g5x&OUijg-?nhP7n?v7?4g@UBjeyugXR7P=E}|B@6;8^pn#;@ ze765R%`+7$&h2!0rfcugs-bDH^kwZ$`>W;Q+tO+&FOha0)P(_0s)U7w6)+QQ$g?|q z*W%kp3h__IVz%{T27_YA!|5~)lvXq*kSA@LvJL5${iuK8Z0Z}Xl0m#wZd`Up0G$v zY;4YGS^-f0roCoUG(U16W`u?-)B3$kAiCCMqJqwp)ApfKf$_OOPEM7!(k+q<32AAC zi)+%4eK1c!61%H>Z>AV?W&VTJP4$jF`9HP*Y(94nW5gWrOAZ~Z2f82d;I)BdE~lPp z-QL3F8+Y@qWmm7;I4}88Joc84E^Aqru37n<*E|D!U3L5~G|e!qd@8mtyehU|cunQj zG0tn>S?L0jR5*C5NoNE^@isu@oK=5ut*gk>@&khCXx|9Tt#qR8k5e{p+g-Rb*lsJuVq22%m4q zR^zMXDq2fE%UdbdpSyqED)bN3kpKN!&_%-BUZ1QMXE&31)s*)#yY4|wE<{1N$KQr= zN%U58xgD0p#TSd_2jr!u5T8mw`kXdD`bGdVrn zfQQr`hJ3ieL%TL&ChswF8>drJULGF(EktQU^_(o_`rJKMBSNl*DaRGhXR)T4csSw@H+K)@#%+196nB4K9C33_0UHdinjoG$WR4Yy8i&{P~iVIow zt-KCFCKn@9+)(BFR%f`ix??5%?>nc&yzj0*TKfPk_5i!i?)h7ogJf$imh?@IH<9GO z`%3{TRtrUfy)#=OlM&dxuzs(ifCCt48|L2&5ZuMZ$nE!F5b18;??0N7rG2?q`O7n`DUKHZr-%mN%vT zbcKnJ@kE7!y{>5=S&Y`wx8)eBf8RElEVkQDtuEYQF}vDuNF@7BxnC#rI`&M$)Ft6InH+8@9HvJ@pnn2mz7@#2exvsN_tg547ZzGtsrzdqvW&3(uSJzj zxBvPLjnaQK>U!?)J71X|GZY_I%q86KjdqC)iDU;Xu%7p##$YIVTkHXm$_ z#AUK<$X{m5hWeXF(I`YFe`xx`V<{luj&g-@1ff#?o1t{eMX6bQinV{m62yy`M+Stz+6=l|a$490RxwrTQaDAALx34drE z$*Lc69|x<XPds zn385ho+OrnSz=i7zot=%`dN7I^dK(%t-Ht9x*H@J444qIGOMX#`M&j@503x1iZx_f z{h$K^tt$yKf5EbK)hY4~wdWoxzdq#OH|;Kgg^~#)0ysAY@Wy896@XthsXMF;TIh;m z3Z2)U`t>t^#2EYVKRnBghK)n6-?}Rm@z7U3|IH#aBspO%D8fV6mSmFRr>}8;O(Q5| zmDh`TCXS?xoT{_DifgcCh&B1&@FNl`$ho1`=Ap~m0$$^7uw@#+YO1OMemp@3bBXDH zKQ_3Gz7&cQ%f&YxNdgUJ#$%N0>ahmP4vBB?u{u7{y#33WG(Wj^JNBKu%_s-fHYKJ3 zk5Z+Cqf*%K-YFq+qs|sLF6-84v5LGxCEZpxIuW(_y8z?gq)frsuVD_b-LGJ0>{YgE zwft*?zMg|-Nug32?2Lcd;v?+jWUfd(A7_edTgc=F|1SsNFUYwL4q$1qOQ~yTp5~Lw zg*Kg1!TYk-cAJ0aYi`K9kKLWm?skrsucf~(PW@VD&{Px(F2Fa;Lsypi*@J&v05!OP zN4RrnS<O7V64s9yR&W|E1 zcgD9~>vq%lYWj>Fel!cucZoR8PY#U3&eVc22sk$oz1R8@D^T zOk$VYp#w)7Vb*K%+W9+QY>pCP^4VH9ns}5_SpnK-JZYv!I4_!lSSJmW+dK!}?}-9+u!?bxFY9x{;Lg#!4ibdz_7T!Ix8 zvyJy8ua516xR^0#oJTIj$)4cJvAWLN(O`=i0e84cMf=XXwsfs@Kct{Ll$cD&@$t13 zq>ifUamN{iU(@k4D3e^C!0)Tuc<8$7z@<{}qgr>%Z8FCF^n>kG$Zz-SISwc)`}e;O zv8<*p4dsq>fxR6nuIh?#dZpHU0ryQ{x6iu`9Vrr-H25P3EG~qQ=D;qT#RNIJ*=-?i ze(s?B11nG^Ro*Q#9O^Vl;`ygv?g7thJs!s@)QviX2qcK?AmE8?PI4>LbS6+>xV)@A zTK~8Y`!1gW~sKy<%SUC(}3LdWPzfUINSiPnR1!=Z=rus3X6?^lXIWufp5MlkEwRoi}R3-)eFr`PlKLaQanZlZ8<*)F@DB}`7%Hhja8lMNzRJnR$x^Kh*Uk5huUGEd<)KYhzwoM2{G zExkpK^*yjc6=ko{TShI)TW>T%GFD}g`!YL}b|~l7-E6+`>iQj_@VviL_t9ZMCMIKx-Ssdn|`ee3)E!HvO_XS=cd#k7aM;-QoCQMy7Nw z`!dn#2VqP5sGcVq7j%b@CWA5ECu#K)nXMuxT}JpkW@HC!bqv(VJ^SR!WeTo~<1l0Y zJi~76zt1o{{J95F2QC94YNAAd*l3x3X&K)ZIhXzFC&SJ4NQQ;)G^A0u@Hm1(T*kR! zuZ;=Yb`Mg#hfGbCGsC?b__if9FY*=Dz9@LyW7X^R})Xe&4kM-V2;Bq=H8kFdCf}`GmSHFF_*N53? zvB?c#ZNvI%9>5jZ+Eo8D0yks0zqV)V|2~kZvoqK5$!=#3RGAsX*pEbkL3GIVt;eAU z|NQ0D>MMU1AC#TYiAW2+ zVYdEcA?K45AG0R%PyCh((DUl|F?nxbZ`e)7wdyjeN?5ZrWErcEpVGa|3TX>rHya}d zL(f0`7Mzf6&|>|ne0?jA`yq|g+2OL}nxdX#cqR4QS?BK>UD^k+3(i9t6}uXJzCs|5wlTR6$OQKDXmmiBb#~`cWSQ+;U4}5K6)H+zoE?0uhne;JydtO- zSGn)0#2TP8l*1I|giD?Bsdkto; zS!!xT`8sHr`@K_JP(q1gFG;z7@!MyIZkMx8;u4ReDG80=qby9evHIJuv>$E%Mzi_K{ zgi?Jv+|<2X(^EdZ zXZJPDUIlx_@M}HzAbOsh&UpO5oAGi!rF{BazW{-?;qMv-j$!G6ltc}+$k-nJSjTTZfWB#9F-cWxzS)SHWBJH|x{(HR;thdE z;bmnehH%_d4>YDKL6{Clw)zt9-`PMxdGt&b2cLLrN0{FkXi7%(AQ_zx7a5H(&L2Gz z-qOso)q>T?$cM5T4T~g*4?7(m+oHLIfLvT^yuw7j<|SRWVIGf$-RupqlW|k!6uhc~ zpleT#;_LzO*~NL`RL}?qD>)cIvkf$IanzUL$rN5MRS5WXw|wqZm9|$W%Vk|NZLc5O zT%4|vw$|vpZ_KZp*?6dsQgZ5aaKNEyuzz+#poWmuz+gCct?jVxZdBE!>S>fX5A19@ zW7oFk{9)8=$~fC$OO$G>krVGKtxB<9mtAL1TAj>jl|k5ANzW8-XUfK>nn1MyfrRYR z*a;4r=ZYuv)WhZk9K{7mE|)DsmyKQB+#vM#cAxc81XmkUaj*s}w|U#axzSYz_|HeX zgWHFu9ILQ$26vTjE}Cvm&E(&)aZ%fk%T?}TSMPe2AG7Xo1j{DT2`RE1dRW@s+i<=* zUZRtW_O8!4U%fVQ#PYCNuqxU3@m^5%=ET+AQnQ+1hlwm@`Vp|NyrG5`3&O-qtO@M# zrcSVfHBt}D{OyVkalB?K}lfswP8EXZOH>jTSyz20)T@IMI&{lo z6uU@HVYpDK0(Fzvf(eST=}b?lXhk(*wt8BqNd&BJMCTD!HRz_yN=Zvw1v<}CDJ>e7 ztFhgwJq5!q^D|Ee^&;LMIl>>P3pz-Lj=ACC_eKCBG;n6NGVAQ|WbkbLog+N}2ecyQ zH2!=}u$0(j%Jwp|bH`MeF z3knGqYD|xZt%a2_n$K6PdC#YdjE+o4E-(VNYe14#e(fSb$BN?YBs&&y3cF)jm;X?NDhc@@4kIL;P9-Csiv&7 z%UtGDuFb^U)gJ6yhMQ#q$P%$STGyo4}EJ z&AWFpO)DZ|owZ2T?4>mdt&gP!9fC}iG%`!-SQZ-!90FtG z4q)s%&r4}HHHx;RDf)A_@>h9T(9VgN<4da<>pq1kXC{inHe4sv9xik2E!D2F>#wpT z^_X!&pTwcGO&$-ZBRddB6TkaM<@AoY&KHonZM6<)nsK7^y{x? zJY$kgX`h}+JTKrq?xW}oyH_`(?$m!nzGeGioy*0|6T?l8PKA4Qlvg`vOr1ofrn#~W z6N~;_8U(WKI11B#J7wy0^9Hr22dk1NyNe4xq^_aZG(l_YyDfRL>3|bE<_!Ai#3~!V z)z6`%NMe@!YxaD)JPnuR;rcYaRed6TD~8X*tN32M8hhFbhalq1&yNX?zS?me^wO1X zcA@uX4ooMd9>yg(7wnG5zpSm;^?p;Vbz@P;2p((Q8WuO*Rah?+VV5+r$3ecj?qZcw~3{xd&Fz2Dj|4a(!ex8XKkNP`u+E6kB3zR@2S09{0ifh+;Hg z>&mudHyk+*`^cBng;lK;#yibPy?dfJFtrp+5<6vX{tthKL@tN{zc#C-iv{IEnk}(9 z_^iLhU20ohKy+d~^)}*(@$%xmSwD0 z@#y4V*0T<;s#NwkWB0C9n9Z+vebuCr^KHsDVb+VljBPkYa=z93XzrO=@9PFx_G!hq*w~EW zSnEWg>$lpY`-4~2%3D;Nnd&~3?ktVL4M`i?zUAj^a{|84;1bPuOK~QoBs*@rkBzfb zlwEngYTsmw;Nl7VDSrFB=U^~q=A-a%FBi6-dJCptO&Mc5$;I#@Q zHbeE{*a)SPf<73;ZMqiKX(i-y=rPmy{|?HIJR|mib`ElQ8l99kz95-n%QAOoi2(Vo?w3t#YvvgUgpyc(3W9pZfCer^D^> zssiF(EPfPR{t^~uC-p+$xgNgGX-M_P=%SSMeRbzRB1;xxNYiIaHe-hh?&;aoAk(&e z?97@?P^g>3p3GX~C9a>n>quf04$)uo!CM36X!O}IO6Ll9A`wrIgG5Wi#irw4m8OK< z{Sj0UqBvlJO+@>ZGfaIxgEk;(FR0jHda~wWt7cTh|Hbz*`7W=}>DYSjklp%8zD~YH zrm9DRZ9Hi#UJst*;{c42Q*@%Cu=3N_S;mduQ@D64hWBQ_$|zL@DA9b^%*ubDZOtS5 zVR1z`vD+hq-J$_+Gno}4~ZalI?MsCrB~DS9Qz6@oHI2VGXn=_b63?vn&-6hjbWpwZ=Q&pnYLB`qw$%7 z*>{-DbT(vAQ$Q15k1WrwzM(o<+w=jQzsToXHinI9wbZLYz8TA=+&k0j*Lq#5qC9Cn zw|ZZw^luZsch_8Y*y~eGcQE1D>fWYp|ItlH)QgzB*BtckCK5}B3O+*o4h2W{TBn+N z{lNc+wfByPduzLglOTvTk?4$;5JW`uUV{*%=)IR<5Wygb9-WBZTZlG9??m*PXwgO| zjNZFal3zM*|_l%QVi0a-1; zZl>4E+BR=*#TATZ;^!#A+Dm-+AV)$)c>MbV_?r!I2QH++D+1f6t`pYSDIW=#4IjV{ z%KgK|j)!`D939=7sI0!L!?W77QhAa(I_z{k=h;{JkqmgrUW1Ue@hnGoTH9#o>r0HH zLhYAR3A#sv{Y!Ca)lG>Jg;&R~7q-4Le^vYGs#TCPVby2>qXtp0Ygh@)k{|UtT|b!Y z((r(6{CYC>sHn&Em+J)qtMmTz_jV&3dxs|yW9&UBzQVW~!*oWf^Ye(TN-iY%m#&}p zDY?z=32u$5L;nSs(Rmr*Se$YT35|H@tl=KG>)Jm|zEReL+Dp$W|iw!-PIOe!?(6jUu=h`E^g(JmK6AXMhr)03vJgAImRULF4Uff-7mN8XGQ+G6Q$%3LWoZ;dT1PjI(q$@*g(WuGs0ROqfyvPs zTpJfHpD6U&K)GJq5QkvR_4b{coZ#>35jvFseW%p{Sy!8HnoG_000*gbfwx&5<$i0d z-gEDzpy>SC5ybS&SGK;vA3t=A_2HeW)mk=WSiKTS()a%X0eJvYZ4H}{C{mmn>`==z zr$WRT*rE6$$ThG-&5}vJYoMFOhqaEvA_?7s&Rm}D#X__8tP5{gEe^_g{T(&xne;0q zUQY2;vU`Ym(NUtzA$PS~0EetQz%}y|)Sty*X+|8aYiGuKZX0}?MsLLFtvyuflA28) z-!Vd~e4JfBryKF2oo;OkbUPEA^%%>Bu8N$uB}zF&u8#{KTJGi%Sg#wIQMZrh&iZLD zQfzN!%DrgLTWsQ)=jI2~G5P%~py*ZTzd zl{xpW=1&(oC%C-Pt8a}89Z9oQLR9T&S$)wy+h$Ht+n1;c@YAA6!b#ZP%Z5#O_!PFL z!HqZ>=d+4NZ14;#9@y4Zj0!IeiF4r{1zK#=r;L|j=VUy4U;n*7k0)x_sJtFpKfS(+ z>e<%7DpewY>V_i<(R)fLWx+2putd+!o|!D9e@zk`ek>LT=1;Z!3gB8}PjF(4og4#B zFs`pUyKbD^Iy7?@J!V7MgeY8Uee!tMbc{jt^C2My!7RGnp^~Y(=+oSr*c1i$?=DBe zvE7D-&qgMK)+S=~CCpfyOKzXQ1yP}V?rnQmnJqzUJjMT0({%OL;m-Obj-e!B*pv}yq?c7iN zRpeqlJ3xSXyh$ z60@JBORg5r+G{q%?M4S7bdO_I@>Dvtp0*xtiqSpUr~C|$?Mq&0`MjCn5|XIPDINPE zB7#m?jC1^HrOb;F+X(YiPl>y|3EP3+`wv)QaPH_?qHvY%&L}4NLn}lD=`JO9z)+bD zz~-GktCVC66=+HY-DY9%|9)rX^q|u;R*zv&EMZk`L_>$Ph5xxFQ8-*2O3pj9UWMfJ z?Fq8a^e0%1^_yNX317!xgho~}D?eWmdwUp)W{FzW3-#ES_3b_f9d1r&x7BYY$!ap= zDpmVF+H9e9Rbi?Z(R9p_4kZ4u+eS51JqXY;gf4zGy1b8q^6jfRhW}aLTM79avbDwf zmBEJPm-^4Y_RN1QG;He5%xuejjZlVb!dAyx3R=|f+zxA4m9E)lrKPxBMX`2I3Y0ER z@-D+oE*-zy{@BmkV0G`9aWft22$q-0a2jY z*?6->E+$w=kKSKHDo^Dz!*qXQW|6y_XgzHwYFNL~zGyZ{>Sq-!`XjrxjB}@r&}lG7 zD;zGJEh1p{5ue&F)@2edsd+&AlfyA8bo1Bal8j|d~WR)ONP{)k&O-Os#pka#P*SD^449IhgIW>FL3=2BN z>`k=(rKRal0sMb*LCrTczTDY`YQ{d$ttq>G?^)A&l$fVSd3a%%u_7kzns#NbENu(v z)k7D-Fn25JQ$T2iW4=yI&R>3aP-N>Ci>f)f-#IDsk4F?A(eoqQYDw#argDn4U+gqX z^bQ3D#Rm;ANZ$A-CsN7I)zTOVT>qaGE<6pD%n6I_Y#&5AbkCXC^lYiz_a`&@{H&oa zt3Y!@O>gtf+7HDGjh&p6)H-MLJS9@8wELOuCI@blKh3c&yX*agHlX+XpJ`k;Ttd+7 zUcKpdJn66TtU(?N)Od|5ZLv!?spVKr@@oH-Gx9;daCEhj&1PC7>PO0G!=`kxwMI>ofL7O8it?C^@hSaRvgoI|8C46mzZ-%WUtcI!ss7uL~74*9al{o`Y;fx zC9Y2ohL6_z8@W?YN?k3G)ebrFMIs+gCgyk;MrOWAmF@?M)&?w%ELRZax(l}!_zW^D zMbavVe4T%wr)W$=n=)WMR1ls2R+1+g=9x!qnhdWle_aQ9yn9PPHf~urhK;h)O%iQh zDV_5q;-58IM??o8s@##=3GuOdDFzKYvav0QfHSY&#DGa zqw3WzMoKKNUsO1Sjnu4dqf2FXr9Rz--Q`05+TBil&7s1Uz!3acj~^AU_wU5+uNWuG z+A_M=XV7?_k4!5ZsB5lVtgO$qdd4jWpAV~@SQ>aj6AG0-Or-jc+WakQ0er{BDKemq z>Ryr4DIGhrcZG6ksd1@L5h`$m97q)vm8Z>-c;h16^B%+%X4Ud-?LW=h@>%;Oi$&P( zP|_M}bC%TP8!fID>3bYULlZI`Ka|k?bv=0rE4+gy?5+(t%+NzRLY;~#d@MKBoCLOq zx*T<5*?ypE(u=UoD#LObukqR-tuMmtUm`pNHEohzKM@23ES2^PDDStV2AI{Syq|tn zVAyd^6lc6u3C$0uFtW9>Z`?K*WlApEL{+L~+K%wNsB2a@JY3vH9Is`H)%*2JUPA3M zmXF@FnVX5d@jSC3h%D1C^`9AcTz=;^wc2zxtHbqe1?I9JmSSGZ40G49WfbA7q4}Rz zi}>5$eBVoC*NL`QS+5H)V|9~UTMvu8p3?-<=_vX7gKcF9LrqAACW4xXJju&@v1mB4el3E(B@ZF ziuH%mn7bxj%+#~IZe2`P<;_vHbIl)2Sn>^aRPv5@o$@k1~Ncl!c6@q;krD&c;h^$>*uG+`48PbDXMc@J^t zB~v4dy8c-2MDbn9Q8vm0kVkgz7aO>Z;d`4^L(T~{kyVF)qt-YY9}v&08m|t#tKEpm zNqsxnv$VkS#?^w?p7XDQwA%YO>$}M@(%jV2QHLDG)q5KJImsG2JkML{d@O{U%N>MP>`GNx>y}Vye2J{ zSDyrb3NS09P(CPfTn|RoFhuJ8=0ZAa9`;ETtfaVkEY&P4@*PBs>bp~PvhwdgO&kP| zgKx{PxzeRBksj>a5h0)|!gaqt38Ez5Q zHu+0Qpq}uJ$KU!sK!g_+{{ahp_xNy_;ua96TovlRoo#)0vq6|U(;zhKB(F&OWUs#h z>UJIMreimwttI5S>I4spyI4JWy#X9c%yqoqO&n3{&b9cng|buQw={~O3D474BFlBL z!%qrv0VLSI)(-}qdZm<7XSnf>-t#XKchyZKiaM>uS?vgI)~xcu48zv*5sjRseqBsj z&;)%?;}2t(mkpO*W1gOocCX7SZmr%L%Vkp&=AI8UI={WS%MnsaILHVsL=A&yv~sh2 zs#fGA_tqy*!9nWLiKyw}&R13`yEg8gv;$J5RFD2*%)}>e{rb76g>I%tLdueGei!h9 zilh6|R`}Z{%O|@7Zyh#0y4V~F7yOH&n3Zd0W*3jP=Zk=A53AcAn9ys}_9d)3Sf5xP z2hOiMLFLBngz8#a307-(9M!Dq`RjQv>MOc89zWFDZTg1-i;j=Dcv?FMorydS zjOJl1&NqJ_WC50&*O$}dQ8lyrzxUTl&DJ5F2lr?heq_Z&Z1l_ivaOH#qo=NuLlcFB zYy6s2c&$>Ujx^54LDA54>_kWxxES}{^`QxsfYp8Ehy6;wITiU;og#UoV#9J01;_K` z&^J6Vo=dcK=_Yk?9S3Q8j_qMHe`B?YYGq-Qw1VKVeo?-YxA{6{|Wf>~I?BBq(mAT9bv+*dVEG z$G!}!)n4l6T&MIMAFuk_TU7}ml-SKqE_$}ElBY9R-hT9bQ2k-ui~P6n^@p$!N4r;B zzs?#ee%F!>H)1AILU?T|loBu*Uby3+>_MjfXRWpW6Ve+~548{H#- zFg_eD6O9*bv58RfOA=tWs3@BEc(`s{$?5y$7J4;#Ec8Jeq>Z{gUePEga%a)z{zZ|3 z?Qx-{(jIjHAS9Z*j`oA|LkeE73uhX)^ajiw6Som zH3#%iH3dIdU(-4`e4T0@Bqp(YOIgHfWPXUP5Z3X>vH|{-d!k`}!>dM1Ry*gqR&0@a zK5ubaQcOi}CJg@k-SYj(r*69gXQ+(n9?`GHafpW(!}pa3l*NQbtawpyTR87%`2Hr3Wz1O8d&}=us2}c z`j!9BPIbZc+Cz_RX%5l%%PIqSrLO6`KMRYP6XxHO35})#tfkgcVt3tnN=m|kTbZun z@!w(?Q8W^QfgkmRj_NmGe!#gtHG|nZ;;2-FDta{9?@#FVFp;hEaQ4|KMj`4eJh7$_1pa)m zJ-y8eKBIVM1@wFO$M=VZxVe=3wNG4o=h?moreK%~=|_J4+*Docv@WPWjP;R_+1=H8 zUXOB9?O=67q{p~{dit?3>`l(UZw+NS$%}>x42tQ%S8zz=g9Ouvh_p)sE#ksZ3D+{W0tdPm8-gRtLCk`t}Nm*$<}!j zUY7$j^^iUJH5L?Koas&bS6fviJ74BMDirS$My|=yDj&u%gOsFbTts3)itCa~sdM_5 zv3m1xO%C6yaBsBnX`Y7;wr#yg>o8!Xxs-ojZqc+MC!YsJAL$8Isk(5q^I##`UtXLx zyrS~-UbIk~VSMB;vb+ROD6?4iz9-rE#jbRlQd+n~H(Q2=E1VC=nH)w_s}4Oq?{iKx zy)5RpbzbY(!P(5e?=jRAj$7q4plsKIN`32YC&C~NV7_5n5gIgbO6?3JoNs2ShYVZ0 zRAJLp{IY9a-ey8KH=_R1jY>zo(~BF%D{qE5ihN<NU5~E2rX?ZC!QvgjJipt($Im zVsyaLYyb1tqwge&fbR?2DJ0mxnEpp-<-`p1xa7Ib8ch5Baal$BS5p zt!mdY5-s{L{voFInv`ksu#NukVQ#$2K686wlhl#c|CF;1>W%IskAE z!tPoCkA*wj<;==VOi0%w8r7fe=>q3zqr3pM#cT;__zqI?o>i36=;1kT!ll+hB_1Wg zS>MNJo?vDM-p?-*EI|H@>25It|#mtlPyhJ!K&213W^|$ z3R(b0%-V}pC#;Rpuv^@g=PBl6)=KF=dHK%8dL}LX6#<3S0Km0p$IIO+yC_4eOAb0a z9SwT>|2jv2$ivqM;703CR!*nK5fW#!*#OTlb@=4yP+`7{rV%H+ktO|ee7e)0AIbcqv_bG?=rvjCtqc6`74dd!GqSLtvRhh{?=a5$%0JNjk~Q=skKJmtFcuKg z<_BZsiS!yDyC2j`fAZuW*3xNpn;gqid65eb``idun&X;_I3qG=@to5G9y3y?)p=H}ALoK$80=7Zx$YcD59jG-^seT{myM1AK$?}B ziQcXpj(=ueM?dYn>}yL$m>Va)6qkDI4U&a8`_MV^uYP22B7jOSYfkF=G+U;OXu;fw z80eYjWhqvM57kty|74AOS?#8%Wb)_oZn;g`CD#G`uN;+Mu#FK&jtq5-w608ip_adU zS8FsM;eiUHyk7cd(>{t^?~~()?PL|r5ZZSOY1{Q#Ej;SFEK$=5xqU>*{x&bB+E(4t zV6VU2*DD)v?FgYFl9fxnsLVfSZV&n3g|YD{{9io~0DaOSznNkrK$f260a-ai15l%Q zsjm*%792Tk41F(iu)ho;TAQSDJ@~NhpBzkk%b-7oMOTKEnW>|>B_KN4eQD3KGgd71 zzW#}rsTH{q`SDif%A<|NZ5fZ2zT%%qi`;0#<%j>$F@Y~IWkI@IkDCbqmr4$vqs8z< z9vCu|6@p=>{BMhGr!t`K_}ZA0*sJ^ph4yP)=>MTir{h$#arMt~D6_VRYSlH(QI{OG zr=c!8a9O#f?L(-Mle%+2^27G!RDH^W}kp8wbvNYBcJC@ zcecw8CwGK2snF_>F~9>DgbJAUzOLMH%`uLJ?X-Uo8@^nq7kcUVW!x-Dm}OMYIn#jV zMSxLj(34Qg;k)zF{%e!v+J%?%KY@;-%Vm9h)mpSNy+Bb=*9Uqrkojn3y)%z!fK=Sr zfWRNF0jocUIdY^A;*qi5XI4&DsRgOp9Tn>n(J?KFNXgjk3dfA)fkhdSi>wgOeaCvX z0UmMGv*KqG#=WkU2`!nDtY$+z4Kw%Y9wyHI!{~O z`*3dUMW`Ja*30Qhk;h<907D|&oemrMx2s$ut&~9{&g)xi1XuM+H9KTz(?dQ z=dtstakqGb)M4cQ-p6+mJ;n(p&?UO_6qlTfi{I!K+vbN*9d$j2`q7=-`7eFCx=zX+ zvs(ip4NX$RfO4^G?k+2z7DS${Iq0Gbxx$GNW~KG>*WDIt6^}WP6#TGwT9Am^ay4G6~}7j z7%ro?x@vQNtw(Q+BYyb@s+v#kMaJQ`Rs4B7J`Z9eXmglM~LI!1gx5qQ^DYTO+yl;(S0zh;9_x|s|e+9t4k&f)LLZ- zJmd8|iKv*RlUpMdX|zp(wT+e7GX$97V%udqkb=>c;t@xeN~Ck79{N2`q05e-wTlmYi!B=oe!W5I)6>?>PxhGGV zn;%M=CO`K$KOtECTr1t;qf(f_lMSW_l!5|2Vk@h{CX2yvtUiZqI5k(DG_%|=Yjn0= zeGN=`4QmhweB#5PKnBwYclTFEROj)siok&5*sG82QGVI-(L35haFXN#y?V6~+n;H| zwY;x}#937fepNnIg5tkH5@ZhZE59&l?@NAtv~|-sC6D}R6iqU7^B2wrOcS7}9;(JA zq70?vjR4cFk8e{6@65S2!Dkf<)z-DZ7>i-5&GoMD=8rF-??&IW+ygAai_j9a=(bSG zC)KY_OWBs}g(DnBGm9gy9Bhr~73oyJLr~f7o5qr^gRaGM>$ZK+c5<2PJ|&A@UDb?M zk!Ge$$Mlnn@r8fP6yo@a4=C@9Q}(iit5(2rr!-0(>^;Fa!}uw5?!;&QJ*}9%B(Ke+ z$#``%z8f*GDX2e&?BNPuCk%?qJqm2EOX%(m;NSS?KBh+Ry)Y_;Nbv#)PHb|1yA&9* z!LjLfoEkXNRKTu&4yJvDDuLSS+z8cQEfh<6A&o~Ei&)d9w9DMt^AV$vOYX({m zg)uO1|K{daPlzMj0uSWPY&x@#s_VCu@UG;;a#r9`Tdq=>UfXwui?W54h1jV!-fkU) zI#}$+6uww3C=GGkk$$37#AwIc2%#>1uq5m{q>93{?fY09L&hzpE)MA6pDgP8V|8aHsCW;sPBkE~O%9^Y; zwUsuda=|ADU;qsRDQNv;YCOiD?XvA97tCFJLQkzx zZK7v84=GIk$IHJC5(fiLU<7z5zUA-nm_KDhbc8rfzuawCWnm)ggZbuR?-N)ooD%)t zuYvb>v!LMP-|AKd8Ss+t(@k5eYR7KKIZusNdSfey^`p_D2eWk z1c$jE!^D66@;|?TFau0$)=IYb|N5*W4sp&7CTKHSyOm`15qdP8PYk)RdX>zBj?we8 z=nvwT1$uI|O~`9Z|1Ha{X}C7QeERHVBp!za?WWYf?}Rxte5N4b@J;y^?>~h;zrWsA z_>C(mrQ+2&F=(EG(VY^5kiS-dZWAK+Wb7Vl#MH%v&}>2AuJxxhfw(cF_&OyzzVT{T zrT=q)RvgLr$2;Fex^&_N>i(M z?2z^O+KZvj0MBf0iGXG&Pni&_Aeq@j?t?LTlaGd@LsaJW$JzhqWgO_ z5&wnNq_zF+$}k341>zhvFu5fUxFDW59f*(l(p{@VKgT&_ zY#5zGO`_IT5Q52|cx8I{^XJcBY^Oz8v?~gLtmH`$2^;B(nS<$hdl*d=u)o1=AOo7^ zYz?M6cNRJ{Ds879B=XtDQ1ZNz1M`~H`?G)}`4AKH=|sQbt&3_AF!BiNS{ivqG4cTM;8K1QILv1vd06HsYI%|G&(Td+K~AAWW0!bkMYQg zXjVcu;sQJHR?9*5ZWpQ!yd73m?@W{Mq26Bsp-3M$8!z=1>p{BE zqS)oJa>XLBJY>7Y^z_;Bho~r!Yo^y(fzO4hGy|+FKLQ7pJn#AT@K`~|RZP9WLSM%Q zn}CnMcvx7Tz-m^Qn(JiP3bZSg1~`N}>`(SAE6wQN)q~KOY+&LITbbNex-vHl+yS*g zh!%4=K7{vhZR{m*Xuck9eBcX=|CLA|=vXi{?)^{z(|jThVznK>1Y`mDN8Wm?ZqdJe z?U_r+h+85xs`VCQAmK`^;DAfUtR|F zTvmvlpQ|$pxgo+39&I1K+0uRZJ^LF8Qt9nq?nD5^J#cG?9QeP@0)Viqc^qA9EEJU3 z{Nn=tb3nfx)0^k*I)Rh@khnZQp$gJPYx@X+f+GP^10p8V`kIpC@WW~eGdHLz-c69OzJp>LOpvBUD(U&~p;z+RekXPOJU!~gWlN_uLE1Vpx zqfmA~f6ynm0~f`!5%p|&lH*O!OB5dwQBYP^R_^AV{C7Y5u#9G&L@|v;O&!$tc7o`c z`=L~PPTk8^*vM!QeMt}ek#?dVpL0+NITh?c3%Q!P_!{t@Bh}y0QI0-#y2O*B@j)CI z^Gt2%Qk=hG;67k_CZh>BAnH;#OaB0~g>Y#=ED@^!ot1RBcolH+F#ZpACZI;ua03oL zNl82v|Jzn(Chm6)eD%TBc%aF8drjK$OtX(XT@cjy*FZDe-A#LuhQz2C49emO!x);3 zS%uYQxavUc*ACb{pPA63Zmc9Qb!${eP?OcA0dZx(|8y*9^{gD%>j;Du zF9xv{MS$$&lkU}D8bH9eoa7KDZoSU-_GBgJnteSXy5gIimeKlcHy5w>+If9EyY3sX zLLQ^WsbgwkgJoU&whc0Zx#}P31HZ|o!)Y+%Y69%KZnd{aZ9O$Y%QTR5e#>wT|1K|!JR~f;;;+Hu}s-|FU)dd*mnLgV;V#OrthY++Sq*0vv(UpL?OQ^1x{hY53$XkIK z$=*m1dFaPZ|HFF;RQYls40Gy4$hZyP1;ik#P=4!y@W`^FBi|mqJ+f0h=NZ*u1hb+c zDle-W^lIOe6X`VVI(~kkIURsaNDkcr6Udf>pF~~op~64&%}sN>&EV<{Anb4ITelW@ zBPjZ+frsv7*zDE7AK41s;QSkMPA3(S>JhdX=Ppe>xe7_X(!U7OPlsw@6VKXb)HIXU zem-uy!0aUdz|r61T6NJi(fwke;Iloj;W1!F-&Ugv|NpCb_xzPyWK(cUoN+vRIY(~ z_hYcO+G_Pa+vq0DF@7z-Z-Z9w|Ik|_mS0uK;D4=SJd8)fw zHaPsW9{`h9E+XGP2)!iv(S&Uyu5{{FLx{r_qedph1e_i5Vhe)Voq-TRDsBrpaCH@{ zA?DPWPSQ9ec7X@=0x*hy33d9;_t0{(s$@q-FE^r=%m%)Cezx1c2nG_LSvTPuX@!uu zmjuB)&-c|ApO8b}29vVq?MS5ILe;jJ_*-^)>(0;?e>1o=Upv8Hd_%b9gjfnBU4(zJ z_|D*U^aW5GFB(1ESCI9!S=rg0z}QYz3wY=4ZS~sRX+tRtyj}ar+3bAvS|4W!Rl)a0 zFzk4>KP&4FIEKPiKEG&PV`Knmkc$+A4 z?4>dgp1xGQlpj**YQ)m};CkEswPwVAjr|Y(kIe$h>_9ONa&rgQ6|aFDg?h9dzgaY` z$5A>6wG>(s?ZXf*^TBqb^BuWr%leK^8~dY#Ce2JbMM}atJ+*Q1CDw_BjYiiZap!zWD?GEo^NdsI!W#|#Q|$F_JnBio9|^nyr9JmrRVyMz}9HFFL}h; z>@5gyk$)C~33mmrSLD(|38K0j7C^*`+7pOKI3-V)>rciD;5l9u_?7gty=D#fcUtxG*q&?M0giR7vyDYBZ8`P(g?B>&)E(b9e1~EV zA4DW2tucKorwv~`2V_+4CdHI2P7ONZupVU*9PkKMzpuu{(V+iOOb~1opHmP+iU?-u zzu4MBU^H+QTw7Kv)C%;d!az>jhteAmf^b7dkjor0E_(`@VT9irXB5#f7T4V-UFzwq zQTl#Zq$_kn&=|4Yz2 z@AikF_vvpz@B6<5z2euJZ=u^cH}U_%0S71LmeTqBH~~UINb%^7mBG#Tk0pCTV4r9O zS%TFz_J48Kv9#pBes~p`GP^$p9p@>cTT~#aV8*W7lm@Hcrw8ui7wM8%i>BmAFUJ8R z@%P7V;j{?M=&h{aE9usSTIDL#o=}&Cn_lp9kWI!}XZnfFo~urzCF*|N^4F*mQWHrW z90@vaRq-2!kEFAdyh*Dh2f((ZWh3f>P-H5jEnqdVq&v16f*xA_xN}!>kIBM6z!*pI zCY_9L)eg|+cjbQ}ElUB_AKHsCx}Eid-?JLPma~f0>H6H1j@insGwY>vcnbOTile&& zo1+CvEj^gDkzj0m(gvEE_myqw&)*Px^h)EcQ&IZa`1H1ArJ-tM0{dqGF(M=xT&XIR}Ah%4* zW#12?=H?~}Dy$}<`@VB>?UUN5>2j-B; zIzzojHJUYL`X;0WB?|mXH3K6ZHBpD;(Cc2y8z2Wr4xv|%DQV#g?0UQW$nyiSeQr4PuwJu9)>VTjE<8gUdf<&UO92PBR?thJ!&@k6hwd6U zU7jB%LcL}I zA!sxF$1&T{SBOq>-QMPQcXl@tz~mmu#g>=nl_F!$o9 z;5s8$^>Vhwy(&>K;PrfsqaO!^j#U!p8SC)t-+X8W=j~U#%J2q0;6zO4)W#FK3pE5* zKcCDU)})T6fcN!Xc|w_i5I$TBqMcl2eXIAV84e<0Yi*Gr)FW3s*Bz)FBpS4Om(h@hK&xBQEY+}cB>@Ta20VJSzlpkiOQFVmgB@uJ~lt+`(KQ$pe| zy8~dV{F)cG1}4sf@^?d8JkmXAd`g}Y^z>yG-z8gTweapVIOBZ;ep`kqisuCSx6dn0^#!auE^LZ{-GW zms^kh=*2?;Wpebui&1Pt&bkZqEecJEb4&pvHmn{NWRgn7Qm5c4RnY_@DI=Rb>{$?C z2}+@A>DEHeI$ZSfm(diYQJ5mCmniSPK(Zs4=LAupILGFF|k61zh3tOXDZds8p-Hhxiq|5)SD`95_xofUHqLj48 zuNqgtey8khtx2qGxnh*OLa#3|bVZ4{Voi{>n-99z;i{RB$)d$Sh$?7rzCsKP*Q7uO zAr6FB>b(`wmeP&zCLAAjJ-=j}gjvVi`;z3VY|`=rAnKf=8bnD)%NkAbe)EA?fa6*1 zE@*JHHMfDA~Ok@_BE`;7GK1q@3s}^PCa*aQ+0BUGUAq7U#sWw&QFW31!=(C9`vHK9JZMYeAjcQK(0!j@CkXHuGCh@ zF2-f-TR#b(>$tuvf=#70JdML{{5uxm%W53sasjaxIMLg>d%!Bc1KfeDOUIg?%Q8mz z7Gg+AH0$$B6WbGwFFX<%Le>3b@B}^7&K+B2r}M%<vyCri5(vBeVuV-|d;5c33{~6&AMT*5kWc(CVoPAN5>>#3)r?s9dFB~7ftQ(dhB8!=?JqCR zyMj5sbzl=x4|M*hu+eq_;N;K+8=}t0)mugqlmpq4em6d(q&$Ftn?2qIX(7Qi?i)d2 z2&$deATuYgY99z!9kSZ3NbuT-*2!VQc+2Xc-RgCC~#aLf!YC4{rnKb~dks1A|1K zgpO7Q_E&o4ixJk79^Z_wG{Y?q7Lx@&%RjMHDg7u*)qDqCbPn8@Q}+e&xr(j_Yb)RJ zr9VPmGKkx0h{#-{R80mx5KSl2e%*gJlkfn z(5r)&?4*UvH%KG-mpGby*o4_y^6z6#X}w)3#4iFn}s1e9oPE<()qbZ;F*X8sT4J;S8W@$zUh=0J&xzK zNtm#IZ%^MQ49sigFXw_e&;w|!FcNM6y-VN*6#C6t!U1~9iktCkVZj77n~Re$MY;E7 zUU8rf7}Tu{=f`$Um07;@avm|jsH^zu@aUQ_ruO5`5y9pDRK&LtxfYD8TWtlW0|0`d z0QCq@E%8fme;$QZ0eDO?{3pajQam4{uIuYdZebzyd^N>SPvbKPAG_9-Fjx?pCCx&( zfx&S9HQN>(we;!((NJtLy0r2e>#AlGv{w3b@PLkbgJ$mvo$cDh$Vhq+hQTKiF4Yl^ zeGk+)**|8^$wvbTAzo7W7ijbU6cJ=&lzovDP9X;L{<~NQ;kxgjvT*@1huKJCmmIsQhAFy~z~wM>4rr=wZ!A0&E;1cEuLM~fTOz1%?l&ip zva8xoXWdq}Y6ohA6~K~=``z0GkzX&aIpN6CXPtxV_$kNNt%0}TY`ixsi0Kak$bt$q zOJ$=(cqhWQ9Fvf*FGQRRO1uen48}=ctjwdOC%Rd2d2C^o}_7RfgWzU~Uy4)ry(QqQ)CvvD``6DYZffgm7 z^01ARh}s|wM9AJJ(|r>u2o3ivNNIPxq_eJFKNZ%d?l3rc9K_`QRJd`FXVML!Ww&YLMb~3V&s(8bxXFh72Z^^Bw^IsOEnM*6M z(SC|%7Qb?B8#B*zdHhDPb0{V!Cf${*IN*DbcUOLYx$uN;1o?O%X<648_RsI5e!_#OqbEs!PRq`84RzrE29Zj|i;I5?ffPQHh~r3%y@ zWYpWC}(?<2L^tdUz8N!+)@%+|9EzuEi$A)eG%Ju){57v z(94)!eS!R@D{-luYWEdaiQ(|I1TuaxzB*~EY?9|=Vou?R*lv#}9*5ss&9bwl_X(wK z-8*MG+=kJQa1!z{KN!+Q-12%gR$cLSO_xyu|9`AASWg$?_vOwvxtCo=PqZ2erYW1QFT#%O#kEbS9ig8G9))SM$I zMs~)q#h)Z}f_O9^R~|RvZq_s)iOwnIUBq~sKi6sMgUqT<*L9D4@k{?VYe$V_*dMNk z-br|!99{4{)!n^q@USz6j{XG1AkI`wIcaZN4tJ3##JvU5l9;y?(w+$7io?}mwiTxI zLg})TIzI_d^^>Ra+|w~W!f~k}k9Py4tlq1^KWFcxDp$HT(8wO`TSl(IfdmmY{mumS z&G~ew*1(z?!AoaeFdK8-bK=BpIQRw#oM+Vv8sXqrJ*nhFVPU#M52l>;E2o?$RQIZF z)+mFRU;*)`8*X=Y^AZ@WWA2?yIk)pdWUXdwD&h|rvXS)~Vb?ndBacqQSeboZ4n z8gEkK8vXk*(h((IP#>Ho&|lj&`PZ@Y*9r8D(@Ud;Pvk09oh8HWPWc0il*%8UavF~2 z@HVr5)amfL`?k6HW-JX%SkBjXjfS0C$GGjEe_o_g8!$?4n3}qw;P^Ssw3|2b$|g{c z92z0BW3;*o%YMicV7h6tMey5!_M=EP912VKBV~PMWZWfG>{}L%G{<#aRIOq$8qNsJ z|M>j`O~@Pq9^rIfj;`+O4K_H($n0(6oV;Yr_p7BqY2)T$i5NN2u5bdB_`df%iMme7 z9kRfN_Hf$GG5G%uAS-LbSt?*r`uoIyYe1s7>~^>Z8V=LI>}#TjzcT)?0$r%t$&q-^AVnf z`nj#-(yREPG>M8huwkB1ryTa$J9G5YN8gFPa?n3oHf>-a<<=(aWjpgjxGmX|F{LyL z?l|4~c9w>{H(~+1jTrg=}$+^t?pSka@3=L*cR{q@~>*#v4p!c{ls~Owc zTCv?=T&|kU(h9Fvr<_2`)WVG~fQuceK+d&8{JJi8 z4>%PkpbkZHoARW?ZcZqv%u)3Vl~H?-o7mg?h{}?rg~ypwKB_RMdvgBwtSw3-kCV5~ z?maHJaN6$L&uqSbtwd{a^@xX8aV-|#_%}Dvn#6xhR#xmZZm}U_XmL!SK=ZWil(E}I zfqCCvdTn+#T*LNwv1)`bXTJizmnsgj#t@dQ_n)3aHg}i7^2w}EJa@$tE6A<_U>(%3 zt{C&(1wD=3S$=zyg@^(691e-zSU;6<5+3jI;6tYPg+j>Zm}~sAW8{q6vZ7k(8;U%w zOU^cGUZ85gSHSVUmH@{S-f`6Z73|_^P%+NrRPDKJG+$|ZVBP#BWp%EIf!>17KNhwQMw30~8}IYx9Ft#|i^!U!8z+DH=c`+CFeW^%OZywnU=3V-DL%*PdG> z|Ksz;IakEf07-C8#-FIYrR>S(Yp+BIr0*W*c(*$=l)tdQ!uR z3r7}>VWsX01MBNUM@}k)GXC`w11Ru$jpIiP*iU{wp?dG&cYV_q3&W=rtFgwWV;>5a zaq&fI;zf&QH8l>d=e(f#AHP?SpaxrxAH9M$ZzlfvGygu66l71h-8#wMzK>zx&kR*qqR`paVbHrOj#|hxDVG>1c%_=|MO6y zb_50l(E^UMlcP4y@9`*CMZ%RWOr6zGXiQ?Ev$nE@NXCb21s6$Z+Bx@w-Si)VVl%>M z`g`#pTKG=T3E3fzSNA;ky>}0j1yB9Vf?kaTukl)-Shl7y2!GpG(}W+*Vk|9d-iA#qD73SgMEmTEC88fO_->%6IZ-sd4{{y5X%*N6 zdX#vv)!?%g;~o^#PsMAAzf(kO`%IuMMZ_qb`SPF1~8ntbWikl{E;7z^H6jQ z7dj5-gj^ov*+O?T)`Q=1Ry9;5s-{8B8}t=VqhxEcj0L4NG`|s=62Q0EkA*E>e|Rud zB@c)Biewa{Ynr5Dp7iOhG-jbwDCFb#i%cHiw%oW{|1*d(9KS^r4NBwr+D1~=7?tr$ zmg2@_JQ^0_hP7YhT`ptTwdBs>kUZHxutp#M?7fjPYAC2DQ;>0FYT_8;u_$Vrl5uLV zF6DRVW^tX1783Rl`!{=gWstyQwOq5iXcTVJw+t^lL*>%#0-d8wr))py4^}wY0P@*oFu9d5~~XY5j!n?QS@M1ku-y+d-a<^N*sJ zQs#dqoAxZR^xKBdahi7W{a7ORC(-NmRqJZQFlIKJ0RIRjW$00(IWw~fqS%BC8_9`7 zd=CC~h|ZMsmoqC%8+?Kb1o2mv>R83>E}Eh2FB^-I1U8M{W*7~3B3v`-nJ*714t^PHQIdKD&qJ1nv(c0rHlV1XV%IGeR; z?K+u@5e6qqv&~*g1HV&Q0;=#DG6cu5y!*q0l&yN{V1OP=@m1(ztJ`4YCsUl4M;mz1 zfC4DL(EQBw(bs9%Otkg<>0@geNI7C2s(o7x@@Zhc@+H56kUw2#6hFZm#|M>%6 zlu+{}rfMy`f=$CdOlh4Aw1~n03$FsP;=xO-p$3D#)G&!IJQGdrAcqwS&`{x$&ct zKjOwg{JRpL=Zc3Nq1!HV+G)D0feqEGcf;GrKfFMYTRJj}ACHWOCEIrUwy@i|LYG>= zREhliD8uZJP{s)QI?D<1g8}|g*}~0!-bP_8);LCRO8cN8Kl4T|&P;JrHF?}HTnnwQJ(OR>dr^(C0~3gTgflQqya3@Jwq7DmaQt*<`}LlEW;uXbYS$& z{riLSXZI)Oo?1%M!yBfVAy@mRm5iMIBiX14qhy;cmY$uuG63?~xsRWJX3B@6D+KoQ z1AS~|lga*(eYnlbm1p*D1;eXcX|}jvI7Sg#%qi5bhMjh!MIxorc?3k9_@k1Dt2ySr z8aIR9ZHuuJ(uo#hy8Prk5zAeI?pyY}ux0OYh$7BUgxgMsL&R1XtYMK?OM2Pv;yoVS zg6j=ZF{*szSNz0{WGyULWBnpaC5yf5NK7~pILCAOPr`G@C2McP?#SR;pC;uuz1N#{ z)eV);J&o$F%E@W%QHueodzp3RH;F-lUxMOsTo6x;mO0L;kNTE^Bu@K@MYRzm8SM*W z3N-+KaB*9X^Ln$BJfTD0-J3tkXQ#&ta9hj_lZ`TutuW{v)hA;&u4QlxuUAVpayCcV zP~|)!{fT=I($c)oLvuBqg&y_ny3*{YXB3v|+su6Byj?o~4CQn4sr$mxEJq(DO6T=WO`MIU+KMw)?O0pj6ecdSO$gQqDxW*hZ+o|Gxyw)8p4}-@j;Z8#~R) zbG$$z&;F!_y13EWZMcEfmaLZgOxC6`9M8gz`f@8z2)KV|$z;u9{8?|QzZBif(p&K{ zTo=?g)MfpW=S>j)p%AN9N)Xy2T`=8IZDekpS0|O4Ei*=l;z?lI$+vAHgYvS4aUXhn z{|Uk}n7DTSt-o59j19k~;Zxa23qyu`%0q%&8vKKGH7&|kdCI1y0kU^rp?xn|8vewY ziOTrBD*+gNQ=zJhXZKXyRo~cO)JBylpe;ZQbj%KZdQ?6ob~2zZdU%=c&;BX+k_Zr z{R~Abg>aRsyK)xi_T}68V-%Uq9@Xd+Hq3z9u2K%q2@UnO+u21` z5-cj3%yB2^G%D|A*<`hLM0a+$Sl!{@xm^*)Vku^c4-dDt|8QadkB3u!Kd?0deFExL z9U9QY^<-CWO;fCzxrC?{7?m={Kd5%nv@Gf#0H?sfs@UYgpJ5zIzQT)MAi6Ixj(4tW zy9~0X;?bAVsCqXrYC0i(1!`B2<2ng}F8qaaG`|Vb_e!m>TC4>6cOJA94*YsMlvSHI zF8E05cq%R@UA;<u<`tHh%EXN|!(K;ry z8q9&Xue271DgvTq5`+hV*~-4sJ1&Wd{6s5JDij^#Ld$7d#dpQPb(^W%dk*U*~vA zaW{v0Z5tFpyT(e@54tttQnKYw&J+mhR^t{ob2gRKp3q>8YO6c?_0m~F>uTGSpSh5R z11BZ^cD1dvMphBCrARohm)*%s*^8QTJ$)zP$L1Pynj4QjRkCs(E?H!WE4BqR`fuXF zzN#?q!C>?%F7B=OTbIl7^u4atzGZDLqssB)#vvWkc0nA=hF0jK^}`AcSRZP&`1yUq zAR7F;R2Bn_VZ)4tQJKQa_uee_RPA*WpS<*W8;c^F1ADI3NR7Xo_tP?I$2#u=Y4;X!MvrI+Dy?f$2bpFaf>m``p#Bv;Zjy)OyROl@?ZLkr}16Eyc z8U$PBVDR~C9nu_8-`hO*zAj{*-dQiMV#`m;Rs_b6vStQPxzC?)Y<32i^!J|HE$dwq zmcX^IarA?1v69FB!X0HV>|}uh$VljTG8@;_w#PU;r=SIR%+Otc>~dyMhtq(KpGe9i zGzg`9t^}~=Kk2vclnrUCT3dVMCh|N5YNLOyhNuQD$x~kd%568F&fV-WOdh*qTEzXr z_GG5!`7@b!vEyrJD;M-lk(0Ew4`CY3-VYIPI5>C*&$c4XsBl&Cnb+stJ(KQZ)Qn4E z32SOvWww#t%6|MtIAp=R2WO8=V58%Xn=haU#8wcCA02-0 zy+_0YOsTOQlfw<*@*e|xR&=3h0FD*`@pF;A2cE>(#iEuG&}uODfKNb$1D4Qu*Bocb zDbceXl+yJ0HYSII>wn6sbS&IJu_W#G`77oa)`b)`y~QwP^#&~v8cs2c{`wT{M);Fl zWPP~fpGvx_uWgHSh{IEdq(n1}k+RODh!I(%mmY7gBz&JOfCEz?Fq!x$D?`i;N{q6@ z=N`O^Fjvf#7rUz^d6LH>lF~IJyDr#7O^kzo$bDl_INR96HC@qr4w?zv&P*M=7kBs< z-~CnbaR8K`1841;FDnnwzyn7N-oANutjTjFX1D}2g(YG!7sNstFM2&DN%E>iyO~Kg$sPd^|L#tXZx74RMeE+dwFF1+_QC5-#Dn3 zUY453j)l%t+R=T&H(LFzQ|ERWT>qfB*_tepd0(V6ZrQ=7gmI_<$4^7R!d8yNaGQ@< z>PjW}4XmIY)8uxZ&qDh24`vm){`Zdxd?-a383QwK33gVIJN}mRWi5oH8)hGb2GT48 zyvMH{<*Pvo>^;(JK_;Yfw`@4%n>gnkDh{8y|8Qdzy|*>vYTZ>wPtMfzAsVgAePR!; zSt%GEdUy$LkOr#i_9Jb#*~{Y=@sHHi+s_F1NV_o~&+taiHm=_emVFD#&l44K+kPTD?;$-@ zEw61+JPm~`jLl~pj+{FmB(==UCSU6O7+S*>qv%eH}jZ{enc%aS8>w55G&L3gIZ z=~-+wxO2z~tcPwQ6&9IQ0(ceVZ@)el*zzE4om^OiUf8RKLkwwEa~^75;4I&hLTOMuAlR9r^zs^ z)5Rs}3yLerg`lY|g?4Dg^(G9{bGYM~nQ+Im8|D=eymC}v_A^hPL5PoyA~ZXQ&v*!P z`Qg4_Zp=<>dsOodtoW5ai0k1~n~+N?F2Z`_+}KM3|*huUWTD@izen@@@5`@&rre zcf+}A&JF;$4*LLCsoT3EhMp^UE=|z@W~W<7ui3bs;G)zrVJ?71>jOU}{Ak5{_2U0h z!bon%JvjS}^q%*Kdci-R8&gpNA#YIKJey+R#KdT+%uT^2bDNWJ2@>4b)fn~F*h`B} zm5)4S;kTT(E}MFz>ohD8pBAp=s8$R%1A((dS+w@u3y}J3T?&-r+eW$7b#^30mw@cr za&fL!*AL|0d2GEgKNwKQ7WU~=CtRMG+@r(l;l@TQ=O|t7QVF>fQktecx_)>o`F2kM zG`5{j_?>e}+g{7{)G{keUKU#K{ebj8Bc?7|SY9XlQEnsXNg02VUeuW_p3m-$PCjla z|4`ve1-W)su{UTM79S2W6cDNa3LU|SQkmcc5_Rn(8O?Ai=4um`W3vkJ1e^^4pg15i z7&yX-X@elx`82LGQ5wwD2jo;UXJ(DSU>M~b=>W_}z@Rs}s}JR3RzY~@eC*5ey(}65 zWgVW2TsPV+cw%eb0Z<7FIc!U`)w~BXCmTd0Hlpha!io8u^7&=ieB6Dxk-*3kTDOYp z7`Oyzc|RQ86?V3MMV^^9kM%359-iNtblf!hT9$2dgx6mYx<6jcf9O5kqvfQPT16dm z?Oge29CNI+sk;%i%jEfCdAlQLuC}Mj8jztjD0Y4}>MS3MVhWyxrRZxtZ|oSG%D>|* z#9`QNs_7b@pJ&Ow&mtQBeRwe56;y=8h;-Z_g4rbD&UTn?+V%DwU{=dg6Fji40vIPD zY(UuA3Q+LSb2$3x8>JK-DquQ%b;4|1BIL(Y8O9UZ zntV0%JZ!_z{cuXBuRZ4-0Q&9%yB$@Tr1x1&D)N_#tTPuJg?CpKJ#lioeO)`YN*xHNU166I(pwlKK65)1yddGdZGY|>o&c09pikm z&d%E5m$OLrY^Jf{Iv&nC!e(6;Q_b%_0Bq@&h8S?&&D!cw+91J>fIRM7;U)ve zS~-4{qX6o%au%B*R3SQ3M{BCqnU2K-GXzt zRp>G3`F(djIe>jR{uh(3S?@t zcJ@WG)(~VZ*<49yXZ9h-eoJ?y6mO#n19PfsQZ{P$)lX{ID8r_#t9cB7O>@Kv=8tok zPS?)LDkRVf<@r=_orCo5G4=-*%2fc~#JlHThsfDN7^{@eOjvV=Dm8@c3B+7VPw`-k z9;*C$D^?Iwg1NaAV_i8VC%AN7Bwl+!)|IFjRe2D~$bWGx)p-)aCk5eIF6R1eH=%!%&V;SP#xXWoiR8x7uofP>PVaFd_7a-oI3^)FH+h;O!>_&*EZt4N z|2o@}AtteJ?{kEP6<<%4r|0+a?B_E@!pa<9)v708u7qo@hI)v%U~)ridJ_%<^cw z@_iZ!zi;(ak#Y#v{rDN!O-lOhon zdWxHgn$`rM&+=oG7UGqxRbsQ5aU=lAVlcacDIyY#%R29(k!)N1nvxf=NW zYNviO5GE|R9pZY~8ip=)1TbS;L5g*sfVJnO2Ee6NHl;D`47{aT3G9BLuoLtDbv zQU|aBC8SrHvfW{ZUG&CNBMhKh$Lstvc)k9SYWEYD}r7^}MD#Au{aPHi*%hFNG7WDW?t zn(Ny?K;&G<0R0e=87%rzZ8>J;3&Xl1E@+&|h^@eMv$&y3rCyU)f#`{gK$pHGQN3Vv zDw>m~$T^dQFnX*-M!SX8ZS~X5`GI{4dp??~|^&Y`x{CNnY!xuiaGq;>4YT z+hQwT)F!;W+4M3nZCAa3LMe?>pWZ-u6Oc(9{u_6(x?DI{4rV!mto>7^(GtV&Ctb;W z7en8&iWOMj-Ati$97~iIb1c$crT_IRAHWGDd)Hcjf{Ut1y>K6cF`0yap?Ydnq?Q|~ zaMmu9hC`nJ%{pYQMsluU+a|Voh>Yu&)-bcJxa*Gex5C|y2b_u5F56r*37@z~;Pc_$ zM>%853w{z#7hUkT{ia}C7=6!$Px;Re9DO91E!h=&R6VzbigOYX7B8GtAL_DSwZ52f zbldQ({thrJ&R-s3mGXz~3j(VAyAVMhwm#+^Ld+o3Gpe4gO0tb%P7p?~k$&|iVxKIZ zyne#DGl~$ti`l1-=h~$ z5sObk@Ha(|)rzN;ik~FWpv)^74l0Vszvg#`KX5%GGiN!1UZYbxt!SnzV9*x( zT2(G~bRa!Xff!Ig$Ld~|_BuC}inuY~z#Y)A^$sTfRu0QDK8*XcKKo47wYY^a+K{GE zAjAic^mrDi!sq>~KXnjtdegJA)0+r`z;ghpi2I|B4HLfYMbtEd#U-OHiX<73l68mF zR6M;&{AK;eycyj0=yARr6WM7$`{1*e$6mtDD5ISBGyC93ack16|IxWYQKx4bWF(UO4phgb2}T#d8T1qeSF3@HFt6N%R)dVTlzg#bB}A9g8mNr*%;Q`EhYtSAi?Cfg zAn=!%-Eu>Q;Rltv^qLY~{Nd+p^d?7n-9J=L*((ZisVKH1v(E&am6a#K4KE_WRDrtV z?0&oyoS@D&Mc13Fwg(pmpr8&u*vn$|5+G2KpCUI*}d zX{JwI^!s`5ABwgd$Di|odp9^$THFCytGHJ$phnRGgjWNcO{`yUy#K!NeODya?Icv~ z+UPE6pA>I2oe0ovmFhR&Oh;AaC8DNZ-D}$nEIKw*-Ko{fAAV7#ZxsNZHkDR>p&h$|x@|73BpYM6Z zjNV-YnB-z4>Ai^oR|ch1_%tP!cD-3UBfBxdWtz!pxq)$;0r)nnW%RHrb!JGt5|t+y z^4DWrnRyq=5f~yc7OI$)zOf?+T_29R@QeN4+V|b|zD>9*qaI^|RnjmL)@;3{YDZ!& zm|%?NhL=V!bCS`d~ zWf{R&f##MnENKaQR;we~XrIg$)6}?e-;Io7VX4dnv^CRgeiM7ca16&UtymL3+zya# zhI+jW)yj0QN^+ZQZvco`WrdXirz}r99Sh zp7^(%YH|vSTKwn`dF5=XN>YX<{m^P<4a=~Tk&O|gT*s(*nk5S}6&mr^!`|m&U@WCN zK{`w`n^CzB0I^Iijvp_RDK7M528>=UvqbgP?E;owCw3S>@%YiNhKpIhuOa3i->~-F zj-hZ}D!ATGxV=zSHEbZq{Hx$dG_j7LIr8#{ISBd<%q&aZ7J>SBEu@DdCq@>Z+@|7v zHfK*+5zLflQ%Z9_H7@N!ThRx#r%D1%SaEo7%ri!kb& zKYdLs{!@_Pa{cvYi`_LR3r94DfurSTo4EZPLGOzE14}i%QTy^s)Ni=uls&0L1~H9@ zMh&ws`BB!`|J;9NuSA>Y*Z{yR(8;gVe*m1WJ3l#>ACGbR!i8BntW>2A0Uj7Fgt+*; zjXa5v4*d@(=M_!=FwQHH@7sM8-UnVa3+CIfBj8*g8i-ehkArW@C=E3<+Ovv1hg}zh zsvb+>orGJJCGf?E%#GUp_*>uvH&+1;p(9vMt-^pePg_A0!Kp%Bi6UjC*=q=+xpU-#UQC?xEp$@XQQb^=rO={pKV0b^ z$1~aJ3l1$_=%fy3F)@}LIm_Rb(4IU~30aBkm=$7REheg_q@usye#RyJ&V^3sl?bXS zZ|0|1q`$#84iU=;{r9TgVy58o)2{yzp;l1W%#1*>2{G+p*-P1N^1$nbB*Co1u~p7F zMD2DVfOW*TE}yqs-rIeK@mL4{w_9T3r`GzRJ`M38y*{(@+G81Gu?v(~nSpS3`Y_Ci z9Rvn-Y@)M4=F5Zv(!2jB89@^=?Ye?yhHRN&T=B$f`_epSC95@{N=2Lg)<0(BxfaK^ ziw@h>U(2_6cge?l9PTb(A?HE^W)JX@J#>u{+j79W)xdNO80ilYwFPo3jneJtPZCt$ zLJB)(=zr~3AH*jIRGy~UuF2vkkyTMHsx>$u;k1YK{-Uw76c@olZ-O*(cgJU;=k5L# zx<|cJS7|JtnEjNK#6 zSPSJ#Hr|4{=`uYm*4B=3W@Lnyqivf}TR$~K@W(3<;nE!mJKWz_V7xtc%Oc%)50;jc zBE3Hc5Vpbi-&WIW(vDu1&$w^43qtjlYV3s9k{ZFt+i!8@dva2I75$;`cb(M$5EeXE zUs0=}KJ}uHroWI=eDS0(kA|bW>RXHT&Pt`WYG|=0(K2Cnr#gAzxuv%eG)fHelB8ZN zK0`SyeKf0a>WeC%TF|}&!5WAGo%Fwzi6|8qHWCQ zkh=K;&fshz2%F#UFiOh8kUAAV*V24xLv!`Pe@PzfrKmjYG@d4&yYVZ^;kCGDmAo#< z=w+w#3~$3b)6Y5$d@Ak^n8ETYwQFa@C5{nQeotA5SslFdKraH5OIbsK(eZRoU5H_I zgD|nzr8~AZQijQ`9C@>#-RM4{|MfTL%{`9`fagmROHa%Jq<5+$=b8ypi>bom9({D= zI+a5oan1(BO~S?+tvpL-qQ@IuuLUGoWv{O+~;$_f}ya<8i82&*x~B zZ|Qrtlq;FE=c2G?a#VvqV;-VNxq*ZI%nwxPE|EuIwoY3Ow7?^*JI;s|9J-1mSSaW_ zd+jUPo5~gRWp0p~SSvIMD3|ob%+uwXmuWlO zVki$xB?j79as{2?<4jkFHac4MJh@0ydBpQwrL@t&af3_0D%D9qOXfng2Il?cw#D&M zM$I?zzA&R_5;|Fzbk4X`hTSxDUE-KkWY6{>eKw6(Z68ds@`U#J{TPdU$+C=3JB6m5 z=#PfnFL||viX#Zi#^J!Vw&haNu6Y~q*H1=lsxzWhgV`<{)CXT@&9D`~lZbI6m2d*b zS@Z-c()*~zR3EZ-k!_p_g*oPjzGbXoG}_`Ev8Ke)zUNS5NmB78F{ekxAbV3hK1Mxn zKxKsIuLpvH>3JZxl8N<;2HjR#_Pg(M-S)$ETMg!=YeYAAdFlWwsM`uV-{~>FeLgcc z@E4Ogmf7pglfcflYhzpCnbw)js=T;ZeuPf0OM;3^YQv#wXIZ-HyEzW*I92O;9nrnE z>nli~`T?;#>-6TV4i^LA@zGSITW+yvY1O?RdKIonhu5{ASFn%A9#;wWP_4gQ>v+I) ztcA)NomX`zM&1Uq!3pkW-!n6($ z|Gb?7w80)$9b-!QLANDi93MYz@)C{N$5relZ2jeq#A-`0hjI|K_q&qiq(^5`Nz1u} z!q>&=Ey6|Bhl!?AY~r-TRN6NmIPeceP7h*d5tCmk@x0tO+bZ2=<+gCyci9_o$odM#>3W{2`PcMI?Jw+3X2qv$e zj63p5q0!YEnk_IlsvH&{v>rq3)!PvlGWk53Rgrg-gDQK^@&YYKdgtV&dk;mP^SWCW zMYoX-hpL;GqVR2{Uc5T#0q zfyWCJgM)!*_&QXEKv%S9A2l1^!qWXI6`cC1)Ym!qr+q)F<0F5hJ7Pp7~~r6K6YPZS~#utKv`CW9B7l=Ud-0r$cl^^)K9UO2iBisG?EiWV!pZ zC;iXg(=Lp^Z`m~8*_%D0&`x?&|$}0a*`Vv*4dI>Q&cWIx?vzr?vQ~tMV7u~}pVVm$grg{)JW8Zw2P4p**hV&M} zsTe_a_z?eCMBK&)*eL z{iA*KCGaOYW%1v#F!CjqX^>JJ^ZZZL+QGlFS^JcW;QlO$qCc7?@!!|_-`DzEJp1oy zg(lH|gTQ}dD`EoqZ*2W<%K9%7_&<&dYK6sFz~+@B6vEnUU+3CX=SQ};?{Fvv%X>Xi z4h5aHo*yW+0$yHj>9l#)7i7GF8DZlOMfg65QN92(^;Tg(ZYj{n&Dlj5^&FV%*rIJS z`yg*Oy5ck8VMNLPp-r9&o#$w5Q8m7E_VohUf8w^eI0UwR?Fye8gu#p@yh9H@WS5LQ zwSvjLDCrA7MH7F+;&J?%`H0%}%p0PJbm|0G&^aM&94OxB`-~ITHby~&-No3;9-PKJ7Gv{F${<3~liPhYyJ#0T? z^u$V=h-MEZ9-CkMte~`!tqW$W0BJs>^KYcjLs4Pe=rR#av#`8v!@OOo>{llm zU(F4cm6fgq?h3ATdCsgUvLq#n=T-r+F~bXvjxC(c3e1&{PEJ9*6oJTg zpt%{C{i)QIn%AuDI@dkY)ZdA-9`1Cs+)Q9)Z^*+8ca!@|ltUGh8*rElT?L9xb! zHqg!?TKm0(N6lUo+q;_&w+?fgWy~l+S+D7_B{tS?9NLg?Cy58W4@UWTC-ybC zs`F1;_JEOvi3y{DLg=9wF4LFWA4T@QbjgdwN5g6u%xf)+Duai2FE}C{pVq0CA%yu3 zQoS_4tG*V#fONB;!*)2GoZ>L&5MI1E)?bB*%ry5KnNmw&7C3i-p;&wD)L<%K+Z62h zc#8I5-nobT+^pRZ6|L=od9ZNTmNUV2-kNFnVf| z*I6;Z!ZJuHeTvFJe|F%|BR3rqbEmp>^VVUpbo0dj`Rb9gzGP)#| z72lo%;f>C2J#A143?isbCBo5FA=%toedqPppkaj8ILu+57=o1%8{5O*hY*`gr4>wk zo_DI+j+2krE&@t%E5wTEdESl_o7!BDx;a}hbVnJLV9AyZXjbH27W>F`@_&B#7we3+ z`cDCs5Im?B|)8ko%Oey9!N-edY62Us7S9ahvKT3yFl=-;85DfWC$Xf6rle-nDuH!l6JOTfmHtKYj;=elAzZoem zxp#t_n6jo8GX070(^oL_&*mt%ofOP=L;r&_icG+dbjOm% zpz=^W+S!*v59<{;6X}h)#3>l)LRWri*nN^53%g}C@`7MQRP-tfzVA#?f`+6j9Lt@^ z<+6d;OYqWNsHBHKJv+^v@&3N)u@*BG5<^}SV?6Ly*)6*Vxi38OGkSIH*O{JP7)FwU zUt>*pV~en5^W&BbJ2hY3C}K?(N*0^Zz|aWb&)B@K*=s=8r!!K2(u&9_<)o4nxEmwhT3Y6Yn4JdUL!4^b0dY@s2O$unPGt zDR;cbA+{sh+f&;K-*=2TjUz8UAeF+(L~+t*nFT*T%Yo8)gjcBN)t8#oaykTMqi!=5 zDA#=#V$D4y)`(=&t}i5ZaVs4CC(QgqC_*#l-n!{C%b4zub(s{8vE@YzDml=*H&7$J z*uOUiO~{btOSi8|vP^BmK)uIG^qp8DjGJ%BTPYNiN)lz;VdB%F&1_y`%JrmN(r6)A z!+fX(l&#+xzMeW(iyuvunEV^*mn}hKV|(7U~x?YR;uDjjt6vQ^enf}O+j-+_LxQ$B7b)2YlqWg+i^n7?ytM# zzh-+HUYxMmV-f0_vmR;|Rd;66Qfv$4W6Xw8u3{@>@`5i;`XVv}=eFiVZR0oxw-|`u z3{TF=TOW)$hz2+mdb)BC{JrwnlpsT9aB}{Z$!J1E`5qNPbcI%kzo%n_mpU`DVX9$e z_sMZuDejdq1TC1`UuM%h1!#fTkSf(Tf=b9OE0GVmpgplwk4&!G`}}i|{^dfS!v&2- zY3Q%;XtU8Y@}KX)P{cng0!@>K&FvkaG#Q2i!H2+i!y$zemCDSh!cnzjTY*^wBYBtS zTDM9{8F|=uAm?SqEi81@;>5*Pz-Jm0dynq6!vid){snD7oglb&LtuvFg&l(*v^zXR zFpHP%_J@=cl;t`n`g6PZ&9Vn(JKtc7=d|0Q!@|obp{p-`puaidMLG%m=fkv)Xn>6L zGa_I+ks{Xd#G0nk&sD$5mv&6V`HjxFD47`h9t#Mw(tUf&*vU1D)BN}sqaJ9_M0e9M z1t1rX)8?o@SP?8Q_Fv`G6u+TZM;NU?*V^mR@=YKOA2;yVr$i#P9gb1Ln>kD`I#7kE zx0xmHdL67aF$~qvxwJPdZDPT_LQv=~O z#qIOg9(NgM%j(0pJ5q&yw=e1M`Mp%e%7$PiXcw&syTjq%rE8bHiW%M1Je$%so)T$X zqQsJ?$St-+3p-Y^qI58)&GMS}Ulmc>wf9e$Pk~mQUZ;3(>n|x(L%|3fTsuH`r8?DX zJvs~PhFCTK^W1#I;R-ziEwK{KyVg-UAgj-zd9dE+y){Vj43p*oGP}3LYUCet zU^D5^eGp%5=p*)voO=f-1I?p6cfL~R+-u&p(C-1jq@zyxTW_M!8E1b&fceD-^)`rY zn7aJl+cE@9n#6-CiD`?^-3#x;qP}I}Ckd;rwA(kns=w)2>K1lH_j;h}1V;eYy_eta zOxSy^)>-TTcf0l@HZE_SW8ExWK#cEKz(GSIotQGa)4Nt^UxiF`ZdK27BGP^_805L5 zUzG*oL}byo1FD9u5;}W`6(M{SnT1{4$edXMyHiU+f?hQbuS|0KIYcj&#g5p~0@5!| zd>I_E&&CkPz#&cf(=j=k^Ol1j-7$3fxr%^t(C?og1XaOh|F1}#r>`gUu@+LHYS#>aI2RYSo5 zk$!54+HhBCcZ61RT(Q$qpEbdz z?$kWsgGIxwmx-U}veSk*Pgedm7B*3cq`v8%Wvks5GJ1ePCh~Ja-`Z?EMZDXxBw~=; zX7;}eGY{Icj4D}???zAAK}do0H%yE2-^XZ2j5Sr1n9p_CWDTC`&*k$xJXORJR1tl{ zm-E*s)~ti8-q@O}^c0zdx@A^71OiR>!SB1n-@h43#tPmOMSaZz&C7t6+fprR=Vczx z9zC-aE4kpyQc#1J!UDiDWJtgpW|jv+c6hn<^B|C0F-+0EXdyaImBrOryicS?v?e@Hf9(;iuz00k zsbYH)ky?H^LC)zbLyK)&)y54;a+O^Vt>UhcxR9;zd@2x|K_x>93RcSK0~s%_Lo0P2 zuVq;o-Dec@t=}FxM`h437FY>K3fy^6B-tbRCnO@h*CXl?C87SukmAYKxbGW>`C!pH zuiNHgGt={{Blu3>*8f(96vkB!v3ZR?Q4utp+sxGOGF)B`@>r$SF>D@z&*~9(gd=yhQ@NhInF1gap>jhRoSCs41gP7H7=4izGdsYp_f;Gw ztq%g&P?0+Q3^5iXzMs_WX2Fm<53;^hCA*1M2N>X6ZzgdHS3T|>AcffpWP`4TyA{LF zk$TEutxh_1`=WGxe zk~tf6=X%J;krT9!^!U~nXwcdnBqqmjM@#48oQ7;}o^yUZ12dFeOXzZbY2GJN?Dv0C z!0iZz@{cY-_3E8Sb%uiBAsX5tdd?5RQ;mIXhXe0&zFqg1hoJ-vK^sjvdmTt+R5=WL zFa{7+TPG;CVOMZEBFxEu4FzY15SXbY&xzT2dtaxbc_(a5RU*lH{O_!)@CtY6%tngq z+$*N}3i;I1iQ#VpvhjNkePKsc4JCV=(rX8%o1C0PH1kq5)(>knt*0A3l3MqZZuybd z(*-d@mK)wir^2<$_ZB`>wDQlCWYNa0@yvtMdvaj{23nwBoP`|km(2HdcZ9O)J_sV8 zP`fOTHq0Y41V3?o=|z;l{5bvxywuM>OwwR+@^<(abiuI2u#;JNZBzW?cVk=$5$|e0 z2#l#%+0JNuOkvNfAtlDDiXqgJ#gO0#axXCZ!U3k{kA%_J`rj7K4lfowtvEe&qhp2Y zAWVwwKRe;_gRl~H*ni{Qqm~PDFp|&JIGijUa77vTvXEgV*yU()KSpUp$p(8+(Hd{R{t%3Hx$;?cx3bxFeODAP!xW%MWFuH&Kog~kD!j#&c?qfUThwlHv$-b zucbxr3DZMWM31tdx3oZ`?`uQuXmC1>ciVcEQ{vgl7tidV9_Btd@hV6-BSK5lw0(mK zg0T_jIOsfCrng|eeGeiK+5my>*wd{6Fe)`X04Buhww8WWHPN_amG`~myfpZ+}oX4rk}u|2Axk%*BTaCo%wk4|1tb7?bRQ)2sU-#mxmA&nS z+=R>e61$Mt6g**y5S;@lna_FDFev<9JX5>seeo2hBO1YwO}BWypehe+&K9&y6ImbZ zfz2+0Og%*u&$HylX@4L0RC7G7WE2xDe$rk*gu}q z1AQDQf5++cRE9CBnrA(+9ydaY_3CCjS^JBa40+^O*efIzmGma!VI9Yw-35)y0|x?& z1oXGLCl(SV6-<6D$C1}{mh{qgArp_KzNIBvP{ADpdhmg^_L4tmmwqz5MWVT=t@$0J zVcp$?>LMz~lW(qUUcXL}diK^?FcS}*!<->wl#>}pw!TagX-Cc=_`52I-)UZx!-YN8 zLPNx)J#n-=9Q^gPQ?F{E2%c%4*jec z63>!7@Ej@JI^G6vc)m(IN5o(W=R!!GO{PKB_V(q6soqz9)E>Rr zP^jOq)Z3y!rSrrH%j4$zu8UNJ&f%`LN%sP1?0cJdQsoG6LL3wc9 z*{-&2^zS$2rHOy**4CDv;($4|`KY0xOBWS(BaZU-kaEPo;`uY<$x*Y=_g{baw7Vmy5pI#=8}ouD{ld3Wv{!`~wS z=3vIrU%uyQVl9fr(oKB!3Cfn{H?XS?et?>Uo*pd~8+OcX|K}`+!8Bpc)gLsTzp;jg zVd7q;9Njn_&9RH;?Zo_ChC)q@qwL)PzP6ZW2t@QYE{EjgZ6KTg@qK~n3Rv9+Z>fR|KYvW zXDD1wA_Hq8d-`-?dLJU}SNV2Caby9swngNJG|IUO1_D?st*V`WFeCFSkda}W?)O%g z-Jp*b{g3`zWMA1b)2>8d&Ob+H6;GT|jv8y#FxZz5@GwMY4bjsVAtO)>!xMZDQy=K$ zIrcWb>6`U*!lB^A%9Li6*sLs8unW`d&Ck3=Kr-`y{!ftQuHD|CmdBu^Kd13*=t;9n z(VA|MIj&9hg_V94I#W7^;aUC1K2fD?UglF$O=nH8g!)%_5m!f1mgJtFMhj5MDnR_I z=s$fymCxq$i}XlIt6_`#%IR%3id)blEHB9%YmiS5Mm07PAtr zr-yRQ#76agr)XX6Poq!!shOLHab8XF$Zhq(?;-U*NBbd?HsHnm@EsdV?!WeCe>4`V zu?py}HD{s;U8-@Hh+cA!$_5jy;sw4R9GRuYIE=T;RM=5=u8hFR5cDn!Gh3Zh-=ob6 zyXKW%toj-<7^jwZ1?MMxdQce2W2eo1Ec#LBlvVg%Q|d|z*>GCa4id7zzpUbX@D%g-!V_ESZ%^y}h0Me4`{1(5d$RQ;=AnF# zuuIH#1~k4il+0c`NGX|VulGgb%cx$nh4I}ti!eVbwd~wBM&b64PokytD{pkj&AMxp z>)hHZe@80v9Pjg;mX8b*WR1;=2j&%CxwAM6v*cRVULl|U;o3yj_1tN1TN)3$5Aic>#tb`)Ut{FJ7 zG5y<9JDwyHIKm&M2Ix*{Ysb-Wym!&)c*EPgm=P~T8mg@ov2t?rK}tx;_%~usOzf9A zb|~P{v624n7tGgplTzNl=H=4!;)cq8cv|?ET&Ef`-TV(Skl^>=Q;Z7UEl(d)e$Z^) zP5-;cU{~T%k{TP{IL|DMcY%!iu$R$SFBCIL{!AemKSjWx2q`5QYdK2jtOOMc@iUQM zzLRxPyIZRL`Y0RS7YSXx0MZWg-f27ycb{*z+l^=s!f*((66dGMT37XgoW+<%oGxb# z3=BOi-ubL`W59{tkiM&{3nxfHb-T5qrQ{0TV>(+D$JNc%_Rgd?oru2mA+LYKTqEgA zfw3eDw%B~$M;xB((T{$lsIQKKMZ!M=retm(`3(=EW(WL zZPDu2U09NuB?jpocCJtDCz>m)8MsgNNtcg1hj)_`o5S~pN(QcIpCbZ+@Lr1fN$TRVHIDa1!c~C zug)oMl3K1Q`}Y*vKX~(XqZ&x9C`#)X`6>tpPx|d3eR(G4p6}tK8%ukv(3ifL+0k`f zqqU2}o-*ax`hQnbP{>Q$V3-U$Ft-2i33}}A%_`lOMWv6)OGi_j=fUXGWvV_hT6mF~ z<6Jc!e%6e9XkEOJ;@p*8WMNl`?`UYq`DpW#9i+iKGEQZkz4-S>QL2=Wm$$iSpK*>n z6}KP7tl|CP!{sSwF_r7rnT3UgE6`5Y2U}8V&GsDZN>#U361_QWIYvVo${6@VZ2RHB zV-k65X$G5b&HFm#`FchwsnN|3iMc~6>NqZVF9a8#l&Xck*<3ZoN}uH*bEW))%wYV; zj-it~17@je?aabL?LIdVM|C z&ZFUCpp1Xci{I^&25-Jo|E{QnqBWx)k_l}?ZjS684{QdKd>^t_ zQIwI6>l=PUf_9kKd>!~VVksU#o~D>#>uENlCv3|fteLh~N-&Te8V9~YH{pyyste1SZe)D{B#k=gpSTv;s&4dJvl28$Q%@1LW-!>UgnXgHSV|LWmM+T*0fPtS@(v6TGT_#ZAI`@Y@r zzuot37Q}sTw@ZlbYpUkr&h6XF)W-X|pIz-kz8(SA?<`)Zvt+M__dPh}u=&Nwz0*XG z+W(%u%<|af!TxeFT-;^Hv5xV4!g0}@Ko*KbIY+JHy$ml5$g(P<^4Ih?k4E%`sC(E3$JYM{zvE zvvRN8`!C`z8efUwf(;G}*A*F@##bju3qsq9x{uE@|+|he^)l+g<=WNxz-j z-0%FJ9I^4%Zu4KG9`3>kjf>dR&gq@DbaU)jzs1*cs6iq@bw*H9HoN5E0_H8Xo%snm z7F*g3;iQ!jigr!i_-DOPhZ|V~PNQ}-3|oeC3~ct0Y%5e=D1OsepsdGPOR^nCy-9Ai=W7!#_K=6&ZfofMj7^uG*C+Ar9nc~Ns7pm%<63I0gnb5IFl1fwyRQpMfAIxD%VWi5fdX9m+wE87rngG_n$AxwRpha+#=tW z>fLyu^iBr)x_4`*dKy1z0xsU%5$Z#ywHA13j*j|S%SPH3hrarBK?eWg16wLT#ow*; zk{yX7V_bB_O8XmOU#IRMsjZ+=VcOmhrs2f<$aQN6(o&I-hFy6hyZKIjoXJdg#qEci z{F%CS06o3K^jc%Jm*TZ2gmW@SIM16fA}FJZx0&*`@s%AU8=GN!#c9y&G1;MBTOSKd z@vj(j#`RTvW2O-wNi79?TNp_#XEav6wN&Jz*;Hm?bjacr8I6(J&(93~Y;@CNirol@ z2=0kdUxY%eiltOM-bvE77n&YRVjdi9VtSFnBas~~NSd}HSB7Vk&1mu%bSNErB4gOC zy|T0Nk>tix`xCs)oHV=t09x;_5G{T?%m?hoBvRh_^T)(z_im*nURk358RS>U#0X|a zw={#2)}BtgYUZSfFxS4(6hF-=pk%wuD(~T%6$d3vjfZZU( zBW=x*OKlQAPfX$`EVXx=O2`(pQknLs70C@nI>xqW4VyL3J$oAWD+QlQD5YLjey_Ex z=Mmo)KTc?spHghAns4DOsq3V}@|5Z{W-R;2uFNmb&I{lMD|@4ijO0X?FII)mCj|}t zjUmj_2qcXZH`=-<{w58ys2Bm_uJ5ed3UFJSqTvv^<9H8?lu?T-`OqMNVEEj`$D${X zllk*YA3v!hO+SrfH%^z3W^26+gM_N@?wL4Fw`Y%Gy*^h)aMeZ*}31pDiP(C2$Bm$5W(?~gNhITL1S8_AK z$rljYRfEP z#K*8NjLr_Re$MJ!IIP`PXlPiK$(E>a?RKoi>a^WdmrIcY7x%{XD$jXxPThqW|E#-> z>n%~L&rh%wEGXut{~uo;ZNsrt=D|PEEsvcx#U?$u^3OHM93%Ya8kF~LVH6_JzgB3q zY9y(VpdnV6azBjdf7Egky&r;i<~o^od3RJ_Xmv zdCn)278@LRWAEkIC6)E{qO5Mp!X7nMgVgnKHk&Jt`=Y(aaum0q* zlBYHunq}YC@_894N^2T33hJVyPE4NF;~AV>Z*r!dF)}fVx4*C)=3Zl?lCNL-tuz$9 zzTdk3RULL&8DRqvagYbQ9di*C`fiBTn~v-Ken z)?)a0v}?`bUb&U;8k(-X-5ATN4Z^bnLaVf#dQ}6GaxEdznj=4WO3!rr4aW!$3KHJ4 zq?(WRea#q>EqZ>ur0(S(>_+=|FA)h$ZlU1qz)$=K{|yqj>Qnx+MF7Hs5k$hCM)~B_ z?n9Z{ELm=&JBD;v?g~qaOD)g+I+VGWffmI)yBYQ$t!rL4pd;_v#qTs4YDU(JUMyCC zdplsGOQA6x>xL~^)!TT{D8tlTiG+m;8VL>stD+l@*4?r7tS3a#yOg<_E7hd?X0lc$ zveHKa^CV$Y6vXM3Sm)kyaP8L4P-v&~|8DDig4%6Vn3J-DsUg)ix#gKt%T2e#=T%bt z9+gb-j5NPZbg=A93Hn(;*x(}#4bd1Lqe!_{Cr%^Vc%xDyG5kP-jx`zA;lny@rk-il zzCHGw65gA$sgkn)+>?s5ZkbbcBu(m zCk?9=E1lnYMk&A}MCT39xTWcfXl{}E_r3CZnk999&IIw1u8cB?!)IeO(|)h6Whh&z zoX<|l1kn9i=vsi{jIp-mQRm8TS0nQowX1CwxH&EF_8k5_sT;%ESwFwsF;=M=Hl^7$ z873}EjU80v^cP85S(4GbI3iEb+1TACyMbrZ82Oyv#VpW^wT#cUF1eyozjrk?jr7=` zq>S>uAMu^;+|OwtDr9{ikgj*E#w+1lFPOM5>37Y|upMG$x3v#E6U3IIWuPwLmr zC`C(%A8fg?m8@b&83}oitp03iiQV}+WD(QVh&h{X?(1787c`awx#~-c)bfaP$3@4q zXB(sZ4FKU|N%H%3NyD@^+H>0vz29ew#xX8ztX)Myo1>a~)%2tFpSkxk7-Vt!jC)Mc z4))h%n;i_h?Q!3IFfC6j3uwgRGA#A7wGOS{x(D1_$D$o8u1`5@elEmlkWX2oo_q35 zCF|7V3xJ}sErjm9Rr9j{@Iyw{!Yr)LfTmiFt=X(zT|) zb29KRSw%X6L4a4!SmzmqN~>-|zjt9agBvSfIZSq$;OM+b!`jx@)WFZ$0If+M7l~at zc&C>gJ$jh?g+NBlZ(d7z)QtbI{{mGp)BNnr0ncBQ7bpsa78K^pGPD$n>aBTRrE9il z<=PimEE*ckiP6hNVQG! z>BrP+hao2fu7#3cymDnjB6y2#ZO@P_HmWrH*4li9j`p%|cT7-r%Pnnk^-Xmzl-R{n zSKS;ZUbR=AxD&6IX#;lYE$bZfkDMb9>U?f0a)>CaGjQ8J^O#CnNH#gCy2kS9EO7Q$ED^T4Ya= zTfV#J5IZ_ES44OSuPJ@qHqncuR!Uv>44}wV*KwREE)bvC%|ky~mc6mu$ro=IErIT6 ztBA0vbV*wjo7OuaZ=q$)iOY^|eIQM}OLFnCeQizfiF(nd>X-J-?*bMY)=?G?6$OQP z;+fjIPP-l~NGy<675^}Hc=9(-X8u5gnB9i|3y9f4y2r0?FgD9fGp`gq#MRB*GABwZ zIP&|FI4dXDrRinXQTo?zY!3T~D4I5E>joK~v7k$%QbXCz3F%b@bXsNAtdjHNf?fqa z7x_|+qs~h^7Z~l~^xPrcZo__Lxh&?EgY8OTZkEH63RYK_acW4k@wy(How9HbhXGSu zOXENYF#muXx$1F#Z~k<$kBWiA==8$+ok-EK z$&JPZ>YU4VF|OTY{GBUFNA0(QJkD}dzdJlbu07D-HPZath1Jh(zRaVgm^x!rK}ih> zC;!H>Mak3L(8N*vMqofyewf_53@^*bfj=7?#e9nR9wQC@7w<8ER6B37u>$`L-STyM zS|qe2PRCJjY^wAAYwB^MYY_SqKjRxns^F%6&)s7q?ZMf13ifsG=DxnQrt!AO=Es4C z;yV)-?4>p;ftmf&G(oE-dJQgZ8~xx5vG`E}pG+UOqI36M4x!t&&PmHCowc8KVB& z#71MR)|In<)6k{b!R5=f##7cPA5u1x_v+Au_LNrS%}Y?R9H+UP$7}oN4rQjDCuRiC zFZ^F+1YUMP^Ehl$lPG)2o6QzMrwqu4J_A>s3?x*2_DTK9fWpe^qzDXerc?1iV&X6Q z*PZ0&q;`?fKEAmb=~?EU$XqJ=2&uovx6AB>qbdqF$&XbgBxv4QxzzRhO8n#`cb<|Z z7iW+|M%gJwiKsh~d^Ei2MK_isJYOnx_}GRYZ76oCX9{%~JDV*sMPH*mKii)*F`1#^ zG9B$E_w(V<%05%a)=pLy1!}i7OB}UB-{>p$L<#B|y5fG%&x@$WLH2Yyty7n4Y8Y9 zge*8CgOBtz?o)Oi8h!IfrBMrssGt~&6t7=Ph~3H=Sr6$f!-OwHo}KnF)8>18JIqIG zBfdMtYe-k1g?G}|CODdpxwWuwIOa{MQU-VNbSPV@&OCh(97fzzkHX(N?Z#w^;hn$g z%U)9J7QU8aFTE9D5hm0r@4x)Z?up#BX~u?BUeR$G&t<)lhO}@s|8TFN2W?8P3ZqN& zM~~PKN5>CWC2HRBq!6uc!_y2$+%l4ra7^7BrIpkaRm6t>QZMl?J@iuN(371RWfo)( zG<-bxpW4>1!mKyUm1<>gm9jtQ7~7Ryrx`eb^Q^zWHl;|iClHdWSfx#RF=^%ORi#G}mvY}qtV=$T zlNF2ExlKC_uVCy$G+J%bOs+Zs5J~03DeS%+ zXDiCL`c|bRu4?rDU87ARrlbb%T4pk=)Lv9nL3wt9gTqU+BRBAQcYpEpt843lH?d2h z4iib%o(+{&JHtY{J@eZ}>$g!MAyx0Z-qZAUr&Og+r&;kyGz;BsglVJc8P>9vrREdt z2FkCxRuVPgiacDs?70elK_$N%u?f=AH+zG7+!CAJ7jhl2pZ;E7)nyXw=6Y^bh0UHH zS{gT|In!q69SDYYHZM;g>g9Cf*%CoXD-Ih$&bu*?qTg;4*#^bNA1$5?niqA z5s+0>imC2hNmP?Li#F{kbq(78!YxlNQRK1Y!1+7YWNOx7YiVsumjWk)q7tLh>jj!y ztGk+%ei!x>C`*ob4z@JT#Hq$l44*Li5w}+Hg~MQYeWa0HSaWn}V?a%bH4GA?wcPz0 z>xpaLgW}4B zP#nXl`9Bg{y$=LkYi=x#J8)S_3axeAaDB>n1lv>ElHOJ_SNKYN>T%oLmjjBs|1f}R z541*H2g;1ATcDFc!i2^&k5`zwnF{Zdct}2xNxUT8f>ZnCGppC9ML9W(fe5@ayxRIp z&l@i13Xzfm=a=W+Paoin+=Olh=&+rCD9!(5I{!DJJ7$ht9_SsQ{B_ki;|CpFS%*+* zBIk;r>iLM+a%ts~!LE892z+50 zU+cq#7vtS%#9n$os#AtP^VN$p;{uO@-#eA5pZ5rV5%tKl=h_wd6D|#ff&9>`8g(~b+YVxK!&IO4 zg%gY>stjsV18i3>R+U(%1feOyhiYFME%f&D#i*0BC$0_UH`X1hNv*cf);tqcH2H!n zij{*Mp(4gQCtfg>iD@RboG{n(L}<7t7P_U)A|dv(HUWlYvYp-3o(psaxuY0#)EDZT zUO7~+1oOKnn1WyF9m@#h=yU02%y{B@CgDM-axOyM$$iNck_XymCZ`RP_N92Xw1`F{ zR4eHCd7FS)!MzdT!@wI(WhD^65020&q}qs`4~4N-rZBIgK8@%)ixi=FVX{g8>4&?k+>; zv;*#_H_S?7DJcP#xN1ZH{qEgRnt3wbJ?w=tHksS!-~dVlrYBF7Y45bvxo9Rjp{-u% z3J5e7Z0s0mh<{curYM{&Rw_Ajkkede9q6I4J#cW6%597i);qK99w76R?Rz8yywK*HN1tTL>#&{q!+#D5t@_M^xkFw zQ7mWOLrTf7tN+=62EUx&n@LqHHg*>)hIPg7qRVifXV+XA-HSVZCE8u-fV6K;<3AXS zt5HP8BJux@u{dw@^Qx|*LjBCc*l{AIeHB-pQe6x6$rhC?N4*_0%6Oq&V<}n=!khw_ zU2}3TZZSYMC3Hcb0od#vq!nAm-c&K*fZ;S3@YTsg-Wbv1dUAraN!(3U%PE z`qf%ooQjrgR++S&fAURS{o%>s5L-O*MNM7#UN7h`8m*T`3 zwJ&Rh4!1J{#X^yKcc459iko*gu9lG6^}e#&p^| z`H0eoB6gQI%h1R+*u6#i{2aMU^Cu1yX$uvGd(AgbJrZOPtCNffP@{L;5MkTa#QM8<9W# z*a8#p#pODDP_K6X=7>G(7GqDb&7hb{5uZQsFM~q0XV2&UDcMVxyaxsb$h43D;r>a; zIvyjO?6Iw2J)#KQpqUF`Z=i`H4EA9js zk)5YG{^GQmz>rm>!D{diNX40H**Cw0Of_9XQIl4<_w3Kut z7^#D2Xzm&R!CyC+iFIL-KXqZ$ty^zEu*QD&?2Cwq2z}h!Kj+#D#ke2g+y-v{zcfqU zru#Jt)6p5_W18BW@$?Q^Up4%M8ysbgpBf@cg?Ythk{X6&8}Fxgk0W2|=+>7SVnNB! zQYw?|D*UmeV@khW)Tezy%zUp8!ylPH%h-uPEh*EC+yD=1Et@9$!P`u z#k#FH^Y3@`H-JF6I!jb6@}A!!82$%q!<~5tuf)!Q3;3xf;5s)rc}V>$7)Gq28z2er zt?E7u;+>G0Ce=8*L~2CG0&l}qVRLKb$1Cr~N^PGJVz@n}K|Cl%|6|Drs-M(c(CPwO zKRqBoAUrOo7~`RXX?Gsl&}Hb7l*{sM389#mJAW@EkhNZx7(^)fxi?di{sf&%JG^Nr zA&Ss4t8B>&;d#9dA-i)L*`31YODsDD*}1uc2L=qQzJ7h-INovmbya03NNpA5Hs{Ir{nSe^WNTS!?#m@eXy9`8CO{??@ zsA}1ORWW5C8FJD75?aLUg)$`vWs@ZtkGjGQ&D_+aq`-Cs4nxB8hh9R$96Ys@`p-lD zt?YvpfHft`;DoHI>Hp(?Ilsx%oP@{YDHu550$uEp-ShI2b0F>lIckO-GQM{e%s^&D zOq6|(T)0eeE-^m7T^WCme2;zKG|W*ft_$ByPmA;bSA$`Cjp{!IlgLTfOC)+~OixUP ztr`B|e_oTS&2>8bwte?*bowdKkr44HItrbhy!-}1xX9_=UIpb=&hwS_rYDk)v=Ix; z)R#Bc$p>|YbNiSR7tjkuAd2&}kB!q6n?%?=q!yEamaakQpmA{2ON7P~j`sHU*4(Tt z-m03KoJMHc&q7>fRWvo(p&KiXo;eL|=0QlyPjz>jAA1Qt$Z~h?Buh9AM>>tRM1W~7 zGdSu-b%CkFvO9DHT}}Jhm8{B!JNFN14Dyb(4)y zY7x-*)OD%A-T>wMW+z;GT%Z9t0x^x<^+YBuR6pTW-P=;6opl7IFer#2(#k$2zz^T5 z`6<#PC#m%WJSWl=Sra9$4R#muF3{~8(Y+9g_O4V-RBC}b%%mqCJh%-9%}q^TO!Zgd zL5x2qOurft6_xIfOac*xk>RsbV1_*it*zN=O2@^Z=Mg%+@_*Txk^h25hhBmRY)q+` z%*^XzO#TNC9+Xv5Qp&M^=JDsdQ1WLJYm9tv5oK{8C^d6@L0QbzU!x0|U0Hdo+O{YJ znkbU!LqYmYGs+&CJQdI;`3*tB%cq_8-Mqi~bSEK1AX2aqn#Xg_9i4!k+O0h*r^sfj6f&;U$oLm)&3o`SULC7jXLei-E zU;*@=%)Ki(nj@d8ov-A&Fp*Xt?%CYZB6s=nJ*DRid|#X+r_aqCa3hPAhf|p38ezX{ zJH(h?AfCcqGXKRXEcf^?1&1Y)KK^f?AMdDo>C<#tlne84uPiU=>p<30PHU>!OP`S^ zijmqXrhcpZRQneZ6Mxr56rxQYhV`AAni9FV1dU0wxzn~Wisa{Mn8Y+piog_k5Zd*H zW4?lEe4J_`wu}r0ZZ-`imIy7$A|lDVTcDFnXQHrcKE?~ne%=VtZ}U;y+8Tyqv^$nAjsj8SnR#Fy+_a$RnX)ZQe7<% zC>@Oswx8_jt~v>AT0y?YMbXjF-1kriMpjY`++YKfagNWQKl8dT-VK%XEFJ4AP`zzn z;LxMMQ*smII>L$_TkQwe^Z;<=x*6T`-sWvwT-?R%7t;T_{5s-@kQDRpC;WQwtiZkC zXVod6v#kQD^)dR#C^{Z8lHfzXjN*l{2UvSA*6}`XC%&Q&TX(~6&%7L3@5hJw)UNJd zTQvQHXK2yHjj42XhHP?vyfYp;p6j@^NAzG?HMqq4e+7%BG&>j`oS<{Jf?Q+`IvDX8 zo@?fWu(iI$e*ch-)nUm4hYzzkK&I!9zzm;aX^H)1E`; zUaYRV6OK|)UVY}Zyz2G`TbYg=TKkJOOpS>f)>wK5t_>FC+7L#rjk++=lORHMo1X}J zAYGoNv=c221q#3}=q*uV3loEo zbc`i(H8R$RVoq(xBSbXbN3hW5SN<)sGt6ynl$BBN*kV{Iya%mIwaVgiuXLc*Eo=nO zQP;$|O>ZpCqlxZ>yi`+0=Y~^Cs zcuhMtve-*8RsSo^6Sshw*UbS+Xu4aT%<*4jW%dNs_+)WfcVYXDyA zux^~cNnHdxazO=NOH_sltV`f#kxCz?p02~rz`(HF0nO*9ni3ReG$&xAgn$tg!F*=8 zKIF&8M=LclsBGm!N`KxwP&f%>P|lvPk(qH8y2v2EB>+Jsk+-T@3qb9}aB5UBD>hcC zR8NcH_aSzh$U#adO|bNYGk3 z+;ggr61VNLsjPzbJ)GRl9X7Xb$0n%-Dqs9ThDCu4v8%w&1Un9mZ-LO(vUtu3Y}~7mw&M)7n>Qb8355Nv3=@wn>NMynnzGWKJJqgd zZ^xq{y5Fg|pa({3vGbhO1fy>$qh3Oy{J2LTRgqIFyIQ5J96XD$*}~yS?T9EH|j#w?UX#(|9`Dq&^tT%JBiL3a5E^l%rMK z60e2+JRSJ(;jWTVJ1n*&0-Rr_p);Bw25~vI4#u6JDWp$aZ@72?y!BmRY{*E0_DXMRK6jRw633KU=yKn;(UC7w?AM(6}L@V0M|gk?g)r{}O4g~Z_Yg)JjSZxr)t3=+$8?=MO&})O)WN>9vOi+qU@mCfZIlTle)U#87 zSXo(FKVzod*_(h8+gZ5^h}DqRE7>Lc{!5l1t8LRzS5-d#9O8q5NU3OH+> zw40J>OLvK1tpB=JXs0%-w)ri)d&WES@!+f!|Ngy3Aabwkbd{>Fc7vV%H1yz(&SV?c z27o_-^E+K|{h5Y|o1=DSQo~(cI@nccAgzKv!8HFw#qcxvUAE8E3&6zY5?H;&)FU={ zM0l^uMilXHM?A_LBO;vrm?zRBuu{LReGWU5>Dq4$ms(W^XzZ5~2N`!Bs^LTH*2zB$ zgeIlapmN)_ps<&yQGg$1slEwi^HxIN4F5a8TqQzTQHEN&Vr1gxr%#{0!ng$^7{1yF z6BX_No_nz8a5aM50+l7c9YZDp4uG{$zG*PGE(bIYiKN|&=yfGxlVTQ+_z(6C7;djyf zN4!lBoIQ8$pnoL`IXQW2$j;bg&sp+qjE9PGA*PYqhqs?AW)K=nEQQS{?xW6<=S_AY znbrvXYxSGWc!bQ$ds{Kd1!MWxpovNzZH6?JoL*vJ5BQ!I*vqT439$ZA7lLDYY0vqF zePJ)Ch=79l3qqUs5y*UpcK5(YAjK%>pSzLNF}qV_t$ZiV*Ap9ymD1|+9vf>5gD~?U z09xXmgVagzNGmR8s-5s*;{cq-STuGWB2o!APAwJ?lo|ufL2Pq5KzTJBD(TEnQ)8aorQ5;_mTnY1)aEWe}4CqN90q3+<><5PvsDhoow=7JFbXXVm8Q7Q1Ah)5?+cf78_1 z7)B6ment$SUgqAspif4`-C4$g`^zm>B^;&H$_pPJCBd7`4Mvpi=B>F1BGi>}ED0ww z60>LrNh@mb4+GBVbnJgBQE+@RFVt%Y=4V}5pU*E_%8e3nNYU|F8l)IlfpDP@@piv03(i zqPX*Coz<}~9l^$BXnf~HOYtc5icbpl!YLR8qaZqZ4KcIkvp34JZL$$7i)pYasE!b} zv$qilajg>`=GSV0F(+IIkC0?L)CxYkq2RfirX^Bw;;(Yf?u(uR>MZvr3X#!*%|Y>v%>lP-GX!t?p3TIzuLN%-OeKjmFZQ- zOp-Uw)zZo!oQnE7$rr`7hv}Wmht6p*bKGrT>d?#9E=itBPTG174!IEk zfu|v0yXDqLy_Uch>NvEh&8zNr8bexZOcek~Pbwo#ddmM!y=NU>F#W+Wl>E<#sdvAY zYcY=0o{x{Owv(a@*ITxzrBi+GJ>+{Zd|P!OyI!Ix@g`~pP*^RVWFci0TpCY#F5&fx zNdwg?z$!zH8%F3xJ0Q*twm@+#7bhr1fpvk%_xAPqSY*%;=fxG+NkvHV;@A3yJBg=> zbEloEo|3q+0=P@&F}0Rhv)^f@?b=#eDb_Yb z^5#uTzB`x;i5Gx_Q}&uj;$^5}B< z{#`3YW6NPwSBt=&$H67{!}AP9?2{>&yp_rQV0m~}e^fPaPrdj;gjG0IG1?$X0=o>& zirE8T1o>)t*%h0Xlq3t(nDN}nMrx#fvt}`}8#BBn2dvKQZeXIZ^{@UeC)Y{jW^Hc` zW||b_=l_Zf)45UBWNLx9S`9#Vdqap?%rp!m#ec~f73%K+gU;4DpM9nXTBD{k6y!7q zk=++x2HVf{>c1>TJ>b$zT@UyfEA?yuF*#g?M02?V>)90u30RaKGbKm6Up(%uLLcdbH z1_J^PjC_Chwsb=0jb`M{iU#8yNorLa)Xk)th=p6*9pa)OPic8se`#5S#j8MG%-q~a z>Vb_8`gmsVM?Mu|#VX(W9#W7nHghlMC06EXfAc_I*Q%EWYcZCA<-*pEJLlJu@edCT zjjDU51XVM+|E}<=m)XY*s&O-@!Ih(T-W;BXz>bHBWV0jestJo&`WxrxLU-PAc6j9b+*JEbim}P0fB*Y?!UqP`ixnw@vtLAziRM~i&yqsn`?{psEifVBdImB z(j=F18t+gWzOZq<%v=PXr#3E^Ev7xT4@p-a%X@db%~xLrAA;B{4o$1nQn7qPpJuCW zhRlq&cB+0cgBPJnUIkRArt2B|-P_oo0>B@pwO9AUt2rVoHlJ@W z+Z5F-U*#=GUoPhSyR=Oc_iG7;fem||Dsmjp!Z6wc`o+U3a7HoBT!qEyJws%=!Qu1* zZnmRygH2T-Hs8E*4{NcPY)+Whk|OlKCgy7~zQ3>y25Dtcis{L6f^!c(D@(5NUw6cc zH%lfKrA-DXaPJpjPZXRplF5yIVV6xIULPr{u1qO1bM%QR7%IAq3_eU1B8<{~IguD)`_$uYFqLyDSd6E-n2?4ko}B-|0Tj@C35E{(bW#ot zmpAWz0>nM4Sz3xV=1QieZyvKIbS3-vULbv)?Mwgq5km!@!>d4HtTdlwk+V{O2p;;# z;AUDbGQU_Ov|_HOpl2uAV+oz7#$FjzN=z{B;3gUk((@LAccIgyNBIl{*Nm$r>6(_K zo$p7r3rh1~xWK6Qdsp&+qxwR51!>kW^!WfIBMt{9Gzx=Yz&q{o46G96n6R@qu!<$N z9qS^eqscEmLG3}Z_>-nqK3iG*rQH!QkkNJU1mKGA2L5tVQV*gG@pW~<;E;9fx)UG^ znuJoAT7X%fiQf1%Uo)x!wAICxQHAtX*gb|RDtmR z>TZ?u=ct)l68S@q`%TXI$<~Fr!)`DT;VQXMG)!FN0_``jb|YN{*+Zf;HGEQQ>z@kJ zbP7~>d3k5=%pKCFUrZHrL(MeCD-42@;R`5r$$Ea%3A)hxd=4fhsC!I3)BDA1R;Rf( zF{JFd9H>Oskjk*g;!6%){QZxvG<~?NZ`pDPD$)ThP;`t{^3XstWa^6l6vc0fr|(f? z(75xP1xKi$!%efU)zL9iyWY`yv<)bq&a0^GqkA1k8W%Z_c+BDSo@l+Z=qvYb%|^_Q z2Jh|RZn)z~w{HB~YuWgn*1oiNfEEylO|o;>FEeN(SsFJS#x@HD0q+y5%%hQsnx2`7 z)cjXl6^ey))Zq2^R!2pglI zMZ+G&z@v@=Gh~{veLtI*M!>%B-cP1)aPRYj@j-P=S4kkS+T9`!j+tkh8 zAG6%RohM>oUNHc17HXI&CqKNir2-M_0Zm0k#>Yd*U>&MoFP+Z$1ekgyZ-|Jg85&YC z!*6$esZ+$dweB_ZKzM~hLVA;CDTH0~Tw{|^m>;N+aW|n3XFcTzm_Af0vAZ>KR~}5x zXm9`qLOga?d8r`1{+nHj56Pvsy#T(M&9wD|g&o!VEDYkE?U#qTO7$AoP1fnWvO^WC zdBYOP_gj$Z*S28kMX-}xn^l!X&V8Y{F)bj5Xf zFaKRCpfwdGhOXN`cze{Wa|L|dTFgTr)X--?!MNG68qXEIj*UGfr>xx8Ry*@n+ApBM zZrC+Q`1Pm5dhRFLqMT#75pt;-XkL4ijhlW}p);7?jxPP|=+IgMHrELJP|*yzBH3E; znwImnL&vh0p5Af5t}V|^L+R!#j52-`Fb!qnU->4<(~A2{jWOy?Q%DyXIG7Y2o&WkX z*CESUF)g+0y_$yxhD+OtpUeWE!Wf>~whPVFk zqi_-Hj%Md;*>BRsdoA)`UySHjqeO()@qsy)Ntr3nC&?Tgjmb) z7|9N>Yb+E!UZ?)KNQnO+Xsq%C3O{XggVCDFhfJ0BFk^YXPXV zx*>CeK7zsnyIOYrbrtEgvM+zw`ukeU-$`)oQ7I`LBRwTWpT2wvOEd(sdm6YLO+EXY zk~6rV7vM2=^3A%i`0M56g-IUs*3_w@%QJ0yo?Kul9WfEUoHZCH7ntfH-RO-#Hvb=> z?yxuvknOP!$(%ITCc_hp#u)rpH&VzH!c=pE2UvhrFclS(ux%Uwkc@i~@FyzJ+Qyh~ z2H2e5jn!;Tc{-gZL2bXC}$+qtAv%YYREfYp1QJmcCL#*%=f@ z(8xfM<>ORxL&DI3?>R<^`-^qhSfLkQZ4e!rpUTmfR`|v7)ywv(fS$Lw{lyW?F4%t7 z1cMc?!AEc_o9iG~4IN4CI@4FMc%J=Ky5xF?{&)E6*GcNa z5&&ZCzlG>|XmUfjKDq_Qzu$cN?3onTo^-UTm4zzqOi{9O+A_kt>y9J%hR?X>_djl$ zwAk3o%^b}Zl=ppz{p@^TmCZz5`)1$v@hZQxXFDMrwLRgn_$I<*d1QI~+?!Qcm*^F> zm@Fy{w80L6L%)f)OKcDKTR}T1X1%2kYZ1t*=>2n5NW)kb+Q0qf_XLRa@Cq$;zwTVT z`P`s+Zu{Xt-p-`?(LMc;-YvhP!k{ZE=F{SLhP9M{H7es0kO_uAK0Ih_XkhlCpb38a zTtt3(e%u@@*R?K64OPPoRRGcf?c&arA}N!WtUJt-*37PDScgubjy2%C>lZ3 z%Ef99^-QUf)f{(&a&4NC=+lTSGfDX#&|JY?rCY7%Q1@6RyB8Yb&>0^5+S;)*Z-`^q zvAA;xzR=Da6}v~Tyv+kcSNyMUZ;h$~SRqCy)+ZBG6Rv%$oR@6YN2e{PVx+JT|Eeob zz+9d&hhx{UKu;|*feBrUcPoZ|gJ-*B;9#~e(cM@o-n%?K`01b27&W?2a%0K=7?=tc ze!oH^ei{Ky64b<8rU-@!4I(+YRTIShIJhw=?599_k%y+`trVj$eCkDqSY4$*0N0vq zMKG2k7FM=@>4+j`GddT2oglq%Tzja~k^n^zj>KjqnR z25C2+$+B{5Q<TEkUNG$CLMm-cKUB_jVHuh$97dqhV33-!`UP6%!zq&L-<7{Q7 zHVkukrK8JWa7}8(5($GJT}87~qa0fmIJqX^VB#`TD|YVwfLVY^5d(mxe-|=QKN;^` zRTMeFck)=+WyXJG9v9D&FepLavve|+zw-58Vbf8hwY$#TSrMTCM@rpeaU*3p*GmfP1gH8oXK zR8Acib>t5>8E*->LE)OQ>wA3dGu`!Gg5Yk^fiU-~;Ggy#rNzX!RZ!by_LWdo({j@U;R|exjF} zs7*pY@e zK68tQcrfPxu6-NX_6a0t#T`c^n=9=dfCJ`1GT{C7@ZSmnWQKH0$7BB??%_H`Oq`%v80aT4rEA06NE^j{=G=xENv^$`0>5sKaB>Ba2V>?2BtX zHdE_8(caP4O$5a=V2*t9!i5XGFmz4)Wd@oBkpw{rX|$yU9MS34Ry`JqdaD3BT8ZP4 zvVGunj<1^guC?UfZFy>psH4Q7ek77Xb@Y6h5l*QX9*Y-Bp4f4~^!Z7TJ#q`ulye36 z_*7d{HQy3$l zT|XCqm~Krsy&bo_!Tg=)U5*srvM>1a@%$bNx2^`OTsF3dyZ6?!-iFbd_m-BVVoEv) z!yfeR#TM z<{I_oTFn#}#kr#LLkh50)bzQE{UeuG?iKCnsat#-D&U1a7Gp zFa)8X+j}R)D9;mI9*#b)`6|@SsCWPor9c}|Nmj^&EuhNzxd%2A(c?6 zBq~`$S+i@kWXqE5WM77ukYz}l%2xKBB1@LBk9{l2I`+X>Dp|)+mXR65bA4=e|DNad ze1HGkx9asW*Id_i&Uv5rIp=-J4S$`(zJ4tU8bBjMi;D~D@A-LL)zhgxJkG)=V4e2m zw%RBqD0*MMd*5xvNxPP13+_P^!GK3S&OF$)*!K4EEC^Yg6tVc*&%_CrdS@i;0QKxJT5jW_|7n+(y0YSO>DbY{_g|Rox>|>ZdeWP7L0?t~72w-V@4D8m zgQ9E%AfHY@kPTJaw9@`q2(AAgjXg(fDmKyjlN^Z=9i~6bh7bH4UmuXX%g0bL3=Mj- z+b&;lF0KMe0Wn}GXgD2cNB|+_8sj}#`I4TXO~uYHvomF-%{_LN#zqX1liNel#+Qf5 z@OcN&TTy{>WDYXSBk_=Cp@%|kTyXRB1wGIktp%I)y|;If_X?b5mOEz*>OrEEmvp!(|Kv~_ zul+@Ky_|_mF_XGxRURbW;-=Vyd=_m+O=GVzabk2bh>RGuJM{AlfOPW0bQzfVkC*Ep zIo^@!*L=Ahd^}mlVi7QE61@C?>0Q3{3{@)4EH5nTX%70L0pRzTI8{3MPQ*<17^_x7 zPa(YD-~YCHGjSUJUq>MWH4sp!e&px7oBak<1Pk@3tHk%kLJwmB9^I+3*#z)w#2|gU z=MxuhM^1@rdsyaOs9q=9lL$s!%D*;w}~1L6Aym7Ej`)VcK%eb+sv z!E2CaO=APy)1T2alQ@S)Ga zph)LOyG?q|%lY~2U{xbh0kqj$wtf?75~GOrKdi`DdWo z$VwNfWd~aB0P9@XDNU`FRK<8K11Mu|)+96B`c0fH(9EyRz2pIwCKM|#PGFFoD7)F% zlkAv#s~Sax5;zU~%CYR>_?5u4as!gijTzzWlJ-WGmz;NrPvpN+&ibO`B5uu$Vys98 zcF1*v#>P$JsNjrnN8SQ;(#985_=T0L?1q#*GqQH|szJV2BSON$P(QM&-H*Dp z_-f0R;{yN*oe2@SyIR*{jl5or05H3Jy|Pj|Da?_u_V(dsMatY>4LHV4*~shL6{*82 z8cuuvzfvqazOiA@sHJrKw*A9=-tI9|g58K4sOr9-2oUZ-md}jJ+wsx zX{R2)uCqxLIQtSnqcI~h-)f$Jyh{2IA-g!B&e_<|oN=|v!gQXJwQ_e70boc|%EMNG z);6@c-7ZeuJsQc41D-Ao=%xI)dIKvXMs6T2E4AxL;r$-8+w*>HIp}s*;O!~)^n>w; zfRf3fc(QGqUoLrZV6~smKx9e-vEJL)WBMqfCetHs(c_N$BJDG*u-iH-e%9j$9v+{r zxS=jTFrZuHI9+3MZI!*Jru(Cu-PmZkMzffSkt(?&HFGr<`JpFJesOaM#Gv~wXtO7C zf&rE`E_r$Q;((^`He_Kx(zfZaZtaTW`Srz59=b`&JTFZZnVZ!MbqH^(B;PeZT*Mk0 za}Mg2#R}e80P+`sJ*AMk<~9kEv#pTGCs+EQx=Q9mk3+f)w;omVAG&)49oa65pRKZk zB$)m)l5w#SnOT6(zvQ8pH)1{Z{FKIWi!OV?>c)!$M;w~d7Ti_X27XSu=B>tcy_oo# zlK6BmmM9_Jzt+|{F_F=|CjH{msNamRJ^dF^lD$uGuWKouDtv2FGX={j-_?|R^ zD&mY)RXa(vxUw)_cvw6A0olENZ72&Jx4IHs7?R5HYqLEfJM8~z`cD1|jmRvJpu1JO zP!BXhAQrw+>+G_DJC(y=bX_I@zwrmBlR(XJj$u?{du$Ezi;qC338YhMjJ}+$Z@&MV z1dHW0*;f}^ub7L*r$(i<SdwgS!y+!@95NQ^UAZLw3kU%*H&ydKy<=Rau_i%;tDC*l!R#%m;+ikd6h6kHn#yGS8tBEBrKNMFo8xATQV6rhq*JRxh zIMLw>jFVRRXSeXnWh9E|MPV>wwsdqF5bC=vFnj!dZELu#CBd?aJI=hxrih=t>S%XC z@7V0vQ0ty6UhF|KAu<}u<97_q`Hp`~J|~>_)mR~~F=$x5xUuX(A_i@i-CQp3 zitci%#b;#2$Ra$sKc)n8AH7%T9FR`|j|k5m;P^1UyfC)V{J3g1rsL+VLo+=4NE|2X zTADz^KhHmw_jB9wJkaGg^&RL5m!F!(Ulp;s-s9@U9(RC{Po2~M^jS}SwIgL?Or4e$ z*$QFjR8vFt<__LBjP@Bm)1N*ED;d?5im#Jgf0C%}VeR9UFeA%*LKQ~rSC9{>S+J6b zXdId)3!%#fq~>v1#-zoHoLLP-T>aP}PBsQTJX)obV7GWo+mfJ!@1306u%4H#o&DI{ zuu(G|^ic8nLr#t(xzCQ@I67Z7b<-i4{k@{X)o*w1|NKF9^o`n$X__Yo&QU18{h_{g zn}O!0-L>XaU7@ovN}Q=ti=*>jijAzn4ZqV=w9Zlc#2XIQM~z{K++^9l?5 z?xhO`9$IN{#;}d8>4^w$xOBy#B+Cj_RJ9VL8wyuWz_ikNAp9eQ)|fHO2-TUgnW2PP zOmuZ_&^rSGYm{w;8>f8JLtqcS?=6N4w@4-r&PqLTC*=d!$pU6W#l#M<11(S%qr%YwEsqE(c*oCzK_ z=aMUX%HME&$Zgma+d6E`QjQUw^DJM6=UBT4OX&IAq?8~Djl6Nvg}DV z7IL#;>tu*t>35Bacjexx!9)mtW0?=_FkHn{>$I~AIA74Z_FCytAyfIR+O$t0n&VIZ zD;fPhwcz{^7%J?iOYT^Vg-Ev(so%|LI9I@#_Nr%Y8f%45+Jak*3XpqIV|Q<{JBd$e zH@c0QcQfM8(lTVscSQF&$*9XD@>o|4kZI{CH)71okoyWp6YZ1W7u*KN+gygqCdS@Q zOD{_g;cQXqB;g>#TtqWUF87rYS~jsHlYE)n>+0SmJMH6MfQ;hAV;di|#1i1e<}+mo z5m*IQ)x$k^+_ivJD8b(r3%iafT*l@yV+65Wxp+hD((=6Lz@#b4iD*lV&%&3I39J32 zVi?I-gd_ys^~QUa8Dr9dW9&U(CXXTotO%lShYTO3ABgb+>jMPa}gtG=U z#ksIFTnus6(}Fi0R%$s${4hR*(HauPxXk@LgO#P0i7d&|ijSHr3^9*#4>3pLE-xjK zyuQfl;-l2&(0jVsDr5>BFyh#Sm`2vyxELHP62>w-F*q%Tq9?e%!m?prM7D?xG(Iq) zPm`$#Ect*nx-FAr7-p;9QU;4HcRN|wmeAN+3~RN&h8rM1#O7m`OFh?#A(>)S-up~+ z1E?!=Y5p0L+N82S98u(L7e#Pw-4c+iy*}1o08@BVImF$~EZSWE!pg(a?Wx01fsAz_ z8=E+{is>1-Y^N&}@KK}kpV`-U+7B{h_l`b_dNwiR#n0h#i}3X`;cI~7{jh}aA9LNo z(H6`VRVu7K1(E}_{ATsdHMe-%wUKY|Zdm6=VOY#q>_WLC*PfWqyu(gT;>&GAgie_A zE18LV1EJxnUNIAcFO+^cx0E)DU_J{()x2< zN0OEW9f+8A(WJ$qc`6tm7AHBkY*}+=>5<#DWQ5U>>`eZ{8oYTFhV1P6&}+ydH^89L zQJ9Us8}Ohd1fht_SXW23QCLfOx4GSDs|DsV_Q%YZ-tL8x%*^@3sDVWLIb$t;r@?}i zX)(NeE~!~2v9G&H*0&-szk}=yiyMn|n=PMnNyC(!Ip3X%x3V)pDwUAk;$m`>&PZvI z+{h{|&Uh<0jEC!(%WdQRp2v@! zDsNwdAF90Kt=7rWR{DxJ*5ZEXr9!-LqX?~>qh`PyhA3&Ne9sw6PshA!Qj&T`{B5H# zruwJy6|q;6gE!4nu|4u!ST&5QM?ytdLPr?BbYL(K`w)#Saa|c$E<%LevnYu(pDCA6 zHwvyu^qd>5`f+%e-+kzJr?g?yLJ~nCT?I)uY~3ni%da2o+*9Bj=G8vXu0=9C*oxD` zKE&pQq~K$4jx_|=3Zt`qD3&(Q{nh! ze4=$)Nvj*F*uKOv4vRFbo&fPoBtGc z9}cX*`kak)GwK$^2YAWh#7K#K=5cTgLULeotuu0IqquT7w@N%0?KWDJEn%XCK5a5N zXVE+eD1>h|?8;jv;*MmOa~64UUco$Im_J23LBsy$SuLuDjL(blPFm!2RvEOB z@@vTI^FMzHBWS=!cdD-iOLuot`p?KGV$;{FG9Mfjod&FLY&=LTWTe`zK_qihd5;+} z#@a88hmulNeD&K2O^k%rxh0+p_GlP3moF1bqBE*=2}9Td)IMH)JOmKD!noIlnbcp>=8K>Nhn z!`!AR$r#x8*NaI;nArl~{n4=RWgeUp_AHJ`sufm_?QPkUQW&@}(o3rm#jiDLea78~ zV6REaMD8Q$_mf*KaxIHk-Mhvbsz!!f2Kw;ku{d1yUE!(`;bImH@(PRAs8|O~|8BZ> zu>Fc$_R%%w6;CnJ&_b-uDR(0k4`E#v7$$d~fJLyDD`n?iE%BLgrgOeTE}C@ookdtl z=&}{XW1UtiRSgKzabeiRz5twmBv^|uD#R>uY!FV0&I~NcLKJhMa;uOXmT;ss&i5Tm zvwDFNM5GlJk6PU8Jm5eY4!r3w%x2%cAbx{Kp|Irnx#HFnP|n~ut1$~7;ULPhG|^U~ zQZJwO^H?XfE$EYZI~ii&s?@V(ae=4%Z-2pea)EzRteGqOn8&sgnrOCehR5E91JTc{ z#61-0tZ#GXhaqMuBUP|%b@_t_J*24i0X0^1?M8B z!5@r4t@-+J$CXi;U}yIh3n_Kj`S#nmDTzpuMUs8VqR7hA#1Ieue(aK=Dn*oY5qelQ zxXLJMthvH+mbe=3j>F=dF}JZjO+G)-Uv0enXu$ZXqwkMC{Q4v|lBDm96-6ox=OV>BXwSK!YO6WfK89b%ejV~2 z9{c?mbIc5qmH4MzntYT(si7=we*2`@HLw&cC(+ zBHjUJcRx^VHIO~#^LW!k>26*4K@?hFP^;^0W(s`F)T!GNCFAaEk8Hfa-muIwy7;iF zXd@!LHXQ9Sh|xr=q6auOPD$~bx62qlSnnSDth!n~Qd}K;FuPIE>UD)Np(-}S|BPHN ztN9(3jRQGZWKm3gPg^?MglFlx^Q7fqn}|5=OHaeX_{{8TwFRE2OrKnQOgo=$6~Kx( zS+?#{vCyI8?m4QYnOuxOcPc5M%3Y|K6(uw0+H>lKbEDFaNu;qCnLXIc_j5v)BQOz1 zUH6sc#A!*Zg?y|SrW1Bv%LsqnUS&cSj%~-Z^uW=d959QWN@(4Uf$@TbtjOCxR-R4! zOQ0PlEmzulgb|o(yN#G5iu$$Uzg$Um+Zn(=r?SUSlZNBiWNbClmVvdp>F55i zkRiD`8Bdld@I)4?6KvW%S!Yv~RtC#R*aEbhiGDXSCpfI7eNI8ZT}p4HD2|^2duFdb zKc4<_QCiNlfqgp+ri9~K_BxkT-ETR&*nu-S?2GFso0pYCjz`yT8wzV=*Me@I z57pJioa%`eLD9BBOaZZbzx_@zKwil3uNOLm_6U2x-6(dPQmF;NV`cK|Vl#@xiaM8E z+fjBi4~t7gm*F?ELTB%tId)NacMu_GYYT_;b)UKM8P{?cjw_#Oc6*<8NGmP6M1@7E zO>|W;JvFp-+0fM`Ec)Yxj$)>ndL}Z)moW8(KyWb;L$=)~A@N6rLhbF#ILMx2c-#Al zYE~oqWI>Eu8?L$^qtrE-j!{4=rQz=9nrO<6dXq-)t|+7>)l`!SIZ?RsXg9ot$8Z{^ zlRRwJJTtUbS%8{$$@O$xbuki0>Pp>_%0-U*R8L4NBSsqr@^k*rP1-}-)^I>`*P~{r z`lbFbOi`$`=ZFT8o{^$=Azqv|K(i!YI3o1q)%t?qT4^~&3m=%;)9m3qQR}F~xhzLG zFNI)JkW)0R`-l%)^!#^B{;|9!MfGn1+BPMly@Q#Od&3^+sGLi(z3$A5IND^$d=IUY zu0s2wY$l4dUiz$DxjN0WYKiTY_4s--A8ccg_bjT&T5nYW`8la3cFm6Q5uALncTrdg zOX%fIr>7kt^I8q8IrNDIhS&;5h4D0=_OoLKuFQye8>RehmjgeAUxP5oQodE#d2gjq^RGO);<(3Z+MNh$x`&ecl))YsxB8^ z&5N#Iv-!|1&UUnE^6B-o>}lnkwKdU|IkjH{WW>ywoiNwtI_%&E~ z&r2wS_#`Upb6_i-s2KJp2LtJXjo=EV$c;`I2{r+sB`PkERSo&%3In` zq^4hcW#E19cF#^jc`NtSG!_jGl0QVFt(9yaazy!>(V@ym5;sAz!nc>KK`ZCgG85Wy zl2Z8`l+`4R??v*}gC_@%^2uL==RfJ$O>peApj$Ati%jK3WhPbYKcxr3q}-a9ZU%j) zXfioXc^=Bp*KpO*SMEWSaHwNZ zghG|g@$GZ~i{(Tpr(Rxxo^fKakhMa&`!si$h@Xck@{)(1g6UjW=Jj^Zqq{MoS#LA6 zmc=7^5kMccfT@Gwn(GYwdzK|eTyl33TD#)0(>GlCum6ic-yueETi5plFm>zI!ApLt zIjdRlGmi`}%`9YGm9cDnGa_X6$albgMHW=)ppNG8i&fNtD)`nVESF3nu%|jW`Gv-3rOU7b2On*m}loN2|C;pM7G+#Q8g&BhNXdb zB^G`=@3ZWwH5T%@XA*Piea+Hzr-VJL+pd&BFZUJ=7lhp3PP?<8Tbp_fx;!7gZC{?A z*jfWRS>gdhGVc)i);upMXt21C=8DNqSWCVLU8uIEBRkOGJ4IHUxPMcNaFm#Gbggv7 zS?lvM74D%=ar>L6z`aglAU|OMSgjFc?tLkLtq#BCw|m!{zpd>!lFfDb@iIVgv3qiR zLE2f56G|Nf8xxQT{osEQmMlBF^jKbK6FrLWpvN}`rgy*%T51RqvK|;@_bbyI(JSS& z{!sPwnI$Wt%+bbafa~4}Tyg&2L+g3Jb!hz6`d6$e2Q`xWYF=j?%9J>|^D!FB5K~I` zX_qO5IxaL#Oa#NY)FaEw%kRf6esp4tocFPEyJw<4k$d8P$j%5eLc0gVDKv4M1N0GlXzs zbivqh(MS1Ws`8xIH^+8v)gB5xh^a%}`I|(;VmcfcqeRUHj0Jw47Lm|PC*Q)KD!HS$ zPzY@%vx|50?hT>w=75YqdfQU~BM41ebnLI>S26Z+QVMG|8MugSHRdru&9^oK@8A_9 z;+PzGm@#_gOrPLszcOo-njS8XY4>We8*hQ}k+7V<>)ZUvk`r>F=@9NrE$C~TDvap0 z_Y9DCrcmM#z$&#mS9O|4f41_H$jz<@`fRnyPH$e+ue#rQ&J&tZHad~F0j|2Ted;~2 z^4Gg>#heMOm9`(y*E_;-sm8d5m+LBE7{B_t?z3q9W(<7E#skU;HwpVYyS_x8oo&mR zct7OX83Nf}sD=w<5%?>>I^QMAwm#iZ*KT;rp46oXG#KGo4>`=lJrKdtwh~qvQi(;w}LaKhHtX zI`{4D8L9jMV>JSI(M}h6@j3Zxi!9$i$WA4b8Owftu6T8+i-v+`?RK(Z;^JI?Oz6qF zDF9z{-mZfGBir;Y;KUr;JQ5C{k-DU)KgR zv|jXN55-B!<_jHd9Wr8ngSz&cO`m-Z+V>xrC?K_9o!}Y?sVmq9v4SxSXh;=OKkF)C zE_7KDE)2+2GosYLnLItr1z~XFoL4wtiZ1vBYH|oLyD)NyF~7g5x4mU~RIRAn{a8C& z2@sy6|Ayzx#6`P;g+SBK<1rwyStsEv250worcO9$VcJa7$DZl{yhg@x<6kkI7BwEZ!YZq<(3;0Iu4zX)(_yuG>3>4^VNK@rImKQANyZ; z5@vZo|CfnnSE^DzY2f9qQ-o?JJs%-t^LxAQ0x&=%a^MIElU@Lhb#z>GY9+B-uejrT zbq;5=o7j<5>4n7SMpG;}`}(tTl}L+z=0#dP~1} zFOS{fCm2k7I~FXZCJBVtKqpa#As7YwP5^^DFN1$TZSEUS@BFoG$cQq8AmsSz+BS-p zplj8u76Tq@?zL-ysU`dhBRqF~Mw~h5bidY@!+1WXTT5YP75LDs1Ed|4yO&aVJ1Er$ ze0j%^LFK0jh9UX;U4nNUhJ4Oum}G*&g8 zRSyFLz4PN>3RfMAK0wiX?vm?_b|w-D>HFL^GBO%S>LXlmdV%QiXf$K`%Rp5~2n#^+xUVftFIJ*_WGenC zVSu)o|LlB*vL6S<@W*H0Q~w^qfz2zi4>H={CXg(ZAn5(H=&yZ!jL+iNw>sM(QE~Cx zU~1$OER0h%6g2XFz*|%K*)ecqo>@sbdysyJUvn&ME2d|P^PgvPjGB<`?4FaN84#;(a)|=&J3W+- z_!sUS;WpWFjtf<3O9yv-`lj=&g*;CVXZ=km%B@FZ`&vpcKDjj+|8@h-Y|8CdFOY2<%fb!@Qv+jWI%gy_1h;? z%86Ssg=;C46Q~JfyT{aTucbp_Q7fmr3w!gX( zP4r|XweK)S6^ z9lvs(vRS#7vK)x=DCjK*wEwe3iJP&32XX~9%Rn~aHmBN}O*jTpssl4Rq5Ur$L9e{T zeC1+20-DV~4dj#Z$75q-@1rVMu*2(ebDVt)+-0?_e2vd&+eXibsNX8Sb`37eWy2U5 zw;pjcoX1%rsYE5_erUWZ3n+MRUiCeG4it-WaQi_=#@OyF2aL~-@J9z8pkX(q_0DtD zxxX7jmpfciX_lL$a$*0n8N28aNiZ(vyi>`oJelG0( z@t5uaohbqo#awxL@AT3H?ZPEmFirKq-rSjUnqFNxd&1cRU=dC6B9zLB5xEnu%5>W$ z|7L+lGGz9SPL^|sU`Fhp3;%btk5>~iHTCTqx;-{y2I(j1fzj&3{uk|X8{TE#U$E38 z4jnoqWLo?9KUY-eylH53*a_D%QH@!S$v>vF$TR_M|$sv5aS&IC%2ifa0IFSMKDY zjOX(FG^tO*;sLp$4*c6h)#ydY*wW}xKc$!V;r4&c0N~2GVucp4#7(u;!~f>{Ajnpu zU6pJmiUI7);r)eo@|AMf2$q_&=^g91vU?=*%XCnRc5o~97*}~Vfw7x$Fl-+M23*8S z-a*qZVA}regHfRCl!_n`^2a~gj)6HBZb+{OOq3}DP?j~_bA=g9&N1{?+J-p{R_c|z z;*KLa&ZnJ-1F5(&<&t~ldhR@!_66fCDa!4yyFnDuq=l&0^=;}k&!<}~CcpF!pfb1@ zpMsIJm|k|Q+idHB?lN<)m?8}ONx;a=%VZ;m;GZs-c_c1cS(@(>;*aG6w@m9GC-^*e z38;soFm)HGB=2E@zl7HB z6qutnH@lEoJmPQ2-~4HCi)n8H8P8{5 za8>X^EH!~7{; z3R;>s9r1h4sj_Qa1z$n8qv3rPjW!@aopU90Hlx1Z3}b(_dAbTcW2E&(yYNkT-Y5Q* z7=(36{Fz#{e?M)jjOE%Hh;Re|QuIF|2ozDbt_mZu2xzgLr251AK2T(K+Yf?s;nslc zn;y!7Q%jelj6Ictr56E*yvW@bs&jv0h_A+irC|)h-)lr5 zmGtu&liC|F3u%%s<;Z)7|#c`lXPri?T8{X`#Xu@r=pY}jlinyYU<Yveep_~@z zyF+m>T=9)-aNIzb5XCrTauUYJh|hp5^P3J(^(zXL=ZB2%)D{BtTR0xg${}FCNnZiN z3Y2YJ+#c0*R%~{B&vwYZKYwXHA1Cmiv=eeZo9Rr{%xh=E|0ADyvRQLw-{fc2^}+U} z-n69$9|Mf25=mcmo!u64FULQ~P}&(l94I&ACPNe+QX2`Adh&DpR!nuqo#SF6EqsM6 z7Gsfk38_E7ou7iG{r*G*K4N9~!|ngX8u?_yt@CXeKLE53KWaZlp8MEE*J8U{7`bo+ z%CK@5Ai7az_tTpVz}gn7WtR^7rBZ<$SU;R_Bm=fta!}klLrFhT+$Ll7^iC9Wu0y2l zRSI~nZIole&u`^>g*+b!4p?vU1!}B=lOOwLNB~IBd_KK}2!JJI=x_O^j|im)_i4iY z_xL$_7*c!gDi~@%-GnMqkca9E1N$RkduQ-^awS-dz&!5yJSe>F?B3v}MZDf(GGJVJ zk9vuKryyF|TVDO->ETs36Hh(rbx-c3L`Hhgp=o|n;1pXs{4Vd+y@`vFdlIhiayL4F zm4=$^E(q>X+qdgPp%rJiK~~`Uml_83mP~DBpeh!1LkqHWn%~;jvUaopAUN_LE>vIr zTh)af(i$_P`zrWryuG9;dKjWCgd9U;ft$QebVcht|Hm$Aw2QWU$xRbC$=yoeCe-N( zhW6&b|1JeL(LhaOD#nQn9G3+TGDfGj*}mb&x>>*~&Hde6z8XKZSOOh~KKDie%a^n? z^M61}b`97zy+~00=6!@)cyIOVj8*}f;j25R$a`-SuEhM$rnX;9_ln)ZQ~;zej1+6C zk7!%40~u3prg})bOGm#SsNX#UJRV>YE~I#GMpS;9%jrRKs#{GL~?NF;ZcFiXi6D^m(Qy z<9F&@iq)hibNcvNJa%m)XRG~kh=_S{{vWUjF%2B-5D(`+vCXso`>+448t5OGRrz3X z>rEa&?`V3)7?ZeecHp~VdL09xJ?k+j{tlf%+*B|<(~%yh^+e}izwuzJd%{QwaPT`@ zzv-cyx8UN7|M|_2*p8IySW!a!vdDAV)^00gI9^Iq&$j&G47-0IEBATol~BZsWjau^ z82Mn{Cpr5!X#!v>a_buK^A7;ZiT1!Rog1|GyZ$n}W02Er`LfOI{=35eRKSb5^WHm~ zzo`Y8G`|ra9ZT$Z5?tP_iG;>mc;1x# zgz)lV(${C-$rgVeCraRYT!4etJERHMxt2$UY zNLhmK=K*DF2+;Y~=o#mjTwrq?j9e6QZ$j+{F}Gta>OlK}M_+S%dQa7Dg_u;@)_q>j zn_46(;8AL(&Qs6k=(7OH{#q}<%pqL*-pQL$NKf*&2K*yLvvGWL3)WF*Z-be+ z0~D0yIggdYcCLVY&;f7+0?eBMW9P)Sy}0sO2z?OMpvVty7Cz9L(;bP|tLV=)l3M*!>L+ApDtJ;Z8>w=l(}cJ*75`5~ z8~QfM-rMnf@>Cbo?g?yuPmoIhL&S3Y&mGV-bzPje3n(tWQ_|byEEDZVUS6K9BQkg~&vVuszZZsLa79S{V{TSj5#N-DabUrKK3S zKkPBNsjZ9%hMq=(yrC?`S`|K}Ps|l6s<{9EToJFb&lPcEuu!0`_X2&1`p_v3z>0|& zwO(-oW8gOfjw=CWZ_Cdw8NihKN}%Q9z}tsjtEUU^wp=`L%P6tB^v=XZ0~U@;;w@5i zn4hWrQi}KbI-_te2$6^<<1+qB?V#NOCRhs}DG{H1xqW9ePVG&k=bE2`qCU9WHpR#P zLzg#R0dvp(Aio~d zH9QNd9IDtvSOcnRgH*E-Rxwx3XVC2e9#(1LNsniY@PK3;TZPdFF<@q0-2+Ude*l`d zo$IAks{OLyQHvC#QwCsSQ$imZ{`}J@$gGnvV~Ja1kC|ITEV%CLQdxy`{zphg;gZ#HaH*@xP+0L8Ujn;#1wb+Ybb zd)4_i0Mi%&WDFYStk3`Jdo7NCo5#0`jTbe7#0vechyKy@@*R^x6wZ+?~>&Z6lm7}w%7X$8^*TKmCV^9JakeZISr!;C$_0?O(4HxH0Dig%=! zKG;t4mng>2yu9na57_k=O)Z3ANL|@wz%uG)+|uMYAWl+TjXG-HiI`nUYC=^xA#ete z6sncXGfq2Spo|dIy!M^m_YII&lhp@P$)s^0rsi362K8XjHoa-seUug7Rijt&aGEhE zYOMZ68@B%E_)4Tm(;5I{&9e~&uB7&CBYe-hDwR{>8n>uvTfOLU%)zxog$C)_uKlmJ ztr=L3yaC-W?^4$W1k~$muu{&^RbHL>(T@mT*{LT$EaI;=b#nMT5YOiY8s%oCMr?_9 zXSh6={NC@JHsl_qDgSt3_vPHsfh?!ugJ&&{@N~I^iF-md6A17>Q~EMzW9BwrOa_{X z{c-oBSXfxbTTIpqAW`6#!$=nJzEQAExS-ZoRAMIjw4XlHuQKboig{T`mTxhwnJfPm zlb6r0r)*!rqcMwtcqq62?tf0wmFWJ&->E8)+Kie)M?j-hW-RVcE>km!Eny0q6pZY>{M4QE@EtpCDu-1zA^h@n)Mp&Lr-A$gibt^_EFc z2g*wb5GYp^0Gg9UVn9cW_~0}3aE6ojVA6gyzOM%;E*$xm^-}q9``PW^mdp+up1{kj z)aM9rY~4G@#sD4L#DvuVVr4jmp+G+M0UP3zjKy@kyADr$ z^F^U%ZCvn^i)2BeEH@i?fdMFVsRCh|z^NFQBHfQ8ErjWX`pLr2pS{TMA%2y_sB z8MoeA4s!B7U#>1{TkbSkJ@DFpzB}gg7tlL!DPKWuExG9;aq%Z$f)3(F-LSBQgn4f+ zyB0Nvi>W)mXkJE3<%hxP-#~8G0!Yww=8mpSh%Yp$`)C4Rwy$hDE$;nWIoDJx)hEJX zPBL0apy^q=1UmXV+&3#LoX5WWdU{H-V4<2c$A^|#F>J31E2@z!SeKZLXL2J0-$5(4 zX4`&!k~ClAyR;Je#RSxDirrE71k$RmAC}-yDeo(7{p`Sh_?yE6rdU^$k5`C?z{F8{ z>MWpr=ien3b^Y4n+DaVMHKtkPop8~CCX!h+ZTYgJ(F6TB;^ShVjP$(( zxY2R%KcRPHj#29CJ(fpB_}T8vQND)U>3dpi_9j1Q$^-B;!n6uc`PxHESwV-qzEZ>H zMgXt+D6yz!cL;Y_G(iz7+YPAow``DnHw0ln1z7V{*xut(Uj@Z}%LsF-$`jf+tMQZ~ z#}z$ggSX-!K;BfPwyD9Z<0>^I<|07sosRwN;QQ+;(`nb9Ypi4=g~d1^pclR0=JJ! zXAK1H;;BI1ZUZqEPc>dY(q($wR>)4a8W2QG-%gX#n*`ij=Gsp#Iog(s#+CPg&PwFn zbs#x$gmp?O9SFETOOZ)G_TcVoTPGmI`W#p4_+&C zwh<$b#tun*d%Ul!WLnvPOJx54yR;AM6m4Bb`g{a_v_)`3_`^oDGS?Bxn#^w(!VgqWaGP@l zOU`8E1xC`{N@~6r%(@L~AiU20Mxb^SJAG!n(&|UI!(|6ql8V&0ckU9u#9k@m!?GMh z@4o|`+?&52{yO{JW&@NNG$$snUuwQLl20gsVKo7gfX@J_Lscr;z&l=Ggd`|<5pebj zSUgn_Ni-MZTQ`OLE->opR&nvX#%<_mA=%GPvIIbbB3(dX@SH24z|$asZW|Jyl#fSX zlBk`#FGLwKMqGA9;2Mh2Vw84QlcT*j72gNU)v>?LrNRKE&2AgtH;T-7Um%FBS@-gN zCVM_tF+eB}ye_%e@c#LA?VjujJsDnm!6}5J58A~0cf-xzLaZg=yLk<2Kc}yLE?5n} zj4%GM8OPVXyZ`X~*`qMxU*aoZ>^|(uKSOnX&>z2_jlw=((DC|Zd`!}JU1JYR!|r_S znMdFJrY4C%|NJDs32<8c&Ax=fq@X|_5N;GK0UdkXGHfW%?Nek=z*rYBV1en_|_N-$*3g#6~2%rxo2L?WiFti~5;lHSCs78Dq% zN8xX%RBgB}zO#2`Ek(OhCx@FmlW*qf& z#utLua(K1CnLPhoG|(JN7}UHj_w}6|v#6~um#ST&oUhLeB>T^M;;pQ?u9)M{3B-vT zIQ2wniDDlzF>ei&Baf0>E5R#DLM3MN+-Wk4a3kLrH9#5tJ9Y2UzKdyc1n1U*gpSkR z*jvrs(RagA+NiaQv)6Mk;i*U0!VcV84CxC~voS6qe(Et9xfe=YO_@(?uNZ5{e$~R6 zzj`}LE`d1BHlir=tG=?wD?pBDnj@hAMxZuL2l|h8${w^ocH(2~9}$CtEGT4xPZDgwg-4|fSw{kopRLO{{Jk2KO2ml=TKnrg9D0zl_pgD0 zU-HMx2~Xa>4z&pu+fA26{I*KJ9TA%3DkB>?o{hJUS9F;q9)B)7tauE$|J8Ny0a3pW zKBcE^L4qEa{=(izB%oAFvqIJM3Y6DZ+$G!Kyx8#*qn5(NZ%#ux)9PWv2kzH};O ztYVz>_*}$i9$i^0cVJp2ch z+JH*Lis5K_UPIU7>iq!d`G>P+Z}#wPkViLIooC3>HXX?}RhgMe#HDGbNQ|c>6|I1f zRcmxcpdQEt=brk&m2YMS=kQrS#ktt&+F30r5LX=ka>rqF#(B7Xv!_*H)4R5Fn zf5WnID26kQdOB7>vu)#>JiT8@u=~@-Z(Y?0(yw18f9jbnfPNdTuVd``l%aX@JWve# z4sQi7cMeq4*Mu!)C4#s2KH>Gn{yf%Sk{0=sMz=bE_TdC#FDhA4wl_ECc#En4>n>Iz z+}N?$C0pu?ChtJ%i)oF+d1g<&XU*B@Fw660;EswIu7Bo|J$p)P|EtjjAO`h((#i)p zm9N@r7d$EKyY_<4l01=6;Mh%w?GMoONRkW`_9qR1!geqWgpDd<;lf00#7&~<|@E{NH z#z7_2NwZPVdmlh2mWh))Hc1O)uE&FlRVlQz>-0g#6Z224B%*dH^r&S7B&O?)(`zeu zDX&V)fk4~59|IWh)3ZjXcocY2__OL~NL%|Sq_Yem@P%HakLXe#N!QnH%0iZ#A@|+} z3|pcq={|Sbv6Egy8eFvRht!l96+OD&=7_ zre=LPPjB%Z2mGLyqwGV5z!9FhmvcW1(2il202erHRx&#Je$T~<+FM|Z+(rG-l5)@+ z7++YN!hwSEjd76I^2mXV3iQv|DC8wKwSo7jK7v=^>PO;T2hwH4-|2-^fJU_Ajm~nZ z0J7WH7jl~8MqfxmI@a&^DcU|MRwJ&w_xYnU*5J()yNJle&Ji|fL+X5KIQSpby$=ap zLZ1BX)}`8gW+$C}gQ{Ia55?Z?lXh3a-m5);_NS}meV?(3+8L%`cU*t}XtlADZ zLR7wg)sNSFKi>xk99WtDdYJfF#IafY7ns{p3xWOhWaFF~4z;mMquB8YEg1PXn7Z&U z@u|g~N!m)acvwH&QTX00YI`j-xIUieu=uhn#ZNPTR?gVc)8Y-w7?iOg$>DnPamvtY zYv|Qj0CnNl%0bVBff{(Wqw++E#KZ|uy;rC&AYP>oE1kXTM#34OVNtG>b~lszy}oe$ z=5HqeHLc^UH?`9p_62)`7d?)vKI&8Q6`L|WBD*X^R-t79f#paBH*_HIE*R^4E)eS8 zPk-k08Dl2DTLMOK}*4@k73@m$8&M$jzZc7z-=cZog5s(p(J<#c!<|ubZS=H zoNmx9f@{DyYO+s1vjT~fh+`1{7RGiK zWpvOQ?U@CIp#Jy!*NCpJ&SB)`Qld^2G41N*FrWL;|8$A_xiE8!y(`o9b&9I6FC(BK zz9Kz`?N@oD^)|q+p;D?jq{KyjZDs^L8TyGDPc$<_l7X;k36Lh!+PBh`Y=l_Ah95y?CnW1YwY zYN6Km?uOX|QCjT##FA0a%m09+TfK111HktNlySh90WcIna}WSN#3Bbf-ou^TWf!xyPGWm z_mu!-^b|{{p-1(AqU9As==sl2PL-VzfZ}TgO}6!TDYsdg4FSoqK;e3)rjDS1(cgC}r#|wunBXMEZ2+aQtic$_fN2Si8g*L?U$f zx;bxx{I6P$W0ebnMosSznrnf`8sT_##Yuiy@$yiZ;NkEGxI1cd*2Jb)ml;JeX^tZFT*Zlmzx@ z;KZ@WL$Z(K^ZNmza;liwo7MSVJIeUW)4Y9tyv&LVi5?((eDAf;bz6{MQLQB|Np5kX zEg**_kn%*t>c~npbL8(FX0JVB=QQ?FGzY@K2Ldt;0(~ zF*ShD$WN)%e8-@UCdK(?Jq$c_LfC%N$twnk`ZCFG*blBs&v04I!Fm+J=j}DuHaN` z4|bcLKL9@rpKK2*LEg(hH<(LmlZd0J^EXc+_Y$(7TPsfR3M(Y9Q7uiTP2B(Gkhz9D zCUfI4DFOo;jn12ntYj8@xAt`gI`wWsez%kPx<4XSKmGQ%v^Su!Nw%l>(VL}~ZXyD^DKNC*s3$Rau z(eyl#4_#6D)%&eeCi4}~bCtTO);q(KiT@9KZy8nP+C>d3ZgnFnY*IiZB}7D88k7(u zM3io%OHz3+3?CdXnY#*BUaIKsc#LBdSawOZv<#LcldiUW>B_k57Whpz_GCq`))_biA698nv5LCi2b3N< zc-m~}?RU#B+$M5GgB7b|RK?SxU4{dq@524g`11&ESf%_rj8o2#&I@c-W#Vo-M4R|L znFI@&x=+E4Q1;I!4YVNJ{j+2DUV$dPc!kAISrp80cYW2O^vc0rJ;;3THG8FyjY%px z82d;%oq(`g1U~64VAm9B{WQ?!Ny*XL*UkS(U(jwHaqJINh6<2BVpwdPg|U3&v6XWy zp!6=cURHm0r$3fiN8I>?KD(Ux-Zm(A+yQe}f5*iS4xBB15$Mrm6#!&3v@Cy&m3TaP znLq{W&r{(dAbc%szt2zEaZnP3wK*V&MfoS!0K16)aezu=dwYGWl@SxQd_hWg6s)kD zjQ2NHC@_5Wvf*T-d8*#0R|Jotuwv*YjCfc1b^F5M$mATbqth>RpG$q-hKAnGL~HyQ zj2RG+34U*H<|IR#KpV@RuzD#(mJSMxye+Elz2L<*6j8C#)J&H8Ei~N#PtqPYf7w&= z5U&`R%8QMIg#+Dt60r`Z;}ZpH%T%mIF$@fpKfu6TUiR%>EPY-O$;zD}Ax5Hca%U4+ zb@VF$(7Ap$NWs3bVEGdbX(qlXZyJ#;H4vt9hHpc2a*uf%YvS~qhT}qo)nZTn)ZXCD zWqGFM!Qs&$r~V3qWvE)TIT-LE(zxhKg!6PM=Ab7Oi8~&|_1U6OtWv~rqTJ3v*eX)Y zsXO-WAzut{rg|kGFoq?#^TfGfVUm*TU?6W)m(W!S8{%su9V~Gc`;Lz$&zyEFwMX&| z7#_Sob>gU82I4G|RX~HYRe_;JpmXq&9xv{HJDxunLJMr%HCaD@KaJ|u3q36I_s_{7 zypp^6ot zWxH)A_ndYTNRyx<9#~QB4R0Kz?40KzYuOgT zOYyY?&hw1|tW+x}>=(}gOV1EMT}I$4`U-a6Rq5CstV{G(+yOcpf#G~vCK@SGA8bIG z(vgG@*G(Lf~2_UzF9U9>bb8j68CTs z<%`7(uI$bsOSy_6CXaHJge&Ag+>3OwY~^STh<=Ssyg|9D8c=2K0MG zk9OWm?#%$){2s#1Di?8LOU={QOAWF!h1WorVPQV%fVeSQ-Ljp{V7$j&vq)kruL~+o zS1*0F37!7wCK5fQDpqQEhLL+tCYr zj#FMV-@h$RV(wKK2Xtgn9t~4|ADM02B0z3nW#-OU#0m&vGZl)0z9cD%DiT|ASZ!mVcY1p;{*1$H{ECLQ&VtCS5b7?_k8PKaH zKlPh!W*FoEmv=b%Fhz;+_L zD`tnyMNXWd(}XWL)kzLN2$Wtx|Gao7gZz01$V0OZ)(gsPP>SEpia&qSqXr&iw5^YT zPFndV^2yC%er-aV{7eB$%RCrm$A%j-ScxDp8#rh_e)i=Q)Tk3g@|59Wq>p?MDM0T2zi(*qWgod5N>(#wXk z>#O4B-o42c2%Dq0_j~l;j_OK;H(wdg0ygpK23|Eb&krf?k3XK&VDnUk{J2zSFxoEu ziRx~(eD$*pvT8Wz{!bV9{{=hCtKR;rnDZg6UoLumg;QgHtX=meI)yy&qVcHfQgoeX39!Wbb2VKj>#xvRulcLioB1TJyEz#yB3n`M*+JTGX|FaVpH{qZFD{RaGmh8oct&kSX86~9B2(j z1Arp*0Q4*)_ujr&tpZ%#l~yofZU@ai&b76YPpDSBE0BogQ*|?I4b*t8Jq%9U_fy^^ z$3ZS10>%(BLB)5dHHO*`H!zm#PofiN3BYzpuyf~H?5l-RZoHA00jQm>(;Cv;5=@^2 zxZQYXjGLr=h{JcwE}0;3UdHUp8-0X zjLUzt%;W0afkagb{#h66>2nv31njy*Iv6%KQ0tLDjR4W$eJ)K2qW^fc+%#6fjQipw zM-o(@MBrwkw?Zqpc~N7spu2-Nx~b9UyWv{Scih2h0!B;#t%L|6<^aw z*5IcpYry9j>m=v>X+_+npoJ=u0}15rmO&&lc(hKx9xXJty!`^caXk%p={C+q{)^aD z=OTGJ0EeD^?O;0h?>jeGquMb@cm;lMQ#?5$;W*lh6TbJn16ttu-oT?Fvqn4te9h!@ zAu8AZMecC!+og8xVhnlk<7jP)A2b4PiLt~5z}D*m%80IylNz$zIagamx$~L){k3uL zRz=DPZ^{t5${~Pi^p%1E?nPM3%X1+fc+CHIce!We;I4a-YAFFbhCThXW0`=DmW1`g ziNS-b5zOmQ{gRdhb*1>p;XO@y(F61ba=cgk0NbXCHWun(Cqv{~i1h<$jG^NXM2|`5 zgIG@gQ+Gsn**3fM%i(Qe*BS^}ZpVYYZ0B6-%pbC%N~XL2(XE8K!_Z9IeyQsJR@ME< zss0kcytwvDc)Rt@-1t9eckq{7%OQOj>caneA^%kMkp08f;O`drd5fh85@FtP`CpIx z_uuRf1x-tOL+)>%+WIxv_Wxw?|1YK*V0XG->cD4q{f~ZErA?ei;f4Ptiu+x%mGnz^ zN|#O4_|NeatS04i{d(2^()#_)K%M_r(k~~D2eYrHhkpB%9W)@acJY4+=O9~+|4>!M zefgyZOe#0LP5jSKk(oiQOWq6rO9l0}y7W92zW^yE#Ib&n_tXCEQ!@-uFkbz0w*T)o z0rc?CXCD4ChWHQNm+Su@jQ@Dm0{`C&#`Fzs4mP%CFxBuHj1GMtH7&!lVox>&v!l=w z`*_RA=KNRs{0_?pdqMBu`5slD?>H2i9-!_QxAx@*lrZkZYaaHUq^X!$?zpHR^OLGc=9VSTwE=&0``oxl*tyk_!lR zde?MLc+*fz(8Az8(0G>UE}ui5bTyPPZc`*<0dt02P#Nac=NzoCtmd1eIB);Sl>eFj z4A1~GK@-xR;%mUTN|w==FTk{p$W|#~wyr^^$+taCTvvo-UuhaH(d8ySi&xiQ&9_Tb zUn8L1;RB9{7HXaEb_sido+GkI>g!1k@tjiun?jcE#mOUgz3TqsZ$HMc1gZc~2eem) zs>n)ku}*J}y}*Exv<8?yN?Uhaw`cgAu`)9Qes8uUuhzA`p`kD?pc(u=P_a9pJZ_k~ zj6B%@VlQ;SzEPt=WsAvZwJd-t->63fo?F-ib`So>ZVE2_`u^olLZ^!o*G00f-Fn8qWH%7(;B>5NdEi*nqG{ZuGRWt%1`UW?Rp=A6sSd?U-#5; zx>YBg3%nIFYmYc02>1C~2?&mAB%hVo_Odw=BoBVmDu=nmq`dRlJMy7x{wJk@30 zzHWIS^O=2vRrO_p#0iJ(_C6r@l8vZK>Wt4S-)h<5H0SFaoUf(RCY@Sm%Z%*_Ced)b0PoA7MlmXLR{Dp)u;j3QM%e) zaSYnWy7WYA^vR@S*_?KC&+meY3?#AinafoM;Fc8x@8O((DIuF1VK@eLn6UE-P$8A% zVuh+$h`kgMHDT`s5kYFAo~z|C9A$Y5(SWhH3rDwBf!r@DIuX2S+%PcZZDj`SiOJ>d zmL83mnQ;(N8W35@ritLR)Kw}Q3pKc%LDu!S3WXfY(R^Z+9b?ra#xT;4c|XkFuH_3D zfSg`?HtDo+#OxP>2WI)6KtSWh7fEg%TW_oXX!JmMB<=%D!eOZ)6SSfdLXKYhk+z`Tmr%W5WQudJ}w zOB=UbN5efvzLVLsAg7ilDviZKni=j)wF<6pDZ7!8k+t8TGZ$*+O!pB$87ZF4x&=Na zl;TO~KEzZT0_pjD5PhOfNH{X;a1^ezL z>f=|zLRE9C^%7FGA4uOY1p%@I@T1LkI_>2xoTjIzKjQ`8?5FuoP!X6-HO}vpvg!Ea zIT124aDy0;`0F$^8@w{+n5(FKU_DMcDekx$QJH)*dh4m$S$@Vqcc}!(OF(sNY!((d zva$0j&ANyr$gp%PuKd(MF*CvXZwtA^%YOx3;*pqrFwKvnJDXrFBW?x331|>`&>#V2 z`sHRsLURYGt$pi3rc%IXr}z}=vVIbgxe0WE!l>^@49KzU;hCY20yeH%Yx1#r)i%hF z)IyQXMWj3!>~3JJKMqXFJ1FTpj7rhN(9H=`2G#88$KE!PAL-q%^mn8Y7#%0$p4FGta=KU$%~^ak&2M(3)kzzuI`#PAd? zGNwYTu-qcn`I2D!gCHc`IG>}bjLGxA=CyO6E~;cCG{;y1t{SN6Ity{<@YK!^kkd^o z=x}_#LPYga>|srUF9|oh3HSP|6zEU_XpJ&TzEpm^)f$gVlY;KkG23ZB25*bXLNd)u zXm;qhmX(J*7|^O^6+qQOHb>}JLcW}!&S>9uLhrR>nOPPN=6`<;+DR>-y|ia(ylv?V zi#@6iyTq)pN4CqjvQ zp$q@#Rg5u93_d1${=@EoM8FrVW#I!%1sDjy<|`lrvz(WDH=z2~BU9aBa(Qc|tQ8~u z%h#P06v@3%=k2QN_R6LM9FDuS`CQfjM(bQG1aGe%4xSF8Ivt0w&putVA`rLcD&+S)5&QQ*U{iqRi zR%O?~aG`zUa$$tZ($-JjOR%S4yg+@RN1(x^uQ94Nukc3x;iL{U`#w2}XmJQhe~YfUEs z(%BkccjDN?-8m)Uw0J!fgGBEKYi_|#Qy+$9)18XSzrF%EQGJtIs zPyzPTCm`s?*wgJC0cwrMOmNG_`vMs8%n0d%Zyl1_S$5lV9_R;$nevCL#04=f#ai{= z3%p;m9aoJk4i{63TG*tuUh5WVd-(RLWbqfZ6sTvhEr(x50`a*IGL%cV?H07TjhUd$ z60=f$%l%%S7Sw&(zhQb5OeZ$%7d=3mH;*GOKz(r5h zp|v(v5w+pW2rA&V@sd^8AXNBn4dB8L02hM9;Q>DYIUq%HbDXN z)rHTcH=FTI3TSnUV#-#cD%P8CaEy`d4TeJsDVg{XRTNli9t4dQ{7eSvK#FfSO?tSj zH^lMir!wQ}4{(j@qD7=;ptl~9so;E?P6juW#LmS~DIgGxodInKfBKNUM$4?gsPQZ3 z;CgLSQ4~Ud>(5TcOTicXkP>fpyimIU7}$@?V2|7lXK6lwSi4W5(q`G_tPem+MuwIzM<0Ly zS@r>Pf6A>a&CGVWR(t!ahU50w=?~fJ8-*`CKyRZ3be6i8XL@5#t@f+uR6ZVp*$`~r zhl6$Hkdm?FTiu#Gv7=c{wABv8 zniz#3uLEyQny5v6sl(t}JhJls9#>5%(k!mh3}CR{!(^}a!*=PziJDL3PkQ$HTf~)*ozn20b>x|xvrA@w z1{<5^K@A4u^^2j5>UWNgW-Mcmq828DxMqnmT8aR!CfV9j_k*IxoQF%n|E5J!&}l{L zdixAFAPbJz)HPdPy?0r6tjYUfSbP|D3D~aQ`!&p$YULMF34(pbm!$*V$tT;h$-Q1q zNqRKYSi>BVn32H52+YmIoxt+LW#P)?rs$PymQpbEl0Y`hkn*d-{i9}^+;8BR0UP8o z%YqhfBq*Df2!rj1Q8{hk-okeB=F2*}N@Dd18d3ZCz_HgEP$TB`T1Dq|h!%IX2&@IP zO^|)=8Bo&ssn`aV6@%((aLSi&(@VMu(ggdvfAs>oE#_W;_|{s%Q4+o2YH{v!c?yxx z&1NTu{1prV$nUh3HJ3vUw-OOs@3r~7Y4Tp}idD4^sL|~~DaE2&m;lc~|0E~g>50$5 z0NzQ%m6L%4i^v`F5T}hEi3qFAW}9$ok`jf=VKHr@Xq!pLx4?SGo*A#RbesyML!a2( zJ|gPps$|)dT{ejMH9Xt4-7168Y18K?hvn-L_4d(;LLa3+y#Pijei~1!XN6>%@%G&F zjYe@Rm7aB;%^8=0tL0#)wW5+G`j`Vq*A{9C!dr4GY%c=;c`DZJqr2m*X_Jgxo-W8_ zOUXk`LPt`mpd)3=kz#Z0{nHZU+fOFn`Y@ezxKkWZ1@-Hp4o8nUVO!@;*^TFRzK)ZW zND$kz%9Dq3@Kk5@x_T)tw>DE;@ha$^SWOu$fW#1;Yh@I>ghC_QF;ILsEe1(lPjx>H@TroAhKs z^k*`d0eB3i6KK0R6zq2v5T@J!$j_#w#C%V8Z_TdQQ~L5cEf${@Q%@Q9I{Q8Z=~PIE zJ_wY+hhjgUfw8FjV1f0#vehcscg6D~Gc^lYA?Sz3o+&j0eSJC1U*t@tyMBC*O2M z=!N5&VcvY9LQHw^Hc$1Q6+l`&8v^KO`WujIQDu}aeKUxx*Ad?SI?s%gCgm_7dTx~ROb(;Hmr{jopN8!%Ek#v&xc*hMDtk`98nL|8 zI_OfB!8n>wAG-j(Fh@Dq zbLwjJ^fi9`JJX_Zn_X&tN#jX`Tb^|1ox7SNPmhv-j?h` z66!@U#qhgH$K*m7yXL)q2Wwm_kti9RzT8Pqh&3=ABVf(Wsks)sZU%6JWJV|YX|f^UHDU?XgCOvqhgNlC;mUd;|z5NeY=ai3eoekN{aiE7aMy2$r0Ma3U<6)jO&w@bHz zmqa5*4VE!KPQp0>5(*+w4{=jj6Qn-hPPtsdH|t&^*Z5Aa5x0~AZozpT6j1!SD% z-R5|#3M2(ogrCp_k}mX%YLk&lwDSR~GxrgYz&(dV@|jp-dZGWpJVevi zT&36d%EP_-g|xCUi`FC){LsacHlYMO3M_P7#|H=dt)dz$9t3KsJZ0w9Z=E4E?5&Fz zw`HPwV_M@6o;x0p7)uHXz?4D+WEwf_9{&ok%DEqi1NQsH{rn$J%jaT*VYX~ICV@Ac zwu0!~*1%Xr0b;-K>7IpNEyf)a8Vb1wg+p5G%Hd$3>+FWINx`dqpmU;cZNXyIOJaK6 z^l1YSD0SMaxs7*z5>)OB&lc;P#j%VO45|?a@q)$T={6$tM?=wfEBvvd936TBh`qHt zYE#ytKJjRzhbRkg&Bm^S^snOAvn&coRW(7HE*O6nOYw4Ul>39ey+7w1?a8fZ-fI6; zbuMno*~$X=m)ZFd#pX#i>4xjzSBgbWzEd0h(d7%Dp?x0t*1CNy8WG$utjXR8EFw zwc5MIq#RINH2^-v88!a4anitfM6BYbMf6SdfLpgfP&(G;qPQ zF9BW@qM+r)PP5xBO6mbF3El@s-2qOl!Nnn|A&{0vC4i9&c+$0;g8kd(#o(@;W`gEn z4^iu+A&R=;LyH9bA=>=AeX`*Od2S+imxZ3HO5OP!BKoG`svdE_W}q;>0p(|%L@J_8 z&pTr7PerusjyD^%zld<%?llj**_hYPuH4oMo;Lo*RnNI5<_==g8ZMfn<7)g(%JsWK zPOpg{b>%WQk6_$kxKXJ2bSm+HHmLA{Vl032=#ncAL2r1A?`#Mykv_k z5fNhe$WeU_D@CIp);`t?><>LpuZ&JbVyTYtPa3%Cu5tT3l7d-fafg(D?1>34{yD?H zJ9ZQ`FUc{E+sZ|>H5coAYTIcU^MzYTl%rMFf1^#w4hh(rkHC#@WX)WhaDOGQvtja! zGyZHv%=YxVja3i505f)A%HXdfkoY(gF67c zN~|e`n@OKcEob|&kI?c>&onMwLhfQaI)RC6bKHyWiG8hO_F$gG|M4}2-^j$;LsbQM>JzZ2Y9{kCvn)QX1eDbb+9Hqap=M2`7UJMs-11nx09!n8O720uyy6# zi!95tL8IM?_lcdn6P}0l9ij6s-P04`-V-2xKhE#VTGT*fFOux8Ei4C_YLH}t>0gnz>hoO{zb zr;Iq)513zW-KaszdJV z?jZg9DS{7LcT{u(yDbp<&*GXay;5I;IrLQcm;1tX!jDoXusR{^VlMM54ZPeDw~53m7{Ay8*&AhIJ=m#OP{5!`uak9=G{lb@E8=jZ>lK)j;Js>5Ph1%zwx z*O?Q(<5N%?5qV_qm)~-GZZzbFfjoM@-%1gTt!N%;Qn;BuZH7bp%BRaW=a%NJ4{?ES z9a-v3SRYWjJ-)9WO>Dv|J3uZ`4(3}%goF!Cu#{Mg<2fei^kGlJLUgSh=6D<0V0v(L zP$g~;rG;C+aSb4hSAaQ0;gHUTi@piNrL;$UUp12TcTMfd2#F)q%wb!zL)?~OYbK@G zV0}X6)Iyi?vhJlCTwI&a554?}Chxl)&&SW>nU&{_J1l{vg48oEYeFCy<7p~5G$fJAOg4!LOjeh7DaFxcJ} z@X-OSl^|=gSofQJ2aPF+m-oxc=(#ykrZw(4T)9fYJ=E&i@ zERLh0lfmxYXLkg*H-6S$3JlK$ylV+_{@aRO3>|(nU9RelV@v6<&G9I`(Iu`BS-lkN z&)*$C_r6=N78zu;LH0#|5tP%V+HU7Hj^|PyTsf@Y`kDs+A{cj{e{hQe!g<$R<0m~A zhspq@@&QySv^cLVgw!3=JA{((C_ecJbk7IZZ3ZmtKW)_ZeX9`DeZO}*LRh3#n2gkw zqSL6mP-9Zjb-lsLjg9^@=3ad^6Efws`pKe}( zzwCiM1@Z!u6>FcSx{Lu!MJm$7%)=fy^s{vt!o0bV&D+l}nSFkWJdnd-4KkY0LsWI8 z;k(Y^Nb|^W5;{Deu*{8j(-k58-tpeE0IT|iv~r&;*BUkgH#f9@(lbIg+Im2oZ(lIK z0i=ot4=AWL;Emqsfej~sCV4#%cZQ}RUih9v_&f7G0 z$Qd8c2_bY8!elrkP{h;8Z&T_Za`Kp3jeZWHN>NT(gM#SASjrG>p||6J+)r-%vR~9s z!D?w}*UBJt`sU}vN4D;9Ix|}5;{|aceH~(7l=B*)Jv%2p>Qe1>OU5_S3VpAgRVBN8 zZ4upZ!SbF5o6;-FuDp}C=_R`)>>hu_zU~TW`%~2ZC@KAe{j~sR-K7fD$+koaJRU~b z@Bwr7Qnb8n$TFQMgtnteDPGk@-Wt42&UJA2TekHrrs{7=g2UpR-sJm<*XIt%H{PGg zjmWs^ZdF$Z*onaGk)vDp?Wffe)smyMzFgSy&K(oQ$wIvg`Y`T1#zCT3IfGz5LS0tB zIiOq_Ru#Iv+rVjga&YPd2J#CR+n$_A6H1@RwtJ7g_sdfMJm2alRUxD(S|LQm%p&6W z8W)y*GJV0%k5#-URm+b!joYE|VjffeyIs&W6yJ}eVgH7H&Xr=zPlWYxsif4OY zvu60wi0Wwj3YvfV`>Dp9<6ZULB$319_j@g@% zu(fPQY=o!wefPlH0S)oo;YX{06E5e!Wk5scpm)ic!L#Z+(DKOd(Hw3<#qhi|=<(9t z6EQaJmgPJh){7r%!FuePf8gJFZ3%%0CKj13L~4Y2^nHpw&|o6srtjKxS(v*u z^#+E1TB6OXLoh2ViXO2@Tg&@nU5sKsvN(b{?ECHRg7DzhHv3J-iowW})zkN?q63Tt zuy%(^YKmgQ-8+LBPfw;UBduQlnD{{)tJh+GndEAQr8h}v2d$8GvaDC|YGGCr?+{|w zhhM$JtDDqu0x>=0i3YIxySrrK^nHvCa(x800IOG$S>bH#k$}1D343*}Gjmg%qD82);W6IJwx*0^j|Q277$K_mr1wYD%K+aRH>_M(Wm;ksNt6#N20WGj$B&{uez zfkK~}0p{v(UBun{IcxfR1VR9T&=hLhKxCY*pPgo7uNIDUZRns!AeeE;hlmk=g#DMd z1HDX$V@2chl+^A|QK3~JC`E?m%$E*qH*H_Mxxz0LC8Vf}O9*@K`FUl8tjEJFA}$>B z2p}3!o(a%-Af{|odO5`&eqh?*ZmD9w_4e?aXyY9Glp6jh}saXxHcGRSZ{jo$A=7EHF`tYE-C~OAajKHR(ZP0)( z-`DQD?~~lZJ0-ln@;(lG^Y(`~2cDGvo|Q(?jDZOZBLxK1EZ&2Vok#Ce?d8jn5Enr# zs4%c8iMIx@dGW!>s=qZcxW`}w|+LlgN&cToJKw)3?qgb#wu&b6#S zs2RR#hXRkhg5UwptKl`Tci9|^!zX&P=A!+KYT%~v=_Yy@qbeep10TJga6S2cgLfaJ z+7J8fc8*4{2{*TnltOpQ7f~>@#6IfIV-cOo0i}2nnaVz{NzfF3h~-C;z=BUwnz%Pk86~~!XwAc#1rll zs$2EzilWxP1JwPvouA2tUQ_cGSZ*LB7%Z5UJ!!pf-MUER)|*%zy6c-E^iuym-6L&6 z8BQ+>pVjH5=i{Wmu(e~l5{hE z)O3*P&-L}RFmUU*`v}$A0~)3l=-+N9O!*PAP?O=-OLXx#9q+eTio;A{&HZia^00W% zC%Lns>U`cwAQKJ@gG_ioVH#~-J6^hQyHOnH7{3JN7;*c(rHF+0EpEch;Wfv?bUA-k z@h%5Yp~z)j-GxU#DmUj{RB+prU@Tktelf6+HwAxlaJ|-yemjCrAIKWZE=61CunKMi z$`;|t+`>x)XAfi~yNvDPp5WE`$W~$@ zMu-ssC89YLC=Jm*#)6K)x5Zm-b0B_rj85=f_poVOI}03|Ao9)oxA<^@vj+X*RCU$Z zNR)q1S3+bQ5HY@W;TLZ?sR(O_#D2HEWAZ4jG1jyJoa^rHX@ZsTPFvwE1&U&fg&4G) z9|4P{sTE-HcIR@6TD}h2%TYc~6Jf!;S-SASnR0^JWb_WK9SVQIO$N}HVGjqV`rz+! zJ?rvaz`ID*tzH3UUip0xjtJX`@Zx}cki09&_n&Ai3WuyTvQ3ZlGc(3*KW*M7-4MFLAzO>7Irx!hTcl0{OptGCo^@0=(qzhdOQysq+AQSZ zFh!0o38PVEz5|}>0Qi**X7T5*9|0gyM8PgKt>LCis!80P5iDwOq%bn(Ghu7vSsZCi z3SK)Ov2l)|aPcru4Chp#VbpEeBn8V|YqL+c#>6W@iN!Lz!?Pw9TwHTPq%P{(wFZiy zM>v*-q-4;|Ke5w6Yz_N+?&04UUDwl5)5BpUsF<)hJJ>3wTq0o9j{z*|sPBb`K#tQO zB{gQ&oQe`&3ROZ=cRmTK^r{N*d$i_p0P!{@@4UQGE=F-M$L$jro|-5` zQFosxkw7|pXef3UniIdtIrFJlABf^Ig*XdLJbUb?{Wz{9>%u)c00o~jq~$XlpEK>x zEkYhBEJm$06$>;$#ZZB)PwA4ivNH}=`5ZPCH=Ep&yMw?4Bx`!TesE#CoU#cXu+&VUC)8U8A=NIasR|K4d}sN%sJ0V7dJf-z*41_h>H+pKph=X` zU-W1fI=yR=<`p41R@C(v!vh%QHGR(F5)LyRWMg=ox5@+bF)xP(vGqpy`jdqQ9l;#_ z6g4=A&wN#QflyP!PQfrOX^O2wT-%Rr03J1u5WS7K18&IT@bPuUqoG|P&(4!D-AFT} z9cbJzqQ&zLX%%#EJPR{6(D`Q|4M0jD+O`7A`FAKlO-h74Bo{#z-bAC;rzT>Td6o;t zrunfOb9M*$>byNg5f{UpDhAi;+sfzTcE63CT+sEYM zaO48*b0|UZ%}3tVH}yD78Yt*`he-W+&bP@za0x*1-+)bAIL!O744ai>``hBsZ&3yQ zSb`4W_N#wX%^ei_BA*~(XnjyA&=OAfVMIvsEOnNK#0?kTo!Y7=P``~eqyg^*O)xTL zk}SyS5VR#4?cB{gahow@TstASYGc5!B-7=)A4$HF2TL=U54`*6iG-&>KvN(~JF6_N zgbkPs_$=4!#9e`Hv3HsD2o=)VR?F|bgg`D@!V0n5&V z*o=oJc}|nS8|o+6o#2@+NrnQ30`>OZ9-aYS{)(**r(T822|d)PabT5(HYHt`9`ywH z(2f{C7P6o?(t`IP3jPQTgf8cuDf#4y?|a*$`KY&g2-cKf`zq8HV z%k7|)^u8A1co0?Oly-MRvO*CX!|PD`rCD3cM+#J2r*py|FTQXqH+=UQTA8kJoGPqk z%aQ?S>#d(V)KvNHqn+wkT^KGe*@*v7XaX`$ljJ!y>GBKjo@p&aYwldd*)ma?|GTY< z+Ct-@7{g8)Af8edDpmkPtJ*M0r*kWjqRJBn{sdYq^SGjNH{H>X3m=6^ ziq7hwZh}pR!@E;C&7R`!Rf)Tfs4QNBYkBLIj)&q!ecS0NW8YJk;d>HM`pwDc5sG?8 z8%Ji&N6NknZR@Pt2UC1%z}V|jjbRps$bt9%0LCPU2gh(X-JW#RgkCIbf&+~bO9-qT&*BAheFl3l$6E8F`u(&n5RI4$W38fCbfYD^+Bg9m z4zXU*MybE(YAZDym^iFg=iiFvc%0|lumu%c$o`HXYa#tPg}vJ>T9**jC!8}CSGnVz z(g377hZcsa7?}jF@6H>0OtWC~g{1IrNkIS>A@-~P#*!W3&+vQTP<)C!?}=XcMO*Xl zen_eH6rF4-GLKe1ap*_>{KOENT@p%gm0bUEd3gudvO75UELa|dxp6o0Ol8o;c7V&u zHBfq}N|2h9)pX2;C_ez|Jp$LKC7yj)wA%2)LeQ|iB^IW+LZED%+m)?e`4C(Kb-R!u z=p!J#nqFtUm_~3h4{<=n3|wtGZlRhFd%fM}AkzUdSgkG)gS{Ej{8-v~TW+sHI<}TNbg{q_j18FAR>FTSbzKD8Gy_i7A!G=#h0{~<`)fkc(^0v2B zWRmgW*`TW_^9BX_n+^JN>y>9tV+vUzt%X-JznOdt3b0^Yhxa z4vI-;!E46EAR9F7egebFfX1HzypzWdl)F^cD?0Zo-tAuFp#f3|e#L8$&<_$jPHndx z;5bI>QJZnPVU2K=Klguw^z``Y`h(P!uM_?u?z2CO0#R#m&B;$iCoud!gVvdNvF(y^z63YCuj0tv(>;EMsiWHgB7s&9d2t zIE+6By|4NzkRyD~ugNHYesqq-=`(ScuTHoYqo8S}p#xWY^aOu_E@QcMh;Vq8xxK3s z5&S+%8e$n<{gY+*m)GWCqXUr*=ShqM@nA{+-K*K4RU1LE@~tqP!0*+lsRy1M9EfgB z^3W+dbF~}gLdbJiHI;OJ&X7{;@73rGyj>n(FbxoBzTSOLCZeJO@Jz0rWxA23r3MgM zH)gjS@pjW?UjCR>)JBQRdhuZ^*0|#973Rx&P}kfgmvD;T-)Qj0UZ|>#;sUVH8cXzy z3nxJk#M(}I*@QiseMd>l`h=C!cqr%5^7<1Tw-FomyS$7_g|p3zU{4|u``DY>7Of5w z;NErvO#G+apH`c#tVBui?)K5P=p}$9Vvj_6GFLtUR{TxfLRi4VR)-Nwjm`_px#uWE zSDjHro3EAVuKua+ijn5=Wo})PLe*Bi(P{68wGNjIlYbt(8hxZ_YFABjL_W2Ha!H!K z+zyXH0nW_j6K^ZlEJY6LU<<`m^R%vz`t#YMw6oG;on|M8K_~-w?Gr`JLFI2qdw*7k zK7;I>$iAEN&ce}-UVquk*K^k%px2+{{2G+;*e`ttm>6uK?0GCZZg5v*qJ_ zmTM85DXPo&msd{dpLD5oI?BB4YGeC$e#73CB=w3uV4X%n0|xZ*_Jwp7nCzuc8b#jR zbkpr<%!YokSCy|xq+5mK;1H9`U3kvq{|eQM0O zD-%?KU~t0JkzB=mGX3CfQ|_$)@&ls1@z-g$l|Q(2wYs;!t@l?w?BTXp1=xOpv^5i+ zJ)E{1KiJ(k2C{P-h?tmHz=X$bVI9a)Nt;T|-2`qT;(vQF$g7fJYZ2f!-hM88GS9rG zJhh#W(#^LRl2JuqOWy~8-{v0Ka$CKOU5}eD9w1d#mw)|H}BGX%P&%zUlxwz zdz_afD6|R=Mw;YDw5|GW+m)HrrLiJE+$cx$bxzHxgivK0@$ioFC8@8YRg-h#tFr1< ze*l9|Ui$dcXqyqb6{sIOA36Q}L7z8Ir;S2^w&bu@%jCN?$2vbDd}(jTw5y(_rqTD8 z7(~mvMz?egwu}|rPeG+a8zOHUggS;y{@?V7(RhG$eFLAfBsi~7-rV;b$~Xr8ig=Vg zI65P~%c70%0Sn3neb*97R)RG3WB65}v=sjx5xd93UYk6!Den@lLu|=31*u=m$fuguj0*wISa?&_D^-6|dXkJE1r>0{c4SS^`ktPth^{`20fh{0t(BB7Crx`oA# zlU4%!v}LaQG-XGSUvUD)9srN@h>*E!6h?gNgteb&4~|svCF%_#iicJWdYbX3H68!L6&$xRg5NM2z(Yhst(LE3{kHN9LV;7t@>N- zcb=X23L36qXRKcpnWMo-6qoNEUaq;1(?JAlY0$+}VxTjz_fx@>AaBhqi$($(tt)1% zdBgZAi%MY%(>*X}VY1TMLG77DsqPD$52SaH@6w{++bj&ZY70yYi4lz%g@U}Z_-k{^4QK|d#>7MYU8k?k*~6Bry@c$b{hzH>ryH+#laQGTtKaSe zp)S1kBPJ$Lg)U!DiR1CEvJ(6enVaq5C9Ot)bXv}RXfU>*GCx)@|5&pDN&hiievGT_ z;iQYokv`?$@!s?dYO(v%KrQxvb4Y}?SMP#0?p60dO3^|wT@aR-5c-MKi9q~brb$EJ zvFFRbC79z6_OKyfT?^6+LC|7%g6PoeAjuk_=z#kO9 ztL{0mSKj(>nOh)m_(xaZ985r!o6^L;uH5w1{+kT~tL_0~u>P-)A}iy66^&ij(R2<< zLf#&fi9G&qiqlmWn|5hkylbp2Jz3l5zqoE76@>^+0f^v?Ao%?hAoPDV(#!rW{6e(< z_qS&K)oxV%x4F@C?!S>=#4l0Kga7x!o5ZSLjda`K*69ZE&&|OJ%mn_hPs^$Qoir2t z{$LK{SP=UVl%EP3^vIrkcOQt@x#S1RAQAh}+u4WP-pU^d=ZHTUk(yAQhrmM^j9u!kmd9`_M0G<{bNcR84?faqVXLGi=9>hM zW?!I6w_3awuQ@3hVkl2EqWDMjD|uBBpki>S`QF);eP`YN#_eHx$$2!3H9znC=n2e| zL!lS*6(YixVk=j2w`(`%vooAUiyGsSiZClx>`04*_X z{p;@}OQRX0V)?SmI+S3Bi1WBt@iFEeB(*-!{yCvHX`**e)`qm$c>T?>6n*ID5Nwe5 z^?6(&NDBP1@7>V{Iuq05i`Fp2f)I@QIzNxYZVzwLjBWDW0Sw+N(J#f7#D!p7l&(M~ z&Flb308QwAW9fLLt;chDRuT4}^~T|xbOmZ?V?!A=P$b(hkd7j`b(7H6xM5@qUsmdc znil_lHRfYLluoT@%$g^if@FkC5%)Ie@lbxypP8x3N*uZmHlX{UKG|@Pp+VB#>(3J` zQ3+E@8b1Sk04Dg+4fnyOARK&^H~*)-w~VT)UE78QQ9wjWr33^NP*A$N8$^(l66q2t zX+cs-Ksu#U8l)AF?uI2G(z$>|=XXuK^~Qbg{XWka?>D|5-`Ea+Fmy53oY%bKJdg7@ zj`K?o=wIIhx*62t&jIk`8YE(XvePKz^ysnF6nLWf3nMWUrk`n3m&n-34gIa}vU5Tq zF<~NOU8jKga}5ZbbyT53Byi^*vrC&3NfJb93MM0412s7Ya6PXa~QQVheM9_6}>nGK{yE9YaxdW!nw0rL8E ztc>zmHD4C*acGC~OI2AyX=K2TzW8f#(`j zfoYlq`U_h~_7XO_{BB?QVLmsC05va~#~zfyI&bxge*oT|z0_vi;+t}Bb+gSNqY5m* z{zbcZ`6?P9AjX>1+&q75IG=?HkD)MODKT4>hZ|xMcNb&+q zxI)~4#*dwVdDxI!++|X1D|@=`!|w^QGX6E|_6OyS0{0>WO3Tz>Vqg<@$NpJ7SXgs# z>B!KH>~HtuiSGFE7skO`@v1U0gBl!U^Od>2onLy%NV=gm}gIta8BL{U(0f6 z0VwVR5Q%eIaT8tY20ZbYLm4>nQ{cWn1u8Z{1lZTdP%5wghU#SfS@7w8;wd`RCkxLy zMOfYxrIzqeoxttO_1;IL^K39u6;H5bJ+do`=d0;b;&&YJjiiS8jyz&}CEj1f>8nYPgCfdomYM;;X3J&yhl$}*jX0T#=3 zfR(q_-sj%WW!eR>vMI2l-iF-Ej$a3@-eG8s;=qvq$sK=_Ll~Fm0`P@#?^I5&#@kvO z=N_svV+L82@(}AmGbG?yK2k^#`$|J{@(SZY$)gdo;!fn<(Jd1yqX>S^nh7K>%U_Yw zIr!P{0PGpE^Rq6-HIo_4eUJb8NR$?OiB#1oNQrO4UihV(iS?k5*;xWdgPs~EdyS{V zp!Q@543-39i`A52bjvlQ9cAVBCWnm6CqTtGdnwY)Nmewe7$7T9P?1FIwuC9~gA-gk z_-??h)@1TUOGG>Soz)oG=*oDfGo5(hc=fk)Qa=K&HUroi=ErM<%Mr+= z*8+6jVwQB-&IW9+5#M{JEI}pAW<|EJKK@f-lofPt?4*&;~ zc7S%>La8=e- ze_!&b0;HFoP^+-ASJ{gfv3Rh9_)qpe-6Gd~yin3`joTAg3y2k#1TS@88=SB%ikmYz zlnJl6OqXd(&tCk{?DOLN)@x_gX9M}4)#uZrb>pd>NXxTwfO*N@oGyIOO~smCr11Uh z2vqgC#=TMyb6DVR>1pnn6dm*+)~rbKWA$DjR0HE^lg>FT0?4u{XjI00UoAti7rY?# zX!*kpu;=p~0oh`1u9R3|W^U$2Lu26}oA9@QMDuv*9CO$NK*YPHXh&OVOskA-D>RwW zNz(^ua}l_|qD7jpGv1ga2;?#?xoXOmvuiSI4@ES7hkRh0d@^kZzQ+$r6@?ssa_D3k3>A89Ex_}-H|Gk`hH}EjwjlWpOW7Tp8^K|6-d<+Q2zYX0H;gjd6^u3$L>NeWiF6J6;5}99z&)ASPkp4M^++4=5P1S6`=U|_@@@_`u+3)oa zx#d^jZ(lf>2DKbQu+3Jn#Vcj6elrqeR@ew5p=`y9w*;z>lqgn_&yv#@mv32j^bTxt zMQ4=k1M1^JG>%oruH+g5(CvMIQ9wrLkDV(qi%~O_Ms6{ULb zC;@r7ZR#9}{@@dpKuYmy@O0d0> zR9O|>9;Y5t|CM5WcR~hdEPPwZ*PvHBTvs101{g@Jw=H~uY9ybaV*0LLdm>hpQ9BW9 zCt_kaQ!mWo!T1x(;JlJp@t?K0Hy=l1-iWi%%-(A~(Oc*yj&T*g6NH8yA{*^77t~@; z*er1+Mly^>4{YlrO0B1m<`6C1sDM2zX;eZfG2*Tfl{(SU@+$cg*`BAo^MRWFzWc!6 z!XLF2RR7sfrCc1T);A>iEfyBL9rkt=E)Y8=U8dsW9S#nol6~8C6$eoWNDVwIK&8#< z)axk7j7;RuWEqLRfz!)DsE(F$^{Gv-=rmjzhlO(O81$F9fp~w{D2P_^LZsa-dq#6S;E)Vn_t!q6*nWQvK&I;e3oTU}))KUiAX}k4QE69{MAYm`fNB~@6hiLOyU`Z+nqwl zZ#d2xL|7eE7U$KmY-_`jx5k)w%a3dai$g!W$V3j?bgQp5JwyO|b?-T%=49pU!C37zo1Tq$@BU3<`9gz0NpPZ!0S!*`CY#Om=g&{ zX?Ja$R^ggj(2E!e$r&GCAE+O|C$Mzup-ytZ^M?ea7zqVHzSZag{H5z3ng>i1;iHHH zU9L*NXAuzwS0`qwIA&yhRp_L;9>UFFSQUGfsWp ze>q+T)O~Cr#lFZ6;B!qgY-2_#Ez4jX1K#Ai(2QqG+%iN9xPV1;nxzzozswm4f@JlG zF`4C>;FSZ0TdOgq9|;$tZGin<^Wk$KVf7#Oal&B!Xnu{up(*h(BU11$F95u6qg}<^ z;7G0q)TO9zc}()p^`meG{Ufh<&e-KAIB&LERf*nIOYP)ToWec!_jXL62Vn|Z&bLUg zeHtHP&n|^6oL^P0{Fnqo=U6Dk6qqlVe7$>y^S*x0t4klly0*7sQ>ZhP@@U2J8 z330m+39)<;3Iai3da--)iHhcpNUJW9Thc_B`;gOg^)Qu86!yrqwYa)8!^1t(2A7M> zYl>^^Yfn7ALCYKrAda`zF)|n}kxhDXi#R3sps;kQ8IXY+ukk%iM7ns{Q@6o;G%cGe z@2-)-Jpai?FymXwIvllQ;Mz267_hYmY^T0Xg1A>aclU?P4=h{aZHkCnYVNzZ0Y&SO zg#IXgrx{D34jAN#g|Na21}3Zd75Mb3b6%Vfz$JY>3UYVjKwwhh0yPt4csbJ&WiXTo z3^kjM)Mh%X;m^0Lm(vEl`1n(8!BlJJia7AFSN2?Jr>}F4Js8=6Jh-2YizGPgPQ`4v zB0#-Fq-j9D`n2z2l)b3hJYboC^f~V*kHpfUm+MSFjafCWZ7tvC?990WEO4xxgSWU9 zJ~EA=>63&wIW%j6VEV@)0wN-^r-fn6GI(Qs=4KQVCn7`Qco=u&vuiW4+Nn)(@Q{!8 zL?ACX#o-d50je`sC44Gh3!Lm)3qZDa zIhiWXTMZyV0;oKg~E9!h4pPx=Dn5#nFiPncxxWpGj- zT|{)s-inM^tq1NK ze^4?Wp83(p35u*A9$@N&F#Eh>6^UbZ6PX?ugB1rHlB|aEGH^NVSkhH+9 zoC5gzKH@5GdUsmcl`Xf+#CaeyKg>8B6vJr!h_|HgEK92~=3L;ESlbK1BNG$C#U)xL zoil@)A*8TP%mm9RXScaqM$F$`hCb55TDh=(A~BeiO?ltI^ZA^Q(zGJ;MBp` zT%r~kCwN^J$w2J7cq1K4<^(W$FXK=ZZ7BGJ##T(BM=n7GTCsx7?$L$fI#Js#+N`H? zpy4CULfLGS=jBIT__`lQ-BC#i_?e=TqA%+Z3CMn)$wxiB-6;4@*h}=yUBvb8#+VgX z^I``dwTNF4(cLMmXCI^7O#3n_{<`dlf=K-O`&34=V{c)ff$6MAf(!pfDtBk~+mp0DaQ3a<(ibRL_Ck#>XV zm?pbe-}UXIAAoFPNx8DV3v*n~!#8@KVJ5}wJu~DlK@@Fls8&ouOqq3Hbp@T)O(V@U z^;%i*&U;og+f7wc!D6_r6zY{Wk=#+77>CFY%t{ez;>1ISHS;4?sM}cE@5)PDx`1)K zR5Mkb`DS@|>vnb6Y*y@wCVk$+5}p^beoBE2F$NqTtEmsXNwndcq%RsU*&DeYRGfmK zf|!GSGK;<cKen4i`tHst-j3i9?RM3{_Soh%Fbs@8$$hrK( zE^U(44Dtl=lAt6Cns58PQqvHrE--20!EiPK=UW}X1&#g3 zZeMNa4sW1siQx~GXFnIHemV2ajrbpui^0DjH31)7BfF|oB-C4vSYLRCvg zeZTj;v1e(MNMo_cXUgPjybgJcZ!e2etVXGkn6$lv+^k zlY6iUKb5&d70{$(8b_4~X*jQ6>R&LeNm}d`nl2D!kYOsc8BMgN?hAjS^AwLLUbaN$ zO&*!t1zq}Qw*oHfH7)|ly-2^(b%JUmr4y*OM}N!oH7gDsY!3^wBaA%?Z_#f-(Koba z`!a%m9;pZA%JvrhVY;cRX7oDC;d1s{JtLK(s%$**y=BA`<*Ek^xwG*}~ z*VHan@|*fJI1C7|rkQ8c2ggM$?s!lRnSspc1CF27#QR7BxFXd+8eehHT{Sr8&RU2{ zPe4QDPz44+ArcAUG&bJZ^YOU}_BADy@AN1`$82A+D8$(XAb!*YR(kXzt&o0DB=&g0 ztPL*;IKVWF=jz+7HHfc6jf%7-J(H*qW}n6K&a@aY)Uq+xv9VxP+(J@f<{ut@shc7h z4X`)jQqeilWF`C%z3IEFN1M1I`zkn1c)=ws-?HpPkMd-nT##>Sh1uRu4>KuIS-Wo)yvdiA1CP#9LT=`@#Ft&i&sL zzQ=kI4!;ThwC6bJ*(qB4;C8|e$;S;*90kSUdLy7k2#S|%l@>oV{)TvJAFwN)fSJHr zd*cOGlw^7WMoInqV(_Glpt+bADnq^HeRlTE!{9i{n z*r+^Zt_8k&og3_c$VIS#r%Abr?TojpUU^Tvcd|?1S7vDe_JG=5wZ*SQO+aEZ1*wr25+5Q)0yHc|<^7_TkG&0PfbYWZ0TCzDkkOwK3GeD2)uUYii7_g4c6(Ph3+3iEqku?j;$JklX}awP~~pY-8k{P-0d`1eCU? z2Pg?Xr;Tn?>JxRLd#5~$r~ctVUz~ZPomH|{81{YgO>@O_ z@(evu=|+3ohG6f>NBy1!hmcWt#*j0J-SSrO24TloSvFIrV|Sr_aJ9#rl*bzC5uw5x zCci!GH(ftI`0Pke|LaI^^DsEd2B0x>0@?|l!nYc%xvzO39OsiIHgA^b zoqDHFFw2lKoT>)6&lA%%-K&&%iuM%9VlyU9XcvYyE>3=PzCu(Cu<6}Tgbg^*n3hSK zts7`KBlDe%iPxtl5xO~pq%Zi;ft=$6+4bTiDR%XbsymlDYvowA%R}AYlg+s~@{h1h z$0FAoFRwgH5iT8;PoEeCw47;QsiD@Epv7fkjmvIg#y>PJZCmv%9hbcpAY-hFS1AqR z0H^u>^akJdJpXBhtV_G&8vPnMdQ6zT$GK9ij>%csx(Jk(6(&n=jM=th!+PYzxuXed zFP#*Y;@=h5dN8In19NFJGt3>fFG39bW{W0Gqn8L(@f)sgD@&SRU2cEX3?PIKdn*sa z&6;n>iU$f_%jUrXu%>boZgC83T>R9dtnCRE5x4i^%c9CAiyxhd9g7}N%_@~-;{7ZV zeoONIb|omEtc(N+l_Hi-xLQIFFEB`iW(TuwWt#RipzoGYaAZ>@>GBiZd?0l5QWk#Z z5N<_*TLk%DVT%V#A1)42JMO`r5Q|!GK_XdtEB9g3-O-kIzS@luw+oqHJzc^)3>rB_ zjMEhe+}Us34-fV$HE-DkiIMYzN`hw^BIOg+*ilVJL}|x9mnZkk4)=*M-dMJOT!8usZvt9g_l}JZ6e$-uV+(IOr%GK)XD8{v1{&?@RS?%%ntkx!-lE2G8 z;ebO$B9)sWU26ZtpF+mwHOR_K+(k&LP)Ue9CVo=3?OU?w+`#P@ zz2D|g_kiJj#$}tPvl&QW)jM)4+xUn(TFgF#!WXJ&LufOjkp;GO&+kkK| zz;5nkH!kLDp7h;CSZqPRk+e8@RdmqVxX;3pTTg4UwM+OnO~W0WQCC(^;!1~$OMwR2 za*WR?tWL_go2o9|KBt`5L+6bV5~D}--s_YTj-9F@r$qBew(UvJniHDfA^$C~j}aQX z2J`Ot`e*^Dd%DF7W%<3W>rc)gvCXQ+myJ?gm<#3eu|-z^-zj`+;G(!fsUtfEN=lFY z%A{Cyyp_J1_}SO2mu4nKAN5Lbg2q(@m}L+x@N-*%geswj0lhD)VDY>V{F(m=KGwlF z8J<0EMJvVA!Dsa!AsVBvh@`$3j-MYEeiZ(VG$8IvRk%+>;GVH`O39`aEZ-^p&^NiOk_fF(C|vm1 zbVbw*UoEEMV8)YEP5^Liuc(2!-gbV!;RxdrqEt(%2&Ge`{f-Hh*qs-RyOJqWC*Vr{ z{x~y;m&(hodU~v{s3qm95P9E6GF?09jn-NYdLwyj6-5#?SbW;zi{(pp0c$zcUmHMvRGQA$lOIM+wd0Nxe6Z>3 zP?31Ji`bJy%dJpWr967>#&al3P3-Kzw-8ehV;k`N4gVO>>VJ2ct)9H;EV40^dCkr& z)~+ORj{8ops!ocQs%sROxk;mny}#>)NH`K_%#SRHQME0r@jHx@z`Yd`;XlAq062N0VUf4sWd4{VXtL}86~J9+$;PM($s9XDj} z51PW*0dfq+pqyiN=(r`W)ewI3-PgWcfVFX(lFhM?RG8! z>ij$t?y9<@E+YY~OgakWl^%&?vIt*w1ky3(mCO84aqubTYW#wPlc{r_zrhT*2atGFs>CG77xv( z%558K&Ts+MmnYsxh~213Z#{+*mLKzUw9-8?`42xL89bYvVLX<8SD0yA7vfD+9d;*U zd|&y6nG%1Qe9ubzp}KuGebweba2e*}1kk3K$9*zs-UL~C+u^1w0KWo%(C~RsQyz!T zZ9O#Fi644_Mqjy*e1X|gbfR^nE;DH~K|#bEZK|)MZ%08fXRs=Dio^*$ra#jzNFNfm zkN&IxKv_dxitw+gS-P2dlZ318M}MjsP=g&ixu_?YNACvqp>&$fyKus~#ap{^Sr7{E zUCc-HV7os2N4mF#O5i-kwa6!hbyPE0rx789W;U0lGjLps(&E`f30~K&QaZ%Csn3l2 zT}y=&5j@u$g!4h2)z~Mn{b+jyjlYw^>&1HR^eC8doQNW0uU|R%Yc!=Z+|A>6Hoxt9 zm9m}=SUQ@^)d#cr=X;&{JJiuhOFT2z*?Usm9J|j(f_yp61;25!3CLngM`~N)CbcZf zK(6ncqS(+dkCzQEQhfoL~nFqbs<4hDM_m(Ly$F8HfWB>S?S}NY-4BqGy z3XHIaI2Y#u^1nXiL)L`cff5&p%Mwh)nvI(W)h6BsIc^@Ak%q9>Xcnl;j8K8;`;tub z?LIIFsbuP)Lx3!t-we+K#!`6m@EObf$1UtXgoT~fz!rLIUWZI`B_ZWY6c)EClSx4^ zory4MGp6Fn2tvq`I$17uHcPd*v31b%2@vgC)eyk=t;-S?AxSP9&R_+q!(udpuFxw0 zG~fTR2R2l;Etcl=6r|p!P&>ee@qxTyA41N_(+usUYwT>wZ|p;7O1LMl1O@WKv&l@| zR(57H19f`9-s)d_B)+dM?}^W%m~cC_%k)I^tN5w>8ob*Hz*koXXbqIT2P-q2<6Qtv z10w3E<1!-8^?U$6kR<9ufDhZob*S4b4mF5M5ggkzfs?8o7SP*%1270tO7Zt^KX_n= z0co8jEg-y(ULa5)vbE_eS`u;+E8d}uFUe#YT+-axfEVMk2&Y=BL%yPAe+L}2_(g3& zxlBo8RoUU<@8Np}YGRa#E3~=qD#wP+Z?1BfH{@uyv5MgP|(p?M>Qv5EY|9&8YsGbwYzznnbiYmS=AH zThY-BPjd|r-_?T`PRK4ACrh$4Nxtc-ar=Osq`L6S8`N@~m(5;l!F35FQyC74^s?&$ zLn|?oJu462yZ+7l6*%KwN^6~!)yJ-^Rt8>Yf$cWT?6#}d-)v+gxohC`0Goy|)rdfSBoE8=G zl{vzO4~jgT@gq01dT-oO4RoqwTUW$h=q2|9V*toJj-WQ7|HBUnJjW`p zQY=)caR81J=>{=5f8f4mm7RWB;C_p0wey~DsyyfnC2FP;kra)8KS4Bryp&L#+gN#R z{hK=dsroLlapo6p$(LjCmY15F))Z-ST#tlxB&_KkIEu$4RfKF)){~&{M8P$Q@Rj!a z0ix84C2MAyyjmj6`2Gm8=u=T3bJC5)&tY8Q@t9^w(PC#L);FnhJ-#1XSWS7Ua{va2 zTPm}fA(g>g49239tr8hyRiw+sOG2C5bFr39_5w?f;T^?f3cL>Tjo8>k&7)gFmYIK()Cs5eIVba^N=y0MTLyvZB zzke~iCTEOWI6`79BXJuxjH8U2Y7PtWFwmYfK-UR#=)h$uCJ9`F_=4ZOOpCVqq*d(l z?;4e15<2*BMcZR_1PAHW3h{c(iS4yU44Wv@Q?OgvhuBjmx^Zoic0}pauhDzb?-flNQ0*Ydj1cy|iZ1e`=~@(z zP-Uq-leU->3MBqT5aCv5crLnJc zF|tR6=2r2gU&p}Lqpv;O4GUOQEO)kl5>k%BVvG+x-iQrA_MFya{!5LQjeF5u8p-J6 za+M{*ltF=j?0%dJ1HFtoC}cH2k>EYr+CvzQhru=dz!zIl zg!d}%Fu6fdbB=z58Wq$GIQkIw)+BJpOi`q#!&MYc>6V|4z9a$bV8@0Wa;G5dI43mZ zp<2re%%@ma1uxPwiRj1?Q9P9Pv0hhbbN*U6q;4wu#o^^CouD$S?vo+DEv9FCsC>A+ ziV0}XTghuz_U!TbD8P~}tG*f!q271XHY@2Bn*#Jg<6<)U>u8J&rq%QI zd53}KD{6{w*jdue54Vo;>Qk*!h+l?kIz7B@Fgbq!hJ#6bTkqJz-~x^Sjo_M zq}{3y4SM=CYO{IRYbGxNt+N&nUB3rtPZ`2rM;sk(M%G;w`Ox@^e(JdKs^JwMVD4Jm zO$j5vnt}+8rq{y)Zs1ZG_cgj%q*n~K1=UfDb6r+6J#D5$Yx_Iqs}k&)`j`Z*|5_m0 z^cjP2SDVZHF`}i8k<IKsmkYnLJgVAf`)HkW$zYk}q-1;2bS?ZYP>QNeC^~3w zN1R3YX?ouwCRf;pu&Ge#hl^ny+f_FJWK#VNM6zQds$E=JXFN33wS4K#Y4b5xW&2k+ z6*cw)ltw$-Dr&AT)m_8dkDR>7jGMd2;Znq&8htmi4zCr8#37{oA0c#8o zL=Xz^g7eAS)o#LhZXEw@Bj9dx2Afp=hD~Y+s0kmr7q?Ox$zhazf1|ukVg+Q{S)VuR zBOJ&4tGk87^|;C%5w2};x6{J{AR6BrR4{sz=!f&UZEUS0vSpdOR8oogcVJHcBnKQI@rt|$l=)Se z>Dz57RTOYZL<~I{%H4|ivnmzjPJUUlU(xavSHP>SJnz&K#FImB;j$wwkuckLkV77* zRFZ}d`mziIb}mimotMlZaHDskM=zU6?3F|sI`G`zK|*uFl$(z~>$Nt~(DwA9u$XDf z_Edwids9oLna>MDpg%_!(T@RuTJp4sLW-5T-#{aF<%~~DU`OmomI6*pYh>_c9$@Y~ zqLB{TWaqy9aG>ol8@>*RuuW6iy01Yx%cksZWkL1&nZpBVJeJ~M_*dbJR3Vt?6~VnP zPJz3)574o*J~UKI;bXa86TGSXa)tzPgA?)MPLZnmMS9*`x;A{wFCu1CF_b@S5-zT# z0R-0f`7 zd^)us)sJM*t$*Kz4Icg}kFw0?Bf@WdoruSU06)r5anxtr9$0yKiPe3Y6 z)h1X~E9A-Qn?3>0PVnGwqPMTz(% zk9RjGp7*BsU1(dlT9D3{MoCKYnt{G2<(ej7kxailobDfg+5SN9#4J8N9@yuDVA1<$ zuy{J}BacyDcjUqwYMJk>@i)hEnof{;+k-XfG2eH+KJt*FBjCIhpdVx_vyoE%C|`A6 zdqVUc{~T~IT%cbMYUif+4u)(Jya4ZwGa~T);sDN``hdb-vGEX4lpigDy`t=g{obx) zn+n(cms)$MH<7Aj1es61KrI4p0%Haab$6G7ZOWCssr3$(O!1zMQW}^%l`I8ygGb06 zUy)AxD_k^Z-PuCKNzrX83wM#=J~tffHzyTeu_vYAE3Wab$xbk0U$?Ff+Un=vgEP&Q z_f?>gb4|Y76BC+e_;{D<^x?A}vR{P~B%I7Mp}{rC!}bil(xgXjI-QCofDe>xe>a7C zZryNu)0F~<`GM4nbsIlYZmZ5|5K+4IErtq67*F<%Q~;nL&yLLPA><9PCx_z!2^xO> zwc&7AHrky}tEylpnI9#~i{geecHXdx`x)7bJxHPIDt{3`_srfYC%STlQSFxY^ zAVUXi0tVIu2DVR0B+3H3rL4QuHTl6_PgHVtH16X7fPEUq`dG zIGR7Fb>i~A2!>L=vap!(XP+NfGU4ddQLAGLpF`$|mvPFSlyys_OmVt=8-OmSu$9f=RFk>Jlyiqo-|8mN$&+|Z%yYz}+p%Y&V8P#s-QEQo<*f+k{Bm1Rwbn>vySsja zFSQ(i#-(E})h{wh1+8%=TN8s_e-o;0Zn-NP@9rW`u?SlC+S7TcVsm_rEghwT3An&= z7r%_q0|4PLh>UQ=M%+wW9^i3KI@%sZ*u5ik`+=21PTDxf3VAyi?HK(gl{r8Nz;;=q zbd?i-OlReg+58e@Xt7dY2s0Yv{0ox%zD;7cj4}20uofRJ(khc=Sd(R?s`WjMi$g!9 zI&F=m%NMVUGSA6GKA=5eKoRj3&lT**kf+zCc%pXwnTF5pS-_qOYQI+yHiwZ9>Q05! zmk`IhliH(!-ZG6Hkl#kY5Tw2F0jgZ(A;dI9p1(C0KQ&PZ3M7~vX#O3}3r8hdQIa@T`0r1aiGSktxv z2Q|gQGrl;tu}6=d;(a9Xris@QobXjL78b;j?58j(lnsIED;v#algGEnkF278orJIWmo6T-a>^zD&~_Y6c#2fRIpy zt3MVb*v;^2{mXr0XKczb;(G34kWRwX#4oVEA}%#{I`~>M@97t;rL`pam&rm|!`L9# z>!oIFlkhqcP5uK@F&l{`;$&?qFP;HJb@QV+Y~3g>Y(~O4Q6s(@^|~y@SNH{_IjyT% z7!H<2MxR@^JEqYdv!{j!=TblB4~c~=9CdHSGQs?3um!Y1Lk@G4&eq*qn{qCnTP{Qm z;vKQM$4ye_Cq-AjO(+Ps{Rq@Jn?HgY5OmxU!YP5v`tT{}9uT1L*jyvs6)mjrn-mMY(n|JVqbnxQ=n*3FJNA-|U{$iiZ|?W&=poy>sX zYec&=@rvwA^L$FF8lW%Ow9FoiSqt-VxN+U$1@xvV5CJH78dPzdGnEv$amHalgE$N) z5QpL5+M(U$lF6R6dSAgkX#)6OT)40IkxHFM0@_xNnAYzPHFl{9?#w|o|>NUsqmU*Bid78l#*{64gm|% zzQ`j9x?anSYgR<@#R*l8eNqYU83f_Je_r`?+lvWifnoM&HBZR zTTyTA1WW1hmatC!dP9Q>eK>RI7a8eXe8;O?Sqwb1>`(Y_3IoF;O0w_ws_vCzkIX%o ze;bE0%L6CjcrtJUGC|%p;%x?$O)NbknJoHDpRD}N+J;AL{er$l6$y@SoCs4wl5v!` z;&X?pv`VXAc1{AJ;j81(AbY`O5oL4!yjZ(4(?1c&dG_(-QPwUgF{$hF#DQJgh~swc z#Rl?=!JVoa&WwRMdm`Hd#5Ugr9>pqaDUvF%_e@=`yTES>q;i>88C7^PIw!*%QTZbs zF%rKXr8+JpQ5Xsy%f196PmJ0rsCK306L%W(f7|=9Rt0A=%kix`?XRL}4;pgbz?$~z(UMDZQHc@# zoKpeTmeDfNAe-sr8`Dzp0+Pur994v7l$}37=amLe2OQzF+5AjYrRjqG^5~a zC4)V3!Pg{*Ko?l31;W~EQ!TOJ35t+t?(YLOXgEe!kSHGEk`qW@V;+BhbukGfvR-KK zd-bg6kVB#|^p&}PDrQSi&f1wFpp7WYtu{cSb6dpVBfrm>@Gl|BFRM27Gs}+nEGWTt z9vdCA?MLbS+O6eu4wyYg0Z+xxyh@JTfAJ>?m&C@t`d=+Fh86H*GQ?GlKUeGaEFBA- zY6begi=$;8lR$})d9AzbHy}EbK`!jwI8{VoV7j+Hh3PLcI-@u=C8|?-d%bgHxW17( zA49utTQ!Qs!ses(+L-mrRM@}s$Z$7)bC$*IKo571jo;)O@l+YUkB{N$u?PY4$>bEQ z^)y>8vghtrjQN*>6%YO^#PpmXh{__b&)(?c`a2DPOcvaY&z?Ud6ku*+?5i@XmAEYB zaCOt)6+`!zXqn@6w}PZZQPS`JEH!7ORSC>fAonx4Lo&_)P*3Y5gw(>2fWop^=5KHT zyqx}r(2EH5g=p*lXETFm2hmbJJjYK> zJgnI#C?YyUlrI2f)d$i_B|Hfx<*lcRHNyp7h55!Sbhcc-Q4a9KAlmmm&`+8*x1{go zAX`p_oU4Vb$k?x0@V7ud>Ec?t^*FA&>o+y! z=kF)!LzC6?N=%~T(*8OSiFpC#0;A1Y3tt*NXcYL|fhCzY;Lg)+EoXel$~ex+W4``N zP>1bah5~``>kBFJ-cTdG+=BW(bCn@ZYW{LLoiSjk{x7$g1VEvKiU`jCWC4L=6kMmF z*mHD*p!ezKKjq0SYTJag>qbMIOP|gU%Fmzs8Qp;dlnQds^#NUWeSp5LTJz4#d>KZf z-QP&mue+sj7l?NKs4xB>d>qtJ#-`yz*I59%9}Dy>!3~@3rwlgBLwRcRnbUtSs$c1% zOfw3Inf!r9m{Pbq!P*{5_Ap*-iC#{d1V^V$<|@m#+LL7ioV1rz2>Z0<#{D>YcGz zT~LcalYYpV^dBEw%Xy=8eWN+i6Y1hjbdYo#R!OvUo8$AX(*4>|f0f@U9wECk9Y1Xk z0=pFGLGwG={_6*UVziRx$-H|;2^<^k>mAJWo`i-`jJefqFFU!!NmTwZe{{I0)<^0R z(6!wVwyG8tY4sh@EQQ5Oax{3 zk7-)EA6!a-+$sL@z4`S+W|%{aqtd@^j{d>a(RBTpjzZyx^c zdarn*CqPyoKyOAB8u&CY@Jew~GXMDWZB%!$g9^J>|7qYU9(mWrYV;gUIO#P2%@y%n zoOMvELg8hb0nVj=W2+4|7(f|#;C!?}J>__^w`DXwV|bFUvg$I|QSd#f-CHZzsG_9^V#3I&HY?8i19k#B4g zE9H-E>+`eGux|^zzJP>0dAp~blf9fBZh%gUgpBg*pHa|6&~5Z4j^_ldtH#%O=5{9+ z*Vh@2j}WT^W_I(8bqcBne_i0`uY7)iUZQT<4-EAs^z`%qE`1Wx)ZAQEiDYe|!s$Zr z$CaH$lZLG=EG#EFah1=1BepO0!cg^Lr~90VVs~PG2LI`|On9I3e3z673lWaf;2Ute zf8U^(%=d)_n(?~R%vx>)7C&98>F`Y_3Z1T1^c<|aXOH&RXWfNvn{;)WcUGy@OF>Wk z`w}(R7v`#+as{kyS>ZKq#}Ry`{Aq)Ovn=fFuy&UNv%fzGeuF^4JufeQYkf+ci`kWG z{CIw{%GG|Z16=rDo?#(PU?47bSLYuO2mXV7eq8TTOpa`gb47RppGyK2HFf`Z)gW1T zvtxxyqU;|xM@iLVL6+{c>neSCc(~XUXu7yGSdnlg%G#-IxdHv$*fVS*_gY(A{^twa z5=w$0)(S6n4yLb7x<#qyv2T;as3=>cily}W*Br?F`FxD-OCcg4FpwP#ukM^nvzhNQ z@qNw7?x}`%{&N9w0(EoWuA2Yz>Cy%0=f?5C(^WlxV&9&v)!shb|Ko=<`|e`#-*XDw zwvNc9p<(v^v9V&u-9bh7?2L<|)g2W$ZI0(2NkC@{<$!hGz!+f>QGok%p8kAm0_>L@ z8Q}>uJx|R|4CkNBe^fKwKCzh8l4WLA+}=GQKmXp5kg0J-#>b_xNO^K6D~pQa3=A{m zHyNp2gE@B_Z6+lC817T?q)BTFzB~Ws8&Mot;Rzhz8<)QNZ+8ug5s{JQlU@8~rnSh>-ib^ISY^AJtuoZjD4|Z`1uKm zQY)SmiCD9T=15-NaR?i`eP3gv2xxk*A(^@FBv-n7elWlwWq;V+og~Y6^X3f;KYwgQ zq}vLu2WNTyQ4HR>LDN~dblJSCGih#|PWSw6TyPIV+j5*ZUK6k`&1w!#OkZ6cKalIp z$zkvB=opaPS>pY@QlK9go*@rT`;ziGKb{0E(C}ywssy@YGt6IWG=717{*we;?t<~w z%&89S(yHets=#*+WE_Nr3No0Gn>T6(ccCd#U;h*=?k9_)f4mKpT5Jw3E-7MSjd-<9 zwPS?fCuRgb>d3v0iZtJtKSu0CN{3+XoAb&)A1)tVq^n93gGD9QapPGHqErEHX<@-O zUcIk3|FKg~8$hjRD@E)$L0i6&D@UZ(TqCo-a%3 zM1fUc_>WbvN9(@eP@V&AVBFmKeJSE#c0!AMd)YtB(X60~b@|k>%I~F>S0`LTE{>3&~O3%`SApl7=*FQ{!7}LSG6Hv7aio!_UKf}?@C)- zOy_1Zyz|GwB9|p#JG+*nlBd+^&Dr8?a3rH%S@x*gyWx|$^%_G0YqQL`so=hb7T3hd z-X8U@OC^Pxfd*kA4>mVrXsx-pFq@m1r8hJ*9LT8He~*c-__lv>C){T3+oTwL1;%*( z?a38>_Q5(`^o%O+Ut3!~K<67&Zf?1|m`?x2mC@pDvP$AW zv6CBr3ozFiP z5^}@6M_y4G=`qHRj`u-KX2rp7)sezBPm0%(|Hn4siy~&#)tS*;9eX$T{CekbL4Lyq z-?`g!v~N5sn?a@NF}z-cB4;biYpr#?ReZKOZP9okAF)`kO*{@8Rc%Ywx?UW*!o zx;n#2dj40=pD5?v+vie;i%EuaSg}ZS+v!{m(&IjWRO6Z~==! zQbbVcj}`XsfA{A_8qv{Mn$?y?eqZu`^@9NU?VhKvY5v$$|L0%y#ltoVDjxFtU*Fu> zxVZ}nBvo8cIDg>$&tG*LoBDRoEyFy#|9K4mxR}>#0c)h>JLjj!|9pkpaso-5|1wMd z7q9#ZlKUL8h~yvNe||cj+i?H>j6fW?!?;qp|M7!=4)S$mz#Uo*ah*SA{`miY|Gh$eCG?J8p + + 4.0.0 + + com.amazonaws + flink-data-generator + 1.0 + jar + + + UTF-8 + ${project.basedir}/target + ${project.name}-${project.version} + 11 + ${target.java.version} + ${target.java.version} + 1.20.0 + 5.0.0-1.20 + 3.3.0-1.20 + 1.2.0 + 2.23.1 + + + + + + + org.apache.flink + flink-streaming-java + ${flink.version} + provided + + + org.apache.flink + flink-clients + ${flink.version} + provided + + + org.apache.flink + flink-runtime-web + ${flink.version} + provided + + + org.apache.flink + flink-json + ${flink.version} + provided + + + org.apache.flink + flink-connector-base + ${flink.version} + provided + + + + org.apache.flink + flink-metrics-dropwizard + ${flink.version} + + + + + com.amazonaws + aws-kinesisanalytics-runtime + ${kda.runtime.version} + provided + + + + + org.apache.flink + flink-connector-datagen + ${flink.version} + + + + + org.apache.flink + flink-connector-aws-kinesis-streams + ${aws.connector.version} + + + + + org.apache.flink + flink-connector-kafka + ${kafka.connector.version} + + + + + + + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + + + junit + junit + 4.13.2 + test + + + + + ${buildDirectory} + ${jar.finalName} + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${target.java.version} + ${target.java.version} + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + + package + + shade + + + + + org.apache.flink:force-shading + com.google.code.findbugs:jsr305 + org.slf4j:* + log4j:* + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + com.amazonaws.services.msf.DataGeneratorJob + + + + + + + + + diff --git a/java/FlinkDataGenerator/src/main/java/com/amazonaws/services/msf/DataGeneratorJob.java b/java/FlinkDataGenerator/src/main/java/com/amazonaws/services/msf/DataGeneratorJob.java new file mode 100644 index 0000000..d41161f --- /dev/null +++ b/java/FlinkDataGenerator/src/main/java/com/amazonaws/services/msf/DataGeneratorJob.java @@ -0,0 +1,225 @@ +package com.amazonaws.services.msf; + +import com.amazonaws.services.kinesisanalytics.runtime.KinesisAnalyticsRuntime; +import com.amazonaws.services.msf.domain.StockPrice; +import com.amazonaws.services.msf.domain.StockPriceGeneratorFunction; +import org.apache.commons.lang3.StringUtils; +import org.apache.flink.api.common.eventtime.WatermarkStrategy; +import org.apache.flink.api.common.serialization.SerializationSchema; +import org.apache.flink.api.common.typeinfo.TypeInformation; +import org.apache.flink.api.connector.source.util.ratelimit.RateLimiterStrategy; +import org.apache.flink.connector.base.DeliveryGuarantee; +import org.apache.flink.connector.datagen.source.DataGeneratorSource; +import org.apache.flink.connector.datagen.source.GeneratorFunction; +import org.apache.flink.connector.kafka.sink.KafkaRecordSerializationSchema; +import org.apache.flink.connector.kafka.sink.KafkaSink; +import org.apache.flink.connector.kinesis.sink.KinesisStreamsSink; +import org.apache.flink.connector.kinesis.sink.PartitionKeyGenerator; +import org.apache.flink.formats.json.JsonSerializationSchema; +import org.apache.flink.streaming.api.datastream.DataStream; +import org.apache.flink.streaming.api.environment.LocalStreamEnvironment; +import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; +import org.apache.flink.util.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Map; +import java.util.Properties; + +/** + * A Flink application that generates random stock data using DataGeneratorSource + * and sends it to Kinesis Data Streams and/or Kafka as JSON based on configuration. + * At least one sink (KinesisSink or KafkaSink) must be configured. + * The generated data matches the schema used by the Python data generator. + */ +public class DataGeneratorJob { + private static final Logger LOG = LoggerFactory.getLogger(DataGeneratorJob.class); + + // Name of the local JSON resource with the application properties in the same format as they are received from the Amazon Managed Service for Apache Flink runtime + private static final String LOCAL_APPLICATION_PROPERTIES_RESOURCE = "flink-application-properties-dev.json"; + + // Default values for configuration + private static final int DEFAULT_RECORDS_PER_SECOND = 10; + + private static boolean isLocal(StreamExecutionEnvironment env) { + return env instanceof LocalStreamEnvironment; + } + + /** + * Load application properties from Amazon Managed Service for Apache Flink runtime or from a local resource, when the environment is local + */ + private static Map loadApplicationProperties(StreamExecutionEnvironment env) throws IOException { + if (isLocal(env)) { + LOG.info("Loading application properties from '{}'", LOCAL_APPLICATION_PROPERTIES_RESOURCE); + return KinesisAnalyticsRuntime.getApplicationProperties( + DataGeneratorJob.class.getClassLoader() + .getResource(LOCAL_APPLICATION_PROPERTIES_RESOURCE).getPath()); + } else { + LOG.info("Loading application properties from Amazon Managed Service for Apache Flink"); + return KinesisAnalyticsRuntime.getApplicationProperties(); + } + } + + /** + * Create a DataGeneratorSource with configurable rate from DataGen properties + * + * @param dataGenProperties Properties from the "DataGen" property group + * @param generatorFunction The generator function to use for data generation + * @param typeInformation Type information for the generated data type + * @param The type of data to generate + * @return Configured DataGeneratorSource + */ + private static DataGeneratorSource createDataGeneratorSource( + Properties dataGenProperties, + GeneratorFunction generatorFunction, + TypeInformation typeInformation) { + + int recordsPerSecond; + if (dataGenProperties != null) { + String recordsPerSecondStr = dataGenProperties.getProperty("records.per.second"); + if (recordsPerSecondStr != null && !recordsPerSecondStr.trim().isEmpty()) { + try { + recordsPerSecond = Integer.parseInt(recordsPerSecondStr.trim()); + } catch (NumberFormatException e) { + LOG.error("Invalid records.per.second value: '{}'. Must be a valid integer. ", recordsPerSecondStr); + throw e; + } + } else { + LOG.info("No records.per.second configured. Using default: {}", DEFAULT_RECORDS_PER_SECOND); + recordsPerSecond = DEFAULT_RECORDS_PER_SECOND; + } + } else { + LOG.info("No DataGen properties found. Using default records per second: {}", DEFAULT_RECORDS_PER_SECOND); + recordsPerSecond = DEFAULT_RECORDS_PER_SECOND; + } + + Preconditions.checkArgument(recordsPerSecond > 0, + "Invalid records.per.second value. Must be positive."); + + + return new DataGeneratorSource( + generatorFunction, + Long.MAX_VALUE, // Generate (practically) unlimited records + RateLimiterStrategy.perSecond(recordsPerSecond), // Configurable rate + typeInformation // Explicit type information + ); + } + + /** + * Create a Kinesis Sink + * + * @param outputProperties Properties from the "KinesisSink" property group + * @param serializationSchema Serialization schema + * @param partitionKeyGenerator Partition key generator + * @param The type of data to sink + * @return an instance of KinesisStreamsSink + */ + private static KinesisStreamsSink createKinesisSink(Properties outputProperties, final SerializationSchema serializationSchema, final PartitionKeyGenerator partitionKeyGenerator + ) { + final String outputStreamArn = outputProperties.getProperty("stream.arn"); + return KinesisStreamsSink.builder() + .setStreamArn(outputStreamArn) + .setKinesisClientProperties(outputProperties) + .setSerializationSchema(serializationSchema) + .setPartitionKeyGenerator(partitionKeyGenerator) + .build(); + } + + /** + * Create a KafkaSink + * + * @param kafkaProperties Properties from the "KafkaSink" property group + * @param recordSerializationSchema Record serialization schema + * @param The type of data to sink + * @return an instance of KafkaSink + */ + private static KafkaSink createKafkaSink(Properties kafkaProperties, KafkaRecordSerializationSchema recordSerializationSchema) { + return KafkaSink.builder() + .setBootstrapServers(kafkaProperties.getProperty("bootstrap.servers")) + .setKafkaProducerConfig(kafkaProperties) + .setRecordSerializer(recordSerializationSchema) + .setDeliveryGuarantee(DeliveryGuarantee.AT_LEAST_ONCE) + .build(); + } + + public static void main(String[] args) throws Exception { + // Set up the streaming execution environment + final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); + + // Allows Flink to reuse objects across forwarded operators, as opposed to do a deep copy + // (this is safe because record objects are never mutated or passed by reference) + env.getConfig().enableObjectReuse(); + + LOG.info("Starting Flink Data Generator Job with conditional sinks"); + + // Load application properties + final Map applicationProperties = loadApplicationProperties(env); + LOG.info("Application properties: {}", applicationProperties); + + // Create a DataGeneratorSource that generates Stock objects using the generic method + DataGeneratorSource source = createDataGeneratorSource( + applicationProperties.get("DataGen"), + new StockPriceGeneratorFunction(), + TypeInformation.of(StockPrice.class) + ); + + // Create the data stream from the source + DataStream stockPricesStream = env.fromSource( + source, + WatermarkStrategy.noWatermarks(), + "Data Generator" + ).uid("data generator"); + + // Add a passthrough operator exposing basic metrics + var outputStream = stockPricesStream.map(new MetricEmitterNoOpMap<>()).uid("metric-emitter"); + + + // Check if at least one sink is configured + Properties kinesisProperties = applicationProperties.get("KinesisSink"); + Properties kafkaProperties = applicationProperties.get("KafkaSink"); + boolean hasKinesisSink = kinesisProperties != null; + boolean hasKafkaSink = kafkaProperties != null; + + if (!hasKinesisSink && !hasKafkaSink) { + throw new IllegalArgumentException( + "At least one sink must be configured. Please provide either 'KinesisSink' or 'KafkaSink' configuration group."); + } + + // Create Kinesis sink with JSON serialization (only if configured) + if (hasKinesisSink) { + PartitionKeyGenerator partitionKeyGenerator = (record) -> String.valueOf(record.getTicker()); + KinesisStreamsSink kinesisSink = createKinesisSink( + kinesisProperties, + // Serialize the Kinesis record as JSON + new JsonSerializationSchema<>(), + // Shard by `ticker` + partitionKeyGenerator + ); + outputStream.sinkTo(kinesisSink).uid("kinesis-sink").disableChaining(); + LOG.info("Kinesis sink configured"); + } + + // Create Kafka sink with JSON serialization (only if configured) + if (hasKafkaSink) { + String kafkaTopic = Preconditions.checkNotNull(StringUtils.trimToNull(kafkaProperties.getProperty("topic")), "Kafka topic not defined"); + SerializationSchema valueSerializationSchema = new JsonSerializationSchema<>(); + SerializationSchema keySerializationSchema = (stockPrice) -> stockPrice.getTicker().getBytes(); + KafkaRecordSerializationSchema kafkaRecordSerializationSchema = + KafkaRecordSerializationSchema.builder() + .setTopic(kafkaTopic) + // Serialize the Kafka record value (payload) as JSON + .setValueSerializationSchema(valueSerializationSchema) + // Partition by `ticker` + .setKeySerializationSchema(keySerializationSchema) + .build(); + + KafkaSink kafkaSink = createKafkaSink(kafkaProperties, kafkaRecordSerializationSchema); + outputStream.sinkTo(kafkaSink).uid("kafka-sink").disableChaining(); + LOG.info("Kafka sink configured"); + } + + // Execute the job + env.execute("Flink Data Generator Job"); + } +} diff --git a/java/FlinkDataGenerator/src/main/java/com/amazonaws/services/msf/MetricEmitterNoOpMap.java b/java/FlinkDataGenerator/src/main/java/com/amazonaws/services/msf/MetricEmitterNoOpMap.java new file mode 100644 index 0000000..c441b3b --- /dev/null +++ b/java/FlinkDataGenerator/src/main/java/com/amazonaws/services/msf/MetricEmitterNoOpMap.java @@ -0,0 +1,52 @@ +package com.amazonaws.services.msf; + +import org.apache.flink.api.common.functions.OpenContext; +import org.apache.flink.api.common.functions.RichMapFunction; +import org.apache.flink.dropwizard.metrics.DropwizardMeterWrapper; +import org.apache.flink.metrics.Counter; +import org.apache.flink.metrics.Gauge; +import org.apache.flink.metrics.Meter; + +/** + * No-op Map function exposing 3 custom metrics: generatedRecordCount, generatedRecordRatePerParallelism, and taskParallelism. + * Note each subtask emits its own metrics. + */ +public class MetricEmitterNoOpMap extends RichMapFunction { + private transient Counter recordCounter; + private transient int taskParallelism = 0; + private transient Meter recordMeter; + + @Override + public void open(OpenContext openContext) throws Exception { + this.recordCounter = getRuntimeContext() + .getMetricGroup() + .addGroup("kinesisAnalytics") // Automatically export metric to CloudWatch + .counter("generatedRecordCount"); + + this.recordMeter = getRuntimeContext() + .getMetricGroup() + .addGroup("kinesisAnalytics") // Automatically export metric to CloudWatch + .meter("generatedRecordRatePerParallelism", new DropwizardMeterWrapper(new com.codahale.metrics.Meter())); + + getRuntimeContext() + .getMetricGroup() + .addGroup("kinesisAnalytics") // Automatically export metric to CloudWatch + .gauge("taskParallelism", new Gauge() { + @Override + public Integer getValue() { + return taskParallelism; + } + }); + + // Capture the task parallelism + taskParallelism = getRuntimeContext().getTaskInfo().getNumberOfParallelSubtasks(); + } + + @Override + public T map(T record) throws Exception { + recordCounter.inc(); + recordMeter.markEvent(); + + return record; + } +} diff --git a/java/FlinkDataGenerator/src/main/java/com/amazonaws/services/msf/domain/StockPrice.java b/java/FlinkDataGenerator/src/main/java/com/amazonaws/services/msf/domain/StockPrice.java new file mode 100644 index 0000000..f159036 --- /dev/null +++ b/java/FlinkDataGenerator/src/main/java/com/amazonaws/services/msf/domain/StockPrice.java @@ -0,0 +1,70 @@ +package com.amazonaws.services.msf.domain; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +public class StockPrice { + // This annotation as well as the associated jackson2 import is needed to correctly map the JSON input key to the + // appropriate POJO property name to ensure event_time isn't missed in serialization and deserialization + @JsonProperty("event_time") + private String eventTime; + private String ticker; + private float price; + + public StockPrice() {} + + public StockPrice(String eventTime, String ticker, float price) { + this.eventTime = eventTime; + this.ticker = ticker; + this.price = price; + } + + public String getEventTime() { + return eventTime; + } + + public void setEventTime(String eventTime) { + this.eventTime = eventTime; + } + + public String getTicker() { + return ticker; + } + + public void setTicker(String ticker) { + this.ticker = ticker; + } + + public float getPrice() { + return price; + } + + public void setPrice(float price) { + this.price = price; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StockPrice stock = (StockPrice) o; + return Float.compare(stock.price, price) == 0 && + Objects.equals(eventTime, stock.eventTime) && + Objects.equals(ticker, stock.ticker); + } + + @Override + public int hashCode() { + return Objects.hash(eventTime, ticker, price); + } + + @Override + public String toString() { + return "Stock{" + + "event_time='" + eventTime + '\'' + + ", ticker='" + ticker + '\'' + + ", price=" + price + + '}'; + } +} diff --git a/java/FlinkDataGenerator/src/main/java/com/amazonaws/services/msf/domain/StockPriceGeneratorFunction.java b/java/FlinkDataGenerator/src/main/java/com/amazonaws/services/msf/domain/StockPriceGeneratorFunction.java new file mode 100644 index 0000000..cb32db2 --- /dev/null +++ b/java/FlinkDataGenerator/src/main/java/com/amazonaws/services/msf/domain/StockPriceGeneratorFunction.java @@ -0,0 +1,59 @@ +package com.amazonaws.services.msf.domain; + +import org.apache.flink.connector.datagen.source.GeneratorFunction; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Random; + +/** + * Generator function that creates random Stock objects. + * Implements GeneratorFunction to work with DataGeneratorSource. + * + * Modify this class to generate a different record type + */ +public class StockPriceGeneratorFunction implements GeneratorFunction { + // Stock tickers to randomly choose from (same as Python data generator) + private static final String[] TICKERS = { + "AAPL", + "MSFT", + "AMZN", + "GOOGL", + "META", + "NVDA", + "TSLA", + "INTC", + "ADBE", + "NFLX", + "PYPL", + "CSCO", + "PEP", + "AVGO", + "AMD", + "COST", + "QCOM", + "AMGN", + "SBUX", + "BKNG" + }; + + // Random number generator + private static final Random RANDOM = new Random(); + + // Date formatter for ISO format timestamps + private static final DateTimeFormatter ISO_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + + @Override + public StockPrice map(Long value) throws Exception { + // Generate current timestamp in ISO format + String eventTime = LocalDateTime.now().format(ISO_FORMATTER); + + // Randomly select a ticker + String ticker = TICKERS[RANDOM.nextInt(TICKERS.length)]; + + // Generate random price between 0 and 100, rounded to 2 decimal places + float price = Math.round(RANDOM.nextFloat() * 100 * 100.0f) / 100.0f; + + return new StockPrice(eventTime, ticker, price); + } +} diff --git a/java/FlinkDataGenerator/src/main/resources/flink-application-properties-dev.json b/java/FlinkDataGenerator/src/main/resources/flink-application-properties-dev.json new file mode 100644 index 0000000..03a4ae1 --- /dev/null +++ b/java/FlinkDataGenerator/src/main/resources/flink-application-properties-dev.json @@ -0,0 +1,22 @@ +[ + { + "PropertyGroupId": "DataGen", + "PropertyMap": { + "records.per.second": "1000" + } + }, + { + "PropertyGroupId": "KinesisSink", + "PropertyMap": { + "stream.arn": "arn:aws:kinesis:eu-west-1::stream/FlinkDataGeneratorTestStream", + "aws.region": "eu-west-1" + } + }, + { + "PropertyGroupId": "KafkaSink-DISABLE", + "PropertyMap": { + "bootstrap.servers": "localhost:9092", + "topic": "stock-prices" + } + } +] diff --git a/java/FlinkDataGenerator/src/main/resources/log4j2.properties b/java/FlinkDataGenerator/src/main/resources/log4j2.properties new file mode 100644 index 0000000..9503413 --- /dev/null +++ b/java/FlinkDataGenerator/src/main/resources/log4j2.properties @@ -0,0 +1,16 @@ +# Set to debug or trace if log4j initialization is failing +status = warn + +# Name of the configuration +name = ConsoleLogConfigDemo + +# Console appender configuration +appender.console.type = Console +appender.console.name = consoleLogger +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n + +# Root logger level +rootLogger.level = info +# Root logger referring to console appender +rootLogger.appenderRef.stdout.ref = consoleLogger diff --git a/java/FlinkDataGenerator/src/test/java/com/amazonaws/services/msf/DataGeneratorJobTest.java b/java/FlinkDataGenerator/src/test/java/com/amazonaws/services/msf/DataGeneratorJobTest.java new file mode 100644 index 0000000..61cd802 --- /dev/null +++ b/java/FlinkDataGenerator/src/test/java/com/amazonaws/services/msf/DataGeneratorJobTest.java @@ -0,0 +1,150 @@ +package com.amazonaws.services.msf; + +import com.amazonaws.services.msf.domain.StockPrice; +import com.amazonaws.services.msf.domain.StockPriceGeneratorFunction; +import org.apache.flink.api.common.typeinfo.TypeInformation; +import org.apache.flink.connector.datagen.source.DataGeneratorSource; +import org.apache.flink.connector.datagen.source.GeneratorFunction; +import org.apache.flink.connector.kafka.sink.KafkaRecordSerializationSchema; +import org.apache.flink.connector.kafka.sink.KafkaSink; +import org.junit.Test; +import static org.junit.Assert.*; +import java.util.Properties; +import java.util.HashMap; +import java.util.Map; +import java.lang.reflect.Method; + +public class DataGeneratorJobTest { + + @Test + public void testCreateDataGeneratorSource() throws Exception { + // Use reflection to test the private createDataGeneratorSource method + Method createDataGeneratorSourceMethod = DataGeneratorJob.class.getDeclaredMethod( + "createDataGeneratorSource", Properties.class, GeneratorFunction.class, TypeInformation.class); + createDataGeneratorSourceMethod.setAccessible(true); + + // Test with valid configuration + Properties dataGenProps = new Properties(); + dataGenProps.setProperty("records.per.second", "15"); + + StockPriceGeneratorFunction generatorFunction = new StockPriceGeneratorFunction(); + TypeInformation typeInfo = TypeInformation.of(StockPrice.class); + + DataGeneratorSource source = (DataGeneratorSource) createDataGeneratorSourceMethod.invoke( + null, dataGenProps, generatorFunction, typeInfo); + + assertNotNull("DataGeneratorSource should not be null", source); + + // Test with null properties (should use default rate) + source = (DataGeneratorSource) createDataGeneratorSourceMethod.invoke( + null, null, generatorFunction, typeInfo); + + assertNotNull("DataGeneratorSource should not be null with null properties", source); + + // Test with empty properties (should use default rate) + Properties emptyProps = new Properties(); + source = (DataGeneratorSource) createDataGeneratorSourceMethod.invoke( + null, emptyProps, generatorFunction, typeInfo); + + assertNotNull("DataGeneratorSource should not be null with empty properties", source); + } + + @Test + public void testCreateKafkaSink() throws Exception { + // Use reflection to test the private createKafkaSink method + Method createKafkaSinkMethod = DataGeneratorJob.class.getDeclaredMethod( + "createKafkaSink", Properties.class, KafkaRecordSerializationSchema.class); + createKafkaSinkMethod.setAccessible(true); + + // Test with valid Kafka properties + Properties kafkaProps = new Properties(); + kafkaProps.setProperty("bootstrap.servers", "localhost:9092"); + kafkaProps.setProperty("topic", "test-topic"); + + // Create a mock KafkaRecordSerializationSchema + KafkaRecordSerializationSchema recordSerializationSchema = + KafkaRecordSerializationSchema.builder() + .setTopic("test-topic") + .setKeySerializationSchema(stock -> stock.getTicker().getBytes()) + .setValueSerializationSchema(new org.apache.flink.formats.json.JsonSerializationSchema<>()) + .build(); + + KafkaSink kafkaSink = (KafkaSink) createKafkaSinkMethod.invoke( + null, kafkaProps, recordSerializationSchema); + + assertNotNull("KafkaSink should not be null", kafkaSink); + } + + @Test + public void testKafkaPartitioningKey() { + // Test that ticker symbol can be used as Kafka partition key + StockPrice stock1 = new StockPrice("2024-01-15T10:30:45", "AAPL", 150.25f); + StockPrice stock2 = new StockPrice("2024-01-15T10:30:46", "MSFT", 200.50f); + + // Test that ticker can be converted to bytes for Kafka key + byte[] key1 = stock1.getTicker().getBytes(); + byte[] key2 = stock2.getTicker().getBytes(); + + assertNotNull("Kafka key should not be null", key1); + assertNotNull("Kafka key should not be null", key2); + assertTrue("Kafka key should not be empty", key1.length > 0); + assertTrue("Kafka key should not be empty", key2.length > 0); + + // Test that different tickers produce different keys + assertFalse("Different tickers should produce different keys", + java.util.Arrays.equals(key1, key2)); + + // Test that same ticker produces same key + StockPrice stock3 = new StockPrice("2024-01-15T10:30:47", "AAPL", 175.50f); + byte[] key3 = stock3.getTicker().getBytes(); + assertTrue("Same ticker should produce same key", + java.util.Arrays.equals(key1, key3)); + } + + @Test + public void testConditionalSinkValidation() { + // Test that the application validates sink configuration properly + Map appProperties = new HashMap<>(); + + // Test with no sinks configured - should be invalid + boolean hasKinesis = appProperties.get("KinesisSink") != null; + boolean hasKafka = appProperties.get("KafkaSink") != null; + assertFalse("Should not have Kinesis sink when not configured", hasKinesis); + assertFalse("Should not have Kafka sink when not configured", hasKafka); + assertTrue("Should require at least one sink", !hasKinesis && !hasKafka); + + // Test with only Kinesis configured - should be valid + Properties kinesisProps = new Properties(); + kinesisProps.setProperty("stream.arn", "test-arn"); + kinesisProps.setProperty("aws.region", "us-east-1"); + appProperties.put("KinesisSink", kinesisProps); + + hasKinesis = appProperties.get("KinesisSink") != null; + hasKafka = appProperties.get("KafkaSink") != null; + assertTrue("Should have Kinesis sink when configured", hasKinesis); + assertFalse("Should not have Kafka sink when not configured", hasKafka); + assertTrue("Should be valid with one sink", hasKinesis || hasKafka); + + // Test with only Kafka configured - should be valid + appProperties.clear(); + Properties kafkaProps = new Properties(); + kafkaProps.setProperty("bootstrap.servers", "localhost:9092"); + kafkaProps.setProperty("topic", "test-topic"); + appProperties.put("KafkaSink", kafkaProps); + + hasKinesis = appProperties.get("KinesisSink") != null; + hasKafka = appProperties.get("KafkaSink") != null; + assertFalse("Should not have Kinesis sink when not configured", hasKinesis); + assertTrue("Should have Kafka sink when configured", hasKafka); + assertTrue("Should be valid with one sink", hasKinesis || hasKafka); + + // Test with both configured - should be valid + appProperties.put("KinesisSink", kinesisProps); + + hasKinesis = appProperties.get("KinesisSink") != null; + hasKafka = appProperties.get("KafkaSink") != null; + assertTrue("Should have Kinesis sink when configured", hasKinesis); + assertTrue("Should have Kafka sink when configured", hasKafka); + assertTrue("Should be valid with both sinks", hasKinesis || hasKafka); + } +} diff --git a/java/FlinkDataGenerator/src/test/java/com/amazonaws/services/msf/domain/StockPriceGeneratorFunctionTest.java b/java/FlinkDataGenerator/src/test/java/com/amazonaws/services/msf/domain/StockPriceGeneratorFunctionTest.java new file mode 100644 index 0000000..a1dc83c --- /dev/null +++ b/java/FlinkDataGenerator/src/test/java/com/amazonaws/services/msf/domain/StockPriceGeneratorFunctionTest.java @@ -0,0 +1,77 @@ +package com.amazonaws.services.msf.domain; + +import org.junit.Test; +import static org.junit.Assert.*; +import java.util.Arrays; +import java.util.List; + +public class StockPriceGeneratorFunctionTest { + private static final String[] TICKERS = { + "AAPL", + "MSFT", + "AMZN", + "GOOGL", + "META", + "NVDA", + "TSLA", + "INTC", + "ADBE", + "NFLX", + "PYPL", + "CSCO", + "PEP", + "AVGO", + "AMD", + "COST", + "QCOM", + "AMGN", + "SBUX", + "BKNG" + }; + + @Test + public void testStockGeneratorFunction() throws Exception { + StockPriceGeneratorFunction generator = new StockPriceGeneratorFunction(); + + // Generate a stock record + StockPrice stock = generator.map(1L); + + // Verify the stock is not null + assertNotNull(stock); + + // Verify event_time is not null and not empty + assertNotNull(stock.getEventTime()); + assertFalse(stock.getEventTime().isEmpty()); + + // Verify ticker is one of the expected values + List expectedTickers = Arrays.asList(TICKERS); + assertTrue("Ticker should be one of the expected values", + expectedTickers.contains(stock.getTicker())); + + // Verify price is within expected range (0 to 100) + assertTrue("Price should be >= 0", stock.getPrice() >= 0); + assertTrue("Price should be <= 100", stock.getPrice() <= 100); + + // Verify price has at most 2 decimal places + String priceStr = String.valueOf(stock.getPrice()); + int decimalIndex = priceStr.indexOf('.'); + if (decimalIndex != -1) { + int decimalPlaces = priceStr.length() - decimalIndex - 1; + assertTrue("Price should have at most 2 decimal places", decimalPlaces <= 2); + } + } + + @Test + public void testMultipleGenerations() throws Exception { + StockPriceGeneratorFunction generator = new StockPriceGeneratorFunction(); + + // Generate multiple records to ensure randomness + for (int i = 0; i < 10; i++) { + StockPrice stock = generator.map((long) i); + assertNotNull(stock); + assertNotNull(stock.getEventTime()); + assertNotNull(stock.getTicker()); + assertTrue(stock.getPrice() >= 0 && stock.getPrice() <= 100); + } + } +} diff --git a/java/FlinkDataGenerator/src/test/java/com/amazonaws/services/msf/domain/StockPriceTest.java b/java/FlinkDataGenerator/src/test/java/com/amazonaws/services/msf/domain/StockPriceTest.java new file mode 100644 index 0000000..32f6c13 --- /dev/null +++ b/java/FlinkDataGenerator/src/test/java/com/amazonaws/services/msf/domain/StockPriceTest.java @@ -0,0 +1,64 @@ +package com.amazonaws.services.msf.domain; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class StockPriceTest { + + @Test + public void testStockCreation() { + StockPrice stock = new StockPrice("2024-01-15T10:30:45", "AAPL", 150.25f); + + assertEquals("2024-01-15T10:30:45", stock.getEventTime()); + assertEquals("AAPL", stock.getTicker()); + assertEquals(150.25f, stock.getPrice(), 0.001); + } + + @Test + public void testStockToString() { + StockPrice stock = new StockPrice("2024-01-15T10:30:45", "AAPL", 150.25f); + String expected = "Stock{event_time='2024-01-15T10:30:45', ticker='AAPL', price=150.25}"; + assertEquals(expected, stock.toString()); + } + + @Test + public void testStockSetters() { + StockPrice stock = new StockPrice(); + stock.setEventTime("2024-01-15T10:30:45"); + stock.setTicker("MSFT"); + stock.setPrice(200.50f); + + assertEquals("2024-01-15T10:30:45", stock.getEventTime()); + assertEquals("MSFT", stock.getTicker()); + assertEquals(200.50f, stock.getPrice(), 0.001); + } + + @Test + public void testStockHashCodeForPartitioning() { + // Create test stock objects + StockPrice stock1 = new StockPrice("2024-01-15T10:30:45", "AAPL", 150.25f); + StockPrice stock2 = new StockPrice("2024-01-15T10:30:46", "MSFT", 200.50f); + StockPrice stock3 = new StockPrice("2024-01-15T10:30:45", "AAPL", 150.25f); // Same as stock1 + + // Test that hashCode is consistent for equal objects + assertEquals("Equal stock objects should have same hashCode", + stock1.hashCode(), stock3.hashCode()); + + // Test that equals works correctly + assertEquals("Same stock objects should be equal", stock1, stock3); + assertNotEquals("Different stock objects should not be equal", stock1, stock2); + + // Test that different stocks likely have different hashCodes + assertNotEquals("Different stock objects should likely have different hashCodes", + stock1.hashCode(), stock2.hashCode()); + + // Test that hashCode can be used as partition key (should not throw exception) + String partitionKey1 = String.valueOf(stock1.hashCode()); + String partitionKey2 = String.valueOf(stock2.hashCode()); + + assertNotNull("Partition key should not be null", partitionKey1); + assertNotNull("Partition key should not be null", partitionKey2); + assertFalse("Partition key should not be empty", partitionKey1.isEmpty()); + assertFalse("Partition key should not be empty", partitionKey2.isEmpty()); + } +} diff --git a/java/FlinkDataGenerator/src/test/resources/flink-application-properties-kafka-only.json b/java/FlinkDataGenerator/src/test/resources/flink-application-properties-kafka-only.json new file mode 100644 index 0000000..477b711 --- /dev/null +++ b/java/FlinkDataGenerator/src/test/resources/flink-application-properties-kafka-only.json @@ -0,0 +1,15 @@ +[ + { + "PropertyGroupId": "DataGen", + "PropertyMap": { + "records.per.second": "5" + } + }, + { + "PropertyGroupId": "KafkaSink", + "PropertyMap": { + "bootstrap.servers": "localhost:9092", + "topic": "test-topic" + } + } +] diff --git a/java/FlinkDataGenerator/src/test/resources/flink-application-properties-kinesis-only.json b/java/FlinkDataGenerator/src/test/resources/flink-application-properties-kinesis-only.json new file mode 100644 index 0000000..7cea8f4 --- /dev/null +++ b/java/FlinkDataGenerator/src/test/resources/flink-application-properties-kinesis-only.json @@ -0,0 +1,15 @@ +[ + { + "PropertyGroupId": "DataGen", + "PropertyMap": { + "records.per.second": "5" + } + }, + { + "PropertyGroupId": "KinesisSink", + "PropertyMap": { + "stream.arn": "arn:aws:kinesis:us-east-1:123456789012:stream/test-stream", + "aws.region": "us-east-1" + } + } +] diff --git a/java/FlinkDataGenerator/src/test/resources/flink-application-properties-no-sinks.json b/java/FlinkDataGenerator/src/test/resources/flink-application-properties-no-sinks.json new file mode 100644 index 0000000..d44e034 --- /dev/null +++ b/java/FlinkDataGenerator/src/test/resources/flink-application-properties-no-sinks.json @@ -0,0 +1,8 @@ +[ + { + "PropertyGroupId": "DataGen", + "PropertyMap": { + "records.per.second": "5" + } + } +] diff --git a/java/FlinkDataGenerator/tools/dashboard-cfn.yaml b/java/FlinkDataGenerator/tools/dashboard-cfn.yaml new file mode 100644 index 0000000..8097940 --- /dev/null +++ b/java/FlinkDataGenerator/tools/dashboard-cfn.yaml @@ -0,0 +1,578 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: 'CloudWatch Dashboard for Flink Data Generator with conditional Kinesis and Kafka widgets' + +Metadata: + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: "Dashboard Configuration" + Parameters: + - DashboardName + - Application + - Region + - Label: + default: "Kinesis Configuration" + Parameters: + - StreamName + - Label: + default: "MSK Configuration" + Parameters: + - ClusterName + - Topic + ParameterLabels: + DashboardName: + default: "Dashboard Name" + Application: + default: "Application Name" + Region: + default: "AWS Region" + StreamName: + default: "Kinesis Stream Name" + ClusterName: + default: "MSK Cluster Name" + Topic: + default: "Kafka Topic Name" + +Parameters: + DashboardName: + Type: String + Default: FlinkDataGenerator + Description: Name of the CloudWatch Dashboard + + Application: + Type: String + Default: FlinkDataGenerator + Description: Name of the Flink application + + Region: + Type: String + Default: eu-west-1 + Description: AWS region where resources are deployed + AllowedValues: + - us-east-1 + - us-east-2 + - us-west-1 + - us-west-2 + - eu-west-1 + - eu-west-2 + - eu-west-3 + - eu-central-1 + - ap-northeast-1 + - ap-northeast-2 + - ap-southeast-1 + - ap-southeast-2 + - ap-south-1 + - sa-east-1 + + StreamName: + Type: String + Default: '' + Description: Name of the Kinesis Data Stream (leave empty to skip Kinesis widget) + + ClusterName: + Type: String + Default: '' + Description: Name of the MSK (Kafka) cluster (leave empty to skip Kafka widget) + + Topic: + Type: String + Default: '' + Description: Name of the Kafka topic (leave empty to skip Kafka widget) + +Conditions: + HasKafkaConfig: !And + - !Not [!Equals [!Ref ClusterName, '']] + - !Not [!Equals [!Ref Topic, '']] + + HasKinesisConfig: !Not [!Equals [!Ref StreamName, '']] + + HasBothSinks: !And + - !Condition HasKafkaConfig + - !Condition HasKinesisConfig + + HasOnlyKafka: !And + - !Condition HasKafkaConfig + - !Not [!Condition HasKinesisConfig] + + HasOnlyKinesis: !And + - !Condition HasKinesisConfig + - !Not [!Condition HasKafkaConfig] + +Resources: + # Dashboard with both Kafka and Kinesis widgets + DashboardWithBothSinks: + Type: AWS::CloudWatch::Dashboard + Condition: HasBothSinks + Properties: + DashboardName: !Ref DashboardName + DashboardBody: !Sub | + { + "widgets": [ + { + "type": "metric", + "x": 0, + "y": 0, + "width": 8, + "height": 6, + "properties": { + "metrics": [ + [{"expression": "m1 / 4", "label": "globalGeneratedRecordCount", "id": "e1", "region": "${Region}"}], + [{"expression": "m2 * m3", "label": "globalGeneratedRecordsPerSec", "id": "e2", "yAxis": "right", "region": "${Region}"}], + ["AWS/KinesisAnalytics", "generatedRecordCount", "Application", "${Application}", {"stat": "Sum", "id": "m1", "visible": false, "region": "${Region}"}], + [".", "generatedRecordRatePerParallelism", ".", ".", {"id": "m2", "visible": false, "region": "${Region}"}], + [".", "taskParallelism", ".", ".", {"id": "m3", "visible": false, "region": "${Region}"}] + ], + "view": "timeSeries", + "stacked": false, + "region": "${Region}", + "stat": "Average", + "period": 60, + "title": "Data Generator", + "yAxis": { + "left": {"label": "Record count", "showUnits": false}, + "right": {"label": "Record/sec", "showUnits": false} + } + } + }, + { + "type": "metric", + "x": 8, + "y": 0, + "width": 8, + "height": 6, + "properties": { + "metrics": [ + [{"expression": "m1+m2+m3+m4+m5+m6", "label": "Total bytesInPerSec", "id": "e1", "yAxis": "right", "region": "${Region}"}], + ["AWS/Kafka", "BytesInPerSec", "Cluster Name", "${ClusterName}", "Broker ID", "1", "Topic", "${Topic}", {"yAxis": "right", "region": "${Region}", "id": "m1", "visible": false}], + ["...", "2", ".", ".", {"yAxis": "right", "region": "${Region}", "id": "m2", "visible": false}], + ["...", "3", ".", ".", {"yAxis": "right", "region": "${Region}", "id": "m3", "visible": false}], + ["...", "4", ".", ".", {"yAxis": "right", "region": "${Region}", "id": "m4", "visible": false}], + ["...", "5", ".", ".", {"yAxis": "right", "region": "${Region}", "id": "m5", "visible": false}], + ["...", "6", ".", ".", {"yAxis": "right", "region": "${Region}", "id": "m6", "visible": false}], + [{"expression": "m7+m8+m9+m10+m11+m12", "label": "Total MessagesInPerSec", "id": "e2", "region": "${Region}", "color": "#aec7e8"}], + ["AWS/Kafka", "MessagesInPerSec", "Cluster Name", "${ClusterName}", "Broker ID", "1", "Topic", "${Topic}", {"region": "${Region}", "id": "m7", "visible": false}], + ["...", "2", ".", ".", {"region": "${Region}", "id": "m8", "visible": false}], + ["...", "3", ".", ".", {"region": "${Region}", "id": "m9", "visible": false}], + ["...", "4", ".", ".", {"region": "${Region}", "id": "m10", "visible": false}], + ["...", "5", ".", ".", {"region": "${Region}", "id": "m11", "visible": false}], + ["...", "6", ".", ".", {"region": "${Region}", "id": "m12", "visible": false}] + ], + "view": "timeSeries", + "stacked": false, + "region": "${Region}", + "period": 60, + "yAxis": { + "left": {"label": "Messages/Sec", "showUnits": false, "min": 0}, + "right": {"label": "Bytes/Sec", "showUnits": false, "min": 0} + }, + "title": "Kafka output", + "stat": "Average" + } + }, + { + "type": "metric", + "x": 16, + "y": 0, + "width": 8, + "height": 6, + "properties": { + "metrics": [ + [{"expression": "m3 / 60", "label": "PublishedRecordsPerSec", "id": "e1", "yAxis": "right"}], + ["AWS/Kinesis", "PutRecords.ThrottledRecords", "StreamName", "${StreamName}", {"region": "${Region}", "id": "m1"}], + [".", "WriteProvisionedThroughputExceeded", ".", ".", {"id": "m2", "region": "${Region}", "stat": "Average"}], + [".", "IncomingRecords", ".", ".", {"id": "m3", "region": "${Region}", "yAxis": "right", "stat": "Sum", "visible": false}] + ], + "view": "timeSeries", + "stacked": false, + "region": "${Region}", + "stat": "Maximum", + "period": 60, + "yAxis": { + "left": {"label": "Throttling", "showUnits": false, "min": 0}, + "right": {"label": "Records/sec", "showUnits": false, "min": 0} + }, + "title": "Kinesis Data Stream output" + } + }, + { + "type": "metric", + "x": 0, + "y": 6, + "width": 8, + "height": 5, + "properties": { + "metrics": [ + ["AWS/KinesisAnalytics", "containerMemoryUtilization", "Application", "${Application}", {"region": "${Region}", "label": "containerMemoryUtilization", "id": "m1"}], + [".", "containerCPUUtilization", ".", ".", {"yAxis": "right", "region": "${Region}", "label": "containerCPUUtilization", "id": "m2"}] + ], + "view": "timeSeries", + "stacked": false, + "region": "${Region}", + "title": "Generator resource utilization", + "stat": "Average", + "period": 60, + "yAxis": { + "left": {"label": "Mem (%)", "showUnits": false, "min": 0, "max": 100}, + "right": {"label": "CPU (%)", "showUnits": false, "min": 0, "max": 100} + }, + "annotations": { + "horizontal": [{"label": "Threshold", "value": 90}] + } + } + }, + { + "type": "metric", + "x": 8, + "y": 6, + "width": 8, + "height": 5, + "properties": { + "view": "timeSeries", + "stacked": false, + "metrics": [ + ["AWS/KinesisAnalytics", "busyTimeMsPerSecond", "Application", "${Application}", {"region": "${Region}"}], + [".", "backPressuredTimeMsPerSecond", ".", ".", {"region": "${Region}"}], + [".", "idleTimeMsPerSecond", ".", ".", {"region": "${Region}"}] + ], + "region": "${Region}", + "title": "Generator application busy-ness", + "yAxis": { + "left": {"label": "1/1000", "showUnits": false, "min": 0, "max": 1000}, + "right": {"label": ""} + }, + "period": 300, + "liveData": true + } + }, + { + "type": "metric", + "x": 16, + "y": 6, + "width": 8, + "height": 5, + "properties": { + "metrics": [ + ["AWS/KinesisAnalytics", "uptime", "Application", "${Application}", {"region": "${Region}"}], + [".", "fullRestarts", ".", ".", {"yAxis": "right", "region": "${Region}"}] + ], + "view": "timeSeries", + "stacked": false, + "region": "${Region}", + "stat": "Maximum", + "period": 60, + "title": "Generator uptime and restart" + } + } + ] + } + # Dashboard with only Kafka widget + DashboardWithKafkaOnly: + Type: AWS::CloudWatch::Dashboard + Condition: HasOnlyKafka + Properties: + DashboardName: !Ref DashboardName + DashboardBody: !Sub | + { + "widgets": [ + { + "type": "metric", + "x": 0, + "y": 0, + "width": 12, + "height": 6, + "properties": { + "metrics": [ + [{"expression": "m1 / 4", "label": "globalGeneratedRecordCount", "id": "e1", "region": "${Region}"}], + [{"expression": "m2 * m3", "label": "globalGeneratedRecordsPerSec", "id": "e2", "yAxis": "right", "region": "${Region}"}], + ["AWS/KinesisAnalytics", "generatedRecordCount", "Application", "${Application}", {"stat": "Sum", "id": "m1", "visible": false, "region": "${Region}"}], + [".", "generatedRecordRatePerParallelism", ".", ".", {"id": "m2", "visible": false, "region": "${Region}"}], + [".", "taskParallelism", ".", ".", {"id": "m3", "visible": false, "region": "${Region}"}] + ], + "view": "timeSeries", + "stacked": false, + "region": "${Region}", + "stat": "Average", + "period": 60, + "title": "Data Generator", + "yAxis": { + "left": {"label": "Record count", "showUnits": false}, + "right": {"label": "Record/sec", "showUnits": false} + } + } + }, + { + "type": "metric", + "x": 12, + "y": 0, + "width": 12, + "height": 6, + "properties": { + "metrics": [ + [{"expression": "m1+m2+m3+m4+m5+m6", "label": "Total bytesInPerSec", "id": "e1", "yAxis": "right", "region": "${Region}"}], + ["AWS/Kafka", "BytesInPerSec", "Cluster Name", "${ClusterName}", "Broker ID", "1", "Topic", "${Topic}", {"yAxis": "right", "region": "${Region}", "id": "m1", "visible": false}], + ["...", "2", ".", ".", {"yAxis": "right", "region": "${Region}", "id": "m2", "visible": false}], + ["...", "3", ".", ".", {"yAxis": "right", "region": "${Region}", "id": "m3", "visible": false}], + ["...", "4", ".", ".", {"yAxis": "right", "region": "${Region}", "id": "m4", "visible": false}], + ["...", "5", ".", ".", {"yAxis": "right", "region": "${Region}", "id": "m5", "visible": false}], + ["...", "6", ".", ".", {"yAxis": "right", "region": "${Region}", "id": "m6", "visible": false}], + [{"expression": "m7+m8+m9+m10+m11+m12", "label": "Total MessagesInPerSec", "id": "e2", "region": "${Region}", "color": "#aec7e8"}], + ["AWS/Kafka", "MessagesInPerSec", "Cluster Name", "${ClusterName}", "Broker ID", "1", "Topic", "${Topic}", {"region": "${Region}", "id": "m7", "visible": false}], + ["...", "2", ".", ".", {"region": "${Region}", "id": "m8", "visible": false}], + ["...", "3", ".", ".", {"region": "${Region}", "id": "m9", "visible": false}], + ["...", "4", ".", ".", {"region": "${Region}", "id": "m10", "visible": false}], + ["...", "5", ".", ".", {"region": "${Region}", "id": "m11", "visible": false}], + ["...", "6", ".", ".", {"region": "${Region}", "id": "m12", "visible": false}] + ], + "view": "timeSeries", + "stacked": false, + "region": "${Region}", + "period": 60, + "yAxis": { + "left": {"label": "Messages/Sec", "showUnits": false, "min": 0}, + "right": {"label": "Bytes/Sec", "showUnits": false, "min": 0} + }, + "title": "Kafka output", + "stat": "Average" + } + }, + { + "type": "metric", + "x": 0, + "y": 6, + "width": 8, + "height": 5, + "properties": { + "metrics": [ + ["AWS/KinesisAnalytics", "containerMemoryUtilization", "Application", "${Application}", {"region": "${Region}", "label": "containerMemoryUtilization", "id": "m1"}], + [".", "containerCPUUtilization", ".", ".", {"yAxis": "right", "region": "${Region}", "label": "containerCPUUtilization", "id": "m2"}] + ], + "view": "timeSeries", + "stacked": false, + "region": "${Region}", + "title": "Generator resource utilization", + "stat": "Average", + "period": 60, + "yAxis": { + "left": {"label": "Mem (%)", "showUnits": false, "min": 0, "max": 100}, + "right": {"label": "CPU (%)", "showUnits": false, "min": 0, "max": 100} + }, + "annotations": { + "horizontal": [{"label": "Threshold", "value": 90}] + } + } + }, + { + "type": "metric", + "x": 8, + "y": 6, + "width": 8, + "height": 5, + "properties": { + "view": "timeSeries", + "stacked": false, + "metrics": [ + ["AWS/KinesisAnalytics", "busyTimeMsPerSecond", "Application", "${Application}", {"region": "${Region}"}], + [".", "backPressuredTimeMsPerSecond", ".", ".", {"region": "${Region}"}], + [".", "idleTimeMsPerSecond", ".", ".", {"region": "${Region}"}] + ], + "region": "${Region}", + "title": "Generator application busy-ness", + "yAxis": { + "left": {"label": "1/1000", "showUnits": false, "min": 0, "max": 1000}, + "right": {"label": ""} + }, + "period": 300, + "liveData": true + } + }, + { + "type": "metric", + "x": 16, + "y": 6, + "width": 8, + "height": 5, + "properties": { + "metrics": [ + ["AWS/KinesisAnalytics", "uptime", "Application", "${Application}", {"region": "${Region}"}], + [".", "fullRestarts", ".", ".", {"yAxis": "right", "region": "${Region}"}] + ], + "view": "timeSeries", + "stacked": false, + "region": "${Region}", + "stat": "Maximum", + "period": 60, + "title": "Generator uptime and restart" + } + } + ] + } + # Dashboard with only Kinesis widget + DashboardWithKinesisOnly: + Type: AWS::CloudWatch::Dashboard + Condition: HasOnlyKinesis + Properties: + DashboardName: !Ref DashboardName + DashboardBody: !Sub | + { + "widgets": [ + { + "type": "metric", + "x": 0, + "y": 0, + "width": 12, + "height": 6, + "properties": { + "metrics": [ + [{"expression": "m1 / 4", "label": "globalGeneratedRecordCount", "id": "e1", "region": "${Region}"}], + [{"expression": "m2 * m3", "label": "globalGeneratedRecordsPerSec", "id": "e2", "yAxis": "right", "region": "${Region}"}], + ["AWS/KinesisAnalytics", "generatedRecordCount", "Application", "${Application}", {"stat": "Sum", "id": "m1", "visible": false, "region": "${Region}"}], + [".", "generatedRecordRatePerParallelism", ".", ".", {"id": "m2", "visible": false, "region": "${Region}"}], + [".", "taskParallelism", ".", ".", {"id": "m3", "visible": false, "region": "${Region}"}] + ], + "view": "timeSeries", + "stacked": false, + "region": "${Region}", + "stat": "Average", + "period": 60, + "title": "Data Generator", + "yAxis": { + "left": {"label": "Record count", "showUnits": false}, + "right": {"label": "Record/sec", "showUnits": false} + } + } + }, + { + "type": "metric", + "x": 12, + "y": 0, + "width": 12, + "height": 6, + "properties": { + "metrics": [ + [{"expression": "m3 / 60", "label": "PublishedRecordsPerSec", "id": "e1", "yAxis": "right"}], + ["AWS/Kinesis", "PutRecords.ThrottledRecords", "StreamName", "${StreamName}", {"region": "${Region}", "id": "m1"}], + [".", "WriteProvisionedThroughputExceeded", ".", ".", {"id": "m2", "region": "${Region}", "stat": "Average"}], + [".", "IncomingRecords", ".", ".", {"id": "m3", "region": "${Region}", "yAxis": "right", "stat": "Sum", "visible": false}] + ], + "view": "timeSeries", + "stacked": false, + "region": "${Region}", + "stat": "Maximum", + "period": 60, + "yAxis": { + "left": {"label": "Throttling", "showUnits": false, "min": 0}, + "right": {"label": "Records/sec", "showUnits": false, "min": 0} + }, + "title": "Kinesis Data Stream output" + } + }, + { + "type": "metric", + "x": 0, + "y": 6, + "width": 8, + "height": 5, + "properties": { + "metrics": [ + ["AWS/KinesisAnalytics", "containerMemoryUtilization", "Application", "${Application}", {"region": "${Region}", "label": "containerMemoryUtilization", "id": "m1"}], + [".", "containerCPUUtilization", ".", ".", {"yAxis": "right", "region": "${Region}", "label": "containerCPUUtilization", "id": "m2"}] + ], + "view": "timeSeries", + "stacked": false, + "region": "${Region}", + "title": "Generator resource utilization", + "stat": "Average", + "period": 60, + "yAxis": { + "left": {"label": "Mem (%)", "showUnits": false, "min": 0, "max": 100}, + "right": {"label": "CPU (%)", "showUnits": false, "min": 0, "max": 100} + }, + "annotations": { + "horizontal": [{"label": "Threshold", "value": 90}] + } + } + }, + { + "type": "metric", + "x": 8, + "y": 6, + "width": 8, + "height": 5, + "properties": { + "view": "timeSeries", + "stacked": false, + "metrics": [ + ["AWS/KinesisAnalytics", "busyTimeMsPerSecond", "Application", "${Application}", {"region": "${Region}"}], + [".", "backPressuredTimeMsPerSecond", ".", ".", {"region": "${Region}"}], + [".", "idleTimeMsPerSecond", ".", ".", {"region": "${Region}"}] + ], + "region": "${Region}", + "title": "Generator application busy-ness", + "yAxis": { + "left": {"label": "1/1000", "showUnits": false, "min": 0, "max": 1000}, + "right": {"label": ""} + }, + "period": 300, + "liveData": true + } + }, + { + "type": "metric", + "x": 16, + "y": 6, + "width": 8, + "height": 5, + "properties": { + "metrics": [ + ["AWS/KinesisAnalytics", "uptime", "Application", "${Application}", {"region": "${Region}"}], + [".", "fullRestarts", ".", ".", {"yAxis": "right", "region": "${Region}"}] + ], + "view": "timeSeries", + "stacked": false, + "region": "${Region}", + "stat": "Maximum", + "period": 60, + "title": "Generator uptime and restart" + } + } + ] + } +Outputs: + DashboardName: + Description: Name of the created CloudWatch Dashboard + Value: !If + - HasBothSinks + - !Ref DashboardWithBothSinks + - !If + - HasOnlyKafka + - !Ref DashboardWithKafkaOnly + - !If + - HasOnlyKinesis + - !Ref DashboardWithKinesisOnly + - 'No dashboard created - missing required parameters' + + DashboardURL: + Description: URL to access the CloudWatch Dashboard + Value: !If + - HasBothSinks + - !Sub 'https://${Region}.console.aws.amazon.com/cloudwatch/home?region=${Region}#dashboards:name=${DashboardName}' + - !If + - HasOnlyKafka + - !Sub 'https://${Region}.console.aws.amazon.com/cloudwatch/home?region=${Region}#dashboards:name=${DashboardName}' + - !If + - HasOnlyKinesis + - !Sub 'https://${Region}.console.aws.amazon.com/cloudwatch/home?region=${Region}#dashboards:name=${DashboardName}' + - 'No dashboard URL - missing required parameters' + + ConfigurationSummary: + Description: Summary of the dashboard configuration + Value: !If + - HasBothSinks + - 'Dashboard created with both Kafka and Kinesis widgets' + - !If + - HasOnlyKafka + - 'Dashboard created with Kafka widget only' + - !If + - HasOnlyKinesis + - 'Dashboard created with Kinesis widget only' + - 'No dashboard created - ClusterName+Topic or StreamName required' diff --git a/java/Iceberg/IcebergDataStreamSink/README.md b/java/Iceberg/IcebergDataStreamSink/README.md index 7da74d0..e1b0f63 100644 --- a/java/Iceberg/IcebergDataStreamSink/README.md +++ b/java/Iceberg/IcebergDataStreamSink/README.md @@ -2,7 +2,7 @@ * Flink version: 1.20.0 * Flink API: DataStream API -* Iceberg 1.6.1 +* Iceberg 1.8.1 * Language: Java (11) * Flink connectors: [DataGen](https://nightlies.apache.org/flink/flink-docs-release-1.20/docs/connectors/datastream/datagen/) and [Iceberg](https://iceberg.apache.org/docs/latest/flink/) @@ -37,16 +37,15 @@ When running locally, the configuration is read from the Runtime parameters: -| Group ID | Key | Default | Description | -|-----------|--------------------------|-------------------|---------------------------------------------------------------------------------------------------------------------| -| `DataGen` | `records.per.sec` | `10.0` | Records per second generated. | -| `Iceberg` | `bucket.prefix` | (mandatory) | S3 bucket prefix, for example `s3://my-bucket/iceberg`. | -| `Iceberg` | `catalog.db` | `default` | Name of the Glue Data Catalog database. | -| `Iceberg` | `catalog.table` | `prices_iceberg` | Name of the Glue Data Catalog table. | -| `Iceberg` | `partition.fields` | `symbol` | Comma separated list of partition fields. | -| `Iceberg` | `sort.field` | `timestamp` | Sort field. | -| `Iceberg` | `operation` | `updsert` | Iceberg operation. One of `upsert`, `append` or `overwrite`. | -| `Iceberg` | `upsert.equality.fields` | `symbol` | Comma separated list of fields used for upsert. It must match partition fields. Required if `operation` = `upsert`. | +| Group ID | Key | Default | Description | +|-----------|--------------------------|------------------|---------------------------------------------------------------------------------------------------------------------| +| `DataGen` | `records.per.sec` | `10.0` | Records per second generated. | +| `Iceberg` | `bucket.prefix` | (mandatory) | S3 bucket prefix, for example `s3://my-bucket/iceberg`. | +| `Iceberg` | `catalog.db` | `default` | Name of the Glue Data Catalog database. | +| `Iceberg` | `catalog.table` | `prices_iceberg` | Name of the Glue Data Catalog table. | +| `Iceberg` | `partition.fields` | `symbol` | Comma separated list of partition fields. | +| `Iceberg` | `operation` | `upsert` | Iceberg operation. One of `upsert`, `append` or `overwrite`. | +| `Iceberg` | `upsert.equality.fields` | `symbol` | Comma separated list of fields used for upsert. It must match partition fields. Required if `operation` = `upsert`. | ### Checkpoints diff --git a/java/Iceberg/IcebergDataStreamSink/pom.xml b/java/Iceberg/IcebergDataStreamSink/pom.xml index 0a1b7e6..215cff5 100644 --- a/java/Iceberg/IcebergDataStreamSink/pom.xml +++ b/java/Iceberg/IcebergDataStreamSink/pom.xml @@ -18,7 +18,7 @@ 1.20.0 1.11.3 3.4.0 - 1.6.1 + 1.8.1 1.2.0 2.23.1 5.8.1 @@ -93,7 +93,7 @@ org.apache.iceberg - iceberg-flink-1.19 + iceberg-flink-1.20 ${iceberg.version} diff --git a/java/Iceberg/IcebergDataStreamSink/src/main/java/com/amazonaws/services/msf/iceberg/IcebergSinkBuilder.java b/java/Iceberg/IcebergDataStreamSink/src/main/java/com/amazonaws/services/msf/iceberg/IcebergSinkBuilder.java index 8dbbcdc..c851a76 100644 --- a/java/Iceberg/IcebergDataStreamSink/src/main/java/com/amazonaws/services/msf/iceberg/IcebergSinkBuilder.java +++ b/java/Iceberg/IcebergDataStreamSink/src/main/java/com/amazonaws/services/msf/iceberg/IcebergSinkBuilder.java @@ -35,7 +35,6 @@ public class IcebergSinkBuilder { private static final String DEFAULT_GLUE_DB = "default"; private static final String DEFAULT_ICEBERG_TABLE_NAME = "prices_iceberg"; - private static final String DEFAULT_ICEBERG_SORT_ORDER_FIELD = "accountNr"; private static final String DEFAULT_ICEBERG_PARTITION_FIELDS = "symbol"; private static final String DEFAULT_ICEBERG_OPERATION = "upsert"; private static final String DEFAULT_ICEBERG_UPSERT_FIELDS = "symbol"; @@ -45,14 +44,10 @@ public class IcebergSinkBuilder { * If Iceberg Table has not been previously created, we will create it using the Partition Fields specified in the * Properties, as well as add a Sort Field to improve query performance */ - private static void createTable(Catalog catalog, TableIdentifier outputTable, org.apache.iceberg.Schema icebergSchema, PartitionSpec partitionSpec, String sortField) { + private static void createTable(Catalog catalog, TableIdentifier outputTable, org.apache.iceberg.Schema icebergSchema, PartitionSpec partitionSpec) { // If table has been previously created, we do not do any operation or modification if (!catalog.tableExists(outputTable)) { Table icebergTable = catalog.createTable(outputTable, icebergSchema, partitionSpec); - // Modifying newly created iceberg table to have a sort field - icebergTable.replaceSortOrder() - .asc(sortField, NullOrder.NULLS_LAST) - .commit(); // The catalog.create table creates an Iceberg V1 table. If we want to perform upserts, we need to upgrade the table version to 2. TableOperations tableOperations = ((BaseTable) icebergTable).operations(); TableMetadata appendTableMetadata = tableOperations.current(); @@ -83,8 +78,6 @@ public static FlinkSink.Builder createBuilder(Properties icebergProperties, Data String partitionFields = icebergProperties.getProperty("partition.fields", DEFAULT_ICEBERG_PARTITION_FIELDS); List partitionFieldList = Arrays.asList(partitionFields.split("\\s*,\\s*")); - String sortField = icebergProperties.getProperty("sort.field", DEFAULT_ICEBERG_SORT_ORDER_FIELD); - // Iceberg you can perform Appends, Upserts and Overwrites. String icebergOperation = icebergProperties.getProperty("operation", DEFAULT_ICEBERG_OPERATION); Preconditions.checkArgument(icebergOperation.equals("append") || icebergOperation.equals("upsert") || icebergOperation.equals("overwrite"), "Invalid Iceberg Operation"); @@ -123,7 +116,7 @@ public static FlinkSink.Builder createBuilder(Properties icebergProperties, Data // Based on how many fields we want to partition, we create the Partition Spec PartitionSpec partitionSpec = getPartitionSpec(icebergSchema, partitionFieldList); // We create the Iceberg Table, using the Iceberg Catalog, Table Identifier, Schema parsed in Iceberg Schema Format and the partition spec - createTable(catalog, outputTable, icebergSchema, partitionSpec, sortField); + createTable(catalog, outputTable, icebergSchema, partitionSpec); // Once the table has been created in the job or before, we load it TableLoader tableLoader = TableLoader.fromCatalog(glueCatalogLoader, outputTable); // Get RowType Schema from Iceberg Schema diff --git a/java/Iceberg/IcebergDataStreamSink/src/main/resources/flink-application-properties-dev.json b/java/Iceberg/IcebergDataStreamSink/src/main/resources/flink-application-properties-dev.json index 2bd46f0..30a6ac0 100644 --- a/java/Iceberg/IcebergDataStreamSink/src/main/resources/flink-application-properties-dev.json +++ b/java/Iceberg/IcebergDataStreamSink/src/main/resources/flink-application-properties-dev.json @@ -12,8 +12,7 @@ "catalog.db": "default", "catalog.table": "prices_iceberg", "partition.fields": "symbol", - "sort.field": "timestamp", - "operation": "upsert", + "operation": "append", "upsert.equality.fields": "symbol" } } diff --git a/java/Iceberg/IcebergDataStreamSource/README.md b/java/Iceberg/IcebergDataStreamSource/README.md index e0d1d7b..f96711f 100644 --- a/java/Iceberg/IcebergDataStreamSource/README.md +++ b/java/Iceberg/IcebergDataStreamSource/README.md @@ -2,7 +2,7 @@ * Flink version: 1.20.0 * Flink API: DataStream API -* Iceberg 1.6.1 +* Iceberg 1.8.1 * Language: Java (11) * Flink connectors: [Iceberg](https://iceberg.apache.org/docs/latest/flink/) diff --git a/java/Iceberg/IcebergDataStreamSource/pom.xml b/java/Iceberg/IcebergDataStreamSource/pom.xml index 2b97e4d..3dab28b 100644 --- a/java/Iceberg/IcebergDataStreamSource/pom.xml +++ b/java/Iceberg/IcebergDataStreamSource/pom.xml @@ -18,7 +18,7 @@ 1.20.0 1.11.3 3.4.0 - 1.6.1 + 1.8.1 1.2.0 2.23.1 5.8.1 @@ -88,7 +88,7 @@ org.apache.iceberg - iceberg-flink-1.19 + iceberg-flink-1.20 ${iceberg.version} diff --git a/java/Iceberg/IcebergDataStreamSource/src/main/resources/flink-application-properties-dev.json b/java/Iceberg/IcebergDataStreamSource/src/main/resources/flink-application-properties-dev.json index e367d8f..e5b2dba 100644 --- a/java/Iceberg/IcebergDataStreamSource/src/main/resources/flink-application-properties-dev.json +++ b/java/Iceberg/IcebergDataStreamSource/src/main/resources/flink-application-properties-dev.json @@ -3,7 +3,7 @@ "PropertyGroupId": "Iceberg", "PropertyMap": { "bucket.prefix": "s3:///iceberg", - "catalog.db": "iceberg", + "catalog.db": "default", "catalog.table": "prices_iceberg" } } diff --git a/java/Iceberg/README.md b/java/Iceberg/README.md new file mode 100644 index 0000000..640a382 --- /dev/null +++ b/java/Iceberg/README.md @@ -0,0 +1,12 @@ +# Apache Iceberg Examples + +Examples demonstrating how to work with Apache Iceberg tables in Amazon Managed Service for Apache Flink using the DataStream API. + +## Table of Contents + +### Iceberg Sinks +- [**Iceberg DataStream Sink**](./IcebergDataStreamSink) - Writing data to Iceberg tables using AWS Glue Data Catalog +- [**S3 Table Sink**](./S3TableSink) - Writing data to Iceberg tables stored directly in S3 + +### Iceberg Sources +- [**Iceberg DataStream Source**](./IcebergDataStreamSource) - Reading data from Iceberg tables using AWS Glue Data Catalog diff --git a/java/Iceberg/S3TableSink/README.md b/java/Iceberg/S3TableSink/README.md index 522a155..07ef1e3 100644 --- a/java/Iceberg/S3TableSink/README.md +++ b/java/Iceberg/S3TableSink/README.md @@ -1,8 +1,8 @@ -# Iceberg Sink to Amazon S3 Tables using DataStream API +# Flink Iceberg Sink using DataStream API * Flink version: 1.19.0 * Flink API: DataStream API -* Iceberg 1.6.1 +* Iceberg 1.8.1 * Language: Java (11) * Flink connectors: [DataGen](https://nightlies.apache.org/flink/flink-docs-release-1.20/docs/connectors/datastream/datagen/) and [Iceberg](https://iceberg.apache.org/docs/latest/flink/) @@ -16,12 +16,12 @@ serialized with AVRO. ### Prerequisites -### Create a Table Bucket +#### Create a Table Bucket The sample application expects the S3 Table Bucket to exist and to have the ARN in the local environment: ```bash aws s3tables create-table-bucket --name flink-example { - "arn": "arn:aws:s3tables:::bucket/flink-example" + "arn": "arn:aws:s3tables:us-east-1:111122223333:bucket/flink-example" } ``` @@ -34,6 +34,13 @@ aws s3tables list-table-buckets This will show you the list of table buckets. Select the one you wish to write to and paste it into the config file in this project. +#### Create a Namespace in the Table Bucket (Database) +The sample application expects the Namespace in the Table Bucket to exist +```bash +aws s3tables create-namespace \ + --table-bucket-arn arn:aws:s3tables:us-east-1:111122223333:bucket/flink-example \ + --namespace default +``` #### IAM Permissions @@ -49,16 +56,15 @@ When running locally, the configuration is read from the Runtime parameters: -| Group ID | Key | Default | Description | -|-----------|--------------------------|-------------------|---------------------------------------------------------------------------------------------------------------------| -| `DataGen` | `records.per.sec` | `100.0` | Records per second generated. | -| `Iceberg` | `table.bucket.arn` | (mandatory) | ARN of the S3 Table bucket, e.g., `arn:aws:s3tables:::bucket/` | -| `Iceberg` | `catalog.db` | `test_from_flink` | Name of the S3 table database. | -| `Iceberg` | `catalog.table` | `test_table` | Name of the S3 table. | -| `Iceberg` | `partition.fields` | `symbol` | Comma separated list of partition fields. | -| `Iceberg` | `sort.field` | `timestamp` | Sort field. | -| `Iceberg` | `operation` | `upsert` | Iceberg operation. One of `upsert`, `append` or `overwrite`. | -| `Iceberg` | `upsert.equality.fields` | `symbol` | Comma separated list of fields used for upsert. It must match partition fields. Required if `operation` = `upsert`. | +| Group ID | Key | Default | Description | +|-----------|--------------------------|------------------|---------------------------------------------------------------------------------------------------------------------| +| `DataGen` | `records.per.sec` | `100.0` | Records per second generated. | +| `Iceberg` | `table.bucket.arn` | (mandatory) | ARN of the S3 bucket, e.g., `arn:aws:s3tables:region:account-id:bucket/bucket-name` | +| `Iceberg` | `catalog.db` | `default` | Name of the S3 table database. | +| `Iceberg` | `catalog.table` | `prices_s3table` | Name of the S3 table. | +| `Iceberg` | `partition.fields` | `symbol` | Comma separated list of partition fields. | +| `Iceberg` | `operation` | `append` | Iceberg operation. One of `upsert`, `append` or `overwrite`. | +| `Iceberg` | `upsert.equality.fields` | `symbol` | Comma separated list of fields used for upsert. It must match partition fields. Required if `operation` = `upsert`. | ### Checkpoints diff --git a/java/Iceberg/S3TableSink/src/main/java/com/amazonaws/services/msf/StreamingJob.java b/java/Iceberg/S3TableSink/src/main/java/com/amazonaws/services/msf/StreamingJob.java index d8e77c4..ff7a960 100644 --- a/java/Iceberg/S3TableSink/src/main/java/com/amazonaws/services/msf/StreamingJob.java +++ b/java/Iceberg/S3TableSink/src/main/java/com/amazonaws/services/msf/StreamingJob.java @@ -68,7 +68,7 @@ private static DataGeneratorSource createDataGenerator(Properties } public static void main(String[] args) throws Exception { - StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironmentWithWebUI(new Configuration()); + StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env); // Local dev specific settings @@ -81,11 +81,6 @@ public static void main(String[] args) throws Exception { Map applicationProperties = loadApplicationProperties(env); icebergProperties = applicationProperties.get("Iceberg"); - Catalog s3 = createCatalog(tableEnv); - s3.createDatabase(icebergProperties.getProperty("catalog.db"), - new CatalogDatabaseImpl(Map.of(), - "Sample Database"), true); - // Get AVRO Schema from the definition bundled with the application // Note that the application must "knows" the AVRO schema upfront, i.e. the schema must be either embedded // with the application or fetched at start time. @@ -109,21 +104,6 @@ public static void main(String[] args) throws Exception { env.execute("Flink S3 Table Sink"); } - private static Catalog createCatalog(StreamTableEnvironment tableEnv) { - - Configuration conf = new Configuration(); - conf.setString("warehouse", icebergProperties.getProperty("table.bucket.arn")); - conf.setString("catalog-impl", "software.amazon.s3tables.iceberg.S3TablesCatalog"); - conf.setString("type", "iceberg"); - conf.setString("io-impl", "org.apache.iceberg.aws.s3.S3FileIO"); - - final String catalogName = "s3"; - CatalogDescriptor descriptor = CatalogDescriptor.of(catalogName, conf); - tableEnv.createCatalog(catalogName, descriptor); - return tableEnv.getCatalog(catalogName).get(); - } - - private static DataStream createDataStream(StreamExecutionEnvironment env, Map applicationProperties, Schema avroSchema) { Properties dataGeneratorProperties = applicationProperties.get("DataGen"); return env.fromSource( diff --git a/java/Iceberg/S3TableSink/src/main/java/com/amazonaws/services/msf/iceberg/IcebergSinkBuilder.java b/java/Iceberg/S3TableSink/src/main/java/com/amazonaws/services/msf/iceberg/IcebergSinkBuilder.java index 91ccac0..923cab4 100644 --- a/java/Iceberg/S3TableSink/src/main/java/com/amazonaws/services/msf/iceberg/IcebergSinkBuilder.java +++ b/java/Iceberg/S3TableSink/src/main/java/com/amazonaws/services/msf/iceberg/IcebergSinkBuilder.java @@ -24,7 +24,6 @@ public class IcebergSinkBuilder { private static final String DEFAULT_S3_CATALOG_DB = "default"; private static final String DEFAULT_ICEBERG_TABLE_NAME = "prices_iceberg"; - private static final String DEFAULT_ICEBERG_SORT_ORDER_FIELD = "accountNr"; private static final String DEFAULT_ICEBERG_PARTITION_FIELDS = "symbol"; private static final String DEFAULT_ICEBERG_OPERATION = "upsert"; private static final String DEFAULT_ICEBERG_UPSERT_FIELDS = "symbol"; @@ -33,14 +32,11 @@ public class IcebergSinkBuilder { * If Iceberg Table has not been previously created, we will create it using the Partition Fields specified in the * Properties, as well as add a Sort Field to improve query performance */ - private static void createTable(Catalog catalog, TableIdentifier outputTable, org.apache.iceberg.Schema icebergSchema, PartitionSpec partitionSpec, String sortField) { + private static void createTable(Catalog catalog, TableIdentifier outputTable, org.apache.iceberg.Schema icebergSchema, PartitionSpec partitionSpec) { // If table has been previously created, we do not do any operation or modification if (!catalog.tableExists(outputTable)) { Table icebergTable = catalog.createTable(outputTable, icebergSchema, partitionSpec); - // Modifying newly created iceberg table to have a sort field - icebergTable.replaceSortOrder() - .asc(sortField, NullOrder.NULLS_LAST) - .commit(); + // The catalog.create table creates an Iceberg V1 table. If we want to perform upserts, we need to upgrade the table version to 2. TableOperations tableOperations = ((BaseTable) icebergTable).operations(); TableMetadata appendTableMetadata = tableOperations.current(); @@ -78,8 +74,6 @@ public static FlinkSink.Builder createBuilder(Properties icebergProperties, Data String partitionFields = icebergProperties.getProperty("partition.fields", DEFAULT_ICEBERG_PARTITION_FIELDS); List partitionFieldList = Arrays.asList(partitionFields.split("\\s*,\\s*")); - String sortField = icebergProperties.getProperty("sort.field", DEFAULT_ICEBERG_SORT_ORDER_FIELD); - // Iceberg you can perform Appends, Upserts and Overwrites. String icebergOperation = icebergProperties.getProperty("operation", DEFAULT_ICEBERG_OPERATION); Preconditions.checkArgument(icebergOperation.equals("append") || icebergOperation.equals("upsert") || icebergOperation.equals("overwrite"), "Invalid Iceberg Operation"); @@ -116,7 +110,7 @@ public static FlinkSink.Builder createBuilder(Properties icebergProperties, Data // Based on how many fields we want to partition, we create the Partition Spec PartitionSpec partitionSpec = getPartitionSpec(icebergSchema, partitionFieldList); // We create the Iceberg Table, using the Iceberg Catalog, Table Identifier, Schema parsed in Iceberg Schema Format and the partition spec - createTable(catalog, outputTable, icebergSchema, partitionSpec, sortField); + createTable(catalog, outputTable, icebergSchema, partitionSpec); // Once the table has been created in the job or before, we load it TableLoader tableLoader = TableLoader.fromCatalog(icebergCatalogLoader, outputTable); // Get RowType Schema from Iceberg Schema diff --git a/java/Iceberg/S3TableSink/src/main/resources/flink-application-properties-dev.json b/java/Iceberg/S3TableSink/src/main/resources/flink-application-properties-dev.json index 7146e8f..b941e5a 100644 --- a/java/Iceberg/S3TableSink/src/main/resources/flink-application-properties-dev.json +++ b/java/Iceberg/S3TableSink/src/main/resources/flink-application-properties-dev.json @@ -9,12 +9,12 @@ "PropertyGroupId": "Iceberg", "PropertyMap": { - "table.bucket.arn": "arn:aws:s3tables:::bucket/", - "catalog.db": "test_from_flink", - "catalog.table": "test_table", + "table.bucket.arn": "<>", + "catalog.db": "default", + "catalog.table": "prices_s3table", "partition.fields": "symbol", "sort.field": "timestamp", - "operation": "upsert", + "operation": "append", "upsert.equality.fields": "symbol" } } diff --git a/java/KafkaConfigProviders/README.md b/java/KafkaConfigProviders/README.md index 8e32abf..db4a2bf 100644 --- a/java/KafkaConfigProviders/README.md +++ b/java/KafkaConfigProviders/README.md @@ -1,10 +1,15 @@ -## Configuring Kafka connectors secrets at runtime, using Config Providers +# Kafka Config Providers Examples -This directory includes example that shows how to configure secrets for Kafka connector authentication -scheme at runtime, using [MSK Config Providers](https://github.com/aws-samples/msk-config-providers). +Examples demonstrating secure configuration management for Kafka connectors using MSK Config Providers in Amazon Managed Service for Apache Flink. -Using Config Providers, secrets and files (TrustStore and KeyStore) required to set up the Kafka authentication -and SSL, can be fetched at runtime and not embedded in the application JAR. +These examples show how to configure secrets and certificates for Kafka connector authentication at runtime, +without embedding sensitive information in the application JAR, leveraging [MSK Config Providers](https://github.com/aws-samples/msk-config-providers). -* [Configuring mTLS TrustStore and KeyStore using Config Providers](./Kafka-mTLS-KeystoreWithConfigProviders) -* [Configuring SASL/SCRAM (SASL_SSL) TrustStore and credentials using Config Providers](./Kafka-SASL_SSL-WithConfigProviders) +## Table of Contents + +### mTLS Authentication +- [**Kafka mTLS with DataStream API**](./Kafka-mTLS-Keystore-ConfigProviders) - Using Config Providers to fetch KeyStore and passwords for mTLS authentication with DataStream API +- [**Kafka mTLS with Table API & SQL**](./Kafka-mTLS-Keystore-Sql-ConfigProviders) - Using Config Providers to fetch KeyStore and passwords for mTLS authentication with Table API & SQL + +### SASL Authentication +- [**Kafka SASL/SCRAM**](./Kafka-SASL_SSL-ConfigProviders) - Using Config Providers to fetch SASL/SCRAM credentials from AWS Secrets Manager diff --git a/java/KinesisConnectors/src/main/resources/flink-application-properties-dev.json b/java/KinesisConnectors/src/main/resources/flink-application-properties-dev.json index 7e4281a..8916740 100644 --- a/java/KinesisConnectors/src/main/resources/flink-application-properties-dev.json +++ b/java/KinesisConnectors/src/main/resources/flink-application-properties-dev.json @@ -2,7 +2,7 @@ { "PropertyGroupId": "InputStream0", "PropertyMap": { - "stream.arn": "arn:aws:kinesis:us-east-1:012345678900:stream/ExampleInputStream", + "stream.arn": "arn:aws:kinesis:us-east-1::stream/ExampleInputStream", "source.init.position": "LATEST", "aws.region": "us-east-1" } @@ -10,7 +10,7 @@ { "PropertyGroupId": "OutputStream0", "PropertyMap": { - "stream.arn": "arn:aws:kinesis:us-east-1:012345678900:stream/ExampleOutputStream", + "stream.arn": "arn:aws:kinesis:us-east-1::stream/ExampleOutputStream", "aws.region": "us-east-1" } } diff --git a/java/KinesisSourceDeaggregation/README.md b/java/KinesisSourceDeaggregation/README.md new file mode 100644 index 0000000..14d1e08 --- /dev/null +++ b/java/KinesisSourceDeaggregation/README.md @@ -0,0 +1,31 @@ +## KinesisStreamsSource de-aggregation + +* Flink version: 1.20 +* Flink API: DataStream API +* Language: Java (11) +* Flink connectors: Kinesis Source and Sink + +This example demonstrates how to consume records published using KPL aggregation using `KinesisStreamsSource`. + +This folder contains two separate modules: +1. [kpl-producer](kpl-producer): a simple command line Java application to produce the JSON record to a Kinesis Stream, using KPL aggregation +2. [flink-app](flink-app): the Flink application demonstrating how to consume the aggregated stream, and publishing the de-aggregated records to another stream. + +Look at the instructions in the subfolders to run the KPL Producer (data generator) and the Flink application. + +### Background and motivation + +As of version `5.0.0`, `KinesisStreamsSource` does not support de-aggregation yet. + +If the connector is used to consume a stream produced with KPL aggregation, the source is not able to deserialize the records out of the box. + +This example shows how to implement de-aggregation in the deserialization schema. + +In particular, this example uses a wrapper which can be used to add de-aggregation to potentially any implementation +of `org.apache.flink.api.common.serialization.DeserializationSchema`. + +Implementation: +[KinesisDeaggregatingDeserializationSchemaWrapper.java](flink-app/src/main/java/com/amazonaws/services/msf/deaggregation/KinesisDeaggregatingDeserializationSchemaWrapper.java) + +> *IMPORTANT*: This implementation of de-aggregation is for demonstration purposes only. +> The code is not meant for production use and is not optimized in terms of performance. diff --git a/java/KinesisSourceDeaggregation/flink-app/README.md b/java/KinesisSourceDeaggregation/flink-app/README.md new file mode 100644 index 0000000..692ee95 --- /dev/null +++ b/java/KinesisSourceDeaggregation/flink-app/README.md @@ -0,0 +1,55 @@ +## Flink job consuming an aggregated stream + +This Flink job consumes a Kinesis stream with aggregated JSON records, and publish them to another stream. + +### Prerequisites + +The application expects two Kinesis Streams: +* Input stream containing the aggregated records +* Output stream where the de-aggregated records are published + +The application must have sufficient permissions to read and write the streams. + +### Runtime configuration + +The application reads the runtime configuration from the Runtime Properties, when running on Amazon Managed Service for Apache Flink, +or, when running locally, from the [`resources/flink-application-properties-dev.json`](resources/flink-application-properties-dev.json) file located in the resources folder. + +All parameters are case-sensitive. + +| Group ID | Key | Description | +|------------------|---------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `InputStream0` | `stream.arn` | ARN of the input stream. | +| `InputStream0` | `aws.region` | Region of the input stream. | +| `InputStream0` | `source.init.position` | (optional) Starting position when the application starts with no state. Default is `LATEST` | +| `InputStream0` | `source.reader.type` | (optional) Choose between standard (`POLLING`) and Enhanced Fan-Out (`EFO`) consumer. Default is `POLLING`. | +| `InputStream0` | `source.efo.consumer.name` | (optional, for EFO consumer mode only) Name of the EFO consumer. Only used if `source.reader.type=EFO`. | +| `InputStream0` | `source.efo.consumer.lifecycle` | (optional, for EFO consumer mode only) Lifecycle management mode of EFO consumer. Choose between `JOB_MANAGED` and `SELF_MANAGED`. Default is `JOB_MANAGED`. | +| `OutputStream0` | `stream.arn` | ARN of the output stream. | +| `OutputStream0` | `aws.region` | Region of the output stream. | + +Every parameter in the `InputStream0` group is passed to the Kinesis consumer, and every parameter in the `OutputStream0` is passed to the Kinesis client of the sink. + +See Flink Kinesis connector docs](https://nightlies.apache.org/flink/flink-docs-release-1.20/docs/connectors/datastream/kinesis/) for details about configuring the Kinesis connector. + +To configure the application on Managed Service for Apache Flink, set up these parameter in the *Runtime properties*. + +To configure the application for running locally, edit the [json file](resources/flink-application-properties-dev.json). + +### Running in IntelliJ + +You can run this example directly in IntelliJ, without any local Flink cluster or local Flink installation. + +See [Running examples locally](../../running-examples-locally.md) for details. + + +### Data Generator + +Use the [KPL Producer](../kpl-producer) to generate aggregates StockPrices to the Kinesis stream + + +### Data example + +``` +{'event_time': '2024-05-28T19:53:17.497201', 'ticker': 'AMZN', 'price': 42.88} +``` \ No newline at end of file diff --git a/java/KinesisSourceDeaggregation/flink-app/pom.xml b/java/KinesisSourceDeaggregation/flink-app/pom.xml new file mode 100644 index 0000000..ef4a81d --- /dev/null +++ b/java/KinesisSourceDeaggregation/flink-app/pom.xml @@ -0,0 +1,199 @@ + + + 4.0.0 + + com.amazonaws + kinesis-source-deaggregation + 1.0 + + + UTF-8 + ${project.basedir}/target + ${project.name}-${project.version} + 11 + ${target.java.version} + ${target.java.version} + 1.20.0 + 5.0.0-1.20 + 2.7.0 + 1.2.0 + 2.23.1 + + + + + + com.amazonaws + aws-java-sdk-bom + + 1.12.677 + pom + import + + + software.amazon.awssdk + bom + 2.31.28 + pom + import + + + + + + + + + org.apache.flink + flink-streaming-java + ${flink.version} + provided + + + org.apache.flink + flink-clients + ${flink.version} + provided + + + org.apache.flink + flink-runtime-web + ${flink.version} + provided + + + org.apache.flink + flink-json + ${flink.version} + provided + + + + + com.amazonaws + aws-kinesisanalytics-runtime + ${kda.runtime.version} + provided + + + + software.amazon.kinesis + amazon-kinesis-client + ${kinesis.client.version} + + + + software.amazon.awssdk + * + + + software.amazon.glue + * + + + io.reactivex.rxjava3 + rxjava + + + com.google.guava + * + + + + + + + org.apache.flink + flink-connector-base + ${flink.version} + provided + + + org.apache.flink + flink-connector-aws-kinesis-streams + ${aws.connector.version} + + + + + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + + + ${buildDirectory} + ${jar.finalName} + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${target.java.version} + ${target.java.version} + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + + package + + shade + + + + + org.apache.flink:force-shading + com.google.code.findbugs:jsr305 + org.slf4j:* + log4j:* + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + com.amazonaws.services.msf.StreamingJob + + + + + + + + + \ No newline at end of file diff --git a/java/KinesisSourceDeaggregation/flink-app/src/main/java/com/amazonaws/services/msf/StreamingJob.java b/java/KinesisSourceDeaggregation/flink-app/src/main/java/com/amazonaws/services/msf/StreamingJob.java new file mode 100644 index 0000000..be5c986 --- /dev/null +++ b/java/KinesisSourceDeaggregation/flink-app/src/main/java/com/amazonaws/services/msf/StreamingJob.java @@ -0,0 +1,99 @@ +package com.amazonaws.services.msf; + +import com.amazonaws.services.kinesisanalytics.runtime.KinesisAnalyticsRuntime; +import com.amazonaws.services.msf.deaggregation.KinesisDeaggregatingDeserializationSchemaWrapper; +import com.amazonaws.services.msf.model.StockPrice; +import org.apache.flink.api.common.eventtime.WatermarkStrategy; +import org.apache.flink.api.common.serialization.SerializationSchema; +import org.apache.flink.api.common.typeinfo.TypeInformation; +import org.apache.flink.configuration.Configuration; +import org.apache.flink.connector.kinesis.sink.KinesisStreamsSink; +import org.apache.flink.connector.kinesis.source.KinesisStreamsSource; +import org.apache.flink.connector.kinesis.source.serialization.KinesisDeserializationSchema; +import org.apache.flink.formats.json.JsonDeserializationSchema; +import org.apache.flink.formats.json.JsonSerializationSchema; +import org.apache.flink.shaded.guava31.com.google.common.collect.Maps; +import org.apache.flink.streaming.api.datastream.DataStream; +import org.apache.flink.streaming.api.environment.LocalStreamEnvironment; +import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Map; +import java.util.Properties; + + +public class StreamingJob { + private static final Logger LOG = LoggerFactory.getLogger(StreamingJob.class); + + // Name of the local JSON resource with the application properties in the same format as they are received from the Amazon Managed Service for Apache Flink runtime + private static final String LOCAL_APPLICATION_PROPERTIES_RESOURCE = "flink-application-properties-dev.json"; + + private static boolean isLocal(StreamExecutionEnvironment env) { + return env instanceof LocalStreamEnvironment; + } + + /** + * Load application properties from Amazon Managed Service for Apache Flink runtime or from a local resource, when the environment is local + */ + private static Map loadApplicationProperties(StreamExecutionEnvironment env) throws IOException { + if (isLocal(env)) { + LOG.info("Loading application properties from '{}'", LOCAL_APPLICATION_PROPERTIES_RESOURCE); + return KinesisAnalyticsRuntime.getApplicationProperties( + StreamingJob.class.getClassLoader() + .getResource(LOCAL_APPLICATION_PROPERTIES_RESOURCE).getPath()); + } else { + LOG.info("Loading application properties from Amazon Managed Service for Apache Flink"); + return KinesisAnalyticsRuntime.getApplicationProperties(); + } + } + + // Create a source using a KinesisDeserializationSchema + private static KinesisStreamsSource createKinesisSource(Properties inputProperties, final KinesisDeserializationSchema kinesisDeserializationSchema) { + final String inputStreamArn = inputProperties.getProperty("stream.arn"); + return KinesisStreamsSource.builder() + .setStreamArn(inputStreamArn) + .setSourceConfig(Configuration.fromMap(Maps.fromProperties(inputProperties))) + .setDeserializationSchema(kinesisDeserializationSchema) + .build(); + } + + // Create a sink + private static KinesisStreamsSink createKinesisSink(Properties outputProperties, final SerializationSchema serializationSchema) { + final String outputStreamArn = outputProperties.getProperty("stream.arn"); + return KinesisStreamsSink.builder() + .setStreamArn(outputStreamArn) + .setKinesisClientProperties(outputProperties) + .setSerializationSchema(serializationSchema) + .setPartitionKeyGenerator(element -> String.valueOf(element.hashCode())) + .build(); + } + + public static void main(String[] args) throws Exception { + final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); + final Map applicationProperties = loadApplicationProperties(env); + LOG.warn("Application properties: {}", applicationProperties); + + // Wrap the deserialization schema + KinesisDeserializationSchema deaggregatingKinesisDeserializationSchema = + // Wrapper which takes care of deaggregation + new KinesisDeaggregatingDeserializationSchemaWrapper<>( + new JsonDeserializationSchema<>(StockPrice.class) // DeserializationSchema to deserialize each deaggregated record + ); + KinesisStreamsSource source = createKinesisSource(applicationProperties.get("InputStream0"), deaggregatingKinesisDeserializationSchema); + + // Set up the source + DataStream input = env.fromSource(source, + WatermarkStrategy.noWatermarks(), + "Kinesis source", + TypeInformation.of(StockPrice.class)); + + // Send the deaggregated records to the sink + KinesisStreamsSink sink = createKinesisSink(applicationProperties.get("OutputStream0"), new JsonSerializationSchema<>()); + + input.sinkTo(sink); + + env.execute("Kinesis de-aggregating Source"); + } +} diff --git a/java/KinesisSourceDeaggregation/flink-app/src/main/java/com/amazonaws/services/msf/deaggregation/KinesisDeaggregatingDeserializationSchemaWrapper.java b/java/KinesisSourceDeaggregation/flink-app/src/main/java/com/amazonaws/services/msf/deaggregation/KinesisDeaggregatingDeserializationSchemaWrapper.java new file mode 100644 index 0000000..9151839 --- /dev/null +++ b/java/KinesisSourceDeaggregation/flink-app/src/main/java/com/amazonaws/services/msf/deaggregation/KinesisDeaggregatingDeserializationSchemaWrapper.java @@ -0,0 +1,105 @@ +package com.amazonaws.services.msf.deaggregation; + +import org.apache.flink.api.common.serialization.DeserializationSchema; +import org.apache.flink.api.common.typeinfo.TypeInformation; +import org.apache.flink.connector.kinesis.source.serialization.KinesisDeserializationSchema; +import org.apache.flink.util.Collector; +import software.amazon.awssdk.services.kinesis.model.Record; +import software.amazon.kinesis.retrieval.AggregatorUtil; +import software.amazon.kinesis.retrieval.KinesisClientRecord; + +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Wrapper adding de-aggregation to any DeserializationSchema + * + * @param type returned by the DeserializationSchema + */ +public class KinesisDeaggregatingDeserializationSchemaWrapper implements KinesisDeserializationSchema { + private static final long serialVersionUID = 1L; + + private final DeserializationSchema deserializationSchema; + private final RecordDeaggregator recordDeaggregator = new RecordDeaggregator(); + + public KinesisDeaggregatingDeserializationSchemaWrapper(DeserializationSchema deserializationSchema) { + this.deserializationSchema = deserializationSchema; + } + + @Override + public void open(DeserializationSchema.InitializationContext context) throws Exception { + this.deserializationSchema.open(context); + } + + @Override + public void deserialize(Record record, String stream, String shardId, Collector output) { + try { + // Deaggregate the record read from the stream, if required + List deaggregatedRecords = recordDeaggregator.deaggregate(record); + // Deserialize each deaggregated record, independently + for (KinesisClientRecord deaggregatedRecord : deaggregatedRecords) { + T deserializedRecord = deserializationSchema.deserialize(toByteArray(deaggregatedRecord.data())); + output.collect(deserializedRecord); + } + } catch (Exception e) { + throw new RuntimeException("Error while deaggregating Kinesis record from stream " + stream + ", shardId " + shardId, e); + } + } + + @Override + public TypeInformation getProducedType() { + return deserializationSchema.getProducedType(); + } + + private static byte[] toByteArray(ByteBuffer buffer) { + byte[] data = new byte[buffer.remaining()]; + buffer.get(data); + return data; + } + + /** + * De-aggregate software.amazon.awssdk.services.kinesis.model.Record into a collection + * of software.amazon.kinesis.retrieval.KinesisClientRecord. + *