<commit_before>from datetime import timedelta

import boto3
from botocore.exceptions import BotoCoreError, ClientError

from cfn_sphere.exceptions import CfnStackActionFailedException
from cfn_sphere.util import *

logging.getLogger('boto').setLevel(logging.FATAL)


class CloudFormationStack(object):
    def __init__(self, template, parameters, name, region, timeout=600, tags=None):
        self.template = template
        self.parameters = parameters
        self.tags = {} if tags is None else tags
        self.name = name
        self.region = region
        self.timeout = timeout

    def __str__(self):
        return str(vars(self))

    def get_parameters_list(self):
        return [{"ParameterKey": str(key), "ParameterValue": str(value)} for key, value in self.parameters.items()]

    def get_tags_list(self):
        return [{"Key": key, "Value": value} for key, value in self.tags.items()]


class CloudFormation(object):
    @with_boto_retry()
    def __init__(self, region="eu-west-1"):
        self.logger = get_logger()
        self.client = boto3.client('cloudformation', region_name=region)
        self.resource = boto3.resource('cloudformation', region_name=region)

    def get_stack(self, stack_name):
        """
        Get stack resource representation for a given stack_name
        This doesn't actually call the AWS API but only creates a stack stub lazy loading it's content
        :param stack_name: str:
        :return: boto3.resources.factory.cloudformation.Stack
        :raise CfnSphereBotoError:
        """
        return self.resource.Stack(stack_name)

    @timed
    @with_boto_retry()
    def get_stacks(self):
        """
        Get all stacks
        :return: List(boto3.resources.factory.cloudformation.Stack)
        :raise CfnSphereBotoError:
        """
        try:
            return list(self.resource.stacks.all())
        except (BotoCoreError, ClientError) as e:
            raise CfnSphereBotoError(e)

    @timed
    @with_boto_retry()
    def get_stack_description(self, stack_name):
        """
        Get a stacks descriptions
        :param stack_name: string
        :return dict
        :raise CfnSphereBotoError:
        """
        try:
            return self.client.describe_stacks(StackName=stack_name)["Stacks"][0]
        except (BotoCoreError, ClientError) as e:
            raise CfnSphereBotoError(e)

    @timed
    @with_boto_retry()
    def get_stack_descriptions(self):
        """
        Get all stacks stack descriptions
        :return List(dict)
        :raise CfnSphereBotoError:
        """
        try:
            stacks = []
            for page in self.client.get_paginator('describe_stacks').paginate():
                stacks += page["Stacks"]
            return stacks
        except (BotoCoreError, ClientError) as e:
            raise CfnSphereBotoError(e)

    @with_boto_retry()
    def stack_exists(self, stack_name):
        """
        Check if a stack exists for given stack_name

        :param stack_name: str
        :return: bool
        """
        try:
            if self.get_stack(stack_name).stack_status:
                return True
            else:
                return False
        except (BotoCoreError, ClientError) as e:
            if self.is_boto_stack_does_not_exist_exception(e):
                return False
            else:
                raise CfnSphereBotoError(e)

    @with_boto_retry()
    def get_stack_events(self, stack_name):
        """
        Get recent stack events for a given stack_name
        :param stack_name: str
        :return: list(dict)
        """
        try:
            paginator = self.client.get_paginator('describe_stack_events')
            pages = paginator.paginate(StackName=stack_name, PaginationConfig={'MaxItems': 100})
            return next(iter(pages))["StackEvents"]
        except (BotoCoreError, ClientError) as e:
            raise CfnSphereBotoError(e)

    @timed
    def get_stack_names(self):
        """
        Get a list of stack names
        :return: list(str)
        """
        return [stack.stack_name for stack in self.get_stacks()]

    @timed
    def get_stacks_dict(self):
        """
        Get a dict containing all stacks with their name as key and {parameters, outputs} as value
        :return: dict
        """
        stacks_dict = {}
        for stack in self.get_stack_descriptions():
            stacks_dict[stack["StackName"]] = {"parameters": stack.get("Parameters", []),
                                               "outputs": stack.get("Outputs", [])}
        return stacks_dict

    def get_stack_outputs(self, stack):
        """
        Get outputs for a specific stack
        :param stack: cfn_sphere.aws.cfn.CloudFormationStack
        :return: list(dict)
        """
        return self.get_stack_description(stack.name).get("Outputs", [])

    def get_stacks_outputs(self):
        """
        Get a dict of all available stack outputs
        :return: dict(dict(output-key, output-value))
        """
        stack_outputs = {}
        stack_descriptions = self.get_stack_descriptions()

        for stack_description in stack_descriptions:

            if stack_description.get("Outputs"):
                stack_name = stack_description["StackName"]
                outputs = {}

                for output in stack_description["Outputs"]:
                    key = output["OutputKey"]
                    value = output["OutputValue"]
                    outputs[key] = value

                stack_outputs[stack_name] = outputs

        return stack_outputs

    def validate_stack_is_ready_for_action(self, stack):
        """
        Check if a stack is in a state capable for modification actions

        :param stack: cfn_sphere.aws.cfn.CloudFormationStack
        :raise CfnStackActionFailedException: if the stack is in an invalid state
        """
        cfn_stack = self.get_stack(stack.name)

        valid_states = ["CREATE_COMPLETE", "UPDATE_COMPLETE", "ROLLBACK_COMPLETE", "UPDATE_ROLLBACK_COMPLETE"]

        if cfn_stack.stack_status not in valid_states:
            raise CfnStackActionFailedException(
                "Stack {0} is in '{1}' state.".format(cfn_stack.stack_name, cfn_stack.stack_status))

    def get_stack_state(self, stack_name):
        """
        Get stack status
        :param stack_name: str
        :return: str: stack status
        :raise CfnSphereBotoError:
        """
        return self.get_stack(stack_name).stack_status

    def get_stack_parameters_dict(self, stack_name):
        """
        Get a stacks parameters
        :param stack_name: str
        :return: dict
        """
        parameters = {}
        stack = self.get_stack(stack_name)

        for parameter in stack.parameters:<commit_msg>#113 boto3 resource calls return iterators lazy loading data on access, this requires the boto retry mechanism to also cover functions using these resource iterators since they are really calling the aws api<commit_after>from datetime import timedelta

import boto3
from botocore.exceptions import BotoCoreError, ClientError

from cfn_sphere.exceptions import CfnStackActionFailedException
from cfn_sphere.util import *

logging.getLogger('boto').setLevel(logging.FATAL)


class CloudFormationStack(object):
    def __init__(self, template, parameters, name, region, timeout=600, tags=None):
        self.template = template
        self.parameters = parameters
        self.tags = {} if tags is None else tags
        self.name = name
        self.region = region
        self.timeout = timeout

    def __str__(self):
        return str(vars(self))

    def get_parameters_list(self):
        return [{"ParameterKey": str(key), "ParameterValue": str(value)} for key, value in self.parameters.items()]

    def get_tags_list(self):
        return [{"Key": key, "Value": value} for key, value in self.tags.items()]


class CloudFormation(object):
    @with_boto_retry()
    def __init__(self, region="eu-west-1"):
        self.logger = get_logger()
        self.client = boto3.client('cloudformation', region_name=region)
        self.resource = boto3.resource('cloudformation', region_name=region)

    def get_stack(self, stack_name):
        """
        Get stack resource representation for a given stack_name
        This doesn't actually call the AWS API but only creates a stack stub lazy loading it's content
        :param stack_name: str:
        :return: boto3.resources.factory.cloudformation.Stack
        :raise CfnSphereBotoError:
        """
        return self.resource.Stack(stack_name)

    @timed
    @with_boto_retry()
    def get_stacks(self):
        """
        Get all stacks
        :return: List(boto3.resources.factory.cloudformation.Stack)
        :raise CfnSphereBotoError:
        """
        try:
            return list(self.resource.stacks.all())
        except (BotoCoreError, ClientError) as e:
            raise CfnSphereBotoError(e)

    @timed
    @with_boto_retry()
    def get_stack_description(self, stack_name):
        """
        Get a stacks descriptions
        :param stack_name: string
        :return dict
        :raise CfnSphereBotoError:
        """
        try:
            return self.client.describe_stacks(StackName=stack_name)["Stacks"][0]
        except (BotoCoreError, ClientError) as e:
            raise CfnSphereBotoError(e)

    @timed
    @with_boto_retry()
    def get_stack_descriptions(self):
        """
        Get all stacks stack descriptions
        :return List(dict)
        :raise CfnSphereBotoError:
        """
        try:
            stacks = []
            for page in self.client.get_paginator('describe_stacks').paginate():
                stacks += page["Stacks"]
            return stacks
        except (BotoCoreError, ClientError) as e:
            raise CfnSphereBotoError(e)

    @with_boto_retry()
    def stack_exists(self, stack_name):
        """
        Check if a stack exists for given stack_name

        :param stack_name: str
        :return: bool
        """
        try:
            if self.get_stack(stack_name).stack_status:
                return True
            else:
                return False
        except (BotoCoreError, ClientError) as e:
            if self.is_boto_stack_does_not_exist_exception(e):
                return False
            else:
                raise CfnSphereBotoError(e)

    @with_boto_retry()
    def get_stack_events(self, stack_name):
        """
        Get recent stack events for a given stack_name
        :param stack_name: str
        :return: list(dict)
        """
        try:
            paginator = self.client.get_paginator('describe_stack_events')
            pages = paginator.paginate(StackName=stack_name, PaginationConfig={'MaxItems': 100})
            return next(iter(pages))["StackEvents"]
        except (BotoCoreError, ClientError) as e:
            raise CfnSphereBotoError(e)

    @timed
    @with_boto_retry()
    def get_stack_names(self):
        """
        Get a list of stack names
        :return: list(str)
        """
        return [stack.stack_name for stack in self.get_stacks()]

    @timed
    def get_stacks_dict(self):
        """
        Get a dict containing all stacks with their name as key and {parameters, outputs} as value
        :return: dict
        """
        stacks_dict = {}
        for stack in self.get_stack_descriptions():
            stacks_dict[stack["StackName"]] = {"parameters": stack.get("Parameters", []),
                                               "outputs": stack.get("Outputs", [])}
        return stacks_dict

    def get_stack_outputs(self, stack):
        """
        Get outputs for a specific stack
        :param stack: cfn_sphere.aws.cfn.CloudFormationStack
        :return: list(dict)
        """
        return self.get_stack_description(stack.name).get("Outputs", [])

    def get_stacks_outputs(self):
        """
        Get a dict of all available stack outputs
        :return: dict(dict(output-key, output-value))
        """
        stack_outputs = {}
        stack_descriptions = self.get_stack_descriptions()

        for stack_description in stack_descriptions:

            if stack_description.get("Outputs"):
                stack_name = stack_description["StackName"]
                outputs = {}

                for output in stack_description["Outputs"]:
                    key = output["OutputKey"]
                    value = output["OutputValue"]
                    outputs[key] = value

                stack_outputs[stack_name] = outputs

        return stack_outputs

    @with_boto_retry()
    def validate_stack_is_ready_for_action(self, stack):
        """
        Check if a stack is in a state capable for modification actions

        :param stack: cfn_sphere.aws.cfn.CloudFormationStack
        :raise CfnStackActionFailedException: if the stack is in an invalid state
        """
        cfn_stack = self.get_stack(stack.name)

        valid_states = ["CREATE_COMPLETE", "UPDATE_COMPLETE", "ROLLBACK_COMPLETE", "UPDATE_ROLLBACK_COMPLETE"]

        if cfn_stack.stack_status not in valid_states:
            raise CfnStackActionFailedException(
                "Stack {0} is in '{1}' state.".format(cfn_stack.stack_name, cfn_stack.stack_status))

    @with_boto_retry()
    def get_stack_state(self, stack_name):
        """
        Get stack status
        :param stack_name: str
        :return: str: stack status
        :raise CfnSphereBotoError:
        """
        return self.get_stack(stack_name).stack_status

    @with_boto_retry()
    def get_stack_parameters_dict(self, stack_name):
        """
        Get a stacks parameters
        :param stack_name: str
        :return: dict
        """
        parameters = {}
        stack = self.get_stack(stack_name)

        for parameter in stack.parameters:
{'fraction_non_alphanumeric': 0.09185441941074524, 'fraction_numerical': 0.002455228191796649, 'mean_word_length': 2.2809760720208483, 'pattern_counts': {'":': 12, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4136688', 'n_tokens_mistral': 3968, 'n_tokens_neox': 3872, 'n_words': 959}