From 1acd0b355ee3a29be54c72a0b985ea43345b1a52 Mon Sep 17 00:00:00 2001 From: Iakov Gan Date: Thu, 29 Feb 2024 13:15:43 +0100 Subject: [PATCH 01/39] simplify boto3 retrival with paginators --- data/resource_collector.py | 102 ++++++------------------------------- 1 file changed, 16 insertions(+), 86 deletions(-) diff --git a/data/resource_collector.py b/data/resource_collector.py index f9976d3..989b6f1 100644 --- a/data/resource_collector.py +++ b/data/resource_collector.py @@ -1,109 +1,39 @@ -import boto3 import json -import math + +import boto3 from botocore.config import Config -singletons = [] def get_resources(tag_name, tag_values, config): """Get resources from resource groups and tagging API. Assembles resources in a list containing only ARN and tags """ - resourcetaggingapi = boto3.client('resourcegroupstaggingapi', config=config) - resources = [] - - tags = len(tag_values) - if tags > 5: - tags_processed = 0 - while tags_processed < tags: - incremental_tag_values = tag_values[tags_processed:tags_processed+5] - resources = get_resources_from_api(resourcetaggingapi, resources, tag_name, incremental_tag_values) - tags_processed += 5 - else: - resources = get_resources_from_api(resourcetaggingapi, resources, tag_name, tag_values) - resources.extend(autoscaling_retriever(tag_name, tag_values, config)) - return resources - - -def get_resources_from_api(resourcetaggingapi, resources, tag_name, tag_values): - response = resourcetaggingapi.get_resources( - TagFilters=[ - { - 'Key': tag_name, - 'Values': tag_values - }, - ], - ResourcesPerPage=40 + return ( + get_resources_from_api(tag_name, tag_values, config) + + autoscaling_retriever(tag_name, tag_values, config) ) - resources.extend(response['ResourceTagMappingList']) - while response['PaginationToken'] != '': - print('Got the pagination token') - response = resourcetaggingapi.get_resources( - PaginationToken=response['PaginationToken'], - TagFilters=[ - { - 'Key': tag_name, - 'Values': tag_values - }, - ], - ResourcesPerPage=40 - ) - resources.extend(response['ResourceTagMappingList']) - - return resources +def get_resources_from_api(tag_name, tag_values, config): + resourcetaggingapi = boto3.client('resourcegroupstaggingapi', config=config) + return list( + resourcetaggingapi.get_paginator('get_resources').paginate( + TagFilters=[{'Key': tag_name, 'Values': tag_values}] + ).search('ResourceTagMappingList') + ) def autoscaling_retriever(tag_name, tag_values, config): - resources = [] - tags = len(tag_values) - if tags > 5: - tags_processed = 0 - while tags_processed < tags: - incremental_tag_values = tag_values[tags_processed:tags_processed+5] - resources.extend(get_asgs_from_api(tag_name, incremental_tag_values, config)) - tags_processed += 5 - else: - resources.extend(get_asgs_from_api(tag_name, tag_values, config)) - - return resources - - -def get_asgs_from_api(tag_name, tag_values, config): """Autoscaling is not supported by resource groups and tagging api This is :return: """ asg = boto3.client('autoscaling', config=config) - resources = [] - response = asg.describe_auto_scaling_groups( - Filters=[ - { - 'Name': 'tag:'+tag_name, - 'Values': tag_values - } - ], - MaxRecords=10 + resources = list( + asg.get_paginator('describe_auto_scaling_groups').paginate( + TagFilters=[{'Name': 'tag:' + tag_name, 'Values': tag_values}] + ).search('AutoScalingGroups') ) - resources.extend(response['AutoScalingGroups']) - try: - while response['NextToken']: - response = asg.describe_auto_scaling_groups( - NextToken=response['NextToken'], - Filters=[ - { - 'Name': 'tag:'+tag_name, - 'Values': tag_values - } - ], - MaxRecords=10 - ) - resources.extend(response['AutoScalingGroups']) - except: - print(f'Done fetching autoscaling groups') - for resource in resources: resource['ResourceARN'] = resource['AutoScalingGroupARN'] - return resources def cw_custom_namespace_retriever(config): From 28d81e1cc2e05586cbbc81ec572668208c251e84 Mon Sep 17 00:00:00 2001 From: Iakov Gan Date: Thu, 29 Feb 2024 21:05:21 +0100 Subject: [PATCH 02/39] add main --- data/resource_collector.py | 82 ++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/data/resource_collector.py b/data/resource_collector.py index 989b6f1..3161d4e 100644 --- a/data/resource_collector.py +++ b/data/resource_collector.py @@ -2,8 +2,12 @@ import boto3 from botocore.config import Config +from tqdm import tqdm +def log(*args, **kwargs): + tqdm.write(*args, **kwargs) + def get_resources(tag_name, tag_values, config): """Get resources from resource groups and tagging API. Assembles resources in a list containing only ARN and tags @@ -29,7 +33,7 @@ def autoscaling_retriever(tag_name, tag_values, config): asg = boto3.client('autoscaling', config=config) resources = list( asg.get_paginator('describe_auto_scaling_groups').paginate( - TagFilters=[{'Name': 'tag:' + tag_name, 'Values': tag_values}] + Filters=[{'Name': 'tag:' + tag_name, 'Values': tag_values}] ).search('AutoScalingGroups') ) for resource in resources: @@ -45,7 +49,7 @@ def cw_custom_namespace_retriever(config): for record in response['Metrics']: if not record['Namespace'].startswith('AWS/') and not record['Namespace'].startswith('CWAgent') and record['Namespace'] not in resources: resources.append(record['Namespace']) - print(resources) + log(resources) try: while response['NextToken']: @@ -55,9 +59,9 @@ def cw_custom_namespace_retriever(config): for record in response['Metrics']: if not record['Namespace'].startswith('AWS/') and not record['Namespace'].startswith('CWAgent') and record['Namespace'] not in resources: resources.append(record['Namespace']) - print(resources) + log(resources) except: - print(f'Done fetching cloudwatch namespaces') + log(f'Done fetching cloudwatch namespaces') return resources @@ -115,7 +119,7 @@ def router(resource, config): def apigw1_decorator(resource, config): - print(f'This resource is API Gateway 1 {resource["ResourceARN"]}') + log(f'This resource is API Gateway 1 {resource["ResourceARN"]}') apiid = resource['ResourceARN'].split('/')[len(resource['ResourceARN'].split('/'))-1] apigw = boto3.client('apigateway', config=config) response = apigw.get_rest_api( @@ -131,7 +135,7 @@ def apigw1_decorator(resource, config): return resource def apigw2_decorator(resource, config): - print(f'This resource is API Gateway 2 {resource["ResourceARN"]}') + log(f'This resource is API Gateway 2 {resource["ResourceARN"]}') apiid = resource['ResourceARN'].split('/')[len(resource['ResourceARN'].split('/')) - 1] apigw = boto3.client('apigatewayv2', config=config) response = apigw.get_api( @@ -146,7 +150,7 @@ def apigw2_decorator(resource, config): def appsync_decorator(resource, config): - print(f'This resource is AppSync {resource["ResourceARN"]}') + log(f'This resource is AppSync {resource["ResourceARN"]}') apiid = resource['ResourceARN'].split('/')[len(resource['ResourceARN'].split('/')) - 1] appsync = boto3.client('appsync', config=config) response = appsync.get_graphql_api( @@ -162,7 +166,7 @@ def appsync_decorator(resource, config): def aurora_decorator(resource, config): - print(f'This resource is Aurora {resource["ResourceARN"]}') + log(f'This resource is Aurora {resource["ResourceARN"]}') clusterid = resource['ResourceARN'].split(':')[len(resource['ResourceARN'].split(':')) - 1] rds = boto3.client('rds', config=config) try: @@ -182,20 +186,20 @@ def aurora_decorator(resource, config): resource['Iops'] = response['DBClusters'][0]['Iops'] resource['PerformanceInsightsEnabled'] = response['DBClusters'][0]['PerformanceInsightsEnabled'] except: - print('Just aurora-resource') + log('Just aurora-resource') return resource def autoscaling_decorator(resource, config): - print(f'This resource is Autoscaling Group {resource["ResourceARN"]}') + log(f'This resource is Autoscaling Group {resource["ResourceARN"]}') return resource def beanstalk_decorator(resource, config): return resource def cloudfront_decorator(resource, config): - print(f'This resource is CloudFront distribution') + log(f'This resource is CloudFront distribution') client = boto3.client('cloudfront', config=config) response = client.get_distribution( Id = resource['ResourceARN'].split('/')[len(resource['ResourceARN'].split('/'))-1] @@ -208,7 +212,7 @@ def cloudfront_decorator(resource, config): return resource def mediapackage_decorator(resource, config): - print(f'this resource is Mediapackage channel') + log(f'this resource is Mediapackage channel') arn = resource['ResourceARN'] client = boto3.client('mediapackage', config=config) response = client.list_channels( @@ -228,7 +232,7 @@ def mediapackage_decorator(resource, config): return resource def medialive_decorator(resource, config): - print(f'this resource is Medialive channel') + log(f'this resource is Medialive channel') arn = resource['ResourceARN'] client = boto3.client('medialive', config=config) response = client.list_channels( @@ -245,12 +249,12 @@ def medialive_decorator(resource, config): return resource def odcr_decorator(resource, config): - print(f'This resource is ODCR {resource["ResourceARN"]}') + log(f'This resource is ODCR {resource["ResourceARN"]}') return resource def dynamodb_decorator(resource, config): - print(f'This resource is DynamoDB {resource["ResourceARN"]}') + log(f'This resource is DynamoDB {resource["ResourceARN"]}') tablename = resource['ResourceARN'].split('/')[len(resource['ResourceARN'].split('/'))-1] ddb = boto3.client('dynamodb', config=config) response = ddb.describe_table( @@ -270,7 +274,7 @@ def dynamodb_decorator(resource, config): return resource def efs_decorator(resource, config): - print(f'This resource is EFS {resource["ResourceARN"]}') + log(f'This resource is EFS {resource["ResourceARN"]}') fsId = resource['ResourceARN'].split('/')[len(resource['ResourceARN'].split('/'))-1] efs = boto3.client('efs', config=config) response = efs.describe_file_systems( @@ -282,7 +286,7 @@ def efs_decorator(resource, config): def ec2_decorator(resource, config): - print(f'This resource is EC2 {resource["ResourceARN"]}') + log(f'This resource is EC2 {resource["ResourceARN"]}') instanceid = resource['ResourceARN'].split('/')[len(resource['ResourceARN'].split('/'))-1] ec2 = boto3.client('ec2', config=config) @@ -322,7 +326,7 @@ def ec2_decorator(resource, config): volumes.append(record) except: - print(f'Done fetching volumes') + log(f'Done fetching volumes') resource['Volumes'] = volumes @@ -354,16 +358,16 @@ def ec2_decorator(resource, config): {'Name': 'InstanceId', 'Value': instanceid} ], ): if len(response['Metrics']) > 0: - print(f'Instance {instanceid} has CWAgent') + log(f'Instance {instanceid} has CWAgent') resource['CWAgent'] = 'True' else: - print(f'Instance {instanceid} does not have CWAgent') + log(f'Instance {instanceid} does not have CWAgent') resource['CWAgent'] = 'False' return resource def elasticache_decorator(resource, config): - print(f'This resource is Elasticache {resource["ResourceARN"]}') + log(f'This resource is Elasticache {resource["ResourceARN"]}') if ':cluster:' in resource['ResourceARN']: clusterid = resource['ResourceARN'].split(':')[len(resource['ResourceARN'].split(':'))-1] client = boto3.client('elasticache', config=config) @@ -385,7 +389,7 @@ def elasticache_decorator(resource, config): def lambda_decorator(resource, config): - print(f'This resource is Lambda {resource["ResourceARN"]}') + log(f'This resource is Lambda {resource["ResourceARN"]}') functionname = resource['ResourceARN'].split(':')[len(resource['ResourceARN'].split(':')) - 1] lambdaclient = boto3.client('lambda', config=config) response = lambdaclient.get_function( @@ -396,7 +400,7 @@ def lambda_decorator(resource, config): def elb1_decorator(resource, config): - print(f'This resource is ELBv1 {resource["ResourceARN"]}') + log(f'This resource is ELBv1 {resource["ResourceARN"]}') elbname = resource['ResourceARN'].split('/')[len(resource['ResourceARN'].split('/'))-1] elb = boto3.client('elb', config=config) response = elb.describe_load_balancers( @@ -409,7 +413,7 @@ def elb1_decorator(resource, config): def elb2_decorator(resource, config): - print(f'This resource is ELBv2 {resource["ResourceARN"]}') + log(f'This resource is ELBv2 {resource["ResourceARN"]}') elb = boto3.client('elbv2', config=config) response = elb.describe_load_balancers( LoadBalancerArns=[ @@ -425,7 +429,7 @@ def elb2_decorator(resource, config): def ecs_decorator(resource, config): - print(f'This resource is ECS {resource["ResourceARN"]}') + log(f'This resource is ECS {resource["ResourceARN"]}') ecs = boto3.client('ecs', config=config) response = ecs.describe_clusters( clusters=[ @@ -469,18 +473,18 @@ def ecs_decorator(resource, config): def natgw_decorator(resource, config): - print(f'This resource is NAT-gw {resource["ResourceARN"]}') + log(f'This resource is NAT-gw {resource["ResourceARN"]}') return resource def rds_decorator(resource, config): - print(f'This resource is RDS {resource["ResourceARN"]}') + log(f'This resource is RDS {resource["ResourceARN"]}') return resource def s3_decorator(resource, config): bucket_name = resource['ResourceARN'].split(':')[len(resource['ResourceARN'].split(':'))-1] resource['BucketName'] = bucket_name - print(f'This resource {bucket_name} is S3 bucket') + log(f'This resource {bucket_name} is S3 bucket') s3client = boto3.client('s3', config=config) try: encryption_request = s3client.get_bucket_encryption( @@ -509,7 +513,7 @@ def s3_decorator(resource, config): def sqs_decorator(resource, config): - print(f'This resource is SQS {resource["ResourceARN"]}') + log(f'This resource is SQS {resource["ResourceARN"]}') queueName = resource['ResourceARN'].split(':')[len(resource['ResourceARN'].split(':'))-1] sqs = boto3.client('sqs', config=config) response = sqs.get_queue_url( @@ -523,7 +527,7 @@ def sqs_decorator(resource, config): return resource def sns_decorator(resource, config): - print(f'This resource is SNS {resource["ResourceARN"]}') + log(f'This resource is SNS {resource["ResourceARN"]}') # sns = boto3.client('sns', config=config) # response = sns.get_topic_attributes( # TopicArn=resource['ResourceARN'] @@ -535,7 +539,7 @@ def sns_decorator(resource, config): def tgw_decorator(resource, config): - print(f'This resource is TGW {resource["ResourceARN"]}') + log(f'This resource is TGW {resource["ResourceARN"]}') tgwid = resource['ResourceARN'].split('/')[len(resource['ResourceARN'].split('/'))-1] tgw = boto3.client('ec2', config=config) response = tgw.describe_transit_gateway_attachments( @@ -552,7 +556,7 @@ def tgw_decorator(resource, config): def debug(resource): - print(json.dumps(resource, indent=4, default=str)) + log(json.dumps(resource, indent=4, default=str)) def get_config(region): return Config( @@ -574,44 +578,44 @@ def handler(): f = open("../lib/config.json", "r") main_config = json.load(f) except: - print("Could not find config file!!! You should run this from 'data' directory!") + log("Could not find config file!!! You should run this from 'data' directory!") quit() try: if main_config['ResourceFile']: output_file = main_config['ResourceFile'] except: - print('No ResourceFile configured using default') + log('No ResourceFile configured using default') try: if main_config['TagKey']: tag_name = main_config['TagKey'] except: - print('No tag key configured') + log('No tag key configured') try: if main_config['TagValues']: tag_values = main_config['TagValues'] except: - print('No tag values configured') + log('No tag values configured') try: if main_config['Regions']: regions = main_config['Regions'] except: - print('No regions configured') + log('No regions configured') try: if main_config['CustomNamespaceFile']: custom_namespace_file = main_config['CustomNamespaceFile'] except: - print('No custom namespaces configured') + log('No custom namespaces configured') decorated_resources = [] region_namespaces = {'RegionNamespaces': []} if 'us-east-1' not in regions: regions.append('us-east-1') - print('Added us-east-1 region for global services') + log('Added us-east-1 region for global services') for region in regions: config = get_config(region) From 4ff3e630817d4dc03c547057ea4f845e9473d800 Mon Sep 17 00:00:00 2001 From: Iakov Gan Date: Thu, 29 Feb 2024 21:05:43 +0100 Subject: [PATCH 03/39] add main --- data/main.py | 154 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 data/main.py diff --git a/data/main.py b/data/main.py new file mode 100644 index 0000000..0e7f5eb --- /dev/null +++ b/data/main.py @@ -0,0 +1,154 @@ +import os +import json +import time +import datetime + +import click +import boto3 +from tqdm import tqdm +from InquirerPy import inquirer +from InquirerPy.base.control import Choice +from InquirerPy.separator import Separator + +from resource_collector import get_config, get_resources, cw_custom_namespace_retriever, router + +def get_active_regions(days=30, threshold=1): + """ Retrieve from Cost Explorer the list of regions where spend is over threshold + """ + client = boto3.client('ce') + end = datetime.datetime.utcnow().date() + start = end - datetime.timedelta(days=days) + response = client.get_cost_and_usage( + TimePeriod={ + 'Start': start.strftime('%Y-%m-%d'), + 'End': end.strftime('%Y-%m-%d') + }, + Granularity='DAILY', + Metrics=['UnblendedCost'], + GroupBy=[ + { + 'Type': 'DIMENSION', + 'Key': 'SERVICE' + }, + { + 'Type': 'DIMENSION', + 'Key': 'REGION' + } + ] + ) + region_spend = {} + for result in response['ResultsByTime']: + for group in result['Groups']: + if len(group['Keys']) > 1: + region = group['Keys'][1] + service = group['Keys'][0] + amount = group['Metrics']['UnblendedCost']['Amount'] + if region == 'global': + continue + if region not in region_spend: + region_spend[region] = 0 + region_spend[region] += float(amount) + return [region for region, amount in region_spend.items() if amount > threshold] + +def get_regions(default=None): + ec2_client = boto3.client('ec2') + all_regions = [region['RegionName'] for region in ec2_client.describe_regions()['Regions']] + if default is None: + try: + default = get_active_regions() + ['us-east-1'] + except: + default = ['us-east-1'] + return inquirer.checkbox( + message="Select regions:", + choices=sorted([Choice(value=name, enabled=name in default) for name in all_regions], key=lambda x: str(int(x.enabled)) + x.value, reverse=True), + cycle=False, + ).execute() + +def get_tag_key(default=None): + resource_tagging_api = boto3.client('resourcegroupstaggingapi') + tag_keys = list(resource_tagging_api.get_paginator('get_tag_keys').paginate().search('TagKeys')) + default = default or [] + return inquirer.fuzzy( + message="Select Tag Key:", + choices=[Choice(value=name, enabled=name in default) for name in tag_keys], + cycle=False, + ).execute() + +def get_tag_values(key, default=None): + resource_tagging_api = boto3.client('resourcegroupstaggingapi') + tag_values = list(resource_tagging_api.get_paginator('get_tag_values').paginate(Key=key).search('TagValues')) + default = default or [] + return inquirer.checkbox( + message=f"Select Tag {key} Value :", + choices=[Choice(value=name, enabled=name in default) for name in tag_values], + cycle=False, + ).execute() + +@click.command() +@click.option('--regions', default=None, help='Comma Separated list of regions') +@click.option('--tag', default=None, help='a Tag name') +@click.option('--values', default=None, help='Comma Separated list of values') +@click.option('--config-file', default=None, help='Json config file', type=click.Path()) +@click.option('--output-file', default="./resources.json", help='output file', type=click.Path()) +@click.option('--custom-namespaces-file', default="./custom_namespaces.json", help='custom_namespaces file', type=click.Path()) +@click.option('--base-name', default=None, help='Base Name') +@click.option('--grouping-tag-key', default=None, help='GroupingTagKey') +def main(base_name, regions, tag, values, config_file, output_file, custom_namespaces_file): + """ Main """ + + if not config_file and os.path.exists("lib/config.json"): + config_file = "lib/config.json" + print('reading from {config}') + main_config = {} + if config_file: + main_config = json.load(open(config_file)) + base_name = base_name or main_config.get('BaseName') + regions = regions or main_config.get('Regions') + tag = tag or main_config.get('TagKey') + values = values or main_config.get('TagValues') + output_file = output_file or main_config.get('ResourceFile') + base_name = base_name or inquirer.text('Enter BaseName', default=base_name or 'Application').execute() + regions = regions or get_regions() + tag = tag or get_tag_key() + values = values or get_tag_values(tag) + + + if not os.path.exists(output_file) or inquirer.confirm(f'Resources scan was done {time.ctime(os.path.getmtime(output_file))}. Re scan?', default=True).execute(): + decorated_resources = [] + region_namespaces = {'RegionNamespaces': []} + if 'us-east-1' not in regions: + regions.append('us-east-1') + print('Added us-east-1 region for global services') + + for region in tqdm(regions, desc='Regions', leave=False): + config = get_config(region) + resources = get_resources(tag, values, config) + region_namespace = {'Region': region, 'Namespaces' : cw_custom_namespace_retriever(config) } + region_namespaces['RegionNamespaces'].append(region_namespace) + for resource in tqdm(resources, desc='Resources', leave=False): + decorated_resources.append(router(resource, config)) + + with open(custom_namespaces_file, "w") as _file: + json.dump(region_namespaces, _file, indent=4, default=str) + print(f'custom_namespaces: {output_file}') + + with open(output_file, "w") as _file: + json.dump(decorated_resources, _file, indent=4, default=str) + print(f'output: {output_file}') + + config_file = config_file or "lib/config.json" + with open(config_file, "w") as _file: + main_config["Regions"] = regions + main_config["TagKey"] = tag + main_config["TagValues"] = values + main_config["ResourceFile"] = output_file + json.dump(main_config, _file, indent=4, default=str) + print(f'config: {config_file}') + print(f'config: {config_file}') + + if inquirer.confirm(f'Run `cdk synth` ?', default=True).execute(): + os.system('cdk synth') + + +if __name__ == '__main__': + main() From 3939db173566815f980988adf0e24712f435b0be Mon Sep 17 00:00:00 2001 From: Iakov Gan Date: Thu, 29 Feb 2024 21:06:19 +0100 Subject: [PATCH 04/39] add main --- requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 94bbd78..cbff2a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ boto3 -botocore \ No newline at end of file +botocore +tqdm +PyInquirer \ No newline at end of file From 3646287e94f5136437604416b3383e21f3608e71 Mon Sep 17 00:00:00 2001 From: Iakov Gan Date: Thu, 29 Feb 2024 21:07:27 +0100 Subject: [PATCH 05/39] better error handling --- lib/iem-dashboard-stack.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/iem-dashboard-stack.ts b/lib/iem-dashboard-stack.ts index 63ae68c..57c00b6 100644 --- a/lib/iem-dashboard-stack.ts +++ b/lib/iem-dashboard-stack.ts @@ -9,7 +9,7 @@ export class IemDashboardStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); - const dashboard = new Dashboard(this,config.BaseName,{ + const dashboard = new Dashboard(this, config.BaseName, { dashboardName: config.BaseName + '-Dashboard' }); @@ -17,8 +17,9 @@ export class IemDashboardStack extends Stack { try { resources = require(config.ResourceFile); console.log(`LOADED RESOURCE FILE ${config.ResourceFile}`); - } catch { - console.log(`ERROR: ${config.ResourceFile} not found, run 'cd data; python resource_collector.py'`); + } catch (error) { + console.error(`An error occurred: ${error.message}`); + console.error(`ERROR: file ${config.ResourceFile} not found, run 'cd data; python resource_collector.py'`); } const graphFactory = new GraphFactory(this,'GraphFactory',resources, config); From f0126dbae790b18c5019134e7675f02c3b7667f2 Mon Sep 17 00:00:00 2001 From: Iakov Gan Date: Thu, 29 Feb 2024 21:10:18 +0100 Subject: [PATCH 06/39] better error handling --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index f60797b..e4744fd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ !jest.config.js *.d.ts node_modules +*/__pycache__/* +lib/*.json +data/*.json # CDK asset staging directory .cdk.staging From a6882c56f6460a8504b9ceb4c09a47949c330759 Mon Sep 17 00:00:00 2001 From: Iakov Gan Date: Thu, 29 Feb 2024 21:26:58 +0100 Subject: [PATCH 07/39] more work --- data/main.py | 19 +++++++++--------- lib/config.json | 53 ++++++++++++++++++++++++++++++------------------- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/data/main.py b/data/main.py index 0e7f5eb..07d0b66 100644 --- a/data/main.py +++ b/data/main.py @@ -67,11 +67,10 @@ def get_regions(default=None): def get_tag_key(default=None): resource_tagging_api = boto3.client('resourcegroupstaggingapi') tag_keys = list(resource_tagging_api.get_paginator('get_tag_keys').paginate().search('TagKeys')) - default = default or [] return inquirer.fuzzy( message="Select Tag Key:", - choices=[Choice(value=name, enabled=name in default) for name in tag_keys], - cycle=False, + choices=[Choice(value=name) for name in tag_keys], + default=default, ).execute() def get_tag_values(key, default=None): @@ -93,24 +92,26 @@ def get_tag_values(key, default=None): @click.option('--custom-namespaces-file', default="./custom_namespaces.json", help='custom_namespaces file', type=click.Path()) @click.option('--base-name', default=None, help='Base Name') @click.option('--grouping-tag-key', default=None, help='GroupingTagKey') -def main(base_name, regions, tag, values, config_file, output_file, custom_namespaces_file): +def main(base_name, regions, tag, values, config_file, output_file, custom_namespaces_file, grouping_tag_key): """ Main """ if not config_file and os.path.exists("lib/config.json"): config_file = "lib/config.json" - print('reading from {config}') main_config = {} if config_file: + print(f'reading from {config_file}') main_config = json.load(open(config_file)) + print(main_config) base_name = base_name or main_config.get('BaseName') + grouping_tag_key = grouping_tag_key or main_config.get('GroupingTagKey') regions = regions or main_config.get('Regions') tag = tag or main_config.get('TagKey') values = values or main_config.get('TagValues') output_file = output_file or main_config.get('ResourceFile') - base_name = base_name or inquirer.text('Enter BaseName', default=base_name or 'Application').execute() - regions = regions or get_regions() - tag = tag or get_tag_key() - values = values or get_tag_values(tag) + base_name = inquirer.text('Enter BaseName', default=base_name or 'Application').execute() + regions = get_regions(default=regions) + tag = get_tag_key(default=tag) + values = get_tag_values(tag, default=values or []) if not os.path.exists(output_file) or inquirer.confirm(f'Resources scan was done {time.ctime(os.path.getmtime(output_file))}. Re scan?', default=True).execute(): diff --git a/lib/config.json b/lib/config.json index fee66e2..cbc1b54 100644 --- a/lib/config.json +++ b/lib/config.json @@ -1,21 +1,34 @@ { - "BaseName": "Application", - "ResourceFile": "../data/resources.json", - "TagKey": "iem", - "TagValues": ["202202","202102"], - "Regions": ["eu-west-1"], - "GroupingTagKey": "groupby", - "CustomEC2TagKeys": ["Add","Your","TagKeys", "Here"], - "CustomNamespaceFile": "../data/custom_namespaces.json", - "Compact": false, - "CompactMaxResourcesPerWidget": 10, - "AlarmTopic": "", - "AlarmDashboard": { - "enabled": false, - "organizationId": "", - "alarmViewListSize": 100 - }, - "MetricDashboards": { - "enabled": true - } -} + "BaseName": "Application", + "ResourceFile": "./resources.json", + "TagKey": "Name", + "TagValues": [ + "attachement1", + "test", + "test1", + "transit-gateway-01" + ], + "Regions": [ + "us-east-1", + "eu-west-1" + ], + "GroupingTagKey": "groupby", + "CustomEC2TagKeys": [ + "Add", + "Your", + "TagKeys", + "Here" + ], + "CustomNamespaceFile": "../data/custom_namespaces.json", + "Compact": false, + "CompactMaxResourcesPerWidget": 10, + "AlarmTopic": "", + "AlarmDashboard": { + "enabled": false, + "organizationId": "", + "alarmViewListSize": 100 + }, + "MetricDashboards": { + "enabled": true + } +} \ No newline at end of file From f8bbd79196951e34478c32b778fd8eb59430da3e Mon Sep 17 00:00:00 2001 From: Iakov Gan Date: Thu, 29 Feb 2024 21:39:55 +0100 Subject: [PATCH 08/39] refactoring --- data/main.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/data/main.py b/data/main.py index 07d0b66..e75b082 100644 --- a/data/main.py +++ b/data/main.py @@ -8,7 +8,6 @@ from tqdm import tqdm from InquirerPy import inquirer from InquirerPy.base.control import Choice -from InquirerPy.separator import Separator from resource_collector import get_config, get_resources, cw_custom_namespace_retriever, router @@ -101,7 +100,6 @@ def main(base_name, regions, tag, values, config_file, output_file, custom_names if config_file: print(f'reading from {config_file}') main_config = json.load(open(config_file)) - print(main_config) base_name = base_name or main_config.get('BaseName') grouping_tag_key = grouping_tag_key or main_config.get('GroupingTagKey') regions = regions or main_config.get('Regions') @@ -145,11 +143,15 @@ def main(base_name, regions, tag, values, config_file, output_file, custom_names main_config["ResourceFile"] = output_file json.dump(main_config, _file, indent=4, default=str) print(f'config: {config_file}') - print(f'config: {config_file}') + + if not os.path.exists('node_modules') and inquirer.confirm(f'Looks like node dependencies are not installed. Run `npm ic` ?', default=True).execute(): + os.system('npm ic') if inquirer.confirm(f'Run `cdk synth` ?', default=True).execute(): os.system('cdk synth') + if inquirer.confirm(f'Run `cdk deploy` ?', default=True).execute(): + os.system('cdk deploy') if __name__ == '__main__': main() From e58152bdf0669b202be0a7e6c4885b5668250894 Mon Sep 17 00:00:00 2001 From: Iakov Gan Date: Thu, 29 Feb 2024 21:41:31 +0100 Subject: [PATCH 09/39] revert config --- lib/config.json | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/lib/config.json b/lib/config.json index cbc1b54..e150b51 100644 --- a/lib/config.json +++ b/lib/config.json @@ -1,34 +1,21 @@ { "BaseName": "Application", - "ResourceFile": "./resources.json", - "TagKey": "Name", - "TagValues": [ - "attachement1", - "test", - "test1", - "transit-gateway-01" - ], - "Regions": [ - "us-east-1", - "eu-west-1" - ], + "ResourceFile": "../data/resources.json", + "TagKey": "iem", + "TagValues": ["202202","202102"], + "Regions": ["eu-west-1"], "GroupingTagKey": "groupby", - "CustomEC2TagKeys": [ - "Add", - "Your", - "TagKeys", - "Here" - ], + "CustomEC2TagKeys": ["Add","Your","TagKeys", "Here"], "CustomNamespaceFile": "../data/custom_namespaces.json", "Compact": false, "CompactMaxResourcesPerWidget": 10, "AlarmTopic": "", "AlarmDashboard": { - "enabled": false, - "organizationId": "", - "alarmViewListSize": 100 + "enabled": false, + "organizationId": "", + "alarmViewListSize": 100 }, "MetricDashboards": { - "enabled": true + "enabled": true } -} \ No newline at end of file + } \ No newline at end of file From 10613803d2c635b22cd7629583fd9d92934db9ef Mon Sep 17 00:00:00 2001 From: Iakov Gan Date: Thu, 29 Feb 2024 22:33:49 +0100 Subject: [PATCH 10/39] rollback changes on debugstackset --- lib/config.json | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/config.json b/lib/config.json index e150b51..7bf64fa 100644 --- a/lib/config.json +++ b/lib/config.json @@ -1,21 +1,21 @@ { - "BaseName": "Application", - "ResourceFile": "../data/resources.json", - "TagKey": "iem", - "TagValues": ["202202","202102"], - "Regions": ["eu-west-1"], - "GroupingTagKey": "groupby", - "CustomEC2TagKeys": ["Add","Your","TagKeys", "Here"], - "CustomNamespaceFile": "../data/custom_namespaces.json", - "Compact": false, - "CompactMaxResourcesPerWidget": 10, - "AlarmTopic": "", - "AlarmDashboard": { - "enabled": false, - "organizationId": "", - "alarmViewListSize": 100 - }, - "MetricDashboards": { - "enabled": true - } - } \ No newline at end of file + "BaseName": "Application", + "ResourceFile": "../data/resources.json", + "TagKey": "iem", + "TagValues": ["202202","202102"], + "Regions": ["eu-west-1"], + "GroupingTagKey": "groupby", + "CustomEC2TagKeys": ["Add","Your","TagKeys", "Here"], + "CustomNamespaceFile": "../data/custom_namespaces.json", + "Compact": false, + "CompactMaxResourcesPerWidget": 10, + "AlarmTopic": "", + "AlarmDashboard": { + "enabled": false, + "organizationId": "", + "alarmViewListSize": 100 + }, + "MetricDashboards": { + "enabled": true + } +} \ No newline at end of file From 9fa86024815f80810b2b2af9e18878608a04ea8a Mon Sep 17 00:00:00 2001 From: Iakov Gan Date: Thu, 29 Feb 2024 23:39:05 +0100 Subject: [PATCH 11/39] update readme and add paramteres in the stack --- README.md | 228 +++++++++-------------- stack_sets/event_forwarder_template.yaml | 18 +- 2 files changed, 103 insertions(+), 143 deletions(-) diff --git a/README.md b/README.md index 2162c0b..abbcc2f 100644 --- a/README.md +++ b/README.md @@ -3,168 +3,89 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![AWS Provider](https://img.shields.io/badge/provider-AWS-orange?logo=amazon-aws&color=ff9900)](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/WhatIsCloudWatch.html) -The project is an example how to use AWS Resource Groups Tagging API to retrieve a specific tag -and then based on found resources pull additional information from respective service APIs to generate -a configuration file (JSON) to build a CloudWatch Dashboard with _reasonable_ metrics and alarms. Optionally customers -can also deploy a central alarm dashboard to monitor alarms across an AWS Organization, AWS Organization OU or across -arbitrary number of AWS accounts. +The project is an example how to use AWS Resource Groups Tagging API to retrieve a specific tag and then based on found resources pull additional information from respective service APIs to generate a configuration file (JSON) to build a CloudWatch Dashboard with _reasonable_ metrics and alarms. Optionally customers +can also deploy a central alarm dashboard to monitor alarms across an AWS Organization, AWS Organization OU or across arbitrary number of AWS accounts. ## Features +### 1. Metric dashboards +- Discover AWS resources based on Tag +- Generates a set of CloudWatch Dashboards for AWS Resources. Dashboards are specifically designed to monitor the most important operational metrics. -### Supported services - -- Amazon API Gateway v1 (REST) -- Amazon API Gateway v2 (HTTP, WebSockets) -- AWS AppSync -- Amazon Aurora -- Auto Scaling groups -- On-Demand Capacity Reservations -- Amazon CloudFront -- Amazon DynamoDB -- Amazon EBS (as part of EC2) -- Amazon EC2 (support for t\* burstable instances, support for CloudWatch Agent) -- ELB v1 (ELB Classic) -- ELB v2 (ALB, NLB) -- Amazon ECS (EC2 and Fargate) -- Amazon EFS -- AWS Lambda -- AWS Elemental MediaLive -- AWS Elemental MediaPackage -- NAT Gateway -- RDS -- S3 -- SNS -- SQS -- Transit Gateway -- AWS WAFv2 - -### Central alarm dashboard features +### 2. Alarm dashboard - Event-driven for scalability and speed - Supports arbitrary source accounts within an AWS Organization (different teams can have own dashboards) - Supports automatic source account configuration through stack-sets -- Supports visualization and sorting of alarm priority (CRITICAL, MEDIUM, LOW) through alarm tags in source accounts. -Simply add tag with key `priority` and values critical, medium or low. +- Supports visualization and sorting of alarm priority (CRITICAL, MEDIUM, LOW) through alarm tags in source accounts. Simply add tag with key `priority` and values critical, medium or low. - Supports tag data for EC2 instances in source accounts ## How it works ### Metric dashboards -1. `data/resource_collector.py` is used to call the Resource Groups Tagging API and to generate the configuration file. +1. A python tool `data/main.py` is used to retrieve a list of resources using Resource Groups Tagging API and to generate the configuration file. 2. CDK (v2) is used to generate CloudFormation template and deploy it -The solution will create metrics and alarms following best practices. - -### Central alarm dashboard -When a CloudWatch Alarm changes state (going from OK to ALARM state), an event is emitted to EventBridge in the account. -An event bus rule forwards the event to the central event bus in the monitoring account. This event is then registered -in DynamoDB. CloudWatch custom widgets visualize current alarm state on the dashboard. +### Central Alarm Dashboard +When a CloudWatch Alarm changes state (going from OK to ALARM state), an event is emitted to EventBridge in the account. An event bus rule forwards the event to the central event bus in the monitoring account. This event is then registered in DynamoDB. CloudWatch custom widgets visualize current alarm state on the dashboard. ## Prerequisites - -### To generate the resource configuration: - -- Python 3 -- Boto 3 (Python module. `python -m pip install boto3`) - -### To generate the dashboard - +- Python3 - NodeJS 16+, recommended 18LTS, (required by CDK v2) - CDK v2 (Installation: `npm -g install aws-cdk@latest`) -## Configuration properties in lib/config.json - -`BaseName` (String:required) - Base-name of your dashboards. This will be the prefix of the dashboard names. - -`ResourceFile` (String:required) - The path for the file where resources are stored. Used by the `resource_collector.py` -when generating resource config and by the CDK when generating the CF template. - -`TagKey` (String:required) - Configuration of the tag key that will select resources to be included. - -`TagValues` (Array:required) - List of values of `TagKey` to include. - -`Regions` (Array:required) - List of regions from which resources are displayed. - -`GroupingTagKey` (String:optional) - If set, separate Lambda and EC2 dashboards will be created for every value of that -tag. Every value groups resources by that value. - -`CustomEC2TagKeys` (Array:optional) - If set, the tag info will show in the EC2 header widget in format -Key:Value. Useful to add auxilary information to the header. - -`CustomNamepsaceFile` (String:required) - Detected custom namespaces. Not yet used. - -`Compact` (boolean (true/false):required) - When set to true, multiple Lambda functions will be put in a single widget -set. Useful when there are many Lambda functions. - -`CompactMaxResourcesPerWidget` (Integer:required) - When `Compact` is set to true, determines how many Lambda functions -will be in each widget set. +## Installation and Run deployment of CloudWatch Dashboards + +1. Check out the project, install dependencies and initiate CDK +```bash +git checkout git@github.com:aws-samples/tag-based-cloudwatch-dashboard.git +cd tag-based-cloudwatch-dashboard +pip3 install -r requirements.txt +npm ic +cdk bootstrap +``` +2. Run +```bash +python3 data/main.py +``` +The tool will read existing configuration file, the read resources from your AWS Account in given regions and generate for all resources with a given Tag a CloudWatch Dashboards. You can fine tune Dashboards using Configuration File and repeating the command above. + +3. Explore Dashboards in your CloudWatch console https://console.aws.amazon.com/cloudwatch/home?#dashboards/ + +## Enabling Alarms Feature +Enabling of alarms feature requires modifications on 2 accounts: source account and destination account. + +1. In addition to CloudWatch Dashboards dashboards deployment, modify in `lib/config.json` by setting `AlarmDashboard.enabled` to `True` and provide your AWS-Organizations id in `AlarmDashboard.organizationId` (o-xxxxx, not ou-xxxx). Then update dashboards using the tool: +```bash +python3 data/main.py +``` + +2. Deploy `event_forwarder.yaml` template manually to each of the source accounts and each region you wish to enable through CloudFormation or deploy it automatically to an AWS Organization, OU or list of accounts through service managed StackSets from your management account or StackSet delegate account. + +If you are using StackSets please note that StackSets are not deploying Stacks in the Management Account. If you want Alerts from Management account you will need additionally to deploy `event_forwarder.yaml` in all relevant regions of your Management Account. + +## Advanced configuration +You can fine tune configuration of dashboards in by editing a configuration file `lib/config.json` + +* `BaseName` (String:required) - Base-name of your dashboards. This will be the prefix of the dashboard names. +* `ResourceFile` (String:required) - The path for the file where resources are stored. Used by the `resource_collector.py` when generating resource config and by the CDK when generating the CF template. +* `TagKey` (String:required) - Configuration of the tag key that will select resources to be included. +* `TagValues` (Array:required) - List of values of `TagKey` to include. +* `Regions` (Array:required) - List of regions from which resources are displayed. +* `GroupingTagKey` (String:optional) - If set, separate Lambda and EC2 dashboards will be created for every value of that tag. Every value groups resources by that value. +* `CustomEC2TagKeys` (Array:optional) - If set, the tag info will show in the EC2 header widget in format Key:Value. Useful to add auxilary information to the header. +* `CustomNamepsaceFile` (String:required) - Detected custom namespaces. Not yet used. +* `Compact` (boolean (true/false):required) - When set to true, multiple Lambda functions will be put in a single widget set. Useful when there are many Lambda functions. +* `CompactMaxResourcesPerWidget` (Integer:required) - When `Compact` is set to true, determines how many Lambda functions will be in each widget set. +* `AlarmTopic` (String:optional) - When `AlarmTopic` contains a string with an ARN to a SNS topic, all alarms will be created with an action to send notification to that SNS topic. +* `AlarmDashboard.enabled` (boolean (true/false):optional) - When set to true deploys the alarm dashboard in the account. +* `AlarmDashboard.organizationId` (String: required when `AlarmDashboard.enabled` is true) - Required in order to set resource policy on the custom event bus to allow PutEvents from the AWS Organization. +* `MetricDashboards.enabled` (boolean (true/false):optional) - If not defined or set to true, deploy metric dashboards. Recommended if only alarm dashboard is being deployed. -`AlarmTopic` (String:optional) - When `AlarmTopic` contains a string with an ARN to a SNS topic, all alarms will be -created with an action to send notification to that SNS topic. - -`AlarmDashboard.enabled` (boolean (true/false):optional) - When set to true deploys the alarm dashboard in the account. - -`AlarmDashboard.organizationId` (String: required when `AlarmDashboard.enabled` is true) - Required in order to set -resource policy on the custom event bus to allow PutEvents from the AWS Organization. - -`MetricDashboards.enabled` (boolean (true/false):optional) - If not defined or set to true, deploy metric dashboards. -Recommended if only alarm dashboard is being deployed. - - -## Getting and preparing the code - -1. Check out the project. -2. Change current directory to project directory. (`cd tag-based-cloudwatch-dashboard`) -3. If deploying for the first time, run `cdk bootstrap` to bootstrap the environment -(https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html). In case you don't want to bootstrap -read [Deploying without boostraping CDK](BOOTSTRAP.md). -4. Run `npm install` to install dependencies. - -## Configuring the dashboards -1. Open the configuration file `lib/config.json` in your editor of choice. -2. Set TagKey to tag key you want to use and TagValues to an array of values. Dashboard will collect all resources tagged -with that key and the specified values. -3. Set Regions to include the regions that contain resources you want to monitor. -4. **OPTIONAL** if you want to deploy central alarm dashboard set `AlarmDashboard.enabled` to true and provide your AWS -Organizations id in `AlarmDashboard.organizationId`. -5. **OPTIONAL** if you don't want to use metric dashboards you can disable creation of those by setting -`MetricDashboards.enabled` to false. See _Configuration properties in lib/config.json_ above for more information. -6. Save the configuration file. - -## Deploying the dashboards - -1. If the deployment of the metric dashboards have been enabled, run `cd data; python3 resource_collector.py` to create -the resource configuration file (`resources.json` in the `data` directory). -2. **OPTIONAL:** Edit `BaseName`-property in `lib/config.json` to change the name of your dashboard. In case you plan to -deploy multiple sets of dashboards for different applications in the same account, ensure all subsequent deploys have -different `BaseName`. -3. Run `cd ..` to change directory to project root. -4. Run `cdk synth` to generate CF template or use `cdk deploy --all` to deploy directly to your AWS account. -5. In case central alarm dashboard is enabled in the configuration, take note of deployment output, -`*.CustomEventBusArn` and `*.CustomDynamoDBFunctionRoleArn` and copy those ARNs to use in the next stage. - -## Enabling source accounts to share alarms -_This only applies in case `AlarmDashboard.enabled` is set_ - -1. Run command `cd stack_sets` to change directory which contains `event_forwarder_template.yaml`. -2. Run command `sh create_stackset.sh ARN_OF_CUSTOM_EVENT_BUS ARN_OF_THE_LAMBDA_FUNCTION_ROLE_ARN`, replace the -placeholder with the ARNs from the previous step. -3. Deploy the generated `event_forwarder.yaml`-template manually to each of the source accounts and each region you wish -to enable through CloudFormation or deploy it automatically to an AWS Organization, OU or list of accounts through -service managed stack-sets from your management account or stack-set delegate account. - -## Monitoring alarms in "Management Account" - -In case you have alarms in the AWS Organizations management account but are deploying the Alarm Dashboard in another -account, you will need to manually deploy `event_forwarder.yaml` in the management account in all regions that you want -to receive alarms from. This is because of that even if the `event_forwarder.yaml` is deployed as a managed stack set it -won't get deployed in the management account. ## Tips - -Try setting up a CodeCommit repository where you store your code. Set up a CI/CD pipeline to automatically redeploy your dashboard. +You can setting up a CodeCommit repository where you store your code. Set up a CI/CD pipeline to automatically redeploy your dashboard. This way, if you want to change/add/remove any metrics for any of the services you change the code, commit it, and it will be automatically deployed. -Try creating an EventBridge rule that will listen to specific tag change and trigger the CodeBuild project to redeploy the dashboard. +You can also create an EventBridge rule that will listen to specific tag change and trigger the CodeBuild project to redeploy the dashboard. This way, if you have an autoscaling group or just tag additional resources the dashboard will deploy automatically. In case you do so, monitor your builds to avoid rare situations where a lot of tag changes could cause excessive amounts of concurrent or queued builds (for example event bridge rule misconfiguration or variable loads that causes ASG to scale up and down quickly). This can be done by specifying tag value in the Event Bridge rule or instead of triggering the build @@ -211,6 +132,33 @@ directly from Event Bridge sending it to a Lambda function for more flexible dec [![Click to open screenshot](screenshots/ECS-EC2-service-thumb.png)](screenshots/ECS-EC2-service.png) -### Developing +## Supported services +- Amazon API Gateway v1 (REST) +- Amazon API Gateway v2 (HTTP, WebSockets) +- AWS AppSync +- Amazon Aurora +- Auto Scaling groups +- On-Demand Capacity Reservations +- Amazon CloudFront +- Amazon DynamoDB +- Amazon EBS (as part of EC2) +- Amazon EC2 (support for t\* burstable instances, support for CloudWatch Agent) +- ELB v1 (ELB Classic) +- ELB v2 (ALB, NLB) +- Amazon ECS (EC2 and Fargate) +- Amazon EFS +- AWS Lambda +- AWS Elemental MediaLive +- AWS Elemental MediaPackage +- NAT Gateway +- RDS +- S3 +- SNS +- SQS +- Transit Gateway +- AWS WAFv2 + + +## Developing [Developing](DEVELOPING.md) diff --git a/stack_sets/event_forwarder_template.yaml b/stack_sets/event_forwarder_template.yaml index 0acd152..78cbefd 100644 --- a/stack_sets/event_forwarder_template.yaml +++ b/stack_sets/event_forwarder_template.yaml @@ -1,5 +1,17 @@ AWSTemplateFormatVersion: '2010-09-09' +Parameters: + + CentralBusARN: + Type: String + Description: ARN of EventBus in the central Account + Default: REPLACE_WITH_CENTRAL_BUS_ARN + + LambdaRoleARN: + Type: String + Description: Central Lambda Function ARN + Default: REPLACE_WITH_LAMBDA_ROLE_ARN + Resources: CentralEventBusForwardingRole: @@ -19,7 +31,7 @@ Resources: Statement: - Effect: 'Allow' Action: 'events:PutEvents' - Resource: 'REPLACE_WITH_CENTRAL_BUS_ARN' + Resource: !Ref CentralBusARN AlarmStateChangeEventRule: Type: 'AWS::Events::Rule' @@ -32,7 +44,7 @@ Resources: - 'CloudWatch Alarm State Change' State: 'ENABLED' Targets: - - Arn: 'REPLACE_WITH_CENTRAL_BUS_ARN' + - Arn: !Ref CentralBusARN Id: 'Target1' RoleArn: !GetAtt [ CentralEventBusForwardingRole, Arn ] @@ -44,7 +56,7 @@ Resources: - Action: sts:AssumeRole Effect: Allow Principal: - AWS: 'REPLACE_WITH_LAMBDA_ROLE_ARN' + AWS: !Ref LambdaRoleARN Version: "2012-10-17" Description: Role used by central Alarm event augmentation Lambda function RoleName: !Sub "CrossAccountAlarmAugmentationAssumeRole-${AWS::Region}" From 38768c8fa0f3d5ff52065a8a118d10b78a7bbec0 Mon Sep 17 00:00:00 2001 From: Iakov Gan Date: Fri, 1 Mar 2024 09:33:29 +0100 Subject: [PATCH 12/39] allow multiple accounts --- data/main.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/data/main.py b/data/main.py index e75b082..d1479cf 100644 --- a/data/main.py +++ b/data/main.py @@ -111,10 +111,27 @@ def main(base_name, regions, tag, values, config_file, output_file, custom_names tag = get_tag_key(default=tag) values = get_tag_values(tag, default=values or []) - - if not os.path.exists(output_file) or inquirer.confirm(f'Resources scan was done {time.ctime(os.path.getmtime(output_file))}. Re scan?', default=True).execute(): - decorated_resources = [] - region_namespaces = {'RegionNamespaces': []} + need_scan = True + decorated_resources = [] + if os.path.exists(output_file): + + choice = inquirer.select( + f'Resources file was updated {time.ctime(os.path.getmtime(output_file))}', + choices=["Amend/update", "Override", "Skip scan and use previous results"], + default="Amend/update", + ).execute() + if choice == "Amend/update": + with open(config_file, "w") as : + decorated_resources = json.load(main_config, _file) + account_id = boto3.client('sts').get_caller_identity()['Account'] + # clean from current account resources + decorated_resources = [resource for resource in decorated_resources if account_id not in resource.get('ResourceARN', '')] + elif choice == "Override": + need_scan = True + else: + need_scan = False + + if need_scan: if 'us-east-1' not in regions: regions.append('us-east-1') print('Added us-east-1 region for global services') From 3fca7fea4775a15d8a737c6ca732052188596b0d Mon Sep 17 00:00:00 2001 From: Iakov Gan Date: Fri, 1 Mar 2024 10:50:58 +0100 Subject: [PATCH 13/39] fixes --- data/main.py | 7 ++++--- lib/config.json | 51 +++++++++++++++++++++++++++++++------------------ 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/data/main.py b/data/main.py index d1479cf..aa99e96 100644 --- a/data/main.py +++ b/data/main.py @@ -113,7 +113,8 @@ def main(base_name, regions, tag, values, config_file, output_file, custom_names need_scan = True decorated_resources = [] - if os.path.exists(output_file): + region_namespaces = {'RegionNamespaces': []} + if os.path.exists(output_file) and open(): choice = inquirer.select( f'Resources file was updated {time.ctime(os.path.getmtime(output_file))}', @@ -121,8 +122,8 @@ def main(base_name, regions, tag, values, config_file, output_file, custom_names default="Amend/update", ).execute() if choice == "Amend/update": - with open(config_file, "w") as : - decorated_resources = json.load(main_config, _file) + with open(output_file) as _file : + decorated_resources = json.load(_file) account_id = boto3.client('sts').get_caller_identity()['Account'] # clean from current account resources decorated_resources = [resource for resource in decorated_resources if account_id not in resource.get('ResourceARN', '')] diff --git a/lib/config.json b/lib/config.json index 7bf64fa..c44fe45 100644 --- a/lib/config.json +++ b/lib/config.json @@ -1,21 +1,34 @@ { - "BaseName": "Application", - "ResourceFile": "../data/resources.json", - "TagKey": "iem", - "TagValues": ["202202","202102"], - "Regions": ["eu-west-1"], - "GroupingTagKey": "groupby", - "CustomEC2TagKeys": ["Add","Your","TagKeys", "Here"], - "CustomNamespaceFile": "../data/custom_namespaces.json", - "Compact": false, - "CompactMaxResourcesPerWidget": 10, - "AlarmTopic": "", - "AlarmDashboard": { - "enabled": false, - "organizationId": "", - "alarmViewListSize": 100 - }, - "MetricDashboards": { - "enabled": true - } + "BaseName": "Application", + "ResourceFile": "./resources.json", + "TagKey": "Name", + "TagValues": [ + "attachement1", + "test", + "test1", + "transit-gateway-01" + ], + "Regions": [ + "eu-west-1", + "us-east-1" + ], + "GroupingTagKey": "groupby", + "CustomEC2TagKeys": [ + "Add", + "Your", + "TagKeys", + "Here" + ], + "CustomNamespaceFile": "../data/custom_namespaces.json", + "Compact": false, + "CompactMaxResourcesPerWidget": 10, + "AlarmTopic": "", + "AlarmDashboard": { + "enabled": false, + "organizationId": "", + "alarmViewListSize": 100 + }, + "MetricDashboards": { + "enabled": true + } } \ No newline at end of file From fc3d5a2b2af650581a0c594dedfd358abb9cf63b Mon Sep 17 00:00:00 2001 From: Iakov Gan Date: Fri, 1 Mar 2024 10:51:43 +0100 Subject: [PATCH 14/39] fixes --- data/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/main.py b/data/main.py index aa99e96..15e9e28 100644 --- a/data/main.py +++ b/data/main.py @@ -114,7 +114,7 @@ def main(base_name, regions, tag, values, config_file, output_file, custom_names need_scan = True decorated_resources = [] region_namespaces = {'RegionNamespaces': []} - if os.path.exists(output_file) and open(): + if os.path.exists(output_file): choice = inquirer.select( f'Resources file was updated {time.ctime(os.path.getmtime(output_file))}', From e7a9bafda52f31a033c0f094dae13083454af2cb Mon Sep 17 00:00:00 2001 From: Iakov Gan Date: Fri, 1 Mar 2024 11:25:39 +0100 Subject: [PATCH 15/39] add profile --- data/resource_collector.py | 167 +++++++++++++++++++------------------ 1 file changed, 84 insertions(+), 83 deletions(-) diff --git a/data/resource_collector.py b/data/resource_collector.py index 3161d4e..aafe617 100644 --- a/data/resource_collector.py +++ b/data/resource_collector.py @@ -8,29 +8,29 @@ def log(*args, **kwargs): tqdm.write(*args, **kwargs) -def get_resources(tag_name, tag_values, config): +def get_resources(tag_name, tag_values, session, config): """Get resources from resource groups and tagging API. Assembles resources in a list containing only ARN and tags """ return ( - get_resources_from_api(tag_name, tag_values, config) - + autoscaling_retriever(tag_name, tag_values, config) + get_resources_from_api(tag_name, tag_values, session, config) + + autoscaling_retriever(tag_name, tag_values, session, config) ) -def get_resources_from_api(tag_name, tag_values, config): - resourcetaggingapi = boto3.client('resourcegroupstaggingapi', config=config) +def get_resources_from_api(tag_name, tag_values, session, config): + resourcetaggingapi = session.client('resourcegroupstaggingapi', config=config) return list( resourcetaggingapi.get_paginator('get_resources').paginate( TagFilters=[{'Key': tag_name, 'Values': tag_values}] ).search('ResourceTagMappingList') ) -def autoscaling_retriever(tag_name, tag_values, config): +def autoscaling_retriever(tag_name, tag_values, session, config): """Autoscaling is not supported by resource groups and tagging api This is :return: """ - asg = boto3.client('autoscaling', config=config) + asg = session.client('autoscaling', config=config) resources = list( asg.get_paginator('describe_auto_scaling_groups').paginate( Filters=[{'Name': 'tag:' + tag_name, 'Values': tag_values}] @@ -40,10 +40,10 @@ def autoscaling_retriever(tag_name, tag_values, config): resource['ResourceARN'] = resource['AutoScalingGroupARN'] return resources -def cw_custom_namespace_retriever(config): +def cw_custom_namespace_retriever(session, config): """Retrieving all custom namespaces """ - cw = boto3.client('cloudwatch', config=config) + cw = session.client('cloudwatch', config=config) resources = [] response = cw.list_metrics() for record in response['Metrics']: @@ -67,61 +67,61 @@ def cw_custom_namespace_retriever(config): -def router(resource, config): +def router(resource, session, config): arn = resource['ResourceARN'] if ':apigateway:' in arn and '/restapis/' in arn and 'stages' not in arn: - resource = apigw1_decorator(resource, config) + resource = apigw1_decorator(resource, session, config) elif ':apigateway:' in arn and '/apis/' in arn and 'stages' not in arn: - resource = apigw2_decorator(resource, config) + resource = apigw2_decorator(resource, session, config) elif ':appsync:' in arn: - resource = appsync_decorator(resource, config) + resource = appsync_decorator(resource, session, config) elif ':rds:' in arn and ':cluster:' in arn: - resource = aurora_decorator(resource, config) + resource = aurora_decorator(resource, session, config) elif ':autoscaling:' in arn and ':autoScalingGroup:' in arn: - resource = autoscaling_decorator(resource, config) + resource = autoscaling_decorator(resource, session, config) elif ':capacity-reservation/' in arn: - resource = odcr_decorator(resource, config) + resource = odcr_decorator(resource, session, config) elif ':dynamodb:' in arn and ':table/' in arn: - resource = dynamodb_decorator(resource, config) + resource = dynamodb_decorator(resource, session, config) elif ':ec2:' in arn and ':instance/' in arn: - resource = ec2_decorator(resource, config) + resource = ec2_decorator(resource, session, config) elif 'lambda' in arn and 'function' in arn: - resource = lambda_decorator(resource, config) + resource = lambda_decorator(resource, session, config) elif 'elasticloadbalancing' in arn and '/net/' not in arn and '/app/' not in arn and ':targetgroup/' not in arn: - resource = elb1_decorator(resource, config) + resource = elb1_decorator(resource, session, config) elif 'elasticloadbalancing' in arn and ( '/net/' in arn or '/app/' in arn ) and ':targetgroup/' not in arn: - resource = elb2_decorator(resource, config) + resource = elb2_decorator(resource, session, config) elif ':ecs:' in arn and ':cluster/' in arn: - resource = ecs_decorator(resource, config) + resource = ecs_decorator(resource, session, config) elif ':natgateway/' in arn and ':ec2:' in arn: - resource = natgw_decorator(resource, config) + resource = natgw_decorator(resource, session, config) elif ':transit-gateway/' in arn and ':ec2:' in arn: - resource = tgw_decorator(resource, config) + resource = tgw_decorator(resource, session, config) elif ':sqs:' in arn: - resource = sqs_decorator(resource, config) + resource = sqs_decorator(resource, session, config) elif 'arn:aws:s3:' in arn: - resource = s3_decorator(resource, config) + resource = s3_decorator(resource, session, config) elif ':sns:' in arn: - resource = sns_decorator(resource, config) + resource = sns_decorator(resource, session, config) elif ':cloudfront:' in arn and ':distribution/' in arn: - resource = cloudfront_decorator(resource, config) + resource = cloudfront_decorator(resource, session, config) elif ':elasticache:' in arn: - resource = elasticache_decorator(resource, config) + resource = elasticache_decorator(resource, session, config) elif ':mediapackage:' in arn and ':channels/' in arn: - resource = mediapackage_decorator(resource, config) + resource = mediapackage_decorator(resource, session, config) elif ':medialive:' in arn and ':channel:' in arn: - resource = medialive_decorator(resource, config) + resource = medialive_decorator(resource, session, config) elif ':elasticfilesystem:' in arn: - resource = efs_decorator(resource, config) + resource = efs_decorator(resource, session, config) elif 'arn:aws:elasticbeanstalk:' in arn: - resource = beanstalk_decorator(resource,config) + resource = beanstalk_decorator(resource,session, config) return resource -def apigw1_decorator(resource, config): +def apigw1_decorator(resource, session, config): log(f'This resource is API Gateway 1 {resource["ResourceARN"]}') apiid = resource['ResourceARN'].split('/')[len(resource['ResourceARN'].split('/'))-1] - apigw = boto3.client('apigateway', config=config) + apigw = session.client('apigateway', config=config) response = apigw.get_rest_api( restApiId=apiid ) @@ -134,10 +134,10 @@ def apigw1_decorator(resource, config): resource['stages'] = response2['item'] return resource -def apigw2_decorator(resource, config): +def apigw2_decorator(resource, session, config): log(f'This resource is API Gateway 2 {resource["ResourceARN"]}') apiid = resource['ResourceARN'].split('/')[len(resource['ResourceARN'].split('/')) - 1] - apigw = boto3.client('apigatewayv2', config=config) + apigw = session.client('apigatewayv2', config=config) response = apigw.get_api( ApiId=apiid ) @@ -149,10 +149,10 @@ def apigw2_decorator(resource, config): return resource -def appsync_decorator(resource, config): +def appsync_decorator(resource, session, config): log(f'This resource is AppSync {resource["ResourceARN"]}') apiid = resource['ResourceARN'].split('/')[len(resource['ResourceARN'].split('/')) - 1] - appsync = boto3.client('appsync', config=config) + appsync = session.client('appsync', config=config) response = appsync.get_graphql_api( apiId=apiid ) @@ -165,10 +165,10 @@ def appsync_decorator(resource, config): return resource -def aurora_decorator(resource, config): +def aurora_decorator(resource, session, config): log(f'This resource is Aurora {resource["ResourceARN"]}') clusterid = resource['ResourceARN'].split(':')[len(resource['ResourceARN'].split(':')) - 1] - rds = boto3.client('rds', config=config) + rds = session.client('rds', config=config) try: response = rds.describe_db_clusters( DBClusterIdentifier=clusterid @@ -191,16 +191,16 @@ def aurora_decorator(resource, config): return resource -def autoscaling_decorator(resource, config): +def autoscaling_decorator(resource, session, config): log(f'This resource is Autoscaling Group {resource["ResourceARN"]}') return resource -def beanstalk_decorator(resource, config): +def beanstalk_decorator(resource, session, config): return resource -def cloudfront_decorator(resource, config): +def cloudfront_decorator(resource, session, config): log(f'This resource is CloudFront distribution') - client = boto3.client('cloudfront', config=config) + client = session.client('cloudfront', config=config) response = client.get_distribution( Id = resource['ResourceARN'].split('/')[len(resource['ResourceARN'].split('/'))-1] ) @@ -211,10 +211,10 @@ def cloudfront_decorator(resource, config): resource['Origins'] = response['Distribution']['DistributionConfig']['Origins'] return resource -def mediapackage_decorator(resource, config): +def mediapackage_decorator(resource, session, config): log(f'this resource is Mediapackage channel') arn = resource['ResourceARN'] - client = boto3.client('mediapackage', config=config) + client = session.client('mediapackage', config=config) response = client.list_channels( MaxResults=40, @@ -231,10 +231,10 @@ def mediapackage_decorator(resource, config): resource['OriginEndpoint'] = origin_endpoint['OriginEndpoints'] return resource -def medialive_decorator(resource, config): +def medialive_decorator(resource, session, config): log(f'this resource is Medialive channel') arn = resource['ResourceARN'] - client = boto3.client('medialive', config=config) + client = session.client('medialive', config=config) response = client.list_channels( MaxResults=40, ) @@ -248,15 +248,15 @@ def medialive_decorator(resource, config): resource['Pipeline'] = response2['PipelineDetails'] return resource -def odcr_decorator(resource, config): +def odcr_decorator(resource, session, config): log(f'This resource is ODCR {resource["ResourceARN"]}') return resource -def dynamodb_decorator(resource, config): +def dynamodb_decorator(resource, session, config): log(f'This resource is DynamoDB {resource["ResourceARN"]}') tablename = resource['ResourceARN'].split('/')[len(resource['ResourceARN'].split('/'))-1] - ddb = boto3.client('dynamodb', config=config) + ddb = session.client('dynamodb', config=config) response = ddb.describe_table( TableName=tablename ) @@ -273,10 +273,10 @@ def dynamodb_decorator(resource, config): resource['rcu'] = rcu return resource -def efs_decorator(resource, config): +def efs_decorator(resource, session, config): log(f'This resource is EFS {resource["ResourceARN"]}') fsId = resource['ResourceARN'].split('/')[len(resource['ResourceARN'].split('/'))-1] - efs = boto3.client('efs', config=config) + efs = session.client('efs', config=config) response = efs.describe_file_systems( FileSystemId=fsId ) @@ -285,10 +285,10 @@ def efs_decorator(resource, config): return resource -def ec2_decorator(resource, config): +def ec2_decorator(resource, session, config): log(f'This resource is EC2 {resource["ResourceARN"]}') instanceid = resource['ResourceARN'].split('/')[len(resource['ResourceARN'].split('/'))-1] - ec2 = boto3.client('ec2', config=config) + ec2 = session.client('ec2', config=config) volumes = [] @@ -349,7 +349,7 @@ def ec2_decorator(resource, config): ) resource['CPUCreditSpecs'] = response['InstanceCreditSpecifications'][0] - cw = boto3.client('cloudwatch', config=config) + cw = session.client('cloudwatch', config=config) results = cw.get_paginator('list_metrics') for response in results.paginate( MetricName='mem_used_percent', @@ -366,11 +366,11 @@ def ec2_decorator(resource, config): return resource -def elasticache_decorator(resource, config): +def elasticache_decorator(resource, session, config): log(f'This resource is Elasticache {resource["ResourceARN"]}') if ':cluster:' in resource['ResourceARN']: clusterid = resource['ResourceARN'].split(':')[len(resource['ResourceARN'].split(':'))-1] - client = boto3.client('elasticache', config=config) + client = session.client('elasticache', config=config) response = client.describe_cache_clusters( CacheClusterId=clusterid ) @@ -388,10 +388,10 @@ def elasticache_decorator(resource, config): return resource -def lambda_decorator(resource, config): +def lambda_decorator(resource, session, config): log(f'This resource is Lambda {resource["ResourceARN"]}') functionname = resource['ResourceARN'].split(':')[len(resource['ResourceARN'].split(':')) - 1] - lambdaclient = boto3.client('lambda', config=config) + lambdaclient = session.client('lambda', config=config) response = lambdaclient.get_function( FunctionName=functionname ) @@ -399,10 +399,10 @@ def lambda_decorator(resource, config): return resource -def elb1_decorator(resource, config): +def elb1_decorator(resource, session, config): log(f'This resource is ELBv1 {resource["ResourceARN"]}') elbname = resource['ResourceARN'].split('/')[len(resource['ResourceARN'].split('/'))-1] - elb = boto3.client('elb', config=config) + elb = session.client('elb', config=config) response = elb.describe_load_balancers( LoadBalancerNames=[ elbname @@ -412,9 +412,9 @@ def elb1_decorator(resource, config): return resource -def elb2_decorator(resource, config): +def elb2_decorator(resource, session, config): log(f'This resource is ELBv2 {resource["ResourceARN"]}') - elb = boto3.client('elbv2', config=config) + elb = session.client('elbv2', config=config) response = elb.describe_load_balancers( LoadBalancerArns=[ resource['ResourceARN'] @@ -428,9 +428,9 @@ def elb2_decorator(resource, config): return resource -def ecs_decorator(resource, config): +def ecs_decorator(resource, session, config): log(f'This resource is ECS {resource["ResourceARN"]}') - ecs = boto3.client('ecs', config=config) + ecs = session.client('ecs', config=config) response = ecs.describe_clusters( clusters=[ resource['ResourceARN'] @@ -456,7 +456,7 @@ def ecs_decorator(resource, config): for lb in service['loadBalancers']: target_groups.append(lb['targetGroupArn']) - elb = boto3.client('elbv2', config=config) + elb = session.client('elbv2', config=config) for target_group in target_groups: response = elb.describe_target_health( TargetGroupArn=target_group @@ -472,20 +472,20 @@ def ecs_decorator(resource, config): return resource -def natgw_decorator(resource, config): +def natgw_decorator(resource, session, config): log(f'This resource is NAT-gw {resource["ResourceARN"]}') return resource -def rds_decorator(resource, config): +def rds_decorator(resource, session, config): log(f'This resource is RDS {resource["ResourceARN"]}') return resource -def s3_decorator(resource, config): +def s3_decorator(resource, session, config): bucket_name = resource['ResourceARN'].split(':')[len(resource['ResourceARN'].split(':'))-1] resource['BucketName'] = bucket_name log(f'This resource {bucket_name} is S3 bucket') - s3client = boto3.client('s3', config=config) + s3client = session.client('s3', config=config) try: encryption_request = s3client.get_bucket_encryption( Bucket=bucket_name @@ -512,10 +512,10 @@ def s3_decorator(resource, config): return resource -def sqs_decorator(resource, config): +def sqs_decorator(resource, session, config): log(f'This resource is SQS {resource["ResourceARN"]}') queueName = resource['ResourceARN'].split(':')[len(resource['ResourceARN'].split(':'))-1] - sqs = boto3.client('sqs', config=config) + sqs = session.client('sqs', config=config) response = sqs.get_queue_url( QueueName=queueName ) @@ -526,9 +526,9 @@ def sqs_decorator(resource, config): resource['Attributes'] = response['Attributes'] return resource -def sns_decorator(resource, config): +def sns_decorator(resource, session, config): log(f'This resource is SNS {resource["ResourceARN"]}') -# sns = boto3.client('sns', config=config) +# sns = session.client('sns', config=config) # response = sns.get_topic_attributes( # TopicArn=resource['ResourceARN'] # ) @@ -538,10 +538,10 @@ def sns_decorator(resource, config): return resource -def tgw_decorator(resource, config): +def tgw_decorator(resource, session, config): log(f'This resource is TGW {resource["ResourceARN"]}') tgwid = resource['ResourceARN'].split('/')[len(resource['ResourceARN'].split('/'))-1] - tgw = boto3.client('ec2', config=config) + tgw = session.client('ec2', config=config) response = tgw.describe_transit_gateway_attachments( Filters=[{ 'Name': 'transit-gateway-id', @@ -575,10 +575,10 @@ def handler(): output_file = "resources.json" custom_namespace_file = "custom_namespaces.json" try: - f = open("../lib/config.json", "r") + f = open("../lib/session.json", "r") main_config = json.load(f) except: - log("Could not find config file!!! You should run this from 'data' directory!") + log("Could not find session file!!! You should run this from 'data' directory!") quit() try: @@ -619,11 +619,12 @@ def handler(): for region in regions: config = get_config(region) - resources = get_resources(tag_name, tag_values, config) - region_namespace = {'Region': region, 'Namespaces' : cw_custom_namespace_retriever(config) } + session = boto3.Session(profile_name=None) + resources = get_resources(tag_name, tag_values, session, config) + region_namespace = {'Region': region, 'Namespaces' : cw_custom_namespace_retriever(session, config) } region_namespaces['RegionNamespaces'].append(region_namespace) for resource in resources: - decorated_resources.append(router(resource, config)) + decorated_resources.append(router(resource, session, config)) cn = open(custom_namespace_file, "w") cn.write(json.dumps(region_namespaces, indent=4, default=str)) cn.close() From 749f39ae43e0a4590bb11ccb17e76a677a181bd6 Mon Sep 17 00:00:00 2001 From: Iakov Gan Date: Fri, 1 Mar 2024 11:25:51 +0100 Subject: [PATCH 16/39] add profile --- data/main.py | 164 +++++++++++++++++++++++++++------------------------ 1 file changed, 87 insertions(+), 77 deletions(-) diff --git a/data/main.py b/data/main.py index 15e9e28..0185b71 100644 --- a/data/main.py +++ b/data/main.py @@ -11,76 +11,84 @@ from resource_collector import get_config, get_resources, cw_custom_namespace_retriever, router -def get_active_regions(days=30, threshold=1): - """ Retrieve from Cost Explorer the list of regions where spend is over threshold - """ - client = boto3.client('ce') - end = datetime.datetime.utcnow().date() - start = end - datetime.timedelta(days=days) - response = client.get_cost_and_usage( - TimePeriod={ - 'Start': start.strftime('%Y-%m-%d'), - 'End': end.strftime('%Y-%m-%d') - }, - Granularity='DAILY', - Metrics=['UnblendedCost'], - GroupBy=[ - { - 'Type': 'DIMENSION', - 'Key': 'SERVICE' +class App(): + def __init__(self, profile=None): + self.session = boto3.session.Session(profile_name=profile) + + def get_active_regions(self, days=30, threshold=1): + """ Retrieve from Cost Explorer the list of regions where spend is over threshold + """ + client = self.session.client('ce') + end = datetime.datetime.utcnow().date() + start = end - datetime.timedelta(days=days) + response = client.get_cost_and_usage( + TimePeriod={ + 'Start': start.strftime('%Y-%m-%d'), + 'End': end.strftime('%Y-%m-%d') }, - { - 'Type': 'DIMENSION', - 'Key': 'REGION' - } - ] - ) - region_spend = {} - for result in response['ResultsByTime']: - for group in result['Groups']: - if len(group['Keys']) > 1: - region = group['Keys'][1] - service = group['Keys'][0] - amount = group['Metrics']['UnblendedCost']['Amount'] - if region == 'global': - continue - if region not in region_spend: - region_spend[region] = 0 - region_spend[region] += float(amount) - return [region for region, amount in region_spend.items() if amount > threshold] - -def get_regions(default=None): - ec2_client = boto3.client('ec2') - all_regions = [region['RegionName'] for region in ec2_client.describe_regions()['Regions']] - if default is None: - try: - default = get_active_regions() + ['us-east-1'] - except: - default = ['us-east-1'] - return inquirer.checkbox( - message="Select regions:", - choices=sorted([Choice(value=name, enabled=name in default) for name in all_regions], key=lambda x: str(int(x.enabled)) + x.value, reverse=True), - cycle=False, - ).execute() - -def get_tag_key(default=None): - resource_tagging_api = boto3.client('resourcegroupstaggingapi') - tag_keys = list(resource_tagging_api.get_paginator('get_tag_keys').paginate().search('TagKeys')) - return inquirer.fuzzy( - message="Select Tag Key:", - choices=[Choice(value=name) for name in tag_keys], - default=default, - ).execute() - -def get_tag_values(key, default=None): - resource_tagging_api = boto3.client('resourcegroupstaggingapi') - tag_values = list(resource_tagging_api.get_paginator('get_tag_values').paginate(Key=key).search('TagValues')) - default = default or [] - return inquirer.checkbox( - message=f"Select Tag {key} Value :", - choices=[Choice(value=name, enabled=name in default) for name in tag_values], - cycle=False, - ).execute() + Granularity='DAILY', + Metrics=['UnblendedCost'], + GroupBy=[ + { + 'Type': 'DIMENSION', + 'Key': 'SERVICE' + }, + { + 'Type': 'DIMENSION', + 'Key': 'REGION' + } + ] + ) + region_spend = {} + for result in response['ResultsByTime']: + for group in result['Groups']: + if len(group['Keys']) > 1: + region = group['Keys'][1] + service = group['Keys'][0] + amount = group['Metrics']['UnblendedCost']['Amount'] + if region == 'global': + continue + if region not in region_spend: + region_spend[region] = 0 + region_spend[region] += float(amount) + return [region for region, amount in region_spend.items() if amount > threshold] + + def get_regions(self, default=None): + ec2_client = self.session.client('ec2') + all_regions = [region['RegionName'] for region in ec2_client.describe_regions()['Regions']] + if default is None: + try: + default = get_active_regions() + ['us-east-1'] + except: + default = ['us-east-1'] + return inquirer.checkbox( + message="Select regions:", + choices=sorted([Choice(value=name, enabled=name in default) for name in all_regions], key=lambda x: str(int(x.enabled)) + x.value, reverse=True), + cycle=False, + ).execute() + + def get_tag_key(self, default=None): + resource_tagging_api = self.session.client('resourcegroupstaggingapi') + tag_keys = list(resource_tagging_api.get_paginator('get_tag_keys').paginate().search('TagKeys')) + return inquirer.fuzzy( + message="Select Tag Key:", + choices=[Choice(value=name) for name in tag_keys], + default=default, + ).execute() + + def get_tag_values(self, key, default=None): + resource_tagging_api = self.session.client('resourcegroupstaggingapi') + tag_values = list(resource_tagging_api.get_paginator('get_tag_values').paginate(Key=key).search('TagValues')) + default = default or [] + return inquirer.checkbox( + message=f"Select Tag {key} Value :", + choices=[Choice(value=name, enabled=name in default) for name in tag_values], + cycle=False, + ).execute() + + def account_id(self): + return self.session.client('sts').get_caller_identity()['Account'] + @click.command() @click.option('--regions', default=None, help='Comma Separated list of regions') @@ -91,9 +99,10 @@ def get_tag_values(key, default=None): @click.option('--custom-namespaces-file', default="./custom_namespaces.json", help='custom_namespaces file', type=click.Path()) @click.option('--base-name', default=None, help='Base Name') @click.option('--grouping-tag-key', default=None, help='GroupingTagKey') -def main(base_name, regions, tag, values, config_file, output_file, custom_namespaces_file, grouping_tag_key): +@click.option('--profile', default=None, help='Profile') +def main(base_name, regions, tag, values, config_file, output_file, custom_namespaces_file, grouping_tag_key, profile): """ Main """ - + app = App(profile) if not config_file and os.path.exists("lib/config.json"): config_file = "lib/config.json" main_config = {} @@ -107,9 +116,9 @@ def main(base_name, regions, tag, values, config_file, output_file, custom_names values = values or main_config.get('TagValues') output_file = output_file or main_config.get('ResourceFile') base_name = inquirer.text('Enter BaseName', default=base_name or 'Application').execute() - regions = get_regions(default=regions) - tag = get_tag_key(default=tag) - values = get_tag_values(tag, default=values or []) + regions = app.get_regions(default=regions) + tag = app.get_tag_key(default=tag) + values = app.get_tag_values(tag, default=values or []) need_scan = True decorated_resources = [] @@ -126,6 +135,7 @@ def main(base_name, regions, tag, values, config_file, output_file, custom_names decorated_resources = json.load(_file) account_id = boto3.client('sts').get_caller_identity()['Account'] # clean from current account resources + account_id = app.account_id() decorated_resources = [resource for resource in decorated_resources if account_id not in resource.get('ResourceARN', '')] elif choice == "Override": need_scan = True @@ -139,11 +149,11 @@ def main(base_name, regions, tag, values, config_file, output_file, custom_names for region in tqdm(regions, desc='Regions', leave=False): config = get_config(region) - resources = get_resources(tag, values, config) - region_namespace = {'Region': region, 'Namespaces' : cw_custom_namespace_retriever(config) } + resources = get_resources(tag, values, app.session, config) + region_namespace = {'Region': region, 'Namespaces' : cw_custom_namespace_retriever(app.session, config) } region_namespaces['RegionNamespaces'].append(region_namespace) for resource in tqdm(resources, desc='Resources', leave=False): - decorated_resources.append(router(resource, config)) + decorated_resources.append(router(resource, app.session, config)) with open(custom_namespaces_file, "w") as _file: json.dump(region_namespaces, _file, indent=4, default=str) From 729bbe0eabedaef940e4bba4525b7549744f269d Mon Sep 17 00:00:00 2001 From: Iakov Gan Date: Fri, 1 Mar 2024 11:37:44 +0100 Subject: [PATCH 17/39] add architecture --- screenshots/Architecture-Alerts.png | Bin 0 -> 482342 bytes .../Architecture-Dashboards-MultiAccount.png | Bin 0 -> 237383 bytes .../Architecture-Dashboards-SingleAccount.png | Bin 0 -> 196725 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 screenshots/Architecture-Alerts.png create mode 100644 screenshots/Architecture-Dashboards-MultiAccount.png create mode 100644 screenshots/Architecture-Dashboards-SingleAccount.png diff --git a/screenshots/Architecture-Alerts.png b/screenshots/Architecture-Alerts.png new file mode 100644 index 0000000000000000000000000000000000000000..5b1daf6514f62a3000e8d26bb550c3e41af14724 GIT binary patch literal 482342 zcmagF1zgkZ_daeQpp=M8hf*TljucUlMo@BNgn%$Y7$Gqb5fJH+99<$^qe}#&Mt4eV zbPw35|9JZQd7j_vzt`CN-d@|d?{lAXo$EU1UZAGBBH0bP8<#F!B2#+)RO`~E>${gO z5!4V9;$NXRxgmV%5~;M6yu7B8ygZAhqrHWdjrpZZ&jY{c66tAo+)Xx&41fIQGC}6Y z83IbK%b6eho@}YTd~%B=n&zrpYbf8tDwmS?WS~1QiU)Wem|DJAhn1wJ<_BG?-Q~FE zG_BhpjxW*{5YLO9lZqIW04`aWYlpdkmM4JvRhg1 zwfkKDcise=yuZmGIj;&RjGlJ_%y5W%y@tE`FLtRHgB$^a=e+nOUQ437L2G~*cJ6q$CF-!}~y=&ge{zTPhtWGoh=H-%yKgh9wlC&F% z4$t)SKi#7*VN;-M1pE;6Gzm`uGIMv|Bx-xGuCliqFcM_jajTg1_R(WG9bmDPtR8(V zYY=ncwc2iKp@jgaBdn5%JmlBT>D5I@yZh1iBRxMP(F-4NuqU57<&px|x`VctU3j#( z-x#1#Ku3^T(NPdC!%7aiuGjl;tqxJey3-Xzp(V(#-H7;zGyeSK9<5iHMq z!pVG{fPsZD{yoL|o$kKir{kY)B1%$;Pp(rkFH-dGMTP9j-CJxVFWVhqdi_&@#9Y|1 zN&C~@;|D+e&e==#znj=!pS%8=;CvhD^Mz!g$M2QI*%tQRfn>`^)wmB4q;8pcWcs(R zemeVMY|7G4INDzCpdm$yXegHWMLGq0!#KNfUE_MvN5zk8ANjs-D19*9(RktS%lDP} z>(6Jnu<9n>v}}^i01r9mufc0`xF#l7f3LVIiv9H~`Z4$ItaO%Tt6FDqU-)9OT^aP22Q&D5BY;*fWXmgo7*rMd5PU zrxF(7y!XhD_5?CXyo(aS>5>2mz*QDl<4L_}ep=qCo3 z#Vdr@E*)!S>yP1FSMZ{EB< z|5zxFIHUK&F_1L-L%cE5gUjZ|0KKbX?|Y2t$F3cGxSD#I;TmT%?ORG9;b=3c%A=;4 zq>p08rIP+for<)Nh!V%c#w57Mk-`hO< z7IFHV%JCK;%;|n*M8I?YT{`SX)2}za`O&4vEWe5N5$e|zAy0a5k*?i$-|2GAZBT$t znn$1eKASsbeW*(sf8S+N$XeQihViA(ZPda~VUcr=choB_tDDKhY`w{!kUD)`w z*aGDrvU~|2i8=^3c&__=I+H@VQ@Kr9UD-#OOZiEbX_jNwv~v5G!UxjYI8_}^!*J=J z5wDrtB;07-B&*{+1EmJ>XcCCT@qPhS_&yGEzK>hbu2G^>{hrd zu>|WxM>9Y4e%b%B_h?XY!(P`VJ8$=jXPsmlMSi@FdJF7UyNGQS(?tbo3I5o^= zvV=C8kmn&%A?$RuqDaX8n@AB}(W@eE2_zzwR>=dNsx5CUMDB*DBx+2mORBAn+_%=Z zDyb-~=qx|5c7B6_K&&MpoNqJ+3;SfUr?Y%l^j3?V9i3xV82YNy_N+esB1RFPeQIU3 zRs_=S7`bK^7V&{AUskFg)@*W0*BHGw3Z1!8PgsA9t7<6lDnEO9s)ozK94lCd>@##Q z>Pzf4{gfp9>Qf*W+)V0gOSnOJ)nCD1&i{T3#y^MjIM^xJGo&;`hn`VUKz;ZmjsUWRFLXYiw8`MSD&s zI-)3^zt=TjOE{ZMII%haJJ8#&vFb3AG1Z=NBW6k?B+I0pNN2P zF-NiNPxdzoeSyYL-oDxXJeO!AFBS{x*`#nFiN7;&_uDNx`kH(DFXC+D9_0fV$SAIZ z?|AWlzyE}$|K|1k;}LoHw-oiCsRc^hQVS0KT=>b0PME{OqI+D=U_Enex@2qqWIm>K zI;1i*_D3bhSN2Sfmz;GP`x?Obu`ePLRyqT%uTNjgIhx(zV!vmu*x7XSeh1R#np2Y* zlQ|P-ENU|GagPeaiBgvmQ?k8fInXTf#K3f7vz#CPT>XwEOHz_i{7kR8!>A*8+tV%2 zt=^OWO!=&gv?%xst#rIme4da;nP+Kd8IFLbnXox;eQv#Hd<%LX>VhIgaiB()xR$b( zM6Zkb0lp(T6rys}HgAsyDnA8jOJ(QB5Ac)U+4`xt%eg3i_f0be)+MlFk-DSh$=3JJ z-zl`IvYFu7f@+TnU^N^2t%E6duH8KqyPDFR)Gsmk8dVohVJT>7n-tQmo-!@bZiD;) zn1=eIHWL)%V}-X4P21?(rU6h!N~r*)U)*R6Nm+KeP=48=N67x}6P$GGr7K?o~=L-kv1$Q>DOmx3WZ{qo(N zZ`-dhc`JD;x{V-Vs~)T)dbbC{1!0HOj`$Tofc6buSR6lnsTi&0rn z@I|d5(OR6|j&JhBD#*C2pg_)WEsrO!vy!s%j-5x1w8`=KEKTF9t-je8h=n!vU&um` zVC}@W>2G#WrMic=m`zFRDe6A#`2J+Od2-?x4iHovWRif!T)3 zn)cB>iGEk1y~TwQr2`86cR)8c>a+SXtWL>xJRb34QqIbAubk&V8jqWJAz&lM$Ko7Gss1Vf7gp;-LzJ0vBX|}mQt+AWp zxni^(jVnKi-zi&V%1O?s_o6f1$U&-dFwQkd$}$1&o)jRN8xJ0fnc%* zqg{oq9%Lob-jFDrkg2ZA;}8p5A*!dSl@XFjK8-$F8As=WpMytW71)Ur-u=n?=DKD% zO2zI7*bU?2bXwX=bu-s5iPB!8lS+Bf*467>`r&79z7W9~RZI${dF!3`21ln%(?pGS zc2|n+C8fONU==SfrGLHjqx!1T`q(cCC!MGy?kqpz-phvR4cX`{UvKS2OCD3HWTB>Z=^=hhe2L&P-KA^z(PjLf%w_t&#tN4oT)O(( z>sKyaxcGfvoB{PugM@!2<}{#W6LFwd;0QeQ}I_~$P*`%h~dT$5AHu; zNq<0dle+bNVby(8gS*cJO<2%uk?fx+sZjZAd}nI~4fZ|N!>VE9y`5Xs4ImuU16!NIg0Vj zpXscI*p;=%-J?5@Kp!LqmvPyL!xk_~|2rl8Bz^7?^E}%rGYm31H8tqf7d@-MR*aXd zRe7%85y*k{Y9wJGQA8mn#}!LVtqpp|!Hx>8pWhRI`k#A~Bdfq0Mw8nzdo`mERBzXM zUcZ>t%yAnxvo57`xD?1!MP>-f2LdKa|8TaMT8qw;cwVS)AAU4|dCvc@b+TO9dtyA4 zZad1+sD4~7a#*qSrfebEdB=x&l}N2YP3*^HKT5L0pp^|e35n+UkB{FM09>)H@O$Bs z{CaaA7*wrWQ#o?K@9Euu-tAr7PYQpxwMZE9sgh-&{jXfAVZmmsX5BF zv@z7UZ#kJ<8uUMl#6NBNO2F~rHY1y5EUIC|(=TlG8PIV3tBWB8jVSe&c|!Z9T*?cy z(jvTFIK*A;GxfimK+lU{;1DzT*m%m4&b8rIM(adQdE@^5aMo0X28J)@`SiMJ6_8}H zM)>qVurU*AC9%y758q@U`) z*(F@QlnSVZ1KuqrNz8EnFe9Wr0Nrm%$5;In_P8UG3ifkG}V&ujhgwg`I$z+Lqrel-d2 z^U#4$kCWfiVRwNsNiGc62IU+@uqc}$2X%W*rP=+(dedNynS&qo*?Jy`LN75wN4nTT zZSR6-j|ras&)t~rdT<`G#Pj4x{(W}(F|y&I(ULU&#Uf7?%Q=tp$my9`J{cDc6atVh zJ&Yd7VgSrW+A#o>t7}(tevyKDAIkm<3ocV$@+owFuQW*44p8V3wDzRRP(s;F=!#Jm z6Ab;dwBpfrW4P8`2$(q0k{pE(8onHer8`nSJ4u~}OXrlK-;|*}i2_S@Kub#KEq8Kk z;J-Yzo7t!Jfi-!lVxoPNbcF-RdNredtB|~}FJSDBF0dr#K8zVW)&A6Z$7CkbUa8lX zrWeDq_n;&T#t~6g9=rb^Ru-@)m{`5k0^E84Pz-FFrYi{ZqiS}Ys$lZ)D= zqAP=K4Ut7fEQiyG^rAzCkv6r2d8lY0-1}Qst3zY9+t{N-CI7M-4!Su2_v8{XWUto;v5Qh$Y^4=#%9y69b}U z(fh?Y2!tpU(PNb?b{+`>x+!C`wkY);B5}=fvp)axFUwSo;|@huwATmL>^t>K$p_TvNvGXtG%#^`R*7whM|<5mNvl#+hnIR#Hsne zQR1dg*8OmgUfIGj(nOEGD!EfP82_Hz9nRC3RMF7-{SX zL&%-dTB02mi?M9~0hRBGd4>oTO!~88jQ2Dqm_5j90;Iu8$xo33Mgu|ksEk=TZ2J1F zk8j6TW<)){tezJzT}3_sX8dN>8a+? zfvCI4aI`b$4!8Ly9Yw~q=||xkyd5h891U;{96`enC&;W9PL2ca8Ff^0Y0yO#Qc8Bl z9e8E{Cm)%$_gKL#6EQ&CFY}SDROJ6fUtcPkec0nD@2H;S{Enj6T`SrkyQIPKvd5cy zN(>*S#rCn;wF3FbADLL=4}3WqvkNFs)m58~mNq>=kL{jbbsn=R;ZB8zCX;Bgo5+9c zOOqp;@$4Z5^wjr<#&_3ncvXYk>1156BzV%RlwaeD^`fEwA#5n!($Mn)IC{osLS-09 zrrWqMwBb^L3|X|?JmHS{nOj`!nse`N{UC*GndhOC%ReB` z$BjdvBW1W2Ww-0gYq^z9|0AFQ?$$dfy3gIG*8^boUjC6+*^P6Lg1H;7NZa!v7Q1?& zg5^snglF)|s(8Naf<#YzjaJShz`t?sQ&aiP2Sb!?&UJEYS3#$-ongS5O^x$xH+Xfz zL4++9A1bp#wTHfUHMON|chnB>&}6y!rO;P~3z02jGv~{113N$*b=ocDPrVbf@0PA8 z;`MhyPK60$qTtlfA|)dtXzwIuyGBIA|Gn4G1X5k|fBIe=?-dm+)M2zf@dWRAOf@fM zS0}_qejyK+glEa~L8;FCm@d(M4EANUvU}MpUhmk$&TSJTq!{yJq1!;!K#fX+R=%ui zKb!CWjl|#4{P7dxkAzKHNi5^{cxsRMvcsg!Vqpc|QBp{t+VM|>)Nc+reHd{gGg&+K zC{p7aIziZ8zDAv-EO$D_)?}@W({DO5$sO6cM?((stX)i_K#TBW28Cjgx&s>fQyr&C z%SOt4SEASizEku@NyRw+(^*tGeXRKJB9e-F>~h7iF*X3$H@x&ic0SUG6@@+8ljy6m zT%*3n9O4yv7JqoHUkAHaFTS7Klex|##C$a_N_an)0tToYuU#}Qd=!Q~^@|;;ce+h>&97xnGmYu2XK7xvFuukq?kRx-aUPWF5L~HDN zZZZT}cWuQznxjD^7L-#8THslkSk3S0Y=wL!-Ct5Y&#un1*KN$Mx9Ai@nVzYFYRS2; zunEjj43ZB1U#_U4WE_6QGRoe1A)&0kjZb3D%zV)bHlK_gpK5!8O4ypLMqvjihRIx_ z_;If!P$f7KPTPIkvcXwo7R_4AiB4=M$a7#qBO#-FGrIORPyK&vB9HJD(9D`;#Ou}D zx!~9-TbU@tq;TbiIc###0cAa@a4FW z&K>N`lq`5=5b*=vtL8U?P_Yl<{cVBO_BBk3R4G02ohu3GCiuj`Jho`?Xy}tN3_8h4Xmq z=u|lotfqURbI_b0%R_O2p@fHSLr=`KJv(Ant5K@!D-&WnSz^bAL#}Z-!x(J*eL>qe zF0-vj(!`!5+=M9ft$$CbOg$hLFSd?%kKV%zd3*E6(AWu!C8lL~m+yP8uRx|leO<1< z(-dAqgr`W;!-QLZ;i(b*Yg?EtYvQnPE=apgm3GUP3w=_+JGK)^E85tNVI`0`sCVM! zh!TD^J~pJ&IedG8A}Tw#fZmL|7e!j(1PkKB9)HOEFLg-EiBE#FOQkg;*a{T?5O1-! zwoQl5K;_b!X#j=7Ad_^MT@ilWw&HF*lK8#uIc7W<6<5Ao#*27e>D~zslmbmYDXJKs z=iL`g>k0P$j zF~>>$?=*tP+6M#_yJZ#~m6?Yf$M0{$^2+p%S%f@mj}{*9FLcXKh%QbfsC%_}y!9~e z=ZDs(orN#2*w5N)x7+52)I2JiiG-KX$e+g<%ROj&$p=~^vvs@cvd$4A39bY!vHxw^ zzepaQOKvUIDx#!`IxN?2RojGAM@n2O^>`e%B%Bj+)(?~rVbk1HsVih%TQjk(F zbdYGv722@P4SQ+dA&bh^>=}_2Omm;s9w(Gs#lb_q<3@+-ZzzmKxfyt>y}O;b$^x<` z0C~<^1PUnIL@KNNmoXk*2@(C^ALSAV+3Og#E;bhob-o5IjNGc)pZXRZ=F$!F%=cDj zQVazet;A4YgvHJ&QF@%%J|UR@b0cgWQqoV8t@g2f0nSr{cZTt%?`R zTsnUP7AmOefh;|uHD*1x!G43%FzJh2BSNgzY!#5xkju38GJr;MBH=(8aAY*t##+Q1 znlbafLi(yF9Z?~*Gck0!xzWt_ipMXTm1HUV8dpO#HVnE3za%8+bU7ulJ%Tk>SK>b` zLPPDd4?B9`R=a4u$2D#{{lSt4doax5`M5(or+3BR&}PiHBreg>u#v-!0g$lUC3cRN zx#I1I@1j4v*0w=fI7b7h9BTMP+e*eryy@lRg?F`(qRSQIv?PsMo-4s(;=9}3ebBKJ z_V@<4eBl$yBpDZkTY=1%T3nXH0#KucmasZmUvi6OQ~`W?Q|?v*5qs)Zl3ZX1_#bvj zX%diS6>IHlqa`Sqva&6n95aZ5TiIk#pq(gI0fTmB%#`u`ZELE99ij(r+J4r+2 zBjKg{S^O?W*KN3sRx zYVkKI%F9`)AK-`elEC;hRs(qQ`^V~{tL-~D6Zvzq1gKGP*$p)@+?boDH@zg;Zhne4 z9nC7~Ul{wxpzq72Sy@+1ic6zhVNOrw>)w^J8q$1t3ct&~8|tKHHJk_CrIujhd(?bg&^1sSc7bEHA&7#5H9g{kajCBRsFgI068TwKQg`x$pZ) zSNO%P0!Ml~5pKP!wlY6+Lu%m3Vv_QRjN6tl7T2A%`6v{ldHvY2Ghj_5Ik&4C-K;2E z+}#4|2K#Q>CI^-OJz;?^f;OzMQ7jOU!Y47wKr|zkt)vk6$g)!6c4SF*Xjy&H81t5J zK}e}Iy|YWU%|cp6^0Bd6%8&wc_o=x(WTWG2{TQUTUJ)sZ?hiZOlX4-#k!HxN@-^_r zR^&JuHVlE+I&SKP9{PBzbI8Bc|KD;{s=9ICA&c6X?}X!-KIrtP$uJtd#=PyrrcZT~6cEZ*KMR!Uldr|0yatB)& z&L(T`B~ELP$x&F=a^2U&R$47tiB1uWDlV%K?N9?G&bQen#z5z4vR8N(P<2y#D*-7h z$K-v|@eLm7RDVmdCbsLr$LL0&%p_?$q{K_df4Z>1W>1sZlA~}wEvJX1Ul~^ZOhqG= zksCKQ-btE;YT;ouji8De+E)rI)%1bB% zJD_xKg;@ze?d;9$ZYu`bs`4yMU08-;X5J*o%GLoO>7-+Co?1|#c5ig`#RF80h%ohf zE`@E}pn^0m+dYL~)1)szIND~eOyVaH>Tj=Ik-zp~*0vvpDjUbss zH119wvg*7YLpBReL^(cEZfu0;IcU{{JAj%RJfZO9LvoKsw5_rfA8&0 zPL)UVci`XjSyC|`OJPl%C#+aH(FUzaRQu&N@;k9HkyKF7;Y{o9<@lbbx^MR1hQO2^ zt!5+f=SdQx01ywX0Fb(aS8IU@~2yS$Us8oyhrs@;;vkQdD{^(f{ zCG+sU^N+fWuo8>503%9b!i$e*T>F#-f^%%E<}yP#S})Konc`khqaPB@1PrvJLcCsu z!b2RkTjwTlLX8!eI%*>;!@hl$5l+~ajqNAZjV#gmH)iC88`rf!HQDy+DTDnfP-%mXJf zG~AA+e!41p5VyE!vfx;qX()4%;w%r{$G#hC1Ca`hmc~3CmmP*lEl=;}1@b3zTm8&n zDw8JZrP=SOhDxk47go4)VY6jiY?m4@q@7FTptR(*kml>he64!lV6pQC6*cJ2Be1z;8eMtgd1ZYR!ZTt16@Y+> z(fK@xnb+8(e7=wX3g_@&kXfQZe?T%(s8=6_*n~&S3`AGbY&p!?jz8JYrEO^hkkm7K z7qU?e%#{Yk9w?up+;|Cpw@*eQ|i6b)1<4!d4*|QzB%|q-V%@ zl2#ew-Q7%g-K@$#$+x=Lt~5ui79q6f=wDzJCL#ukMGj|HdT84vLXQ!x-9az___%IL z9uz>_EphpH>qkB~=${oj6*>G0b1qg0ml-B2wcFh&qb<(up=JtpPAhz3p3*4pG<;7H z8CzXDR$%|Ia!LKP!f57qPwwo9gf2eyij<*|_(STrXomSJkX7Xh~b<2aCBK$%_)Livi{A(1yW$?oU z%4D8^3#~GeA>B0FD!q?kZ@(H1cm|iaug%6}1WBU@5sKF6nicqde1Kau5G^&u4Z@e& z9w%Qqhjkcc{j-usvxv@$++x8EjTROJOPYA6$gYrAG|6wlyyDL#AfHcS^M`L6N&SQGbh_XX;6hs~Ly*Q&- zwXs-1n0Y^rn|>+vxl?d+Z{dDM=vkaCD-&j4#e377>8RhrIk*x5w^@J+L1Rpzf>V+e zOC#)z(6``16{s$rb|-obU~^#Sh`kQJ3(v^7IoBwfv8b8UVbHSah7NK(tqUu%5}%V7hRFd=M7i|*o;otNeC2}tZO;Ib^r%lg zIK6R~etj4&qNd!z!mDrtSSqI>yI$MxPhWF7`}R?oUXK#D!K~1W%zoY;M~8DZK9CD0 zueD-bZ5G9yI(lRwvPE7+;{-f^B&y7VCnbYCH$&)u9bG%PkVCbA<}5}Hy7ud*Jo`c>P+P=r zOw;wGfS@x0Y>~9*L(UqqVMT2;xlWSoY&BNpezyRXbisWW$tkp-+kEaAAA7F&dZfLx zAzdZbz(L%0-j`}YV6vzXQ65O)fKh1hD{G38d22t!xxI68&lQ!`V?-i2W$oVhyzsv0 z=qQ=nYG_8mj0lQzm+Xgid4}ufWW=76L^=E-?HPv>FZXmaOsj%E|F-K*LzHnCZ%ZP8 zca(ic%wSR7Zs(4S*WPNLyj-kT-1zhMUBQuZk9$8HL*j*^3>cwHHo3?KS2g;>xh~jp zfav1O-a7b7%cEte(5aT>-dkiV%BEUMEJhHXXj2UgUK8Qhf@KZr-^xL(sZUkhzNlqZ zvo?9Rqc*=g$^C^AU9pc}NzxELp6pcscu<2@%|>}|H)w+F;hCG&qC6`dG{yoIFpYa_>s)19 z%^`Cn<&+WfjdPcM!yeZ4)TDvBD)z;*k`6+A-cY*z?8Xw+E2gWrBMhMqgZI$ljPK^c zBL=BQu*qt({m%C{|nKWzv4Gg$U1X%;PgIA$Yxb8?VkyCApWSHEnE%^T~! zIM@^}xL+HbQ)J{X&-LlW#k^ij3)zBJqnP^8PQm63Bl~L3k9l;a;H_4A$F^7p7+yqZ5LNVORo z^?U!4egOMW@Q`ORTe?kv$gHYOl@YpA&%5{57J4uAsyj|oH=%%eQ6$C%$?lPma@H5S zI22k&##-%k_vT#r|+nZ;BxVW=VFYp?#4gX|0cJ29ci6W@)W*c4Fn zoQfT_Lvt8ztK8^9Js`2V#Y|J`MMBvSRWi)_&ULReu(r!yYWw{rQe}DEJuGJ1X2Rfi zD+&?p6TT4wetGrB>|;JRrJf^VPl)<+8uIc>mFrHr8h1i8e`Zlwhz(^4eb7|!PX-$i zR~NT^!WFO9>m^|_NSyTy$0x8}V-sl$2|}kdLDfo@wmKG4&SL?N31yiYr%#Z;X|{~6 zHh%&Pb*<~P(9Cd^wNQ}r%#Klg=>p=ILV)$m923qkstq{M_w8&CV4vGHdZAPHnOLPZGT%2grR5x>UPYGTXn53a|lo(US(I}dH zthNoK)UuSP_Gnzc*Om&8U8A}B+4RO4?1fXf93GF2ui&7f0z%jR>ZI}9@_FH{uK(s$ z7jsp#>~Id`9MurhH!-;ghiQVMyhkk5AsYu0*6v&OqdFGf;N*zNhMu;%>S``aB)I0c z<`eX75fQJyC>CvFlg#=M-vhgASWuW7NP&%`7YxRiCB4E6il&1c#z=NKD7>pC&oLy- z1#1=0BN&6#LVQ=F72qkL1f5U9JiJ!f}DBv z7Nl+7%;kMv%*vKN5(-Pwt}btrnF^E14TX#WQd^3Jdrh79OFT3K##d=AH^ObzT5 zYonf7tVPfK{UFN)HI3PpD@=eIwAQ9{_lo64T?=r*G3Zyy;gZXA%?y?;bqkHHE2??; z2WrNNUuik%9=NS--?7%;iOs2*sQISEGO-F3a)#s2Kj8Bf$kTd9?`2CH?y5FON|M1^ zFkUn&(`yK7S2oC>6cD1a_eL6g5%ENa&~Q`zM0@LYe3O#Y$pLidT><&CmlIH7rxQ~K zd{uF4;zvN0n)u8kvE45TM{}|CzN@F#eu$80Wm%W9hj_qBo}0`saHu9dR9^FE$OH8sCv71)woJrmj+=)$tcHnV;FK!k zNF9p!t1>k3$^8-WkP<|A`w(}2(U)V>y8F7aN@h>aRT1`qo*+4Q4@{4TjeQ5 z8z-efO;yC1-dmyEMZ2p0?>9$ZS3S)8lQ=dl;!$)@1z#F2(xp$JAGhAgR+p2u)J%mV z)et4vgvlnj+!sKZG)0TGpiz9Gir0`CUIM4UrEGwFu}*sz9aCX1ej?uF-<&p;l>_gz zVoCa-!NH?BMKHHn<4X;XzQp@Nr$=luI9o&qM;iWA7#>G`N0}`R--aslNAGV7_ISYj zEUDU^U@{`c$RoqOswIYi(MIcY2YX@7hN*Z$WfsdneBmqca!6#uoj=L|#-;f3pS|rx z`Om~T*wD6O_h@oqt8mU@Jy}Di@e3|#2*O2~_gHvs zmb*8!=Y$GBc(Ex#LDe!r0WWj){%f#a1NP&et zXlu^*?qI@+HvI^IG2&UG)x(TXd~(l1B5iK+tbDaJV;+5Q^aLe=XPRe}iFT_-SlsAd zTt&QtPW)hV^6(_YJQ5y?(ArB5yzbFIiwq1C73~;@ieDqOlC(M3C8q@#jG2OF-{ib3 z|D9Cw-0Dlr(yxO^l>PoZ{}#>C>@;F5QW}qE#pM#*vs+$=L0|dnmeY@JOCJ?0Q3#o> zt4iK_+(z6W_w99?&a+)0U)wKONrg5ziv&Zuslf$2kH_VG1*49}orXd(Jb3O;$gyS}}( zwquc0Xw&tJE+?o;o%K(Nj86pXy=?fpp$ES4JlQ6*7W+kw?CNOTUYea&*|fOF!D{db z|CieQU%t(2%LS`NYKR(3n5YdCuDDR&gllsG#F$)Kt7mgDvib zBd-hV>;p^N1(w=B;!N*~``~M<=YKQ}eOm7cbT>BQm8v1WTMRoto|XMjHTRvw!_N>* zszB~FoUk9v>E}ATko(HGPy}c!{VUfD?<7v}BvL{ACO;J0$&Cg3#O z(G-iWpNYt{Lk~mWFZ7d%@b^7#&7(ytU3x`&W9>iXtc+;dPXu->H6aKT~ys9WP3EAECuG?xb z8IzWxG1EnHZmrRleTInOZ(Cp)UjEjSOjB~X$Y9h7e{4HcE-kRL2zm+qj z*?p`E$d@PS)TleQ^mI`v33fl=l@?xonaddi&g;wJvrl=@5b#~^V&{n8O-6N30O>7T zyV!|!>d(3;Sq|%hr5z=%I&aUK$yTl^sN_|5TK8EIbbncuSI_(D2FB^aCB1!(S)x-`2_wzeI->&|7G-S;9*o zM$heeBIv6i8Bk9$&T`~l^G)}*FQ|P6cG;C!qKkg(Hq64LqKp0Fyl9m*#_8qu{@QOB>bqwPDPK9O3$%rcaocAoJ=9>>cnyS#3-V%9p z{TeJW#ENLAOmM?-<1^hf`1o^&={_JYal-vlZp@$wd&a*jQ1?@YmLrii~f$>5Lk z@l8LjlkV9=@D;z2Y?TeXjfiBneInWc=w$jE%@v%huhg79pD^3njFnY?m4C9l5+q8G zOKs`RZT6osQm`XM%`-#~lbC8rj~rOe3?#_Fe^oZSueI)5F9to};Fxk6cBASl`0J$M%BWnL|8SM+040!{I%VfSQs~J1gzs z|IJ?Y=D*u3TPzABj@bwMG&J@6ll>+>bXHIJXX_6QrFQ6R^S7N!cFKs}eL7wD^^ud7dwWf+#DK?73E0^$OZ#Vz4^a>DL#)Bm44Z!v3%SKaThD5`^{W zT8oS*Y?$osNuGRPGSUWN7J5^ALmrIlLy428Ez=U+h^2Re` zDQOBBo6d!9PP7S`$0kXKmb6pw^@Gj;_qxNU!QMXKIEixk2E(>qQJOj53kb`oSEkHK zRZMGc&yC=Jo7r&FaHCld`BRQ|Z^Y2{mB)SN-m9{~PK-qvmj3d+VfY^BHfAGHlH@Qy zCAD&uF%R{NA~Byj#=Pr;M#@W&M~Y0wqj$j}o&d3U-q@F-B$hy}#}mK{2Wxw795a*O zJ)qzIE+|_05Aodg#JJB}G4TY7csW{*K)~xp3?QwLGhDo-{}uid%`>vH*zu3;qOum$ zK7IY6v|EBJ?V=m~v+Z;B(z153XZH}eI1uhhyHuYuqf|hWKDAtEh}e`s_5c<3{^!8! zcwnb7VE^lQ&tLg8?$(vB_HU;g*9y{PK8RxSy)`W{Y7!IH!gq&_nHniyIcPjcmyW>m zGzKw$iEj3KOh-pc+mfS_c+ zVl7n|;9JnCA-k9kM~1oWXPB`v@3kQ;w(cNda zsi^R?6D(K$N8{)Z91~zCu<4mc*?UWl2&Co|D7Beqx~!H8M|%mlRxA$=ujhJOgb-Mf zI?=GhdJVi7#R}msV)WVYH7Y!PJnOciM63uUc>cy)_Y@6ZEs~v~q{^WRQH+G6aRs3e z$a!txbv5^UEl(DbLrkcco!^?XGDCHK?T`XJp51K8iI$}XBWp|e}W^&2}YN!JiA+f2rRi+tpycW zE#;_>S(tEFiwIj{Xun~!Q!{#v9$Y0p`lu0hgLz*s#R2En|Lpr~;weuIHcBZ!|GzTH zRRX^1^-|4c^A;vch9sfSh?8$Cd23||K2no)C)S(*I{pqkmtqX5~}N0T(G--D@T-xS@2)g(w`dh;V# zLVUHI#j60k4J_-lT=1-M6cLHGxLs4tYKM1nAkc& zJQ!gTCS-||+7t}S72TC%R%BOHE7f5Ivxy4IJzEJ(du96Xt>9ue4l1SwyIXB!IGdPV zWe(M#6}F~}qh&^~N-We^cQ4RcaI;d3BGjMIiv9dWQgTL85fw806V1glquKL%M-RgJ z@9)gI*MuW*Y1~02n$=%%_i7$_g~V#@;xuP*z#0U1R)bW zNL&3qwzs5bfr*+PTSe{Y9x!v;C?U!JIV^J7Qi#st!FGt8}gg-x`iDFgU?Wo1$#yP$? zei6nmh2#HUxRtLl_egxkiRv?RiN$65M&q1rnnJ4un~fodP_YLmb2SG0E7k`?vSkV% zL%wZu+88 z9l|5z{|*)vl}i}HQ2Xh3W<}!#-+UhT8W3CNL8728zd83jZMS|pa)PS*ZqPGP8y@F4 z75BBMsDO~JrAWJji#y{t2@Hb(!)#|-=*05m83}5PNs2JkV{=sN3J4=a!eZvOGo8qFheJU{<{9OeE02>b51CbPBMQB+WbSVmD=P*ki$L_k_1 zf?z{HMMO#{3etN|5fLI%A}Z3Gh=6qI1PC3Z_ZC_RJ&@2-Ajy51IcH|Rd(Qpt|NMaL z{gk!Vv-Xp#Jr7ImL`WG*{rHZ3{^0fWKhv6;%fDY5)mz*)&$eU#?D)eKImmjkQb^7N zikK)lHgroZ=EBU+&igTKhXvOVSKZvz9}?aQAne8#E_+UbNplJ91^Izg4x7puIa)MY zG}hEOGC0^ga~E;7+I?m~lK-pmIgOjQ=YQqt{^tnYcm9kCZ?1r$r|FF|u9xVk3;shb zR^njuR~jESs-vOEpL)6WwGvBv!R7R@VQu(+Z)sK9J54!0@A9-T$k)io#OPmjofrS7 z*=PslRDC{_TNo@R`wE3Uc(>K8fs7S%K15@4wYRv1)FwN z7hSm3zHk@O+b75-fsP z-Q

o*s+K&>$EY0V^yu+w6$a&L5+cJfD|lG-Pi)^)_ebN_2eGa>&zFz%?D>9MCG z=cUQbB6xaxHsQsve7;>re67PimqBTsz!8i4ORZhsilOO-iU!ub-jp3xsli@~H{A$p zw7TO8k0N9z8y<7?ZdbK1^E6Y_wDfX zd7i=HJv;1ki>K&EAf!(^d&b5r3-+>IR(1~Hp&Q0?7c#R|0$hUYYp|}PJzq8oB4!>{ z0!FDnrAl{*j<(2)&)Zq?*DR+Xzmn$pG~N47&0&U6QkcH32tKc8s_GAe#mr?(ZT&=9 zHu6ZNjaC~WhG)AYCHMZISUmnaZ3`%7AI`}E#q7U#)wn&jD;pzg?Y#9ybCVGc6Sa4Q z7$-Df^lu8iw=>(biP&9=8<8vZ5=@nbT=#6baoc>8+92q2x&J+~P58JPOZl~?)}-Ac z{cLT{88;+e+0YySW?r*%y#8QApTB~e7uM~jMewovsSmpZI%-Zj+ab9FQU)u`oeEp> z?(g`HV?%wO3B{u@)%)a!BmG>K(^ns1qT0NB^TM#?iO2PMQdlSVP&Qj>nIy2TXp67# zVc5WhI_4nuD(%XFIUdi-e}qA_BNulfeL7?DACWG`z=!;M7r?*dxgL$Gs91Lc?P%S#t~0(%T2az21h>y!hxCpib(kT}DZH9q;qkJv zq*D-AL)94|5o{A0G~kMl`SYSuS5)9OQ)}d0IpgQi^g}+ICnNTuD;|Wt2F}7fqxBzW z@$b)n7x?^LO&FP&jaTN<34J~LM7Z5zlQ6-MUfJ#__tt*?lU`D!pxS02@WcQ2!1r$0 zv6R>dzDh7hiqoGI^2hc927PZ_`!A%z`E4Gbjkmek^O*n#!H)>*4n=*l z?~Dpi&P-RmObceCs1i(x)g^L5lfb_1Q}lDt;N^Qp+E2W5YJvCXRad(NfV6+F_v^o; z{fF0AD&yTYhmw5x8y9VcrTI{~;-OGORm?C?%yMoK!!()S*$QyPu_9y0J96T|fj>#u zkEjO+vyS}p5!|0V_QC#L2DP~%h#SV~QCK(r26rP&?05S|K{=k<0$;`=q{2&|x}-C_ zF-2~Y+-`O*(YQAFCgT*qNlunUT>k2=?EGtd;Q|0+aNkGP^UB_tKNfjW_YI!pl=Jg8 zoYZ&@TGtid-t`n2Yj3FM#qJ;{Y}ZL_3NjLFO|R0gLvw0*a>9lC!!y+@Ut4~K0KNV9 zoR9zIe>}Cz0HI9E%pZAQd9`()^5FTc1CBQ;`lMu;kz_wUcpCQGHCN$vqM)?-d6$r= zJOlf@+Xr>T;c8M+Dl(t`AE5ZuYr(yZeFMfqj+?; zt=z%v^Lyd&^+JUy)5oHVI@#}QtBrpZ3`NrGlxMOmm}VB@Ca)L-g2Z>zgb1)dYMVa0sM!d7aGa~?}$Lgx2l(Se#SH?arVO6gZGo0?d{%a)Q-cH8Nck| z<4ppurlF4pww%NyIxzYnh+U0p@F$<+&l{ip&-dezw{{mTzgLGIlwI9BBsW)$LD|lz zl0>ZqZ0AVB%+v8sA9{AhWT6LT5Tyaw@H~JV-E;YA1Hii$y`le1s4wxqS%ZY_LJ-Tm zY~8j!&dl53g{9%>IzqHy6d}dsS(i1nXEFg6RoXY|d?0zPLEEbU3!G)TL^%St>9Kf(| z%vpQ)tYg$GBXqT<^1Q@LQ*m(M1N)~s-p;n+fBHrKxvjxOVgFzad7wkd zC24LE%Nt|YTYD@q4AOenS5SfZ^~vum$=lZZn9fkSCoQa@VZ{F?<1Y5`yXk`}Fnqe`!jG+w+O2w~iaQ zdyAa={@6@jQwFI2nj1^$;LzLsWwQ8-jORe(Xnis8d8nzezc?-DeK}xMjb6N(RxB;F zTrM@-qYh=7d=C~@k=67mI3;>bkQL9=;`9PtK#RmXyHVu$Wz5&xIfa~l{Qbaeyqi}p z`K9w7ARL4ZUj7#q=^+pf%tGn*cmC#cD=(m}D%tU^m<%4Nz!1I0eU%fcDN&{xB75Ci zPm~t!50nvXs6vyoir!29sWhiqT>I|~3Xp29keV~H-^(1Hvw^eA-IoLk-P*n|Wix$! z^Hdswz@56c9E!?9ukAJ$z5KE3#eKtCXKjul|GWiuN9UW0&zl?GDEYH3lrBZDriCD0 z{j4=188wl4Ya;#H311E86N}lOsmpENpH&}Bf(04;3O5!T)uHVw`b&G|V{2Qjr?|p@ z@dKUUzXtAi?CUG#s0}h4blXUllotlFp?5`G10*`V+al&(DZxaAI8zP2x6+H5fzRao z03>}@eyGs;-2a6y{?$|OYgP4MRkxd}?GsC2-E(t~3r||Fjc#3{N+mMK8#UM}+st*rfnizE>&~X#RkTu)ZOLsqh=UB()Y) z8OiWZT_Oq>_}`eyT)nnwrAV1Go)KaUmJe)IxZr(hK|Yw51#e?>)5Ac&pHl>``7->! zY>fa60Khee=OC}3lq5GIUHFh)#>lslF)VGNB-@|CkPYnH&=_CZD8Uh`>Mqy{Xy zB@bO7*8UVW0(;S&MoQSeOp({DiK8Mci`lCEPM2~J4m3Px z3r`Wi)5EvMK=!-|{8HI2KP;|Eer*10IQrKx;%e5OXZcl(%9S>U{^(_s45Swr$eQ6Y zi~9)E+VhOg;}+%g>Jy8P4GauUr=m(_Tu(@${{|3+JEfzr_&e(A@h9Zr)gMz@%9by3 zoy+)T;S(t!gYgmzX%SLlF}$;f+q-$}0#gQ=DQSM9QN6OW7lY6h9ZYYLUG;*+OYyMx zmXgcxTk4gUUiB-T+UXrUnce?|A79yJ;L2Lnt2`33JQ&QaC3l)Lm^@=_>Q6ARCQ5RD^ad(WjP1zZlrgP~3}g*O0Le z90p|667N{1$bPF;0^25HbvT`T10I;lnPWr+_G$#aM0}iWPeYF1i4em2w0@zS^;x1& zTvHWG@$8KpN}747OYsPEG&aL$dDy13mLHXZ3vxK8W;Z`kJ2r{(-s(uEeiw3>+C+7U z+7s+VNo*qc>3eUUCS-m1z}qzxvBk32w)XN9>5~D`W`8r9e-})Pz}H!(hECJrL}Xt^DXaB>b{iiM-`FwZL z9CNxy%VWD_q(HmP0-7;O&|Y9_=k~dAsEPJ^D`(~*s~0nh z)YFe5$hO0_yR=3qJ{3x`eOa>!0ko8k*x${et9hSP{fwWSASO40$wgzg=W`+vUvxtzn!>$bai7%9Y4Qh=I*0PKd2i)C$%vt9Nfb!e~5U^%yi(U*7ML+Fg2J_$%>A8F;B@IKAn)f|8mEhQuxQKR4gVt(^a2FxuWR zdTIE4W1zLLO|! ztfkg_m^gTAuu*t>MMg83Ec2izs?qI>GHs+M&b9kiEmfJ~1j6j=*|GMMhHrVDdtegH zvDKt^hyPp$D^A7i&ERNm`9+VLD+o-0JO@MsAEJ{KD#Di(&eQ$B4fT(A&O8)7z&6_T3@C~+ovLEWEl zNB#>OIpk}<$F}QO2%_?<(EGB>+NmLkrs&~BRcUx+#tf`iEmFwEUS#(+uU+KQgrLiU zbh~;bU^pRE09%c4IdNXuiffC!BX%%$y0ZEQDy|th!aaukOtAfgQ75=~paXF;WkR*g z4_q`ipQV*@F;XUW)YNjEhfbK|B_57}j>I9lxvdrG_JvD5q)(f#$~mm3ZnMaxCz7K= zh<LQjc(`Q6QelwLA4~7S$hAdupn!5 z)C2BOy;q>514(hQk1%qN`axcB=6mcmAtf^7f!~TVKyLJnCa~92pEr;~727KoUqUkG z3nCxx?qOTnD5TFn`J3}!_1W{RvFaB97zdr<+x3lyV(4O8lIQ5>WXS{xRU$X3yz}u1 zqk@wnahZH&u12fLz}A%2&)X>p$Q?ULn9sX6Br)+>EfLG9eCM+7At4B*T^qIqwZjYd zY!>`-u>0G*A~QT?Nw<-GO#cY0q%yYxAPB$bL6_r`@_6gs@AdBq<{{BDz0tKDEudkG$R_?>!YfX?IZw?l4vZpHuj zAdJ*BJ*lxD8YP`{Znuz)xGPc6C2iL!@C#012)ahFF->dAJif@Qc4NRT0XAJX=icfA zoePB#kD9X5H0)=fY@+|Zd=^MyB#vE}pA2~`fL~@1Nsgz+nWI!3M# zszlOV26AB9dweC-qr{J2A+5u0OpsAYOgNqgRBlDYLFoWN2GgGL@j@t!?Mds7s4Q^c zgvgBz8u=_0BE2ts530JnJrpfzrV#S?IHpx{mqBsK;`>Y9ZinDOCULd>s%fhCjXvuX zU){lquIF7Iy19!0jd&O5-dvlyfStOAgpHr1QN(r{q%B41EPCED;SWSD`ZEnkVR`hz zjUB-023Q{mfX)XFHmwzL>m)$t=4+$!#-s)TaHEV|d~nkL1wGLUvVcJ08Z@Xio56AJ z&OTe}(8cO%WY3VIu03kA9T14}ezgS|eU2XqKmVsa@1H}_*F3J`Kw*D1RjV+!-MU+5 z)@03UN$?ssDL2&XVw6HJ><#$$zFe8ZHkb!7St*GPtDc;$ufjonO&AuiZs$5Cb+viUG9;u~(Y`4agaKDto7(#T`9&xpto-*1jvDsqvCb-eX~;k-Fyi4$=j z^u6yfY?WFX=P@RqovQK*7hrXRbf*3FzLxutit90g;ILrqz5Yn!-TBLwHs15|FaD1B zs|I^)D<->JAfOFi11{4nKbKs_{V9by0YS#i8m9ix@Xu?XQE#`E4+lZx3L~Z}m0D*l zh0J?HCa+3`?0*?+y_25{gnY-6XWB;EGWY(tR^Od#jWUxYOaj0+IKS*kXqyN0_-)fc zzjp2(i&=Va#SdRl2m-Mx$^xXM^O(}-Ky@7qvMg``T*fYO=!fWPsXgSEwtJ4_&MO!B zT?+d<3jYEY2Lbia5Widx3HW*?W~~ew4w8?=fb)V#YDT(-Gl$b>qc;v`o~nIl6pOU; z=PFCFo>E-G!zN{iCr%O1!&kIMd{WS>qK!Hl<^9Sov#9~oG&EhD_SsS?;k%<|L;~%s zM~BZ?tr2pGk9YxL+k|5dJCqvH(qLJ<;EW*bK)vUdA{Mv=SLar@c0JhAQ#^-N!vvwX zKl$`kJGjmoIo>VMy7g=^`tDz# z&#$xea>GChsADnA?evMG3m3v+{LLlZv1@q>&8C}>rlT5X4U7cjr+#{w)x?{)ocX>8 z&|BLh7c2w*NXDmmiGL&`urIAgVer8N195Ql{M8byQ9iMK=AZ&qOUPo&-_UT)+$A{& zCSa#xY36}ykha#EBFTJTGrPV@g!H(d=EqB;xE8eyy<*1Bae4$PUdEGbVH z@~B*k8)bs@8?zSC`6A%Mt3mMXW+aunxE?Dl+UP3)Tyd!MCCuNS>8~pez6D${jN=cf ztC3!>1^(9MQxcObfq^vSHyaz?P(r zwdIWYB~uo_e_Rq2OKE!wIZ7*^C?bt)%vN{0L@ChYC3@wHMrN!i9cae=<$4VYijfZv z@`^9-v8QmO(4hj9$_$^S40!v?NQaUYi$B12oKwFS1*i#k+hjxQ%?2KHt5@ni)d!HG z9{xbY>T7PLD%KpJ@2szsy^8j(6*^7{dnsiD2uhY6zpQ1Ob@aVQeYr*F#u)7$0e%zI zs)X$+iJgWVs5OTR%4wM-ebqB694Nu9OZ4x5w40e9ex&Qq*EN4osF{f0$j3`+?n~k_ zjm6U!gqN%JveG-Y9)q3FRQ9W%^wFM)T`1v~Hr{^iaP~>hEf;u!K3+}z!$$3J=((7U zWTiA1>3YYxozXxO(%iR@?Bv3}+%PPho?ygXdU4{kMQjM7=oDznF%Iem8Qj*G>E=ce zGZX|^j5_F``wG}HkCp*HDk*Kq)lOka-5^* zG-4+0QH&dvS+s-O{|6`STK|iaLN@U9n$<7|bn#ZD!?%`@MrDipw2u|;9v)>qaU*aJ zQ_rJUs_PnI!b^?b1_89Y;EzQWO9A+(A!d(8KJEK&TNAa+A zMyX8yGjYU=W(9I)&)IOq_t@VkR}K^lmvoS`ibW%$BjkN+`4ulC(23~`5vnH!N2~Aw z5A^NjW#^MIux(R;feAl3(IzbCJg!lh(j{*nE#go%e-4AjU0t`diKc#TKlg7tckb_d zY^!DLrq*u1*5*z-mZ~4TiQ&UrQ7oqtK{>vfuhTpnLGA0+2Ssd7N6u>*>7EJPILXA~ z!a!aLTbXeqq8;d>9k{r3*l)WS&&t7V;Kn^{GEto6C(7pmWY%jWsXQCRFWq1RLdPg> z6!iE7m#3jk%BPNTm%4P|>qJm<86&*zNQ=&j<&+#}0XQ>6jMHM@;p%W>N>`vM;iT-&h{YRVJsB^~F!a#>pDvG^6c;}hR39~!^h!%u7Bp^4P!1St` zMvw=~a}+oWA_XXoJbL9l4;4!x%+!a@uE4X$tObC^+BZ3oLgbjky|5BZ*`Yc+fSV-A z)*6{>8;g1tQp!|_U%R4J47I}Rw6Bi56_Zdyod>i#fK@H)0qN=8n-RDDWSnQP#! z6VM8J(46r;7R3Ya0tXb4(X8UJg?>1irH#Lp{Cr;URxbLaob=4}HVMobM3RKY9mj!W z9Z5iZ0(R~)y09o$1eVnZV@u5u9jGTs+;N=EVtlF0fML%l$|onuW35dN1YQf4xK}xO zEa%<)7<;oA2LQM8*|-CApVe!egrMRP=b)`>5NbkO`sS7WaQavWD=ub6RD*v?c$Ri} zp^jEIP|=;--;bx)LniSeX{HmyOD7s>-D&8eMX&#{`LuQ4t}W^7LX1R`fDPEtoL>R$ z^>p?RQIy`j@78B2g4+kb@3W6jUTS+2>Qs$y4r?xTdoZ;(rgq`}Y9c#Dsr%14VHNWI z?Y>Y;fSV@(yYVi?oFFtc)*Lp!s;c+A=lEg+E%YL6ag!G%W%;sJu>O5wj&oi47`JRD z2DsCQ34=zC6gkC5?KtHLc1G!A*oLXcxUTo@%JP9X$MF>vTgDKqrW=`&N<_{}OWej_ z5sOoui2}#e>2oWMWm(0;r>&tY@YFK#PzBlSEXTp>EyigEpnKFeHQM6yw??U)L;a9p z7dcuqi#ETGt+z4uctLM-iSg0)0tlbxtm>!9QyAe%=r$3A8|ScTn%PNLF=peUK8sy} zy5`qTMg&*I`%vZ|Dp(OaP8?|Pths4GbpFMk8M4JQK_z36i{?a_>2iVsNsx}^zfPST zKk|FNPOv14`+FHJvH<*6X$6Kd$m-6@0e*x~F{?~Qy}D=DzQV30z%1$b7=-Upw?nkT}*p?@|~nf z>{sUzJuzcb(2aDhdW$>5%M~kcC6rF(0*mvsrXjp`e&O2?6=gt)UM09paon`+XHkE5)LpFF4dVQ!8lMGR#}@q0|&pj_pct z^gpLu7TDj$z>bE9`4pn%dZqU}+OPRTfu4iqhhAhxeR8&H05)F??2I#7XZ_=A=DxPg z&AqVeOd)>+Zvd9>RKW6WyR!)5;0UZ-2MpLel8R+7iQ|nlhU%7o&rOQ7F;6o$w9UJ*PVXGbXI`p53*fRmQ# zKCV0R+$NhSJ8nx_j-fP)(oWHQL01rq2hH}00Hjbd4umIG5bSaOsmtLAXJ#XGGcV~x zCU&x^E6QF3^;l%@{cYJ25xUm?-|;fv6Mkchn~fuBe7D&t|>iIcQ{JZ}Rokp^)VQ%+ZH8s+HHGDkK7)5eiyTILpzSE@Z^t3@i5p(6%@7 z*_Qgh$LfD5s<+HAZ+(B)R1lix))~6SJ@k8omu9B{JbS2|8(&~w6AiK@*-q^ceiz*0 zy4D?`hH&5s5!9`DcBGh<%A0s{KmJwW@DJJsz%6qLqcD9CkTzj9Q$CxLS}qRAo>SHA zd;*$SguF-HZzT}VkT3p`^KwGmF`<+Yk3LnR}k3a?b|>XYOmPHs=z9!w@m@6qdUD`H1v@;IKB!)Gto12+G`|axotKE z^mGdSiJSsK{q*&2Vuc|Aa*P;Jv@A*~?LbQQ)RnGRT0~kTI~j@dTM9AQ8*q-y^#`jR z_DbafomK9uza-??ANxw8Owl76N>PWj9Bl;YQ@p_DJSAvHh zi>(9-*-GB#niJS=jB<%ZG3V>QD>6;g%`a5KhASgZtTo70c8Q?qM8g1J>EDM}vEv%R z#a2x0g*YbA>m;lPZtSEZ+qREC$^Rc5rK45}Z;pOfj3;wIBSx)x7dEZ0h8G$Nkmk+jn zxUr&~O`Col3h(UVUUO&fe=aO!+wbEoGT_%rrZ*JlpxB?JrWV&Eowv$Y3xPKaX>U8& z1L!i0U4hE8X+g%*(vAwG%a~PI*JF+s?F0Ez>PGZ~hud^Cu9&s8W%hgyT)z370zoq} z2+lF7Wf=Q{O2ugs_~0g$Ga*a+90xyy#PQIdO^n9p=My%D6t}Q&qYD8_EPWq3d|?}9 z2)cOLz3t3E69yYvNR89s4BF$C-bbu{z$l%8W-V4wkvX-E(M5osbm67v5yEr?+_+^6 zka}+*4qjT!O$zJ`T`C?Q%lq}gn{Y=>#A2JFvL-)hE#e&h{*K-wkK6t`a=<+)$IH{=wB*hfbJ* zJNSQmy+Zz7v3|+z6-e3C$7f*QcsM=k8qrA5dG;b z8C@G2n-7P7cQG+lzw2OVJ(n_n=T@^`<+a}psDN6pv$WiW6EOBnQe15kSADE`-E&txB{@`~`NtV8`CZq2jTXQi zu|nsohB>dA7=t~*h>_E@%%ObJ1CG^Ro2Lm+KiEvEc1pkiFxAv1$G76(O;BV#X0d0+oaxo371Uih zE|#%ws1Ga7b*9{9v&xncxnp)6$dP1lp}$XG3kUv;GaH!TO1-fOd!sbh63RCB88<@O zLd?cDsZ=)!)b?kU9yJayOq&v9XbY(1gJQA);T4R*-fH8!P5@rlKdpXy?n!xSjYh;U zZJ<hg9z5^@xT7?}aB(euV5|PAV5suuGmlOcZhwn&3sCk77Kdg;pou>> zyW*dip_PycS}$u05i`;W`<>Fj&J*)yp3RXWfTquMMLH<7+tiJf0_!}EXplqxA2^pcf!$%TFM)}P2hdk>xs#RJu}5SAT6d=L)v8`J!EUC zc=?yatGBg{L}zi!wcztayJy?Lfa6c#Qi_NTdMC&Z}y2#QCU z0=V=PS}QFF_*yoKzr}*|shD5zteEefU(QJk&HN!mPCWey2$BEDI9Y$gy7lHVUQKTm zy<-R>oN4yhc%7!{u?+a;YztDImwiVOwvw*4Q$cK9D4~hg%>@rxvaX*?yvYSG`Cv>E zJ^t)8WQw;VgY*3*#Nt|xK%852Z4curFe~AuG3RKd%kBxXsld& z^^KW`cD^}V*y!Z|UJ`m;rn7hiGT$iAVcbVtHd@2Q}; z00zX5yz?;^pm3LTC}~0_bV#ZnK^m)gy%i~fPaFM#iO}<^{%hxy$crn4PT?n++A?R zP-95wLM@h&sH*8_({4;$4#K*JlyR@0C^#kCtCqNS&HBo_BzwaU1Y~^6pK{EHV=yQL z(ejxQB?=r(uHm7-@O8m6qdYhB+e@pK^3g2Imn9qm9@-&-sH!}5Bq4Zt7RDJuyHZ1m ziv|v~gS41&wxq@Ae7cr%WBC%FDY6yEskK`h)Y!79Y^(JiP~LRcgZrqii>Up^v4JSo zW)(c5rX#3O3T$)>=GrZG!MYNdi2#q7iNCBBDJ98U3wl2I?IA!REqeOqIWM&TIR4O3 zZxcox&nG)5hQbnm+36X3y;! zq%oI7(YILBcE0P#NuNbMJX802f)Aw>u)q|D*j$qvFew0Em~hDE82gB1IeqdO^|dHG6aBeA&9*ZD{w~;AcP*Fv z@t}9X2H^GF<#Tr3A~{Gl+3yqZJx`rZ-zs}&XH*a_E8T6!cU0Uemf1=HS?50ce8X@U za|p0~PHgyj%a)spxP?CeqO`J?gWjy#BFL zeu1zzch&d#?cXPUZTFr?+qY`!ubXxUH{IRm7kETv>ioeYCrKPUoZ4$>H{QC~N@*Qi z9L=B-mM9Ag3-NL;#fpUvWw!NwBKx)ax~Q)GPZw(+9cH``zITdpihkK`4r5oEa`)V- zZj+Dwm2j?u9@94G!l*?;+1Fb(^oLCmSbzr}!mY(OrK9DC4-(9d4j{HhX>IB;dfnG< z`l7o#ln|*=Hret9)9_yMc&q9$vGZw<(lQRbNNlT z!bRpPnM@&MDkrRabLcZmN{m)w-L|?!A z#A2JiVzHyz5i&mu>|&92vjd>p%c_2^7k{e4i|t1bKB}fykFAR83>fH#?K1k-tqinX zk+eI0u0t;)UhX>qV=n@33}Q*98uVZt2recR6ze{0Wl!60m49Zp+H0OeyH4NS^X|99 zuZ;G%R&7||9*6HqVRetKV~{8OQ13DbMRb)5ka>@QO+KmPb`sut!%jzHd_(GNT=y$a zUi#q14RSv_CU`dsM|enxG!H0V`sjax$Lv(4Npa>{`;9K!Tw@`tnQ1+V7*oZ?rI_fm z$Kkyr9$SSEIn(PWXKaK9=1QAxSu~R!qLK)U#xJoYJd@%dQYBa|>jqYPd)kgvK-p8$ z5v7UEH&o37>%F75C?%4@PE%46F)JPqc?kNK+Cro14l{CbMc>}lOTCWWV-|MYP+IKM zulAWE@bueLexqYP#c>?~b{IEw)!9%@_ZIC{nc_;)QlmD{e(JWzv)m(|KecsZYz)A0 zqvRLG)jP892#~w*CxkX&H(;VJ5VKC!2qmK@CH@pqIHz2UM@7-tIX|a6MS*24G??tEU0*_{7 zyM1`RQJ89{Gk|+^y2y1hP$C)-X!A|Mbi(CHg^STm15JgWXB=7@6YT!H8?iqw%C}$b zrrGgl%I^@bNN1&v;K#D<8h@SmT%^<2kJNE%&DbbgTJX%DMCe`2&T=zNC|X+>7IlLc zfbbm-mU$bEKS#ZJJ~R#No;~QBcln4VMCIAaG-19dgW#3n>Ml0C`Uafd7#{hUHl!}3 z16kfHly~%&Xu}PoPJvBsb|=4`u1Hyy?V(H2O_)NQE~c*HUjEXJwR5Yc1Guq@a4!qd5E0;l7n3Vy8%}qB|-~*<*A} z*iCJE-mUA@O(?s4%l1GtrVU3$pI;bhw%|@-`HP=z9cTA{xyHS#xbLY!n_Q5s%}>z5 zuidqDE+@^?FS+Bj8;<_+R5+n>Z;$TEXXh(XvUbnuTnBc)(b0Nt1HN9-ph|78p4Jq1aJZmqpsEORU-OCz5LmM)u?RU0Q6}(ay$^NeqMliZkVD>MT_x3nloGE3 zefRmXZrABSa$^)e_=J9ns(B6F=0VQc;~Cx9iuU37;W76t89f>CMb$6G29L&+X`0s| zc!d-ZR13|he@N^h7o&l~bK_NVR$K5%xNcimO4nlKUEwg$_!7WHOk=X?+{N2z>t!I= zcCNlrF?yz7C85)ZBmm+o1MsK!V7-ob3S z@i=lOgCK_G7p#psiP1F`utl+uLyYCCB9!{u_7xjXX`SB-+yWzynrWYw8+HA3f8&zf zg|P1e^WL}5J%}`s9dT5(Qvc#9mg7MkqwKxrEDF73F8)25U0m8^WCMjG|OxERbKRM zOvZvu8%Cdct)4((S)^0;BbD4BIgs+|H+i-eCMvEdL&+c4h{8lk#4}UL<6aoknUds- zTGf>b48AqO(x$7oS1mVDK2_8qz_uVe?ML&#_i|H8;V!ZhiOL`NR(GG<^=OgLn-TAN z!^I{7C(<20yzFkelv6(W=;dkUJ2c9u(T|gHW4E`eHvCOazwzL)(72mpT+!+^8)sQG zXcm-apK5caYUa~x*G^jZeTW+z%70_x_0_A`{VD|tjHyojB{ zi}uW4yZhmYM(VwXKR32+KByhPj(TvjN@SoOvDP*l(`QSfR& zCtQgWe{-h3eb<1oj~yM@sZ4a(4eKP17ZL;utn}2pm7QCA(_?=#F+$;96zHtkx$b`1 zG!pKl@u9)Mo0yU|ru<>X1ntLt^=P(D=IO^L>NJeeEwK*(PpnBH682RB#I05BW%(Y5)gMSLJ{Y;ZGLn?gt!wfBV8pQT01Sm=M)ZO{^Kz*3> z{3mlE19%>@aCY_7_t%jpv-x;L5AIbt3JSj+nDA8oeO;gvZt-W(Nkw|kP{HrL+%P>V3zpC zg}7kVlSkN3L=@KT>^y91LJ>cq7nTm|A@q(59-+T&;>Iwb0*NhYL~yjw=uArs7FdT>ki z(6ZJxA*;`ih87bG@R>X2ibbGTCJ8EzjM}2rszVLE_Pb92W1FR##1~iaIBF$M<))5% zdH1H*zCqVcffK(472&Z(`YO&`LwviBoZdZ!Ra(6hqjGEM?a_y6r%wl7h<~TC@aUU% z`dRwv)va;PnDVX=BQBw1-(Z!b&?4~TsroTA0{M>+X*#+~ z#BpyJ;NGv~_I<_Eq%#*$b^T54BLU^bI_`$Al6FQI5jEyie20Tx=0$35n85W%8s%rS zPQz)8S!q3EMA1BMu{%6ad`#ts;zG&eRt+%PI-9k%*J2uVsq7$kW2|Fzsp9ElR7eEX zyurvve{R21QlZ=li|TR^5mdl5+b{e!J-UTMc#Q=;VLSB=t}SVmi9OGmN( zaqFX=H&+!mGIeH{F~gKAg*lfuEE*qte1|}7tECRqKOFJA$`87wl8vJ%pyYPm5^t^CafQn^sn35C6n)GQX-6GbhAI80&K(Eop(bRi(k2B(d)YPeS`)nC|*rt{I{0-y?Q; zfjSef{F}Jrz`VW>jmtXb7oT&yb9SEDpL z9^Jjb800FB79E`}wlX^R2e)H(1&FTpN_Rw!Zpzmo9hF&~BE)QTj6*&qYMU9;evV>S zsQdJq&|pC&IV;~C=PG^TC~XfS7JnX@@7K*1{BY!mR`=<%$zN3b_Q}5izp>L#DCk4w z)(X0(-)c6#a$mCa+kKrt)cUEt@4vV$NpkbN-}-3zT9X^e^7DpE1utU+#~W`u9Dmhv_v_A(`N%!Hj0$c$#u2^iHbTfP<8dHZ=R~^4w%Aqg z^;gB>U>=j`R{>;g_jZBPvJ^gWW6Co*U6hf4@;b zJ}_*f$1{2j)C~084-MA-$DXDsa1U1hW`$vHF4^QBy?_$?1h!aL*ajO~vz~pXZA3S2 zq+l^_K$rbDDE~e1HGk2V$zz2B!1Iy)O1w!Ra#c3stH!+Rk?Tc#^G7Q-sliJ^l?LyP z@6{cRGko{&T>zTjB;7kzOdybfA_uP{jEQ`MV*OTrf~~3Sn2AyT3u^r+ML$7vYovUc z8qp>~Q!)`nt>2B5SIIPQTtbnr{a((g*7$0#$4@Z3LpPb6y5JDKAEY$WzVCgMW&E$j za#rrW*t!6}c*Kmp^!$*(X}hF74P5B_fRjp&SJpD%k`4p7w^1&kLvgGD*`n8_qF##^ zTzaxR1ncZpKm3{EJ*ir9gMlEmbMMBagkNNZ$Ys~za}|Yo+&$0pJ_>5$_NlWFD;dAL zHIX=W7er$K2VS(6MCF|{yVQQw(`H{7glb;b?e>5hWVWNfF>`+`6jIt&YZife?vi&Z zO36M#aXw;o=Q%+sV0AX?!RP!Q^Zcxt!v^%7L?uXZ@u~};wBPRu_b$Hqg4F5s?qcm* zby4U<H5R6e@K4~>x0(p)xGlOw?M=d$FJ*H zrqvgrmpNQ{leNV?OG@t$*DQ6d{AyoqDXv;Mi!xOoB@}AhJPA3OY8U3M$4u!}g9fL+ zJH2}9Dqo5u7f$^hq9Wp1>xd#d+5{?u@T5Dfp)tZs{UR52r@xRTr7vC9^IDL8-z3LD ztUYrcv!ifv*UAn#3(S(^2@MJmG}y)Oq|;1VCuy`eRO%?e8|8-%jX%L$3Cl8S7TWh5=(Jf*jSxQloBH|jM*K08#54Fo(At$99 z7R%S#yIo(#?~)Z+55Ie8WUr|`;E8m@*%Dh0KeD;?5XN%#ORVI1vwA0Ezt7=AF1ftp z7gA$SU-UD=-K_aPjJ;)8lwI34EF~Za3`qBoihzVPLx-TCDBUSYN!QRSAT1y*s30QU zU6M-YkRn|}cfV`!@_nxReeQ4DzMpLH((_~NrZcz#p1zl%|{%jkMa(sWW3>W68u-AsvN`oOH`D*Pip zb|I)ovM{PGWwvB)x?05QL!^kB^Ajo$wlDQDeX|rNplZ(~-2lesayyQvH%1kveYmed z7)_X_JBmA-5u&Wc~2 zYpxmkm)P+-5@ZYBb&DR}GeZAv>ez|VXl<~f32hm7A4E}QPAbS{B3e)G^}NaAm<#mc z$|~_x)TL>;-|WX{vvp1qAR=qy5>j0XsTphZ;U--fv`>FlW1}ssAW=eCT{=}Wk1Jnd z%KAVSMuR9cZvGxyB~q8qYI3|HU=2BTzM~-~xi{(5n;Agl5DM_ffw9%pvpsJq?O?bE zJrjXWyE95ob#L9_mOCo4S{`o3e(hBrs+Pqj{)(Jjn$~>#NG$}rXG>D&bHGrKVtY=B z)Ni!j?YVYQ#E}Y69 z%axb@2;^B!AaNSf=XcLqH<&SIk)MO%%aw~ipI8jXX*vq9MF>r7tR8+oUfU)$8TJpN zH{G~Y(5)V9pz(S&=94@vGC}KlDN(Yxnv>8>Fy3CqeR_I-F)Z5GX+PIq#Fo1)LTDGh zd&m>FN3QMNR6i!QQh7fh>djQa*4|6bCN4XfAOe?~F8_IQwt|L>>Irj|d^Y5ZePl^5 zmDoBhi{jkd-#It|X$5r$I_hurX^k+VubWjcHxZZ{!na@ZZDZJoEaPEB4I99>ahlK|RQR@D#4FW-+F3B1KU_r4~b^vMvqWQWTgqc|Fdn>MlY{+*5f7*0*)NCfp3 zs+dY8HQx_PJ%FSAAYPJgnT{?B_S-;VYlS8Bn5yHZQ0mVe9-jgOPVkI`DBZvY( z9I&@6)m}yqe37?!>^hERt<4Q2NJFnO!%%y}2-ogr#Xi zh(p?Fpegph>Q=C15R_pX=dN@x{D}k6=nPBIyKm*R9Ja9(1ruef3Az)@x zpH;XRO4sq0?nq`jrT>ziR*HYt{UU8X)O$f5AV!X9^c5nwU%)^TR)5 zt`jI*><)f;Z9dK$?0KbB5b!$(Gp&DN^Jf-@jR(o`))421`fy^*VaUB({ZfwoE5s`S z2rbi_S*s&?$x?6C&KO<-iM~d6Q7`N?0d%smgtnRrNzZHpOKaW*RHQltp!=I)#>$mX zIw}V~tFHGG!zuoPbFV!DVA6U^{&*cB9!9zcYd76j{kX;}rhRIdT0d>z;n*HQ6ckf@ zzNJIat4Arf(gO(`m7Au^k@71AoR22hb}}hR%meA4OO6gBGDa894_~G7M+z@eIBfdl zO1}6g)bHNZcG<)d(xKV1pR}}3>ee;%?=a!UbS@;7g_W8sz=pQb6IBBS6Ew3Bq|{kK z;wQG0ZGXWY5{YCVmb{UCLO0fr9lE3AtZEqzRDM(J5O73~1 zZInp~JbY)7G17fX;UH$tva>QpS)?M?N97_B3d2#f_deOf-8?@({rOh?Ow{E1Xf^>N zRWZq0(FTGowq81zvEsVWzHtU6*LRL?a^VWl-)0FfT|Ofw#F>?nI#b2!Cw)~R@6E8_ z3_(EXR=t5Jc9IdFPHYrhdmE`R-Yaog+mxROW!}LhzWX0QE0vt&EM#jp_*|U4i0Jh7 zxOdk3N*tTn)n%u1sg<+r>OHo&1l_~6zY$bfKJwaoyeG%nYskAFb_Q!mZ5^TSbhZ8i zwiJBzvmPi0Md`6d+lgy+>ido$9tZBO+b|hwx%n8Mv<~dLwUW#|V=>pc7}yHJwuls( z*0qiUf7`LNrzPlzGhAu$LlQ$=byPdmQ7@|Z`*)?BwZ3Ei#AW@7(i6bT0#hOrOT zovoVj*#rroCph0NI{LI&Kfz(8(vSjc-k97AU-=gc$7PX^DZDj<8|RFps(KQ?QZaDj z%f#K1gh+|wF8p!JoI4YD!&}$${SPOmFytb{7R~rBdTL)#Va_hl-^M>;l zJ&OXpcd3+)4kSZNs(OZ5imLBdomUNczEg18xWJ=pw?=2PjUZ7ulEpvoC=TK|#^ESF zRJ-Q&Bb!&%wyO~hp z*?R2^s4cSd9J8L?yC*x5asOuM=EK2g_5w=3CvuMytd-%mGS`!wD9>z3D3ygBMIYg` zedNmA0C0{F9wraT(1UrWm;CE_e`*R%=E02 z=rkX#@W#}2%0-jU(|HHu^T{tP4ExHN>m}>u6C(5{_34zduuHUtuR+X!ucWaA9OXrR zcyJBF1u>gXZd*e^7&V(G@>;g3XmIvSk}^lfxF7PS4*xNc0}kyay8j+XZNrP|E_H(v zB0%9N8HoLWdY=c-RZm-7UOmKRfJsZPCX2YdI2xOcUA;-T^5Nxo+>c)@p@vBfc`tc# ziTFizCP7XFtj5p#65wY zn{nRw=GV?~YL*@0Rl3D%IMt>5Z@=+Cr{8ky-4`?*B&$t1IjFHn3w)F8_RD+vxP3y2 zgUdcbZ11v*DyK}m8%I3CVUE+4ceC~oElJ(6z58=_vGQU5KzN-`%5i6Kw7LjCYgj|u zo{mY;9q*KNAo+eFf3{%hRKv0` zEEa=yEf-IARSC(3JwRkII(&152M)$$kFL2$i4CQ~CGq0pwCHmcY2N*92 zQApCwsOg6CS#-T*mV2Q4HufWf4YbZK_Q*=#n>aD-2Sz+-xW&yIeih1}2j1}6>R++$ z+YD7HDCrSdqrh~K+hN`DkA2hdv`YS6a9;77i~+VCPT>E!UUTEEUrTW8lTq5)K~)LT zGLA^m4WYnzBBLA;b*9Y*wnr%~5jcwL`pQu!o&y;Vdb<8qNz~UW$5j6>TtVb759w>t zAUbB?lf%r0_>D+e4lz=2dk(S81A6JPHiAdK-J|cIbu+#aAIThp9s-BGh&w=*bX#1{ zElWCHaP*$UhjqF04=f)u zgwPBIL8gOi5Vc#_-k2=apg5nZ#t2`nK@iGR)v{tHTfdu(bv2_jbh5IrAVk|?pz3g` zgV36DU;Y^MT}F~>=4we zvXnFO5Z(5!v({z%gfNw%g?n;$C1>EBJ}E+J zmEz0#k?GDO`^N{Psf1`%d_6+Vnb`g3IA+rnXV);q)ryq-@Uo4fi3P&%mi9?spTFOs z#F6)`j8E^0ER4u5N=-mztA$4mAt}j)o>)ZogZ(EWT`F*rE9i;0m5bGfoO9J-&@~f8|EuQ= zg+1N2aZ%Ex@ZSrP^ca}d{@|;6G?{GvUi-VV{3z!&QTq|%I_jXyKxN|Sg5b6Ly=yrm z4kc;-`tsq>^uRpn`Q|#ofwfPn1}a(T2p zKdtWC_6;*=W(fFQi ztJl0*ISsHNI!4Wq3Ip%29r!m1H#qzKRlygk1bonr|L2#GqF+)N!43P$=2$ z5v`fpEX1Ip(`i*Xsy{l6^J%d2-RoDdBNvWZXQ<3-#RzPKg&rMlx{s0)9(IGpFFoB; zZqw^(ilM4VJzuaPgdzh%cD&Qc-Mux_+|3fN2T-4>4b&t^UH~9lnk8@AnazjocO-+c z=w#mD^K?j}(?s2#i;^M0efpvkjqK69@@WOjn(>4GNl$3dzG|=^iaz4EaWbc>39Z0A z$cP-au71IbiEYbHD8L$ej($sJ(_A)NGOPVjtX`%Iba(yRP zxJ8EFGq)j!tb|C|#gN|VAzs(da}9n&-`#NG#aDw}Q;=+T-zqpg-^O^Sv|*#s`pq|q zR%w@PLl4YL7}t_ZRK*RFMix=j%W4)|rjtM2ZZc1Nn!V@Yx4KR$MW3t>_kZg74zBao zZlG#dH8~Xz(F${vhBT%j6{>F^{H#EjCH;Go+KY=(u_lzi&PsMuy1a#0IRT6%qBI(} zq6sq?F&AfLH>-#1yxexjo~N}YvaWg^Zs@y+g|zXcQri?tDc%X=vC6stZuvRahc<|4 zDSx(i+h)b@|Df*Az@P)zP3*YZ5I9a{U^<@EdFh~v)4u2Q89n-JFnpHY%8>weWMoyn zgTfjl4AgI{GxavG5#4xRQWG59vMzo5Tx~W8?|8fQUp0xPz5U0G~G96X4Ob)4Qk&ulf7Fkh1$~gy1*qCj$%bx5} zvkwm~{5}ZZ~F-A433Y+oyXwW5t`^%Ec)f_Eo zpI*Lq=TVI<^7*Tr=N{I7;%^O`5w9lO3a$V7e8<5Ubb#oXsJR&jFGvla|i`M|8a z;V+ai7pKp{bN#SZ6?UA_gz*;Yanx}tBK_qWvWI(HS(`&KI+UyWs|T26Wry?cf--er zpELFSD?nBLB_kBZWRCEE9oP{hV8T?cM#|K`{jQ^$mYauYi7z-lxaWA!CPOt|E2uPc zL#b9<%J4DnTP4vUYwCgsv~dDk&IjJ(1p9Z(FSN>a@vcBSxEz*t%zIk%$lp2K<0=T_ zc-eUp-Pl=jm&@0%{t#74rVOznsj+;z<&0kX!27ef507LYH;;{-xVpxdmeU>=atV4J z$qL^hRX6X>Cl@*O+$k#OC+b5mO;vZBxQ<%jm26*MOJecj-ca-K5m@Q99i{ zBR}gk{y0UBfOID+sc+h1Kk&BQb_Qi(VpC#vla;dZ?n+u}Xs2l9cclzdzhNS9;U)@s zxdHm4QQs&6O3RaQ5=Y2K>HvS5bzczNS*)b~b8ZMjTt=)v+_|>#88e~Y&X)gv?73$% z)$W-Ti#;^$!h!V;?X8hCxne*ZodUC+0O*)r4D*jZQj=IH5Z%4-8mRFP9q++ zqAr=@qku*lAU=-zdF!xPM^Z-zNMg!<4i6F?8yy`lrHSq!P;TEIg;;daTI0hK;iXa@ zZWfPe@@22q${_y3QBy*vVRg-H^=ID{2|)};2A$MKKnO}&oPnb2SHcgpxih~$Y^X&n z1J|pdUr_|)MbtP0lTcW^{9a&Tp~Co5UitW)?mkz6Q)G#%Vzn$Aj=PBXJwI4yb5HcgKfncf`0JvD>vSqU%1KBXzzL=*lZQlxkFUL>>$La)mj+O^;+5QoKK5$`RKUZ^O`IRd)9jByz6;1|}76>w? zafPr4Uwcfb2~09sAgBtZl1z~MMcWRbb%phoHLm*%eH@;cj!)vr;UBqtr`f8sy_qsMuYYx*=)tzR?v6o;3u;6-UOnP-M9j}<|jt$c8uj*nYrEXw_>oPxf3eQ z@!$-oMF%C!KMqvMmK+`fJZj7Jc8n?};v+_=^n#yY*!!Z1bMbAiYduHA53Un3+J(t(F+d%}hs{_TsJRRDeHdhL& zHICE$H}@AQ`?_N~f}ZiX56}eoodRrGPl+z;>uw$$l)hv6$XGE8&{bPU5T;e^A~64N z1^xgq?P>&tD4ycmO=g@uLN>sL$mtb$7#Kci@2b!D_k$a*Qz>+QzS2_9M_$7SDdQQY zz>OySY*BppdwDl-aNtdN27)y8NIF}lNoTWd%;%Diz_v)nMsC$)soj@~WyhFy4IgRW zBeEcLI^{)hYBVdeINRBt5O0temaoFH+btUD;W)d#kw8G}12-Fat%E&@Ou$t`K-Keb zHrBi*ygUtG7DQtE&oj1wj%Wb2`KLb7`Vu%i9BbY`SPF(PeKX< z2zKH|)e_yx4duzs^}54fT>5v_2t!+@H1B(cZAG4PXVAU20sErt>=423IWG7freEUy zf^;@(cUAbT2AB`Op9|2@G>dN#h$4;dRrn|hbPESw{)l0#;jiN*H?dMI3-wy|#FTwT z#9ij6S2^j}X%#mSN9gcH)6lzYiYu%V28{NahkkUkZVmxq6yYa+n_~G)+DvNK0=tg( z232dY7vv(RxHNUFSjd$zvVpMs0B;cc)ImP%$EnWG6VUZp{AbsvgQIvCJ%nq;2o;e}OHT%f{PS44w_3>fSrkitTlKF$$)Z3xk?kfVTpWMs~-=v|7pR}@m zyO2;GB1y}a*y1i#))GBlTL+SfQ`VSUyS zyS3YEvu-H~#~`rl7TXn^sp1Fq@#Dk;0j5CzvJq|eCssbBQH2<{WmXweZ30?Wc=`Dn z`tPzcziT1s+&I2ybDmy#`~qd98r>;&)JLL)4BIbGHYPE|Iv)TXoDN$Rz0x z4DytCu|m$%{p(#j%sLAX=4D=JjqP)-M_zZl*%G%;bo?7n;yS~9!r)3ML+4C8_a$}+ z2rlO&y{_;fNiF4+-wJgw?~_+j7k6a=JBSb~D5y03Qttw3bQAlBZI31K&!=)8YWMDT zL8xP8K-}exr~=dihCYSfFzXz9JA*VOlPM;>sh3cPYNUoMCiUY+qn>B*wXP?JT%D1+YNNWMrZF4bR}!?7FG1Mnp4-( zqJy=l8u)Sq%Q0N|P2p?{22m^l5>}rg!ix6!`GnG1yz9T(D+crhNl6okR=13+6JAK5 zBiv4eEuzr}Pc8L}K#i1MOIR2J_LxL`=LtEH%MbX4o)~Z4?m{v(Z=>YUs4P%ebjWCL5KrW&_Ip9zf z#D}JJfb~_D^5InG`3)DIhb`cew2X(xyJ?p;DzIZSN)rN!vzPInEYFyqojFUm-=MYf z9(a5CO0dn!@hM%U-@_#AaxmZf>3@*rK)QzLPswgEiwrzMFewg)HQdL^f2vmyu2d%r z>+(8T$)e6E9SG3p*_-A&0ZP2;o(sRQy=PFxI2X_!E0yR2&y5!qO_m}V)bI}tNkh^( zBHienWu3?3RQBhKMxp!nP{j_l-yiuxEJvhcfOBa=&WwYU_9k*JSIUw}+Y_B@H2B`m z8x(62sOD?;$#9m@n6@5?$3u;C0(t%eXkyYvqc?kf#ae8tj*0kFYj=V1Vr1Jbd19jy zQF{pVI$Ykuhk8y)-GUQ+HYE#>2i?XpyFQ%K+i4hd9>9_Yq^Tbq> zbxG;dp@}&MX;ILfd|&$=NT|})vIWMh#HIr&bqKP}ec%DxgBZ|B{JY}^61Qt;v=!H) z3D=`?pU<&aTI-Kyt)o-7@McUC+-QFrsl)P{EZNnC{VJ8dmNgkak*2maq#PE2S*}S( z4xW7V{113)plJjcu+~|G)*^>p)m^B+l0v+x09C(zPh*w0f+No`_8k;4EcaYFEZQMI z-*wW-R#bo^n$T%<%V}5z&c(dG7uV?A`b7=EAU$vga|P^7$YV?g1x$J*LxjI9%3={x ztskO4wiR2bLR$h%B8kcN^}?m+por-A5x$VhaD{$>qG zfj&?3E|>B&G>j6tp22z+9?TD5nI2b$6{CuK5X|RAAK1r$$~tG@B4Dfh;$%&9fcb-( z{0oCxZ60E9v!?;@p6SXe@HQKKRA)lAYyyTI!fZl;7e$ zBkfJa&(P6UwA&#XTESa?7YPnUw3*SwTf+Mg$( z`mxW&*@R047a!cmfx=ajjTVM%Dv9Je}uj1Lp_E0L6%0;UEe;&yGhyg3Z z>dQ;4CcV8-*xL>^DgB~B+w9KCeEfjM>|i)ri0WB-OC>W9{LKaGPej0l>?*47-N(Nr zJ&zapXhsG~zx53VeF$Sjr~AyY3if3QikB#Ck=A{-HRo%4@0?`!tikXfV4Hn0yEB47 z33yCSEx=Hcr&Ra0^$LFb8%4CTq_1JcdwHuU#=ZM1SdJ_gmTZ#@9uo(JGkC5=3v_0C@s{w7GVh zu;~+OHmW;m96K0iRQydQaH0ue8}+AlC14%*ThS{g6j)-$@bJOg2$VJmpB^^pm%*#a>c21znDvbgb*JFX765 z1umR>lRP}(gri5Va)lQYnrH52%k4C>5YlT|Cl(0ia;ttK9YFXAv7%24`5=Qs8m6(x zv?QyEm7^SA>xJqwXFW`OXPH($*kA{UjNESxQ#wA9vlxklm-MoQpP4%vcN#EGK!;I? zk=9Vc=(>X8K#8ey))+8_7acJge;3H+x=;V`cj4<1GLrRaE|WKe`2L*yQeyNIFSRj5 z9ztXU#=PxRgFzqA#YJRG9_d3H+eYBs_d{q@Syu^e_bGy(m#9eYws*C&d{gp8mx}|j zD))8*G#H-n;zQLEFAynQ>7+M@w$E|P- zrcdW@HTWfwPTcdA&~HXNhMR`@ z?YzB8KRb`dZ`cb&s|GZ)H&;VcAN=c?cvv&s@4iH)~ZcKeI@f z#B=&?peFZ+rP(qpsqgSFdVMcU$#iXsT$#nAhN2b^!Gb*EL09w7vM`}Zo04vtCyzz3 zjc=f76D>-@-qGS1V+>FSt8wI4exfrG_~mZOXZ-!u9a@`h<^yRw&iRgK43AzkIWQfC zI(Jyjhw=5i%#bdUyD_0_S0$H}-3i21<9CZYpcZXmlrt0(=PyvBsJRLN2%H}U0gPux9~6kAWEjq1Cxh@i zF8K3kpYxMMU$B04hNyJGm;NFPW!yEUzvE$unl9Dx%s*#?yR_3Er6$Bct_W7iELjHp z-1l3{d*hPxr-?VYm>AX#ApVvsF%Gb3RRYMl2~(5x-VC3>kS_HlpJv7D4mOb%;}!)# zcnJD?>*n_kEOy8c-m@mu(to!8bF<;{7Yb0Z7;xfFAUA-w!fbZlODP#>Kd(V^xwDvN zPgjyV^~4_K*=x8MzOje^I5U95=vQn9N_vwa&m$by0-9zTrV+@}_4gJzsRulpwG#&~ zb>@F#np2vv;&z&nLxN=+dn2wfnORBN;1Ir^_3uVG6yOKi`zp!qmNqDN`rd>8)lH<5 zqlIYx#$g-RuYO$^G;p2oy8zOeetd_xu0>il(p_0g^PrxS@BWLvuV0@_SA!{Q{8}$tYDvr7Hr~laTQDlrVx1JHSFBN zC{r@{W7z&tDAoIC`*Ek|1M6Z3T@qkNhM~o0l)ib=^E_@5c&6^9!9}{i2&NiaV44YL z)UGs~+_A7b9TJ;=54x_;5!BJgFMkrpUQe_GLIXinpj7>0Dyh`t)GLkvHerIIxu{~2 zmIUj9I)M>+H={!iq@^I(!~DWUi(b?mrpgF*@z8p-uFzW&F;d&Vf*DaOWz$kL4T6iD28ZpH9x9nn}nILpXQ)hVdgpYEYc#155<|iXi0udkw?Txg0Yk1QyUo57dAMoeoPnP4k z4bV(qZUrU+ihnULd$YO=EzRekO1b@XthD#0>&3~%DSZIqkN_K=mTLaqa5Qy&iGOz- z`_*Eqn_bD4G+t%u9$8t<3TN3o@t4!HU6w~HCz9+m!mDb#9dpA)uN)8t7Ahk9G`o|# z!-QXlotG(NSuAEbLmxq-f*bc%25)YZEg10hkGjtKjy+&YHH(0qEbDwh{{2t?>h#k| zpbQT9G&;9HqJ4la{h>v^H2<5OcRDAxZuZ-P;9uN4Ztr^b=elno1C$<@${A&-Dp4#~ zQxkm-Es5jtp!bL31-T` zZ*J%8yo4x(r5kLQBCke2CiRWf#uC*uX&2{Y*a0MGtIooz$ECRcZ*ZrBThfCV)+%x} zX5*lK)N0k}c#wf3jsQOYa`$xUR<>W7`z$UZ$h0v6&*5GBe$wGSX@$lrwSuH?ugxU2 z_+PzDM}#CLdaCD5V6ax{@BqU^Kcgb2|BG0a+`JLZSg-=S6e2H=l==R{Q)N$PZ>f0n zy?=+Mb}A1?ejfr^J}6)j6OQ03(c00c#5`;y1RBY(D!UYq)x@P6`f7_sh6g&DiDp1lR)?onU@1y1O@iL9`o4=1mhLncuze8$PwH5T z7#x>+96$HoQyfU9be9}UZweYI-fEG!F2PN)#UCN0(4LRY zk%Dfj?)P@L5;N;$^?{O-eu64(JK6m=Guf&WecPty``8$#a|vK z&Ym%NzeE{<)s#8SJr6h5&nz4@(C4n0%k<@k2;Ir-R-RcZOv+onpGWh!Zd#Gt&SSyi zR{RVMoywE9I5X`Rr%7sr&4%< z7$}z>36|tVN9l-aeygU64A-7+)NB~gZh!tD?p3Wn1-bnA_t0LQ6!1e|8Q2YQ#IW;` zbDJQoBkNR}+M0fqGxlp|r^fh6tij0>X=mM4olVe7LQQ|ye9Jo}UcI0wm6M+Sq{v<} z%35ixiT5YJG2^Cx(f?*&5Lh_N^ph1?;;#cKib>BuV38FWl`fc2Q@DLD-4nL>DKT4aRcDUGX;#=^+!W1Cj265~iuxnZ)DH*^J_A@Hq=|BLwT z8+<=j_%_w4VECLBc2>E&9?N-h5SsX0KtgAB_(e{xS(^dRPviQ7>&;zDm&edu2oHNI zB`?9%KYJ_Snky95oIOf|c~7EX5rL^1x5(5rMlZhmqZ`b30%#SY2|}bX`O!CO-fI-9 z{G%>NZ^7Z`yY_AA8z_#P0fQov;YLLxc)5Kco8Z}kvw{`jvOgoBJ3l-hL$NlZZel0< zV~p^hYn)^zcEV(}U%~hH1uam$7F(Lnh}hO|rp44l7`sW5Y+6_%^Y><17`N+dh@T+~;b>zVnv)KEqI?;O|=JM~JhlLCxrn_(+NnAW(r+GuzVom_A<+{Y6w#K*ajQA6aI*)w?D74o_QAgvK1I}Vp5=uFBd;laBhf|x$` zw7wNp>k(evUi9!=tDOdkUire^i^;Z@Voo}LKE$kZF$C`_p9Cx34auy|M!;9p)1MpfTqCT}N13x)mSF(MbtwHKp zwTJBvwLs2y>&4c1wi@@iQD@m72@e#~dxZak#{2VR>4`XsDdo6@X%nDpLayXSt-)jTY@Cy0fy@U`(%@CZTrU? znuYPr>iyb;GLc4PcyaWF1>~4Nk(&@Yt%{bd8Vrv!&1KyreKb#kkD%pmOmUu!w(zI3 z^*76XK2XA)FtPc;$Zf^zJN)d&a}$FkaLtDWUm$kg!Ut!1o5+tq_gh(3ci!V=QQ=L% z6L0YH%}Pm^n#FWrP;wBRM{&LjAGb#^KR%jM)lZt-tUom%|)KHEM-vj>bnB&I*x!57&(cG%gw23_K1a2VVju6DrnqV)-Gplj& z*q3*kcfzKw?KZ_||BD?=21RVhoM&pXCi{x*ef2EB zFYjFfw53U4GsVzlFTjLR|0c_Un0&aQT`w`*!ul+9di-#lYOK4Sd(g^c` zOwE5pCcOlUI8=yk{b?|~4_CVY(DtxSGXln)A92i{qs)-w`Q5%TtnGKj>m6+EL)yx1 z4@K4z7aabKwEx~;670vo;BcdqK1GO!d8-w2Y(#%>3}DBGS{s(!P_9QIWx=TJ1vmDj zGS%(?gxUGtz{ytuXJ~!i@;Pr_szk*9>@c|+_zchsJ>lTh$G`4a8dy+LGN6`JQOcY6 z(BVdLV;daRHF!h@!aPh%#(=4ua%Sv@AP4Gjbmt#tJmzDZZ=fh)-&LX`QX)5 zyEV%p;4-7j#Zo}TIC1mqQy$=;OfX-M@Z`v=tUcZ@(74yIQSoV|-}^$*KIVh>uP`B} zSY2{_g3I%Ii!9p;e%CcZ-`XZgD*Tveq%u#^+{L|*C9=ZYYK0_?`BVg0pHY(2#EYg~9MRWT%V&n6}fb1S~3GG~y4|}~L zOWS3^Fyv0r*+me2*HFduefvk=h%YneE%1}EB`(^{=Yy~cE93h6(-Wol;t+=$f#|nQ zEfN>VE%K`t`FDHxA`{(yRds*IB-LZ@dtjh}7R{PE~U(qUM0t zX&`$kVd;Z!wo+PIQ@XQ8i{+I%nVk$RW0Y3dF@y5{i`ZkZsRzo|+}#8;pj{zsA1p^e zBq&nf5fwx~sWC~6EL9)Tw!p6vK-FAHK8WS(Yi0)4Lq6cY^<+dIo5A`!N7Gt~j$if| zk&Jt=K|GJca(fVuLqdY9Mgfkxz)Ed@cj}93gTHZVoC%RpR|bXNO5A@*IzI#bTDS{8 z0SFmMMBSfh=(f(l8c=rTLzHSBTAx9y`K2tB zTGe~)Pea9ln}KSmB3)xuP6yAQ$ktzK(-W=Zq3Kfwsg0X3i3qB=j>uS%7Lt0^4brs@ zL3gSgn%sxX#YVI&p!6^Oax0|`OTad_QKKVI3o_+;j=*ab*sQP@ZF+(|V*6cAuem~` zR-|V#ovvQS6i!zHPqWzm&{K=mq3lH<*DRag(#BVu756&e$G|4w2PE=0Kn|7*pda~Y z$lqS8{uxY)xGETyESNv3Vr`uDJ!?T7OvnxBk0^ca!p=zi_5Dzd3gupC_`z+{$MPU= zq$OWuRWI|XtOl1!K7QA+lx{grZOZy6Fp1>9q!CGM#`HN#0D5)4!<&N=(?=7Vy3pFT zNXZ5jsl*py-r`kR&2LF2-NTAY|H`^2MLF*7(m-g->6tQex?k#gAylqQ2)ZC8pC0Dm zB=bbjvjV&`Yh&}FEG%A&yxpE~gw}LRrnHSYcLM0fEo~}#g8(8U)8lW&eS+7$O! ze@T1fGy%DEH}r8SuJZaGXmLhM(xs3F*RlRlwa3SviWlZA{kKJYiMe;!{GCmiM`~;& z#^eV-;}v4!e;u+Md^(@OB3Mi(E)dZXM079MiwIZpb4zE)O{!1>n>(}vw3s=zBo0vw zEy&rM?njVyAt2LAVTmpXc&<>(XE6Zz>hzx-;s;?ziNCu5{0=urA`;?0*l zL_?zlahs7YgJoXSia9wLZ zX)gir#anX$<0HbW;{`*}pEOC13kj^&RNY(D%)*9P1Oj>b928&-{bI&l86H|rtbbk) z!$08FRi<2~z+@)#9~&e-z9-+|>60p3A0Zr>QYHkdJqN*ro)}_xE>l7t+t~cc69MD2 zn?rAdX@i~B;)&7p8zRbmny#y9K+|%k<6PIfC!EIeo( z(%Ll}%K3s!FTKV-Y4pPJ1b=r?*FIU3t-;P9`Rt9yN*n@|epOfhqZWTGH(qxAdyUS2 zT^8w5nDeJk0_|4me)aDn>b8jSnO#|gaP`n3mVlXUv1xo1PdVQ2Zk}p50f7JFWj!J) ze@pVP^5?3q^MQ5CPeh7D`ag$PA8@$D9zBe5H7_iyhy#|Tu=Y?{m{frGkOZB-Chv$)ef*2rnlKdSpoQ8HyQzYaA3Op&F2n-nZM!=<4 zW2!Q`0y~!A6Rj&#hi2`tycqq{7pS&?%euuuRR2LR+>!3gaYVZ&v6W1}rAuPE)&eMw zcuU3(xP7+qo*#f^;)$E}mra0W6<+a+kk)4AN*MBHmk^mtMuze7K}G#v9tI{8A_Dnw<792WIik(j4d-cp-B0&02ym z_Ds&ql;_V)soA$FS=S4vHGl)_yAVML99{P*XkLSCwA8*Ss^(&XrRaP?;clE?y%L@UO zltsdR|IB^)DvSvA51+H_gj{~*#lMDKbNG?7=Cn#=yh*t&FojLFuCSM@{B0>VwU?fH zb^4V2d`WwFAbx})f@4)c?>C;S`6#&9d#&qxy7O~bjo4Mi0%(Fd=A_$4R_=PH#aJ0+ zN0s@_KbFQ+O8-){4(Pq*;Htr{Y+JBshdAX;9kL^+nwp-(kBr8gAoeIX#&y)51LV`u zx8k$+i26aOU@Hv%qTsWl8cMku$^NI8xO({_IB2v54AI<)^I59VZI9NpH{n;A!s%_J zmBx0v;+Fbp2pCi3L*OFI)FZYG6iy;021E?5F2u)}2V75i*{94^?i0rT`I0)<&@M%d z`<3lR;N3}CmbJ@Q-iSVJh^5BwBJJJZV#7(whk&K-yL76xq0%4H*HzoNZkvOD+HyS; zpjWdckH?K0@|M|2W_e9G4v5>M-~InO(ZS`ekqfRJTe)@-2)-J*2qm+k)SVif|XU}BaK z{8j$sZ2?HwN44Ma>N6rtqkDNaI>=otLJ>R`YGQ3E{{N+K5+Oqiu{I*Tbl+;)l9CH? zYFY0Ha`+gYuPd?a1Qs%h9Lzf&=mCcYH#tS96BKReF5i)48Lj+jzvsLw3HmdxF}yUJF0cxpj3cXPN!3ni<+C zQVCF@42g2(bdj&%Y$Dr~>Pm&=-o~`^wdtJoul(4|N2jHLPu~=jT5<|4vRBsv2e8nT zEBd8)Qj+b8it|j-1!9^O9vae6LysQ0c_rqTRAZE%_u7}Q z<>H4F`J;w#RWc9u5KYPaE|RXF1*W zi$A2z&q@{K{F6Cbs`JG=3{uTykR8FV@u6up6^zskFAIqPFL#Qis0jK zck!hCcwz$lGIjq;D6!u}7g328jV9zt2wQPM!yjShzRK|5@9T{PnWlfAaGj{pE#*_jlWAIU7zbB) zzTg`C&#)hhU54na+a`V03Y*2#Cy3qO%@i8zvjWznC|>5c!2(I+*^W{w#G8_ zq%`~_&kbHT_b#FsC+)#*Q6wrt`o4o!h`Cox#9p^_<0KKn?-l(p5gi}Jsnds9fC!wuqN#rd_-oT8MH zm=4XJUE+Kt^z4sNxmMKs7Uu`=TQ|Cct!A5%sGIh9{^9!)v&YpbORsbOIV(LVaD%(x za}L73qg@ao8#))0(we?opH)$p%R4uf7KH0_mA`tjnjC zB?CR(kpNI#!sR_y!xCec^j@<3J*5A7_i6GE+Ca zr?FNT@wI#qu!5-C@+@ogHE-$mst^6R&Ynx`_Gd~nzgYuX+b<-!x_ zPGIWvv{Xh#Bs-PA1t#`d>Sxri(c1PS2A3564`E*&7v;KztB8W4w4l^b5>nDV)F9nT zhZ54A0|SVNfOJc@fRwbf(%mq0cQfRW_XD@*>~rqE=l-+Vs6UwRd)Io_v!1oyweXMy zM1td89D}k^{JK~-vTEzzJ!@)&DMyD2ATS85d|>#U zz+4;AzaH94A3OBN-I|ED7^dxvB8KJ7KJ%y7*X14>Q@nk2c&`4N^kfZR==itMQiDl^ z{9&h`=?Z{wVmmtUS62@9?TbWa7LN73SVvh^HxeyDgR~dzoeGiG>o>nf=dPE4x8LDO z2OCX5KG^dx7!L$QaLXDGn#^ZYz^R)!PKC2Z6u50e7yqv_@hbT6KDa0{sLj{@{b4(i zxBE#$dH#<<{3A5Wrz?DSK+4BwrFE)$R5zEB&~DJQU>*6k|4)zy`V6TWU8m7s&?R!G z9sHS|obuuQB{+tVM#G)JGWNoVA)v7?YKiu6+U@0^-o5HSYcn;XmJtorMLqUWIf#PMdIsRu?n9Yj(7wP&4Y`HjS#|KUAm@p92}-evJ9p%FuCVsm{UMVLb9o7j)HfT2^X-)|7$ zZ-#LLHFr?>3V?wM-iaP3N)@^51o)+mgly!iPCoA!mzd!Tt%y72e$ zUl*@tmWaeDa|;=dOf3h;=hRC^Pe(b}Mvj}gu7NBim#WW%oVD|$wA^&dX1jQF!~i4# zw6*Ju*@o~V91rU$4W6*iasJoC+}L6w>1|SPQ()w&Q1GhpZ81SuSQ~tdB~{)NMvaI% z#|JXSG@*MyHwxHc3Q&Syembndd85c@4O#o^^;Gcz4QSfw>V}6=Bj6w%B4QFfP5vNp zL4$*n4A-!_h-IT2s3liGb-2Z;P(RO*dj&j$#o*|heABIU-U2{9I$eAXG`wr@kA(C- zKd_+@USBZxa}s^&OGt7l!U{=ND4ZVSN(OQIt?sr;-cpT!IVS0ui-YtK{WkTcRS-4J z(QGOkU{1$^Z^#|#E2)yT?>0wKBOd!Q3I1hLa4|sshohrtz%t9^3}Rw}NZg1#^@-J% z=cszP53fInSI_(Rm_J6`1Wer*O=DWrh=|?0Ot*kpWwU7f`xleZ`T@!ZY*iwp`xig?@1qhS*(zw2j(!C?qz@QYv7U?k8mf9Me4>jAT%v#MWfly678=)_Nk-*J z->je<4MhqFf`DTs?9bbF?##a-iyY@j6{XO}U=j&%(Pi?mPS;pso9! z_fne5Bm-7G7pU|vjQvl*t-7i45@Fo(HfU=N&~ag76fdgep`m32kLGu z9aj51ZIC5X{L3@jt}>X3cJb)Ct%o3sP@UdiQ1D)uOr)U{JTSWSHo_OnecKEvBev-M zNO|LPK5Ai+O7cmsfdm-#6`5@ASMmmPEdq#}?$H|m-9_^kHvtCj=#%UY_5-x^_C{EY zRW;vq-beHD@~WF#IsZM${Cadqcrt^~ccA^iHE<|1a!+bYl?rijTz|!R@>HwMT{fS= zxH93HKWs^WrT)EIVFp$Z7>}g0& zp1Qpbgq#5-k#$zJmo@Y0V76TTUfHRw)!=9wXQ&~=qQGbiplCGJ#xmJrQEsLA!%n(> zVr;}z0q0ez@{1b)e!eHS&lS=DF7zK3*+=G;p$>7|uroP`lUnR#IeHab()oJP3y=n& zDe5U9t{?Dk9ww8f3@BGbsuNuQjd+T5^6z8mivoCX>#*CV#`#dOWk!8i5zmq40w-9* z;*dwDx6${gmG1`D@&HC`R%_lhpB`;VbrBTUTm&a7-19H#4rntbTp00&EWv5}?gYN-Ose}{L-+bJ^Y^|{V> z{OB8+ASE>LxN3#Y^~e`IKmu_!2GfZV7j7S?=8ONW+xi~hSW)K!!+1@D1{utmBv#7R zW5BSrmS)#oyXti#+5Ap`w&j(-l%mu#PoORM>ppi!!22t~f~2T-drF;LIrV;E66mGwLsZb=lN=d(-}oV#Fa2Hw#Iv5hVetbj?IUy zYY{C&>oS8_#F7K&j|0YcpeNaa7)&>iK*`z+1Rr~_@G+v6J9(igXViik%nSr>s)>?Y>Hi}wMd-XCabJuEU=fU_=#5hneN{5p3zcTEi%MLvA`2QSsYdW z$0sI7DZ>dJ6|w9*XF8Y!aHchEUJ*XpJf=8v`djZ&S5Q>G1{%8`Qd>Qei;eQ0Jnw4K z*3-nUr>H!TeeJcS*#7rvs(gUfbEYLIBe{_tt)uKI=x%E;e_*#6bQA!hH%H=C3v>p5 zVedU0Bm_)(NhE~|1*)-me(mp#Ky;e_pB;nqMq+q9p*jH57Bgzy0CSJuGvo7)|nmNeenXFpUZ#Z2zKJW6;yOHN4r>3_%Ii-|wUVn^zmtAlE}I>Wa$amSwkFD3s`*Zh zqJcf@T+L|G|3_E&Qz`XgqY7=UwI>4)yvbMr!m!grX@hS?gRUd|gg5>J8^Kc?_~Sph z*hoV{@EZz80Lj(R|9v|mMHc;JW~#V=Y3oGO$@YRY6AjbW9=^WMDvMZ{_-eYc#nPSd zTP__EP4Ro9N02O4e*K~8HL(&FmUnx-Mu%k<8(pxG4SOOFbXJ$1ZHN#j3(<*ZO%!vj z9;`%`8r764IYG>HYPonZHQcB&cPeo7NNzHX|L_^$o0Ls-Nf`MC-v()n6WJC(Tn|`j zW^;>V0=Tz;8_zd&nDzT?>#WK#%9z|aa+DzF|2%-cJ>XF$iVNs|KZ?eI_}Bv)+snbD zfJA0<sq>eQ2WCoW%Kmywx|2EoZRUI%Oi^Dvcpb$+i z+Y6;rI?@r+OoiM4;^d6NVP6l)A)@DkWi!N2wI=v~`*ycPO4yqlJF<1xI8Z7#brk0B z;F8kI#k!y*U+_14hVbem46exF%2TPIRU1FC2wxD+nP3xgIAxa|*JrVe>>dwOD3lnX z&m;*c&6Fjg;>y=_S~f5rwr_C!+XTSBVz1_HuODtEwMQIW`pM0zEkn=C6mXIA3rb2n zW}9iJIU6Lui$LlMv+{qr`v2`DeMP_-jTINrbf0K)DP1V>1Z56t~F zOSTKMbU*gGj{IT;!kGgT>gNaBnTJ}P%mqTypv=axz=4(gfOv_5ui3~9g^o}^W1!|x zmRP}&96~h1arA+=Bk#zS@tWsxfNo!QbAjx8B>$a$0-{44`l{Pf29;#7%=#dBaD0tb z%5f_MQdj;&3isVWRuLX)XOHg<3}+ppvK;&ihC?9WO8LKGAF1#AD|e<1>4CsPBfMeY zgN>&h)b&_rkrEoKg^(GLE1BmUKC$6+vcbqN&(1;5~MhISaRHdCl zVn$*{v*-hMW8dliZXW0eymiGLjoh@c{LKJ_fy%V;OX2N9b5etF9c{e?-QK8{mlL$Z zWq<;jw(#2a#B_7sBHP;iCS~HyTWL}Me;*htaA3ph8sbDCDJ~^<=sM!y*$D&*0e)yp zW?BD{vciIv#qTiK5hz^i^^;>^-lBLl-NHR_=H4{$N+ibGpdI@0i)ijpY~&;fS2y!W zrXCx&!-;md(;i6~CDny5zx|zbJ@Bfz12a7zF7{vVRarShO#Gy`PJ9DeB6J5X%V3B1Ht{69 zdBzomW4Y1+m+?#hTh@;jF`}WA^%(Q2w!g;qDhRrDC0~7nT|kyH&OU+3+tB+a4!qt0dWpZ5JE3A{4qlt1f3Mg*+2Ij{ZVFKCJL z5gw{7?btJy~ z%E(E^Wp*7$l`CLF8WTU8L3mj*1L+FX9mxmcQ5sw6FvS~rh7J!!!`rKDGvYM;6b4wV zY8~^A(-4V8sqC4@ndQ_03Kgjys4ng!A*2%jCQoBHp>8uA zRM&oD)jG_oHeQ$QxK^ZeVaq2N45?cO&N_CL(<#kh40)}OEv}R%#fuBRTvzUxyJr1V zVG0F!(SP2apRhg7Zhg8@V(p)&&f}_cAK`kD!}-!?f6HtQSOzhvZi)B0&{;c@<+n^G zPSGdX$I?B@6JP2&(_?*yP%mquO1G2iTfLIb&`E*p;vF);;ubxfPAxt2w&9MW$St14 z9rkaeOAH;se3X6YC^bDqA|VFxLAMIw@2Gl^|MJP(l!_w4Gh*hlu!}3>o#(eUzXcx| za#!7#@}ou7m!!iMwbhhP@zHOd`hZWrt(mbqh{9CR`~eT;k!l{kIsG;+z>53<;2>ZI%l;uV5&7@x+|8wwa(4CFu{6d6~3^}Q#&J#X?Z@x z>GU>sDIN>u}HnFprePwy#q2Srm-_12>XbmJ71 zHR#LxvcSkZN81+s+=#{WUJjl5RDYO>RvMz6=S?H$%hN~uY+NU+=3$F-^%i4nPIy}G zRESj4ry&O{3&RGXY?gRB%VEnAukOW{QY@u1s*;^*UABt+4DJl^wy!+aUlFMUr6Nd^ z6Zx0iEG!IxU;hUE2@3E#Z!O36 zAgUGxzq39?e-c*M74lYx4AYUcU%bO%5b<)6oiaIrmgdp*dEXrSY^xY~jZo)+eNmO^ zl9W+uKE=L?dCSbq*SlDCY6xIyHTWYRX`OD8C(qZlRzbPW7Wwvlsm@nAC+24R`?~(j z14msmQ!(ngZKG>N?ii1ok2LtFKpV}G1m0CDtqh(K>*c$NQl+yW2$kI(-YzklEBT7~ zny4_jZ*PJ61|+}*z@$aG%4`cwjO7(wB+7Fm`gC z=nqCC$;ry&7J&;9W!)_&$hGqm59oW^T8@OGg<2@MnN25I;KkvygAWz70}1w*uk&Jm z)Yz3J4K1{gF4T~vkIbd)YhrbVBQz|=D_H$tPx#XiQbKJDL^Spf_BU7SW~tzOqtsYe zpFXz{28*3WUm6C6#~^I9^$F>hQf19)88IfpVfNu46zt;$6)c&Z9&R`{3Av`njZA0j zOrj&2l`GE(`Dno~dmIJJ83Vp%m71$ljUrvi_WAmrPx%G{UYQ3P*;_3L(0+9C#SnWdHg}vfWvw>q&al{IBh* zqZ4%nWt)r2lb)n=ve`!X${;UV)=WT3P*w|Ohnw}Sc!6|Z*J$)0hu-lWVF zv*%Qp>ZprRJ57hRhoeDSZD`Sq=cQscUAeI-UPfeH#TEfvPI> zBp0{wQ0?6d$*Uyyd<#dVqm&7i!-t{-48!GOXxHStu&(M73-;36yv-AB_?dIo&G#|%4$Kt4VVkP7E)2+t-ut zx&^WYc#b4Ts4Yf| z8q*f{T$boMp8OS7`a;o%R+7@yr$m=B$Hjya;e5M&(0pH`5-VuTGJo3WkgA>{#$Cez zg2&sL@7Z|D#PLo2@(cQymiR#+cXTdw&C+aJ%^p=Rt{UQFjt|cIwQr z*5tbYW6fOfAXW}p;F5EUx7*6)IIH7U0brHh(!+jNEEd>sQ!pOIhfk&X&cc3JEd<^u zKh$dFDwihMK57;O9@+^O4F9aJHL{>09q$CwcTtH_{%-NSm3D*G`hk!Tk4v)$&4w#X zcv0*78xfet;ziHvde1pUGmX{wNESnH(j24CXkFVG=V?d6;~*2)Ct~78M!I!ur-`7L z>YCu1m4q>nVixK6AJPtE`Mi!|$>t^(I6Zw3>npUOQ zQ5U`ww#Oit*HUg~eMhd{TEGS*F;(y>ar}dmGlKL2YHzZXT$qQW9$f`Nox5J^VaF9> zy|K1sEIl5xRGf3W#?MOa2e>ZH9_9HNkFN{*_ndgrNO%Jz%AWW>G!S#`Y@$_(00n2= zaIaUIykk8%O8BFAlY7WoplRQfnnHRQy!TygU(6AU4|ZgfaOAKMh8Md1DLvCHrcfpE zBcU8LP)Eekc@^E<>Sv|gzCJmAj<~D+nTx=Q7Pu-yi2tFeG{Kc4cTQR^8PQL<+T#vw zKGQ<$2b`Tpd6z#Cvk`7~Q~U*AWSw=x$ksB!m$ch^ulo%gGpWiSO?82CTXsF?&dT)9 z%Qv+Il%WJyd85XMg;+-#A(jSjk`G7U{USr-4DeLlEIFMh`s~?-`W%LK#2isamel_1hwhk59Yk7#(v6YZ-5!*q?eWVW{&dz%p2dC6gAj|{?K zalvd-<9ZfS%4bi*SU@Y(HKlR}SzBMZSx^Jp)RtRSbhY0-{d$enrnF2~DpZKMXPmn~ z(N0^4iGe{ecoF}0i`@ls?CE%;wX$o+kUHv%epcvS7F@Y1K*?BUHWjDIH_+glrMs?2 zlzksgxnDWFx;uxDF%;M3yY6wpU$LI3mE4k6N+sp;OOTT>R-wfg6Q*pwWiiVul^XFS zBnc0zTjBW;V&QP#-*=nnQ-MFs_O8keIemfpYXYagoo;0-H`*m!2qo6rB zC)nXkg#xi=UN=jt9CM#JR9_+ddn$PGDA#?>AC`@;&HW6d$L?)MCT5doq-`RSaW~(6$7y$8r0TMx+f!XYb6MV+cO7<;JMQk-jhqW$a7s zwO2cRhwe_j*iYsMTPp7R2pvbUI!#L_Wk`5dhSw9H8vlAJtasY&8EL1t!Zuk%qp*pY zi6_PQ+fs7j$1<6x21#Pvrl1f~NZqk6LyZo@DU-418$G(WlaiwI-oZ!$8M$G-km^yA z{K-Fr93W_3`sl02&kzC+=|MwSjzy;TNu|bjD<_>P+s7%DD!&aj8z?uwI~ZH0cY+Z;pTcBU>cMYgjtR4Ky!AjiiqB%fD-{ z$n>+aoXCxz9=Uv0(ezj*{8rRMqO~`>PEc6(D{4u6S&Yo_gAHd5Sy1yK-O@-_zdATJ zgs^Qc>omvB5Pa1j#vQG&9H8q5S(vvB)`v5Ng;L%ts&qkD;~_LxrS zG&5F>Z}?Ysl-R>D_(MbJke3g_*+(+|#Du>>!|tHitsR-=-eiyvS|@u^w6HiLjaJIi zyFg{yzMNUH>WGyxxmEjD#5c1|WzzibQdBoV2Zlhfb-uV};?n%4P%6aJ|MIN96ZDVA zMjEb+s@ypO?VrG6)jj5d2g630t8s1Il><0P7m*3P<8!tWi4=m^UfxewIojO16vu}h zFv`b^p7lG=P2P=VdFG%91smseF<*~{hXSgsRBXJWpF-VU|2!f$)M5Adh}8IwwT(Q_yEp?V3+e)KBcP+$yLVs8SYx z<<0WF(5w4dohwbGJ?YWz<=ox?p^epZjM&~XV%y!+J=&hLmeEN?!#W$-7gfog!6&RV z^oC$6#BGs3C-?XF#sNYt-#oJwh?M|uclLI>U@{+gl`u!taHJ)tmLV?%$?kw~?|ADwdsdGl7 zBov9LYDAxOSoFMA?xO>kV{T>`+m~natnLrUfsp1>)oeeArz%7H|e_>#fznJ zwfm4#@c_cX&k_Zon2nf(w1AH)^saGw>YoI{t&Rc7VV`c%CiDz7jLw%C+Al`$!Bg%= z!WW!lJ;_Bts_tKR@{-c{_T>^@ES7--+@tdK9-$)|=H;2E0EUk4LdSXcL&D53WM6{! z021w(@N?b@v4}5OdT5ug-4Hh=h{{Os?*@9K#{en?F)Byc4hVFxeEituT?+_Rl;G?Z z`W}vAC}H(zX`~&X^u#<3>os5)#g8DW)~{2YcEEW-sTT|4hsMSleW?mwrOZtHCxyyH z8@0i$SlNw1l_jSJUu!jeU*mvdhQ7X*Drvdtv%+M!Q>p$|^4S!|?c>?cK3361xt(rW zJ@bU|`-pxSG)pbu-Cm)fmV~|1V*`!n*w}AEXPyEZI{wl^e149~75du?Ch>mJV+#<` zb@=?T7VN5CuYTD|j%bIKHgReoo>1Xh%#~@LuLFC##vh#R)j|`RB{BlVs5hlRf=N>& zvBFGT*-rbvhB>S1=Eqq`MeN@8d|(|8=u*oSJP4!eOy%Le<*b0WxSxC0@TCjQ*h!f4 zilq?T!4`@+@e#$Mn?baqiQR|f3VTin^(m$!n zvA-#A0y^QzwpP@=~#jdjJF$V2>(k;esyJO&!B(z#mMjHSNNgF;Kk z;ecTbV4iYCk0H1Mo3!rXuK6GiFLqG%t2#rlkO$+2Oats7!KGb~=?B%!#wC06)Sn468A>g`T+_qb3~u`B9PiT7I(7n$lf)gjU%4z&xr^Uj-lAo34vOk#-`Q!yt}x*6rfVTCW`Kg2};N1E>PHkNaZFD)Rl zeBd9X%am)0wpDtIK3_MrxGxDvL?RCI%7IOTvI3e$%!yqqdelu{#90XD1>$7CbDFY& zmc8%unF7638FJj$Y?$0vSaMO!zh?GfCi7s`iK$kO>#DB+?)XE0*!S1c)Zj33-Ctxl zLrZt7YP4g@ZJ6FC!$6UUHSMW1mX0XAJ9VHCQ}K zYe9b7&O=n73os`=?4?bLh&-#lwNiNM?0F9d9`i2UAi1+aZ&+zj@JQ*2jaoq+4iZ)H zpRj=g>l%COxT|;Fymu4tBzZOythaUU+h${kDl@sszZ}Q)C*Ik=fhMl-db1UNeoi`Yi!s zQ-nem#lp^+9~mjfmJf?#ZIFiyeS6JN_`}*FKcI+1jWT%><4tg%ILfWXOl^lS(P=TV zs-DL3-cyyF=H8+*tB09X#mEhqEUa-}wBQNaj3*^=$X%|OAMZqY;Yyd;5i$50!2#0L z`>7J8k=2}Y+RQt|3&^(wd$NnF_m+ZiVzIXDX2>GWnAg5=>vaAVx&HGDhvoIVOoH>b zLcqvI5sFKuFSt92aX!(N~ ztNt;3LUQH+nZyrISN9BtYC$~2-5PAaGePugc(YOC5Eb zMp*yCIt>SD6GXUqP;*$OxB1;)r!zE<7D|B-p%mG!m~${IN&+68c~1Kt4GD9_<+mN# z{=*rOP(lJ+GvA^>*0T7NqfkymJ&FB|k1JzIOU>emaI zVKJda&kkU4GU)y>3gZ4gg%_5G=rZ2L`ygZ7eD2mMv>$G$6y>&^+dM2QZLlA_{lFx= zEkA4Z$v3IF0>%rxCk-u>`)?UBeLRUlhNY%1a zwska>R^*v-BF;)J7bX(;>p#8g){tg5W=f}5AslQF$2>b3p^{kku3+#FRuUk}dV~4A zcVTaTE;c1?=XnxZi}#{VI(QV2j#2-Yf;^j6EaGpv*TW~L>@q&26D(j;(CR(j^oEC6gge*z}mm-{O$={fAt zedaq|=~;RS;`TbzS$-xf)nXf$AZL=wSzR&2=E?>n$(B9ujRnv2`n?Z3uQNO*4SQy@ zB03wzBaopWsD?56Kjrj*7J_=)BEQ;YPd|>`n0d=po!H|_F|f~ft6;_ScC^0?0-%UcA_>M%_YJ)kYm3`8WdT2DQAu7ett{ zq#+dkNPM|bR3I)8py*Ah?IVBdKT+5X65U)~irvLG@TL`dhDPb?MJ`56BHSZ|8%RT!KW40;>&)Dd)TQoN^sb8g ziB^q$Eb3pJ8Ek`fmf9+3o0w*R6C4@cDq|0Kt;`XoWlr_WF&Y9Uia+-pHp^*)7CE9Ey$oT zb81+KT$b8&l0=D4X&uo+g1dEYS#LViTzHgjFVDWBKH7IYXPjj`k{tP&OnOmjhW`}X zlH9Zy1n-^FOHq#iIxx^6{|M4B?9t+#);&Wj_Z&--C>D&^kmUTSI~H{PFD$}Kb=$rI z@E#MA{62c74D{*FfJwbb9}91S;;a*dPg5cP-paJ3tl>tGY< zjxO4)JYyJ^4@f#T&?h0ni9ZpC>K;TlTc3#EpIG^Je9xjg;1%BE^6b2f$6V?W_Y#V* z$~Iq#cM`CqiVrULg~!^P?gjD|Ob`rx3z$IpVUK1u<#^Yuzc`#gE>V2uYKua1cP<9J zDZhR3k=Ts`>nTfTi+@*4RNFDIQ)p10e~TL1D4JYS2UzDByAfsX<2()Z1!jnvm<@h? z1T1`&iS@l6T{)U%5hOTE%)`af56* z2H))BOoD1Z|9ln%g=KmGbt~T?U;#2@qoA4@Pt@ZuR}c6jG`R?PU>kf9>#eu!n~t(V zwmtK?H1VOmF6hje)=U0*v>sicTaA$1QVnBgp~#QNqs2SHr6g3+u$KcU8J zX8|<_@-9=Y)O^RWc6T4k0G-n)j|_#dT`|2jF&cO32Ot`IFeS5S zMVsKQr5+mpz+oA;<+G=4!@OwePx5_iHob`vrqxZnv;MEd4h{wmkj))}zSk9j#+16b z(RGikSibdIemlBF)qypZg|T%I|2QX~3Q3^@ouGEz8?LmfV-9&YKX)PoY2#eMSlU6Y zNiJM4#(5pdOO@LOo0KP|+7GI+5Pvuv{$CY|8_hzPT|^iUd3gzA0Jd)a$ZB`fjB&;f zbt!*L-}{U5Hr4@kd&{Z}tm*6i%f-&ObSD{7K{|2CQJj{)Ge9gBhX0uX3ZTrntJ4c+ z02Y8@X$sYe&d3_eV9dp*oUtb+?ZG;Y>uT*B=^uAUw?mVMj5nIOisKexFKDQWkycE;KP9VRFVjvXl{ni93Lj-;XDrk)&5(pNtNJjnfmbzb|&pt zVpsxvv1D$peBH6tT@iN{E-#=IEBBoJ*R0G> z@RXUoM;tVR1rK_-;2jjflE)B>B{%epZRU>W#(Z^!^`DNce+ehoRfl(80RNpj(C+-w zhW0^Ma&?dz+<#GG22REL>8Imxn_-8S7ZFk12Xr}*Ndzgxk?F@Tn;ZR0j1uo>Y#efg z5YlgD09_8o5HPR8-+CN`8!54;y`u(Pi}<9LZptL#4n;n=23Xg}JCdV98{CFQ2~xJ| z=C3$q7IDzmC*d!y)=CdKPxG-V*s;)c1vz06?7TJS{?3x`zL8Vv@Qd>FVAR~?7aZ@y z*0FLM%&M%`2;Nh=?g-4>uGrchqSei!hYLL*KNp1!ISMSBz&x0r0whxh&Ny=N4C#A7 zP%14*i8$PvhygYu@S7rNX^G?7ocEdu2Z3ST7ZHs`B(xTgIy-^qoyM}{i0(Mf06iK5 zR4JCIrA%WA`onnCRi{JYpYxg>s6+?)kRX0i_<-M>Yjyw-gt7B73Iwg z=2;ud;cwxW4tCa>aLdX7heN;igDx3+U`NTeM@v-eVy%gtZIbUC&1yy#buk6Ev7xON z?qP4EEKqsO@@la=cnMV1(f?%Mz}fz*1uq3j4ireZFDE4CBhZ04zqDKwyCU;YHR^*5 ztk8kpW}t1Y0Vp#eitd0->6^H^;T>_zzxz334iF`yp*O8$+7h9jlOvl z>zHG%)u^qB!ExtAsr+iE3>IKPm7FKSv&j1Ljtx<4;RnsbHThf}_lIShebIGQv&?Z< zEwlw&POr-P-h@nGhDS_mR*?9^^gJ&(gFr$_ z_pr!hb*i11pt|F>a}7QRmuEY8J^8=_njk&qHM{=K*tQD8c3dKUCq`h0n%o6UlIabuSKI>hda!eByGZg%6}# zGL^NP;JP;|gmhP0hG>ODh{1gPcReAwX0nS-_C_s~ z|3|fu5oJzLoj#v17cbhl-F${x$AXeow?D!#oGFierTjR@GM-CT^<=hS;sY8?pfv4M z9)zDsv#Ed^D2tBUmQa+o=wYVET6@atCBWQE6RoyIc&{jUg1X=zRBbFD@m$@~RZ(Xy~k4FUJJkG0qUf(UZfk-5on^*jRdv7U5Z{T_u7s@Z=@lsRU z5@5QY{>9gLtF%PtY2NZI`X@60b8TjRgcG&{09q!nKH7KMZAXwS`QQ;Fux*yOh0;Hg zwl&>;rRQMg>N4hN(p~`!KH+T&CB9eS|6 zyAqv&qzNb}-J0h4;_Svf8Q;D=KLu7c86*QLNY!R>=W4$MD6#g6z1YC+aTI$ z2lrY*dI6iN9VfPA?d3hd-WoO?hkkL`c_tpo2Cy zh8*}RS%44lF`1|1(RGl9uR@~J-jP_a)T=q}p7NfqFFyz(kU#D?H@N zv7b%U-kZ%6+8pCTE{Apyn$ir|QU|{-$6>g8fmMll`D=;<^a*rZq>g~>o5ByL?V<1< z!Bp-?P7_eo`b~06D5bD9g0z~ZifC0xEXc`NE>t*iu3JHaj5&-*%JUvnTF&6m~ zoyJXRML&vU2wfq2W#32G%bbazA-CFTlQRHc`f}eax3JE26?aAClz%-teI6D!pa=m& zW+#9pj%%sDWYQbKz;t;zURIHl zn%+_rl|8WKYXJCP(upfzCDaR6W-gv3;M>e@u6@XBNUa+%i2278!%AQVzE9o7k=9mH zwzsKuErx<~;71iq{w6vS*uy6ROo8|7%>hOkB=5`j>h)~wCR76n$?DHL zSh7TMW6^g|fUP7h&sT^uMp!dWmm|je$`vKZ!k$0ll%NE8Fr%tnuv79Gp7f)F^D(>> zsMke=T+In%L;PXF;jtFm;0BqV5gyX9-{y=;*mP;q@C+=ZeOLSI@kBZ;Y4MEoL1U@7 zLyP!=$vLV*Q=#n&HZJ#F!H&ijdrZWKI~`-(g$X9RW?QShS;s~wGjh%fsh(h=_$gNh zOSxLtSIY3mS|7Y62H!EJ(+@@uU=1!gCnsymE-xHSWxLcR_0aB1;A~*x#j<0Omh29` zCA{pQ+Q$j1s?jRw;`m2;N)rOmQ=Q|$iOQA>ink~CrC_3~jqC-XrzcOxm&#Ci!U_PhBFYhbfeZ1xCQ{MMxTpx5_&AXj$r@5K52WkwVU@ULzp= z>JnCivt-%u@%|oHVa22!23~O!vC`ntjHl=NU>4T(bZ-aW0A)^Kys2cMU2CB_1-BkA4t77ZZK; zvk{X&Fnu=F6C2%YA8vJ~DHS4kq)sx1S+3mTISpOG=_v`~8mGH-_d$2zvEuRYTSfms zznrTgxtQAv&GLs;g&bMsZbSE%anexc6U3*Lq2xf>ud!{BFTId=oZystDZ*ml6ElY6 zqNf=ItiA6QR3{Ybc02Salmm*z*XBxD#$0Yto|F2_ynEJ3XOJ0*VLn|(!u7>qjyYkw zULFaD{wTMpH)dGdjvq;T%wLu^u@f(RT+QSdws?(U2rO!}W42A_ZqgRAG6OZtxC*G^;H-&_G^%dr9i zs``+}sPCg^9*4C9mTSDbSk;O~uZdmK$ysuaZ&@6Z-S>$5L0A@=JJBFMbmryZvdi1b z{8PU&1H}*wR~;VD9cU<|RqCFE?tM?-4#7686M>V-$Sl7ajA;}ZwOjE2^oo`Zv|jc0 z(G&Nihk4Zg=uYHS6kgb$vh_1*uxqb(EH-bCetO*B8j9WhXz|C3izOD%)N2K}dvx1z zC|fDd0AuQ!?t7ffq4j{ACv6>?AFI9ClO6xUV@nw^Htn+Th>O(ypufiQTcj@pY%%tk z$v}iG!lC7<@vc*0+d|FhcJ+=)W)y4@Qi{<)f}7T4IshV;IT?U}v2ozKZd++(*m77} z_0ppCJpYHXw+xH&Yx{*2R6wK!BnO64DFp=S8l*umkXAxKO1g$_$pHarBt%8JTYBga z>1JrD8EVMA@PFTXKhJwS@4b)xML(gyTaNwSaoRDC@E1J0?O_jd>ImB2PR(Ihsha^cd`;oMKLRqgj7iHi18#O%?MQ_(xe` zbFl%Nj>Y?%!{45@!IF?#vlt#$^qYpG#OA5?NQQ2cHqhH9vNic}DawZw`+(f7`di^c za9W1h(x2lAvaS)j1K|RV*D`Z*%Sg?UCp#vP#|y00$ALJeN*+|U!X(;9 z^cBw79vvT0zw}P+B~2tLx9gm>Oh|JaZsPp4M-ygoktJb&O;n3?nyD4@0S!095%0pd zlA|?Amj2|Oik1~-+-X>pQiE<_nB7-;@ku zk6r^TM0M_0jo6{ghsbXh`d_|V>i}&7pMjt5-T&r#yvHl{AJvaU!BValFO<65)&rKb z(GyyZh&#!vIY=&vz{?sQ!Xc(6#b()WR;816`M=H2Ce`9HpTt4_I5Bgl#ZSf$y1ex1 z=?Gi#Do+u$cRI0TcwU=Vl>atv z+CDRS>F~K&aK5!680W{xu03SNf!fm}U%M7(#YUoo@~uskU%|9$S2M%LC|UVQ>jbW< zPe|RCDD$;`J}3x1Wj2Duu%q*9*1yx|5F?Hpo3SCqs)-9(_k0ELhA1^&xpftDk&1te6&2~e5{a&(dDYP_>+D}duEqy1-SgJOl=ie9KmVwTK_2#hdYs^q? zCtCUnOd_QNZNz^lDsX627EM$-us;N7!3nDM98xTb&CL|{H6~%vC~7j?AUYT$#n^~T z*n4#!0StRKsq*4jH;VL4eUoR3M~NSAAACNEtKnlDCZn4eb$>{n&W>iNv?i2CC2#3q zsMX50&sWfH)X%j@^+oF6A5vHA-g<7o6mnN;OJ~^6=4c~H;Hx*~G-eJ~v)B;Ypnvh*(Sj3F0twC9w4dME7 zCdPUXbDy?BSIb45)!nuSUUPH|!EY#dcs@51NTtHm+j6he9t>kW3W;38rC!T4{)ux z-MXG@q-m_xGuoB>FC-~AAz3^2FaFQ@KjQzyzB}i6ALD;0flVcB8O z);+b3!)f}hXVU&Uhrp&UwZ**I>@juw;OljnW!*HG1idWjYLU}@@(^t%bH!%CuOUf6 z*U$_KuC7@BY?g6-@3Z{?bB{ps5SNRyZSU$=hrpT~Vw79?<+ru375)(O5Ey%UC#z(5 zsiEoYqR?jgiUaPC0`r@oZ#zTsbe`nT_f4i7GmgJh%af!8M55WGteUBcn4aP%;*;T! z<&*Gk9ZXpA)_~@es`JM4ecl*+2lHpgEiZqTwDHgvxAFDiMzc#xC@6syMu-8FT zh(U0TTEt7;GWlciv%TvOIIAM#{<<$sd^EMQ1(VPZbpa>NiC=`xZ{YBfT@9OwD zY71{bIlw;}8_i9d*#F!9Rbic`Zp|^TKR|DkL^l2US7X&zx{Ff%EfwB``?&*+u#_Vl zuW%y1;IS>GPSw{Swb~<5+LPZ<7qu#TNy^9e9~;VF+Nvg`{Z0tIV1cAdABrz;X^i0+ z9(%Q$Hoa_(&hjp5z4oMP(cs>C`lLcg&*9vQ>aV$ajf{q4e)I^pVtp|$Ar@YOQ8w7R z_~}lIn};*lr6i)6igTy==T`6sHGzad0YHd3v!}~KDn{zZ##g7ez*0yl@6h(B=v9Dz z_!-#JP=UM_W}xYAnR}JZ@H>2`36H@0&kjHve}M)|l@Oz_01pmWGXp>h4U>0+Nh}B? z&0dAzMlQvWYPf>YcW!I$RloUc7iQbfx?1X;#avQKBH9edL>=QJf(lg~7g*6sF2 zhR>qkNGchQg#N|dmi&CI@H*imT2O^s2%BNQnxcHj52#fXn#z zVcjp7M6}w@Xm@-UwQq6qa;B^^+!1mpI&(1+JS_6|DU#h(+a!k9N@1vw{odIpn8ay8 z6QN7$>)2W8B4tKnYZVZQew0n301s&8s3ARyLG9-D_mchk`O2XaqC5hZphzsdZe3&` zU$gV$w|RSyoZ|%6a3K3$*ryvE5q70j2;}~C3M9O!h|*M}+NCfx0dr;P4;|LFIP|2R z&xNM`uoGwWZo=`Gsy8T3^G?3Z`C6AR_0Fp@iyXX}n1WA#D#rBJQ?oQs=Z~94eXAcN z_Q%2Q><-w?-aeEcehQ2cS@QhS>Ix%+J`~q0+#8qTb&VG4otrEE*hdT<2zdVVxq1!f zcxS%F7{_BZDqWFu8ZlI0cNZ^|B-_K2Bv6v)3bXo!Rf`b!Jgxn(hI|_eD`0p!)l98n zyA%a2y8xxFEu*$D_L0RQiEh~o&?NUMJfuMy1}dt)P=2#;d;Qk)uVrIknDmvHsbIwP zA+3&}D0M!K!`Ia@o`#OQxx(D3-_8|qaUHhJZp?VyE3$_mqsB zY+NU3B>X^nv{Uz4Qf7_AqIGeLud)iVT0INLmrzK;$6H`oPWf0+f1rbNl~hr|ovB9g zW4h$|Zj{RHpVhvsx@Sg&sxU!6Sea;wkb4S~06u+>k4W40|&IGI`*K40UX} zPCbBKp=K|JerLj`RFFYghG5hvutCHOUDgg92QT9B17H%j$XD`HYCrQVLCe#}hD?A~ z*g`+nGXQ8*Z@KXH4pstx@km@T=1!JysOqsapCfjB#h3o@ zdX?5EKGU1?6Azu%pWn87H(9|gKR9g^#@*Ul!UL1AcnvB&IABR~lW*?FAroK=lb|yh zOFQ=cMPAXkpIh{i;Mi+i_PQ{N_CSS>pVtOWLFJz%X!_mG`D99tU_iOSwp#jcX5OJg z@RIQTTj?wS#i>d-SzevAn9(&{-XHQ>eDQ6z9IxY0(JioBn18o0YH&}Za3vyhzf0d@ zuD(Olw06YJ`Jb;I&?5sv6V8k3$1^%Yp*_Ja_#*ey>+*bBG4DT<6veJAJte_R0NW|H z>S|tDcCj+1wB$>~-%yC7&anRfOi447&UjN=R)6w-mBz-~0DVX`C>JDHoZFerXYDUx zmN0doGp3`Jt79U;B_*FX6oRZ|rohfHsRKhzFC`D0Xd)kJxa&+w)^dY$d}~R@ly#8t z2bY8RiG)2Fc4l%BM`xj8B!a@=X$ISt><=0w{Obl)59jyqUonx%n%lCK^ZD=&!~F-n zZhc^$sZ!2~!QK!@a~OVZ-40?$f7Z3iJ^51#P_rV2X~0l-BPL0kh%8Eb1+y-P${&k_ zVRKApip|c2IXqs*gkxY3EYy1e?kaQKV(xo!{0I`ly%4d0fCN)=qPrH1Mbn_FB_Q7c zK}w_mC3**R6#e4zy4K~CCjo|_jgM%58=yqwG%El@H`RoMljo>coV10BST%*^M+Dvt z-E<Ga`izyS9{_XncaSru9=cvGvhNsVc}!wUY83S4 zH*!F=&zJbE6;efmn6U5;x?Z+EQNuwy)qu!roukMl6;KZIfEeoozjjG@23Bb)c%7$B z8XTrgjr`b(VazdaaAL)5*6F*5oFlL94RVw+AZ+QmU#(_MKm1~kc%uIE#T&wBvU1C@ z%Y>jLoLJBPt*ws3Z)eBZtY2;(`Y?{jApfRa4t9*4KQYrS;bZFRjMmx~#(q5ZZ9_w7 z`b<27etyI8x=L%iU|ChWf=<qw8~>6m6ggT1m(Bm&24*2(n15xywiR7_nl@eJayKlra!hl|NEs$yf{FS+0UQU${~^_ zuJMJ<5x#SJHzqwn-#|HoqyN)q#^rK=pP=nsH%zc4toMiFUwdcV|77p{2jKXz zBya(^qAwV2N08#gj=f#FM24jjfkH-7Uyv`PeJX0WrK^JS=pw`RHIC;yoCMCPZJdRV zhzYq_KATcpY&wYjWs|vH*x?X6Wq^}NGC58}vs5|a>Ec3)eaqh9IzZA@_kcLaAX0Iz z(^ZTE?)#^s`EV2czHq{oTW2Z7(_*dey1>>?<)1bocCWoh(-QQ0NB51jcu*h7wBMaW>VbrCD90# zvf}#KC%i&`3^28xHl~VcuanIm2}&q`erV@ahm^*Uj1c4CP3JuN>f4hGemy>4Jws-K z_W(FS!irY$9NvB9t@y~vgkLV2NC{y#uWY=G^uKN#+p59yR zs0c~WTkv|K*NhFoe1AH$Nq`_}08j=9Pr%tX5I zEqRn~l|n^+L?44^X=Xu(z1rCw1U9}}H?ZXrbg-_4)}QQ()y#eGEOVsU;-9K!F!oWb zX`}4qSMiF3_d_uQOI6qH2>D{9mOWOJ7-M?5$egIM?O+MAe#8rndekwr@bz!G_@gfL z#@V3gUa*0YxiA!7)y_n#uhZ=0l%Vg-e0mZCE{QzHanl4!D;&g4-&B}JyExTWJu8qKL_heKwtdGK`t)r4+A(@A zIb@0lS0w+m68GyOmF2r<<7jlS=w@QfQxB-f8N%DZgp1eyLG23lHQpuC7a z+{5b^lUeoWxh8=?+sG-0Yp2D^iuYJb0&sls#*d+x|L$`p{QRG>acQQy!}~eE%S1rZ zmFi0A>OwMmY}beJY&{BWI}e=hpgZmfJVgWWj*1w*EJW z7D7arf>l!BQXy zD!x%Xn_4><*+zR}=H2jDliZSxy(aX`TBc!?;>jwi3Wkd`R)rHu) z=p%t*5?DiYXxj58^)Zw_KVQhu zSM6~bhwe&`uF{2v2c!65qubtDtDJzhFTMJ({U#y%6n-B~MP`5#zFM>m+>fk%f8cjR z6p|(O%HSsGe_a#_DG;)ms(M~hBT&$Zs*1YetBUm22I+SWz+PMzv3az6P9khF>AFV3 z2k3s-LQ6TtXnCD+E*)22Fe2MMWo2Vx?woyfsxl zTZE=>c|GnYC$QzSVKFH5 z48vZsU_+;wUh3Rg8fW=-;;e8WoAb1^JiIv5SX5`bCxy_Cd)anLmEtceW%rBs<$o3s z-|*d;$$OReFDjS%7kE#8$r*WwhYA~a6yB?@jr@>H0>p;*Od8ZI3)SeR?QW>PC&RX< zxH#;gGlK34;hx{vQ9k9 z>oJu+YkSnWjtMKKe0?zPW#i&=Mr3a7S*YF-L?E#JC=dFsKlb=FhTx3YeNUz{o|O*B z-yPw5gYv(C0s(0Z79;UhFew?G`QQ>~06lrK(^=rAcJVYgJ$9Y|JtD6hZIf{TA3!1! z&yXwWlCFBX0UX&t5@Fs#B!sGvF`@z7lZ+!Hj6|m9Xd%qavX1=wQZ0Ud2`S)RD97p$A zLJfn14;N-<(I>Pew;A|{y<*f$2x#9V4i7FRJ>YM)rilFf!TCO^#me0|TTC*r>O2n2xA)jzZ)5cy=B{9?v4Oy>qJQsT z0pqC1RB#C9YW;N?lx*9jlDxrN>Kqh)L{wJDH;^KDLtC}D{1_)VTC;KsNS1LVPitA6X^R2zj;C_TFWnC8d*?)sdUst0>PFj_ ziHgr|;C`Fk4<4?$=yoUx{|k4h(gT|zUQAl_5$t9n$>FNZ0tEqGnz39Le}23VzMGg)DV-(T4TA(ch=3_%tJFTqZ9*!|t_PIYY7a`YeCDdji$-jHr- zFggYHtVm@#Z5^|eZ~hLPnY}R5S+5xRt;oCD-BO~ky`s}4wd`;*KbVoMp5uJxF+?qI zAQ#q{{AXPrGq<}m@oWlr>)r1Td)=h^JI3*UwPVRAs3}0h=?`6PFi*qN? z2qrwAM{0*u+?`}Mx@uU_NXbEJ`xFm$U2#m4v=ye96L}%jh$WKehKWbh7K(VGMDX83Ev9Xjd@bD#t~^BMR)DXNNC7_N?$^3=Yas9@x${#Hb0sy<%N1=c0FP zF>}hcH>Lb*)pwA>;(vo$l`JXfmwDoOiVjOha^hAY?G+n`Dn)7r81(9A3 zce$Ng_9P_biMp61$HXSFV2^KDMh%Kj4J}qDJu4%3&i%Cj))4)isQDZa>U2aIRbLAV zmh)F+`)kOtmyqtDzciOT?L(t$3;?SCvB(*Ed3$!tD1_6gKaG66fL{YZ$f|7c!#M&F zYY3qIadX;Zdu`N)55|k5@sM8Rom6DkY^$2AnL{YV*?b2Mqr6Z3oyXn;i}B6{Rru)Y zG@o7HdO`Y)<{q$?r5jsOgr7(6h?0=e@7vAeoczlweV6mv+r~WtPnUIZtnoW-McnNp z+;ZKPi}QY7uGy)DZw}^D4Wk1?i5Sd{@x`%O&BOV|u2Svxzp+8f0RZ?o#ng!fTZS&` z-R(CW47~zTLwu}LIG}_i3?3APPih!`F1-Ie=k0F(i}72kH~wCzpYI=(uvr{kMxnGx z{*R2@e?5lz1vSIUv4AuE;?zUoX6-*yZmDG;=J~FEe#=)d_=%250~`ve*xlp&<@#PV zJZL2;BGhgby3Uc71o~W%EEnmswma}q4JoKYD@Mp5M&CXIaINJ50Bj_=b`!3p!VBX5L(P9&BJoGaII3 z`ald4mg?ZsPj^nyqA=Y2lxEV$CjtxWDG{BX9P1Qt5DGU7G@6V z`%62vJksHCX`J8i$g+&+&mFrSxK%ogz#QjK#XC=b79~27Gg}{Pifxs|!Wr|6bv&jF z22R&=_GU-TezB;l=N20Z4`podiwRC8ZVfmENV3n}lL}stAeNDb6=porvX=GuQv}U# zbf#n$na%3&%dUl_EuD$UBzD5H3iBrY#E{tBwDnA)+G~DP3U=0$6-#i>hIJ*BYKd7bzCJ`G<_5hD?dNppnx9PixXIrgWbhfmlnSuguWri9h`l|2m zi3Lz4&@`Tp$(*;mWe8Pw6ziThV1NuV+ZB@Bs}bvdM-UL5kPYVGFreE_p3_nYs-i;k zd;EIxGX!qxb&6^;i+{6znOWleKW3J^A;sUW|4^>eBgv)Sy-~s{-pg(ptmYg8TZ2Olj$3SsOx9zV)tL5KDp`Dzx_6ewr!HBRM;|jUv@Z&`Aj^aTVww4 zcNMpT!wa~2gNr=yPBE>Yc9%0Cw%O%TC+$%Lo>gALP1EHRG1zrk6o$y`qW!>{$t!T} z17|_TGc{p0!F2C)>?L&HihU}5TL|W&IN>g|Cubr7^Frt-Q$IoF zJ?HJz;Nk7kfQay@@lmhIqpN1#C8OGc1YgH)l=QW%fZQiV2y-ZjGxMgRfJCP(AHuf$ z-j%B@%dJ`?Q-LFNi$)F6?j`Aq9BKBaQWI`?>YBBC-+5PebG_eB-oaJZ>a9IKiLUY* z4qhjrsy5rn>FLcqJ6M$q-b~j$N)<3AOLf{!*jpMU{OZy#=(y&F`_*uk{@dnf4zCQ| z?UN>k@KX;$i)a0alU89e%9@tjQKP?|J7QMDvub}W)431r_a3U}bfhQKJ(^El8fg|< zSn0t;zclUZ^V}=1c57X{C*5>*Ws5NSrM}Y(nZ2qY1@MYq%l{)iA+HN243fbByNgUk z_>SzgX(GWfsqe$OMjN`Yt`;mkJFK>?q8Yp@jqM>Jfq_GGwslPiPYvRjG^=Nv=SPb^ zz=rv;lxcENhn_upYhSSLvB6^-g~j#=<7TmnCb{NM(oDk|;nK^%VliDsZ+Ph}usulz zvQM}jL*vEy$vEUtaKMh&sPkX^Ih^e0*@93keqk22;(*DX@L@q0~#oSEijX`xdK|-mlY_ zb{{R4tD8@03uy#5;acoOaG;3ZbY&F|1#CPiu)Pe|cbTH## z>m|(Uj0|EzdjcYf{)|{-o0W~*Pg(me-3KJcrr!^Iv`_RvYb3RW=vEbre5^8*%!Il5 zvugBQjnl>x;6_>E**owU;>euwEegG>q~LvVGE1O)m7NVXBfOR!`_8&8%#MYRB$M&Y z#**jZbVAm7Mno8I!>2xp6Du2@jiW&bXpp!1{0qqP-E;wOQp?M390ZftE$ntX>=WI7 z@5oYCj9pH4xXohGZ-TuH3*407rfGcDuCObrWZ?W|O|ij<;@hMz(Y@Xvdw@PYOUT+FfWogJN^Hma*(O<3Q!>$$IiT_jRnrUJoJvSNG2> zFKV9bSWMUWRUE%Co3P2_DLejopMBg@)az=5mpg4ex?L|x+$AxYTrBIF{z|IwJ-+nA z0;vo`k@EqYvVC5Wp>S>GRk2D1Og%^r=8*gVLf=^URzXam*I(5*sD!tzUr&I!S^8tGK7Ab6kWYSraM2pbbb_P#FOz zgH*cb0gW%5nq7}tdQMgZoar@y;1y6JNK6z??Si83`aZ%x4nbM|`-Ik!eRAIJ7Fhc} zRZfwWzd9EAlEF=^e0`?44$siUv73tLiyfYxdRRkWBV%FJ#*N{XKl)+A6-NKi8tgcw zX8p?^9?Y8-_fX>8fmL{PGj1S+EW1vZjNmzHTH$N3+a^f*girar&>0%b!D?&&`!+S1 z=l_q9sSg0-ZqTNq3REwIK=tz1lB~L3QnSiL@n(0g^E2=UP;y|L^emkwPz9y*h;1iY zhXUPbCti}GhH2B-&jP3zlT3tdt<7xHgj>^-x2N02$Hxnz-`r z*zrx(hRt-IsOyfs!sgQX9!^wpH-{sf zH@60N8vqY$U~=Ogq3*@`9bSM)LDc`U^J#+|RJ93aNe_*0*gg{jW&n11qi(}!gKlwO zrU%Y{Ud?_s>3AGj&Ki_t_0mEBmt%w<8~cT0f=JVDE2(;Pjd*sqq?Rj9{t-7nox$fifB~P4PK?RJWFGZj(rV2z2Eg(S8|s92!X(55SSBK32W5o0ejM`}#7BOpgXmU? z8n(Y2b`eaN>+T*`3sq^j@^Y*Uff2^-I`uEBoS!@?ZQsOS$=FWBdz!#Knsbk&QRdnC zIlicU<0{CbKh3H^5bbMupDviIu>Z>_CxGK%c+t0;Nj)S+9r}IWT>>ZWVtUg%xJ3)y zwwG;R90x5YaRl`(QAiFG3GPRT?VwVfA&#&wSNrs02=hvJD4#8jciR1auZ~6{Rln*c zjOd!--&o6&yS4V=L4x%fjc4B zEZtKuGL4hLm(>l}FqPv!kYSDFbA7l@fW(dL_{uKvV07{CGXP@tB6!*k0rru7Y2PV5 zJ2~WlJ$Tamsbb1rc|;jIq`MJx4lJn)=X89{Z^nX`{#63A9(6@NPjo;o@8o{S%gV5@ zmo93U4=+bkvWQ1O0Sy)SpfZG+-!Cv4?m8|%=9_((z!gCN0sEp>B~0VhP2; zhi@LaBS#t#$}^a{j~sEKswS(P2dtnW=Uc;g(v5#oh*=3U{_pIZl4bcPTg~bU3x+cV z5DRIc-xYiLc=F1D-AHm&d;srPlhk8McFYn5*2Z4F&x>+GvI=_q)aC_k)T~5u>26I! zX@~AersPG+R=|eVihhuy)7ED(d+B?J2;Ey|vNJ8!hn)zyXnC|}7TckA#YYj_`iJ|H z#Ts2dpXRIr*wzYt4UT~!LqB# zdWBAm5=>|2lTi!^_1L2fODJS7RxRD%e<|pb%ja;6o}c}Jor4pbzgAwRuI;Pz@6=p5 z(}&(+%YBA^_iCu%bPd85Bu>@Qp7-DTzH}>dIg<9YXqRLlp#4ZB=yDg?mX!{lePjO$NDZIjg_3~Z^jk=zGc4hgGaYTw%FsHbN?=S&~0RQN%J&?vEDYi4k-rNjKWDMORq#V{OYndxagm8 zx$;Uwe;eYQP;$5au;A|Nq9<7z#l9h9Kgo@S)(us?o~Xk9aNl>1P%nm-wou?fZlRf&XSJFIIk~`H-9*-&|9tE9zH*g|- z3N>Vte7P0&+c9_jfK2;cC|NEiLOm43_M7M9-^Kah1I++`jkAz8Y%5VdjPrh8YuOEp zL{lc~a>N$%nk$c_{pe*wGY3WgVOmr!!JJK&qMn|mH4WALD)62n-M6nMqH}YN)1mz% zZ+X#OE=lbD<$$rO-uCDVU-z`2rHZgy_46ttB8sineu=)xuS~a;v}o`QJ19K=_&3hl zTtm%%8?Rz6f4u$bo%5e_SR0$b$+DTn;)fsMI36^xwTI#AjeV07l5)%r)W&hsOEFAX>t)T?= z&Zk8~&63pf_d0Yne=>RY7Wek|awWYjc_x?Zc5Ecx_b$Ev`aWNdAucg$ay*VltTuuf zUH)(!qI~pfw7@h0>1m20cQ>25Uz%dUY&N~7zaR&nEzt!8YbXwAb^XCp${G1mKB z6E+9+0q70XF-+p93wtw>u)nu`SGDg9FX^Gn&I3qwIfIjpVRBGok>5;eSyOAt;XMhr zekkh&d-J3jKMe-5oV;`tq%-0T)9v*FvH0QEJf%;nOp?u zqCbFa0x5}?fYmtU=coZv?NfN;_Mh?6Z(uPV5;tW4=;g@_NK#t^+~UEB(YR@kdf49& z>x0yi?CXs>N&(9O|2SJC(Kk;vw?xC%Arqb);Xh>WUp{_M&+%DV&}_N})E60~*61kW zoc7Z1r^d2wHgUA2L0oNdixcdLX1qsAdf%2H4?pmfZ)|kWj1BDa(@Ca{#r)&iL`N!b6H#h1m%a(+E%BylxWKXZ%zADG$ajp4K{~PoRI_jN^bs|!u!AN$q>yyjaaz!p@OCfkW zpOV=P8<-u_pfwR#+JjX!JP8+WZdweDFETjT4*sd{5#*WWBb%S;+4aBjYuZbF>V zwh$Jw?M3DkJCnX? zdCMmfQaN&$KZx($Yf|po)%H193abmjZbG%eL3xwP-g5a)qgatCtv@!KT}cRDOpcTE zltU<-zwzFe;56-R3e0BPSp8FXY7Ja5L+xJS*N^Q5Q0oN(Mc$UQLT_lM1+9YCmZz!b zjQTFd?>x0uk+3%)MOKL=OH8RrAhu)sM+cC_oU1>YxWA32?e}_;4`#CInl}gMnWJhs zRhK1Y_xOb#7mZG`jrM%L$KYhn@^11wF$rd|2giLapVINDiD(rmOUnibsV^zH(bYTf zO+C+NAw9uX-rlv@*BK5b-38vFMUGcWIo;g!Rnjh~rA)+#n$#Tw_W3Xo@E`dB{a<XtY2d>rX#%87F|;3tr&)JXva{ zZt#KHIU46uJQ;$xt}V7Jt@=S3g&RtueEkBLSWPuq+odc<3mDFJT1W_B%4>z?|D2+q zeR8iCR~iTEx^d9^eW|^D4E4Wf@iZ5knhu*&Md=vy|9xH&s(hL(Y0#ZM)$BcxBYbJ6APuI zrq?o7g7^^IcqS+e<99m9aPmRJ_Rv_%8^||l80+y~_J;YS&WW#fyWGm=BoSge!uzS$ z2KyeCVD#DG{gd6&FU$m6hZEeLiziJHO!i1R?ehSqBBLqlGE};)MrXXt!Ghf&kL&H# zIBix>IXZh>=7j}WX0xMS4sy4=O=h+nPz=g;nIGSLT*_EesHYFBoEA;?N+}Ml*?yT| zQ&+?Ph=2S@s%+9I{)k~0t?6#LcfW7SrL8T0ttu<&BNRC z0BW8H9&fFYML}R9mehCp{o)Uf=AC#4-~p!CFl9zFV;GQD;HFr)!_-<2QVt28DFh?6 zUcEwIlVq-C{s}TMx@eZk{c@i2)T~0?L8Qf49)RXl?yC$2CPzu0yIw}LXgd@5*ufe` z4ZWw+AqsHt=Fbs7oGKlDUnPmROWrDZn{+EuWc_YD;?dI`cOU~?1wf&hhbv}()R14X z5V-VJZeJ7RLD&Lx=rcfVV`_aBLYF=WSx>ZzAOqDUuqKbQ*Z(z*h|>7Ry#r6sET+GHsiCg)HCY2f6bKX6dz zpIhMaGANN}g3rd?H%jq-t~<88u3?{|I+6Kq^d*({v6aY@6T}ZfD1k9${*8q!SGSRl z_h24)Ng!RB1HdQXP#k{bM2x`r1A3#taL0s0m-koijDcoFP}B36Mmh`qj_Fs zlGBL}RzPEr_+IaOkY(-B*aLb8vlzthAOB2pbL3c`8|UoZylM%8-0Wr9z3O<2S?+D%+I_0b zrA3vjEBgd~sz;-J_}Za#TbkGmWehc3VSD@)p1-$EdbF|SFY2nHjUCWAd?A9+O(%|X%6UAIm z{?#fMtFy5Fd8J^Dq7A{E_cZoLywT2VL32!_TaD&E<*!ofGTE^4cYh#pUn zV%ZBagVSCaMM1VHT#WU1-V#ovIFAMN1iPKiUhiG~Buh8YdQp&ja$ev37DZ8#b{KUH zCDF16v%P4*T}O6IVjh@|`?Ow1Q$<2BQ94O>&8)8)$U2S- zz%TB7TAIgJwMtbilScRH7g?07w@Kur_r=*+&RaSGb19}Gr}bgQ=G-6p_6#Y7z_QZ! zc=c@wA)Ypf!Hy+kQ=Y%n-Fu%dLdG`%YCR_`EqpZRzqS}9c~I+5N?^o#GsKN)RW9x_ zh?amOWlrd-!|NzM6C`O3SZuUGepCG#IGaYob=5!#xY|x;sTsS7`9pR*&xKd=PqVDs}iJ`cxQ}Pfv9T9?LHmaUn9>9IuF8w4q zfmxDEIZnGxNrak{(b_dUM^I}=v5#3LO{Vx_s1fAD@!dM;wJYJt3MjgF@gw?Xxo<^S zbkiAnxqfumvEe+ikIykKts^Ccs3&0EC4?V6*kyV}Iy&K+9MF>slW2Nr`S&_dVqt=_;Lh0jSBty`|65Ac8u~zPb+WJ71VE?hw=#a5C~R3sTG0W7e^HSGTKI4 zl#XA^p}g|j~wN5w0_7~5N83w+I7AVomAe|xBeoq z<=WL0=e=NQ8PrLjJ!Wp@Y!Ua@n^Ap-wGYY@$!rdE4qR3e$Bx|MkKxSLcgL2;Z(WVq zWk1gAL<;>Ry#kh1JPCs@`Zpa9@05to&)_^mKJ2g+o!&hhDO2y)U;RCpXVGezcTek- zo)2;yHolb|x?LVcZ{Q4tEL=t&fAK!Yq?1iV4U{P?qctln`r|US#l&Ry@>fm9kJsK;m5lGrHe>llYp8GK|e^5&r`p z@w-2q2N0T@Iad-cW6Lge><_W~fWDJPE>ybjzpG$goSv$7xFbcWxEVmKkX(D}viaNByXo(wbFGW%~a5Vg9-e4hhYU^3bT^#3Ln0ejX!^5-JbX z(c@X1wDtV&hVbw|Z3yjH`SL*P0!Fru@$BAweHw@0t%+v)S1uB4nG^>=C*s8IK-dGU9Xxe0#$&hqP{ZA`N1Cz;fv#?=P6vV-%_*h1i+%3KhzB}4zz zlPgsjZ)%z6on}3$-H&(A%uBV0oChEhx%+fte5u}|K6k59Jjz|-R=NcRABdVXg!Nfi zhQO`s!{Lkw61Db(lRJmj>=bj>qs}?9B=2AlI`UZ}mvb36x3^MuKV6^j2-;MsR9_G&2e&UQOag< z2ct65kK;N9aP%#SN7re(tBpEG*M8-O5pHjH^ERpgsOY<`Ke+IuQY?Ree<35vhqWFn z^1XTUCi$PImoY1STh!%sw(P(K>wT?NNT_yt_WVGIR?zCzw}-JbAgq14X-7=DM0TY1 zU~>`^Q_HkQP)t_rGYa0)3c2ZCH4yjC7`EXWCrd-T&o&?lEDP9)V-xt=A8QhWRk#8- zjE4X10}b*|IQ*5jAQCA&4M#?^jv%CL066adYycepORTy?;v$RkJTQ95ZH(kE9$cm! zv8#M~-M3r!3JOV)-UF!W+1IaML%_mpz%`rqcX#{WyXfT<;!F>o$`*|K9?92Q>w-Q$ z!7o2MDC)Y_8>aUOI#=h8rCpTuk~i>#DQ4dr{>~%*=MnDue|v;^N`~)d#ZBP@H4eWI|2n%oAm$91O#KY47M8x$&D|FMkyTihb2-B=j}&k% zZvF{}Z>7Aqj^E(_lWAP=A_&!eNz!T#1*PQ{*E^l3!8uNe$ivUtI)^sQ=wkoWsNJSdqo;;=A_vH~do$T_0)OV+ zmLbd;jfa>iX@R;0a<2Tzk5ANJ{V&qqJD%#l{~s?DvI*HC8dk{OD+$RcWS6~VbCB%p zgpNHEW$$$)>&VWYC!6eJ9LM}UkKWh&bA4`~>vMgs@9p!nMLQZ;=hARkpe~MKkeABOuy1qHg)drWKZ2~J0}Ieu zI5p2F!$vQ(F7kf+N8VQ_iU*{__s==F^u~q6+2iVy45A zY|O~{AdIBX^$zkY!$@r;$hi%F!eo!y6Be0jcS?zjSo`??9rTc$D2phb5!^}7ka7z! z9aZ-Nd#2djgd0wPd;0*ty&N|3*AE5%#7JPx9v5km&7WjXd>7QF*0qk{DjQN4tWc`g z%j4)k2ZcdyNHOrG_rTpHxI9prN3&kaU{goK3)j!)*+-2&%+AUb3UTlJS@$WD z;6yrf3LHuX9~Q)PFg$U&t+JGqP`M~6*Y(_J63iQ03Nt!Y43ZG;om&~Y2E9Ce-+Zi- z*nC6uXWkJ{SZF654L__~JY!>*qumti50mI=U$=c^G2HxX!_S>``Kt}-D5K{B8D{E3 z>jY!JHFd|hsa;166hSUBflzrGaV!Q58~UV5ceVi>3@+fER?mBq$}%6us|nP@CY_Tq z0aumqzQ3;~pyD*v$Z$|$zDdED@Vo72116J~BD%$ZR=`^RcA5tZCOri>P?jC}*K{Or zNCIuVk(eu$2wfTsFNg~>Ms6u&EJ1J~c9^it~WOrDx5RkIikwPOy zFi%Uz$#wIfRr$Yn&RKj za4$~FD>fz>e(0x3Mbmt?Cn-hplAx@6X8i>VVX%ACXz4>3$Fo-9^Us2#OFwQXy5tX5 zHX9w)xTd7o>tV$@v}o-vSdrn!EgHM>?<$a^yF2#Saku1-rI3DtkG{K!`FOTvR~$7N z&DW-S&G?8^u9sJDsW(){NJ=a|lv^r?q6o92eL3V|^8pc%&ZUr#ORiU>guUyHU(K`m ziw|{KdaT|o%ik$UJ^U8=sO);y{({3P=gR=M@_k8AK)kCdR2BYw)R$$9M`TFCAEArW z=gte`o9dC(LeQw*pX_Br_6T9RiKneb3t9j-pJx*pU8?hEscXnc(Akcqp5P;%j9?gj zHA*!?hAmiYUoU#(|8#lc2({KBDtHrD2|xO>m@TmL%W`^<)0~S4k|E z%16~|vRpQ*I3yfV4xfD+lYs8Vc{QJG_b#>a^y3|cJvrgxrC(!0H=HV~Q|t8og9$IO=PR{cvV096!s;25Y)(#OY^B@1>pS72QBhLu<_q5ar0B8m z4uTUJhqS;*5`!rJh+3m9153DBVUK5=TnnR-Y`Sk_XL4h=k$7W`uSa<5ltshoR@9u1 z{r)G_WJ{1nt>UFuWlO^o9iD4kDsJl`^~0|J9yy-8bj(hP3Rl8-+s|hB2Ws{qw#ih5 za4NF<3Pv$B)4tz)M!xDk@IJ6D(5vRQxK7BUn!;&61~c!WHKd~MVsT#^iA+Sgo)-n3 zKGVkZgVJQ5lklz=PD%4eH-52x0!y)&UaHc}H)%qAaMmKq!}QPJdgU&iJ6as=t>)rE zJc3EF(uX`qE`uAa1;A_BG0PhHr52mJsT>`ZcVl_Ts8-43)``&Kfg*>>~6ecZ48hO;6njdB_JDS6=6(=Gf zn$b-ORfR8X7)tk#0y~34ue@Vu`ij>OIj|lk2Hoy(TTz+XZOYiaDIy?EBCUA*Ds^5b zwfYliq4i6iJ-=w7rEjV%$Pt`Jx}6c<9yCB`l~pKjz~m`)t#_R8dCqBg-!8N9G&ut~ zU?n+P*jw*s1mqGW8yl3Trk9qO*}%)`_EA9Rj}Uty#15A?? zgcwxMFC^J|M$(RCC7+GN*11mx!OpF)r*~IaUckyo&tc!%$=w*VU^{qN($!K~pCWC3 zVpqFlv=i;FOfnIyB;ZmFCKLt_S@p85`ftKm#MZW)rI#kjo>nz zQc?rfJA z@t467MCYxz4r0{nVW+k9S8Q6UMr)8=@M;M9wQO)aw?W-4Q^#46-w!X~_Itr^lo!72 zPIMkP@SjO~ORY!2ktl7KYOZ+Bjn$#KYCS92 z8jpOYDD3ZSXT>=+R7Jb}03F^9rM_LV9ns=A4|Ah6j=W(z?dvb_;zz5H=-U@xur5Xx zMJ35#b3WDTXt;)QiZIx{^%!i3l>dX=T-;r7up!1rTfbEcroK_c6;D6RT~#w-N6=-$ zaEzOL^4*IicB|>rxNpi*ANXD)tj?0b0RP`=-6zHwQx zdE(&5TX?n0s)iTH+4aO1;+(GnD+6ZKNL4UN>PI~ezH+$qj+at_etqHnld-!MP$37# zN`S898hxH5e&zr}n|tl5DpUC^#44@;AFw+oO#CU{#y7i?}tBdsX@bK!UX>d-9A0p76woGg|ANx5&!i|}S zB7}$_(#(D4lFj@eEOeGCvK~^?cUs`l`*#8n@(wm<4XiO1&(YH*@7il@+ z995()Z8iI3SKW){i9^XTzD7#I8-$I+S2S8ast{K@U9y-zaPE$M$qtLXJfy>hnMK&7 zRFEeBjn4Yl041gn5n;%zWVpsFVwR3!b-N;>*Y^zC$&C`>sEwm@ufE2ESNske&LwOM zndNyx0E!{@S$(JsV8~F&*(ll;4rx}jk6G4dL5#%1!RoLN{LnscC1ciPu05@;by}wK z!{QJHq6wR+`%T{L-@#(C)hXvbHq%+HBMM)J2@YoT| zPoa}nK27>%jXMkSk`+HMq;>fQBeLpOva@Vzq;qO&Q?Bl>!S7n#a8DfgU8(RPJzZIm zX&^9aYxKR_Ru#u_cbcBG1FSrP-LD~-`&?5@^x*T-1Gl+rUJD3pkL z>tZ$s?6eZTS0c}iiR{M9QhyKkWzR?1tN5sPyA|4vG;R4!JYwxA%Ia&HkPdoOw!ATC zZ-DI4s7qB9oTja)bQJSJwuySpMn0>sw2E!4M&AGB-=JD&3Ug0eUw6~g{V&$0fDB7r z9!4B(BwfFWT5FQ}Sat4-ulOPig75elgAqQX2StbtM)CN-_YU$j+I>B@N77{yrDBQP z;C($tQl~d{qe$X0V2TQwCJL_pfm4(uBmSeiNf`0Ds-$N)dqFt4qSjS+!7;srksW!N zq^0nF+xy=hYd$${iQ{%-*%!CcE&}}lCP0IJO(FTqLT#x<+z(Y{xWp1@%gK3pEaCtAj@i)qGmHnm27jje)5>wnbE(0bnEjcmmNd~&rh zKjro7>cxyiE5}xA`S#QLzsF$QJdJvmq3&vuP2V_c>yrc>?v*x|w24?9WP(=RWg<+~ zszmn(wOO*R6e=&BU@eOd(c7@mH=)Eiw37mGfY~U3Nfh zw(U$QOQhHP&at}cCm%ja4J+OosQZm6d5?X&7g{XX2VIUsIV)Sk2IJv?T@X>LxabuB zbWQ7IcnJ44z)9x}!kA*S#v6R^FV72Qb$$m2xLn z+KiR~_SZqk^is>!`-?ze^a=3AM*20Ih$cV;Grc26-?%yrW&vq7nVY~=l|RdtcZ?x% zE=um_7q*WVy3*k80L;+px0k%RIN#Z6O~GCC$=}txry^0!+3%+L=8YJ1hCRP{-GSY% z)m9_j&yI5gg*+l_WeY-k#Y*eNkAxmSGZBbhG=?1VISSUg&yFJqSwV0qyw?>1z)PrQ$kv+B@e<_fr9|e>#frW)lqOPwkn`ST` zCx*|TbRSaVGPkxGxq2o((kp0_`ebp6%&D?*CAZZ14r2u-cVKI#$6W+^?yFA z*1}`!!HrUV+NYvteC!5B1W8FIWn7r&sy68$~DqqvsdtK)Ol7|ozIqBZ(>^X zU!gM!|9+eakv>f0cvb<>nTzURTB;O@4qXR0wj^jbblgwB+nJodfeVefl(!0_XKZb3 z<_l3KwN4-+q(9MNAp&v}IqU!Ik*jQ33q2_b@2#}TkBS~N!5&EG5B;?6}7OQL((qWhx^osfM_FcF;wYI9n(D*J_RxA5g% z3_r)EQ}TuuklzM%F1*UuM