From 907d32811892bad9354a0edd2207b1890dc55d29 Mon Sep 17 00:00:00 2001 From: Noah Paige Date: Fri, 10 Nov 2023 15:51:50 -0500 Subject: [PATCH 01/13] Upgrade DDK and remove cli calls --- .../dataall/base/cdkproxy/requirements.txt | 3 +- .../blueprints/cookiecutter_config.yaml | 2 - .../data_pipeline_blueprint/.gitignore | 10 ++ .../data_pipeline_blueprint/README.md | 58 +++++++++++ .../blueprints/data_pipeline_blueprint/app.py | 17 ++++ .../app_multiaccount.py | 25 ----- .../data_pipeline_blueprint/cdk.json | 61 ++++++++++++ .../__init__.py | 0 .../dataall_pipeline_app_stack.py | 26 +++++ .../ddk_app/ddk_app_stack_multiaccount.py | 28 ------ .../requirements-dev.txt | 1 + .../data_pipeline_blueprint/requirements.txt | 3 + .../data_pipeline_blueprint/source.bat | 13 +++ .../data_pipeline_blueprint/tests/__init__.py | 0 .../tests/unit/__init__.py | 0 .../unit/test_dataall_pipeline_app_stack.py | 15 +++ .../data_pipeline_blueprint/utils/config.py | 50 ---------- ...datapipelines_cdk_cli_wrapper_extension.py | 8 +- .../cdk/datapipelines_cdk_pipeline.py | 95 +++++++++++-------- .../cdk/datapipelines_pipeline.py | 34 +++---- 20 files changed, 280 insertions(+), 169 deletions(-) delete mode 100644 backend/dataall/modules/datapipelines/blueprints/cookiecutter_config.yaml create mode 100644 backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/.gitignore create mode 100644 backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/README.md create mode 100644 backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/app.py delete mode 100644 backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/app_multiaccount.py create mode 100644 backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/cdk.json rename backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/{ddk_app => dataall_pipeline_app}/__init__.py (100%) create mode 100644 backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/dataall_pipeline_app/dataall_pipeline_app_stack.py delete mode 100644 backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/ddk_app/ddk_app_stack_multiaccount.py create mode 100644 backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/requirements-dev.txt create mode 100644 backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/requirements.txt create mode 100644 backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/source.bat create mode 100644 backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/tests/__init__.py create mode 100644 backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/tests/unit/__init__.py create mode 100644 backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/tests/unit/test_dataall_pipeline_app_stack.py delete mode 100644 backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/utils/config.py diff --git a/backend/dataall/base/cdkproxy/requirements.txt b/backend/dataall/base/cdkproxy/requirements.txt index 74eb975e2..d50cc92f3 100644 --- a/backend/dataall/base/cdkproxy/requirements.txt +++ b/backend/dataall/base/cdkproxy/requirements.txt @@ -15,6 +15,5 @@ jinja2==3.1.2 werkzeug==3.0.1 constructs>=10.0.0,<11.0.0 git-remote-codecommit==1.16 -aws-ddk==0.5.1 -aws-ddk-core==0.5.1 +aws-ddk-core==1.3.0 deprecated==1.2.13 \ No newline at end of file diff --git a/backend/dataall/modules/datapipelines/blueprints/cookiecutter_config.yaml b/backend/dataall/modules/datapipelines/blueprints/cookiecutter_config.yaml deleted file mode 100644 index 7b0c8b2e6..000000000 --- a/backend/dataall/modules/datapipelines/blueprints/cookiecutter_config.yaml +++ /dev/null @@ -1,2 +0,0 @@ -cookiecutters_dir: "/dataall" -replay_dir: "/dataall" \ No newline at end of file diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/.gitignore b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/.gitignore new file mode 100644 index 000000000..37833f8be --- /dev/null +++ b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/.gitignore @@ -0,0 +1,10 @@ +*.swp +package-lock.json +__pycache__ +.pytest_cache +.venv +*.egg-info + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/README.md b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/README.md new file mode 100644 index 000000000..c53f0b50c --- /dev/null +++ b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/README.md @@ -0,0 +1,58 @@ + +# Welcome to your CDK Python project! + +This is a blank project for CDK development with Python. + +The `cdk.json` file tells the CDK Toolkit how to execute your app. + +This project is set up like a standard Python project. The initialization +process also creates a virtualenv within this project, stored under the `.venv` +directory. To create the virtualenv it assumes that there is a `python3` +(or `python` for Windows) executable in your path with access to the `venv` +package. If for any reason the automatic creation of the virtualenv fails, +you can create the virtualenv manually. + +To manually create a virtualenv on MacOS and Linux: + +``` +$ python3 -m venv .venv +``` + +After the init process completes and the virtualenv is created, you can use the following +step to activate your virtualenv. + +``` +$ source .venv/bin/activate +``` + +If you are a Windows platform, you would activate the virtualenv like this: + +``` +% .venv\Scripts\activate.bat +``` + +Once the virtualenv is activated, you can install the required dependencies. + +``` +$ pip install -r requirements.txt +``` + +At this point you can now synthesize the CloudFormation template for this code. + +``` +$ cdk synth +``` + +To add additional dependencies, for example other CDK libraries, just add +them to your `setup.py` file and rerun the `pip install -r requirements.txt` +command. + +## Useful commands + + * `cdk ls` list all stacks in the app + * `cdk synth` emits the synthesized CloudFormation template + * `cdk deploy` deploy this stack to your default AWS account/region + * `cdk diff` compare deployed stack with current state + * `cdk docs` open CDK documentation + +Enjoy! diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/app.py b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/app.py new file mode 100644 index 000000000..0ef694f25 --- /dev/null +++ b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/app.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +import os +import aws_cdk as cdk +from dataall_pipeline_app.dataall_pipeline_app_stack import DataallPipelineStack + +environment_id = os.environ.get('STAGE', "dev") +pipeline_name = os.environ.get('PIPELINE_NAME', "dataall-pipeline-stack") + +app = cdk.App() + +DataallPipelineStack( + app, + f"{pipeline_name}-DataallPipelineStack", + environment_id +) + +app.synth() diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/app_multiaccount.py b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/app_multiaccount.py deleted file mode 100644 index c22b92f2f..000000000 --- a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/app_multiaccount.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 -import os -import aws_cdk as cdk -from aws_cdk import Tags -from ddk_app.ddk_app_stack import DdkApplicationStack - -from utils.config import MultiaccountConfig - -stage_id = os.environ.get('STAGE', None) -pipeline_name = os.environ.get('PIPELINE_NAME') - -app = cdk.App() - -config = MultiaccountConfig() -environment_id = config.get_stage_env_id(stage_id) -env_vars = config.get_env_var_config(environment_id)['env_vars'] - -Tags.of(app).add("dataall", "true") -Tags.of(app).add("Target", pipeline_name) -DdkApplicationStack(app, - f"{pipeline_name}-DdkApplicationStack", - environment_id, - env_vars) - -app.synth() diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/cdk.json b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/cdk.json new file mode 100644 index 000000000..33ab988ba --- /dev/null +++ b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/cdk.json @@ -0,0 +1,61 @@ +{ + "app": "python3 app.py", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "source.bat", + "**/__init__.py", + "**/__pycache__", + "tests" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true + } +} diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/ddk_app/__init__.py b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/dataall_pipeline_app/__init__.py similarity index 100% rename from backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/ddk_app/__init__.py rename to backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/dataall_pipeline_app/__init__.py diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/dataall_pipeline_app/dataall_pipeline_app_stack.py b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/dataall_pipeline_app/dataall_pipeline_app_stack.py new file mode 100644 index 000000000..19340bd44 --- /dev/null +++ b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/dataall_pipeline_app/dataall_pipeline_app_stack.py @@ -0,0 +1,26 @@ +from typing import Any, Optional + +from aws_cdk import Environment, Tags +from aws_ddk_core import BaseStack, Configurator +from constructs import Construct + + +class DataallPipelineStack(BaseStack): + def __init__( + self, + scope: Construct, + id: str, + environment_id: str, + env: Optional[Environment] = None, + **kwargs: Any + ) -> None: + super().__init__( + scope, + id, + environment_id=environment_id, + env=env or Configurator.get_environment(config_path="./ddk.json", environment_id=environment_id), + **kwargs + ) + Configurator(scope=self, config="./ddk.json", environment_id=environment_id) + + # The code that defines your stack goes here: diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/ddk_app/ddk_app_stack_multiaccount.py b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/ddk_app/ddk_app_stack_multiaccount.py deleted file mode 100644 index bdd275647..000000000 --- a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/ddk_app/ddk_app_stack_multiaccount.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Any, Optional - -from aws_cdk import Environment, Tags -from aws_ddk_core.base import BaseStack -from aws_ddk_core.config import Config -from constructs import Construct - - -class DdkApplicationStack(BaseStack): - - - def __init__(self, scope: Construct, - id: str, - environment_id: str, - env_vars: dict, - env: Optional[Environment] = None, - **kwargs: Any) -> None: - self._config = Config() - super().__init__( - scope, - id, - environment_id=environment_id, - env=env or self._config.get_env(environment_id), - **kwargs) - - Tags.of(self).add("Team", str(env_vars['Team'])) - - # The code that defines your stack goes here: diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/requirements-dev.txt b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/requirements-dev.txt new file mode 100644 index 000000000..927094516 --- /dev/null +++ b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/requirements-dev.txt @@ -0,0 +1 @@ +pytest==6.2.5 diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/requirements.txt b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/requirements.txt new file mode 100644 index 000000000..4067e0fd9 --- /dev/null +++ b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/requirements.txt @@ -0,0 +1,3 @@ +aws-cdk-lib==2.103.1 +constructs>=10.0.0,<11.0.0 +aws-ddk-core==1.3.0 diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/source.bat b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/source.bat new file mode 100644 index 000000000..9e1a83442 --- /dev/null +++ b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/source.bat @@ -0,0 +1,13 @@ +@echo off + +rem The sole purpose of this script is to make the command +rem +rem source .venv/bin/activate +rem +rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. +rem On Windows, this command just runs this batch file (the argument is ignored). +rem +rem Now we don't need to document a Windows command for activating a virtualenv. + +echo Executing .venv\Scripts\activate.bat for you +.venv\Scripts\activate.bat diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/tests/__init__.py b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/tests/unit/__init__.py b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/tests/unit/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/tests/unit/test_dataall_pipeline_app_stack.py b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/tests/unit/test_dataall_pipeline_app_stack.py new file mode 100644 index 000000000..d74726b32 --- /dev/null +++ b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/tests/unit/test_dataall_pipeline_app_stack.py @@ -0,0 +1,15 @@ +import aws_cdk as core +import aws_cdk.assertions as assertions + +from data_pipeline_blueprint.dataall_pipeline_app.dataall_pipeline_app_stack import DataallPipelineStack + +# example tests. To run these tests, uncomment this file along with the example +# resource in data_pipeline_blueprint/data_pipeline_blueprint_stack.py +def test_sqs_queue_created(): + app = core.App() + stack = DataallPipelineStack(app, "dataall-pipeline-stack", "test") + template = assertions.Template.from_stack(stack) + +# template.has_resource_properties("AWS::SQS::Queue", { +# "VisibilityTimeout": 300 +# }) diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/utils/config.py b/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/utils/config.py deleted file mode 100644 index 2a11ff176..000000000 --- a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/utils/config.py +++ /dev/null @@ -1,50 +0,0 @@ -from aws_ddk_core.config.config import Config -from typing import Dict - - -class MultiaccountConfig(Config): - def __int__(self, *args, **kwargs) -> None: - super.__init__(*args, **kwargs) - - def get_stage_env_id( - self, - stage_id: str, - ) -> str: - """ - Get environment id representing AWS account and region with specified stage_id. - Parameters - ---------- - stage_id : str - Identifier of the stage - Returns - ------- - environment_id : str - """ - environments = self._config_strategy.get_config(key="environments") - - for env_id, env in environments.items(): - if env.get('stage', {}) == stage_id: - environment_id = env_id - break - else: - raise ValueError(f'Environment id with stage_id {stage_id} was not found!') - - return environment_id - - def get_env_var_config( - self, - environment_id: str, - ) -> dict: - """ - Get environment specific variable from config for given environment id. - Parameters - ---------- - environment_id : str - Identifier of the environment - Returns - ------- - config : Dict[str, Any] - Dictionary that contains environmental variables for the given environment - """ - env_config = self.get_env_config(environment_id) - return env_config \ No newline at end of file diff --git a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_cli_wrapper_extension.py b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_cli_wrapper_extension.py index 082fc1dba..f9b09d913 100644 --- a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_cli_wrapper_extension.py +++ b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_cli_wrapper_extension.py @@ -16,11 +16,11 @@ def __init__(self): def extend_deployment(self, stack, session, env): cdkpipeline = CDKPipelineStack(stack.targetUri) - venv_name = cdkpipeline.venv_name if cdkpipeline.venv_name else None + is_create = cdkpipeline.is_create if cdkpipeline.is_create else None self.pipeline = DatapipelinesRepository.get_pipeline_by_uri(session, stack.targetUri) - path = f'/dataall/modules/datapipelines/cdk/{self.pipeline.repo}/' + path = f'/dataall/modules/datapipelines/blueprints/{self.pipeline.repo}/' app_path = './app.py' - if not venv_name: + if not is_create: logger.info('Successfully Updated CDK Pipeline') meta = describe_stack(stack) stack.stackid = meta['StackId'] @@ -45,4 +45,4 @@ def extend_deployment(self, stack, session, env): return False, path, app_path def post_deployment(self): - CDKPipelineStack.clean_up_repo(path=f'./{self.pipeline.repo}') + CDKPipelineStack.clean_up_repo(pipeline_dir=self.pipeline.repo) diff --git a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py index d82f7c2ca..b91fadc1e 100644 --- a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py +++ b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py @@ -47,13 +47,19 @@ def __init__(self, target_uri): self.env, aws = CDKPipelineStack._set_env_vars(self.pipeline_environment) - self.code_dir_path = os.path.dirname(os.path.abspath(__file__)) - + self.code_dir_path = os.path.realpath( + os.path.abspath( + os.path.join( + __file__, "..", "..", "blueprints" + ) + ) + ) + self.is_create = True try: codecommit_client = aws.client('codecommit', region_name=self.pipeline.region) repository = CDKPipelineStack._check_repository(codecommit_client, self.pipeline.repo) if repository: - self.venv_name = None + self.is_create = False self.code_dir_path = os.path.realpath( os.path.abspath( os.path.join( @@ -61,16 +67,16 @@ def __init__(self, target_uri): ) ) ) - CDKPipelineStack.write_ddk_json_multienvironment(path=self.code_dir_path, output_file="ddk.json", pipeline_environment=self.pipeline_environment, development_environments=self.development_environments) - CDKPipelineStack.write_ddk_app_multienvironment(path=self.code_dir_path, output_file="app.py", pipeline=self.pipeline, development_environments=self.development_environments) + CDKPipelineStack.write_ddk_json_multienvironment(path=os.path.join(self.code_dir_path, self.pipeline.repo), output_file="ddk.json", pipeline_environment=self.pipeline_environment, development_environments=self.development_environments, pipeline_name=self.pipeline.name) + CDKPipelineStack.write_ddk_app_multienvironment(path=os.path.join(self.code_dir_path, self.pipeline.repo), output_file="app.py", pipeline=self.pipeline, development_environments=self.development_environments) logger.info(f"Pipeline Repo {self.pipeline.repo} Exists...Handling Update") update_cmds = [ f'REPO_NAME={self.pipeline.repo}', 'COMMITID=$(aws codecommit get-branch --repository-name ${REPO_NAME} --branch-name main --query branch.commitId --output text)', - 'aws codecommit put-file --repository-name ${REPO_NAME} --branch-name main --file-content file://ddk.json --file-path ddk.json --parent-commit-id ${COMMITID} --cli-binary-format raw-in-base64-out', + 'aws codecommit put-file --repository-name ${REPO_NAME} --branch-name main --file-content file://${REPO_NAME}/ddk.json --file-path ddk.json --parent-commit-id ${COMMITID} --cli-binary-format raw-in-base64-out', 'COMMITID=$(aws codecommit get-branch --repository-name ${REPO_NAME} --branch-name main --query branch.commitId --output text)', - 'aws codecommit put-file --repository-name ${REPO_NAME} --branch-name main --file-content file://app.py --file-path app.py --parent-commit-id ${COMMITID} --cli-binary-format raw-in-base64-out', + 'aws codecommit put-file --repository-name ${REPO_NAME} --branch-name main --file-content file://${REPO_NAME}/app.py --file-path app.py --parent-commit-id ${COMMITID} --cli-binary-format raw-in-base64-out', ] CommandSanitizer(args=[self.pipeline.repo]) @@ -88,18 +94,19 @@ def __init__(self, target_uri): else: raise Exception except Exception as e: - self.venv_name = self.initialize_repo() + self.initialize_repo() CDKPipelineStack.write_ddk_app_multienvironment(path=os.path.join(self.code_dir_path, self.pipeline.repo), output_file="app.py", pipeline=self.pipeline, development_environments=self.development_environments) - CDKPipelineStack.write_ddk_json_multienvironment(path=os.path.join(self.code_dir_path, self.pipeline.repo), output_file="ddk.json", pipeline_environment=self.pipeline_environment, development_environments=self.development_environments) + CDKPipelineStack.write_ddk_json_multienvironment(path=os.path.join(self.code_dir_path, self.pipeline.repo), output_file="ddk.json", pipeline_environment=self.pipeline_environment, development_environments=self.development_environments, pipeline_name=self.pipeline.name) self.git_push_repo() def initialize_repo(self): - venv_name = ".venv" cmd_init = [ - f"ddk init {self.pipeline.repo} --generate-only", + f"mkdir {self.pipeline.repo}", + f"cp -R data_pipeline_blueprint/* {self.pipeline.repo}/", f"cd {self.pipeline.repo}", "git init --initial-branch main", - f"ddk create-repository {self.pipeline.repo} -t application dataall -t team {self.pipeline.SamlGroupName}" + f"REPO_URL=$(aws codecommit create-repository --repository-name {self.pipeline.repo} --tags application=dataall,team={self.pipeline.SamlGroupName} --query 'repositoryMetadata.cloneUrlHttp' --output text)", + "git remote add origin ${REPO_URL}", ] logger.info(f"Running Commands: {'; '.join(cmd_init)}") @@ -120,31 +127,35 @@ def initialize_repo(self): if process.returncode == 0: logger.info("Successfully Initialized New CDK/DDK App") - return venv_name - @staticmethod - def write_ddk_json_multienvironment(path, output_file, pipeline_environment, development_environments): + def write_ddk_json_multienvironment(path, output_file, pipeline_environment, development_environments, pipeline_name): json_envs = "" for env in development_environments: json_env = f""", "{env.stage}": {{ "account": "{env.AwsAccountId}", "region": "{env.region}", - "resources": {{ - "ddk-bucket": {{"versioned": false, "removal_policy": "destroy"}} + "stage": "{env.stage}", + "tags": {{ + "Team": "{env.samlGroupName}" }} }}""" json_envs = json_envs + json_env json = f"""{{ + "tags": {{ + "dataall": "true", + "Target": "{pipeline_name}" + }}, "environments": {{ "cicd": {{ "account": "{pipeline_environment.AwsAccountId}", - "region": "{pipeline_environment.region}" + "region": "{pipeline_environment.region}", + "stage": "cicd" }}{json_envs} }} }}""" - + os.makedirs(path, exist_ok=True) with open(f'{path}/{output_file}', 'w') as text_file: print(json, file=text_file) @@ -154,9 +165,8 @@ def write_ddk_app_multienvironment(path, output_file, pipeline, development_envi # !/usr/bin/env python3 import aws_cdk as cdk -from aws_ddk_core.cicd import CICDPipelineStack -from ddk_app.ddk_app_stack import DdkApplicationStack -from aws_ddk_core.config import Config +import aws_ddk_core as ddk +from dataall_pipeline_app.dataall_pipeline_app_stack import DataallPipelineStack app = cdk.App() @@ -168,24 +178,25 @@ def __init__( **kwargs, ) -> None: super().__init__(scope, f"dataall-{{environment_id.title()}}", **kwargs) - DdkApplicationStack(self, "DataPipeline-{pipeline.label}-{pipeline.DataPipelineUri}", environment_id) + DataallPipelineStack(self, "DataPipeline-{pipeline.label}-{pipeline.DataPipelineUri}", environment_id) id = f"dataall-cdkpipeline-{pipeline.DataPipelineUri}" -config = Config() -( - CICDPipelineStack( +cicd_pipeline = ( + ddk.CICDPipelineStack( app, id=id, - environment_id="cicd", pipeline_name="{pipeline.label}", + env=ddk.Configurator.get_environment( + config_path="./ddk.json", environment_id="cicd" + ), ) .add_source_action(repository_name="{pipeline.repo}") .add_synth_action() - .build()""" + .build_pipeline()""" stages = "" for env in sorted(development_environments, key=lambda env: env.order): - stage = f""".add_stage("{env.stage}", ApplicationStage(app, "{env.stage}", env=config.get_env("{env.stage}")))""" + stage = f""".add_stage(stage_id="{env.stage}", stage=ApplicationStage(app, "{env.stage}", ddk.Configurator.get_environment(config_path="./ddk.json", environment_id="{env.stage}")))""" stages = stages + stage footer = """ .synth() @@ -194,7 +205,7 @@ def __init__( app.synth() """ app = header + stages + footer - + os.makedirs(path, exist_ok=True) with open(f'{path}/{output_file}', 'w') as text_file: print(app, file=text_file) @@ -226,10 +237,17 @@ def git_push_repo(self): logger.info("Successfully Pushed DDK App Code") @staticmethod - def clean_up_repo(path): - if path: - cmd = ['rm', '-rf', f"{path}"] - cwd = os.path.dirname(os.path.abspath(__file__)) + def clean_up_repo(pipeline_dir): + if pipeline_dir: + code_dir_path = os.path.realpath( + os.path.abspath( + os.path.join( + __file__, "..", "..", "blueprints" + ) + ) + ) + + cmd = ['rm', '-rf', f"./{pipeline_dir}"] logger.info(f"Running command : \n {' '.join(cmd)}") process = subprocess.run( @@ -238,17 +256,17 @@ def clean_up_repo(path): shell=False, encoding='utf-8', capture_output=True, - cwd=cwd + cwd=code_dir_path ) if process.returncode == 0: - print(f"Successfully cleaned cloned repo: {path}. {str(process.stdout)}") + print(f"Successfully cleaned cloned repo: {pipeline_dir}. {str(process.stdout)}") else: logger.error( - f'Failed clean cloned repo: {path} due to {str(process.stderr)}' + f'Failed clean cloned repo: {pipeline_dir} due to {str(process.stderr)}' ) else: - logger.info(f"Info:Path {path} not found") + logger.info(f"Info:Path {pipeline_dir} not found") return @staticmethod @@ -278,7 +296,6 @@ def _set_env_vars(pipeline_environment): 'PYTHONPATH': python_path, 'PATH': python_path, 'envname': os.environ.get('envname', 'local'), - 'COOKIECUTTER_CONFIG': "/dataall/modules/datapipelines/blueprints/cookiecutter_config.yaml", } if env_creds: env.update( diff --git a/backend/dataall/modules/datapipelines/cdk/datapipelines_pipeline.py b/backend/dataall/modules/datapipelines/cdk/datapipelines_pipeline.py index db5b97484..d36aa9191 100644 --- a/backend/dataall/modules/datapipelines/cdk/datapipelines_pipeline.py +++ b/backend/dataall/modules/datapipelines/cdk/datapipelines_pipeline.py @@ -169,7 +169,7 @@ def __init__(self, scope, id, target_uri: str = None, **kwargs): code_dir_path = os.path.realpath( os.path.abspath( os.path.join( - __file__, "..", "..", "blueprints", "data_pipeline_blueprint" + __file__, "..", "..", "blueprints" ) ) ) @@ -178,13 +178,13 @@ def __init__(self, scope, id, target_uri: str = None, **kwargs): try: repository = PipelineStack._check_repository(aws, pipeline_environment.region, pipeline.repo) if repository: - PipelineStack.write_ddk_json_multienvironment(path=code_dir_path, output_file="ddk.json", pipeline_environment=pipeline_environment, development_environments=development_environments) + PipelineStack.write_ddk_json_multienvironment(path=os.path.join(code_dir_path, pipeline.repo), output_file="ddk.json", pipeline_environment=pipeline_environment, development_environments=development_environments, pipeline_name=pipeline.name) logger.info(f"Pipeline Repo {pipeline.repo} Exists...Handling Update") update_cmds = [ f'REPO_NAME={pipeline.repo}', 'COMMITID=$(aws codecommit get-branch --repository-name ${REPO_NAME} --branch-name main --query branch.commitId --output text)', - 'aws codecommit put-file --repository-name ${REPO_NAME} --branch-name main --file-content file://ddk.json --file-path ddk.json --parent-commit-id ${COMMITID} --cli-binary-format raw-in-base64-out', + 'aws codecommit put-file --repository-name ${REPO_NAME} --branch-name main --file-content file://${REPO_NAME}/ddk.json --file-path ddk.json --parent-commit-id ${COMMITID} --cli-binary-format raw-in-base64-out', ] CommandSanitizer(args=[pipeline.repo]) @@ -207,7 +207,7 @@ def __init__(self, scope, id, target_uri: str = None, **kwargs): PipelineStack.write_deploy_buildspec(path=code_dir_path, output_file=f"{pipeline.repo}/deploy_buildspec.yaml") - PipelineStack.write_ddk_json_multienvironment(path=code_dir_path, output_file=f"{pipeline.repo}/ddk.json", pipeline_environment=pipeline_environment, development_environments=development_environments) + PipelineStack.write_ddk_json_multienvironment(path=os.path.join(code_dir_path, pipeline.repo), output_file="ddk.json", pipeline_environment=pipeline_environment, development_environments=development_environments, pipeline_name=pipeline.name) logger.info(f"Pipeline Repo {pipeline.repo} Does Not Exists... Creating Repository") @@ -440,12 +440,11 @@ def write_deploy_buildspec(path, output_file): commands: - n 16.15.1 - npm install -g aws-cdk - - pip install aws-ddk - pip install -r requirements.txt build: commands: - aws sts get-caller-identity - - ddk deploy + - cdk deploy """ with open(f'{path}/{output_file}', 'x') as text_file: print(yaml, file=text_file) @@ -485,7 +484,7 @@ def make_codebuild_policy_statements( ] @staticmethod - def write_ddk_json_multienvironment(path, output_file, pipeline_environment, development_environments): + def write_ddk_json_multienvironment(path, output_file, pipeline_environment, development_environments, pipeline_name): json_envs = "" for env in development_environments: json_env = f""", @@ -493,14 +492,17 @@ def write_ddk_json_multienvironment(path, output_file, pipeline_environment, dev "account": "{env.AwsAccountId}", "region": "{env.region}", "stage": "{env.stage}", - "env_vars": {{ - "database": "example_database", + "tags": {{ "Team": "{env.samlGroupName}" }} }}""" json_envs = json_envs + json_env json = f"""{{ + "tags": {{ + "dataall": "true", + "Target": "{pipeline_name}" + }}, "environments": {{ "cicd": {{ "account": "{pipeline_environment.AwsAccountId}", @@ -509,21 +511,16 @@ def write_ddk_json_multienvironment(path, output_file, pipeline_environment, dev }}{json_envs} }} }}""" - + os.makedirs(path, exist_ok=True) with open(f'{path}/{output_file}', 'w') as text_file: print(json, file=text_file) @staticmethod def initialize_repo(pipeline, code_dir_path, env_vars): - venv_name = ".venv" - cmd_init = [ - f"ddk init {pipeline.repo} --generate-only", - f"cp app_multiaccount.py ./{pipeline.repo}/app.py", - f"cp ddk_app/ddk_app_stack_multiaccount.py ./{pipeline.repo}/ddk_app/ddk_app_stack.py", - f"mkdir ./{pipeline.repo}/utils", - f"cp -R utils/* ./{pipeline.repo}/utils/" + f"mkdir {pipeline.repo}", + f"cp -R data_pipeline_blueprint/* {pipeline.repo}/" ] logger.info(f"Running Commands: {'; '.join(cmd_init)}") @@ -554,8 +551,7 @@ def _set_env_vars(pipeline_environment): 'AWS_REGION': pipeline_environment.region, 'AWS_DEFAULT_REGION': pipeline_environment.region, 'CURRENT_AWS_ACCOUNT': pipeline_environment.AwsAccountId, - 'envname': os.environ.get('envname', 'local'), - 'COOKIECUTTER_CONFIG': "/dataall/modules/datapipelines/blueprints/cookiecutter_config.yaml", + 'envname': os.environ.get('envname', 'local') } if env_creds: env.update( From 9ec7c87060cf930a82830fb740f8f20ce1e92fd6 Mon Sep 17 00:00:00 2001 From: Noah Paige Date: Fri, 10 Nov 2023 16:26:43 -0500 Subject: [PATCH 02/13] Add kwarg env --- .../modules/datapipelines/cdk/datapipelines_cdk_pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py index b91fadc1e..63c58d242 100644 --- a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py +++ b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py @@ -196,7 +196,7 @@ def __init__( stages = "" for env in sorted(development_environments, key=lambda env: env.order): - stage = f""".add_stage(stage_id="{env.stage}", stage=ApplicationStage(app, "{env.stage}", ddk.Configurator.get_environment(config_path="./ddk.json", environment_id="{env.stage}")))""" + stage = f""".add_stage(stage_id="{env.stage}", stage=ApplicationStage(app, "{env.stage}", env=ddk.Configurator.get_environment(config_path="./ddk.json", environment_id="{env.stage}")))""" stages = stages + stage footer = """ .synth() From 057fe8342ce5232cee92f31f96a765096213bf93 Mon Sep 17 00:00:00 2001 From: Noah Paige Date: Fri, 10 Nov 2023 17:17:26 -0500 Subject: [PATCH 03/13] Switch Permissions to cdk --- .../datapipelines/cdk/pivot_role_datapipelines_policy.py | 6 +++--- deploy/pivot_role/pivotRole.yaml | 4 ++-- deploy/stacks/container.py | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/backend/dataall/modules/datapipelines/cdk/pivot_role_datapipelines_policy.py b/backend/dataall/modules/datapipelines/cdk/pivot_role_datapipelines_policy.py index 0bed7e176..6f3a767ef 100644 --- a/backend/dataall/modules/datapipelines/cdk/pivot_role_datapipelines_policy.py +++ b/backend/dataall/modules/datapipelines/cdk/pivot_role_datapipelines_policy.py @@ -37,7 +37,7 @@ def get_statements(self): effect=iam.Effect.ALLOW, actions=['sts:AssumeRole'], resources=[ - f'arn:aws:iam::{self.account}:role/ddk-*', + f'arn:aws:iam::{self.account}:role/cdk-*', ], ), iam.PolicyStatement( @@ -54,11 +54,11 @@ def get_statements(self): ], ), iam.PolicyStatement( - sid='ParameterStoreDDK', + sid='ParameterStorePipelines', effect=iam.Effect.ALLOW, actions=['ssm:GetParameter'], resources=[ - f'arn:aws:ssm:*:{self.account}:parameter/ddk/*', + f'arn:aws:ssm:*:{self.account}:parameter/cdk*', ], ), ] diff --git a/deploy/pivot_role/pivotRole.yaml b/deploy/pivot_role/pivotRole.yaml index 0ef6ff277..ced8b4af9 100644 --- a/deploy/pivot_role/pivotRole.yaml +++ b/deploy/pivot_role/pivotRole.yaml @@ -430,7 +430,7 @@ Resources: Resource: - !Sub 'arn:aws:ssm:*:${AWS::AccountId}:parameter/${EnvironmentResourcePrefix}/*' - !Sub 'arn:aws:ssm:*:${AWS::AccountId}:parameter/dataall/*' - - !Sub 'arn:aws:ssm:*:${AWS::AccountId}:parameter/ddk/*' + - !Sub 'arn:aws:ssm:*:${AWS::AccountId}:parameter/cdk*' - Sid: IAMListGet Action: - 'iam:Get*' @@ -464,7 +464,7 @@ Resources: Effect: Allow Resource: - !Sub 'arn:aws:iam::${AWS::AccountId}:role/${EnvironmentResourcePrefix}*' - - !Sub 'arn:aws:iam::${AWS::AccountId}:role/ddk-*' + - !Sub 'arn:aws:iam::${AWS::AccountId}:role/cdk-*' - Sid: CodeCommit Action: - 'codecommit:GetFile' diff --git a/deploy/stacks/container.py b/deploy/stacks/container.py index 94026ad9b..25d1775e3 100644 --- a/deploy/stacks/container.py +++ b/deploy/stacks/container.py @@ -501,7 +501,6 @@ def create_task_role(self, envname, resource_prefix, pivot_role_name): resources=[ f'arn:aws:iam::*:role/{pivot_role_name}', f'arn:aws:iam::*:role/cdk*', - 'arn:aws:iam::*:role/ddk*', f'arn:aws:iam::{self.account}:role/{resource_prefix}-{envname}-ecs-tasks-role', ], ), From 1fc9f3b6e45cfa0afa2f39a63813c29483ebf658 Mon Sep 17 00:00:00 2001 From: Noah Paige Date: Fri, 10 Nov 2023 17:20:25 -0500 Subject: [PATCH 04/13] Change description pivotRole Statements --- .../datapipelines/cdk/pivot_role_datapipelines_policy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/dataall/modules/datapipelines/cdk/pivot_role_datapipelines_policy.py b/backend/dataall/modules/datapipelines/cdk/pivot_role_datapipelines_policy.py index 6f3a767ef..605568254 100644 --- a/backend/dataall/modules/datapipelines/cdk/pivot_role_datapipelines_policy.py +++ b/backend/dataall/modules/datapipelines/cdk/pivot_role_datapipelines_policy.py @@ -4,7 +4,7 @@ class PipelinesPivotRole(PivotRoleStatementSet): """ - Class including all permissions needed by the pivot role to work with AWS CodeCommit and STS assume for DDK pipelines + Class including all permissions needed by the pivot role to work with AWS CodeCommit and STS assume for CDK Pipelines It allows pivot role to: - .... """ From a188232d37ce8356831698dc7c95786d6a505453 Mon Sep 17 00:00:00 2001 From: Noah Paige Date: Fri, 10 Nov 2023 17:38:51 -0500 Subject: [PATCH 05/13] Add cdk language and fix pipeline name cdk pipe --- .../modules/datapipelines/cdk/datapipelines_cdk_pipeline.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py index 63c58d242..46378307d 100644 --- a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py +++ b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py @@ -178,14 +178,15 @@ def __init__( **kwargs, ) -> None: super().__init__(scope, f"dataall-{{environment_id.title()}}", **kwargs) - DataallPipelineStack(self, "DataPipeline-{pipeline.label}-{pipeline.DataPipelineUri}", environment_id) + DataallPipelineStack(self, "{pipeline.name}-DataallPipelineStack", environment_id) id = f"dataall-cdkpipeline-{pipeline.DataPipelineUri}" cicd_pipeline = ( ddk.CICDPipelineStack( app, id=id, - pipeline_name="{pipeline.label}", + pipeline_name="{pipeline.name}", + cdk_language="python", env=ddk.Configurator.get_environment( config_path="./ddk.json", environment_id="cicd" ), From 0d661b44db0d0ba03a1dde94bb5fcb83302cbbd3 Mon Sep 17 00:00:00 2001 From: Noah Paige Date: Mon, 13 Nov 2023 14:34:43 -0500 Subject: [PATCH 06/13] Add Permissions to pass role to cdk cross account pipeline --- deploy/cdk_exec_policy/cdkExecPolicy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/cdk_exec_policy/cdkExecPolicy.yaml b/deploy/cdk_exec_policy/cdkExecPolicy.yaml index 1bf9f7207..a5e7b3af2 100644 --- a/deploy/cdk_exec_policy/cdkExecPolicy.yaml +++ b/deploy/cdk_exec_policy/cdkExecPolicy.yaml @@ -53,7 +53,7 @@ Resources: Action: - 'sts:AssumeRole' - 'iam:*Role*' - Resource: !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-*' + Resource: !Sub 'arn:${AWS::Partition}:iam::*:role/cdk-*' - Sid: Quicksight Effect: Allow From 384604a75ed77709425f2f988f1f2e3c1c1a1281 Mon Sep 17 00:00:00 2001 From: Noah Paige Date: Mon, 13 Nov 2023 14:55:14 -0500 Subject: [PATCH 07/13] Allow updates CDK Pipeline --- backend/dataall/modules/datapipelines/__init__.py | 1 + .../cdk/datapipelines_cdk_cli_wrapper_extension.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/dataall/modules/datapipelines/__init__.py b/backend/dataall/modules/datapipelines/__init__.py index a0865ea00..2775a701f 100644 --- a/backend/dataall/modules/datapipelines/__init__.py +++ b/backend/dataall/modules/datapipelines/__init__.py @@ -34,6 +34,7 @@ def __init__(self): FeedRegistry.register(FeedDefinition("DataPipeline", DataPipeline)) TargetType("pipeline", GET_PIPELINE, UPDATE_PIPELINE) + TargetType("cdkpipeline", GET_PIPELINE, UPDATE_PIPELINE) EnvironmentResourceManager.register(DatapipelinesRepository()) diff --git a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_cli_wrapper_extension.py b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_cli_wrapper_extension.py index f9b09d913..01b56839c 100644 --- a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_cli_wrapper_extension.py +++ b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_cli_wrapper_extension.py @@ -26,7 +26,7 @@ def extend_deployment(self, stack, session, env): stack.stackid = meta['StackId'] stack.status = meta['StackStatus'] update_stack_output(session, stack) - return True, path + return True, path, app_path aws = SessionHelper.remote_session(stack.accountid) creds = aws.get_credentials() From e0bce078932f19eb2501f7ab8e93276bc74dcbbb Mon Sep 17 00:00:00 2001 From: Noah Paige Date: Tue, 14 Nov 2023 17:14:13 -0500 Subject: [PATCH 08/13] Remove stage from ddk json and update docs --- .../cdk/datapipelines_cdk_pipeline.py | 4 +- documentation/userguide/docs/pipelines.md | 77 +++++++++++++------ 2 files changed, 53 insertions(+), 28 deletions(-) diff --git a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py index 46378307d..df3e0419e 100644 --- a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py +++ b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py @@ -135,7 +135,6 @@ def write_ddk_json_multienvironment(path, output_file, pipeline_environment, dev "{env.stage}": {{ "account": "{env.AwsAccountId}", "region": "{env.region}", - "stage": "{env.stage}", "tags": {{ "Team": "{env.samlGroupName}" }} @@ -150,8 +149,7 @@ def write_ddk_json_multienvironment(path, output_file, pipeline_environment, dev "environments": {{ "cicd": {{ "account": "{pipeline_environment.AwsAccountId}", - "region": "{pipeline_environment.region}", - "stage": "cicd" + "region": "{pipeline_environment.region}" }}{json_envs} }} }}""" diff --git a/documentation/userguide/docs/pipelines.md b/documentation/userguide/docs/pipelines.md index 478a467fa..9e18bdcad 100644 --- a/documentation/userguide/docs/pipelines.md +++ b/documentation/userguide/docs/pipelines.md @@ -24,14 +24,26 @@ in the Research-CICD account. The actual data pipeline is deployed in 2 data acc ### Pre-requisites -As a pre-requisite, Research-DEV and Research-PROD accounts need to be bootstrapped trusting the CICD account (`-a` parameter) and setting the stage of the AWS account, the environment id, with the `e` parameter. Assuming 111111111111 = CICD account the commands are as follows: +As a pre-requisite, Research-DEV and Research-PROD accounts need to be bootstrapped using AWS CDK, trusting the CICD account (`--trust` parameter). Assuming 111111111111 = CICD account the commands are as follows: -- In Research-CICD (111111111111): `ddk bootstrap -e cicd` -- In Research-DEV (222222222222): `ddk bootstrap -e dev -a 111111111111` -- In Research-PROD (333333333333): `ddk bootstrap -e prod -a 111111111111` +- In Research-CICD (111111111111): `cdk bootstrap` +- In Research-DEV (222222222222): `cdk bootstrap --trust 111111111111` +- In Research-PROD (333333333333): `cdk bootstrap --trust 111111111111` In data.all we need to link the AWS accounts to the platform by creating 3 data.all Environments: Research-CICD Environment, Research-DEV Environment and Research-PROD Environment. +NOTE: In practice, the cdk bootstrap command would already be run once when linking an environment. For example, if bootstrapping an environment with the default AdministratorAccess CDK execution policy, the command run before linking a new environment would look similar to: + +``` +cdk bootstrap --trust DATA.ALL_AWS_ACCOUNT_NUMBER -c @aws-cdk/core:newStyleStackSynthesis=true --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess aws://YOUR_ENVIRONMENT_AWS_ACCOUNT_NUMBER/ENVIRONMENT_REGION +``` + +In order for the DEV and PROD accounts to also trust the CICD account without impacting the initial bootstrap requirements, the Research-DEV and Research-PROD accounts need to edit the aforementioned bootstrap command similar to the following: + +``` +cdk bootstrap --trust DATA.ALL_AWS_ACCOUNT_NUMBER --trust Research-CICD_AWS_ACCOUNT_NUMBER -c @aws-cdk/core:newStyleStackSynthesis=true --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess aws://YOUR_ENVIRONMENT_AWS_ACCOUNT_NUMBER/ENVIRONMENT_REGION +``` + ### Creating a pipeline data.all pipelines are created from the UI, under Pipelines. We need to fill the creation form with the following information: @@ -46,11 +58,6 @@ data.all pipelines are created from the UI, under Pipelines. We need to fill the Finally, we need to add **Development environments**. These are the AWS accounts and regions where the infrastructure defined in the CICD pipeline is deployed. -!!! warning "environment ID = data.all environment stage" - When creating the pipeline and adding development environments, you define the stage of the environment. The ddk bootstrap `-e` parameter needs to match the one that you define in the data.all UI. - In our example, we bootstraped with the parameters "dev" and "prod" and then we defined the stages as "dev" and "prod" correspondingly. - - ![create_pipeline](pictures/pipelines/pip_create_form.png#zoom#shadow) --- @@ -66,6 +73,10 @@ In the deployed repository, data.all pushes a `ddk.json` file with the details o ```json { + "tags": { + "dataall": "true", + "Target": "PIPELINE_NAME" + }, "environments": { "cicd": { "account": "111111111111", @@ -74,15 +85,15 @@ In the deployed repository, data.all pushes a `ddk.json` file with the details o "dev": { "account": "222222222222", "region": "eu-west-1", - "resources": { - "ddk-bucket": {"versioned": false, "removal_policy": "destroy"} + "tags": { + "Team": "DATAALL_GROUP" } }, "prod": { "account": "333333333333", "region": "eu-west-1", - "resources": { - "ddk-bucket": {"versioned": true, "removal_policy": "retain"} + "tags": { + "Team": "DATAALL_GROUP" } } } @@ -95,9 +106,8 @@ In addition, the `app.py` file is also written accordingly to the development en # !/usr/bin/env python3 import aws_cdk as cdk -from aws_ddk_core.cicd import CICDPipelineStack -from ddk_app.ddk_app_stack import DDKApplicationStack -from aws_ddk_core.config import Config +import aws_ddk_core as ddk +from dataall_pipeline_app.dataall_pipeline_app_stack import DataallPipelineStack app = cdk.App() @@ -111,17 +121,37 @@ class ApplicationStage(cdk.Stage): super().__init__(scope, f"dataall-{environment_id.title()}", **kwargs) DDKApplicationStack(self, "DataPipeline-PIPELINENAME-PIPELINEURI", environment_id) -config = Config() -( - CICDPipelineStack( +id = f"dataall-cdkpipeline-PIPELINEURI" +cicd_pipeline = ( + ddk.CICDPipelineStack( app, id="dataall-pipeline-PIPELINENAME-PIPELINEURI", environment_id="cicd", pipeline_name="PIPELINENAME", + cdk_language="python", + env=ddk.Configurator.get_environment( + config_path="./ddk.json", environment_id="cicd" + ), ) .add_source_action(repository_name="dataall-PIPELINENAME-PIPELINEURI") .add_synth_action() - .build().add_stage("dev", ApplicationStage(app, "dev", env=config.get_env("dev"))).add_stage("prod", ApplicationStage(app, "prod", env=config.get_env("prod"))) + .build_pipeline() + .add_stage( + stage_id="dev", + stage=ApplicationStage( + app, + "dev", + env=ddk.Configurator.get_environment(config_path="./ddk.json", environment_id="dev") + ) + ) + .add_stage( + stage_id="prod", + stage=ApplicationStage( + app, + "prod", + env=ddk.Configurator.get_environment(config_path="./ddk.json", environment_id="prod") + ) + ) .synth() ) @@ -145,16 +175,13 @@ use CodePipeline CICD Strategy which leverages the [aws-codepipeline](https://do #### CodeCommit repository and CICD deployment When a pipeline is created, a CloudFormation stack is deployed in the CICD environment AWS account. It contains: -- an AWS CodeCommit repository with the code of an AWS DDK application (by running `ddk init`) with some modifications to allow cross-account deployments. +- an AWS CodeCommit repository with the code of an AWS CDK application (by running `cdk init`) with some modifications to allow cross-account deployments. - CICD CodePipeline(s) pipeline that deploy(s) the application The repository structure will look similar to: ![created_pipeline](pictures/pipelines/pip_cp_init2.png#zoom#shadow) -The added `Multiaccount` configuration class allows us to define the deployment environment based on the `ddk.json`. -Go ahead and customize this configuration further, for example you can set additional `env_vars`. - Trunk-based pipelines append one stage after the other and read from the main branch of our repository: ![created_pipeline](pictures/pipelines/pip_cp_trunk.png#zoom#shadow) @@ -163,7 +190,7 @@ Gitflow strategy uses multiple CodePipeline pipelines for each of the stages. Fo ![created_pipeline](pictures/pipelines/pip_cp_gitflow.png#zoom#shadow) -The `dev` pipeline reads from the `dev` branch of the repository: +Using the Gitflow strategy, the `dev` pipeline reads from the `dev` branch of the repository: ![created_pipeline](pictures/pipelines/pip_cp_gitflow2.png#zoom#shadow) From 83b1e2b65639abdf1323a130b28ab7e1c40c11a6 Mon Sep 17 00:00:00 2001 From: Noah Paige Date: Tue, 14 Nov 2023 17:15:46 -0500 Subject: [PATCH 09/13] Add screenshot --- .../docs/pictures/pipelines/pip_cp_init2.png | Bin 38007 -> 58571 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/documentation/userguide/docs/pictures/pipelines/pip_cp_init2.png b/documentation/userguide/docs/pictures/pipelines/pip_cp_init2.png index b9a4f3193587ad21cd12f79e11c7c29762fddcbc..00d752f2a212937ce0f62281c10e63d3d134f437 100644 GIT binary patch literal 58571 zcmeFZcT`hR*FH!`KtNGIL5hIVK|y*40qLQ4B3(d;0-;wyL8U3ZgR~HufT0HzkX{rB zy{Poiq=a7P#)9uVGylx2`K_5X>tn4XHz)VrbIv|{@3Wt0@5@V&rZPDx9VreD4!O#M z`#Lx{crY9se69;bK#OK|r34PnMHvSL1(1q@0yD@JV(0MK76<3S%XmX#Bi&A#4AZwU zvX5{Hazm#HmI-30oXf`55T|2j;MYXa;E#kjFM~zX4L|mV>@(wdQ5&-H z5d?9}_vT-Hm3z$~>(&cg`3BG{!TB21SA+9jWj(iVMiD;c#!=@`6>NH0ud*dQX!>KBbeZXmX2@G_A2V>D8;@ zu403y{qDybZxgw%Z&{KQsV0ct(-gN)s!kO<_^wlo_cZg46c?S9M!3ucF9F|hZPf&} zd)hA1bWg~P?%2x`TPZWL-??FhA0G0&R{Jukw3Nc7o)_0jFS*0BV$yNS`C7>i2<0|O zQ+t0Li$3T7a8sejife!SMtM)!(1^c^CW~(NnfU8|gC%SDXJeAiOS;U${8Vg;a>be~1FO_Zf>jQ}X>d~f zC9ZzT;ja0PA7O5v#P{Xxe#B5TJ$&5y#mwvYBlE#zxxJsIRfGIu2<#5CrZi1KxU2Qm z-5O3Ak^5b#@~bAGt7X?-vr{({a*nhM-hY0VWJ`4O$74;S2uVi2>)|*&mo6;H;yH-k6j66olW(eC1~_^4WE|H#{qso1G*%hyvwXi3wX~@obQOAcBLkmf~Ya zN&y?BV#Q^#ZzQeW%s1wmeP2F~dopTD<#qA;Cz%Gfhx5%|ZkF-8XTAa9Cw+HrUdevQ zrog!DsF2XgY{h&x?wb0CPnf6RN&4s@!$70-r$p;w_o6p;L)k+$n-!X&@*>}d+2mgC z7jUr3i40L1%(0xbn|(_F4U$uS^v@F4YlA-Ve;8daFQYz5>EjW?qjtIa9`{b>hcn?2 zri>TOQL7J>I$1kTZB{as;p6Z+@}dWBC)NRT<@uD0sX~+6m4U;QV~d}JkL}eS-}Jzd z2up9~d#GHlB-^g}P{WAcL;Y2l;fVDZnr@4R{WUu=`oigV$Wo`Opz$}OmC==b#c9Ki zOfG+Ae1pbEDMm5L`VZ`z`SCLQXj~bTzNRyzo8!a;fal7}M9M2Rh~5qErl;-^<4Cg; z5fexIEjDZ7yzj?%Icu2_b0Ru+=fHn*ddHB3@2y+Sj(iY=M5tzPC+PAW{NX3keIFUT z@B=RqG|S$5fW!HaB|@64)a`B(o?TFF*44NW+lN9%l)}L!57|fYcSDq(TusBX|8(mM zZfS_b6UY1BUo72h3n#P+6J@>FN=qWo5+lk+sG=Zn`6i2;9rJdK zs|{fV%kJ0JFiLPx)S{t-KASQ;^UExvcz|SIIYE#NbI8T-l3t1%(J?lbS?FHyd|}fkU^MG z%c39kO55T4Xwl0B-`yt}OTU36FE9Rx-89~}@{?-30{LjVS+tMez;vdRMRoCl2LV4` z>Yaobl_YtNTY8tOu25X=`-mJ;@KR=@f`%L1r>vpX4IgV=ZvEU!-sktw`#NDN&$#EqgX z@+_o$^yOaKy>~)e_h_6}2Wql5)njuV3a{sO4yKjW>TiEoV5L(M%qf3lHZD0XJbv9I znnt@@^>s;niGC?UxyZZ-Y^pn4L*p6|=SYry7AVV@aGa@=p zxqA=RJ1X8BDNhT_l0PQ@aWS9%vCt#IYj^GmT)cbrda0nK^Qe8U^&BM5`9s%Ijdt1H z-q8`GhYel(p_^`X!s#mgd1ZOk14kMc)#WwVZkwu)Ye=V>B;QU+NE%L^7R*Vy+au6p zkc>#eq|&D&?PYD8f=?0FUW1qU8P^HUxG4sf+I39 zHY<)RPQv%;UuZ=25~Wg@4h{DTcUKMURxmg&y0E{WcsL^V>Q0wi+!H+vjL(+>BGq-NR&%dZPdh#4N(X2=VI}qLJ~-iD(Hf^p*Ywr zyN;l~$HT$Zd){%e%X4bUXE1VsX1VHP$>+7A`mp*@WFK;*Ws~vTYk=9VGe<^wBqg*6H?&4KvL(DkGXv#@y?1*FH!u z1-y57V)w-PNnp5X$4*RacIcO%U*@yTA1eqanzyV|ZeK{fK1^3i#mLkseQX)(94h)w zj4JZtYqRU#B3&%1*HG8U-;I6c&e`~E^jSShj7lvc@?}YcKh@$@5!Q7xN!|dRu2+>9rM zCTS%Dr@1WnXTqAEo!x6;w^nScRL|^*PevLC7*iYj<0U*qe$)4Ow41-WC-zlL!j;G^ z)@@}wZfjtRf=uYSSo5%7&T-*EI*WvMA|pJnsa;Y}lEmyM^@SeZ${6G2 zN)x-(-VB*~d(DotbT>{nw}n%SA4V1L=GGeKynDCF7yFaXmJ(yYj%xa9pYzPg8fYJz zg!5nJc?_Pbcr^76V%t#hdOm8NdLB&UMhiYhEsr3YDEK;!1{#O_m)mSxZJTXH5{`Ih zYUQmJH(x1pvlsFx4TTRO#S=C)g?@ttz0Z0@szV<;z^aN%M^z_HK43bla-A0gtd9*U%>#W8 zcFQC}CF(re)*hE^!V1T1-#a<*ns67cO-TiKwr6~l(tz$lP0>(v_uez)TV(%U-dcUR zj9*|kZd-6&2n}%&#aD`WA!hPHx#cH$PqZ%ub~FN^>{8OA-UK)#*+jwAKPUMLIjnTnSh>9{W~V(0{i-U3A0kbcwKx zER0`(PuRk>Y3R$su*Z^T^$aDVzFNd=w`Owa``Fh>vHb0Q>L%`jQX&f}-$?(p0Ss&< zU4#lUxk@*K@{jf09xItNEpwrrI`y;fGjE=%@I~)&t z^jf^U_w{u6awc7%0rEqD_fP(_ zrt!n}-Mt2?COSXle&lp21H^o$>SF7~$-u3(&6h<8W`OH>l;5%tH1ih=?-UPyc#M^c=7UeY-cv6*K!gl{s9Z7&?N^T@Dv zy^_Fz{ydYaqCA~)$9?ZfFBu@`kJ!X%1>rvUNdSA@{e(fhsIxDAKt6OasCTt&*?alh z%+-VB?Xa3amtZ{<_3`8>S-ZHO>gJGh8-OTY*&3$ zAKM>}oUVe33h=3G?P_c5;`RvQJ~^&X3N(;Bd0^~@gF|x*`@&VxVfzW(f7n6K$lXXo zUECVt%x7r>v9jg!a(;rn500dlIB@7}>u$;H<^0&iP25Y0<<}YFz%jO&pN09?Deg{E zEJhk2W(A0=EweD+9lkp((xlAH%#yA)cH%nsmHu1~e3N2%*esTCl|Lr(*h>QkNt)JF5eyg^LqoAN@82ZK@MKFkB#p;I0G^R`j8gA zdq?uu`TvJs{`L4DR~r4hQdsoAuKbT*{`XhvyV<%bK%9Xt-KGC6*Pm|x^T$6IO7df8 z{vVR~E#_aXfSjdCCHc?SOqz7D7E=Q(;|+)VT6(}Ipk&xT+<4%P5 z7Dwg2oSql%>J+ixRp{y2CM5|;D3#9!>8orGyTZC6upa1mdY#P0qtI44*V7vZ344LK^O_%kG6aKYghE`eB^dsO}nbdq$A z1U4n^>F&#O^lxO$mt1l296MAvFP&eU#f(QV^la%V&-q|wI|whpZnEdep3gh@8E}8H zckShW=Zu4Qla@K_MOqx*<$t1Kk(K2QXKZIVmqVyoP!OG~4(a{B(!0bF4oHUC=f>+_ zlEc3J&w&1KNk~yROEX1MU#an~$#5BX1AWBK&fZ;8y&M0DziG}f&_n*8$ZrI*(1T17 z%N!044hIJGXQ#dc1)lwWOzpV;R`bnejtX3aPp^+0398iG)J`*#XTIweF;tTHpYDVj zGB4o47mmBZr#xjB>8@EAtMb2>9X=p)UEMLxNo(Er;xuPz{Af$_98aLnuG0Y%ouPV* zCXwp%tR$br7mw(4<2wX*X2Y=Bag{eX=>m+F&n(93^DFPkPZF zwb`6Lu-HX6P+@U!A#-YP8812A@Qi75Y+%ZDZpgT{N$o^3kn^Xxfaxz2X})=h6<2@I z`^C_0jTD&jNo=#FYVu90n!rg{<9MHs9FajOujx1*&#zw}tHo~@%v=0uJggFSk{555 zsDm=$>q(ZiJ89#4Go%p~p~eHw&h7CjH?_3jTT}wXZe_uI8xP*tJ?*GHHR(TP2aQ1= zXti^Rm}F0}r{O|>Y)x&obV=!$2{CwW_i^8d@t+sUP9U#df1}^7nHlTC-*e|w zYLMM;53-g!b&HzSbzPR7#AbIX6eSQiZ;_LfBGlbQspUzb$2RfH&YL+dS>x)f`ADNZ zo$4X|n$`6c;BW$Y*rtMy9MvCEDxE!&?DqX>hL~AeHfd>1xE|lF=$%G& zACV$E^PqK0Y0p92u@8Hp>K6M&n8bKEN>wdyZgRY_;LywkBRkR_ky>q@;G&v1%ih3s`~u98{A{Ffli#8uQZ9R zX#1p8gV!5AVmpjU77T9I%UZJ&J2>(4T`(|iSk8JnYU*izi(S*cGl)MhA^k|RgA+6L zx^Sg5OZ<}gR4)8t&3DqW&oe?Eg-gD=l8s3bwFU8EuLB}7O2u{b^@NHbMO*f{r?;*1 zo!|I(8C9RDKZuBhqZ_BD5RH?QbyXuh+snu16*4DrY0}WXnkoU?D|r~(_&JPe zib_MB!O_)@RT;H_cqaW+8!7Xm0Kvd@vzerkz@EeTpavQH)N()Y2$51dzyF%gRK|>E zXJ@SaPRMo6H+p7=A4+)nVKb#=Dk33X>)X%@v9+WdVy@$dy@v1@HvUu2Te%u6&ra6F zn;?nZTxw@8WxB_m!-j01I@Q42c5YDa6JX(w<(u`*8ZYtx`0^wudA z$XtHjx_VK6Kl$CWcl|2%5v!z-r@KFz7Lj=sE(&%13ccjj;J}xXgGa8`n|+>%9sH@4 zzK*FANdM`flHkh%n(#a0d1v<^^%jP50Dx8|Y-#Vt4ayk}p3Ec;$!C}P|g z&=9Na{cBYipU+!=vT)%=Q3yJh(V@B8XN)5#FjtsXlGxKe__HJA-r6DDz~NsP z#tPp;*Bi=D1`g+KSE5#hKMvzoC6|^sb)|ZmIhe*ur4~jt8BQHVno=x|*_A%f3wHRS zo1Uwgr?bFS9w-7)@I0A6p{{T}fwY~XiW1+b^e<32>>lPuZXHCd2EhZveEgUM&RJlrsLqE=6P>r)rGp&PEb1ScBwIy_Dz{S|C*ai@KRR@ zavd!bUYGw>S_~h(W=&&yQ}Il>;y#H`7V@wMB&5e4T|~$0rZeAn!>>e-Bc5Y7JG7^F zTKdQX!h8}qV-!_XQ70`kHL5t`lK}d}u7Y~IY`~%%ji#GfeV^y~Xrdt{0$H7>KiQ;L zvQj_u_)fwsO7`d$>ZuBPU(mk4yF8~r>eFDX=%YUW zuQLZ90vw*LRo#ObYMvI!!~I?9LA};HC&Q)2@ZIBxttzd(T?e*ki;*vlsxEX$P}PQ{ zW67Y9H?^L#`-{Ww(6M|YNU?cQvEQDai!%F>=Qa)V6p`LatE1A{ZPf%-kWh_xVA)K= z7k%_N-WCz{?#4Cg8F90~4Y%gKtz&H|Rm6I3O~=zt+oIB!WtoM65Q}d^0vB~F?)4N! zra0?J(WXG}S;w;SJQy}!WrDt2AQqX?s3@ILg(3~^3leue2^h;`+?XD!OSmlEJG--Z z+)F(F)icq!$fEH`^`J#xsKi!kaEnuaFmZdv23}SbqrkCnyG{Q2F z@Yj zwWeKYuR^4zDrJ*z8ar{`KORG3r@9>_3DI;^{EGp zP11Y(S9XKwdXQD5c0|CIZGiPK!&d+gQ#-0bT>U-s?D5(`3Ea6tCQ3-;YHsY1P>5ak zT~wQv)&i?ehFB=aPXbq9OJ};Wzc-b1!r&gE$=tf)?BDQ_;}ftiAg0 zGRA>7pJa?kPB-?0Aq~x5+;P)1OZBxSawwsEGH`95WICp>#n)4)H9_oTIpxqmVk%SL zcd%any~yo&OcT?S$aZks&f$<$d)`d=K^tEk@5(#FGLeNAOjr|EPY(|p59?OXqHb{} z5(?A$%91Zwgsu4M@&SPv1At|^eHCS)B=c6)>@w!uZhDKirSwa6RP!+#XbkKbfu1k( zfkL9s)>b4Z(fI9@DFQwJtTJDQrNgl(z#leY z{h`!ttUoMIMJ5#Gp)X6XpBh+1yIui7HNITbU#Z&uNP=?FmtxcpE_~xtEU{UT?kXR7 zF{=QWhq=UVDf)Y(_sml<*my<)e=G+%JmC!~dON2xHPUS#h<)rHKl2eep`fLVW#bs3@? zGz(&0$p=& zMa1H^@BtaD2`-$m7h%kzOkpq6w&&4MTgr@!K`;q-BvyJFed^#DJXpR6t0T{16#58; zxK&2HqXA)_=rJAFbk_JS?olYpA`eQQS2!zsD04`ee7ve(Up&DJmUx)2vVb0xex_ha zq~{x1nMyX2j!qzVn=xxOO#XE00aypVW5@dpIB%_3q;GMEkYwsa5PHobb$;_D4qiF0 ztSr>~j0DD)ZMHgW`%S&5z(3JI$o2y|=)3b4H3+)Dvh_pVDa(J>NPk5y|1k-Q{r=Ed7M0aP2moq$0delsWL=$QLISYTO<6fzFVNYeNgmoWtJk?F@@X z1;cG=$?d*EI>Cj{%Jq_PQKu5a7Kw#7tBy6G+=;sfiNb;HW7X7i)hohT;}gATw|*at zIA@vCk1w&mCP`2DlNj$pYXepxwSbkO>iX>B2}{x1-mGGzbo-IR(76us?e#Nqq!V3wY2Ze*%G!v7gLY?rwt%Y{ZUt!ZL zZS6Ug4_SWC$Y^~BO@2TEgGdc2Atj_CVf4PvSxAj+@AO7I^j^_k$H(a5r20-}$1h%s zvI`y|fbLBc{`5CMR$6G)-QJE38#kUoxhz1k0>D!B)l8zCBFKcPO?BGCj?B`SBk>WD zuIjL47c{RKE2MuN$M*5XpURd*LlDEYH;FQRr&3Gb>+OoB*X>MuaS|DJ?2i09`XPpy zUz}wTG8z1Zj+(S^Iz6|FGh3DkQXGQ&US=SjO}w+1VX5raVkDr;O~a^J@{daTF< z46F8UgIV;re zJ6LL6>{_GLWABO5{>wc!{MB)29AOL8;dr|!%9i3-T))wf;Y-i5xqj?epRiJ~)HT4) z>lXZu@tq}~rMNvdK1qyz?ug^H&CkDkR62c+`^u2)F(*3G(B(D|E7{S!vN&!OS6LoO z^8=mf^Mbo(a0_GSrI2&y2ATewzy9UDx3BZrZ*a-|>DKd;Q;Lc?8MKSL* z>8@FAZ0LYjkCvjiMPP33kBn4?SH)Ro_@pyBmtJhQlzGi4t$>m>%ZGWpbzq$kC!z z(Tkz+nf8cEBK>I4&vwH_pH=6Lh^vq4+k+2`e$s&a8A{6*t@SG>U?%IPxjT)3jW>1; zuRu3WHZs?LT=v;Hq@-Pq`wGr8S@%gVTV4wrS1nK)!xf4#uEB?wGBobSiw$4F1&KtvJhKsV!vHmoI^jK|U0ZcOqYntR()9h+@IlSA* z$kpZdIma7z8g0>D1n*9FyZ{e#vv3B)<|A|Bkr6g+n!x@IosuF0PpQ_q^{Sg)xB8l* z4;jpdEr?jg=vMbg(TLEvqbXEl*?Gi*MGN!T=7v+mL9J2bfy0C{1ZKpkGJW92UF$o< zGK)&<3xdzoxL5l`Kms8R+F7IsPWaTqwn(lKgu<0b51F~G|JD{Yn(RjGA@b@;<<$6H zCPBcCm=qE@lVb{mO-4+HL9;ahhYyIOHNguGtI-K3Qa_s3>eOr!;A^M6isAtmb zW84^?`G{#`+)P~@$voS?t=~AP!FHpG-Hpk-a$M`=lo9RmvX*P9A24oUNKG5O<}2Ny z4yKYyZC_~f%Al(u&UwjjLJ!T|Ad?v1HJ(IUfROqfMQQ*}p1Re=AxzMwb5}u$9+{8m zGTzYg)E&}+P7bV%Ig8bcl%nf2-WgYwjt#WT-{=)(b*9scqp}cp%FFDCt3`i3RzhS3 zuuYu2a?~+FG1n#U(EBbxq&b=jJ<1vA4H9+}i5M9>c??IcZfFZr>-&Nr>5h2LBHd4q zu-Z>w>>dL?TG1Zcnjk4GZnmIBCHF|1Je#P%1nhpX@<{s{y+-Szk{Hre^Aw6o`=~a$ z38jz{Mq8nxJo6j}{RFnthV=OzYI0Q_YL%q6L?%kaqsbA;m3|`K_BPmL>h&5_67 zL!Sn&6gfe(tK~Cf!E*?gr!sdt&da(PhP<-0%I2tp@%>OvxSXd zSV1=Ujzz)@w|IGvpvm7G%{e(a1)SREo@Bn*y37&tv30)QHr}N^wZE_&{m9+3-3NEZ zFtgukdv%@n!`#?Q>K8#2Gpzb0B@~qh>5R3F*+U$L0!?AYM@OWvVG2IGni0O3Q9@jT zW74Z*LE;Wude`r|#^vV2cOO}%IildVLWs%Zq>*H&c8PZ^ZY#1fk08aBDe~;+Oyxxk zlQx@$2!{N+pKsupubeK`cRv#kF0&k*q*0QEa(@m0$Tv+aqulaRe(b_kMIfD^-QYA` zmK=nhdX4~olmOBRqqQS`gRjkGJsHQ{?wtw2gt~8CcVfeMtvUd7?lxD$gk^OP+Vz_9iHlFk`=;PKah1cb48P$7g`Dw$|Z6 zlSZbcOy`@NWTj;O>0zG!ZyPtIu0JQ_{rS0KN&uZo-nOaF&E6@Z<=*9KJh|c*`xM}t zMF}pzC@#_bN3Oy!D9EJdG>zdr4f63O5K{)d4?j;#fNFq=G-DQ5|FPs0Kp5y%MAV#B?~8m(RNfNYD$g=PUhV z|HgL%k3f(3!s~xsKj#LLD`tUx08;tW`0WyNmbk3Zf8*C8=8(QpouC+Inl@+^KX8C zM}~u!Ae#|1UgDCyy20CwD@NzzxWwTXe2oM~H!hef z>@aX&3Z1F9?-|7X@AsdVfLp+u%BxNEC`p*e>Uz=4sO`)sed!?5_lNNt0dIPbqSSgx z^X3qZ8e|}yN0i936KYpJZne66y?7bVQl{}{PVztYk4M_7E{mf2^g_?k6#q2p*?6}9 znS<+X!R|)^2~Haz(7Pro6i zkL}|PKvu>nBl*jP4**wyNqq5$1opa`;ceKAv6Y`3p5vo+!Mxvsi~$EHk;3SdN8X`3 z#~iwhmKQ1U;7n=E!s)$r%xf9RRyPVa6h9CQk-YZoqzBtH9R~-|wMNcg`t4lgnJ>BF z3K$DUWY}1gCxE0s+hCD?f5w^K@LT>&IPz9q?^dpUC{;$=kZc zq#nJI51tz&IZ2}W)ANZ2?_U)g=6$$WD^^{yT3xi;ArULmU2dK1y8Aex_48*HtCwDd z;L7632}I-0Bb{FHgzJAiP2SgIFU)xcaI+}@H>-m?FmrF`G+_X<#WnDKtnk!BuC0+25&>9sTP?Gl6Kx=Q$U61GfCQ(CX6Gxx;(X@!U=mfqHPsSGkv?5ovXO@EeDuS*ZM_qG){SEi<4Z!Eo|E31kAN#mdH;w=Gwh|k0HdM& z<74NrIP7f%mSdQAIec@tJ!?qkd*l>ScOPov!%;B!cKV>xH01%n<(5?TJUG$FV5n|3 zIqerr&wCnK@h(sgrK!Bv0CRVQ2TU9Ab@ffs_u z2r*)`iz32!wggtB;OhON6Zck^GXq$36V^mokSSZ?GtX=RCKg-F;0NrX=2n-nWb9-w z0Jg(E*+p`9hEaa#bt5{G4n%H}0_j|T@6962R*qvCyufu{t!p*0ZeFZ!f>Z<-vG~Q? zRTklZ(#29jJL!f#+^?X2Z6z1j5O3YUvhLkelSD(i{aT5F1(g%UQCPOo-*{fq{(V^2 zy0cSf_(V$g!U_A_@JL|A?%ET5sJA}yVwRuIGeqA+1d<0V1miFKd8DAZk76iXM~{I{ zJ=XFpBpkdYh*$p9V0^?9icQgo_#j8^P@6VqS)q1S6gzL(xVH~Ij{t}KG#zL85*4T!+*HDtv7s@$zkS06550@eKEIGHW2}&{yakmQ;-)uDDDVN_ z=7iJA{M}dyekt-VQpHt71R$IY*b_f@t&MVyZuFOZ7!>j$vA6G}D*C|Hb=z725*hAS^{rtGp z8|s9d2!d0J=BF@TJh2SvF-B-<+IvN*XGz%gUUPT;c$;weWwP9#eE?u19kur}Z)P3t zb#54l;OZxiDMe~IQjiEO0D0<@TebLyi+gEJaNJYq(N`gQ3be>HovuIX^;uE7go)xj zdzJ}t;q{YOLxC__yyGgF`t7*s;W9%7?+RtsBbS~(p-&JQu(y-3A<52_fe+ult8;d2 z?Lj@F@D?isvc7e((eo$(!+3rFE1fm*RHws1xW#ZEfOE=q-u8HXr*le8O(q`tV%&|o zWa0zYL3~j6Uw0Gw=@q?LsryERFk&we*%#DA)@^;-H8fdjs zIv727Pma75{6+HtxJ*6v)0JTp+wqzGoyk_Hfb&-u>r}x8JEm`E{YR$=_2J`pj*n1r z#V@K;W?j9w&+;m$f1AxH6?0Zy!wi95B8bm(5P#nD@aUe?Mrf;-gP)4GN_ceQOq5N> zB_Tb%L6gpRV7KwYGk1(f{;G(~vdBGisG{w1u7#FQAv|01iKK4Ts>DsQA?qW5^fs^1 zRsHrC@pB2)w4D~1gu*sVml`dq{*(%3$tj2VkomzbQ{B970*xtCCQ8CstzRo-;+^Eo zX;ep)sx|p@DH1MTfk!UC9JHTbX_ru8#|Rg{MWf)n^!jhB5)ERA;?=ru3cdXpU9{JE z9Pe8)ePUR2S4a7-65O`);%_;+VD&E#tAAVNrW+%RodKq)8b{gq8+AITqwn&|(Jh1$ zwmXJg_ZB|TBQ_S(0*|t*22=>!Wg?RJ9ekrCM11**-b>uaR8==^st_+0fs-r|z8Fba zn|XSq)c9xd>eTK-1We}Q5*O;NT1wIW--tR9h`|#<*w?|R^TyY~t;KRRDFrxb8ckMC5JK@6duf)@)x7(xAc72GXq#cpsom+u>)o;0M)jE!3)?zl{ivew%{nMC* zPbzs-bG50ubCia*VGrGqr)ky8S(1C-9iuZ2>W+16spbk2%Y;%GctAqsc8k16LmaR+4^Pc02aey>$+jr-ZJI9TsZWxML}9?;hiO?0qa!0X&)nl zbx|I6!G4vguDpo$y%Cu5=}WZNVs6OVtMoA2FgmldaMB_SD(932UEY-X6=%oV*WVn& z9ls@>zf6US5{CBQ)!*e5D@tL$XMn%sS@YrTJ-E^x=rnP{??9>}TJ{Y&tYq9h?!$)< z$9^MOdm1V#Z;SkE!TMz5ShA?8O|Xc=V2x5{H~t2Jmwd4pvwqRf`Y+UBh675X?eAM7 zdfu`NSYNqk|3TZ|uuxWy>=G#FaQDg6^OjYTOCalwjP1z3(P1zQizP5?-+%nAqzfLs z!kpDMEwxC0j+wN9^g|?iqUG~B;3#JYT9CedBG~$$Ust}v;uchO*lUHqEmi=PPv^Ur z1_6@qzXxz1frBdqhfvaphRzmk?*|PFI);}S)FogJg+Xi z^#8Vi{s-y(UDE%*0?%L;CJ?(u1{dqhHQtqxvb`h4bo$uXSlgjg)?BTPj6MyXbL+Tp z^_}b+%D=85iv;=rxi;J=$J8JE*X(fcvZ*I-m3WpTqrBrcCKBI2FcTmuu7)23 z!OQ)h4{dHVwTl+MITmu|`|Dj~vBL}?nwEz%_5=>TfF6VX&H$Hyh2v5sE-JOWditbM ziSVa~&_QQiw^49Evb7EfV z!2JG8K>!P?^<~QNe;zt}tIFOHPtM)--ZWKhnQD`f_RdNo{?=)lb6ed}op20aqJF~Z zmB0kV1V!f?)?^mPh=~E)EAw08^R{ygy?Kv2e=xSMJXdBfPS}}E)+tvf@jfaovQa`q z1nkCX#tKR~1!vRP*xFQ3U3qisrI`7JVZ`>04Tz##DdbCaQ%9Rrd&G~_VNQ0S!e?}` zFyr)b$*NSE<>L{Gkvu(2EkFHl8E`ma=krTjl%n1a4GsBCvEn&4)|jpN={*0+cTzw& zDWd&+a4e)WJ~wnS=J0QglgXc&C7X1K4Fr$>aZM8 ztaKa}_yc*aLb277E|9pBNZrG0cyLp(y(0!0e@$$?k|;A!pBD#0q^YQUhRzq6CH@=eTOjPkU+Tr$g1IepOW%1Gx2#rjCp z;Sulk@s?V$?%z6l6`-Twv%8l8I?>Wdvh3|r%!Wt@RHI1cNR7)U?dHlcVXfty-&p-# zXnCW$D@pQR^!l)LAC8w|&a17qbVF6d+}mMk%2VGw9`pJclg4^aR`27($sm9j7au*< zAp8`twqkNrHtBtz@kXUyUwW^=us2!la9>pNGYfF^$U@gA{DKG_ZbQyngNL5bG`ZZt z*Ma~Ty|J>KB8-@eoJd_93;SbTS8cH)UB`~}`Fqj7B{9jfKW1%!l9*xrQT&IUIwTc8 z<5!BEcaxondK0XbHR+4q4h4`sJ{rSEU-3`vFlTUWDKu`2%iQbUo~dvOdnMXGYCfrJ|C7Zqga1iC=vhhxI zpQ#V}Yj)MewTYgjy**`E*~ah+-4QtVNLX>u_)t}-{lkJ=Hx)XgCo8tF|}_?+iX#~{S;6vbr!j4+G`0j$<=$a zcQ!CS+Q;M`%~9E6Qvx_vsYqaIUEPTsouImmSo7 zkBcdaci8F*MJQeWR3(Q1FCx|)yMLN0j1Zw_Fw3tq-g_lBY<~8cJ?z!AV6aGXX?XB) zw6sUZF+r$m8z{oSE;DT^0OX%4lfWoq?(kFrWuN-!$dRE$T1ZW^$*^Gd1z-ucZ|U>z zKZg^c(k6$pTx6c;xcKl^S0A$m*2%B0n9cAsFBVKFAD@=?^GpV$_s@J)e{pP|UfwCT zlTa&@`Dao**h!_c{hrhoCpE~8@%tXyNcqL&c#c1(8vCPTs!cEE#<>H{_^fc>o^lkfzafY5LBty)>Ae zTbLm(3Qe7JxLGeYhVS@(#6AJ@GpxvFGi9|;r;cXyaoNNlsO%>4hncNe!YY`lDptWP zzfNC~vOsoizvzSJDYU3o*o|C1@Yr+_d&)B?UepxixUSb|_15fH#+e8MF6tVsnSWoa zB6K-CRuQeCM&ek*)6r@>`Ez`18i1J z2#`8?aE+$DZ|wGlf1;F9;m8kYg8NsMu9fntc<~-g6S5Fn2kl%EE|}%>*qvu-`e;UG z>ND%N@vz{5`U$6CB%z-2o8fn5E0rD~!QDO`AO9Aj`T5t%;-j9HjK89d;Mbbip7O9g zCD9yd9UJHKouWwJEjVx)sJ`_)om5?CM09weU`=#-zs(8IF+fM&{}aAL@WlBCy4w(vl$w(Sw$A7 z)nm6}qy6utu>c7AIK`Glg!Ag>0*K(_J1Wr3nM2s|_DUKp9n7OvkjR{yT8&-FC~fd} z40p*J8=wAp#KrQB+Ak#P+P-?RTZQ8u7!Rm$L~ek)BSsQW;cb>uuo<@a!4Jao6|V%o zfATn}u(JOhiM@hjQTJe`zl~o)aJtUBzsc;j7W9K(H{5&Aa&E|URD$o3+oLJ9y zt+X>Q?8l*{tYKk-*?`&SKMF{Tz}|uuGxfcD{_?)nn9KEeK_UlKm&#V}mGrIED~}X^ z=Pt*rOB%gQD<$zyh{b`b?q?P=h_aqHEn$MiDTAbr5hK+o7*WIUP^S3AFCWh*jEjf< z?4f)ht!VM>_&M2@lh2DGgYWk&koSXr(*nUpSd==>uy}Niy|sZ*GjfZ*b^bigEx!%4 zASC+s6#rt7p^A;hXrFiOT2&s`g zZyCnIc#rmj#@}v|IY>4R3*(Q8p8idM1j!Zy8gIQ8we5cn<2it*)07U#e{B5?%l{t~ z&_^d(spK~|3x;IW|BeZOfA>w8wPc+q;Dq1r4R{7`p9}qh5uP`mV|5=xN#viP;a3*m zf0B*=Urxoo@{>)!=M9hWv;I&FZkpbpg}SI~Y9?qyqGF_xzTzU~=Nt|*C>&2wI;+ZY zP89oM@R%cg833j*M?v z5I=0}64E-m1WIeobe^@&A1lr^9IADGji?;Oq{B266RSFX{AVHhL%#~S&NPJjV5yNje#o#m z4n}(mygyM7^QBPP9%4P`1@7Faz%!{f74jd-`blB;#j~al)~OC`w{Na+QzD!4uz5QZ zY^DA$d&W|ry?3{1bTAK3Q-zDWo4Ay(rpV^vqp``qZlAL_48QiHir(j!K{*jY-M@1cSsi3R@|Juz9!4&4G zuZh_0hp-p!Ex07s7B(5T%M2H3XKb6&yV(t|N#9mzI{Rz{&nfkNG-JnDe?uCobQri< zSv=wTKtQm+^ZE9#tf_kP{pI0?6v?3zszSZ|(Du7NmfNRS1eR9x#!8f!L<5c^=#sxZ zV~f{}LC33hX4adtAXB{|i-*92e{V}8x)L?x0;i|scix+ZG^xcSCZapCTdun*9nkPI z^6PW4ccywGV!Gjpoo+4$e{j&~e?$IN#u1nA0LaeUYMP<#_UL}kKiqx56F=Udfp$3P zdV?(C4b#kclfmDgQ0z9nH{vY2HnDXDG?P-Ucq-*k2`#OdSM7*ftXI{MZvgUbJ&E0( z-7#l2)w)r-(R+!M8@6jz?ngJh_cPVzj}OwGmKF2X3Z|P5$5bDh$JfjE{>sf}R4Cf& z%(NpO6tY-vIK>yJ$k9`O(Fl=oFJzbDo6Pg`kIV6ETA$HJ7e4F-sviMQE_;szSWM_^ zm%!OL=@&t01HQncoI-f&#DRxsDsZ-*~b?-$#%n227x54>B!1O*jY-o`Sgih z2s9!C`^?;r+1a@`(xruKWj-g=kz8Hul!(E~VW8%9SXC(2h-I3Cbyjb^n{;PaZ6z`B zWwgipjjlhD(sN*ohfd-FAeZgpYh$=l`5+lr!+dC$D0L&_L1vq)R)Np(SQSwAEHR*^ zQr)H>0o4kqGg#nJtE}2vo0dI1Yb-|;6aNTA$D%4$My!X$MR1|V#|&G2hGWr7nBW7F z0-r!IF7yuy^r5x(S^Qo#P;Pn4O?xM`#9%|5#a5NgD0-|PmhxSbYbZr1OFSuH{{gX+ zZ`&A8t@n^TmYvY=Bi3?{<6eoYOjyGe(i93@ntb=-Vq@XUI9}t)QDqT^n(fHvuA1?A z1-Sr8@GX8N!u#~MgJd@Og>2=10gtD1Lk{H3z(0|oTfzP>fZnanAzp({Y+<;#2>dax zZ|d^SkBgn6$>v8R@8!G?Dm^Zwus6ITni%?04my@FpGg3ERpbeXV&rEjltp|;{uNgzaty= zm21AT{>KYfYwN9DmP9svgMrNF z*TZB^gCn2_u8Uwb&b>Y9R)riie?a#ZiqE~y5r4(mXJH#E;MVD4t=pMPV7%|>LJpJ4 zlKxu#auSv8lEJcyXd@Vslq~)w&~QBLkZ%7TQbRz8nTm|p|Hj^1M@8BGZND&rf{2uW zf^>+a#LzKH2}lnOgNn3(bjPTmbc50zLrF_pfOK~^3>_me#J&da`}Tg;^RB(u+WU`p z?`Qwk8s?8{U0id{+27A`e9viTJKhT*UmSQhftCo~w?E#CY+zPt&>oyv1Ze56B3#1U zlhbn$5-^yzpD3@<#_Iv$-DH1F@#IYQ!xSnlPwEK@WkfsruzF8;*zm}yt%OfJcUh+i z=Ip=s;2xkvPoKy8S+(y1mPf!EsEi45r!clPAZ~BG5GXja92o{Lb%_-pc*fZy7J5G< zg-O>ME$xxiz&qX3*8Qx+UQL?NWSKVXRe`t6&v2ne?ceE&qnfRME!|hU_T8XvB;MS^ z*w&`J?%=OTdE6{juWU0y{GglEdMJruM9D#gS`&n5{&p!QLP!94N7R-UaK(`Z$a;GA zYbzJDH=S=ZLcgF1gX4yNCre{(DyD)gGv2GDfSVE;?c3H%S10!C~T z9k21P4H4TI!#ZufjxC+yfwVY+OQquC~%_WnwAXcUSGT~h+1`{WoU z;dbnKx(Rdz{nm@TswBHSO56|8v>XSXL%jKlWA%0}e24Zv%lFa}TB+a$gyp8gIboiK z4OY+99{y%nqIDvEwxCZ=xsJ=d6oou^`fI?ski27#O?haHGZD?81!q{f18&LHP{ zDf*h#lYwd*lZ4tk*HBYq6N|<(D>wT~JeyUidbQdMR2|2&TTC`DnnlOrQy5%bt!H^* z)(^0?n$8c#Db4kWwljU_$C#pM&^kg3s~#+SzN0%XyCG{LokSYaB8v^e9}#aK6h~& zpn2mE;NRack_dyTa~ElA-^@GHwo$1bw@ZlMU`<1Mzf&G(OYIspqh-w=oJe6UNPs%N zLQk}E#sSVFb32_o9$|=Guo%nn^*p;L6P(7K)7+>b;cT5;Xl%!zTTZQ+OY!v;Oin`s ziIQ))ERw`G!vk89+i1rXRU{VnB30>3uUe(-pdJx$OQ%HLD5v;Yjg_yJN^Og4I+EU$ zt`9I=Aa^dus9Fq$Xx3d4O-uC=#c4hVcxwTqAsN{UFjhI0y54Y#tr&IV9FK{vbx|P6 zflhq3Y27r+irPxWqxanMN^vA9yReJ5d)!^A1WVhf((sDtDnfRujz$o|vDbV3DUcHo z=LD$f4h!p2xK4e_Ot6}2{{t)iu1pya;@UNW%aBSIioq-Pmmr6-$IK`K&@2SUGhu2a z=T0i*gCEjoBnFx@y%Ae44vq_Htgue^Y3t&-6WOHEMl5fjz2a#wPsBY>$W-R*nQh|~ z8yb`CW7}e{jcD-RK#;_8#_Cs;FLZ?t>>cbS;9g!TnP1@nUY(tU?|S1xC2kF3p%}3o z>valxlkQACnae3giNi*$wla;~PnzfoBfw2}1$5lKkPlBU&eRO@wHVq=8ekTOlL8{1 ztY~mf#lx=e-!FIRa7N%j6z54GQS1uyqhL`8_oSB^km<`259Q|K8vL9EAPf%)PvHEF zGPiYxF;4+$MFebTib@BCR|+FypKVV@HiHa^l47=Wpu229aunEAQ7ZEOzSjN~?~QY& z+-wAb&{l&(uU4q|xx}#0p|cM%tyULzs@b4lHXN&0zhSTGge}$?VxZJHA{~o7K zQ?PuN>3Jh)C-!%2j#u7Y5$;uQbg&n{t9%O2c(v!1 zlH`=Vjh}x9KRX<1i&qdccCB(a-{>FMPP^PGxOym6BFE4hP1{jnv~R_?oI>$Tm|FHX z4Ee_{ut1D|7f^b_%u}KhL>4Ln@^piK@8jv0fnLvhL5B4=7$JVo4b+6w6jAZtF$ZwU z25_bG0tYyZ=&vv&128I486CbU_}jPy9Q^@Y*_Xn``Ug9#0;U|fk+WH=f3AE0&_v6r zDi+p1kz6gX&FOQN`b_oDmH(;5`42k51R9vV(Cq(I7yJK5(a9r&Nayp{cg4Jig?*oU z)yHs?q;6Y_eEi5_URD;e(9Xj0*s$C9Nq0e0*4?h3+y-`qYV5o-rjD0^Bk=ZC{@*xd z=d}2d2GUqcpCwp(o4*~Qo*8m_^d#OZeqt}bjghj9C~?QhTp?d?Sae314s~O6VG-D<(-ezD4YW*1huXbAQC+tI-7BQrOi z&31KAY{rPO>80e0$Q%C+wSWNF>KIb*50&_Ekwdr+;wY2X#R+;2Idd7Jbw6GJ2gv`6 zv_w!+Jh=WFF3&QOydMKHw$I(yr?#!3Saz-D+!Y{M-ZU$?guH4;JI%Xn^8sWOv8piP zX36XR%h~byLtyrtb2k!RwCyn8TQ!wbM_&y(fW(GH%>&gTnhTGh0#f6 z%CUZ?SI8#I^2pZMTd|FQylpl;Q6nk(tTKxub1$20Db=-_SYl&%c3Z=A^B0rD>Z^bx zk!Y6YV2*u2G*~v`S}a4F?jcNi;>Q{TuXg^en@W3V_AEohJ($EEWq zM|?&riu5$R`6T=Lr1W|8l&y%0#56`L*J|nCe788~%N@IK<)ve+xe^CnEb`HDe@Tnk zHx)jrw|zZtZ562@3%LB*Rij@EvZp0p^=|)gUcrOjn;glflF~&sUynOzZTykx9g(T7 z=_*8Dafsb4Me?3GR5=}U#K>=Luewik(nfgb+7UtCW@>7&m3xog5|XO746%IhGjd=U zC3*$f1jvZ4O=C}KGR-j()zOi#NH(bU&MfMLgGwt#mDVcPyr_*&tNj;;p}^TJ#21>Wg>irfyL`9?KW|}WJHoZG-AAT` zs8dyV2$Q!*{fjnUETCtk?)HEfQFUw>A;-tyjG32}9wQ|8H5tEpbo$5*-Eh@NA1=fm z`g-tPUQ~C2A2?^N3rL=xZn#(IAnBHoX{eW*RQZx9&mM6sy+Q$gv7@Y<1=xOCBx~2c zB-?SuI3hSDSYj0;a7zr$JO38A{aJv0Dv*_hAr+^X5dL&OwJ55|JH22~-R6FBbd!ok zGA!xYUFjkDH!&r`| zds=IVg6GaKN6&(rr-;Wn#+vX$Z=z@@^`aqI01;OmYBABIS`IQx9&g(#x{h83kXQIj z5~HhYp}B3DBs$cHF+Z`a)izYK64CMQu%e3h&E<3pd4|ccL;h}y&C1l+f|w{v#8hT= zn=z#FUv!0>0q{9ERtrU^r{-5>igby0HrGt#j{v58AXm-rz%EY3P_6%6Z+;9W!h zpR*N(p(oGh3TnV_6`-qpGXeHx)6Q`_J9Z9q#S1~zdwoqnS@jJpA)TWcDP z;KQ6C(lgJICQ292LpP>0KWHu6pbYpdR=*S6RzV#DN0QXFaL1w&`z5%`>#0g-YJi*9m03bOhA%z84a~C3Pb!xn1Z0Q~27DUs=Z{9uBy=JLbQg0KA*7F4@ z)820jfVcbgL2!JvO9>AqZD4#DgJ|}lC1K7les(Dr0p1-Jz*r`|%~`75b;qHBd8LAv zB~736x2$!G0RxxJe%qBc_+I5C4Vt(k@|6k__)fYx8d92dfW}dQj(8Q{9h+F+^~nBT zKo;;NWRY+#-}nt#JQ`AJtBY8eY^`lOT`~C)q9mi1?LFwF<6VwWlLNY6RHpwzJHSp` z)Ry8ev@@aT`<)61msP)U4{TKAaagpD7gIqjHc_ zr(Zm@KotO1oR5hTq`dazj-Nf;8qI1R5-w0O-hQ^>@crj;nb_uM-t7{mQL*x>_9HsM zI{+STc&Tf9?;Z$|-}SpK)G?*l`l}Q)?UKojU2J;cp0zubv{8AlV;zChNr3F**`@8> zY9JxPn&u)?j?!%*7&UCVTKcF-hb9^k(}gy>XML~ri4O?jo=5W8SPd?gOStj5b-aM} zD*0jwT~U;5L3!epRaPvecc`85;i{fbsV`<4H_~&WI^ttQc%1ozf4$lpQV0^vqoDk4 zdQ-oQMDc$2&JNeRao48xU&d|}We!ef<}Ur}3ts#{^#i8bZLj88FQ{{qR@n6Az}zMi z_}pBvtE{E$xb^>%uf+N&HWWK0Zz%Od)YX9_JVl52g@EW8zDLjMstM0Jv|SYmUK`-!ip)9Dz4yH?sAs37bW7R z#xD{=yu6&7zhy631q|LWz>L@r6d1g*^~OvuIUAJRaz_ny{#W`0p;KPvlnrF)+Kr=a zkB?d70SVEx6~5zs)>&_@f_hO)dJd-+IW-C`lKm>xL^mle*W)1IEY>0WOkmoIc)*R^aSo>nnf8L( zr6`Uty3`|TC-Vc+JcP}UnC*a*EJ`NIeI%{lp%kIzA%LaS)S{{2Ma1=lYlGIRC}7(5 z9IWxZ*n8>d8lJMf+}6R-hr=_E`zm?h(+<<(cqh-3m~0?wj>Db`iHTI z&kJe&PSberf|+%0VrCr@-6vv63`$VXj{Oy$#{>FHS@wiIRatOu1|^#6|4LZ84yn^X z$=_?dbFU}-O;|=3InoluFvkuYBaY#cE=^X(23nz{y;<62PIiXP9))&W>!*VcolCFR z23KbV(_*JpdNfr$Ett#JfWr}Kdq2&*66VGz|#XI!>v1?-txX{CfnknL8gqG4(A}Tg-kCn#Q z)-L-EGEdN(#xvQ*k9ABvl-m;x@DUYNcb6Bqe?_Qnc(;!Ikh?az5MXJ$OeP$fJzp8> z)${1jB(AWADYr}xWDpkzX#W08wF7{l$X?Ly5oXBD9<4b#hy$l5KqdD7+WJB0-GJ}C z<8t>BGX8czTm`P&Rs+J8f3U}Y?)fkE1HT}`!n(M45VBGD_Yv3|$=AG~&A(cSxDFtz z4I{l`Z)t0q+(c=(fVgGsk zWCQ8ZtMOl#K;r)E>L(^FxRpF^4+IP6pBG<p>6jnMv|SnN7>S;{Lhc4|0{*57LFTnn(KQ@sKZIY&_`5OX4dadaS=+ z5!g6D;jlyzIRAR)D`hOu5SaSMzl-n!3mb3nhF=)_!+(|7kjxL_+;x@r&-1WAcOY0G z@RhVo#XnVmg*^g%ic$H)8~-Wd|Fc@;Yk(w6nA3({ykguOE&cAyWAprxw6x_n`GwVD zTcelHck%w!-t?JxFFfGmc?vkP^76R}sqY7;i4GN=eO~;#tsfhMKEl%C*t8_OWau>p zLWptyv^u1g#1Enj!H-O^-M)1l^iL0g3y|Om!#(zno*B^BW}~{3#bQ{L;+WUyHKOUi zUis7RJ0kG3TX~Q4eynm_cyZysdOyhgm~Z22(S+-2e;QsAa|t zx3UW-P83zcWTE@QQE|*r3!@$D&OU+fS`VUO#o~C2|3GH_I~#FR zUVZCqlf5^B7ThKJibNNurE^+Yid+1t6T42z-3pHvtzVGz9#=pQA@q5;h!7qb#6{zh zs_N9^#LVv7gl{3JY2oa6tS3qYO@_`NSkLd8-eTym(+*#IB0|QQv^LbJz0H|Xpi@lk z3`xr|tvHq9!9iSvlj!4t+DPy`;Ou^efrD!UIbT$*9PA<}7Z}tU@N_st8*tLJUj*^c z9edGiPI^vv3Z1M44AyAdFGx0wUxQoC6CM%Z4f@eNcY1(_2T5bSQI`Ce-2B|r#fIqh z$QP+PJUBAZUV#VuL)`Fr)v++)g-3z9j9^t#G0oJ+GE?W5FP@bAdI%KfEuRhyZ?F># zv*5xTCBf5&TV4BZ>)OOog5`3&%jt~|6q!d=CmZ;FPFjt*J>kana)~#M(zq*?)WTi{ z!UTaUcURG^{YSCtt9N?2C1IqRk#l|&%0(Yxjn zf;SM<`rN|>$QLHiLnbv%PiA%;g#5NTf~;%OErhjaN~MwQENHUJoq$=yPHW6Eg=nN7Or7YXdm#OT(6gWDUj-=c694&+9hmlSL`z z?MpWjf`qV^dgvaR2CrnNAC6?SH}54ZZHu`32VB&CucteHYJ<=f;GGVcB^cY4w49S% zh(x}lO^vdyJNGKxdvI32ocHx$Idme1!_|eo%f@E~t`DtqvWS{De<>{2WaD&KWU^SB z*P}|gnb|TdsZQ~YnQ&a^E&MK7GP->8oH3sb($JL3fzLc08}f$;la{f{o^I=Jgg(A4 zU1Q4SLNc0@L)1*w51N3 z^@_H(N$-vaXGXo{*x8FdAN{(O!oHU9W13orO^SxIff=U{t!jeLP3M;IpFOL|BP@3l zyJLslf2iKHowdqedW$3R^A>Si_r3$mUY~sUaOQkxyHk6{H!2FS5|F|5Qe|perU6e#C=x#zg^;{~D zlwF7IF`B4ZxOrX3hB`hY{D>%xR-EJsW88`I%wq#rQJIhwW1|jRRBNqvdc2qxFKzSG zXW61@LrB@6%%gR=>?E0_-3N1-D?f&uc6MYJxP9SC6)!EaXp=Lnt$nZW(Vg9<5OaAr ze%k(K?jW3EDJjD_{;6!xa{BR`ana_E(Dz76DdTOoZj(AoFCqRe)l=!Ev+cX{T5nrh zN{zaNt4l4keV_CP07Wksm#okD=3%;*nV8v(u)im8Q*CIuv9x<%$dOl{#hmrq!@D z9bD09UCJqEK$MW0r96C%VWZBit&WDxHB5%CrPl2R*N0%+9Z$Pcw{o*(^j(HA3&dW(Aaun*T{!&Bbq|_X{MmW4)Hr;U@HCRVl$*gm^D&EX2r9;~y+6=&H<_oJ z*=$~luf}oZK_TN_D^gHwZRCwsS`gXA`>zC}&pkRKL-r}3cUZ%?7sN|ULmD<--D8ey zVC3je^R)WVyav*exTPKwo$NmI{F!?)m#~N1d@fcx{qgddNY(StB{V~Ns#b=<3nw`xJ^Xj{CgPc; z`G^Y7)Z0wUqjXi8^|Nsrg3xoJ!_ZJJL-rRH5N{OPrXq$bmmP0g{P@FRmzb0CP0v?8 z$w@efLhlCiBhuI*VN;bKMmM*7pLBxDw~KMCp3Ak0aW?T3I%{(p`}vqfBHjgZ1~m=J zl!mxI^LfXTB!0*GT&=Bp-ge28Ciy0Xo7uQ=g=MQ{gK*~59&JIX!#y1W#6~i2GI9i|T^bj!KVv zvDtdf@%NKceas^fuVL445E-xSh<_RadpE8HT(}kXy#EA&PKjo2dT{7{@B6T~6CpYV z8fy<)>CD|(ee0W2nr2R=&B^nmeM2+!IBqIwEQAmH!Yt%a9V{yQXTiBqlshr$K{GoK zDHrr5o#5;lv;Lmey_sEPT(1*vwyIqytgo>exG=vbH}A?*$>H}YNGsPrV7}lNPcD3| zJ8D)x`K^Mn?Eq;47*+j0oj!x1eD0Z?xJ|+L4<>Whq1p+j&-tzNOC#KQD5Z*}6is}N zCfryby(C26Q0~ZoJbZp@`Tj3;?{$rZ>(9&@26SjMRDGB3AK|3yG#avgP&BV1wq!wGdKwwcqR+^15~NjMSRGOW=-$0uV%nhDy>8;;Kx2f z1v;-KJqsI;!(@%$XQJh}gqm~CnXD-dtkGdc`9a|GGp_@v^u9zS-GhlR(ZcnUCVYPB zZ?gK+;QR&es&kh@qof2K2Op`W*32Cd`}rVsO-Yz-xtDH+zcFR_^RpWz`tRiqo)~sJ z1jNrc`TBk=eF3S|!)fH?$=9j!+kJd4mDT!zJm*8@`GZg*)`gQ))|M;_N zuHu15HL}IKgMpxHt@C6~*5ZU;xzowPen!4v^=F?_OSx6-_tkeRiDJ#U2QBPsMXwEF z59db=EQXfJka60dA~aij%(5Wfds5V$*|)UQp9{wE7}`Wp?RDHdaab`P3FlF(&RI zy2>cJ4~R7QOR#a&Q*b;TK6NV#H^FY!O|6@aK+tp=jj3P5-}PId+FGk$!ZuV(niucp zQEK$Fx2Zaoa8h~`-PmCP<9!5^}5-fEpS>JB<@q`vv{zU zE{$4!<>SklWvV&P%TO_@m75CSQm@V$Nsa`89Tqi$6h1*+LfMqI_m}zVW);)6B%J+e z&8(BIb0Oo?4n2l)Ue_D`(AtqnQ=EP$sV9xTDLO11Z0hlKwn{GYgf!x3PPV1tcC7*; zp=se(gGkpd)BTX#bIOLP?#E=i!;fGtUppv-9~(${7Q*e56Nu~@tWU&P81;=cHv-*~ z$~Neg&u!xSSk{}chg7Fri%}QX#Pg+N-`D}s#04`DE8n5S1J0q~nVgnz@fSH8iEtHB z^{$Py;VQ;KYDkux4f)zREzaHl_Q1x{57sM7k!!`Z{@I*b^F>#NTtzNOYsN{lLGiY? z)R$-ZgBM4NaP44|Ot_;mwn&~G4bpT0AF{U=CuLNxEA*+pXQ0~1on7nohD*whW{iC@ zOf$X>)tbjSm$_^0sGM_UtKeY|^Tu$`V>rTK;2OW3SPwzJmkwx%%G@b-roh4UQNEPj zQ_YRg6@0ps>w^zB$e*UXd@)b_ZCzlbjK%oTHHsmt6RCS8Q=KME0VVtWph{Xz!D24j z`A-|j?GZ=ut1h%;A&TNhkJsy1mF`HIhhOznh2@mj|EhS}NQ}_sr1Rgc9(RhPf}h~> z+&^B7PnIJx#D`8P62OsPpEBX&z2E~8=fz>afmCAcezS3`@mPtAvIC&Y_s&)ee)_FjICn$3rJ?<@>C%|@7TrEdI4L6->1`MADT8E$cQ^NfsC zk<6(doV4W1z4Mh)WxlzLL9x1CD_=LltbX*{&vEZb#URP8QHkl~DGS0TDMKDUt&y!0 z6RuBViTholmaf(8@=R_UVF^J>9HE|uSG4cZE@R)ftGKTxZQljqfD>lm-Esr0s8jbwdk3Y~jnPIbqJcBtXJsAR3P zqfuSw#`aUWl4;j4*legi=?m{E57>9t=-fktQ|m@);#a8@`?a#J1HER?+6+Ihhl2}j zi#A3wMNxJ-ZI7eWL132pDSYUVs(UqS&xASm9kre%&mUxjt6Q}!A@ac2_NK{il+?Bh z)|KzDIDdr_9dd3JZ6=f|Ec=l;wjQ`WG7&zUeT^zkyG;X?oZ#9uDiHs44jS8T_DD~a zL(-zzIc8k#8XSQ)M~wraXTf~`ir*8?gloU(c;$Azdf5>}@^RF6QH!GXSjdpjZN1!I zg{GrZp6qJQ3{!BL%5YPTgtv=0$*}YCjqkfPziyJ4+bHo&x0QH(isPVGyg-}J7rQA; z?Ac6nHv4X{{A4W>cF9M5+{jv?Pr)|RJFTN4$v=8rEQNSnaQk$zCTFk=vC#TZ4j*cn z*tmR{?}_Lmh>X>WPHuEo&)c4NRg8J%7(+zv_D)FEW~!OmnN*w-I%{;=R6qZUd%WTe1&* z=GD|P*cf2qo$s-{Wk4BG?6OO@sOjxdmPkhqdtscdT7`Hz$$D>f{HF0sTH2@1;o0qE zr6VCl>Fs0>3j$cy7Ub6pEEXnS%{Tri)lk8t_3mw0)0s9!YD6=!66QAOjpCT?jks)c?#G8v9r5I86(YAI^eoYaIPcmng< zwqhE+yGu!mFMNgiPHb{R98iR62$<_ktuQB4ScuE3Ah~XlwmJL2$odF*x7<*yln4m^ zF*p#y4g}}=Umm9E`?<}zETj_*ZZA}twYgAdt9;sb5=qe5{8oLv;U|ew=RnvCeS7Zh zSK78&ov)1yS3z2wp9j!j`*x0bb)5I4Z{}Ie!RUutjN|OEi`(NVisU~{hXpr$)eq2n z-f>yjcM2nvhdCz7#ms{v1MO#p44X-(<3XQ&&?_{7_ukq2ic2W4i%bubwZY7-9aC}Q zpY~JWTaPtu5Tx2Y@FgA%WS3W5=`j|P9e8h7B|>XuGmL9RJ54N*Yy?@mM)~;alupT{ zZ#OMv0?W~jA=gzUL*HM`6XD@!2h6%N$>yVqQeXUIVX?VkA0^u410{zW86~F6>Vx;3ebm8jE?Qfvi81QQYUzJC8?eVC0+o7@hLb6}DY%xRo;>H)EM}w8mfruDLx$ zu>9Dj+AV`2AU#Pbo~K#tSbY!|5yKT%iri2YsDFYj+G-wDvOwlBI9U7R&_XjMpp?u- zLSWh1ui9NA4M(Ijmwm-MUDI^o6BdbyLwl@)x-93*ggvTna|E!1{V=^mUtMCWF{Yo%y1~rba!_w~|Sv z(itK2$6|ZxMX|a+ZReQuMm|-l(Re*&?){N6d9k>hry1zlYM=r}{?N9L7Rrr(gJZS# zVMndkj3PHzwONtZtQQyYl2r7oWaw*r+Rng?xJ9kSvWbG z$T;I?3yZRl`QFXSMc}v6?rV?gpKei_<{o*vkr@f}V7)NENZ-F!2}TS~>9?UjA2g`k zYKP$-L`1oAy^c>cRMPcftlRM138#Ct$Z7h&&z(Ul)T0vhtr4dCoyBTWdF zT>4HtNTfU@>GcK+@8}+Y&O1H>qsh;0P5b<(Tw~qnBxkH47^urNl%+f5=`mMd#>!mihUMJGv<`dTNfd_??euIhcG!D#hj5(scO&sloZdEb{3U z31!9NO&mnssf&-;(u*u@&V)hUM3-aJyD=BqBnm0|YT1R0ZXLCYjT>{28uIc2W8WMC zsJBF3O3PZp1o}KWaI0|x2chY)DHkX1)n%?^*)G5d=7q9+Sd-PAeVDfYqzI$D7dflse3hJPYJVB@#?jN?NU zczp9_7EGj^t)tlG8=GqfDqMK)nJ?EuHm6ESxy*R;4eFRvE7DEhkSYxOD<mY;3@~D;@UEhoa%hRFk{0 zvd4O2QI7wUZ1OmsDdT}(i(Cp!xpsudR!SOQn_nV#ES1RILT#!ez234Ih!=rSJvR3& z@d!NdwI4F|h|e`VKngVMPYF)ANCCeIB>Ih_ZLA>dv3(Z7pe4u6KBwuL5`(T#8GO33 zw~JlZz9$c#B0*YSi_0CcZ_b~0)RCIXX>YkIR3-WJaDOWtR@7?L^+f~dr|!^5Ej-Gr zV;PZtYFmjw3R^s4iSU)`c`^pMr@JaT)PcWvR%Fx=;qEYmBPptT%tBtRS0LvEZ5H;< zT{Pm4b1pQcD;a4&+M#X8m_#00KI%R~(OQr@rn7fb(3|K+wz= z=eI`Ktv7}@q~s@#@p?DvcNM)-h8OcS>NMk=;0Qs5KwP!S&7=lWRj}d?luLEQgH(q3 zFzvPo=K z*ZWcv!O{rpCDAw+o@{OJRH2Km!mo3?f*thQ{EhB$M{{~cs`=9_2^HU#6FI)-Py1q_ z-)AdP^wXno&5`@wBi*}A@ur^L{kAY|wZw}qnPE;_THk{9&Q-f(?3Ro8S%)~r%!zj= zKp3}ykCOzc+%`j|ril9pqdrCs@ZgW)Lz`Z*3AJ0X=nLg2k@hjEEVq`+D&lA;eh>E2 zh{^?VXPaJg@DPJ1jG@=t>f*-G?|%W4$_rXxM74#G<5=;O&HMyi8VNEcnxV5o@@AI4OB-v58)-lO@&emRfCrLA(B#bKf89-I6T0kk(nfvn50dB3@oSDymwT=z_M-D} z{qugAiZ}SD`*lSdJcl7)GiW?L-7X%Y>4?s65Nsz%h2I>%3P_spAXnh=5nql+*8@E7 z9=$yH{uTcd>zon6Cu~_)2t5RlL9YIUWKjRnD~Z{FL2|{ExsT|NY=uV*vQ zawX1nK(Bz@Q8)kw*53js=q(nGmER5KTI|2oR4fqu5ukpm$&sG@k$~Y?3|Md~`YVC9 z|5Db{0DYD)CQ?S~pQru*uXz7=)$UCL*$L;v z`+v)wI~Wbzm`vXLPZz)f*#g_-+U7Tp|8#->Jn*mj`QKTGfOswH*)2|WF()%#YVvL( zOrJ^mkJw$P{`uNJiZnLyPY6cG7PKOL*w@#WFUQ7MdHB6SBbn|FW{&*()gO^gdt1u}$CaC=ZBZ-= zq8KrI=kEstw|lA3v*9AtD0olWrHqw8cNxL>q~cicC$jXcTUXn{dtd66YgiO-d_-(o za9++Ues3VX*P*2q=Q|5e-a}87u;5sX56sc`J=TMj<-*I%Iuxl}zbVOM5%Sc6K=i~i zIIs27+J&XF%sMBg(dCu5yhVpim!74Ix-0 zSbx2$x&fI3Y7H6)+t!}q{651&uXg7BwpS7}@OMIh*&K9FERwzZ^l-yL*F^k#AT)&( z6UgXxWCVVhs2-zoZ>`C?zI45pr$k0rq9RCyBxdVT_vD!6(EBj%XaPEb)Z$jL;N^JF zfPn=rqdLCwpvJoE2l2r&F^m9jc17X#uDqS=)zNByZM%RLFO3J!sOco4v? zSfV5k+wh_JEGf7~1z!oCJku$+VmSF#V`g^6i8o#wKK?hDatS_RvFM76Hj^NbcS?KY zF5T&Q#wY?u^uromqCLvrivu!;hYa1hv5A#VLmnXVc%&Y@?^6}Z>aGl05AR$Dc6pjJ zr5s(J&e-rEWeud!7a`0rdm;G8KHv(oRK-CI78+dLr~IkV5g?a9*xziZkAnmpDS2bm z`>TS!N*};kG-nF~mA4${66|LhoZYE>d_UXUob5o<6dRQ|HJD~#7Q@l0xhL_2N*m!> zeQ0LQo##XG^c|o$gMKQK(>{|^MNtaxvCl*MM;d932VE4Rt|BSd%OWl|O0PQ2&x5JX zt#56LxCC+=2jz-&3RXPjM*f)h?UewBRJE7AJv7_QIBVt61Qfwq@AaEYi_R@v>lC@X zL_;}+_)08LsiDI-CSHM!6&j~`DCg9yvej6*dFE?e$Nsaf4vlBic<+=of^K2-1_=E= zDIV4D8C1vzBr7YMZ2`bQ&9@y3KQN$iLA6+1!~*s62RN(tgHkZ$?D-PI2?i%m#1h{X zGZ*p(P}wf0>-1gjk>VTx`adf<`xHu1qB{A$(%wtw9z)Jy+owfaQ>9|*%>E{ik(_sl zqT6{hZmS>^+O4QhWgeBqg=Eq9^}er``00y|(rx4*+mi|4SMZA7x`R^&$W|3Au<7xoMklfvb!N+^x!h?;>Sq32kL=44C#>U1`Y~3g zD;E3Hp%TWQ4RRisBRJ^%f%CA<`(z8H4Sh4EmY;T*uM}M76gZI{N4?nI1v+bxfpfcy zhU?3j_ZWi9gn%G4n_wwNN}HB-p!WK38!eT$3D3CkciTB>4Y}+A5GyU~u4x<9mKpJvi$zP(*cgr46 z_1DiuLIr$P7GRXVgx2Jl`Km&E1Np%**P(hk6R(Rx*>#IDJW^%SZf?Qej9YhrQZ`}9 zDN3VSB?e@&H@NSTDA09Ht(QW`4~>o;$4ZKdbUDLC$VU-(o^#^CnTwF^8EFd1vGdci$Vx;?L|`(Vr|; z>RCd3%F^cywEGox0gFw**{9^uN>o21OueXvz42U`cgSL;Evw@@Lg%F>j>Bs<=7i zE{z)}!E|{itwD)ns5p}jri9_($8I~J94J_48RgBc&8FD1@gK4hJ!=6Rt{Q=U38CJ z?y;{u=dR&1#2<{4rqD3lAY`fohbGkzMiJdo8#tAwEmArZ;@pI4A#jlE^$Z-(5^AN( z`2>E=iJE=%NR5HieNBAFs5_YtPU-Ka+W6x8Luewz>$$Sw*Lexou$Mj@YT46+1yb}a zSN@2bH{vaNTszHmtH{zY_s47YxNd}3QlSyvoG8`aZedEmB4?h4$`1pAF2Op|Jezks zIUSVI*P$YyU-8-RLY99L#gTs(yFD^Yga_pynfyJTF113T-`@lYs`QTTAM`gOP!Mii zzt(zwRIO*tKOK91yg&%7E{`Z3^T(J`2Ng{gi{!|&uh(Of0Ua!fN(YaB8zl)?OGsBK9>+3>87P~!8`HEVkiwBx3-Rn? zEHl#-dETr=*;8ol+sY2EZXg^hML9yt%azd=)8kvN{@vp%QLL}sCu&dqs&FXBHjXN}l7{2Yo0^M^GQ`U$wcI{z_+mJdR0uDVh zi)UjpG~Ye(6DKi?yed2v3v($iaXFX#7q&umXH+F3*fIY zB)h+eTg}-0K@x2+o1H=Qlagw$U?GZK1sT$V{Z(gP^Te?|HBQ@ow~r8J^a6D5w|p5` zH#_85x80KP=1dfPeIWiUft5o|_STAdhm8WP+{5*|#$dIcUR_3)xksu;#eLQ9TLeN2 z;x&)kZZ+LoT7!XTPhJpiimiTP94XLy+vrvmsP|Sfr8wsZB~4;zKjJH{t2(9;EAeX^ zb#R#Cm_w}9)2zC=XilXWPNEbVUHkN*){7lNOknjf9OEeI#MJI90uU8^2;F_hw$y2+ zNWY?oo2T^NTfmg0-Z?}Kt$k{11*?fzBN#Rhg~(zSUMvudiTD)=+@O#QP|Sv48Ta2; zf4|!ToU9hkqz=$YtVaTWAXpYZ{0Wa5H8?PBb#>LLWuA@u5;i6+#FnYE{khH0PXcqF zdCy1rg%;JcJhl}k`pvraPxvwd zTCQp)yy_1*!@?uRVTXbKjcS@OcjPwg{fE~5?_^>B?+29s6KEYa@#AZtzf(@4_5xnyMr`N}q(g6A5`H+~ z9|!if`o2f@tTI+l8%KQl3MO9WUpzbG4XW{YlVWQGcX$+W)cpAclnL;OJc;LNp&pu< zDFzX)sq<$hy!N=<0B+;sR6=6=;Ez#gSAhY+oxZkpHme@@%ro5Y4t?_soBTG$y*m|T z7N~BhT7-XLB|ZRY1&qW#PL{N}ewual7kYbWy3)-WySY^)$0;yg z+>UCds{n;-J;&rbqkyrEX*_ec0D>Q%V1{zML4c49&j6g+yHy+6khjH%pAkl&EkRz|WM8R2cCU_5~uS_hMiiKtH0u};%xk}KDjvdLCSR?Wg&j`By{57(TLSxk&1o! zp3;bIf1;FaZbM%@8|yOFl-V=VC=P1!n;pv1w0N#{Dz+^FCI{Q@=+*vgHzwn5!?FCw z>4t{#T2}4r>nRu8teGj@&Pu%tTx6=Oo`==R6^Ro}bH^%LY#E9g9IJMT;R7L+@}UA< z1?1_JVo<^2rG+|Y`L!P9)=>1#$@pA-tp~+RCcEAI0ye;}IiKfOk>^Xh@s4{OFnTvy zNYRLS#+L0R3|XEfsP&x!M>R}@8)c~1PxGBT;cRXeYbDFUe@g$bWw zd}7s}d(ojl9>GhwQ~!by%ifSCOLXJ@Ejr)mtx4AQ6efkRJInU*z`%KiHNI`ZmbFHL z2bkMrqF)zw-fMRv$D}eT6~1C$(a6DTFLdNBqAZrlGmv`fW4PdnU*oWn7G%0dJ+jxA z4Z{c0WW*)J&Uk9uZ?Fi=`BIkbG!a?%wpPonsd+>WWM$p7cE<=4c-$E7_|G0FFYi|9 z101WMSMD)x7i+pmut!a#ps|Gta-a%ywn7BkZ+WfcpR&o(Z!l$jyV}=QOyPUum}2#4 zcve9#ID@m{;%Pr2-w8CsX!;b_A)(k;u`J?fA2n>oH^miEuxD)ES8tJQA}RpTMEw&L zPyP#jPI|#jVVlYLrzFDY&>v;3f90$F053?L>F#a>q@+u_n@w*z43KU%UDA!jrZ?QRL0|v(-1FgnyJOsO7z{p4 z*o(R5nrqJaJimCpY?j#ZqwV)kzky3V+Qsu?_e<%oKk_Oj?pZ}qFCBB|T5(#>X4k6+ z{Te#zaf*ntWXtq$N@j5>?`5kR-cIoYQKwFf0;YZ*{9je;fPi-+GObMHOzdl)A48Gl zPu{g7xWimQf%30Lp5yUQXRIiuSaZ))lkRF+LIU! z+&Xjh7=Gid)afC&F&1u*wPG*l;WbT&bJLm9MY_wAhB(I5NY<_1!JCK#jIbJz#}c4v zSxs$uo2s6q7O`g->Ky<6qDSO-lPMxxVJm5_PE!g7gxG1`Pdu-MSqn%*v?AJvl<>ws zzd7D)U&`>Y%*(}OWn}~+t`9MSL#WDfVrl|*l81tTFlgY&u~E<|UJ6ZlYInwEN0K#3 z{U{W_UrIlmr$c}7RTv|Fe%r{0twgDsUm;EoXA@c~Y$0EI`Uzgphy*07lp4kf%2;L| z#p9|qeu;Ygf@{iNo4PLgY^7V>0G2mvXd?Bq=af4cbJ5AqoI>_2%8_g8;S(s5-9XDcFyV~5M(F#aujm`xTAx&~dyTzoQ$yKxIn*V@!~UvMg0cHz znNV&D8$~9{?0DGiXw6FDWPk=01`PWFye%%PXvXly@84%y7uz^ z3~?iMfc_vxok|N+^Ife%DJX+ab>T6>FyYcht3t2Lwtab5DBA`}(CU;#W|cMS`sO|k zpM}yJf-~F!?TCUsdfhm|SQGqS!b}&E!Z%RY=M-Hvu93`i(v?|Zq9J-Ni@bcNo7@d- z-%79`*FBy?LQ5e$1{1v$5G%P7L2Y%4HD}+mCz!3z1JU>>+A>pM zDJ(OL=F6)0-a>)tFUL+?3^faR(9-_seVMcbi!Q&1`UkyDtBa(&kny+ z5=be`20HIEW=YY{Qr2tv(@N1_cr}mH7xOW(K3OPW#5gxXfptsqQFKLq6$q-4s>=N` z%H8+P4mkN>JHk^pBAB;CYONAMuic;1AD>jKLOIJhy)3V2I}NV21h#jbWf7EKW=H7d zi*<6*mvT)l)vdM2zP+#+C&|Jx*|A+&9bA|<-)3tmeVLx#7|kJ;#%v9w(tAh`XqedQ zbQWm%`F%9pyG-@0YZXC6Qwr>JS0rD}yk*3dasWY~wID0GEMcMkPfe@XQ$TXjmxrw1 zF#82zi0DC;ldZ%DC;0?i%jXjiY5UVje8;7B{N+~J@NdUPZTZHjjYFd=MqMZO?s5CW zEwFigd}NlD8A3*p%^7qHE}U+1-F~Zb^Fm+hEcS@VA#LS44XZ#jAtf$Y%qt|U zPri#K7H7elHLxCv5>`x0=~T!IXVL*%1CW0>_;}0E2dQLQ8!p6~o6DD5ZyoxxZTd8Q z64U3?X-5FKZ_i(Mp7|GQ4e6dc(=n`v^W%;^&d~u%VR-_4YPBVW=^2v|O$RFz!_g78 zGM71uxzBgfi}zngxi**DRfv@mh;zH!xSjyn(+fqkv`RM73~m>rG>?OAh8puDw46D2 zQb#|{CinLjTQ@NP7R_I4EKoq}`MuQ;%3hI|t2_(*al08)4MYWrRECOXZfzszpecJjoS~JE$5J$l z9b-5c$L=K|8e{oT8!k00)*Pn&feJxogxxdV$Cn~t?BSS?w5}Y_NvcxElLh2+u|W|~ zPvx7s?ASL>`$cre?NlL36S7IT){O^|fps2ZY+o2#rL#FtygJ zIPYa}Ep9V|ypflAP#vM$G#h!^&YE|NmecZdB7zF-^DjJu6XuLXpWglwQ@uVovNc4m z)>+!y?v7L~F`cn7B7R&azw3&a@K8xFA&=<&*7)X3yr4|42P3{`vn?si}r(6F|3cW2n(p*)&foagG=3u-XC z0oyb(>^t@2orpwB4tc9kzX^Jdwc242ZoXwk9}3a>ucXOQ2bb8ZPPF;cMaVaJlQKMREW)@G`#hU$uhq9Be;z zR(b?>DNX{XQd_WHiCBa)i)*{5$18V~Zz5I1`oKjj3@zTDzK6$4u6Nh%#X^58DXgji z4Fm87s=T?loX_?~b3fYKa@_am{j?ExLYKFs;9`(BO}-HLa6LcJ20TidG(iLU-UpcS z0h=+vN;7|I{_n$s_H$Kvc%tnK4-R53@S(lrZ`HH?B2L@1qE>y=hdrNXjRBy>?0FXb z`&NF_8FCGpZM$&_pw|06)0j;jdZ@~CilB0z8jDD%#@mO=5CVhHYlie}Idy?e>rma* zO7DfspS0Yx1R9uo94o)OtmT*}ryppwQ1Q55%V8|o`a3Hso^~Hu;nhVQBT2l9M&v{} z)J6I_9CJq{><56|B(406h~Ln2;Yx7)jCKB!(rM$|7D$BtZ#o~~5fujqi92_dJ>OC* z$9=niWxA&jb}TR%^VK?M3@tr5ZkZ~6P^<)5Mc}EptfnUC^i<=k-R(lW}Z%@l2Dj`CwveFu}If^_FsVEt9XDXQna|?IWG4M{1lkZxPQ;$@Cksb0S zd#3a5KjXty-`yzX#RTS}xF=|>6USDozY4%)9MpK)Gb!BD=JKKMNo(DsoLhgLKX71; z=kLf!IMpuCS&a%@qj`6jBkjiUnZ13AFNIzzid5C-fVmpS^9tl-ELa!Zh&SsYtD7~y zV=P^%G`=iGRi0DG7cqtIM&-SNHymf91<{q;mfr+4oMhOZ@U7b5_f`#zg)-mq1a&fa zsbcEk0kXT?5sPvPC*mq*w`|bBkC7jUgrANeM@MLH9c~~$q%Xe&KpZ#3m(avslC5f= z&C+wYR7XO*f?N?*Kdef#XdrJbdwQ;)=!EdJ8TB0Ur*8qYe~ulV&ZcjJDcjc+ZJbEO zaV0!?9nX`U0yRldf*hmDDu+Uks#-Wn-w+D9f1 zK74T^F2^k6r=3}NDH^F;K^WrAjPk4e4Eqf$iJ46Jw!AN@w1{$=z%hpqv%jGJ!*t%my7+##URDZOz`fq`vxbH07uROr`JZ`Fn~2( z9&k+oKLSPrzNRa^l<$}(FH4AvAJuU<7Kn+Fl{E{dEjXDc zTb;{QDl;^13n%@ps?V?8>8x}oQ`L_@_;=I`w$S}(p+p16KJQH~k|))6E4_UVwUx{@ z*kQns+u;9lPpjgh4)ys|2<~%Fg-)koDOqT4w|r92(`70xg^jyzx2R<1ukxuGJ>xe( zsaBu(@iQLfd%uhW?c9-`RX)3xZ1D^QmAr-=3M<~>8-D(8&gT{4-w~Qj=a|JF-%y{Q z%b47hTvQ=T7(#P@N)ShUI{-5G*ro8PMh<9=0A0A&6x3tz7o~A`*>h+L+SxbTB=Q91 z-Nk8<|>trU!`88OfYFhA$0iFY^7*NCQ2ZFJJf;8pc>V1Zux^rY zOC3PF!PW@jZdv}tZU#v5u(9dJEYr7?g(5Uy_Oc%=$OsE~i)s!TR2#iVIg@2Cp|717 z5WJ|%?^~)s?BepMoUZDxsl(5|RvHgMXv7;yl{Zg+REAJV6g9rdgr-z|UByECsncpn z4NHLyhe8#FZ+U0$g92ybbG#+i2e69Ud!Gn&c*{bf>kp;r8(vt-q@7tBET8O>)~2iv zKf|iQvSWo*Te%$tt25BPNwhEB=2@`Hu~4&ly^42gGB~dj9#N6!!C})A^A5j|R>XY4 zAzsgf7N&V4v`!K9?g^FJ0Tzi{fsyu(^NdfUs{kxg9DnfU#^Bx!~)tWj-x)a?X@DACVpO}KJ&r|CH`6aCG{8D+|) zf{eu-`$C4{$5tt*U;4%LA0=w-HtfV`%Sj4r&O69$N0agb(N69jR7%nMM0b()xv>I6 z^Hw6AVBq1$ddY|u{y+=5s%>AVA~JORFxiy?v-lLI*hzrn1<0$_^9 zK79grnlU;wMIDpo_ci6e1*!xEal4KYzzVUbp%D=$J&OTBL8D87A?v;#dAN_0vw+XB zJ{F+;Lon5Xb4v%(StEahX9y}NG>5$=7TFxhGiz@W*#vx-Mk=qV*Vzpl9eyeALp%)s{QZqT_}Lj9 zKv4D}ep-Y6m$v#L4xlDRVe&TK!hUx*cP$C6B%$GBg#dLw7$bR;c%F5 z8;~r8PDBee{Rg8}9N-{4*!+?+^Lw0tbIxKpy!%duLyUisHB?35a4y9CYlGu2w)a2X zBNIoPd^k2)|GKf~2Jj~QJ2bw&%I9o&e&RB$!jEq;{JWvsd<#fU z?o*1IH?}BI|A+tEh$z`2(pL9!kd@y?!Z?r1o_qqmMog~1xXcG6tiP+lmvy8MikT0M zaB|A8j=&UOUi?=RHyXc8{6c***w4-+>>_@zxep~({}uHz9wgGfH>CCZw!|s@G6@R5 zJ$lI=Z-ZGyDYf_;;v(1iTA^LzDt>8rM0Wq*_eNm|UD*xo4?CRpIzBg9FszP1-g8z) z%v<-c+03&}IWh248QP~SFDm#(B^kJyrO;yDB>Opnxk*+#U!mHWh!S%{Juq2?HZCHM zCp5X?;0dg9^R>}nLO8#BE=k~8Px_uy<{g7t4Or9A$PK6zJCbhFb!K2rJ41{DRKG?z zwM`WTYNc|eOn44?Hj?=CPM)1joR#r!wy#TJ4*3}Ga%^plH9rb> zE0k@VxE!LgFkSfiBxhC*g)J|bHgL2B^D!!%*>Eih#|!6tC;xlA>!ICyqgdV3@CO5w zP@4%ggn!X4!e&Z&o|d*x18FVhWB2`%b#2gl?gQ`{{q1f>1K-VF5FxQdeF@hBXv^CU z;RM0kul3NzItxk*cAEyCLfxe@Ydt+)bR0b3K?6vB58xy}BpkTEQV}06J_0-JhCMa1 zMV84MJVjmqREt1wiQV(ivM-~_Z16zJ5%$B%JM@=} zxOMeEXx;JQ4}(_oUtk3^=^(x~_|ANB+U(`Hsn{INL{+{@r$|jWp%9;ZIQ@0~rm4HI zz+n#e*tNGkwtF6CMYX$jBJSF%gg-+wzQ93R@YEub$ShfnPMFEL&{Dg%(`7qiB%}GG zyfDgMuM&v%$;4r$>-y8=+GAh48|9hMu@}uxtoYoc`SoftA}S&Dbmbap1|H>&{i(8Y&$qO$M1cSzH9AXaKa*Hlv!8f-+tYj|;Z%4eAe&<+T)8wR zz&lPh1E?;cYV>GFA+2$O$!KjJU`luUXkqE)I%&xz4_c_oXrm{m&G4Ofzm4M&;~>8V z=w)oCKNf1d7TiK3+az49@r(we;}2KSvrYgHCx__xLfcl+dE+cJLIAP)ylFQ5)GO)Q z@Z-dpu5j5bSD}8=6t4(!l z2ACnwH2)!F@S?ISxMl?)JQ*I@XRGo58?@z!I%Z67x@0q(%bg?H}xK3l8Pc2wAl(F)aMiHy9>lC?wuNyN zd%M@>L2b6Ju3SlwD@h{NWOlvB0y3;})Cu*5`ZHGrWjsS_r{_GJ>i#|5KsbWZtbkfJ^1a8t{4HvY% z=6Ks6Nj?a1U*`BuFp7VzK=I;P(-W<0T`?v4-s%H~)1kVf(_Ts&HOk}*MF%1Q53F57 z%x+}Zf|$J-4%Y1>i3T2tvK)%oe}$}I_Y>wuygJL+Vnf)=)rXrY#_N?#%0^@ebZDi0 z@{l1+iKajCw%2}@*A<(JAgvf%B*wiE?x$5{k<5I>F?22&QWAoGGMwmek{taTxU%y( zG%PhU>K#b<7Ek{{821Xag#hX6BaC6B(#kMks>UCGgDn&f7#fZ$mc-oBfdLz_pmaR7 zQK4GH;;2MW{`;%%Lo^F=KYwM`XC*4`lR~XdlHTn#ksP50OpBzB?6}+XX??^T#}V znm@`?eQJALb>h#Me=qU!&asPFicK4kcwcu_maWGj;@3V8$@1iK_)K6yGoxdAx`*4? zzNGr0iM>$P;ENfM=NkIFwcoQ3-b#cVT0ia~#XuQg?f`-NX)*5PC-QKn^IHNq(;2-^ z)$d~ZI~ar67_?=hYjZJqrV`OfUttWmLrJ`o6dgP0J>Ga76v$4cbv52R(Jg+{KTUr8 z^AmAI^VRu`mQ`AQqDuRy66eTTA?pt-0Q{)jkaf#y*i9BbI5_Ve5qW!Qf8;)@Le5j1 zpRz#yy1yLF%v_m9bOFOwebdp7!a_xDX2r{V>w$xrpE@IF#l4!3ehW4^l+UumJFDW} zE!USB$23g|yMCCElDsnoD=&IlzP zk;~$Kh6v6%o+0qD+JPuIOEy^45zk+T$XzkLpz(4vx?9di$#@hSPC)X znPTdXg}~?tD$#ItlHfmZg6}8pyG#+FQy{-AQzj5n*c|>u)S*xpE#wCv&_^_jBt- zTP^~7LcxMob%qjt>wCSb3nFzuH??Bb#{)AmNJBb*aX2hME=L-=*7(?6ZLC_9Cjw%h2BfJa=5%{|?~!`R)95rvXQ3(P*+`iOzS;4%4mC@SUoQ z7Qx+r>}iUwLjqs_Hz*IS4%!&kjNGu6-lSX(9ck{!t0FC=Qug+$pa2H!e){)ojBV&bQ@4OB}?{HT-*fQ*w3wkj( zWq0iX2Uc$aTHb;y2!v#VuB?$U{d4yX2gnvy_}l63ErQ-dpY3!<@t+iGTeRZg@<}6fBo->}=n6hL8r;yR|%W(w@E%w8(ath5rU6CY3068;s&8g4}lE|-A5N&4Gg4WDK?-m0yZiF9MKc{ z_klZ&kv5sGE&0Die|YpN2HAgiB6^y=azEFpCXBdBs<7{y^O_XHPK+CG$3os44I&6# zQ3mArm;aV*rt><+yWwUWY2kGOBU8t6?pj!d$A*<3trK9un!YLYZUnu*kjvx6FE+yf zK^3c5Fvwti2T*7cGf(7)UV@+o&#g<9xz!eEaDd=tk1dW*lEO7?sMnDy^o6{WB3z_{ z$^q{xPS70=3pa^NC2#6X{Ka^DwexE(mTYPH5*g)FZk3cb`{khWwGi|Q@M0-ZrWa?` zm8P|CVUWK(qEXwfOJhkcwecOfT-=%_L{OmZv43RT7Kx{gZim?vu1LkG zn*$J^K3~&=l&;dm7+AeZLp?H`a?(H+Bdz=yrOb2m8ZZra_n0^y^x{?#J?{CNd7`n% zNCqgynQ-RTJg#;c+iEeW(bfSRrgzvlLjg{JRbUQXRW#yyluf@Y%_07_ZA$#?N=6`0 z?5uuVh_!Z9f*<#!@&vyhuT+_ zf1;&-oALW7su(!joOpu?yaFN6sN&2rKu!X+?+Qw|~KY93GxlzFTB%}Vk7wi{A> zzP~8odO{1D_xgB3+A10yE|uJS&+~;m2n8(|wM4HP6>=<~@^5=oovbyy?^HL)n-WLyF5zi@&0CKDFT}{k&z+xnNg+RI@-~7&+ z`>QV9-kK9cuC6RIW(n|zyN84j0T&R6!yRIpZBualXrI}&Lt?nWojMMs+sdnK`CTrA zDJglgtMRW(1i_l`cMrn~8^}ZB{p*m5pC_Gn1(o!H)17za2OV)%{sW;vQx%60G}(Bq zySuB)=V6xkPHYojM7lrj5O_PLY0Nj^4$`8Tx#P#CNW5yB{FJOJt*vK)|Jhi$wX3tU znUR!Wb0t%+bcJq8viS1S>$;@B#<*94wFDAb3hhiviegG0XONA}FfA1L#tzRTAU%e= zGuaB?`kbA-mIE!YdWL9J(TTx~5n6ANY#sU{M$ssBRnlGgko4-;e2HdPoo?UbxG`1Y zX}(ptwJbR%MUGy1)P0vU$1jW-pDgSc=d@_4Dud3CubK%OQf?g&=;x@H2tts90VmqF zVNDe|`@SvUbWC)*mw%@6v9~!jbKy8Vf7BbQeM8{KU!%t00LuP@L;SSKd9ux%Uu-#t zRebERJlP(bt4xG549mB)Jr@ynO=JRm^^ARL^jqSF9G1+vT_@lFlm#KH^h7gzhr&{e z$?aB?)cHgvlerJ3e*D;-5wI+9ur#YhUotK>nK_g-nUPyOIH!H@x$=2dg*+yG5w3+i z)|1V_ycbm!CXMSHJV9BWs$O%tVpQBW`W@s}9m*}In|A=IlP&m%d-9{Z4O6X$9DS zTBqcWK$z5gAFg89rEsuEvxJd}UFd#ll9ibZO^Lk>y3M5d$pX1Db9b!C?qy4`2&y^) zaBTA3<>iK(%D}(9fowZ_lAoWT%mevXksk&7R}h2O?y~Gi6n{lAq8T4Hxg>x7{8{7r zeu-;wQ-h=D5DOT^Yi3N-Kjk?#;C4N?MI4?9_+@9i654Tp{40!U%rz*Y?pXi?I2SX) znS6Kuo$QN;jsc@?v%{_?Z(19+2-RR})(Pn47i6RlAARc&H7XA3hZh<>6L_^xk@|;n za(?$&8i;fvQBY+5mJ7K(&k%jeiQxYagn$%oowEE@X!LJRkn9N@Ay_WV8T{P=U=S8N z;Ru1N?GW4F>?P`3;O4%*-r8XOH=T!i#tgiS0wmAbF#aYmKSTon#y=6<7mt3ACwxf% z|Lfj$TMJKQd2#d6>uXlDfQkUhs{KcotxAa0VO8KU&D;%)b(Y}2m_%qmFh6AsIeBY*_g`)+H=}~@-De+L(#HB* zcc>6(p~iZ4SD_mIN5Tw(f*9b}6C!^~J!7NN0xdn?-~Oppbk9#7s7)rqFaZ5~=?@RF z<^2yUaQmP}qt(Z3h|pYHtU=)5PDC!}OZkg$=Cx0vf}-3zJ>ntxZd@=*ig55r|IZ( zAvI94^VK7%uMFhynJ8K;*#1CSY4jq|?Fg+X`XbRMCGischR0}rq@`6cfm%aaHL~fH zgNeE0cE;7GA-&|zOh%CqZ%MmOr3nM-bAl<(ZVT(8iuna&6|Q9=KP+;X_X_h?{C-fL z?2(X-Ex&H_nmWUhn}T_Gn8i(;E~FsozE9HaYjXaWEK}G2@L^Ft>x~Xi?MiIS95yvK z!kZVF*OnJV5;8oyB1s3LViKhp@{Ydsc<|x#d&z)Zc^|S}Qug(H7-Pv0T>G4N;LqtAIUPS-JmLB5(4 zH`4Vqz*RH59*Rl}GHgT@B8_Ps)p@8~W~W8fNL{C=?x8cQm1)OsY*5qS5RzLFQFQ$x zxp^s565s7~Fef(H9fq#BvIx}r^*G56PNynUW-f}6SG)#!@NLdy+m{*ig^JBfCiJSc zCT)0b z3-vPDre7S~xrKuv>yOIUUwf978gZVSmBX0E&y7xGcTE#jY6fqCmXGX2Wh!Eix{O3-btc8}8gM%%_BmQVVu;JJamtc!#$KVj`UARsi*rWTVtfF^iR{g+~UdoF*m8=-@>{ z#&52xMAS zDb|IbwV-x*dNNlC`P^2N%3}Cb>+^lce)|ML>V5F`q*08SxeIYD+GlRhByC8tJ}VdE zmNLftKGkmRDF+D}{*q^}ScKuUAMaa{wc^D6_$6kAlU6_DD}-VLDhF$abaaixV_kc5 z&i*z1jvS-@vrCE|=5NVITuICN1_sA%rrjg=_SE(i(+4h)om6peY%=T>X^5R=tYxBQh78)1Mu7%Tz zmvFDdB} ze{gbWG855LM>W{%djfG_7gGmc23&QPLQh(m9$8Srv@R$1xcklg&6Vy-|3+Z}w~Zd+ zyM@qz9JZTru3Vq=k-4e!1Ket17&0=J*niHKynRN{1h#wD3J&nq=VoUtg+Bv;)&LcaM>RqFs zVWX8=%LyAXXfH@~%s`$vG1G4QaCMG$GyT3Cj|b7bB09IRZwEOaw}7<_QZr`OSXl;4k-L?2ItJ7_c-K>BO|A@k&<1bj=GgDT^g8 z;wlgK4{SAAw~98}#=IC%61HGd+upXdu>Y8sf>GH2jIs-*ZOb%m`kEb_lg3V=HQE~q zhFvp-E z*7{ur^y`O&*KEPV`W?%}nKlb6x1WCb-k03{XocQv^tW5w3b2|x-N(6|TL*JYC*3LN zwU=pANRV`^lI~G2qO;a07werOQsuP96CuSEb*Zz>2Q@&s{Ytk%@|t`0i6?8jJewcU4` zo8LEV|KxOXKnG3lt$*t&Ic=|;V9iH+8JRJfY!gkS$vT^9{xs>P!<(z11@Ci+uQ?ve zZB=M_R0Jet!k70U$o63b>t)kaqcY+x*J-i?@yGrt9MdhMrD8MFq=>M0V1<#O1so%uTy18GdfVd&*W5s8M%DSiWfZydRom3ngRy0k z>b%p*)sxEos|&`a%L2sd>th^H3%?g_0h(`1Z0Do<5X3;0+t(ABfGo3d1GSJbN~_-V zg3U{%No~5+%Ry7GPAjn_3Y$vGXQ97ta`oKEe*PG}SZTr;Vp#0Gv&`;Q4ED@fUuk~G zIm5ZJZN6P{ERQti){U|PZd(~!Y%Je*YBNr&w#;{~d7G!`=L`k4sa?7eC#dsm@EaR= zBbxVQRkJuQ(O!D?f;B|BKppH}na*C;Nj_@th#I95L-0pUtZKcx_jize|C@os&F&BY z!BmX_x1^?|o~LeI+RlF7@07LlsCZ_$$IG>3uf^IHPm_)57swrspBsmcmwg^@o1dBQ ze=2VR^Pt*PE?wq|pZ z0gPuPM_zBefY)+UYPCkd(}2Z1i|zg&D2@Pn6}F;tFYG^cS35f_Na%1Yal-YW$sW{6(LhCn9=%F+)n@5ddT2_Bte6k^G0 zF?gJ=KB8W{s<3Ro)a!2S|JrXuktzFZSg4V$o^@!q>aH;SeF0!eB1M5^Hog{aojm6v zZsTJuAb=3{({SzbJmrU71o7(b$|vSMk2P7CGF{l01OKq;i%gm99Vi2BjWg2uz?`yM z`ujdl558AYo3YXXS!nTaS=a&nt1f3hXG%@}B z_%zfRV&u*^StwHRiUUNkjH0_QH~v(<-|Le{jZ9fOZi0;f<6v|3S8EhY7?xkdh7JEL zeTEJ1N|Se7L{EGvcv#f|t$O5GiRax?qVp&^Q1IB9XdIL!+mfENzjAd~YTY*oSib^= zF^bW1qX=cFuCQd(DODUTPX@yGH(RlD*Zue?TIw7qAcx@t~49$PfZ_ZuDW zO7{Xd9L&Uvr`9?u`fR+Q356mK(@P}_mK+k$c=>ReU-8isj8@g7`(9EyVmt>oA$IjinE^#4A zvZ4{N(wcLTF%g7r}>k~i7QPpX1~Vav%jMu%5c<5~JW zzibmVr#Vy%L%bnvb~DU8lgVk#2PjUv{_Pq2O$#IUG#=R(siU11PB2)PZO47 zvzfmc4$scpa_I^&>8ZiLKJVNyWE@-r*dxp*KSYwd{% zL!O=>d;iKg zR1>DAl@Rgnj*q(VH|ybDFP{?qLzb4B>mca z@9k7GEWiQGXTyEqVCbYEsA_vGsU^$xPGm?5* z3!qLF2CZ)v6T%uM<}W#S1t`}bK3o|77*4_C(|ZI)mr&M4wMefFP#_Lenk(IT0*!1alHW%eQ}$6%RGYb`Z^*FhgR}({;P(xe6?%Yl`Ugk{zW6`X&BmPnwax z@n&9Fc!eJHx*%(G-G$;kgAe8h-}I?;mWk%#UA1 zmI(fB5yvIVd^<9lUwV6T0DcHQ0cPFz#TRyA|F%$|VatXmQ$6~N1^XvJ2@B6&Z^G-wLWyMjDaFL*(pim?wL=>Q)pdnCDFggfNfxqnU z&jmn1A!V5h3(HFi3lqyb*qWGI8ACxygha={%YU7G;k$dA732>E{r$Q7^Y70ErPiUb zzJJX2C-;|si-1V0C;UQHk0Dq>QPoLVq@@C?RE1s*y#ka0N=_$Lp&^0d^;wRq8=BN!V`n{!A0;4wA_J#9?-$Q&-Q+Ged`!NfBoSj zEHr7^#qL2mvY_Hc%DGa$&+TuS4}`c1EtgqA_;Ut%iVFtF=mWK5@EI#F7(dl}95I@qe*v>3qzMEfjQ3KPd)%S_o+#0_y z_B2VZdQ6x4Bx)7&C!F+k4NaqhhnPvWQxQ&0l%vPTAEt5b2WTnA=+E-~U?t#IgDc?# z`?M)iELvG*sx{izC_>GjEj!zrzk{AO;P0OrnZotWsp59To(*`{BqVWumBg?O7F zk7EyBi1?Yv#}(i3u_PoBJ&Iq@c!K=YaMR7Q^3W$AuIHIKpUV1**}c$$0XH)YK@<8C z4k15nrej9p_eDhbV2A!H<85vfyAf2p&^S7QLqL7HAT?ZQ;36?0xi7gqYB>h2e`q?A z8Icm~HBz!4qYy`$>^jC6Jc_Vw`XDnBWlLx=r4<5`?;mDZRNuCFV`enjpt5;h5VF=& z)i&{kr)QX+$lg$=twr-JSc2VY-`;amJ7N*6gdn}`NW+vT9n(;P&c#v=&lP6N)Odd? z-;j+lM)l@}jOggls+nmy5jTFnSXL%DBWwtJy>0#J5%v+E6OlXCHHvVjnO+~WEDj|l zQd?Vo`?8@F3p`5}iz*8mGi)518okOZOMmC)3;oOz$q6ef3>S_{LJy4TknOIB1v^gV zq*nt^Uiup@v{!p-aqUp=!0a&EK%HVReFS%EE_v;-fS~zb>V^t@5QuCFBWjmJUq*Ne zNA(unPgqT6LW)z+jBZbUCtA zNgY}JC%w2!vegf(k6YT4U@Vvp-_|txETi{QG*mRM)MO5DT*6#(Tro2mHBvRK4iE>! z10^@QbK-NQbJBCN1CHe&EIXt?q*)~V0N#LWq*&~W9O_)c-(P!v+I%NW)A@l)ZGat) z6(1@Y8W7rrHH|GEjrGkcn)I6rjWTVr;)b#q4IV9PwBola+En=+g&STuM?ynF*V$D! z=fj-=Jk|HA_NoiHSlMHFyz=MyQi{haGAfY5sq70Slme67N)hWHQ-l?H7GxWt8v`3w z$1umwHzL{eP!z+_!>z)vajkI;6EqSU6W;ba^!Fx^Y4m99Xh>>MXslHPR^&_-PI+!) zZX-`InM=KS$`GI;R`(X~_wdmE=;!sVQFCzo5Zed}xc_$n*eh3jEOV1$J$k)jJ$%D* zlWAmdxahZ!l^}B(9y#vTYcn<+jyJDAy_qqZFyl1(ZqWXw=XEZdyTLnl7gkx0LiR@6 z4{Yg)38q!5C)4TjI{$!4Rn_iI~P3=UDM(td4MT1SVp@O}_L{n8`u8L&(Y$CtR zywuIm(xkTnVt6?eVNvzV=BsuXMAZY{v752k>M*%BvMJ>l?sz>}0hZ|sUs2gU znb96hUh*FDiqdbDFu*siKPf&!cS}0KIdwYFT5=mkURl{xp8vg5Fi)_fy>Q(p4h~vV z++^LbTua>c7)D!qIbQ`SO3djtVzrFRyg|LOyefi=f_V!|3zGt$@H7-A@nuWc)yr+Q zBcqZQgOM{apMkiv$yG!GGuT5)mf_HCUQZz?sTcFt@doK-*Mw6 znk})7#H<|;_;nTM>2>F?@P4-hx6Q)~pS|mJc3pPeZ*vXZZlkkA!vY6BR&!QmHJ*o6 zFtRQEZ+lxuTA4(*G7<&Uua!5bMhYgY(7w#T$Y!mp9*r zq>!X$ceP3m#d56w%xbr5f6;C*_A{1QxmcO*X;LCr;mtnE&Mz9tp3-7#Of0+HAw1=C z_Fy}Gv2%UJdb<{M>3Pbggr^d_)3q}=bGsU7D|6KI_$8^4{jgu*+B>zczx~;O$+>uk{KQn-XJq9lWcl5SpJ5GBlV_#u}av~c|avak-O@cci(gNyc=2lWhWyN zQ-s!{_J*zc@Q5SV#=$k6bYZzJO11lS!eB*j!KZO#uy$c;!-CUNpUg#nr{+l&?$p+l z>|AA?S1qKmVIiU|`6p^u#Ogpm^4e}Kq`_%$bGG8L@3M-J-g{^gd-k}Pr?T#pWNl$6 z^P9LWp(%gLy812L;rIERnb8qb+`(KQj`M~_aO0^~s;chv;m{i2_12B5kI~h>G4bw` zbJ0I(Cwl!+$3v6)sS6RY(z>Yin2iZI-M5N2gSp_&BDw388`WvEvGr!W;l%DL(OsoE z0(aZXMq_%=)@0Ba+ViC+M9Kn$HzK!V7hDI2^X)xM)l9~^GL7s$$Y&=;ubmwfeqomU z6onk9ts#YcgvfQ4EdiC1^mNQ*!BNoe+2jCb?U*n*yzLlIPChw`g}rY;f` zQzz%Ac;TyuWZ?R=I6T~`f1qKM_2Qe_ICL{^N=C|o)t+2@{9IGsWh(AF(TXRxW4AB_ zR!`Q8;!r;V$W_HyP0~b028tH=jsOK6WDW%fe1islaDg8vDA+H7Q1HNSOyEZ(4d(A% zXh<6D-|sLwcLjwMg(W3{--?C~#>O^|X0}f2hP4quQ{(1JYEEi0@3;+Ztr_)=Yz>SV zLDqJ6RiJo5+`w0BV<&xLkhPVKBR7bT^u7c)@cr)3Or*s3MVu`8NY!NIiG^()jEUJ9 znHiZ$`H_f;iFqB2Ot=+9#2%{ySA3*qPEK~*OiZq>u8gj%jJ6J@Oe|bnTujWbnO?tU z07@`8y4g7CgBWZa$sQW{+m48_qoISjos+q(4e?#O`UbYnPJE=KcOCuf-@`bKLFWI_ zla1r!w15dR-Q8hgVPt0dSKB~U-n+kY%bSCYt<*)#t%2nM`rv10X6L;x|6lI>N00xg zsrDZ=-*9mJv*thU{LeL&9gQ7?ZLNVWo%sLb$vjs6=gr58yi9j9|Hn!^Ec5+efpz9b z;$`~RGvh~ci3|J+1tkb2DI%l1-nZ9wlW<;zT1Uhz|D_qoX#@@<+{$ zwswA9URdo(px$IfvF~~=mZ~aq-9N z6B5DUib1Vns>j<&P@+{4a?C*w_w|Tiu(U7xo_M^yFD~DODDSj+2ImNRDE)u3@=$Yb zA%1UnNiI#L@ZY(8@ioqvrl!xAg;`9K@P)hkG}FR7W6~7;vKSJ?BAD~1}v3x-Z_k;6=%O@2ab|&lI zjdFo)6@C~(FVG@`aChKii)j`xSdiTFJ^H@`5(5VGN{U_Nanj1q)7Dk`gG~R<=QZ%e zdgV(=9_N#t06b6&eqQOv;xQO$>l%Tj4#IRQMQsE=ZV0ZBQ|1U0LPr&lduwm?VbQIi zxp_|6C!WXskP3_!f56_`n;!`Y`EzV+Ia_n1zI@aPXwGx33FLSnKsUp3Jfu&5wg2*s*$|X>Ve40T^=C}x$(VbxX4oqIHL92cLrBO^ zLV0DA!=`09mMYCSY!!Q_8}@!>{iaBo_Z(2{h$fnta5+K+S4-$UFWWMZT&?C9kw&u` zLeWg>BW@d6pZGyTOGpn23JIl`>Fny1oXz!dFsJDfX}jpp&0bWy7U6i+J5Qr}g(KLQ znIX)kT)f_CSYr3u2}+ISoL+d?HiFxjX<3@WSG#qt^eMMsBhQT1Cg_`+fx4L3X#Leo zBE#PCZED5K4ho73t?vD$0q(_`(15_e4Qu)NkfkVeFRh>BffPsdF^P#m)4OhhySwz7$bD34a)xR@HTI|m}?N|N0&C-aoLkOz2hkqGe|B)p@L-v(O`UJ#^U1W zjxq=o*C+;eVe5K3>E>mWsPfNd0%I+sZgqESWvF;-W?$3sh!e3}kEq#3#-<$EX>TT5 zG>YwLUOjHS+E5#o0-tTTf6kvCz2oLe^sCw5S@B;el1HM3bH9qH0O2(XO@`8^Y%rpQrkqGpJAx_j4ww zFL?7loabylm^))=dR=PvcG0`tCGU*(-8RaftsVN?6q`(D@$8c=4eo}#Q>q`?=#I#6 z*&?}REN-W&uhS?n@}eyU)o%F`+1(iKN9W51SN+oxrK`;5hgV&tDJ1X>@$TjUHi@)+ zDaPHNC{BWE>y05Z{`mLo%K|5LAmGQ}OD@0^tVm1}KOW>b;NaHU=Y*5fRx$2H-P?n- z2+Xwg@!^n_;)f%(B@VRE#HAnkc*3ZGCq_s4<#Dg1pC;Ayf7U7<&H7k<#eeG{OIcHU}AX7$HjMpB@Zj_3%4cP zT$y6y=EkKZ0OK$N8#$4j1wPr;bHTXbHN~qW7&x3_DPFp3OwSdj^2iCrjGv#sv+%|I z@;9s*{u7(_P7$8owzf7F_b~(!5r3ZEYR3z>6C~skpz-P+~SSq|3p#-_;<|&)7N5 z;oLPYTm!mFg!^uUoi1mD^HX~cMPQphG)MZYle(~vF0X^V$IMh!MVaQ{b@9u&Z3%G+ zU!t8?6E8kv_+4mysNLN)webZtzI3(ul#^SK_Oy|jfv(Fdb+>(yFMfTT7iXzx{rm2U zRn();$RgL9X45W9*|aOmF4||wTkbGUwdVi8vu4xgQcHE2~Xyi4U`4#RZ@a0xELKr^#<>B<3D^i3|JrHM1cQ>Gj}4% z>kuY&eEhPZ=Ed`G>pQZ&;wCrto@IOx$?cBR6OtSG6z}Hv#sjFIR~yx9yQbDk)`Wti zdOrD5J#U+6@_t`0mjsDD;2CWgouxl2u7a~VyBmc4d&dL~$IKUU+Tq3_>Nq1M7zqJq zOCKxX_gj^dk58%(c&W4|U!i%HC?YT4WCf*ZXA-aNH8qq)I_ioz%x_vUi@Obh#W>nV zyt>fA{4l+{O+)Hy9Lkg@1HpvV9(v>=6W2uA{=2Rw^ZN?~#Udf++PPZ1n@9DSZ-|=U z%2bG&8|Rw zA);{5(+g#Ets0&7gYI3-;w1_G56U3*{4-Fzjoe95m^EXf)QkDGb>E_m;HIgUx+-ur z652e%Zne9#>1^~gWkVAyF%Ny>0tV%`uac%R`4!zFI9(DwrM{d@=f=1R6}d=epygUP z!>Ova0-;5&F2Z6^X(Kcw)_v(R5hHip()807v>Uzx=O_mduN8`L6WIO zXaUfBo;71@Jg#xS!^rXu@`?^2O@v@)oa5|LhYVOc#-@pzMGv7oaYX}Fbcb}J1P zgZn$(KyV@GduuBD` zQ>bqitk7^HO1Jha$CI0efG=}uO|sn4%?LM&b}95ta39>vG2+D3Xf#t-wk>71?5#V0?Hgp5f)Aw8$@C}wQdSNX%09bAq8dAvo+Tai0>^JPY*-FLrIkUo z>6XqjV|h*ZQTpX&H4k4 zFY(=NAA>9EI?9ncjMv!gnO0fuMmmNJQ;7x1`Kl}G;2>2&DG2AY7Sd$yiD(ZB^nsdzh^X=UM9L zFu)y{7*$Q=Wnj30UG2y+(L1M(=4QMO9cx5UGq{js@XXyX+*{mbHQVgSg20{8W{~{> z6A!Qeke=&l-|An8Ox(O>{1L9|(KFaZ071?Rlh8f8#3kwT4d=_w|q^-%uE;C$7il0u$ zzmrxfBV1G#%z(i@(w)sG7-vn$8iol|0tvbwvKY6`y3&i~9v-}@MDuutru{p94N#Nj z8SL_sMGsXcpDScRquuN8xCx+(FtqL6mD|tT(cs0ouxvSbdU=gBr^}llDG@#lQX=si z>%lq%-QwW&}y1h!0B(P#Lt0 zlS%Z_30-yyfmSvShnloGzeHd!rnpV-XEheEIGevNMT%UCLDpFK(~GZj)J}BTItYgM z7(p(k-^ZEqhhKyY53f&rPw(>Bhr8by&G8t;+X;n}c23pmW8kwmRI;MjM(o(~&Jr zjBX!A(@;~>2vbqS&|#L@U3(-tHGJ=TOF#43Pm_N(w0=?HyUELi_yCW+@dGTco6O^& zy(3qTy&BA|=BJHnr5w+_sES@Uz#~5%zX)G2sMeIDFQLi(3eBVZ)ZM66G#J6cmq7er zNEk6Y4ne!}fOD=khi50;GOzaoq3-vpPPF|ELtRb3_C6R-zDf@7xy*R`9;d{!F?7@O z>(pI;a>MA*Ft1jVh)Tuzi%dEwNscg7f-IR0vh0Om_KCysh1^3bZ5f5RJ`nFK3G7XfnA_Wo+OI$g5b3Gc7sSUA*!bYhdH@yX4(tl~rZRoMeY8WCh!gHIs*U(< zn%mKq18swd;w*9$Lfzu^@@*HQ0ZTU|@7*EJXVaVI;&g9K<3v7qDVM5Q<;l}uYYIGb zGc3?R(~0<=6-4@GXfS(qMu%t`|GktI1)fIVv5pwUzKsuuVWu}aI@2iti9RX1U!H_w z)$fh+Cug2qDJRnQl`ZoNE0;=y2HK#vN#zfEeo!kEp@uLgOh!WzXoU!2+)#3Iq(UubhME*AV!gqy z3@^$@(v9~6wF^EY1xS-dlgm|vBaBRr7p~PjI0HQtXwA@_vF;*Lu1PJunIOQWn~X`E z8cgF`LLcq*4x+HYq5zw@LlncH+3<8ngB8d!2QXDiZqs*^udj6~9Irn(6kgtesPc617KL@|Z3KHppN zB#(y(w!tpp!TSw(*}B0$TBtn(7^*VYdJy^+_oJ5p!2))lqYv})!A;0h0d&%4`x^)J z2dg(L=$o;LQr6D@U|q)$0r$nt5|001N_;uc)7IVF=u09WN;?B|Rh8^JGI*hTCo_)% zIFVUq=`FJR&IFT*^(HwKR_?r6@j;V~A{Dj$V>je_(9pdi9ORrh2k8&f0Dwf?b3M>x z60-DPKkRTvRedfMdYlV0mEbVjTC2ie;|oxcV2AHke*?5Wz)knp6o0#is=jdaKTwo%Q)y*sXx6?UuF^c@&Of0g!wY? zX~TYwjEpSFd&0>@@uVuU0k4E*c;H}f&+Zc;8w>`DPZP96`C~4I?r-5dGfs8}t_T?bz!v;v=hn^0Mg#;}+xhqeo z=M>#YoT=0E^m>yX?*|IwoVS>)F~?94w(XkO2m;tL<%%7;ZshRuH}&jZ6KXULz9P6T zq{zIkpq|QOc}B5YTjbh?di=~W7gY`K6)E}$tY6Fr&2-=d4Yn(z^myyEEpeV`{pZZ8 z7sC`hPHt?t5b^9gjlDMDH3De;XPUv{$hNr}y^z@VX@k}W1UorVvaB=Pwk?QjKGuZ64aq}R)g^D_!F%&o{Xas0`N#p?w1c=zI8bc~`} zmk6FNnoMVx$y#gdd4mrI75Wl$FJZ6L3nOJZ#y!NU_w;sY?`BrES4Lm$p0>@-n$+i$)Eq47}LTk@Nvxvv}#lt*z2u*=+k)^_{~ z7EIoyD7v`-Sz%+{U7WR7YBzR{w*x&1pxAt?FYo_9% zTFZZl@Er3rQ_kv>F*BbR%J(e1cS(Dntmj6fRRi2}U2QwcoU2Ak9hP&+a}d5dDyYsb zp`5sa8V;lI z<0xQa?YDoldE5YnR3Hca$VJKP-dV`rcaBL9Hm?GJ*_!{U>liOGWgTz+17ze)WR8fh zj|NCK5l|cOF!7)Loe40XK1AkvtN)(z`*%?F|1&_}+xWSsC&k#(C*lF?1S#0x*RY!w6C<^mhq^{YMiB+5u*IdSg+L5L;?_rW zLkw+8swX|n1{Lv&4+eR4a5!~h0_W`POmFO0B6a;!;(L(g3s)cNTObPlv|OI^mAc+U zPwjzh(er@Op{gyCN%(NGoq_%wBJ5E=YT8%IXXNtN!u$I&JkRO-*(s6at{hSrm%l4j z9sf{Q5N7smv!%%CG0j(EM3q46N!-Z&!3I72DVw@&J3_)+O6a=LL;L!Ce2#YZE;?{h z)+N_JNitTXp+}PJ#+zTQm<>g@Gb>#)OwY4=CyFuA&e-&dq*nb0i@SyCO$-^x)|!^T zk>W)--o@r7eCdD`+Oy3)@-Q z@(_v`XCsj_TUidSV$Q4%G5RJ!IPb$h%;zN?%Z+)9c_VsPU&44cvBKVJe+`8a&}}bA zY60lEYJeo*JeUzRBgBF0`j;s--$j+xwR(>bg@uca3_2`74*TbfFAiJ84vj6@qJ9Q$oVH(Lqndw_p_Q)B?_6yJIt`C`83 z6(0J{n86z_myj2H82oKov6%od@#$(&fxv=&0B>4W2G64Cbkq8e*!zHjESDWz^pIu4 z0EzcHl?X0h^?aTw8$50!fRhJe=63I~+e*FRolMZ5)8ze56v+JU=BwkKa!+iN6QLkV zp=ftSQ2jF?Mp>WaE+64Kg#o(&bX2B|V=J0R2|E{j)rs}CPIA}7p=Ep5T!uMjC%!SY ztxy(*{$f2S@v^-2rtj_cpV|of8YHSgjpWc%rC;jf_QuyL{@~(ifsn4!cEjqOrbMS< zuaB8WCm9@IgM%VTF*c9|l@i25J_x}%5|&!^HoPe#^APv1Sb_?zNu|J`TY_|;Qy5P{ zkyvD(0NA)vszBG~xtCjSS+O-xt>(j?eqmWLp!l&;ZF_?nOhvbrr33C?p)WS@c`++c zLdjDv{ro88nXt`!8gIq7NxBMw@Jtq2o2tJf$lq&UjdObyeD2U%p!};~yuSmD!FF8& zCdHl|Qop+JwrzV@8x#xjc9hbWbSTpo^po)x@=6*$r|HmajL=t)pu6TS=atVGz)=fD zAUM4H3Mv8uq8zZVtgNQ;Hdan?c^og)qxH2&ZSHQGt(bdhini<#vo@Hx&0AJ@B!B1L}o<6PVZd%?5k2UKqRoe4hV>MrIneHvzmx=>A15I0V_;}FzqIu6J(+E;Vk?i~Z)kvUAmm$4 zbs_4Q;F|ul;4$-pkV)8GC#36;_aTtblLK(8HmlL-Pg$VKB|sy*J7-fzJ1*-N-~v<{C=9UJ=?dA-oX9BHCf>ugBVm9MhAPggqTgbL7Mnc?bYfV&1;6**iUK zsb+Fn05`-}XILsOW5>GVf6&77;e1Inga5G;r;(^4XsW30+pe``}FGQ8dn`L_}Yf0TH3_y9;4e zujE4@LH0gEDNgbQ2k&euQVL<_KbtOPBV0=LXyGh}xE!`<9o?KtL5c_&-WG%8erg0U zbTSaxb{w?RGuTNhO%|2|yoE|ipr$}X7XTg?7s94h(Y4I&P6CA`B$%2tIh@63%Gvhb z-)hg)uq;MH*(xkfmq-dbd78fh7$A&kS}Z_GU^(iqP&+?GW=YeABmcx_x}^Wx2G+>} z_ft{Tk0mp2#2AQc`Zi~&@ZIK+k3~?rv9ui>4$0yHq97R$)f&w^5yWk&`B4SY7Q##u zu`601$C{h_Egq}Q>=yk(9{gfQfiM?gMapLBj+z~p_83;s zANg`!>p6u2u>%NxqnCu@?W1A)X$QrzbGzu{bjn08Qr15r9L0Qe!=74wBm$1%>TP7e z?r>{AI~ra+K|ncjL@JyS<<%_7`!=s^%&IJQxW_27H$}vYT&T61*ShI?6gl>m@T#43 z=3rRm+qW3QB|k056~oK&k!b*tPvyz<(T{Z<2)E7zhIC0k7z@r*z*ejU+jC$m3@|Z% z1Z4(R*lz8CT=8?J^0vmxNxUZ=u>h@&h4HvLnWYrIQUEt7&*M#77wwkkCDpze?xNal zxPa3Ee@zb=sv;WnfA*sk`E(yVw(Am$87c5|bDT%V7yJz3C1;B8_vF~*duIS&EM7FhEZ(b2_ zw`|M~x|pGn7`SjQDfe&-BkFs6l{)!E!3c$D$CDGJjO)#t)( zUMZyWe5sgG5BPlj>_>rJ<$+LO4`WP)DuylDIqhXfn6s!Gh2)nXGrJ_GI_a2IK!lrC z<8B@NsPNUnS^fDV=FfA(@^OVuo0uqbpmITcgq4&97EU5Q? z7Z_?;BVEt;5S*|fPQ0$y>TfLj>mxwe&?ZbCznx0T9*phmJAi}fFY9W2h%JHJp@ph4 zFL@qvbmsf-NETj?EuQ=4d{u#*2>1W{m5aCVat;m-KkI@H<}8nCk+%LO5{*Iq|BIoy z6HXF1d`y0*t52?T=pmGrmC>=GKYt!RxaRdCICxKLP3>=TfeWRcw4AG@XZ?$r%FmyJ z@i+y2THgx4*U^bMJF~9VZ^sE{9({D_&4~yT3Qj1#hGLP;AI=?*sMy(`0?IxP+W}<} z)@9EZ)1Y?T)LVRZdO(d6IRNV5BivQT*HgN44FC1(eK0ri}lP9HLtUIm!rq?%7-9X?-jHrr9yo8 zz(3-p$$^vY6Q?W7_oO{Nei#iJO6wMYXh`^;WQFh56C4kHJhl|1tYr8ATFJuKF6r$_uje4LxG*P&N2L?Im%~DTEAauC&C+c+|Wy`Uql_0*Hs~O z_I(~Ud-cK>kmRdPXI&VkFneb6F6`~sI^*2EIN;d!YE4U!v_SU{!-Zm=E#vayn`9Im zU0bD9c-PsQ4l}BCm3H#CaLZW;f{(85vki1V)26x2EkkcfnhRO=@T82Tbs}uPZrX2J zS{iQFmEX(phl}!?0Wj~$cH+5q+&Qp?5>&iv2EBuaA5d*>IiGp+eBqNSOub}CQMtfb zIGfr0W+og5++22$uk5W>VCM5663L1hJMqAqa!V|KL5lKGRYJioN|gPxJ=9Ky>IcfO zI0|scg`nDx@Ez)$M~>5DXGAAvyT`x$!_YCtpm-vd(}OVOOCraK-nuN<*E=h^&ftMn zVxRCeO4x=IEuMEaR>FkDKRI)^vb|~)XI)bIVI7~Fq^cEK4MH{+N;KtYqQz2p6h^mahzSwGrJ`B)$~6 z-+Dyb44O$-ezBt1J!7|7kBraEs8Eys5QDI1AKk&G(m+&yuE)5ekZxR}$!G_vzwi}! zINTPWe8KsD#&IyhY>s-^))vh${sLs15%qqDtpsSBT`jvm4EM0PKG9dsMRq`)X^G-4v^frc zuQFM>LT9%WoXA3LlO~mdYc^%2;DEH+mmCyCgc~GI=si>(L4dT9n2jXeGsMkXtl)Z7 zo7cl1qw0}sdBC95FHEptF4>5i{Uz(g?T?~HUWz;kDl^flXE{SjT4l%bgip)S#EYAB z104Uxkz98OpRASOHm>CWn~sFYmL2gtBmEqK-2RUfoT>&!OSDM#OP{GMK2105U&WOe zQWGmnCJy}++W2p`T|$E%18s@sm<+nPGEYvnlBX;nZ~>5tQ$oq5+4Pm?<{)DBx7*46 zUfN)R)(ni@-1oFwf2iji*WA68%*`u-`ZtQ$0+a~LAay;WhljNRB;MLI#F?OexsNiM zQ2{UL68Gf9@BT7P2M{L4!^~HS9|-+_xAMQdpqd&}ntjy6n8@uY;pm@3qUQB}JthR* zrFY5tE*Ss6CnWrv0o#ACVE|DntoiI!e6-0wNIk*-g`{4+0*Dn^YHy8ex|nfgi}1xw zqaGDM#A!m%=+8;GaW`mx&C7Jm1P}&7BVzwp%Kh#VZ<-B90~7IPUQS$Rl(2bcV-aA0W8U~0{(7s+EHPUhbV2oF!sB4UCGN+8)CYe zs|4=d?7Nv{&SypELKb7!`Z z;L-ZsK=T?of{dZ4TERmhDk>#XT#4w^2up7(zs?M=Bfl{wkYl-hb*Csyf~*uPxoPmg^>r9|V=DfK zle?!X>zYS-bbCvMnZXJ^Tc)(1@%C!T-dtV}JiAg2NKe(SR@J2?KTui z$gW0uVH=Tg`Exxb7>vM6QRu7I{27BaH@}!@MCWMoNET@g1k?kH!lVf-n@E>As265a z)w3_@d6A@g>}a1hrB-0vK?TKQd1AxetSKfMhAu$0UuQF?XoP$&jOcZm8__0&i^vK3 z?5diz0gv!!3CR81>+Wo$RwU)u=8GaHpc4t|6yi+tKdqEo{H&>z8*~tl3oan)TH;U^ zDE4D&R6%?4o{7E#P}SK^0ci8m>amfjIK!f~$MSpk>sJ>a?6gWXkbI{eEHFa( zY&0w7QlE)3JMw7ZoeUg}W(Wiz7rGkkBQD_q)WcD=Yv#9e3#5^$;wCEXwd;jwylk@6 zBcD~V4h@>|yu$3$@Pcww{tyouzDy;|;8Q*2ms7&{ScTmM_?Vzbn~)B=Ch`owBD5VM z%m8>si?w{lxBPp@Y;l=Wb(ubR+J$_Ol7{2P+f0E>pE^7Gy4|;4GxFi$gz{#02ilJI zZkMH45WDa+0r`%tFnDGy&bx`^)4|&OUVJcwm#>yyY>jl8%;x($z+U0cMi(w07^Y$h z*93t)4LDA1epKNpI_{0^)|o+nVJ;yT55bYCFU>I9>`}g|1`;~Rz1c%F7q!E}cOa!B z$%Q+^&+eRjeJG5-Kn2d%n5`luaU`u*!BsS97_8s2=(eWbT`B8+{a$bE#u+7F>L1$w zj*!~D3UPG7lBC&g(Q9-j-+wJ z)pw2-4CrksW!bkqMJHiqMdS$c@)Ebj9Y32t5R@roW6O#^E;A41&jf;7M}&M{epX6Eh05lbL878# zh3c;N_D`Q@?Pztc_lgguRz`oQ7kgk=zm9@OFR6>px=agmovM6KJf3r`jF=ajL-kA- zZSxwFAu6h}9yhwE^Jq~y+4&l8Ztl|%l)xPngvVC=gM9DV^C_Ffo$^9b%;YfRmLqR} zp|KmPFR~=h7|vKMpQ9G-{@Fb-6GD_HrMJv1d~08)q4;gH@N$>ZYx0hSS1BhySNp89 zge|vUjoDJNajmO9ST7wA(_vH7ys#gEHziADA&GOBEOJz%c+L3}Gb2U48vZVqnW_nr z*(bZNAllELD_YfQbJjeg&&Xb6)YI?JKKq()guz*|pq;>3=E$TB zLyh?E%Q{&~slNopz7)SfsSPbD<%ut*cK1Z|Ue4!`JUP%A$xRhGl4R#GvIRu3{m^X% zF*Ib2zLhTi_W=fz#z1cuvmv-5a&C4~yePh>_IR%f{O1J4R+;dP5rZZnemi&e$w~!T z_q!K|?RtSdLye9*?8$nR$KM;yt4>E=&6bYSzGWnG3s%1r^l2DR{6R?h?9wn*g&U(+ zttgtMx6P0IU4@r-&HyRL2CgR9uJpZ0Wy5vrIJctaxc?{>Dr;UVptbwCzq?nPj}`+c zi!lMcIr5^DI?rnI0K3}0y&ngQ1N|dB${TBT8m=dfs{?IqXyI*SXaZ&$U8n(fDLy@i z<^+UlIXqt`>li7-gS^9?C!0Jgg{dyTHMzu}n81D`mLQGE>vk?Iy8fk63^IkC3R0zx zb;qzMIC@G!?P$B5lX#|P{3@}4psO6oxlxCFHX~m$HS2AI81+nb6KHHi6$6sn*AR)h z_#(2-@~~&YQ2k?slIN#AS7#gvqLpI;{UyRLF?QvE8Y!XuE+R7T60Zmqk`AXys8Fg~ z-L6h~>>uVoEP6HAx$KSEU>B|hFr@VW22R^S&prdsP%eccxLz4P?N!b9_pF~hs!Jri z2@JCf1#fmg!*CGTrLA_5-OwKSvFN@nth>{?wgJ(yC%yHuRyBQIs`BRoH{5;^b43Pk z_b5~i=Yw$)N9E$aO4rOK^`bRp^+*gkhudqpXWt{erW0;6UkYsqbyVCsJUdQgR3B7S zHmRy_bkhDr*0<%ZTchd-{=hl|KILh%bbEyuB?tj1lHurGb^fFTwW@9p9LSJtcscyq zf>T9U2QrrJ=5=D`F;8)4K+Yg4n~QZ_MX28;lrhA$z`g3+b)%QpCUl&&-_53Sp$sM_zSn$*azr8dv{q!v*!B%{Vl=(vA^=yVdFzz#}UBgtPB_Yy9e!G z6L`-8|Py?z$+di&98@15K#j??~mp^L9q&lqr?9Hk))&lIaW|R zVT)SrPgOTpQDMJ8!TGSYwIzkp-Y&9q=$1-MN?Jd{_2NMYj`B@LoERB};1U%*sj@aA zUwn!>lv+F_|1CC_jNcp228ZP->7$?vjk!k&=!x2}^Krvu#dF7=-PTthM5vvM8bJ^VPzSC4*uN<385g|U0 zZV}>yhEuyaP(j znaeJweSIe`>HdzOdVZtmEpzgp2Lei7Z)7(|nyLzW`_2h%dtWw{r8nj|$X1KI^T;Ly zAOy0u=Bm@*gEmGYYG((^J$cVo7_GSD9D6p^(swZKLIseST3sy%a|G}*mf-VZW76o! z8JvHLPru1UK>2%|J<9N3AF_QbilsO~MF;+jJ2Na1F{;W zhlFvf^j>NNJedy@NJSIS9b8hvA=dBY_;v~=pCHmqB>rjG@@ z=yF?A;t_55PGYc&jP^S{;j-HAh)3XA?Bu8@yWSzFtIZ9@yLWExQp|#W*QjS^B*#MX)jFKb{G0Ojm5g8S zD!3Y2Fxu>lVh-x;=()6cB#yzV(|uAwGdA!^Zi3y$gI_3-=AI9(1+yGq>Y0?nvFwCt zpE=u77Oqbz4F50X7Zkh_jAcDuos?AH{F!K_E5!!Wpxot{Le2wjZf;HYyUG3 zWaGKdOxW0#!PVu|q228o55RcfO@^NPx?q5o0&UC+o zkfmXv{aUB$_<$({{k={~T)lCMK0r-KErsyDL@mfiSxboV!Tj4K*Vi<^ij^M*X^|8!p*5EJ_>hbgJ2 zTZnfO&xG&K9qg+x$mUoi9|KwA#3bsjbTka&U9m9>!22#2`XX?`s}|8nXuTgt>+%$n zH_Hx)_d_s~OBH24)rJI*10k2xr_gfZ#vDS_cK_7A5PhNo0sl!ne9NVSI^r%9_N#4Y zka)Oi4Ds@*Wo&3Djtd~L9ZhhvS!au0dQdcG8O(?*jsdO2Oe`?PIrw^yU$ z2=j0QNg`W!^(2;0e7`HQa-;T22*x?uSJmx~Id(hd1v35NcK{yOza0N2lJUcpzNaS$ zel2cieeUVv=q!tCI6|A@-~S5$kN?W>*;6{(4W;qNtaXb6ah)6aP%7(*>yMe`;@0?h zni${wii%qBqY)@hvFl1T73u5qB4=M>OgRjFMmQQ74ABU7)U6dsb-Z5qlR(pxd7}6^ z7)S)NU^*#v9jALQ-Sz8Gb0a8X`l7(`^3l67`RdUm|j1{g6no&Mb6MS`c2#=b&=vU4CKy2 zsHC$JYv^H#b_=|Iw?MWs*^ogfh@1WEBWOG8^{L0LErjGsroxAk5>e$~$T1)xb62i! z_{jz|9SMX5e1QI(a7+-e_|X#~*FOo-+h;EInfKZYZKs)kwFQWf#LW4m+q4}Ai9ju% z?!a~^kkXV!2bCbIx`+Xpu;1mJuK0V{fN0oHl$NBA89WEJVPXQK`E>q|>b^Rv$~M|l z0SQ46kQR{c5(!C>?(S~s?nb4%r3LAd?k?%>ZsE{y=$Q9_-@V_x^W8glX4cHC@gJ89 zj_-;0JbUk7?E^(Guy>89;ImHvfsNoC`g`)LDEy(_mq3qL?8|~%dG^vA?sYu3s^JWp z9?AHWDFN*R+ao)~yjC!%a;%}ntH=+`d<~xhKER|I-DFl{s$pH6WeUR&qypS8)lvqME9ReA2tEujsUo#cf zIguYUDO#3}Io`D=Nt0>^Qmk4YcOQ10vymtiWex!2-LYCHX-y;dohxS*ET=aMxGXy{vBLXrP>tcTp?Gwt||Wff`MwY+sRAX zn%f`00-o%$uO5zSg8E$ZR-VFbftwkO4o|$}Ia zX{}jQhWj#Y-t{8MK^xaQbYM1qTWu(*dW=bMp_dc(TA?9Pm{i6KA%WL-Qk$TX=NMV? zwU>rrsDp!Cl~!{>zI>EDiCOHi)>`z^{>VpT=i1__KsZWJAGgAVl}b+6bUsXW9l2LO zIy2&7jaX0=i|Q1MrnH5|Y*wI_TP(N^exo&@x7QklSSE@?vbtBt`6 zMQ9_QvRa$y)TKQRlGS!&I&{dyt5MG6xAqv>fGI<4aQh2o<)C>51q#e1nISb-Qpq#% zeIf)&yjTkN17prN4W#z;j*Z6`fTs7Iv1;0W4p+}kLXO*YfdMhXEqOkRn=0~4A~t4G zNAzeLapL6DOa01C8bVd9Sg>vTy2tis>d5W1=7S7EJ%gepM(JDNo0jMLDbV!Qm)d+u zRcNemm9~Ml{PSjI-EU%v=meiR1au~MpsDrbi%BqTn^PzV97%g5(3(ko_9Xfpc8zVh zONgG<16r)qwQrGt4?f6#po$0QmxFx^J3(=W*xj;d+i;6XfsF{lSBhveQoIZ8U^JUr zHD<{k>W*@14Vq0)={yQw65ZY^Y8Wcjy#38gAY&9D{ig;YS0_dN0my z?yB4$IXq$tq^jHQ7;>qFT{;QQQLD+gpe3Q&nMoan^CsIXKe2Oy?_~vKk&=@G+5qY@ z^quLxcyA%^V+~qUyiKt8z^%M=W1}D$-oo{a<3)W(C|SMwrqE=);A_L82}Wt8d_4;N zcsLZ7myu5XX>ur>G>Qb?TyJ{beodI?PY}6S_@x);pxN{5;AY6M<}#B`2@_ikz?v7G6G=ofQTx_g6+PAP!rkkUgcVal=a5ZY5U*_&xi2YKr=ZH_Z*(An^S3n!x3PZ%W=lPn!$lh`po0MlaP^7+sby`k9Sl9MSH<#l>VtwmsA?jBQqI!NkT+gk}-TKsiGl6&d^A^|QYGX_5%c2W62C2no zUOKI5MLns~bV)o;3{wGSbZ#d{IirN+=)-yenvV7JZuoj}7(N2(&Ba-HieETWzm%ps zDv-E04l+S3``1SL4@2uAD^!)nSST==uV^&sk*&)UO2Q~hOmdA)a5n5a1|A2z7{M5c zC>+^IxM_W-&CLy-h*E%IzjD}_4a2n!olz@v7WNBV4$3hyDx0K1fj?$k5w+gna8VTE zF(OZmum3H|P36S$zUhZ}r-MGKvdfV2$DL6ugo2>Pd@co4e5Yuzf$~VK%e&3d%Hjrk zvNZW`{T4c(gBHJ2AV&olz!$sKr(%mV=1sT{i zqdaO_{%0TjHw8W}Vc4TX9drM<=#IoTX4d`ClrcMFTe=jiB|1*@l*adg=r z{Sp;bYx_0gU#gTrh-}F+Goa1p%iD8SwPYT~ke$+S&QgWaK$z|utZefCY_m-!62@)c!Oh#UnnjbD=G zcomHt0<-x-*Uk^x%wMB>>V5Kut#zMs7B(}!WF~F_4aVe@(!?O(KAKcUK-xDwBrrk2Cs-(|=(`-;w`%CyR<*etAq;~+xOPWpwDjXVvsFFjB$n5re`9Y_LyeujL}L@4FYX0#`7-Qs&|`rqF|ZW@B0k55sWT> zj}aNHA=*3aT^0E8nx-w3Tm82Ud=51Ax>q!Z=Ux8b;58)SqJ0qru19&CA1;ZO-TG2%`%pE)no_d$~gapa~wz5!PM!SipNX`qWZFiq;iMVbT z2;7^%0hslFSi&jslxuX)>0k(=e4bTPfrK_2Bjdc4{o*Lv^m?QOe?_bI!&y9qks?)% z+`-m@Fp&qP0%$so^67PZJzf^}lDT-du~@(h_2`(Q+Memr3I4!C4MVgut)d;x%vqJJ zD!^{ye0JR^gr2zl;=rQKlLZb?7_y+C-pVr_cIPlCG5a&>{Wz`1Rk_Dc}SDkndySeT>?`o0}sAXnqg1ptMNp z7K1MghM;4x$Ttff!>Z;}1Up zsf_=;CMbEZNdWGVwQQDxX;KEsgeWf_m<2~GP}XEJpMun;Vtu~Eb@oqhyu$BJKT9R_ z>&l1AX&zki5e+`i6b^ogKg!zHb{qlzp~e$&2U~Dhrcx?8=?eP!!DDcFTDP1h zGXdTwK=sG*vQh4^10)!s- zSLI1d!##xtbC<6|Fj5ke2x$-&Fb_NdVF5e&8Z;16f{J(}GNRZ>AGJc53BS=PM;_q03njskERK~acV0XGK!l*6mn4V(Z6y_3p>yA9 zAt~QMiABi)1z`oxtPU#=eAPoS`I`*t4XE*;M6y#czlky4A5C@X0ALPDIZ73a_R{id z!+?`~*#V$5D*3%r_up^Qds&C>>Q9-Q`JbjV)Y@XA%J1TyuDNIA(vw`Z z)rZAA0Uj1v$SGoZsD3fG-^C0JLRC8UZbWIaL=f|xHT^_ki%Ea~BD5iap`PiW1G1}E zwo`kfZ*R^{h(N`FieFrtJoVt3t^4<3%`2{S0p-ak!;q*zb^K`7eFe~@UQ{I)xX#k` zzBPE3%0_xp+nnfGF+rXNi4oU3jr=c41n)?3L?^|(ln#!1`TgBVL3512QB-M;%2$d^ z{asXRC`y2ui5XV06kRGRnLJSH7(v)ALBqBhxCC?6CW?`9O zpYn1SDx1E{{0Ah0cf%DkKh|IMf8#_!;eCf3&UKS+f^S71^Cc&nT2xFclS$n_RQBK= z7gQ33Iw$azn;#lqD<4#|5&U*CYyJ0dNd?8^-o^%ptO#0O2fqa#>iR3$HMQD7a3vw? z%N}BaVfuM0cRA%9L9R+{HcGy4Zf4TA7AnLBM&yCY3$a_Yz+bjjYtxlY4bNf3%>#;${FfTTUIQfK)@Bb^V-g797>k@OH4_!hH;BMw1Zf3McAbGY=argZ z(Vi<0fkqAkBQ!l00VOV8{iVkJyt1m$t9_3cu#)p|*`h^TNFTLD0i+!;;DHYF$2J*G zU~tLWk&i8Ro6(&Sez2O#zl0!ZTg?zm*x*%Vc z;U6_Zv+7(`#aDFQt9EqFCJb`Y$>KhG8?4=QYM#WiHKpF!(wKRiMRdGDfs%Hyigv`8MRoe*!-SMh%Yg*I19E z#vUL)wO}LLefeij0gPcq|LYI_m%M}F6W$Uf<)^k9e=(k#P;r5lBG!!S{C|1Q&=@IN z4$Js~D;pc%@EQ0s zbFeZs*WT>9?;5z-9s^+~HTw&aH9S@CjqbIjYtH4_(mf>zXP^O!%;I$__zVn)4}W^V z+#NuHX1PF8r4Kf(T-|>%Ew=#z1(o(`DUbThOEOA@Rj2b7k$~m98W1JKql-%!=?-YF z*rU`#-gs%xv$6Nis3ZYUY18hFu}hGp{^ZX1^9(n}9C*PjLua5P7U`pFJ?CV81Jy`P zg}AgZ1GllGR(lTkyc&R6;JmBSp~`J)e(y@-xRhAa==3DqL+v{t5*&2J60#YXNBI1Z zx~GE*NhUgO*s-PPEXk@at#VlXJ5Mq)kL5D|vkOj!K};Or_HgN-S5XR@xt zbU$?q4d(M4wgQq#djcor)DwV>f~d^!6WKfD54p}HedX=9%(+4U#eY!J$=j_kuGdJQ z`DvHkrg9xK+@=8OF^`u-Lv-|=&fr6t!jAg^Ln_znX)=69K%VYl^6v}?+WU0J36Ps% zhw!O-$#$Jo_uuKTn$)UWPZQNz%>1qw(mY$QVq#1W6u1;FMO3DLZZ$0xv=1LFy)LRo zkEKwL4dq%QawKl>3zajR`PzL1>6(8ZAUbAbqOAv{M|&{tSaQdJcKfu*V)n@wPXQyw zQT&kOZ+Rd}i4o!s*WYKQtKp1Q_5ke`U1>DoDg%nZN)fp0YhAzwTo>k+1UY`*_*@C4 zA8TtTp}m9{zS)z`2JNbgQ)^LuUbvTP7cIpV+^06}{%~@AQveUGt-aRZ+|CbOp-lS> zh*9ibMh;yrm>(tTZHMGr81`z$=rh-E_$oQCBx7vAbZ?yTh(qwx3q-`z zv3g+IFPzfs)PE)YwmSC}U{{9NQ%X?rz?#)dDTmf`1?yDG*N5Sy0WJ>DI{NmqI>uQ9 z_S6nQoUUdz@oz;>IbvB5kERsI(JEQjR0xpbD2%q>l8o=>wx&HEidyJeXOnrZfLW~2 z%i8-4mJD6>%Q#~XSDXwG3)wI|6}LM@u-Ce)m*VX}nWI7|t(s_{p=QFVZYUnC>8KU` zd<*D2bp%Q}&dLf<6cMnoLWsF=mFlUKcGIX6#Ssf1igW($6Jtmr#c%6X0! zHo%^STrMfOzdv*LnBs~JAvMeLW03 zujf1D?!P00errO}93KRnH*)ku>+!J(fq<@!m~2(AwO726KKuRV{H#1-+K#{K;e}1? z#;z$uA)t*GrspAsj24bbEji8_aTK8D|8G1v&f~q%pMVcImasyZBOF;O^#5~-oR_|?mSv~x17YcbY`aS zQnK?TnByHQaV4Ks+d|LF%m_St`gG>m;&t88GRbRJ?p2p%C-D2B_d|_KOJm(jwfol` zDE}0er0}4xddMFBhp;5ZhoTD{wf`KJB=MhzCCNYPtwdV>yPPJSnIVW3&m<49;$gq! zL7JJFQN)FR@uD>!gRp&SDo2a}{oeqYLfsvoT3(ePDPS@xZtAQuJ4fG^?s6ZW+-7T6wsF9uxpYyYW|0o^K_*`U9 z!aaW!nZ<;$2AImMXL%YPkilh=E!$^Y@62ERel?H!ov5+0yWV+F2`>onRP*#*`aW<-OXtZf$D=(Q?;)WE{fArYeP1-A9y4q^vsmT`e z#9;2?Q|CQ@7=!#$32b}1)Yl^j(q0(;{5dBwYHEJuIv{?}Q4f(r!25!;CTX zdZ39g1PO0+2#GnWyVlw{cHWiS?D3>RZyzk!Y=8;0Jzf^?+%rf|IBvY>!X;1{VP$~a zG3h)o$z5X}`gaj*7%4#mrY1@f9kO2rexi7B1ZLGsZI_5U zrF?P6Dv(Z-J8Q(Jx>(DBvtJ;|IbqTY9Q(3Ss%DKsZF37A{ggsMNK_)Ad;1;9-h2V- zAir_qkgW>DWwNE6pJH=AFIENfRs2U2dNgLlh>VUyEfzLa z^8KA9IbK?k+a2?DPTtkD9iMaomY=fRxdF>~zAyroehk(jG|?rld@!YV zz31ME>eh%I_=^Tpy08)mf~vpG}E0C);UW70!El$Ja~AUF8p4XyvEE zVwy`$npD%>h^EEb<TcT(vbHp>d6iDYvE4dH{Vgh`q;7R>If4S-biZiNs1-v zM)82cimyN^V)g^HSL>=VxjbCiUODB4ogcbLfLw_ZldQNiY0&8bvhi{pfyHTAqbg^Z z3CPpwv!{qmHG;MzEtYGyyB1`YZP@^bb1nK-j>+?v=b|$QF*%$Y*@!;Ud__rXc$FR0 ztLo8#E#p(BJ10Fu$WyiN`DL0DhEY;()s)6T1lB8c#HDe;{#Q;dnvFM1#EX)e(8 zj@ut)sHw3s@?Oq-54v4Q++hrV=+F7nH4t{>KN~Uxdc#NrI(w9D#hjVXxz?|BubVga zyB8dHbTGdzI3ug>NNd$@4yn&YaZ*;^qw3?yqrKczd`?-H`?Ebh@yec33ACYtfke*A zPZu5>;F9Yi!k7SqJ7H87<4#;JGNgEBV0d?Y+gV?qS+7PAN4E1}X_F&e%r}3%Mn2HC z#8E4sP}%**S>Z-R<6%2BW#3QByHxgs#2jtmoph)=gQ5ACpKwZY{P9xnIFO?#Uau-F zc$QdEe_u|Gc+%KR<91|Z=s^774qZkATE<40PY!u{v}3r2g4S4} zFIqb`N~se*RZC&Ys$L#~FRr<{4>dJlm3Ad3{ApP@69sO94(%`JFzLt#l4{!|_IJjN zH_F0p-)jZ`6#G&xK-%YNzErF3P!*5Hax_a*O>*5FMo!EjDELNvIDtzez?wYyeCKOV zUnaeCO&ECH_4588lTg4DtXmPJ0H;RtG(JnaRNBjq+pnpX|A4OVt}Nul%MVsJ0k-agE}aq4Lb~8CNyxd2UPf=?qDJ zjOP6{o_*2{#F*}Lg=XC|*pqi(XPBk6x{5xmeXAFvSVi{s`FXg@OMYTl{ofI-KnMdl znLeP)Pq^wZkVxX_tO&tJ@624t(vpt!raC%(O~>4<;!>JL{rn;s`G+fbi~F>IO^#eu z@Ge#MQWw7gzhHd3E}I85-g5cTrIV~ze}GTKjiJ#=ZK@8aS(_fWo11} zH8T_DTDPR0Rn^MLHy0i|0h_j_FVA`ftDm{gxw64rlv^ve?IEf#!p+YtfXly~! ztr46@Vkk)aqBZuQtU_J6HTL+JL)wA7grG+j%kGs1m9~opOQmdD8B1k57w}GHOG5bc z=5lvxvZkKJZ%-Id==wd1^O}|C48Gf}LY~Po3?vUis%0+-SypRm=5mWqxdMgt(UU)X z2!q`?f7n0sCm|CZfSW8b*iy$Hh3}u7fkxHOc#z<+wMv96bU<%BN@jiRa@&DeJ60~c zv&Y9YRq^=SxInsK^~$$$;YUq9IgnvACt$^uBl#eF2d+$eZ4)xzz_pGs zezUcW>*=~1`5>SNgzx--@Ev@7`sMY*iTMy!U* zE=#Ch17;_x;Fk zs_ywvgp1qdH#ct!=?LVZDpo|ui_JiR->RJi?^~2ypKI^&hGI2``zIA38s(i%v61Os z{gQ%5IqUQ|*lWG;#6SGdd{(!crbO_8q8T&cu6$60Tm1|3<(9px$5l+3ncgU4gSc4( zdTdC!AX$Qoy7H^onAIY>o-5itJde~~s#a z-n0_75yh#ZFj`9`s|TwRka#v3pePCE=#{G_VxZf+(q-OlPm`k7<;ZFd`u)qP^y-w# z&=BD~JdI%3Av}4o`vT=mx9wPkt_Ci)=FZimVlX;9LOJ8-oeV5Tm8FkUHNNl#-kd{f z#RyD%Gzw$^(0QzYOfbR(XV& zKbkTHyVCNeGvhdpcCbeVP_*&c0j^@-kCvh@VPoY3HtzL|kZO08^TYSukz`pRb$^%$ z)p1Q36)xGK7!EuW|=t zlMi4Qb5Czg;djzH>~h;+4#b?NL@I~8@M@4nTuxGtI*qca6Jsu1&Wf~lvKwh*K*24D zfos?(^@(_2C5wW?G}e2tpa*`lnz>oytN~wqWMZjM!hUZjVf9*hl~&JuJhDH>YTNhz z_6+-~vFe2ufu+ici1s_HO;fe13??ne81Fp*&mQ`FHbgzepQNu{Z?zILUt~?t)Wkjz zO|89yAHAeZ@x(~YI`p*f{Z!Ua?;f0UEr>L$WH#g=YsW;Vb<-flajq@xRQ3J0+0pUQ zDqZ3ly|P_8a&Z4{mh8e2C|vI)yD>Vf<7sQ}(y3s&mEuv6#`#yydK1U8Wct2-;+lt} z(V7)FpkeFu2`o2=k2p8BrE0f7F3Zb+HZO!+9Eh7(R7F{cmE=5AuG zxp!)3shre(z?gsHJ*`80VJ}RoBh~?m=FX`wo|~M*=Os4=8E&e(Rg9#rOrM|Y1<#J% zfZ}biLVFh`6y18uq^ZT5T&REN(F2vvAQ;wFEWhT}5OW2}cJkg)6iu#5Y^`|WDABvw3?MEE zsF1a(5=R;GRvpY2amb%3cAAyUhwa{TBnnVStJ;ach4!>}>S7EeTQ3%1ku!wv_>@yW zF)MeS{w2?+T=x7;&{n6{z(fgSUyMW|(Pk>J3DEt>`!|9T<1QJT29IM9} zeD}V~!?&i2csVL1TEFR1DO|U!lOnzOsuQpxiW|JzCp>xMCxFy7O!QeYNm4?r3&`eR z1PAscusQ^p;Bb=nYUMavp)r!Bre+#$V$m>EDF{AI8}m^pxohyp#Gr~U^bH=&bw^FP zQj_$U%qeOBOnar9Z+;z&zc!Ob1+MT5Wxh}V~6WbnmGAAr1)+zj<;aT6MtcfxrUvlwJPh)mtPT?(tAjQ4x?lD0U>%ze|3lvac{F96 z6A1l63HeX8_VfU|rK7K8} z0k70zlQ$c*kL{62OEQ4hfi|`g-v8a+R|5&;&3n8Qsm?#_-~s}9sUO-eVA+zFk9k&WgxH@aXyUg?XlY;3 z)6-+KIllWk^`wLO5nYu>h4eO&8i!Z&LNpBkAR@4vr%`V5C*d(2_2oZwFg7GLFE-?z z4OD3t=cC3yZMFwK-XFSVCFlEA#zKX? z=4$3=gtyd4o^%TijWeKdVzPSpgOGj2EPV1mPoLmv&XN660xuSgGHnsN(U9xev61Ij zv!!|il*KR!A?5ox0CGb%i@gZ?+JN>I?EiMS)B&-feviFvX*w*VK~JD7ZRGN@lT4(a z_JbeV9t39cI^g;~w!tspyj?7RRbO#lf=}hVpOmR)ohhB#*(mNvvS%(U=(4st)|i)2 z!-aW0G3sC=N(I0^&oeO(KBimQx!b+Lw__vU$;jH_Ga9_o-|_JlpNHjqKz&-oprkgo zk@3-VH|9$|sUwK7d#Jr@U$r5s>B_0y7e6x-)M2|h5(@dfnG+vHQN#i7#o*K-5$xSJ zZgr${k9^U6>u~-Sg89s0qEyWVs~qDL44Nw?<3Vh%s}StLWCqdivM<#tM1g7~Ha7%(sYV)|sKOSr3x$4G;`bclZu$Tvr09 z3If4cbNl*)|7mW2!`KzFGM6a|o3cBq+iXStnpgw8rmZ|4Eu9u?5&m#)ED0~;X z#_-!?vHxVV6;dp}ODzV=H2x|Gc)Vesa}+!n3SEZe`FyZFyV0Zh7OLTxYT_bNXA%XVG?! zNwu_Rk4FxD=o9V*@iis0B58%;Q3Q;L)76EA5$+cBZBtbj^QE)8#gLNl4+5k7&v-4^ zqw=1~ycK}2CO6P^5(`^Q0-Mc9R1@`$9^~E5aZ?!=oL(PxGnwXHAbB2@aC01#e&5J& z8B^zEI`HoZI*v4b?{akZY9VYF&foEH^wx#VsXbz_iTZ%OcifBF>OfA`AM5cuKMAaS zG@}~|n#}gF!7eMIA&=|?>fiTx3kswPYqEI*TwDOJ@4U74xV{c`kPMo`j<@kQW4X&QL+|m zyX~zY@BGfxrhD6qyY+d@|5M2{f9$!UkGkG!WA*^0ITV|mlZq{i z@p!xMYH^rurUu3yn~&SGT4IT;SF|^1#_q@}cR=!Y-AKflRUeU!Spu~jUuozQr0!cz zz|sWja3scr{k8zXM zh6ptVTkGR=TvdKH9gqeCP3*4}$cPVMploVz<83}3JQ3bmKp~7!1?iSD?H|1l zh4T{SMtKR}?(yyyN#8~TQjHtAxQXEY%H{%U>63UM)%gDuf%HF^{~?P${*z0`-68?0 z#_@5GRO6>B?GY&{npr->!!HZfj+op%Jo49>9vcw=_yB5Cs-?X|2C#?6#{hqWF$xJO<=#PR~RC zr;;k)?Eiw}{1v2W z|LiLB@YwouLE_0)sfn|dJEgCBb;}7TY!w8ZV0eCyW|INNaYmB~zFm=CA;;GDU*N7+2d<0xHC%a8 zFFB;bEsl5ie-HOUn-OdyvMFCL!f~s3LT-O=NyxC*#NeV}vQ;@&>U>c`oHZ zvkXohz06vDh!sF%LMp}Ftq!)s>AmJM1nCMtS7TPLSnYL8cEg$Ks-s84N;_3QAGtlu ziB^LVwBNWo+~xxRe0ou74y(G@C^~`R6MK+yqqf?2n`2|g#v5|cEmZ+N{s4fIHyeHJ z0X!~B!S;qC+b*3KeOUe@vVrGancr$`7FiAYYjKWno~fXx_*p)sTiF|Vowm#UY=UmT zW{?n0?!=y1j!iqTT+PByeZ%iEtN}tM4zW%uW!>3Y;ALg>21D@rhhM=HnS6KNRar~<{EnZp=uYb$?q?dPH zj<11|y}xGcFrmFL%$Ft@(w!NSKOu4srv}Bs0ZFZ;psMJ=QdXVb8d_#Vz zUH{7If`^_Tu`yD6;qs1W&wlcnH&tfQ42M7!Gq{SGt!?Yu3$*~U&_Bnb3xw8dY%pPU z-}@V3@eA}G716QfWmQ6SEyi_?aqQNd(%v4flAN#&qB248bn8i9qmajpHrU#(vnlU$ zC~1X^o@UY>Hz79Jbzk{J!qQx1#`$-{+N&WmWYdy{D%OO@txYn_KC|~M#kD+k1w@BE zsq*ZRnqROE_QUp{rbckfpy~lz4FO`{Gq1br6M!^yEm0U<(9|qdrNikqUI%3@a*_R- z_}2W~J*shfBR{R0BpTdQM9a1`+V~=+8M@Hw>B!NXt#MaJnc)^oGps)QU*Q(&>U^(_t+p~@hV-CF>Od}vfF!Xp* zv!DaZote|b2UD`4VhpsE`+>F=GV1cR@^D0{z?1@2-Ck?SOkH=eksAyKh}o4)O=@Sc zXX-BbB8GwNtgF5ow2^&bB=)YV+ToGLd1NzN95A#Jz%1RW6pt<=stZEy4RuLDJURI1#c zW%DrM00Hkd{bd7{4VLBFE}_T0p_v$z2TasFh`^Liucj9Fw#W3~Jj8WEI3o#UigvSJ z;+yfhQX`eZ z9~e(*d4`p}?@;V4s-1SsYxEMQ62q3gKr~&!ur;=}LWW>-sP6iklsIwpMsBzR*h)34 zMu0qA=DIY@h&+n81+B>3=r#sRE9G0IqNZy0V;2yE4otwk2efHofm__f4`9t3A0a_+ zABE5zN87iLpPwT+Fu$HGtV}(CHZmJ0FcZgW54I_#LoKoEd(B&F_&ve zK>?}q+)g`ho;-nL5EbB405BHF4g=05{IJI=oGuIG-mvD+_4GXU3)9~K&C8&Zzy71X z^pgV64*Z*^p);72+>kT=M6EeKPWS{nyt6D7l?rlzU~U~|JNM_$$xef-1#u6G}?aSX>0qqvbySP zqc1G){Nlr(E{`4K!Ohn5tYj$bFwy6I+S%R;(v734`3tfH!3S2wx)Poph=$>C;^BDa zB=AYZYVU|%qvFD}w4ibc5EBreDOeV5L z6TEVoHyw0d|4(cm`4UQC9l@=ps%2Frv5$8Yz}}b$K#*>tr%HZ%!C|faTiT3Gvpen_)!6_2;EeS9_h+_hQr)AfR~6Py zo#Ut}j8|8VuDd0cA}#*BDdXFjfV^XvzUZib%7(-0@8x+45e0XIVn`2|H>ZK)v1zKR z4bjQ5VEN5$QyCGEnz6;mhj1UA&+ypLwoH&MzssO`z@U}mmGIq$e$faS+~8<(G}piY zNyxu`F+ij#2^BkUT!An#7yZSh;1Y^67;zRnAtJ)bL4(8co(g^+FUA9>1Ld_qhVc_nD zdYXf}P|&g&RlhqkiOw`ey!;ld&Nbc4-+63%KUMWz*qcI6F*RYS9FNgTo){{orC00{ z(aE9y6OO9;EJC0DQ7ivbxypuAKu81B--}bWsrH4UdFVSHdigTh!%>lxsxuu9 zi2S4=W(y$cbyB+jRMi6hmTjoKVq|hN*Vv8ano}-kiF+wRx7|yn~|xluTKT42_i&qX27wjNuRzNc>YY9T0}c zEI!<=4xE{SzD}T=7yYgbJ+_5zn71(+9B{q7tgRHb>ErbhxCwjl4#yEFJU6g}4mZKGSI582!AQ-+ zti3n4X*%R+riTg0E56ux%oY4Hf3q#&Gy9qDTyBFUeJ^k(LOHYPP4{Hsl5eBhK&B<{UuekTIQ=~2g^7+mDsE>}`NdmWpp55iP4*eYmTg7(7k z9iaX|Q&RIvKbBouqe)v>r0+*#QvFH_Q{&H1prc^ycXk1eO_ZOt{~W@}-r_*ovZ2*k z=L5?7O{joYtr6M2Z!dFae14pQTz__)%7NOA<#`W^`meiu=L|o5MS{ErrFSBYUS}xV5HwW;D1`aw zKY`S(yUy<%k$MkA-W#6+$(E|VzQJRu{fQ=@-L%JM(FI;Z<-|Uf$?nIWVGURaPgy7B zGROYup-Mb8CjBtt&{f~X^jL@3$$(i-evuV9@$vCEKut+GDz@o=e7KY-uumYSI`s47 zdv7fS)Dhzbv)6xteSpe@W5Z%C{ShZkjs)rm&H--sV_+I^Y1(VS{0H9n6+0QspKEx- z;Q9h^>h=EUhKS_QrvRq%#~c1Bnza~I+)XQVPV#%WN9Oqh)C&o3x4IWd$qSECzIU1c ziIVMh@ZRpR=D`9L(n4o>H|*ir1LG!8*B0D{zCFIUcZ@*6goZYDdVDdG@c_%cz34WW p{P^2Gz-z_BX0QKK_3B{mP!31k`4jk3`JVuPL Date: Fri, 24 Nov 2023 10:38:26 -0500 Subject: [PATCH 10/13] move blueprints under datapipelines/cdk --- .../data_pipeline_blueprint/.gitignore | 0 .../data_pipeline_blueprint/README.md | 0 .../blueprints/data_pipeline_blueprint/app.py | 0 .../data_pipeline_blueprint/cdk.json | 0 .../dataall_pipeline_app/__init__.py | 0 .../dataall_pipeline_app_stack.py | 0 .../requirements-dev.txt | 0 .../data_pipeline_blueprint/requirements.txt | 0 .../data_pipeline_blueprint/source.bat | 0 .../data_pipeline_blueprint/tests/__init__.py | 0 .../tests/unit/__init__.py | 0 .../unit/test_dataall_pipeline_app_stack.py | 0 ...datapipelines_cdk_cli_wrapper_extension.py | 2 +- .../cdk/datapipelines_cdk_pipeline.py | 22 +++++++++---------- .../cdk/datapipelines_pipeline.py | 7 +++--- 15 files changed, 14 insertions(+), 17 deletions(-) rename backend/dataall/modules/datapipelines/{ => cdk}/blueprints/data_pipeline_blueprint/.gitignore (100%) rename backend/dataall/modules/datapipelines/{ => cdk}/blueprints/data_pipeline_blueprint/README.md (100%) rename backend/dataall/modules/datapipelines/{ => cdk}/blueprints/data_pipeline_blueprint/app.py (100%) rename backend/dataall/modules/datapipelines/{ => cdk}/blueprints/data_pipeline_blueprint/cdk.json (100%) rename backend/dataall/modules/datapipelines/{ => cdk}/blueprints/data_pipeline_blueprint/dataall_pipeline_app/__init__.py (100%) rename backend/dataall/modules/datapipelines/{ => cdk}/blueprints/data_pipeline_blueprint/dataall_pipeline_app/dataall_pipeline_app_stack.py (100%) rename backend/dataall/modules/datapipelines/{ => cdk}/blueprints/data_pipeline_blueprint/requirements-dev.txt (100%) rename backend/dataall/modules/datapipelines/{ => cdk}/blueprints/data_pipeline_blueprint/requirements.txt (100%) rename backend/dataall/modules/datapipelines/{ => cdk}/blueprints/data_pipeline_blueprint/source.bat (100%) rename backend/dataall/modules/datapipelines/{ => cdk}/blueprints/data_pipeline_blueprint/tests/__init__.py (100%) rename backend/dataall/modules/datapipelines/{ => cdk}/blueprints/data_pipeline_blueprint/tests/unit/__init__.py (100%) rename backend/dataall/modules/datapipelines/{ => cdk}/blueprints/data_pipeline_blueprint/tests/unit/test_dataall_pipeline_app_stack.py (100%) diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/.gitignore b/backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/.gitignore similarity index 100% rename from backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/.gitignore rename to backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/.gitignore diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/README.md b/backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/README.md similarity index 100% rename from backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/README.md rename to backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/README.md diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/app.py b/backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/app.py similarity index 100% rename from backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/app.py rename to backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/app.py diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/cdk.json b/backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/cdk.json similarity index 100% rename from backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/cdk.json rename to backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/cdk.json diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/dataall_pipeline_app/__init__.py b/backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/dataall_pipeline_app/__init__.py similarity index 100% rename from backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/dataall_pipeline_app/__init__.py rename to backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/dataall_pipeline_app/__init__.py diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/dataall_pipeline_app/dataall_pipeline_app_stack.py b/backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/dataall_pipeline_app/dataall_pipeline_app_stack.py similarity index 100% rename from backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/dataall_pipeline_app/dataall_pipeline_app_stack.py rename to backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/dataall_pipeline_app/dataall_pipeline_app_stack.py diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/requirements-dev.txt b/backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/requirements-dev.txt similarity index 100% rename from backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/requirements-dev.txt rename to backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/requirements-dev.txt diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/requirements.txt b/backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/requirements.txt similarity index 100% rename from backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/requirements.txt rename to backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/requirements.txt diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/source.bat b/backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/source.bat similarity index 100% rename from backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/source.bat rename to backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/source.bat diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/tests/__init__.py b/backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/tests/__init__.py similarity index 100% rename from backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/tests/__init__.py rename to backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/tests/__init__.py diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/tests/unit/__init__.py b/backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/tests/unit/__init__.py similarity index 100% rename from backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/tests/unit/__init__.py rename to backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/tests/unit/__init__.py diff --git a/backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/tests/unit/test_dataall_pipeline_app_stack.py b/backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/tests/unit/test_dataall_pipeline_app_stack.py similarity index 100% rename from backend/dataall/modules/datapipelines/blueprints/data_pipeline_blueprint/tests/unit/test_dataall_pipeline_app_stack.py rename to backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/tests/unit/test_dataall_pipeline_app_stack.py diff --git a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_cli_wrapper_extension.py b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_cli_wrapper_extension.py index 01b56839c..5074a398a 100644 --- a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_cli_wrapper_extension.py +++ b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_cli_wrapper_extension.py @@ -18,7 +18,7 @@ def extend_deployment(self, stack, session, env): cdkpipeline = CDKPipelineStack(stack.targetUri) is_create = cdkpipeline.is_create if cdkpipeline.is_create else None self.pipeline = DatapipelinesRepository.get_pipeline_by_uri(session, stack.targetUri) - path = f'/dataall/modules/datapipelines/blueprints/{self.pipeline.repo}/' + path = f'/dataall/modules/datapipelines/cdk/blueprints/{self.pipeline.repo}/' app_path = './app.py' if not is_create: logger.info('Successfully Updated CDK Pipeline') diff --git a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py index df3e0419e..b0ca682a3 100644 --- a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py +++ b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py @@ -48,10 +48,9 @@ def __init__(self, target_uri): self.env, aws = CDKPipelineStack._set_env_vars(self.pipeline_environment) self.code_dir_path = os.path.realpath( - os.path.abspath( - os.path.join( - __file__, "..", "..", "blueprints" - ) + os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "blueprints" ) ) self.is_create = True @@ -61,10 +60,10 @@ def __init__(self, target_uri): if repository: self.is_create = False self.code_dir_path = os.path.realpath( - os.path.abspath( - os.path.join( - __file__, "..", "..", "blueprints", "data_pipeline_blueprint" - ) + os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "blueprints", + "data_pipeline_blueprint" ) ) CDKPipelineStack.write_ddk_json_multienvironment(path=os.path.join(self.code_dir_path, self.pipeline.repo), output_file="ddk.json", pipeline_environment=self.pipeline_environment, development_environments=self.development_environments, pipeline_name=self.pipeline.name) @@ -239,10 +238,9 @@ def git_push_repo(self): def clean_up_repo(pipeline_dir): if pipeline_dir: code_dir_path = os.path.realpath( - os.path.abspath( - os.path.join( - __file__, "..", "..", "blueprints" - ) + os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "blueprints" ) ) diff --git a/backend/dataall/modules/datapipelines/cdk/datapipelines_pipeline.py b/backend/dataall/modules/datapipelines/cdk/datapipelines_pipeline.py index d36aa9191..12a4a7818 100644 --- a/backend/dataall/modules/datapipelines/cdk/datapipelines_pipeline.py +++ b/backend/dataall/modules/datapipelines/cdk/datapipelines_pipeline.py @@ -167,10 +167,9 @@ def __init__(self, scope, id, target_uri: str = None, **kwargs): # Create CodeCommit repository and mirror blueprint code code_dir_path = os.path.realpath( - os.path.abspath( - os.path.join( - __file__, "..", "..", "blueprints" - ) + os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "blueprints" ) ) logger.info(f"code directory path = {code_dir_path}") From b15fea06aefd6f775bb76ce726166f0c035cfc58 Mon Sep 17 00:00:00 2001 From: Noah Paige Date: Fri, 24 Nov 2023 10:45:03 -0500 Subject: [PATCH 11/13] remove hardcoded path cdk pipeline extension --- .../cdk/datapipelines_cdk_cli_wrapper_extension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_cli_wrapper_extension.py b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_cli_wrapper_extension.py index 5074a398a..d8ef76423 100644 --- a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_cli_wrapper_extension.py +++ b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_cli_wrapper_extension.py @@ -18,7 +18,7 @@ def extend_deployment(self, stack, session, env): cdkpipeline = CDKPipelineStack(stack.targetUri) is_create = cdkpipeline.is_create if cdkpipeline.is_create else None self.pipeline = DatapipelinesRepository.get_pipeline_by_uri(session, stack.targetUri) - path = f'/dataall/modules/datapipelines/cdk/blueprints/{self.pipeline.repo}/' + path = f'{cdkpipeline.code_dir_path}/{self.pipeline.repo}/' app_path = './app.py' if not is_create: logger.info('Successfully Updated CDK Pipeline') From 5296d1884d4467bcc97e7fb6416b1e859e281974 Mon Sep 17 00:00:00 2001 From: Noah Paige Date: Fri, 24 Nov 2023 10:46:17 -0500 Subject: [PATCH 12/13] lint --- .../cdk/datapipelines_cdk_pipeline.py | 14 +++++++------- .../datapipelines/cdk/datapipelines_pipeline.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py index b0ca682a3..a85fbf399 100644 --- a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py +++ b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py @@ -49,8 +49,8 @@ def __init__(self, target_uri): self.code_dir_path = os.path.realpath( os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "blueprints" + os.path.dirname(os.path.abspath(__file__)), + "blueprints" ) ) self.is_create = True @@ -61,9 +61,9 @@ def __init__(self, target_uri): self.is_create = False self.code_dir_path = os.path.realpath( os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "blueprints", - "data_pipeline_blueprint" + os.path.dirname(os.path.abspath(__file__)), + "blueprints", + "data_pipeline_blueprint" ) ) CDKPipelineStack.write_ddk_json_multienvironment(path=os.path.join(self.code_dir_path, self.pipeline.repo), output_file="ddk.json", pipeline_environment=self.pipeline_environment, development_environments=self.development_environments, pipeline_name=self.pipeline.name) @@ -239,8 +239,8 @@ def clean_up_repo(pipeline_dir): if pipeline_dir: code_dir_path = os.path.realpath( os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "blueprints" + os.path.dirname(os.path.abspath(__file__)), + "blueprints" ) ) diff --git a/backend/dataall/modules/datapipelines/cdk/datapipelines_pipeline.py b/backend/dataall/modules/datapipelines/cdk/datapipelines_pipeline.py index 12a4a7818..ea8d34f3e 100644 --- a/backend/dataall/modules/datapipelines/cdk/datapipelines_pipeline.py +++ b/backend/dataall/modules/datapipelines/cdk/datapipelines_pipeline.py @@ -168,8 +168,8 @@ def __init__(self, scope, id, target_uri: str = None, **kwargs): # Create CodeCommit repository and mirror blueprint code code_dir_path = os.path.realpath( os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "blueprints" + os.path.dirname(os.path.abspath(__file__)), + "blueprints" ) ) logger.info(f"code directory path = {code_dir_path}") From e761d7349d98ae6bf8c5669300f6499f9641e3d4 Mon Sep 17 00:00:00 2001 From: Noah Paige Date: Mon, 27 Nov 2023 09:50:37 -0500 Subject: [PATCH 13/13] Add description to cdkpipeline stack and envname to datapipeline deployment stack --- .../datapipelines/cdk/blueprints/data_pipeline_blueprint/app.py | 2 +- .../modules/datapipelines/cdk/datapipelines_cdk_pipeline.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/app.py b/backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/app.py index 0ef694f25..21ce3aea2 100644 --- a/backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/app.py +++ b/backend/dataall/modules/datapipelines/cdk/blueprints/data_pipeline_blueprint/app.py @@ -10,7 +10,7 @@ DataallPipelineStack( app, - f"{pipeline_name}-DataallPipelineStack", + f"{pipeline_name}-{environment_id}-DataallPipelineStack", environment_id ) diff --git a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py index a85fbf399..75167db66 100644 --- a/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py +++ b/backend/dataall/modules/datapipelines/cdk/datapipelines_cdk_pipeline.py @@ -183,6 +183,7 @@ def __init__( app, id=id, pipeline_name="{pipeline.name}", + description="Cloud formation stack of PIPELINE: {pipeline.label}; URI: {pipeline.DataPipelineUri}; DESCRIPTION: {pipeline.description}", cdk_language="python", env=ddk.Configurator.get_environment( config_path="./ddk.json", environment_id="cicd"