docs: def has_stack(self, stack_name):
          """
          Checks if a CloudFormation stack with given name exists
          :param stack_name: Name or ID of the stack
          :return: True if stack exists. False otherwise
          """
          cf = self.cf_client
          try:
              resp = cf.describe_stacks(StackName=stack_name)
              if len(resp["Stacks"]) != 1:
                  return False
  
              # When you run CreateChangeSet on a a stack that does not exist,
              # CloudFormation will create a stack and set it's status
              # REVIEW_IN_PROGRESS. However this stack is cannot be manipulated
              # by "update" commands. Under this circumstances, we treat like
              # this stack does not exist and call CreateChangeSet will
              # ChangeSetType set to CREATE and not UPDATE.
              stack = resp["Stacks"][0]
              return stack["StackStatus"] != "REVIEW_IN_PROGRESS"
  
          except botocore.exceptions.ClientError as e:
              # If a stack does not exist, describe_stacks will throw an
              # exception. Unfortunately we don't have a better way than parsing
              # the exception msg to understand the nature of this exception.
              msg = str(e)
  
              if "Stack with id {0} does not exist".format(stack_name) in msg:
                  LOG.debug("Stack with id {0} does not exist".format(
                      stack_name))
                  return False
              else:
                  # We don't know anything about this exception. Don't handle
                  LOG.debug("Unable to get stack details.", exc_info=e)
                  raise e