noqdev__iambic METASEP

docs/schemas/variable.md METASEP
# Variable

## Properties

- **`key`** *(string)*
- **`value`** *(string)*

docs/schemas/role_template.md METASEP
# RoleTemplate

## Properties

- **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
- **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
  - **Any of**
    - *boolean*
    - *array*
      - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
- **`template_type`** *(string)*: Default: `"NOQ::IAM::MultiAccountRole"`.
- **`file_path`** *(string)*
- **`read_only`** *(boolean)*: If set to True, iambic will only log drift instead of apply changes when drift is detected. Default: `false`.
- **`role_name`** *(string)*: Name of the role.
- **`description`**: Description of the role. Default: `""`.
  - **Any of**
    - *string*
    - *array*
      - **Items**: Refer to *[#/definitions/Description](#definitions/Description)*.
- **`owner`** *(string)*
- **`max_session_duration`**: Default: `3600`.
  - **Any of**
    - *integer*
    - *array*
      - **Items**: Refer to *[#/definitions/MaxSessionDuration](#definitions/MaxSessionDuration)*.
- **`path`**: Default: `"/"`.
  - **Any of**
    - *string*
    - *array*
      - **Items**: Refer to *[#/definitions/Path](#definitions/Path)*.
- **`permissions_boundary`**
  - **Any of**
    - : Refer to *[#/definitions/PermissionBoundary](#definitions/PermissionBoundary)*.
    - *array*
      - **Items**: Refer to *[#/definitions/PermissionBoundary](#definitions/PermissionBoundary)*.
- **`role_access`** *(array)*: List of users and groups who can assume into the role. Default: `[]`.
  - **Items**: Refer to *[#/definitions/RoleAccess](#definitions/RoleAccess)*.
- **`assume_role_policy_document`**
  - **Any of**
    - : Refer to *[#/definitions/AssumeRolePolicyDocument](#definitions/AssumeRolePolicyDocument)*.
    - *array*
      - **Items**: Refer to *[#/definitions/AssumeRolePolicyDocument](#definitions/AssumeRolePolicyDocument)*.
- **`tags`** *(array)*: List of tags attached to the role. Default: `[]`.
  - **Items**: Refer to *[#/definitions/Tag](#definitions/Tag)*.
- **`managed_policies`** *(array)*: Managed policy arns attached to the role. Default: `[]`.
  - **Items**: Refer to *[#/definitions/ManagedPolicyRef](#definitions/ManagedPolicyRef)*.
- **`inline_policies`** *(array)*: List of the role's inline policies. Default: `[]`.
  - **Items**: Refer to *[#/definitions/PolicyDocument](#definitions/PolicyDocument)*.
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.
- <a id="definitions/Description"></a>**`Description`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`description`** *(string)*: Default: `""`.
- <a id="definitions/MaxSessionDuration"></a>**`MaxSessionDuration`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`max_session_duration`** *(integer)*
- <a id="definitions/Path"></a>**`Path`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`file_path`** *(string)*
- <a id="definitions/PermissionBoundary"></a>**`PermissionBoundary`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`permissions_boundary_type`** *(string)*
  - **`permissions_boundary_arn`** *(string)*
- <a id="definitions/RoleAccess"></a>**`RoleAccess`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`users`** *(array)*: List of users who can assume into the role. Default: `[]`.
    - **Items** *(string)*
  - **`groups`** *(array)*: List of groups. Users in one or more of the groups can assume into the role. Default: `[]`.
    - **Items** *(string)*
- <a id="definitions/Principal"></a>**`Principal`** *(object)*
  - **`aws`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`service`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`canonical_user`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`federated`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
- <a id="definitions/PolicyStatement"></a>**`PolicyStatement`** *(object)*
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`effect`** *(string)*: Allow | Deny.
  - **`principal`**
    - **Any of**
      - : Refer to *[#/definitions/Principal](#definitions/Principal)*.
      - *string*
  - **`not_principal`**
    - **Any of**
      - : Refer to *[#/definitions/Principal](#definitions/Principal)*.
      - *string*
  - **`action`**: A single regex or list of regexes. Values are the actions that can be performed on the resources in the policy statement.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`not_resource`**: A single regex or list of regexes. Values specified are the resources the statement applies to.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`not_action`**: An advanced policy element that explicitly matches everything except the specified list of actions.DON'T use this with effect: allow in the same statement OR policy.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`resource`**: A single regex or list of regexes. Values specified are the resources the statement applies to.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`condition`** *(object)*: An optional set of conditions to determine of the policy applies to a resource.
  - **`sid`** *(string)*: The Policy Statement ID.
- <a id="definitions/AssumeRolePolicyDocument"></a>**`AssumeRolePolicyDocument`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`version`** *(string)*
  - **`statement`** *(array)*
    - **Items**: Refer to *[#/definitions/PolicyStatement](#definitions/PolicyStatement)*.
- <a id="definitions/Tag"></a>**`Tag`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`key`** *(string)*
  - **`value`** *(string)*
- <a id="definitions/ManagedPolicyRef"></a>**`ManagedPolicyRef`** *(object)*
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`policy_arn`** *(string)*
- <a id="definitions/PolicyDocument"></a>**`PolicyDocument`** *(object)*
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`policy_name`** *(string)*: The name of the policy.
  - **`version`** *(string)*
  - **`statement`** *(array)*: List of policy statements.
    - **Items**: Refer to *[#/definitions/PolicyStatement](#definitions/PolicyStatement)*.

docs/schemas/role_access.md METASEP
# RoleAccess

## Properties

- **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
- **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
  - **Any of**
    - *boolean*
    - *array*
      - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
- **`users`** *(array)*: List of users who can assume into the role. Default: `[]`.
  - **Items** *(string)*
- **`groups`** *(array)*: List of groups. Users in one or more of the groups can assume into the role. Default: `[]`.
  - **Items** *(string)*
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.

docs/schemas/policy_statement.md METASEP
# PolicyStatement

## Properties

- **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
- **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
  - **Any of**
    - *boolean*
    - *array*
      - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
- **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`effect`** *(string)*: Allow | Deny.
- **`principal`**
  - **Any of**
    - : Refer to *[#/definitions/Principal](#definitions/Principal)*.
    - *string*
- **`not_principal`**
  - **Any of**
    - : Refer to *[#/definitions/Principal](#definitions/Principal)*.
    - *string*
- **`action`**: A single regex or list of regexes. Values are the actions that can be performed on the resources in the policy statement.
  - **Any of**
    - *array*
      - **Items** *(string)*
    - *string*
- **`not_resource`**: A single regex or list of regexes. Values specified are the resources the statement applies to.
  - **Any of**
    - *array*
      - **Items** *(string)*
    - *string*
- **`not_action`**: An advanced policy element that explicitly matches everything except the specified list of actions.DON'T use this with effect: allow in the same statement OR policy.
  - **Any of**
    - *array*
      - **Items** *(string)*
    - *string*
- **`resource`**: A single regex or list of regexes. Values specified are the resources the statement applies to.
  - **Any of**
    - *array*
      - **Items** *(string)*
    - *string*
- **`condition`** *(object)*: An optional set of conditions to determine of the policy applies to a resource.
- **`sid`** *(string)*: The Policy Statement ID.
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.
- <a id="definitions/Principal"></a>**`Principal`** *(object)*
  - **`aws`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`service`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`canonical_user`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`federated`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*

docs/schemas/policy_document.md METASEP
# PolicyDocument

## Properties

- **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
- **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
  - **Any of**
    - *boolean*
    - *array*
      - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
- **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`policy_name`** *(string)*: The name of the policy.
- **`version`** *(string)*
- **`statement`** *(array)*: List of policy statements.
  - **Items**: Refer to *[#/definitions/PolicyStatement](#definitions/PolicyStatement)*.
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.
- <a id="definitions/Principal"></a>**`Principal`** *(object)*
  - **`aws`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`service`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`canonical_user`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`federated`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
- <a id="definitions/PolicyStatement"></a>**`PolicyStatement`** *(object)*
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`effect`** *(string)*: Allow | Deny.
  - **`principal`**
    - **Any of**
      - : Refer to *[#/definitions/Principal](#definitions/Principal)*.
      - *string*
  - **`not_principal`**
    - **Any of**
      - : Refer to *[#/definitions/Principal](#definitions/Principal)*.
      - *string*
  - **`action`**: A single regex or list of regexes. Values are the actions that can be performed on the resources in the policy statement.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`not_resource`**: A single regex or list of regexes. Values specified are the resources the statement applies to.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`not_action`**: An advanced policy element that explicitly matches everything except the specified list of actions.DON'T use this with effect: allow in the same statement OR policy.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`resource`**: A single regex or list of regexes. Values specified are the resources the statement applies to.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`condition`** *(object)*: An optional set of conditions to determine of the policy applies to a resource.
  - **`sid`** *(string)*: The Policy Statement ID.

docs/schemas/permission_boundary.md METASEP
# PermissionBoundary

## Properties

- **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
- **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
  - **Any of**
    - *boolean*
    - *array*
      - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
- **`permissions_boundary_type`** *(string)*
- **`permissions_boundary_arn`** *(string)*
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.

docs/schemas/path.md METASEP
# Path

## Properties

- **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`file_path`** *(string)*

docs/schemas/max_session_duration.md METASEP
# MaxSessionDuration

## Properties

- **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`max_session_duration`** *(integer)*

docs/schemas/managed_policy_ref.md METASEP
# ManagedPolicyRef

## Properties

- **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
- **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
  - **Any of**
    - *boolean*
    - *array*
      - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
- **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`policy_arn`** *(string)*
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.

docs/schemas/group_template.md METASEP
# GroupTemplate

## Properties

- **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
- **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
  - **Any of**
    - *boolean*
    - *array*
      - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
- **`template_type`** *(string)*: Default: `"NOQ::Google::GroupTemplate"`.
- **`file_path`** *(string)*
- **`read_only`** *(boolean)*: If set to True, iambic will only log drift instead of apply changes when drift is detected. Default: `false`.
- **`name`** *(string)*
- **`email`** *(string)*
- **`description`** *(string)*
- **`welcome_message`** *(string)*
- **`members`** *(array)*
  - **Items**: Refer to *[#/definitions/GroupMember](#definitions/GroupMember)*.
- **`who_can_invite`**: Default: `"ALL_MANAGERS_CAN_INVITE"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanInvite](#definitions/WhoCanInvite)*.
- **`who_can_join`**: Default: `"CAN_REQUEST_TO_JOIN"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanJoin](#definitions/WhoCanJoin)*.
- **`who_can_post_message`**: Default: `"NONE_CAN_POST"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanPostMessage](#definitions/WhoCanPostMessage)*.
- **`who_can_view_group`**: Default: `"ALL_MANAGERS_CAN_VIEW"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanViewGroup](#definitions/WhoCanViewGroup)*.
- **`who_can_view_membership`**: Default: `"ALL_MANAGERS_CAN_VIEW"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanViewMembership](#definitions/WhoCanViewMembership)*.
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.
- <a id="definitions/GroupMemberRole"></a>**`GroupMemberRole`**: An enumeration. Must be one of: `["OWNER", "MANAGER", "MEMBER"]`.
- <a id="definitions/GroupMemberType"></a>**`GroupMemberType`**: An enumeration. Must be one of: `["USER", "GROUP", "EXTERNAL"]`.
- <a id="definitions/GroupMemberStatus"></a>**`GroupMemberStatus`**: An enumeration. Must be one of: `["ACTIVE", "INACTIVE", "PENDING"]`.
- <a id="definitions/GroupMemberSubscription"></a>**`GroupMemberSubscription`**: An enumeration. Must be one of: `["EACH_EMAIL", "DIGEST", "ABRIDGED", "NO_EMAIL"]`.
- <a id="definitions/GroupMember"></a>**`GroupMember`** *(object)*
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`email`** *(string)*
  - **`expand`** *(boolean)*: Expand the group into the members of the group. This is useful for nested groups. Default: `false`.
  - **`role`**: Default: `"MEMBER"`.
    - **All of**
      - : Refer to *[#/definitions/GroupMemberRole](#definitions/GroupMemberRole)*.
  - **`type`**: Default: `"USER"`.
    - **All of**
      - : Refer to *[#/definitions/GroupMemberType](#definitions/GroupMemberType)*.
  - **`status`**: Default: `"ACTIVE"`.
    - **All of**
      - : Refer to *[#/definitions/GroupMemberStatus](#definitions/GroupMemberStatus)*.
  - **`subscription`**: Default: `"EACH_EMAIL"`.
    - **All of**
      - : Refer to *[#/definitions/GroupMemberSubscription](#definitions/GroupMemberSubscription)*.
- <a id="definitions/WhoCanInvite"></a>**`WhoCanInvite`**: An enumeration. Must be one of: `["ALL_MANAGERS_CAN_INVITE", "ALL_MEMBERS_CAN_INVITE"]`.
- <a id="definitions/WhoCanJoin"></a>**`WhoCanJoin`**: An enumeration. Must be one of: `["ALL_IN_DOMAIN_CAN_JOIN", "ANYONE_CAN_JOIN", "CAN_REQUEST_TO_JOIN"]`.
- <a id="definitions/WhoCanPostMessage"></a>**`WhoCanPostMessage`**: An enumeration. Must be one of: `["ALL_IN_DOMAIN_CAN_POST", "ALL_MANAGERS_CAN_POST", "ALL_MEMBERS_CAN_POST", "ANYONE_CAN_POST", "NONE_CAN_POST"]`.
- <a id="definitions/WhoCanViewGroup"></a>**`WhoCanViewGroup`**: An enumeration. Must be one of: `["ALL_IN_DOMAIN_CAN_VIEW", "ALL_MANAGERS_CAN_VIEW", "ALL_MEMBERS_CAN_VIEW", "ANYONE_CAN_VIEW"]`.
- <a id="definitions/WhoCanViewMembership"></a>**`WhoCanViewMembership`**: An enumeration. Must be one of: `["ALL_IN_DOMAIN_CAN_VIEW", "ALL_MANAGERS_CAN_VIEW", "ALL_MEMBERS_CAN_VIEW", "ANYONE_CAN_VIEW"]`.

docs/schemas/group_member.md METASEP
# GroupMember

## Properties

- **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
- **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
  - **Any of**
    - *boolean*
    - *array*
      - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
- **`email`** *(string)*
- **`expand`** *(boolean)*: Expand the group into the members of the group. This is useful for nested groups. Default: `false`.
- **`role`**: Default: `"MEMBER"`.
  - **All of**
    - : Refer to *[#/definitions/GroupMemberRole](#definitions/GroupMemberRole)*.
- **`type`**: Default: `"USER"`.
  - **All of**
    - : Refer to *[#/definitions/GroupMemberType](#definitions/GroupMemberType)*.
- **`status`**: Default: `"ACTIVE"`.
  - **All of**
    - : Refer to *[#/definitions/GroupMemberStatus](#definitions/GroupMemberStatus)*.
- **`subscription`**: Default: `"EACH_EMAIL"`.
  - **All of**
    - : Refer to *[#/definitions/GroupMemberSubscription](#definitions/GroupMemberSubscription)*.
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.
- <a id="definitions/GroupMemberRole"></a>**`GroupMemberRole`**: An enumeration. Must be one of: `["OWNER", "MANAGER", "MEMBER"]`.
- <a id="definitions/GroupMemberType"></a>**`GroupMemberType`**: An enumeration. Must be one of: `["USER", "GROUP", "EXTERNAL"]`.
- <a id="definitions/GroupMemberStatus"></a>**`GroupMemberStatus`**: An enumeration. Must be one of: `["ACTIVE", "INACTIVE", "PENDING"]`.
- <a id="definitions/GroupMemberSubscription"></a>**`GroupMemberSubscription`**: An enumeration. Must be one of: `["EACH_EMAIL", "DIGEST", "ABRIDGED", "NO_EMAIL"]`.

docs/schemas/group.md METASEP
# Group

## Properties

- **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
- **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
  - **Any of**
    - *boolean*
    - *array*
      - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
- **`template_type`** *(string)*: Default: `"NOQ::Google::Group"`.
- **`file_path`** *(string)*
- **`read_only`** *(boolean)*: If set to True, iambic will only log drift instead of apply changes when drift is detected. Default: `false`.
- **`name`** *(string)*
- **`email`** *(string)*
- **`description`** *(string)*
- **`welcome_message`** *(string)*
- **`members`** *(array)*
  - **Items**: Refer to *[#/definitions/GroupMember](#definitions/GroupMember)*.
- **`who_can_invite`**: Default: `"ALL_MANAGERS_CAN_INVITE"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanInvite](#definitions/WhoCanInvite)*.
- **`who_can_join`**: Default: `"CAN_REQUEST_TO_JOIN"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanJoin](#definitions/WhoCanJoin)*.
- **`who_can_post_message`**: Default: `"NONE_CAN_POST"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanPostMessage](#definitions/WhoCanPostMessage)*.
- **`who_can_view_group`**: Default: `"ALL_MANAGERS_CAN_VIEW"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanViewGroup](#definitions/WhoCanViewGroup)*.
- **`who_can_view_membership`**: Default: `"ALL_MANAGERS_CAN_VIEW"`.
  - **All of**
    - : Refer to *[#/definitions/WhoCanViewMembership](#definitions/WhoCanViewMembership)*.
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.
- <a id="definitions/GroupMemberRole"></a>**`GroupMemberRole`**: An enumeration. Must be one of: `["OWNER", "MANAGER", "MEMBER"]`.
- <a id="definitions/GroupMemberType"></a>**`GroupMemberType`**: An enumeration. Must be one of: `["USER", "GROUP", "EXTERNAL"]`.
- <a id="definitions/GroupMemberStatus"></a>**`GroupMemberStatus`**: An enumeration. Must be one of: `["ACTIVE", "INACTIVE", "PENDING"]`.
- <a id="definitions/GroupMemberSubscription"></a>**`GroupMemberSubscription`**: An enumeration. Must be one of: `["EACH_EMAIL", "DIGEST", "ABRIDGED", "NO_EMAIL"]`.
- <a id="definitions/GroupMember"></a>**`GroupMember`** *(object)*
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`email`** *(string)*
  - **`expand`** *(boolean)*: Expand the group into the members of the group. This is useful for nested groups. Default: `false`.
  - **`role`**: Default: `"MEMBER"`.
    - **All of**
      - : Refer to *[#/definitions/GroupMemberRole](#definitions/GroupMemberRole)*.
  - **`type`**: Default: `"USER"`.
    - **All of**
      - : Refer to *[#/definitions/GroupMemberType](#definitions/GroupMemberType)*.
  - **`status`**: Default: `"ACTIVE"`.
    - **All of**
      - : Refer to *[#/definitions/GroupMemberStatus](#definitions/GroupMemberStatus)*.
  - **`subscription`**: Default: `"EACH_EMAIL"`.
    - **All of**
      - : Refer to *[#/definitions/GroupMemberSubscription](#definitions/GroupMemberSubscription)*.
- <a id="definitions/WhoCanInvite"></a>**`WhoCanInvite`**: An enumeration. Must be one of: `["ALL_MANAGERS_CAN_INVITE", "ALL_MEMBERS_CAN_INVITE"]`.
- <a id="definitions/WhoCanJoin"></a>**`WhoCanJoin`**: An enumeration. Must be one of: `["ALL_IN_DOMAIN_CAN_JOIN", "ANYONE_CAN_JOIN", "CAN_REQUEST_TO_JOIN"]`.
- <a id="definitions/WhoCanPostMessage"></a>**`WhoCanPostMessage`**: An enumeration. Must be one of: `["ALL_IN_DOMAIN_CAN_POST", "ALL_MANAGERS_CAN_POST", "ALL_MEMBERS_CAN_POST", "ANYONE_CAN_POST", "NONE_CAN_POST"]`.
- <a id="definitions/WhoCanViewGroup"></a>**`WhoCanViewGroup`**: An enumeration. Must be one of: `["ALL_IN_DOMAIN_CAN_VIEW", "ALL_MANAGERS_CAN_VIEW", "ALL_MEMBERS_CAN_VIEW", "ANYONE_CAN_VIEW"]`.
- <a id="definitions/WhoCanViewMembership"></a>**`WhoCanViewMembership`**: An enumeration. Must be one of: `["ALL_IN_DOMAIN_CAN_VIEW", "ALL_MANAGERS_CAN_VIEW", "ALL_MEMBERS_CAN_VIEW", "ANYONE_CAN_VIEW"]`.

docs/schemas/extends_config.md METASEP
# ExtendsConfig

## Properties

- **`key`**: Refer to *[#/definitions/ExtendsConfigKey](#definitions/ExtendsConfigKey)*.
- **`value`** *(string)*
## Definitions

- <a id="definitions/ExtendsConfigKey"></a>**`ExtendsConfigKey`**: An enumeration. Must be one of: `["AWS_SECRETS_MANAGER"]`.

docs/schemas/config.md METASEP
# Config

## Properties

- **`accounts`** *(array)*
  - **Items**: Refer to *[#/definitions/AccountConfig](#definitions/AccountConfig)*.
- **`google`**: Refer to *[#/definitions/GoogleConfig](#definitions/GoogleConfig)*.
- **`extends`** *(array)*: Default: `[]`.
  - **Items**: Refer to *[#/definitions/ExtendsConfig](#definitions/ExtendsConfig)*.
- **`secrets`** *(object)*
- **`role_access_tag`** *(string)*: The key of the tag used to store users and groups that can assume into the role the tag is on. Default: `"noq-authorized"`.
- **`variables`** *(array)*: A list of variables to be used when creating templates. These apply to all accounts but can be overwritten by an account. Default: `[]`.
  - **Items**: Refer to *[#/definitions/Variable](#definitions/Variable)*.
- **`slack_app`** *(string)*
- **`sqs`** *(object)*: Default: `{}`.
- **`slack`** *(object)*: Default: `{}`.
## Definitions

- <a id="definitions/RegionName"></a>**`RegionName`**: An enumeration. Must be one of: `["us-east-1", "us-west-1", "us-west-2", "eu-west-1", "eu-west-2", "eu-central-1", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1", "ap-northeast-2", "sa-east-1", "cn-north-1"]`.
- <a id="definitions/Variable"></a>**`Variable`** *(object)*
  - **`key`** *(string)*
  - **`value`** *(string)*
- <a id="definitions/AccountConfig"></a>**`AccountConfig`** *(object)*
  - **`account_id`** *(string)*: The AWS Account ID.
  - **`org_id`** *(string)*: A unique identifier designating the identity of the organization.
  - **`account_name`** *(string)*
  - **`default_region`**: Default region to use when making AWS requests. Default: `"us-east-1"`.
    - **All of**
      - : Refer to *[#/definitions/RegionName](#definitions/RegionName)*.
  - **`aws_profile`** *(string)*: The AWS profile used when making calls to the account.
  - **`assume_role_arn`** *(string)*: The role arn to assume into when making calls to the account.
  - **`external_id`** *(string)*: The external id to use for assuming into a role when making calls to the account.
  - **`role_access_tag`** *(string)*: The key of the tag used to store users and groups that can assume into the role the tag is on.
  - **`variables`** *(array)*: A list of variables to be used when creating templates. Default: `[]`.
    - **Items**: Refer to *[#/definitions/Variable](#definitions/Variable)*.
  - **`boto3_session_map`** *(object)*
  - **`read_only`** *(boolean)*: If set to True, iambic will only log drift instead of apply changes when drift is detected. Default: `false`.
- <a id="definitions/GoogleGroupsConfig"></a>**`GoogleGroupsConfig`** *(object)*
  - **`enabled`** *(boolean)*: Default: `false`.
- <a id="definitions/GoogleConfig"></a>**`GoogleConfig`** *(object)*
  - **`groups`**: Refer to *[#/definitions/GoogleGroupsConfig](#definitions/GoogleGroupsConfig)*.
- <a id="definitions/ExtendsConfigKey"></a>**`ExtendsConfigKey`**: An enumeration. Must be one of: `["AWS_SECRETS_MANAGER"]`.
- <a id="definitions/ExtendsConfig"></a>**`ExtendsConfig`** *(object)*
  - **`key`**: Refer to *[#/definitions/ExtendsConfigKey](#definitions/ExtendsConfigKey)*.
  - **`value`** *(string)*

docs/schemas/assume_role_policy_document.md METASEP
# AssumeRolePolicyDocument

## Properties

- **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
  - **Items**
- **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
  - **Items**
- **`version`** *(string)*
- **`statement`** *(array)*
  - **Items**: Refer to *[#/definitions/PolicyStatement](#definitions/PolicyStatement)*.
## Definitions

- <a id="definitions/Deleted"></a>**`Deleted`** *(object)*
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`deleted`** *(boolean)*: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran.
- <a id="definitions/Principal"></a>**`Principal`** *(object)*
  - **`aws`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`service`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`canonical_user`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
  - **`federated`**
    - **Any of**
      - *string*
      - *array*
        - **Items** *(string)*
- <a id="definitions/PolicyStatement"></a>**`PolicyStatement`** *(object)*
  - **`expires_at`** *(string)*: The date and time the resource will be/was set to deleted.
  - **`deleted`**: Denotes whether the resource has been removed from AWS.Upon being set to true, the resource will be deleted the next time iambic is ran. Default: `false`.
    - **Any of**
      - *boolean*
      - *array*
        - **Items**: Refer to *[#/definitions/Deleted](#definitions/Deleted)*.
  - **`included_accounts`** *(array)*: A list of account ids and/or account names this statement applies to. Account ids/names can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_accounts`** *(array)*: A list of account ids and/or account names this statement explicitly does not apply to. Account ids/names can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`included_orgs`** *(array)*: A list of AWS organization ids this statement applies to. Org ids can be represented as a regex and string. Default: `["*"]`.
    - **Items**
  - **`excluded_orgs`** *(array)*: A list of AWS organization ids this statement explicitly does not apply to. Org ids can be represented as a regex and string. Default: `[]`.
    - **Items**
  - **`effect`** *(string)*: Allow | Deny.
  - **`principal`**
    - **Any of**
      - : Refer to *[#/definitions/Principal](#definitions/Principal)*.
      - *string*
  - **`not_principal`**
    - **Any of**
      - : Refer to *[#/definitions/Principal](#definitions/Principal)*.
      - *string*
  - **`action`**: A single regex or list of regexes. Values are the actions that can be performed on the resources in the policy statement.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`not_resource`**: A single regex or list of regexes. Values specified are the resources the statement applies to.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`not_action`**: An advanced policy element that explicitly matches everything except the specified list of actions.DON'T use this with effect: allow in the same statement OR policy.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`resource`**: A single regex or list of regexes. Values specified are the resources the statement applies to.
    - **Any of**
      - *array*
        - **Items** *(string)*
      - *string*
  - **`condition`** *(object)*: An optional set of conditions to determine of the policy applies to a resource.
  - **`sid`** *(string)*: The Policy Statement ID.

docs/schemas/account_config.md METASEP
# AccountConfig

## Properties

- **`account_id`** *(string)*: The AWS Account ID.
- **`org_id`** *(string)*: A unique identifier designating the identity of the organization.
- **`account_name`** *(string)*
- **`default_region`**: Default region to use when making AWS requests. Default: `"us-east-1"`.
  - **All of**
    - : Refer to *[#/definitions/RegionName](#definitions/RegionName)*.
- **`aws_profile`** *(string)*: The AWS profile used when making calls to the account.
- **`assume_role_arn`** *(string)*: The role arn to assume into when making calls to the account.
- **`external_id`** *(string)*: The external id to use for assuming into a role when making calls to the account.
- **`role_access_tag`** *(string)*: The key of the tag used to store users and groups that can assume into the role the tag is on.
- **`variables`** *(array)*: A list of variables to be used when creating templates. Default: `[]`.
  - **Items**: Refer to *[#/definitions/Variable](#definitions/Variable)*.
- **`boto3_session_map`** *(object)*
- **`read_only`** *(boolean)*: If set to True, iambic will only log drift instead of apply changes when drift is detected. Default: `false`.
## Definitions

- <a id="definitions/RegionName"></a>**`RegionName`**: An enumeration. Must be one of: `["us-east-1", "us-west-1", "us-west-2", "eu-west-1", "eu-west-2", "eu-central-1", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1", "ap-northeast-2", "sa-east-1", "cn-north-1"]`.
- <a id="definitions/Variable"></a>**`Variable`** *(object)*
  - **`key`** *(string)*
  - **`value`** *(string)*

iambic/aws/iam/role/utils.py METASEP
import asyncio
import json

from deepdiff import DeepDiff

from iambic.aws.utils import paginated_search
from iambic.core.context import ctx
from iambic.core.logger import log
from iambic.core.utils import aio_wrapper


async def get_role_inline_policy_names(role_name: str, iam_client):
    return await paginated_search(
        iam_client.list_role_policies, "PolicyNames", RoleName=role_name
    )


async def get_role_instance_profiles(role_name: str, iam_client):
    return await paginated_search(
        iam_client.list_instance_profiles_for_role,
        "InstanceProfiles",
        RoleName=role_name,
    )


async def list_roles(iam_client):
    return await paginated_search(iam_client.list_roles, "Roles")


async def list_role_tags(role_name: str, iam_client):
    return await paginated_search(iam_client.list_role_tags, "Tags", RoleName=role_name)


async def get_role_policy(role_name: str, policy_name: str, iam_client):
    return await aio_wrapper(
        iam_client.get_role_policy, RoleName=role_name, PolicyName=policy_name
    )


async def get_role_inline_policies(role_name: str, iam_client):
    policy_names = await get_role_inline_policy_names(role_name, iam_client)
    policies = await asyncio.gather(
        *[
            get_role_policy(role_name, policy_name, iam_client)
            for policy_name in policy_names
        ]
    )
    return {policy["PolicyName"]: policy["PolicyDocument"] for policy in policies}


async def get_role_managed_policies(role_name: str, iam_client) -> list[dict[str, str]]:
    marker: dict[str, str] = {}
    policies = []

    while True:
        response = await aio_wrapper(
            iam_client.list_attached_role_policies, RoleName=role_name, **marker
        )
        policies.extend(response["AttachedPolicies"])

        if response["IsTruncated"]:
            marker["Marker"] = response["Marker"]
        else:
            break

    return policies


async def apply_role_tags(
    role_name: str,
    iam_client,
    template_tags: list[dict],
    existing_tags: list[dict],
    log_params: dict,
) -> bool:
    existing_tag_map = {tag["Key"]: tag["Value"] for tag in existing_tags}
    template_tag_map = {tag["Key"]: tag["Value"] for tag in template_tags}
    tags_to_apply = [
        tag for tag in template_tags if tag["Value"] != existing_tag_map.get(tag["Key"])
    ]
    tasks = []
    changes_made = False

    if tags_to_remove := [
        tag["Key"] for tag in existing_tags if not template_tag_map.get(tag["Key"])
    ]:
        changes_made = True
        log_str = "Stale tags discovered."
        if ctx.execute:
            log_str = f"{log_str} Removing tags..."
            tasks.append(
                aio_wrapper(
                    iam_client.untag_role, RoleName=role_name, TagKeys=tags_to_remove
                )
            )
        log.info(log_str, tags=tags_to_remove, **log_params)

    if tags_to_apply:
        changes_made = True
        log_str = "New tags discovered in AWS."
        if ctx.execute:
            log_str = f"{log_str} Adding tags..."
            tasks.append(
                aio_wrapper(iam_client.tag_role, RoleName=role_name, Tags=tags_to_apply)
            )
        log.info(log_str, tags=tags_to_apply, **log_params)

    if tasks:
        await asyncio.gather(*tasks)

    return changes_made


async def update_assume_role_policy(
    role_name,
    iam_client,
    template_policy_document: dict,
    existing_policy_document: str,
    log_params: dict,
) -> bool:
    if existing_policy_document and isinstance(existing_policy_document, str):
        existing_policy_document = json.loads(existing_policy_document)

    if not existing_policy_document or bool(
        await aio_wrapper(
            DeepDiff,
            template_policy_document,
            existing_policy_document,
            ignore_order=True,
        )
    ):

        log_str = "Changes to the AssumeRolePolicyDocument discovered."
        if ctx.execute:
            boto_action = "Creating" if existing_policy_document else "Updating"
            log_str = f"{log_str} {boto_action} AssumeRolePolicyDocument..."
            await aio_wrapper(
                iam_client.update_assume_role_policy,
                RoleName=role_name,
                PolicyDocument=json.dumps(template_policy_document),
            )
        log.info(log_str, **log_params)

        return True

    return False


async def apply_role_managed_policies(
    role_name,
    iam_client,
    template_policies: list[dict],
    role_exists: bool,
    log_params: dict,
) -> bool:
    changes_made = False
    tasks = []
    template_policies = [policy["PolicyArn"] for policy in template_policies]
    if role_exists:
        managed_policies_resp = await get_role_managed_policies(role_name, iam_client)
        existing_managed_policies = [
            policy["PolicyArn"] for policy in managed_policies_resp
        ]
    else:
        existing_managed_policies = []

    new_managed_policies = [
        policy_arn
        for policy_arn in template_policies
        if policy_arn not in existing_managed_policies
    ]
    if new_managed_policies:
        changes_made = False
        log_str = "New managed policies discovered."
        if ctx.execute:
            log_str = f"{log_str} Adding managed policies..."
            tasks = [
                aio_wrapper(
                    iam_client.attach_role_policy,
                    RoleName=role_name,
                    PolicyArn=policy_arn,
                )
                for policy_arn in new_managed_policies
            ]
        log.info(log_str, managed_policies=new_managed_policies, **log_params)

    existing_managed_policies = [
        policy_arn
        for policy_arn in existing_managed_policies
        if policy_arn not in template_policies
    ]
    if existing_managed_policies:
        log_str = "Stale managed policies discovered."
        if ctx.execute:
            log_str = f"{log_str} Removing managed policies..."
            tasks.extend(
                [
                    aio_wrapper(
                        iam_client.detach_role_policy,
                        RoleName=role_name,
                        PolicyArn=policy_arn,
                    )
                    for policy_arn in existing_managed_policies
                ]
            )
        log.info(log_str, managed_policies=existing_managed_policies, **log_params)

    if tasks:
        await asyncio.gather(*tasks)

    return changes_made


async def apply_role_inline_policies(
    role_name,
    iam_client,
    template_policies: list[dict],
    role_exists: bool,
    log_params: dict,
) -> bool:
    tasks = []
    changes_made = False

    if role_exists:
        template_policy_map = {
            policy["PolicyName"]: {k: v for k, v in policy.items() if k != "PolicyName"}
            for policy in template_policies
        }
        existing_policy_map = await get_role_inline_policies(role_name, iam_client)

        for policy_name in existing_policy_map.keys():
            if not template_policy_map.get(policy_name):
                changes_made = True
                log_str = "Stale inline policies discovered."
                if ctx.execute:
                    log_str = f"{log_str} Removing inline policy..."
                    tasks.append(
                        aio_wrapper(
                            iam_client.delete_role_policy,
                            RoleName=role_name,
                            PolicyName=policy_name,
                        )
                    )
                log.info(log_str, policy_name=policy_name, **log_params)

        for policy_name, policy_document in template_policy_map.items():
            existing_policy_doc = existing_policy_map.get(policy_name)
            policy_drift = None
            if existing_policy_doc:
                policy_drift = await aio_wrapper(
                    DeepDiff,
                    existing_policy_doc,
                    policy_document,
                    ignore_order=True,
                )

            if not existing_policy_doc or policy_drift:
                if policy_drift:
                    log_params["policy_drift"] = policy_drift
                    boto_action = "Updating"
                    resource_existence = "Stale"
                else:
                    boto_action = "Creating"
                    resource_existence = "New"

                changes_made = True
                log_str = f"{resource_existence} inline policies discovered."
                if ctx.execute:
                    log_str = f"{log_str} {boto_action} inline policy..."
                    tasks.append(
                        aio_wrapper(
                            iam_client.put_role_policy,
                            RoleName=role_name,
                            PolicyName=policy_name,
                            PolicyDocument=json.dumps(policy_document),
                        )
                    )
                log.info(log_str, policy_name=policy_name, **log_params)

        if tasks:
            await asyncio.gather(*tasks)

    return changes_made


async def delete_iam_role(role_name: str, iam_client, log_params: dict):
    instance_profiles = await get_role_instance_profiles(role_name, iam_client)

    tasks = []
    for instance_profile in instance_profiles:
        tasks.append(
            aio_wrapper(
                iam_client.remove_role_from_instance_profile,
                RoleName=role_name,
                InstanceProfileName=instance_profile["InstanceProfileName"],
            )
        )
    await asyncio.gather(*tasks)

    tasks = []
    for instance_profile in instance_profiles:
        tasks.append(
            aio_wrapper(
                iam_client.delete_instance_profile,
                InstanceProfileName=instance_profile["InstanceProfileName"],
            )
        )
    await asyncio.gather(*tasks)

    tasks = []
    # Detach managed policies
    managed_policies = await get_role_managed_policies(role_name, iam_client)
    managed_policies = [policy["PolicyArn"] for policy in managed_policies]
    log.info(
        "Detaching managed policies.", managed_policies=managed_policies, **log_params
    )
    for policy in managed_policies:
        tasks.append(
            aio_wrapper(
                iam_client.detach_role_policy, RoleName=role_name, PolicyArn=policy
            )
        )

    # Delete inline policies
    inline_policies = await get_role_inline_policies(role_name, iam_client)
    inline_policies = list(inline_policies.keys())
    log.info(
        "Deleting inline policies.", managed_policies=inline_policies, **log_params
    )
    for policy_name in inline_policies:
        tasks.append(
            aio_wrapper(
                iam_client.delete_role_policy,
                RoleName=role_name,
                PolicyName=policy_name,
            )
        )

    # Actually perform the deletion of Managed & Inline policies
    await asyncio.gather(*tasks)
    # Now that everything has been removed from the role, delete the role itself
    await aio_wrapper(iam_client.delete_role, RoleName=role_name)

iambic/aws/iam/role/template_generation.py METASEP
import os
import pathlib

import aiofiles

from iambic.aws.iam.role.models import RoleTemplate
from iambic.aws.iam.role.utils import (
    get_role_inline_policies,
    get_role_managed_policies,
    list_role_tags,
    list_roles,
)
from iambic.config.models import AccountConfig, Config
from iambic.core import noq_json as json
from iambic.core.logger import log
from iambic.core.template_generation import (
    base_group_dict_attribute,
    base_group_str_attribute,
    group_dict_attribute,
    group_int_or_str_attribute,
    set_included_accounts_for_grouped_attribute,
)
from iambic.core.utils import (
    NoqSemaphore,
    get_account_config_map,
    normalize_boto3_resp,
    resource_file_upsert,
)

ROLE_RESPONSE_DIR = pathlib.Path.home().joinpath(
    ".noqform", "resources", "aws", "roles"
)


def get_role_dir(base_dir: str) -> str:
    repo_dir = os.path.join(base_dir, "resources", "aws", "roles")
    os.makedirs(repo_dir, exist_ok=True)
    return str(repo_dir)


def get_templated_role_file_path(role_dir: str, role_name: str):
    file_name = (
        role_name.replace("{{", "")
        .replace("}}_", "_")
        .replace("}}", "_")
        .replace(".", "_")
        .lower()
    )
    return str(os.path.join(role_dir, f"{file_name}.yaml"))


def get_account_role_resource_dir(account_id: str) -> str:
    account_role_response_dir = os.path.join(ROLE_RESPONSE_DIR, account_id)
    os.makedirs(account_role_response_dir, exist_ok=True)
    return account_role_response_dir


async def generate_account_role_resource_files(account_config: AccountConfig) -> dict:
    account_role_response_dir = get_account_role_resource_dir(account_config.account_id)
    role_resource_file_upsert_semaphore = NoqSemaphore(resource_file_upsert, 10)
    messages = []

    response = dict(account_id=account_config.account_id, roles=[])
    iam_client = account_config.get_boto3_client("iam")
    account_roles = await list_roles(iam_client)

    log.info(
        "Retrieved AWS IAM Roles.",
        account_id=account_config.account_id,
        account_name=account_config.account_name,
        role_count=len(account_roles),
    )

    for account_role in account_roles:
        role_path = os.path.join(
            account_role_response_dir, f'{account_role["RoleName"]}.json'
        )
        response["roles"].append(
            {
                "path": role_path,
                "name": account_role["RoleName"],
                "account_id": account_config.account_id,
            }
        )
        messages.append(
            dict(file_path=role_path, content_as_dict=account_role, replace_file=True)
        )

    await role_resource_file_upsert_semaphore.process(messages)
    log.info(
        "Finished caching AWS IAM Roles.",
        account_id=account_config.account_id,
        role_count=len(account_roles),
    )

    return response


async def set_role_resource_tags(
    role_name: str, role_resource_path: str, account_config: AccountConfig
):
    iam_client = account_config.get_boto3_client("iam")
    role_tags = await list_role_tags(role_name, iam_client)
    await resource_file_upsert(role_resource_path, {"Tags": role_tags}, False)


async def set_role_resource_inline_policies(
    role_name: str, role_resource_path: str, account_config: AccountConfig
):
    iam_client = account_config.get_boto3_client("iam")
    role_inline_policies = await get_role_inline_policies(role_name, iam_client)
    for k in role_inline_policies.keys():
        role_inline_policies[k]["policy_name"] = k

    role_inline_policies = list(role_inline_policies.values())
    await resource_file_upsert(
        role_resource_path, {"InlinePolicies": role_inline_policies}, False
    )


async def set_role_resource_managed_policies(
    role_name: str, role_resource_path: str, account_config: AccountConfig
):
    iam_client = account_config.get_boto3_client("iam")
    role_managed_policies = await get_role_managed_policies(role_name, iam_client)
    await resource_file_upsert(
        role_resource_path, {"ManagedPolicies": role_managed_policies}, False
    )


async def create_templated_role(  # noqa: C901
    global_config: Config,
    account_config_map: dict[str, AccountConfig],
    role_name: str,
    role_refs: list[dict],
    role_dir: str,
):
    account_id_to_role_map = {}
    num_of_accounts = len(role_refs)
    for role_ref in role_refs:
        async with aiofiles.open(role_ref["path"], mode="r") as f:
            content_dict = json.loads(await f.read())
            account_id_to_role_map[role_ref["account_id"]] = normalize_boto3_resp(
                content_dict
            )

    # Generate the params used for attribute creation
    role_template_params = {"role_name": role_name}
    path_resources = list()
    description_resources = list()
    managed_policy_resources = list()
    assume_role_policy_document_resources = list()
    inline_policy_document_resources = list()
    permissions_boundary_resources = list()
    tag_resources = list()
    max_session_duration_resources = dict()
    for account_id, role_dict in account_id_to_role_map.items():
        path_resources.append(
            {
                "account_id": account_id,
                "resources": [{"resource_val": role_dict["path"]}],
            }
        )
        managed_policy_resources.append(
            {
                "account_id": account_id,
                "resources": [
                    {"resource_val": mp} for mp in role_dict.get("managed_policies", [])
                ],
            }
        )
        assume_role_policy_document_resources.append(
            {
                "account_id": account_id,
                "resources": [
                    {"resource_val": role_dict["assume_role_policy_document"]}
                ],
            }
        )

        if permissions_boundary := role_dict.get("permissions_boundary"):
            permissions_boundary_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": permissions_boundary}],
                }
            )

        max_session_duration_resources[account_id] = role_dict["max_session_duration"]

        if inline_policies := role_dict.get("inline_policies"):
            inline_policy_document_resources.append(
                {
                    "account_id": account_id,
                    "resources": [
                        {"resource_val": inline_policy}
                        for inline_policy in inline_policies
                    ],
                }
            )

        if tags := role_dict.get("tags"):
            tag_resources.append(
                {
                    "account_id": account_id,
                    "resources": [{"resource_val": tag} for tag in tags],
                }
            )

        if description := role_dict.get("description"):
            description_resources.append(
                {"account_id": account_id, "resources": [{"resource_val": description}]}
            )

    if len(role_refs) != len(account_config_map):
        role_template_params["included_accounts"] = [
            account_config_map[role_ref["account_id"]].account_name
            for role_ref in role_refs
        ]

    path = await group_int_or_str_attribute(
        account_config_map, num_of_accounts, path_resources, "path"
    )
    if path != "/":
        role_template_params["path"] = path

    max_session_duration = await group_int_or_str_attribute(
        account_config_map,
        num_of_accounts,
        max_session_duration_resources,
        "max_session_duration",
    )
    if max_session_duration != 3600:
        role_template_params["max_session_duration"] = max_session_duration

    if assume_role_policy_document_resources:
        role_template_params[
            "assume_role_policy_document"
        ] = await group_dict_attribute(
            account_config_map, num_of_accounts, assume_role_policy_document_resources
        )

    if permissions_boundary_resources:
        role_template_params["permissions_boundary"] = await group_dict_attribute(
            account_config_map, num_of_accounts, permissions_boundary_resources
        )

    if description_resources:
        role_template_params["description"] = await group_int_or_str_attribute(
            account_config_map, num_of_accounts, description_resources, "description"
        )

    if managed_policy_resources:
        role_template_params["managed_policies"] = await group_dict_attribute(
            account_config_map, num_of_accounts, managed_policy_resources, False
        )

    if inline_policy_document_resources:
        role_template_params["inline_policies"] = await group_dict_attribute(
            account_config_map, num_of_accounts, inline_policy_document_resources, False
        )

    if tag_resources:
        tags = []
        role_access = []
        tag_lists = await base_group_dict_attribute(account_config_map, tag_resources)
        for tag_val in tag_lists:
            included_accounts = tag_val["included_accounts"]
            tag = tag_val["resource_val"]
            if tag["key"] == global_config.role_access_tag and tag["value"]:
                role_access.append(
                    {
                        "included_accounts": included_accounts,
                        "groups": tag["value"].split(":"),
                    }
                )
            elif tag["value"]:
                tags.append({"included_accounts": included_accounts, **tag})

        if tags:
            role_template_params[
                "tags"
            ] = await set_included_accounts_for_grouped_attribute(
                account_config_map, num_of_accounts, tags
            )
            for elem in range(len(role_template_params["tags"])):
                if role_template_params["tags"][elem]["included_accounts"] == ["*"]:
                    role_template_params["tags"][elem].pop("included_accounts")

        if role_access:
            role_template_params[
                "role_access"
            ] = await set_included_accounts_for_grouped_attribute(
                account_config_map, num_of_accounts, role_access
            )
            for elem in range(len(role_template_params["role_access"])):
                if role_template_params["role_access"][elem]["included_accounts"] == [
                    "*"
                ]:
                    role_template_params["role_access"][elem].pop("included_accounts")

    try:
        role = RoleTemplate(
            file_path=get_templated_role_file_path(role_dir, role_name),
            **role_template_params,
        )
        role.write()
    except Exception as err:
        log.info(str(err), role_params=role_template_params)


async def generate_aws_role_templates(configs: list[Config], base_output_dir: str):
    account_config_map = get_account_config_map(configs)
    generate_account_role_resource_files_semaphore = NoqSemaphore(
        generate_account_role_resource_files, 5
    )
    set_role_resource_inline_policies_semaphore = NoqSemaphore(
        set_role_resource_inline_policies, 20
    )
    set_role_resource_managed_policies_semaphore = NoqSemaphore(
        set_role_resource_managed_policies, 30
    )
    set_role_resource_tags_semaphore = NoqSemaphore(set_role_resource_tags, 75)
    role_dir = get_role_dir(base_output_dir)

    log.info("Generating AWS role templates.")
    log.info(
        "Beginning to retrieve AWS IAM Roles.", accounts=list(account_config_map.keys())
    )

    account_roles = await generate_account_role_resource_files_semaphore.process(
        [
            {"account_config": account_config}
            for account_config in account_config_map.values()
        ]
    )
    messages = []
    for account_role in account_roles:
        for role in account_role["roles"]:
            messages.append(
                {
                    "role_name": role["name"],
                    "role_resource_path": role["path"],
                    "account_config": account_config_map[account_role["account_id"]],
                }
            )

    log.info("Setting inline policies for roles")
    await set_role_resource_inline_policies_semaphore.process(messages)
    log.info("Setting managed policies for roles")
    await set_role_resource_managed_policies_semaphore.process(messages)
    log.info("Setting tags for roles")
    await set_role_resource_tags_semaphore.process(messages)
    log.info("Finished retrieving role details")

    # Use these for testing `create_templated_role`
    # account_role_output = json.dumps(account_roles)
    # with open("account_role_output.json", "w") as f:
    #     f.write(account_role_output)
    # with open("account_role_output.json") as f:
    #     account_roles = json.loads(f.read())

    log.info("Grouping roles")
    # Move everything to required structure
    for account_role_elem in range(len(account_roles)):
        for role_elem in range(len(account_roles[account_role_elem]["roles"])):
            role_name = account_roles[account_role_elem]["roles"][role_elem].pop("name")
            account_roles[account_role_elem]["roles"][role_elem][
                "resource_val"
            ] = role_name

        account_roles[account_role_elem]["resources"] = account_roles[
            account_role_elem
        ].pop("roles", [])

    grouped_role_map = await base_group_str_attribute(account_config_map, account_roles)

    log.info("Writing templated roles")
    for role_name, role_refs in grouped_role_map.items():
        await create_templated_role(
            configs[0], account_config_map, role_name, role_refs, role_dir
        )

    log.info("Finished templated role generation")

iambic/aws/iam/role/models.py METASEP
import asyncio
import json
from typing import Optional, Union

from pydantic import Field, constr

from iambic.aws.iam.models import Description, MaxSessionDuration, Path
from iambic.aws.iam.policy.models import (
    AssumeRolePolicyDocument,
    ManagedPolicyRef,
    PolicyDocument,
)
from iambic.aws.iam.role.utils import (
    apply_role_inline_policies,
    apply_role_managed_policies,
    apply_role_tags,
    delete_iam_role,
    update_assume_role_policy,
)
from iambic.aws.models import ARN_RE
from iambic.config.models import AccountConfig
from iambic.core.context import ctx
from iambic.core.logger import log
from iambic.core.models import AccessModel, ExpiryModel, NoqTemplate, Tag
from iambic.core.utils import aio_wrapper, apply_to_account


class RoleAccess(ExpiryModel, AccessModel):
    users: list[str] = Field(
        [],
        description="List of users who can assume into the role",
    )
    groups: list[str] = Field(
        [],
        description="List of groups. Users in one or more of the groups can assume into the role",
    )

    @property
    def resource_type(self):
        return "Role Access"

    @property
    def resource_name(self):
        return


class PermissionBoundary(ExpiryModel, AccessModel):
    permissions_boundary_type: str
    permissions_boundary_arn: constr(regex=ARN_RE)


class RoleTemplate(NoqTemplate, AccessModel):
    template_type = "NOQ::IAM::MultiAccountRole"
    role_name: str = Field(
        description="Name of the role",
    )
    description: Optional[Union[str | list[Description]]] = Field(
        "",
        description="Description of the role",
    )
    owner: Optional[str] = None
    max_session_duration: Optional[Union[int | list[MaxSessionDuration]]] = 3600
    path: Optional[Union[str | list[Path]]] = "/"
    permissions_boundary: Optional[
        None | PermissionBoundary | list[PermissionBoundary]
    ] = None
    role_access: Optional[list[RoleAccess]] = Field(
        [],
        description="List of users and groups who can assume into the role",
    )
    assume_role_policy_document: Optional[
        None | AssumeRolePolicyDocument | list[AssumeRolePolicyDocument]
    ] = None
    tags: Optional[list[Tag]] = Field(
        [],
        description="List of tags attached to the role",
    )
    managed_policies: Optional[list[ManagedPolicyRef]] = Field(
        [],
        description="Managed policy arns attached to the role",
    )
    inline_policies: Optional[list[PolicyDocument]] = Field(
        [],
        description="List of the role's inline policies",
    )

    def _apply_resource_dict(self, account_config: AccountConfig = None) -> dict:
        response = super(RoleTemplate, self)._apply_resource_dict(account_config)
        response.pop("RoleAccess", None)
        if "Tags" not in response:
            response["Tags"] = []

        # Add RoleAccess Tag to role tags
        role_access = [
            ra._apply_resource_dict(account_config)
            for ra in self.role_access
            if apply_to_account(ra, account_config)
        ]
        if role_access:
            value = []
            for role_access_dict in role_access:
                value.extend(role_access_dict.get("Users", []))
                value.extend(role_access_dict.get("Groups", []))
            response["Tags"].append(
                {"Key": account_config.role_access_tag, "Value": ":".join(value)}
            )

        # Ensure only 1 of the following objects
        # TODO: Have this handled in a cleaner way. Maybe via an attribute on a pydantic field
        if assume_role_policy := response.pop("AssumeRolePolicyDocument", []):
            if isinstance(assume_role_policy, list):
                assume_role_policy = assume_role_policy[0]
            response["AssumeRolePolicyDocument"] = assume_role_policy

        if permissions_boundary := response.pop("PermissionsBoundary", []):
            if isinstance(permissions_boundary, list):
                permissions_boundary = permissions_boundary[0]
            response["PermissionsBoundary"] = permissions_boundary

        if isinstance(response.get("Description"), list):
            response["Description"] = response["Description"][0]["Description"]

        if isinstance(response.get("MaxSessionDuration"), list):
            response["MaxSessionDuration"] = response["MaxSessionDuration"][0][
                "MaxSessionDuration"
            ]

        return response

    def _is_read_only(self, account_config: AccountConfig):
        return (
            "aws-service-role" in self.path
            or account_config.read_only
            or self.read_only
        )

    async def _apply_to_account(self, account_config: AccountConfig) -> bool:
        boto3_session = account_config.get_boto3_session()
        client = boto3_session.client("iam")
        account_role = self.apply_resource_dict(account_config)
        role_name = account_role["RoleName"]
        log_params = dict(
            resource_type=self.resource_type,
            resource_name=role_name,
            account=str(account_config),
        )
        changes_made = False
        read_only = self._is_read_only(account_config)

        try:
            current_role = (await aio_wrapper(client.get_role, RoleName=role_name))[
                "Role"
            ]
        except client.exceptions.NoSuchEntityException:
            current_role = {}

        deleted = self.get_attribute_val_for_account(account_config, "deleted", False)
        if isinstance(deleted, list):
            deleted = deleted[0].deleted

        if deleted:
            if current_role:
                changes_made = True
                log_str = "Active resource found with deleted=false."
                if ctx.execute and not read_only:
                    log_str = f"{log_str} Deleting resource..."
                log.info(log_str, **log_params)

                if ctx.execute:
                    await delete_iam_role(role_name, client, log_params)

            return changes_made and not read_only

        role_exists = bool(current_role)
        inline_policies = account_role.pop("InlinePolicies", [])
        managed_policies = account_role.pop("ManagedPolicies", [])
        tasks = []

        if role_exists:
            tasks.extend(
                [
                    apply_role_tags(
                        role_name,
                        client,
                        account_role["Tags"],
                        current_role.get("Tags", []),
                        log_params,
                    ),
                    update_assume_role_policy(
                        role_name,
                        client,
                        account_role.pop("AssumeRolePolicyDocument", {}),
                        current_role["AssumeRolePolicyDocument"],
                        log_params,
                    ),
                ]
            )

            supported_update_keys = ["Description", "MaxSessionDuration"]
            update_resource_log_params = {**log_params}
            update_role_params = {}
            for k in supported_update_keys:
                if account_role.get(k) is not None and account_role.get(
                    k
                ) != current_role.get(k):
                    update_resource_log_params[k] = dict(
                        old_value=current_role.get(k), new_value=account_role.get(k)
                    )
                    update_role_params[k] = current_role.get(k)

            if update_role_params:
                changes_made = True
                log_str = "Out of date resource found."
                if ctx.execute:
                    log.info(
                        f"{log_str} Updating resource...", **update_resource_log_params
                    )
                    tasks.append(
                        aio_wrapper(
                            client.update_role,
                            RoleName=role_name,
                            **{k: account_role.get(k) for k in supported_update_keys},
                        )
                    )
                else:
                    log.info(log_str, **update_resource_log_params)
        else:
            log_str = "New resource found."
            if not ctx.execute:
                log.info(log_str, **log_params)
                # Exit now because apply functions won't work if resource doesn't exist
                return not read_only

            log_str = f"{log_str} Creating resource..."
            log.info(log_str, **log_params)
            account_role["AssumeRolePolicyDocument"] = json.dumps(
                account_role["AssumeRolePolicyDocument"]
            )
            await aio_wrapper(client.create_role, **account_role)
            changes_made = True

        tasks.extend(
            [
                apply_role_managed_policies(
                    role_name,
                    client,
                    managed_policies,
                    role_exists,
                    log_params,
                ),
                apply_role_inline_policies(
                    role_name,
                    client,
                    inline_policies,
                    role_exists,
                    log_params,
                ),
            ]
        )

        changes_made = any(await asyncio.gather(*tasks)) or changes_made

        if ctx.execute:
            log.debug(
                "Successfully finished execution on account for resource",
                changes_made=changes_made,
                **log_params,
            )
        else:
            log.debug(
                "Successfully finished scanning for drift on account for resource",
                requires_changes=changes_made,
                **log_params,
            )

        return changes_made and not read_only

    @property
    def resource_type(self):
        return "role"

    @property
    def resource_name(self):
        return self.role_name

iambic/aws/iam/role/__init__.py METASEP

iambic/aws/iam/policy/models.py METASEP
from typing import List, Optional, Union

from pydantic import Field, constr

from iambic.aws.iam.models import Description, Path
from iambic.aws.models import ARN_RE
from iambic.config.models import AccountConfig
from iambic.core.models import AccessModel, BaseModel, ExpiryModel, NoqTemplate, Tag


class Principal(BaseModel):
    aws: Optional[Union[str | list[str]]] = None
    service: Optional[Union[str | list[str]]] = None
    canonical_user: Optional[Union[str | list[str]]] = None
    federated: Optional[Union[str | list[str]]] = None

    def _apply_resource_dict(self, account_config: AccountConfig = None) -> dict:
        resource_dict = super(Principal, self)._apply_resource_dict(account_config)
        if aws_val := resource_dict.pop("aws", resource_dict.pop("Aws", None)):
            resource_dict["AWS"] = aws_val
        return resource_dict


class ConditionalStatement(BaseModel):
    for_all_values: Optional[bool] = None
    for_any_values: Optional[bool] = None


class Condition(BaseModel):
    """TODO: Finish this.

    I put in a pin it because it became obvious this will be a large undertaking.
    The problem stems from the way conditions are structured.

    Take string_equals: Optional[dict] = None for example
    For this one attribute it also requires the following:
        for_all_values__string_equals
        for_any_values__string_equals
        for_all_values__string_equals__if_exists
        for_any_values__string_equals__if_exists
    On top of that we still need to add clarity on what the supported keys are.

    For more info on conditions:
    https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html

    """

    # Key = service/resource_type
    # Value = resource_value | list[resource_value]

    # String conditionals
    string_equals: Optional[dict] = None
    string_not_equals: Optional[dict] = None
    string_equals_ignore_case: Optional[dict] = None
    string_like: Optional[dict] = None
    string_not_like: Optional[dict] = None

    # Numeric Conditionals
    numeric_equals: Optional[dict] = None
    numeric_not_equals: Optional[dict] = None
    numeric_less_than: Optional[dict] = None
    numeric_less_than_equals: Optional[dict] = None
    numeric_greater_than: Optional[dict] = None
    numeric_greater_than_equals: Optional[dict] = None

    # Date Conditionals
    date_equals: Optional[dict] = None
    date_not_equals: Optional[dict] = None
    date_less_than: Optional[dict] = None
    date_less_than_equals: Optional[dict] = None
    date_greater_than: Optional[dict] = None
    date_greater_than_equals: Optional[dict] = None

    # IP Addr Conditionals
    ip_address: Optional[dict] = None
    not_ip_address: Optional[dict] = None

    # ARN Conditionals
    arn_equals: Optional[dict] = None
    arn_like: Optional[dict] = None
    arn_not_equals: Optional[dict] = None
    arn_not_like: Optional[dict] = None

    # Null Conditional
    null: Optional[dict] = None

    # Boolean conditional
    bool: Optional[dict] = None


class PolicyStatement(AccessModel, ExpiryModel):
    effect: str = Field(..., description="Allow | Deny")
    principal: Optional[Union[Principal | str]] = None
    not_principal: Optional[Union[Principal | str]] = None
    action: Optional[Union[List[str] | str]] = Field(
        None,
        description="A single regex or list of regexes. "
        "Values are the actions that can be performed on the resources in the policy statement",
        example="dynamodb:list*",
    )
    not_resource: Optional[Union[List[str] | str]] = Field(
        None,
        description="An advanced policy element that explicitly matches every resource except those specified."
        "DON'T use this with effect: allow and action: '*'",
    )
    not_action: Optional[Union[List[str] | str]] = Field(
        None,
        description="An advanced policy element that explicitly matches everything except the specified list of actions."
        "DON'T use this with effect: allow in the same statement OR policy",
    )
    resource: Optional[Union[List[str] | str]] = Field(
        None,
        description="A single regex or list of regexes. Values specified are the resources the statement applies to",
    )
    not_resource: Optional[Union[List[str] | str]] = Field(
        None,
        description="A single regex or list of regexes. Values specified are the resources the statement applies to",
    )
    condition: Optional[dict] = Field(
        None,
        description="An optional set of conditions to determine of the policy applies to a resource.",
    )
    sid: Optional[str] = Field(
        None,
        description="The Policy Statement ID.",
    )

    @property
    def resource_type(self):
        return "Policy Statement"

    @property
    def resource_name(self):
        return self.sid


class AssumeRolePolicyDocument(AccessModel):
    version: Optional[str] = None
    statement: Optional[List[PolicyStatement]] = None


class PolicyDocument(AccessModel, ExpiryModel):
    policy_name: str = Field(
        description="The name of the policy.",
    )
    version: Optional[str] = None
    statement: Optional[List[PolicyStatement]] = Field(
        None,
        description="List of policy statements",
    )

    @property
    def resource_type(self):
        return "aws:policy_document"

    @property
    def resource_name(self):
        return self.policy_name


class ManagedPolicyTemplate(NoqTemplate):
    template_type = "NOQ::IAM::ManagedPolicy"
    policy_name: str = Field(
        description="The name of the policy.",
    )
    path: Optional[Union[str | List[Path]]] = "/"
    description: Optional[Union[str | list[Description]]] = Field(
        "",
        description="Description of the role",
    )
    policy_document: str
    tags: Optional[List[Tag]] = Field(
        [],
        description="List of tags attached to the role",
    )

    @property
    def resource_type(self):
        return "aws:policy_document"

    @property
    def resource_name(self):
        return self.policy_name


class ManagedPolicyRef(AccessModel, ExpiryModel):
    policy_arn: constr(regex=ARN_RE)

    @property
    def resource_type(self):
        return "Managed Policy"

    @property
    def resource_name(self):
        return self.policy_arn

iambic/aws/iam/policy/__init__.py METASEP

docs/SCHEMA.md METASEP
# AWS Template Models
* [RoleTemplate](schemas/role_template.md)
* [AssumeRolePolicyDocument](schemas/assume_role_policy_document.md)
* [ManagedPolicyRef](schemas/managed_policy_ref.md)
* [PolicyDocument](schemas/policy_document.md)
* [Path](schemas/path.md)
* [PermissionBoundary](schemas/permission_boundary.md)
* [MaxSessionDuration](schemas/max_session_duration.md)
* [RoleAccess](schemas/role_access.md)
* [PolicyStatement](schemas/policy_statement.md)

# Google Template Models
* [GroupTemplate](schemas/group_template.md)
* [GroupMember](schemas/group_member.md)

# Config Models
* [AccountConfig](schemas/account_config.md)
* [Config](schemas/config.md)
* [ExtendsConfig](schemas/extends_config.md)
* [Variable](schemas/variable.md)

demo/demo_config.yaml METASEP
accounts:
- account_id: '694815895589'
  account_name: 'demo-2'
  aws_profile: demo_2
- account_id: '518317429440'
  account_name: 'demo-3'
  aws_profile: demo_3

demo/config.yaml METASEP
extends:
  - key: AWS_SECRETS_MANAGER
    value: arn:aws:secretsmanager:us-west-2:759357822767:secret:dev/google-MmAS1o

accounts:
- account_id: '759357822767'
  account_name: dev
  aws_profile: noq_dev
- account_id: '259868150464'
  account_name: staging
  aws_profile: noq_staging
- account_id: '940552945933'
  account_name: prod
  aws_profile: noq_prod

google:
  groups:
    enabled: false

slack:
  alert_channel_id: 'C046KESSMQ8'

sqs:
  queues:
    iam_mutation: 
      arn: arn:aws:sqs:us-east-1:759357822767:noqform-iam-mutation-events

iambic/aws/sqs/util.py METASEP

iambic/aws/sqs/__init__.py METASEP

iambic/aws/iam/models.py METASEP
from typing import Optional

from iambic.core.models import AccessModel


class Path(AccessModel):
    file_path: str


class MaxSessionDuration(AccessModel):
    max_session_duration: int


class Description(AccessModel):
    description: Optional[str] = ""

iambic/aws/iam/__init__.py METASEP

test/__init__.py METASEP

docs/generate_schema.py METASEP
import os

import jsonschema2md2

from iambic.aws.iam.models import MaxSessionDuration, Path
from iambic.aws.iam.policy.models import (
    AssumeRolePolicyDocument,
    ManagedPolicyRef,
    PolicyDocument,
    PolicyStatement,
)
from iambic.aws.iam.role.models import PermissionBoundary, RoleAccess, RoleTemplate
from iambic.config.models import AccountConfig, Config, ExtendsConfig, Variable
from iambic.core.utils import camel_to_snake
from iambic.google.models import GroupMember, GroupTemplate


def create_model_schemas(
    parser: jsonschema2md2.Parser,
    schema_dir: str,
    schema_md_str: str,
    model_schemas: list,
) -> str:
    for model in model_schemas:
        class_name = str(model.__name__)
        file_name = camel_to_snake(class_name)
        model_schema_path = str(os.path.join(schema_dir, f"{file_name}.md"))
        schema_md_str += f"* [{class_name}]({model_schema_path.replace('docs/', '')})\n"
        with open(model_schema_path, "w") as f:
            f.write("".join(parser.parse_schema(model.schema())))

    return schema_md_str


def generate_docs():
    aws_template_models = [
        RoleTemplate,
        AssumeRolePolicyDocument,
        ManagedPolicyRef,
        PolicyDocument,
        Path,
        PermissionBoundary,
        MaxSessionDuration,
        RoleAccess,
        PolicyStatement,
    ]
    google_template_models = [GroupTemplate, GroupMember]
    config_models = [
        AccountConfig,
        Config,
        ExtendsConfig,
        Variable,
    ]

    schema_dir = os.path.join("docs", "schemas")
    os.makedirs(schema_dir, exist_ok=True)
    parser = jsonschema2md2.Parser(
        examples_as_yaml=False,
        show_examples="all",
    )
    schema_md_str = "# AWS Template Models\n"
    schema_md_str = create_model_schemas(
        parser, schema_dir, schema_md_str, aws_template_models
    )
    schema_md_str += "\n# Google Template Models\n"
    schema_md_str = create_model_schemas(
        parser, schema_dir, schema_md_str, google_template_models
    )
    schema_md_str += "\n# Config Models\n"
    schema_md_str = create_model_schemas(
        parser, schema_dir, schema_md_str, config_models
    )

    with open(os.path.join("docs", "SCHEMA.md"), "w") as f:
        f.write(schema_md_str)


if __name__ == "__main__":
    generate_docs()

docs/__init__.py METASEP

setup.cfg METASEP
[flake8]
ignore = E203, E266, E501, W503
max-line-length = 88
max-complexity = 18
select = B,C,E,F,W,T4

[isort]
multi_line_output=3
include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=88

[tool:pytest]
testpaths=test/

pyproject.toml METASEP
[tool.poetry]
name = "iambic"
version = "0.1.0"
description = "The python package used to generate, parse, and execute noqform yaml templates."
authors = ["Handsome Engineer <engineering@noq.dev>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.10"
boto3 = "^1.24.88"
click = "^8.1.3"
"ruamel.yaml" = "^0.17.21"
asgiref = "^3.5.2"
structlog = "^22.1.0"
pydantic = "^1.10.2"
deepdiff = "^5.8.1"
Jinja2 = "^3.1.2"
black = "^22.10.0"
isort = "^5.10.1"
flake8 = "^5.0.4"
pytest = "^7.1.3"
pytest-cov = "^4.0.0"
pre-commit = "^2.20.0"
ujson = "^5.5.0"
aiofiles = "^22.1.0"
xxhash = "^3.0.0"
slack-bolt = "^1.15.1"
google-api-python-client = "^2.64.0"
google-auth = "^2.12.0"
jsonschema2md2 = "^0.6.0"
GitPython = "^3.1.29"

[tool.poetry.scripts]
iambic = "iambic.main:cli"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"


poetry.lock METASEP
[[package]]
name = "aiofiles"
version = "22.1.0"
description = "File support for asyncio."
category = "main"
optional = false
python-versions = ">=3.7,<4.0"

[[package]]
name = "asgiref"
version = "3.5.2"
description = "ASGI specs, helper code, and adapters"
category = "main"
optional = false
python-versions = ">=3.7"

[package.extras]
tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]

[[package]]
name = "attrs"
version = "22.1.0"
description = "Classes Without Boilerplate"
category = "main"
optional = false
python-versions = ">=3.5"

[package.extras]
dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]

[[package]]
name = "black"
version = "22.10.0"
description = "The uncompromising code formatter."
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}

[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]

[[package]]
name = "boto3"
version = "1.24.88"
description = "The AWS SDK for Python"
category = "main"
optional = false
python-versions = ">= 3.7"

[package.dependencies]
botocore = ">=1.27.88,<1.28.0"
jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.6.0,<0.7.0"

[package.extras]
crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]

[[package]]
name = "botocore"
version = "1.27.88"
description = "Low-level, data-driven core of boto 3."
category = "main"
optional = false
python-versions = ">= 3.7"

[package.dependencies]
jmespath = ">=0.7.1,<2.0.0"
python-dateutil = ">=2.1,<3.0.0"
urllib3 = ">=1.25.4,<1.27"

[package.extras]
crt = ["awscrt (==0.14.0)"]

[[package]]
name = "cachetools"
version = "5.2.0"
description = "Extensible memoizing collections and decorators"
category = "main"
optional = false
python-versions = "~=3.7"

[[package]]
name = "certifi"
version = "2022.9.24"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = ">=3.6"

[[package]]
name = "cfgv"
version = "3.3.1"
description = "Validate configuration and produce human readable error messages."
category = "main"
optional = false
python-versions = ">=3.6.1"

[[package]]
name = "charset-normalizer"
version = "2.1.1"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
python-versions = ">=3.6.0"

[package.extras]
unicode_backport = ["unicodedata2"]

[[package]]
name = "click"
version = "8.1.3"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}

[[package]]
name = "colorama"
version = "0.4.5"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"

[[package]]
name = "coverage"
version = "6.5.0"
description = "Code coverage measurement for Python"
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}

[package.extras]
toml = ["tomli"]

[[package]]
name = "deepdiff"
version = "5.8.1"
description = "Deep Difference and Search of any Python object/data."
category = "main"
optional = false
python-versions = ">=3.6"

[package.dependencies]
ordered-set = ">=4.1.0,<4.2.0"

[package.extras]
cli = ["clevercsv (==0.7.1)", "click (==8.0.3)", "pyyaml (==5.4.1)", "toml (==0.10.2)"]

[[package]]
name = "distlib"
version = "0.3.6"
description = "Distribution utilities"
category = "main"
optional = false
python-versions = "*"

[[package]]
name = "filelock"
version = "3.8.0"
description = "A platform independent file lock."
category = "main"
optional = false
python-versions = ">=3.7"

[package.extras]
docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"]
testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"]

[[package]]
name = "flake8"
version = "5.0.4"
description = "the modular source code checker: pep8 pyflakes and co"
category = "main"
optional = false
python-versions = ">=3.6.1"

[package.dependencies]
mccabe = ">=0.7.0,<0.8.0"
pycodestyle = ">=2.9.0,<2.10.0"
pyflakes = ">=2.5.0,<2.6.0"

[[package]]
name = "gitdb"
version = "4.0.9"
description = "Git Object Database"
category = "main"
optional = false
python-versions = ">=3.6"

[package.dependencies]
smmap = ">=3.0.1,<6"

[[package]]
name = "GitPython"
version = "3.1.29"
description = "GitPython is a python library used to interact with Git repositories"
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
gitdb = ">=4.0.1,<5"

[[package]]
name = "google-api-core"
version = "2.10.2"
description = "Google API client core library"
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
google-auth = ">=1.25.0,<3.0dev"
googleapis-common-protos = ">=1.56.2,<2.0dev"
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0dev"
requests = ">=2.18.0,<3.0.0dev"

[package.extras]
grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio-status (>=1.33.2,<2.0dev)"]
grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0dev)"]
grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0dev)"]

[[package]]
name = "google-api-python-client"
version = "2.64.0"
description = "Google API Client Library for Python"
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
google-api-core = ">=1.31.5,<2.0.0 || >2.3.0,<3.0.0dev"
google-auth = ">=1.19.0,<3.0.0dev"
google-auth-httplib2 = ">=0.1.0"
httplib2 = ">=0.15.0,<1dev"
uritemplate = ">=3.0.1,<5"

[[package]]
name = "google-auth"
version = "2.12.0"
description = "Google Authentication Library"
category = "main"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"

[package.dependencies]
cachetools = ">=2.0.0,<6.0"
pyasn1-modules = ">=0.2.1"
rsa = {version = ">=3.1.4,<5", markers = "python_version >= \"3.6\""}
six = ">=1.9.0"

[package.extras]
aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)", "requests (>=2.20.0,<3.0.0dev)"]
enterprise_cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"]
pyopenssl = ["pyopenssl (>=20.0.0)"]
reauth = ["pyu2f (>=0.1.5)"]

[[package]]
name = "google-auth-httplib2"
version = "0.1.0"
description = "Google Authentication Library: httplib2 transport"
category = "main"
optional = false
python-versions = "*"

[package.dependencies]
google-auth = "*"
httplib2 = ">=0.15.0"
six = "*"

[[package]]
name = "googleapis-common-protos"
version = "1.56.4"
description = "Common protobufs used in Google APIs"
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
protobuf = ">=3.15.0,<5.0.0dev"

[package.extras]
grpc = ["grpcio (>=1.0.0,<2.0.0dev)"]

[[package]]
name = "httplib2"
version = "0.20.4"
description = "A comprehensive HTTP client library."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"

[package.dependencies]
pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""}

[[package]]
name = "identify"
version = "2.5.6"
description = "File identification library for Python"
category = "main"
optional = false
python-versions = ">=3.7"

[package.extras]
license = ["ukkonen"]

[[package]]
name = "idna"
version = "3.4"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"

[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "main"
optional = false
python-versions = "*"

[[package]]
name = "isort"
version = "5.10.1"
description = "A Python utility / library to sort Python imports."
category = "main"
optional = false
python-versions = ">=3.6.1,<4.0"

[package.extras]
colors = ["colorama (>=0.4.3,<0.5.0)"]
pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
plugins = ["setuptools"]
requirements_deprecated_finder = ["pip-api", "pipreqs"]

[[package]]
name = "jinja2"
version = "3.1.2"
description = "A very fast and expressive template engine."
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
MarkupSafe = ">=2.0"

[package.extras]
i18n = ["Babel (>=2.7)"]

[[package]]
name = "jmespath"
version = "1.0.1"
description = "JSON Matching Expressions"
category = "main"
optional = false
python-versions = ">=3.7"

[[package]]
name = "jsonschema2md2"
version = "0.6.0"
description = "Convert JSON Schema to human-readable Markdown documentation"
category = "main"
optional = false
python-versions = ">=3.6,<4.0"

[package.dependencies]
click = ">=7"
PyYAML = ">=5.1"

[[package]]
name = "markupsafe"
version = "2.1.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false
python-versions = ">=3.7"

[[package]]
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
category = "main"
optional = false
python-versions = ">=3.6"

[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "main"
optional = false
python-versions = "*"

[[package]]
name = "nodeenv"
version = "1.7.0"
description = "Node.js virtual environment builder"
category = "main"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"

[package.dependencies]
setuptools = "*"

[[package]]
name = "ordered-set"
version = "4.1.0"
description = "An OrderedSet is a custom MutableSet that remembers its order, so that every"
category = "main"
optional = false
python-versions = ">=3.7"

[package.extras]
dev = ["black", "mypy", "pytest"]

[[package]]
name = "packaging"
version = "21.3"
description = "Core utilities for Python packages"
category = "main"
optional = false
python-versions = ">=3.6"

[package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"

[[package]]
name = "pathspec"
version = "0.10.1"
description = "Utility library for gitignore style pattern matching of file paths."
category = "main"
optional = false
python-versions = ">=3.7"

[[package]]
name = "platformdirs"
version = "2.5.2"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "main"
optional = false
python-versions = ">=3.7"

[package.extras]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]

[[package]]
name = "pluggy"
version = "1.0.0"
description = "plugin and hook calling mechanisms for python"
category = "main"
optional = false
python-versions = ">=3.6"

[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]

[[package]]
name = "pre-commit"
version = "2.20.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
cfgv = ">=2.0.0"
identify = ">=1.0.0"
nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
toml = "*"
virtualenv = ">=20.0.8"

[[package]]
name = "protobuf"
version = "4.21.7"
description = ""
category = "main"
optional = false
python-versions = ">=3.7"

[[package]]
name = "py"
version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"

[[package]]
name = "pyasn1"
version = "0.4.8"
description = "ASN.1 types and codecs"
category = "main"
optional = false
python-versions = "*"

[[package]]
name = "pyasn1-modules"
version = "0.2.8"
description = "A collection of ASN.1-based protocols modules."
category = "main"
optional = false
python-versions = "*"

[package.dependencies]
pyasn1 = ">=0.4.6,<0.5.0"

[[package]]
name = "pycodestyle"
version = "2.9.1"
description = "Python style guide checker"
category = "main"
optional = false
python-versions = ">=3.6"

[[package]]
name = "pydantic"
version = "1.10.2"
description = "Data validation and settings management using python type hints"
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
typing-extensions = ">=4.1.0"

[package.extras]
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]

[[package]]
name = "pyflakes"
version = "2.5.0"
description = "passive checker of Python programs"
category = "main"
optional = false
python-versions = ">=3.6"

[[package]]
name = "pyparsing"
version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "main"
optional = false
python-versions = ">=3.6.8"

[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]

[[package]]
name = "pytest"
version = "7.1.3"
description = "pytest: simple powerful testing with Python"
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
py = ">=1.8.2"
tomli = ">=1.0.0"

[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]

[[package]]
name = "pytest-cov"
version = "4.0.0"
description = "Pytest plugin for measuring coverage."
category = "main"
optional = false
python-versions = ">=3.6"

[package.dependencies]
coverage = {version = ">=5.2.1", extras = ["toml"]}
pytest = ">=4.6"

[package.extras]
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]

[[package]]
name = "python-dateutil"
version = "2.8.2"
description = "Extensions to the standard Python datetime module"
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"

[package.dependencies]
six = ">=1.5"

[[package]]
name = "pyyaml"
version = "6.0"
description = "YAML parser and emitter for Python"
category = "main"
optional = false
python-versions = ">=3.6"

[[package]]
name = "requests"
version = "2.28.1"
description = "Python HTTP for Humans."
category = "main"
optional = false
python-versions = ">=3.7, <4"

[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = ">=2,<3"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<1.27"

[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]

[[package]]
name = "rsa"
version = "4.9"
description = "Pure-Python RSA implementation"
category = "main"
optional = false
python-versions = ">=3.6,<4"

[package.dependencies]
pyasn1 = ">=0.1.3"

[[package]]
name = "ruamel.yaml"
version = "0.17.21"
description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
category = "main"
optional = false
python-versions = ">=3"

[package.dependencies]
"ruamel.yaml.clib" = {version = ">=0.2.6", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""}

[package.extras]
docs = ["ryd"]
jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"]

[[package]]
name = "ruamel.yaml.clib"
version = "0.2.6"
description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml"
category = "main"
optional = false
python-versions = ">=3.5"

[[package]]
name = "s3transfer"
version = "0.6.0"
description = "An Amazon S3 Transfer Manager"
category = "main"
optional = false
python-versions = ">= 3.7"

[package.dependencies]
botocore = ">=1.12.36,<2.0a.0"

[package.extras]
crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"]

[[package]]
name = "setuptools"
version = "65.5.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "main"
optional = false
python-versions = ">=3.7"

[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]

[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"

[[package]]
name = "slack-bolt"
version = "1.15.1"
description = "The Bolt Framework for Python"
category = "main"
optional = false
python-versions = ">=3.6"

[package.dependencies]
slack-sdk = ">=3.17.2,<4"

[package.extras]
adapter = ["CherryPy (>=18,<19)", "Django (>=3,<5)", "Flask (>=1,<3)", "Werkzeug (>=2,<3)", "boto3 (<=2)", "bottle (>=0.12,<1)", "chalice (>=1.26.5,<2)", "click (>=7,<8)", "falcon (>=2,<4)", "fastapi (>=0.70.0,<1)", "gunicorn (>=20,<21)", "pyramid (>=1,<3)", "sanic (>=22,<23)", "starlette (>=0.14,<1)", "tornado (>=6,<7)", "uvicorn (<1)", "websocket-client (>=1.2.3,<2)"]
adapter_testing = ["Flask (>=1,<2)", "Werkzeug (>=1,<2)", "boddle (>=0.2,<0.3)", "docker (>=5,<6)", "moto (>=3,<4)", "requests (>=2,<3)", "sanic-testing (>=0.7)"]
async = ["aiohttp (>=3,<4)", "websockets (>=10,<11)"]
testing = ["Flask-Sockets (>=0.2,<1)", "Jinja2 (==3.0.3)", "Werkzeug (>=1,<2)", "aiohttp (>=3,<4)", "black (==22.8.0)", "click (<=8.0.4)", "itsdangerous (==2.0.1)", "pytest (>=6.2.5,<7)", "pytest-asyncio (>=0.18.2,<1)", "pytest-cov (>=3,<4)"]
testing_without_asyncio = ["Flask-Sockets (>=0.2,<1)", "Jinja2 (==3.0.3)", "Werkzeug (>=1,<2)", "black (==22.8.0)", "click (<=8.0.4)", "itsdangerous (==2.0.1)", "pytest (>=6.2.5,<7)", "pytest-cov (>=3,<4)"]

[[package]]
name = "slack-sdk"
version = "3.19.1"
description = "The Slack API Platform SDK for Python"
category = "main"
optional = false
python-versions = ">=3.6.0"

[package.extras]
optional = ["SQLAlchemy (>=1,<2)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=10,<11)"]
testing = ["Flask (>=1,<2)", "Flask-Sockets (>=0.2,<1)", "Jinja2 (==3.0.3)", "Werkzeug (<2)", "black (==22.8.0)", "boto3 (<=2)", "click (==8.0.4)", "codecov (>=2,<3)", "databases (>=0.5)", "flake8 (>=5,<6)", "itsdangerous (==1.1.0)", "moto (>=3,<4)", "psutil (>=5,<6)", "pytest (>=6.2.5,<7)", "pytest-asyncio (<1)", "pytest-cov (>=2,<3)"]

[[package]]
name = "smmap"
version = "5.0.0"
description = "A pure Python implementation of a sliding window memory map manager"
category = "main"
optional = false
python-versions = ">=3.6"

[[package]]
name = "structlog"
version = "22.1.0"
description = "Structured Logging for Python"
category = "main"
optional = false
python-versions = ">=3.7"

[package.extras]
dev = ["cogapp", "coverage[toml]", "freezegun (>=0.2.8)", "furo", "myst-parser", "pre-commit", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "rich", "simplejson", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "tomli", "twisted"]
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "twisted"]
tests = ["coverage[toml]", "freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"]

[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"

[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "main"
optional = false
python-versions = ">=3.7"

[[package]]
name = "typing-extensions"
version = "4.4.0"
description = "Backported and Experimental Type Hints for Python 3.7+"
category = "main"
optional = false
python-versions = ">=3.7"

[[package]]
name = "ujson"
version = "5.5.0"
description = "Ultra fast JSON encoder and decoder for Python"
category = "main"
optional = false
python-versions = ">=3.7"

[[package]]
name = "uritemplate"
version = "4.1.1"
description = "Implementation of RFC 6570 URI Templates"
category = "main"
optional = false
python-versions = ">=3.6"

[[package]]
name = "urllib3"
version = "1.26.12"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"

[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]

[[package]]
name = "virtualenv"
version = "20.16.5"
description = "Virtual Python Environment builder"
category = "main"
optional = false
python-versions = ">=3.6"

[package.dependencies]
distlib = ">=0.3.5,<1"
filelock = ">=3.4.1,<4"
platformdirs = ">=2.4,<3"

[package.extras]
docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"]
testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"]

[[package]]
name = "xxhash"
version = "3.0.0"
description = "Python binding for xxHash"
category = "main"
optional = false
python-versions = ">=3.6"

[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "ce0563a0c7be9c1e29024d3bb1f926fc94ff737b144b858e95f13868df6a171c"

[metadata.files]
aiofiles = [
    {file = "aiofiles-22.1.0-py3-none-any.whl", hash = "sha256:1142fa8e80dbae46bb6339573ad4c8c0841358f79c6eb50a493dceca14621bad"},
    {file = "aiofiles-22.1.0.tar.gz", hash = "sha256:9107f1ca0b2a5553987a94a3c9959fe5b491fdf731389aa5b7b1bd0733e32de6"},
]
asgiref = [
    {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"},
    {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"},
]
attrs = [
    {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
    {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
]
black = [
    {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"},
    {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"},
    {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"},
    {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"},
    {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"},
    {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"},
    {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"},
    {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"},
    {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"},
    {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"},
    {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"},
    {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"},
    {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"},
    {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"},
    {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"},
    {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"},
    {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"},
]
boto3 = [
    {file = "boto3-1.24.88-py3-none-any.whl", hash = "sha256:6b4cf1cd0be65202c4cf0e4c69099bac3620bcd4049ca25a5e223c668401dd69"},
    {file = "boto3-1.24.88.tar.gz", hash = "sha256:93934343cac76084600a520e5be70c52152364d0c410681c2e25c2290f0e151c"},
]
botocore = [
    {file = "botocore-1.27.88-py3-none-any.whl", hash = "sha256:de4e087b24cd3bc369eb2e27f8fe94a6499f7dea08c919fba13cefb2496bd2bb"},
    {file = "botocore-1.27.88.tar.gz", hash = "sha256:ded0a4035baf91eb358ef501c92a8512543f5ab7658f459df3077a70a555b5cd"},
]
cachetools = [
    {file = "cachetools-5.2.0-py3-none-any.whl", hash = "sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db"},
    {file = "cachetools-5.2.0.tar.gz", hash = "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757"},
]
certifi = [
    {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
    {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
]
cfgv = [
    {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
    {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
]
charset-normalizer = [
    {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
    {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
]
click = [
    {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
    {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
colorama = [
    {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
    {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
coverage = [
    {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"},
    {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"},
    {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"},
    {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"},
    {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"},
    {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"},
    {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"},
    {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"},
    {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"},
    {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"},
    {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"},
    {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"},
    {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"},
    {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"},
    {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"},
    {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"},
    {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"},
    {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"},
    {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"},
    {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"},
    {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"},
    {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"},
    {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"},
    {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"},
    {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"},
    {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"},
    {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"},
    {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"},
    {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"},
    {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"},
    {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"},
    {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"},
    {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"},
    {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"},
    {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"},
    {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"},
    {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"},
    {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"},
    {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"},
    {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"},
    {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"},
    {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"},
    {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"},
    {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"},
    {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"},
    {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"},
    {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"},
    {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"},
    {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"},
    {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"},
]
deepdiff = [
    {file = "deepdiff-5.8.1-py3-none-any.whl", hash = "sha256:e9aea49733f34fab9a0897038d8f26f9d94a97db1790f1b814cced89e9e0d2b7"},
    {file = "deepdiff-5.8.1.tar.gz", hash = "sha256:8d4eb2c4e6cbc80b811266419cb71dd95a157094a3947ccf937a94d44943c7b8"},
]
distlib = [
    {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
    {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
]
filelock = [
    {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"},
    {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"},
]
flake8 = [
    {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"},
    {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"},
]
gitdb = [
    {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"},
    {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"},
]
GitPython = [
    {file = "GitPython-3.1.29-py3-none-any.whl", hash = "sha256:41eea0deec2deea139b459ac03656f0dd28fc4a3387240ec1d3c259a2c47850f"},
    {file = "GitPython-3.1.29.tar.gz", hash = "sha256:cc36bfc4a3f913e66805a28e84703e419d9c264c1077e537b54f0e1af85dbefd"},
]
google-api-core = [
    {file = "google-api-core-2.10.2.tar.gz", hash = "sha256:10c06f7739fe57781f87523375e8e1a3a4674bf6392cd6131a3222182b971320"},
    {file = "google_api_core-2.10.2-py3-none-any.whl", hash = "sha256:34f24bd1d5f72a8c4519773d99ca6bf080a6c4e041b4e9f024fe230191dda62e"},
]
google-api-python-client = [
    {file = "google-api-python-client-2.64.0.tar.gz", hash = "sha256:0dc4c967a5c795e981af01340f1bd22173a986534de968b5456cb208ed6775a6"},
    {file = "google_api_python_client-2.64.0-py2.py3-none-any.whl", hash = "sha256:90545cd71969f8bcf15a6362c2a8c44c38b94ec35a88cfd60cf2c0df68a5eb74"},
]
google-auth = [
    {file = "google-auth-2.12.0.tar.gz", hash = "sha256:f12d86502ce0f2c0174e2e70ecc8d36c69593817e67e1d9c5e34489120422e4b"},
    {file = "google_auth-2.12.0-py2.py3-none-any.whl", hash = "sha256:98f601773978c969e1769f97265e732a81a8e598da3263895023958d456ee625"},
]
google-auth-httplib2 = [
    {file = "google-auth-httplib2-0.1.0.tar.gz", hash = "sha256:a07c39fd632becacd3f07718dfd6021bf396978f03ad3ce4321d060015cc30ac"},
    {file = "google_auth_httplib2-0.1.0-py2.py3-none-any.whl", hash = "sha256:31e49c36c6b5643b57e82617cb3e021e3e1d2df9da63af67252c02fa9c1f4a10"},
]
googleapis-common-protos = [
    {file = "googleapis-common-protos-1.56.4.tar.gz", hash = "sha256:c25873c47279387cfdcbdafa36149887901d36202cb645a0e4f29686bf6e4417"},
    {file = "googleapis_common_protos-1.56.4-py2.py3-none-any.whl", hash = "sha256:8eb2cbc91b69feaf23e32452a7ae60e791e09967d81d4fcc7fc388182d1bd394"},
]
httplib2 = [
    {file = "httplib2-0.20.4-py3-none-any.whl", hash = "sha256:8b6a905cb1c79eefd03f8669fd993c36dc341f7c558f056cb5a33b5c2f458543"},
    {file = "httplib2-0.20.4.tar.gz", hash = "sha256:58a98e45b4b1a48273073f905d2961666ecf0fbac4250ea5b47aef259eb5c585"},
]
identify = [
    {file = "identify-2.5.6-py2.py3-none-any.whl", hash = "sha256:b276db7ec52d7e89f5bc4653380e33054ddc803d25875952ad90b0f012cbcdaa"},
    {file = "identify-2.5.6.tar.gz", hash = "sha256:6c32dbd747aa4ceee1df33f25fed0b0f6e0d65721b15bd151307ff7056d50245"},
]
idna = [
    {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
    {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
]
iniconfig = [
    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
isort = [
    {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
    {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
]
jinja2 = [
    {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
    {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
]
jmespath = [
    {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"},
    {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"},
]
jsonschema2md2 = [
    {file = "jsonschema2md2-0.6.0-py3-none-any.whl", hash = "sha256:d71fe2c8418a93821597bfe151636e25eccedc41afa29e9ca875951f4966e451"},
    {file = "jsonschema2md2-0.6.0.tar.gz", hash = "sha256:9c397bf3977290267dde4293e88f33feee41f90d9f77c61857447be7ce16d15b"},
]
markupsafe = [
    {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
    {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
    {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"},
    {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"},
    {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"},
    {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"},
    {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"},
    {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"},
    {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"},
    {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"},
    {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"},
    {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"},
    {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"},
    {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"},
    {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"},
    {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"},
    {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"},
    {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"},
    {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"},
    {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"},
    {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
    {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
]
mccabe = [
    {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
    {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
mypy-extensions = [
    {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
    {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
nodeenv = [
    {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
    {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},
]
ordered-set = [
    {file = "ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"},
    {file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"},
]
packaging = [
    {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
    {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
pathspec = [
    {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"},
    {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"},
]
platformdirs = [
    {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
    {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
]
pluggy = [
    {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
    {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
pre-commit = [
    {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"},
    {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"},
]
protobuf = [
    {file = "protobuf-4.21.7-cp310-abi3-win32.whl", hash = "sha256:c7cb105d69a87416bd9023e64324e1c089593e6dae64d2536f06bcbe49cd97d8"},
    {file = "protobuf-4.21.7-cp310-abi3-win_amd64.whl", hash = "sha256:3ec85328a35a16463c6f419dbce3c0fc42b3e904d966f17f48bae39597c7a543"},
    {file = "protobuf-4.21.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:db9056b6a11cb5131036d734bcbf91ef3ef9235d6b681b2fc431cbfe5a7f2e56"},
    {file = "protobuf-4.21.7-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:ca200645d6235ce0df3ccfdff1567acbab35c4db222a97357806e015f85b5744"},
    {file = "protobuf-4.21.7-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:b019c79e23a80735cc8a71b95f76a49a262f579d6b84fd20a0b82279f40e2cc1"},
    {file = "protobuf-4.21.7-cp37-cp37m-win32.whl", hash = "sha256:d3f89ccf7182293feba2de2739c8bf34fed1ed7c65a5cf987be00311acac57c1"},
    {file = "protobuf-4.21.7-cp37-cp37m-win_amd64.whl", hash = "sha256:a74d96cd960b87b4b712797c741bb3ea3a913f5c2dc4b6cbe9c0f8360b75297d"},
    {file = "protobuf-4.21.7-cp38-cp38-win32.whl", hash = "sha256:8e09d1916386eca1ef1353767b6efcebc0a6859ed7f73cb7fb974feba3184830"},
    {file = "protobuf-4.21.7-cp38-cp38-win_amd64.whl", hash = "sha256:9e355f2a839d9930d83971b9f562395e13493f0e9211520f8913bd11efa53c02"},
    {file = "protobuf-4.21.7-cp39-cp39-win32.whl", hash = "sha256:f370c0a71712f8965023dd5b13277444d3cdfecc96b2c778b0e19acbfd60df6e"},
    {file = "protobuf-4.21.7-cp39-cp39-win_amd64.whl", hash = "sha256:9643684232b6b340b5e63bb69c9b4904cdd39e4303d498d1a92abddc7e895b7f"},
    {file = "protobuf-4.21.7-py2.py3-none-any.whl", hash = "sha256:8066322588d4b499869bf9f665ebe448e793036b552f68c585a9b28f1e393f66"},
    {file = "protobuf-4.21.7-py3-none-any.whl", hash = "sha256:58b81358ec6c0b5d50df761460ae2db58405c063fd415e1101209221a0a810e1"},
    {file = "protobuf-4.21.7.tar.gz", hash = "sha256:71d9dba03ed3432c878a801e2ea51e034b0ea01cf3a4344fb60166cb5f6c8757"},
]
py = [
    {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
    {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pyasn1 = [
    {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
    {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
]
pyasn1-modules = [
    {file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"},
    {file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"},
]
pycodestyle = [
    {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
    {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"},
]
pydantic = [
    {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"},
    {file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"},
    {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912"},
    {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559"},
    {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236"},
    {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c"},
    {file = "pydantic-1.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644"},
    {file = "pydantic-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f"},
    {file = "pydantic-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a"},
    {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525"},
    {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283"},
    {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42"},
    {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52"},
    {file = "pydantic-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c"},
    {file = "pydantic-1.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5"},
    {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c"},
    {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254"},
    {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5"},
    {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d"},
    {file = "pydantic-1.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2"},
    {file = "pydantic-1.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13"},
    {file = "pydantic-1.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116"},
    {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624"},
    {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1"},
    {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9"},
    {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965"},
    {file = "pydantic-1.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e"},
    {file = "pydantic-1.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488"},
    {file = "pydantic-1.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41"},
    {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b"},
    {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe"},
    {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d"},
    {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda"},
    {file = "pydantic-1.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6"},
    {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"},
    {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"},
]
pyflakes = [
    {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"},
    {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
]
pyparsing = [
    {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
    {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
pytest = [
    {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"},
    {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"},
]
pytest-cov = [
    {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"},
    {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"},
]
python-dateutil = [
    {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
    {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
]
pyyaml = [
    {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
    {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
    {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
    {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
    {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
    {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
    {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
    {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
    {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
    {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
    {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
    {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
    {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
    {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
    {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
    {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
    {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
    {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
    {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
    {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
    {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
    {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
    {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
    {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
    {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
]
requests = [
    {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
    {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
]
rsa = [
    {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"},
    {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"},
]
"ruamel.yaml" = [
    {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"},
    {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"},
]
"ruamel.yaml.clib" = [
    {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6e7be2c5bcb297f5b82fee9c665eb2eb7001d1050deaba8471842979293a80b0"},
    {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:066f886bc90cc2ce44df8b5f7acfc6a7e2b2e672713f027136464492b0c34d7c"},
    {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:221eca6f35076c6ae472a531afa1c223b9c29377e62936f61bc8e6e8bdc5f9e7"},
    {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win32.whl", hash = "sha256:1070ba9dd7f9370d0513d649420c3b362ac2d687fe78c6e888f5b12bf8bc7bee"},
    {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:77df077d32921ad46f34816a9a16e6356d8100374579bc35e15bab5d4e9377de"},
    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751"},
    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527"},
    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win32.whl", hash = "sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5"},
    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win_amd64.whl", hash = "sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c"},
    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502"},
    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78"},
    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d3c620a54748a3d4cf0bcfe623e388407c8e85a4b06b8188e126302bcab93ea8"},
    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win32.whl", hash = "sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94"},
    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win_amd64.whl", hash = "sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468"},
    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd"},
    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99"},
    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:210c8fcfeff90514b7133010bf14e3bad652c8efde6b20e00c43854bf94fa5a6"},
    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win32.whl", hash = "sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb"},
    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe"},
    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233"},
    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84"},
    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:61bc5e5ca632d95925907c569daa559ea194a4d16084ba86084be98ab1cec1c6"},
    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win32.whl", hash = "sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b"},
    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277"},
    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed"},
    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0"},
    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:1b4139a6ffbca8ef60fdaf9b33dec05143ba746a6f0ae0f9d11d38239211d335"},
    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win32.whl", hash = "sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104"},
    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7"},
    {file = "ruamel.yaml.clib-0.2.6.tar.gz", hash = "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd"},
]
s3transfer = [
    {file = "s3transfer-0.6.0-py3-none-any.whl", hash = "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd"},
    {file = "s3transfer-0.6.0.tar.gz", hash = "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"},
]
setuptools = [
    {file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"},
    {file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"},
]
six = [
    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
slack-bolt = [
    {file = "slack_bolt-1.15.1-py2.py3-none-any.whl", hash = "sha256:583a319734ee7de2fca758605c92253de7b3c9ef72d0c38251126c055af0d1c1"},
    {file = "slack_bolt-1.15.1.tar.gz", hash = "sha256:8ead9331d7733a1ed364c4c57cf7a10fcb8c4be509ddba778c04e9efee9883cf"},
]
slack-sdk = [
    {file = "slack_sdk-3.19.1-py2.py3-none-any.whl", hash = "sha256:a0bafb9e53ffbc408a8b5a42443525aac2c0c7347223d9373dd9d41cf90c9343"},
    {file = "slack_sdk-3.19.1.tar.gz", hash = "sha256:5db78a3ed3c58b3b0a2bdbe40bac43c7b00edcbc12ff6a7778ac5cbe3a3a2899"},
]
smmap = [
    {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
    {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
]
structlog = [
    {file = "structlog-22.1.0-py3-none-any.whl", hash = "sha256:760d37b8839bd4fe1747bed7b80f7f4de160078405f4b6a1db9270ccbfce6c30"},
    {file = "structlog-22.1.0.tar.gz", hash = "sha256:94b29b1d62b2659db154f67a9379ec1770183933d6115d21f21aa25cfc9a7393"},
]
toml = [
    {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
tomli = [
    {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
    {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
typing-extensions = [
    {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
    {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
]
ujson = [
    {file = "ujson-5.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ff4928dc1e9704b567171c16787238201fdbf023665573c12c02146fe1e02eec"},
    {file = "ujson-5.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1dc2f46c31ef22b0aaa28cd71be897bea271e700636658d573df9c43c49ebbd0"},
    {file = "ujson-5.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6019e3480d933d3698f2ecb4b46d64bfadd64e718f04fac36e681f3254b49a93"},
    {file = "ujson-5.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5179088ef6487c475604b7898731a6ddeeada7702cfb2162155b016703a8475"},
    {file = "ujson-5.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c04ae27e076d81a3839047d8eed57c1e17e361640616fd520d752375e3ba8f0c"},
    {file = "ujson-5.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:60a4b481978ea2aad8fe8af1ecc271624d01b3cf4b09e9b643dd2fe19c07634c"},
    {file = "ujson-5.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7a09d203983104918c62f2eef9406f24c355511f9217967df23e70fa7f5b54ff"},
    {file = "ujson-5.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b9812638d7aa8ecda2e8e1513fb4da999249603bffab7439a5f8f0bb362b0db"},
    {file = "ujson-5.5.0-cp310-cp310-win32.whl", hash = "sha256:33cd9084fefc74cbacf88c92fd260b61211e00bcde38d640c369e5dc34a2b4e1"},
    {file = "ujson-5.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:765d46f3d5e7a1d48075035e2d1a9164f683e3fccde834ca04602e6c588835bc"},
    {file = "ujson-5.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:278aa9d7cb56435c96d19f5d702e026bcf69f824e24b41e9b52706abd3565837"},
    {file = "ujson-5.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9585892091ae86045135d6a6129a644142d6a51b23e1428bb5de6d10bc0ce0c7"},
    {file = "ujson-5.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cdc46859024501c20ab74ad542cdf2f08b94b5ce384f2f569483fa3ed926d04"},
    {file = "ujson-5.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5bea13c73f36c4346808df3fa806596163a7962b6d28001ca2a391cab856089"},
    {file = "ujson-5.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3f4240d99d55eb97cb012e9adf401f5ed9cd827af0341ac44603832202b0d2"},
    {file = "ujson-5.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d93940664a5ccfd79f72dcb939b0c31a3479889f14f0eb95ec52976f8c0cae7d"},
    {file = "ujson-5.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:880c84ce59f49776cf120f77e7ca04877c97c6887917078dbc369eb47004d7cf"},
    {file = "ujson-5.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:977bf5be704a88d46bf5b228df8b44521b1f3119d741062191608b3a6a38f224"},
    {file = "ujson-5.5.0-cp311-cp311-win32.whl", hash = "sha256:e0b36257dc90194784531c3b922d8d31fb2b4d8e5adfd27aff4eee7174176365"},
    {file = "ujson-5.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:a34a5f034b339f69ef7f6a134c22d04b92e07b6ddc1dd65382e7e4ec65d6437d"},
    {file = "ujson-5.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f26544bc10c83a2ff9aa2e093500c1b473f327faae31fb468d591e5823333376"},
    {file = "ujson-5.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fd797a4837ba10671954e7c09010cec7aca67e09d193f4920a16beea5f66f65"},
    {file = "ujson-5.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d7cfac2547c93389fa303fc0c0eb6698825564e8389c41c9b60009c746207b6"},
    {file = "ujson-5.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4875cafc9a6482c04c7df52a725d1c41beb74913c0ff4ec8f189f1954a2afe9"},
    {file = "ujson-5.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0762a4fdf86e01f3f8d8b6b7158d01fdd870799ff3f402b676e358fcd879e7eb"},
    {file = "ujson-5.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6c7ae6e0778ab9610f5e80e0595957d101ab8de18c32a8c053a19943ef4831d0"},
    {file = "ujson-5.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:94874584b733a18b310b0e954d53168e62cd4a0fd9db85b1903f0902a7eb33e8"},
    {file = "ujson-5.5.0-cp37-cp37m-win32.whl", hash = "sha256:3b74467564814fbce322427a5664e6bcc7dae6dbc8acbef76300fe43ca4072ab"},
    {file = "ujson-5.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:59cdcd934385f36e8bd76aedc234371cc75c848d95bdce804ac8aa8744cfeffa"},
    {file = "ujson-5.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2e506ecf89b6b9d304362ccef770831ec242a52c89dab1b4aabf1ab0eb1d5ed6"},
    {file = "ujson-5.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:10095160dbe6bba8059ad6677a01da251431f4c68041bf796dcac0956b34f8f7"},
    {file = "ujson-5.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5035bb997d163f346c22abcec75190e7e756a5349e7c708bd3d5fd7066a9a854"},
    {file = "ujson-5.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7d12f2d2df195c8c4e49d2cdbad640353a856c62ca2c624d8b47aa33b65a2a2"},
    {file = "ujson-5.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a485117f97312bef45f5d79d2ff97eff4da503b8a04f3691f59d31141686459"},
    {file = "ujson-5.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:21678d7e068707e4d54bdfeb8c250ebc548b51e499aed778b22112ca31a79669"},
    {file = "ujson-5.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a9b1320d8363a42d857fae8065a2174d38217cdd58cd8dc4f48d54e0591271e"},
    {file = "ujson-5.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:701e81e047f5c0cffd4ac828efca68b0bd270c616654966a051e9a5f836b385e"},
    {file = "ujson-5.5.0-cp38-cp38-win32.whl", hash = "sha256:1cef44ea4973344baed3d50a5da4a8843de3a6af7dea7fadf0a594e53ce5892f"},
    {file = "ujson-5.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e510d288e613d6927796dfb728e13e4530fc83b9ccac5888a21f7860486eab21"},
    {file = "ujson-5.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e1135264bcd40965cd35b0869e36952f54825024befdc7a923df9a7d83cfd800"},
    {file = "ujson-5.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:703fd69d9cb21d6ec2086789df9be2cf8140a76ff127050c24007ea8940dcd3b"},
    {file = "ujson-5.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:849f2ff40264152f25589cb48ddb4a43d14db811f841ec73989bfc0c8c4853fa"},
    {file = "ujson-5.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf416a93e1331820c77e3429df26946dbd4fe105e9b487cd2d1b7298b75784a8"},
    {file = "ujson-5.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:593a0f6fb0e186c5ba65465ed6f6215a30d1efa898c25e74de1c8577a1bff6d0"},
    {file = "ujson-5.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c20cc83b0df47129ec6ed8a47fa7dcfc309c5bad029464004162738502568bb"},
    {file = "ujson-5.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6f83be8257b2f2dd6dea5ee62cd28db90584da7a7af1fba77a2102fc7943638a"},
    {file = "ujson-5.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8141f654432cf75144d6103bfac2286b8adf23467201590b173a74535d6be22d"},
    {file = "ujson-5.5.0-cp39-cp39-win32.whl", hash = "sha256:3fe1aea596f9539fc20cd9e52f098c842afc090168824fd4ca9744fe13151a03"},
    {file = "ujson-5.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:a655f7b755cfc5c07f2116b6dcf0ba148c89adef9a6d40c1b0f1fada878c4345"},
    {file = "ujson-5.5.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f19f11055ba2961eb39bdb1ff15763a53fca4fa0b5b624da3c7a528e83cdd09c"},
    {file = "ujson-5.5.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d87c817b292efb748f1974f37e8bb8a8772ef92f05f84e507159360814bcc3f"},
    {file = "ujson-5.5.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f9681ec4c60d0da590552427d770636d9079038c30b265f507ccde23caa7823"},
    {file = "ujson-5.5.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f63d1ae1ca17bb2c847e298c7bcf084a73d56d434b4c50509fb93a4b4300b0b2"},
    {file = "ujson-5.5.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:95603eff711b8f3b9596e1c961dbeb745a792ba1904141612f194e07edd71e5f"},
    {file = "ujson-5.5.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2d90414e3b4b44b39825049185959488e084ea7fcaf6124afd5c00893938b09d"},
    {file = "ujson-5.5.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7471d4486f23518cff343f1eec6c68d1b977ed74c3e6cc3e1ac896b9b7d68645"},
    {file = "ujson-5.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee9a2c9a4b2421e77f8fe33ed0621dea03c66c710707553020b1e32f3afb6240"},
    {file = "ujson-5.5.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a8cb3c8637006c5bd8237ebb5992a76ba06e39988ad5cff2096227443e8fd6a"},
    {file = "ujson-5.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d9c89c521dc90c7564358e525f849b93ad1d710553c1491f66b8cce8113bc901"},
    {file = "ujson-5.5.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2ab011e3556a9a1d9461bd686870c527327765ed02fe53550531d6609a8a33ff"},
    {file = "ujson-5.5.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:603607f56a0ee84d9cd2c7e9b1d29b18a70684b94ee34f07b9ffe8dc9c8a9f81"},
    {file = "ujson-5.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75bef34e69e7effb7b4849e3f830e3174d2cc6ec7273503fdde111c222dc9b3"},
    {file = "ujson-5.5.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abfe83e082c9208891e2158c1b5044a650ecec408b823bf6bf16cd7f8085cafa"},
    {file = "ujson-5.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4ef4ab8352861b99bd7fedb1fc6df3ea7f7d5216c789ba6d859e4ea06f1a4c45"},
    {file = "ujson-5.5.0.tar.gz", hash = "sha256:b25077a971c7da47bd6846a912a747f6963776d90720c88603b1b55d81790780"},
]
uritemplate = [
    {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"},
    {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"},
]
urllib3 = [
    {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"},
    {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
]
virtualenv = [
    {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"},
    {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"},
]
xxhash = [
    {file = "xxhash-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:219cba13991fd73cf21a5efdafa5056f0ae0b8f79e5e0112967e3058daf73eea"},
    {file = "xxhash-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3fcbb846af15eff100c412ae54f4974ff277c92eacd41f1ec7803a64fd07fa0c"},
    {file = "xxhash-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f475fa817ff7955fc118fc1ca29a6e691d329b7ff43f486af36c22dbdcff1db"},
    {file = "xxhash-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9200a90f02ff6fd5fb63dea107842da71d8626d99b768fd31be44f3002c60bbe"},
    {file = "xxhash-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a1403e4f551c9ef7bcef09af55f1adb169f13e4de253db0887928e5129f87af1"},
    {file = "xxhash-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa7f6ca53170189a2268c83af0980e6c10aae69e6a5efa7ca989f89fff9f8c02"},
    {file = "xxhash-3.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b63fbeb6d9c93d50ae0dc2b8a8b7f52f2de19e40fe9edc86637bfa5743b8ba2"},
    {file = "xxhash-3.0.0-cp310-cp310-win32.whl", hash = "sha256:31f25efd10b6f1f6d5c34cd231986d8aae9a42e042daa90b783917f170807869"},
    {file = "xxhash-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:807e88ed56e0fb347cb57d5bf44851f9878360fed700f2f63e622ef4eede87a5"},
    {file = "xxhash-3.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6d612c55a75d84d25898f6c5ad6a589aa556d1cb9af770b6c574ee62995167f6"},
    {file = "xxhash-3.0.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f9309fcaf73f93df3101f03a61dc30644adff3e8d0044fff8c0c195dbbe63e2"},
    {file = "xxhash-3.0.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2273fe40720e86346a17f06ef95cd60ee0d66ffce7cf55e390ef7350112b16d"},
    {file = "xxhash-3.0.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc6f3a334587c83c5ba56c19b254a97542ce1fc05ccfd66fbf568e6117718d65"},
    {file = "xxhash-3.0.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36cf410da5bfcca51ac3c2c51a3317dcd7af91f70fa61eca57fba39554f06ae3"},
    {file = "xxhash-3.0.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:21752a3e9a2391d91bd51f4aa2fe028ae14ba6a8d37db9ebe00ccac10be5ac4a"},
    {file = "xxhash-3.0.0-cp36-cp36m-win32.whl", hash = "sha256:322068a063ef156455a401ab720f0892f2d2dd1540c1a308e95a7cbf356df51c"},
    {file = "xxhash-3.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2984fa9a880587c0bfa46d32717b2d209863ee68727ea0fc17f05fce25efa692"},
    {file = "xxhash-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6493dd938b360235da81b1c79d8cd048c4f11977e1159b4e744c54f98d3a7bb4"},
    {file = "xxhash-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb9eca32f9b4acc7149db2c86f8108167b9929b7da1887d4287a90cfdb3ea53a"},
    {file = "xxhash-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4125e70e4e1d79992d81de837a0586aa0241665dbc5ce01b9c89330ed5cbb66"},
    {file = "xxhash-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:583bea142569485bdb0c5900e804058c16edba1850b74519688c22bc546e6175"},
    {file = "xxhash-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f3adf2891acc18abacd15113e9cbbefd30e5f4ecaae32c23e5486fc09c76ea5"},
    {file = "xxhash-3.0.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed65a2671d380ae05262ce1e4ccc2b63f3c30506d207bf6fae8cd72be0ad65d4"},
    {file = "xxhash-3.0.0-cp37-cp37m-win32.whl", hash = "sha256:c604b3dcac9d37e3fceaa11884927024953260cc4224d9b89400d16e6cf34021"},
    {file = "xxhash-3.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1c6fc59e182506496544bc6d426bcf6077066ed1b40cfcd937f707cc06c7ef50"},
    {file = "xxhash-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5628375dbb76d33b93b44854a6c5433e2a78115e03ea2ae1bb74a34ab012a43f"},
    {file = "xxhash-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:687aa4373690f23a3f43cc23d81005304d284ff6c041bff1f967664ab6410f36"},
    {file = "xxhash-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fa2100fb68b163e99370561c9e29ed37b9153fe99443600bea28829150eb0e4"},
    {file = "xxhash-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:891d7651431a055f76fe2c8f86c593c3dede8ec5b10ca55e8ff5c9fdceb55f0b"},
    {file = "xxhash-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:197c32d7b62be02957ca31aa69febadf9c5a34ef953053ea16e2c72465bc450f"},
    {file = "xxhash-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91fa4df41bda3cbec4084d9696028780b47128c1f8450d1ad9c3e4b6bf8b1f99"},
    {file = "xxhash-3.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cd38b766fc40e9fe37b80112656d2e5a0cb2f9bc12e01b286353b5ecd2768e8"},
    {file = "xxhash-3.0.0-cp38-cp38-win32.whl", hash = "sha256:4258ef78f5a7d1f9c595846134c7d81a868c74942051453258eb383498662d4d"},
    {file = "xxhash-3.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:b82b1cf4407ad908e04e864473cc3baa8e764c7bbebea959150764cc681a1611"},
    {file = "xxhash-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:da4d91e28418469b29eed8635c08af28b588e51cd04288bed1ba1cf60f2d91f6"},
    {file = "xxhash-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48aab36169b0c00e586cb4eb2814ab8bfed686933126019906f917ff9a78c99e"},
    {file = "xxhash-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b0d522570c9ccea6203b3d96ac7f0cfc1d29e613640475d513be432545c48cc"},
    {file = "xxhash-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6054434ddb060685e86e7457f52d188b0886834baaa532f9f78b4f2b53cfd9b"},
    {file = "xxhash-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbf546ca5f5903ceeb46d9e6abf81f3a64edb95bb7dbe0f75283eec93a7eb2a0"},
    {file = "xxhash-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22704f23f23ccbe892cee3e7568c67f07ac25beaa2d1cff183274005d9d39149"},
    {file = "xxhash-3.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83198e223bcc4b2418b5282ac930e444738c2a33859dee4e570b25c8433d83a2"},
    {file = "xxhash-3.0.0-cp39-cp39-win32.whl", hash = "sha256:3bcd4cd9b22293ea1c08822518fbb6d933c2960d66662d468a1945a45cace194"},
    {file = "xxhash-3.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5dd4c37da3408d56ae942dc103f4ae3b43510daa4f5accd0a411fc6e914f10a"},
    {file = "xxhash-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:485f172abc03f78afd4f38dbdbb5665f59c5487126fa4c3181c6582cda4de03b"},
    {file = "xxhash-3.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:035248b3d7ab6deb7b247278494d293b9faccfa853078319d25e2926f566b2f8"},
    {file = "xxhash-3.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30ae90c0cfd10ffe852c6b0f263253782eea74a8189d5f2440f6595c1e8047e"},
    {file = "xxhash-3.0.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8fd203d8a3c013e679722047ef4f061f690c6cff49380622444bca4c30f3bf23"},
    {file = "xxhash-3.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:6d60059aaef12a01c0cc24f1d7aaaab7933ae9f4b7adfd9ebbd37dc7ceac1745"},
    {file = "xxhash-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:676c97bf7cc298b65eec0368c2cb5611d87a8e876930843311ca728f69292752"},
    {file = "xxhash-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2245c6e20e96e3f8fdfb61ad6bc5cde6ce8a1c2b93aa4a32a27bba7ab3aeaf12"},
    {file = "xxhash-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae926a52d020085a2d7f69d0e2155cbf819ae409f2e5dbb345dd40a6462de32"},
    {file = "xxhash-3.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a2efdcb811be3edc520b78364c11a1e54f5d8e5db895a9ff2bcdd4a7ffa36a5"},
    {file = "xxhash-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:885b3a851980056707ab99a2c19c35dfe2c2ba5f602066dbfcd8af45ea855760"},
    {file = "xxhash-3.0.0.tar.gz", hash = "sha256:30b2d97aaf11fb122023f6b44ebb97c6955e9e00d7461a96415ca030b5ceb9c7"},
]

README.md METASEP
# iambic
> IAM-better-in-code
The python package used to generate, parse, and execute noqform yaml templates.

# Template Generation

## How to run
`python -m iambic.main import -c demo/config.yaml`

## Design Decisions
### Why write to disk?
So we don't have to hold everything in memory which could get rough for tenants with hundreds of accounts. Instead, we keep a small ref to the role.

## How it works

### `base_group_str_attribute(account_configs: list[AccountConfig], account_resources: list[dict]) -> Union[str | dict[str, list]]`
Groups a string attribute by a shared name across of accounts.
The ability to pass in and maintain arbitrary keys is necessary for parsing resource names related to a boto3 response.
An example of this would be roles and maintaining the reference to the role's file path. 

    :param account_configs: list[AccountConfig]
    :param account_resources: list[dict(account_id:str, resources=list[dict(resource_val: str, **)])]
    :return: dict(attribute_val: str = list[dict(resource_val: str, account_id: str, **)])

It works by creating a map of the 2 different representations of the string. 
That is the raw string and the string with jinja compatible variables added.
An example of this would be `prod_admin_role` vs `{{account_name}}_admin_role`.

Another map is then created where the key is the index position of the original value in `resources` and the value is the different representations of the string.

A nested for-loop then checks for string matches across accounts. This is used to form the response.
When there's a hit the reference maps created earlier are nulled out.

A final for loop creates the one-off resource values (`resource_val`). 
These are values only observed in 1 account. It works by looking for references that are not None.
If multiple representations of the value are observed (`prod_admin_role` vs `{{account_name}}_admin_role`) the raw string is used `prod_admin_role`.

Normally you will use `group_int_or_str_attribute` which handles normalizing `base_group_str_attribute`'s output


### base_group_dict_attribute(account_configs: list[AccountConfig], account_resources: list[dict]):
Groups an attribute that is a dict or list of dicts by with matching accounts

    :param account_configs: list[AccountConfig]
    :param account_resources: list[dict(account_id:str, resources=list[dict])]
    :return: list[dict(included_accounts: str, resource_val=list[dict]|dict)]

The way `base_group_dict_attribute` works is very similar to `base_group_str_attribute`.
The primary distinction is, instead of using the resource values a hashed reference to the value is used.

The reason the `resource_val` key exists is due to the fact the value can be a list or dict.
It is the responsibility of the caller to format the response as needed.

Normally you will use `group_dict_attribute` which handles normalizing `base_group_dict_attribute`'s output

## Additional Documentation
* [Schemas](docs/SCHEMA.md)

iambic/slack/notifications.py METASEP
from iambic.config.models import Config


async def send_iam_mutation_message(
    config,
    identity: str,
    actor: str,
    event_source: str,
    event_name: str,
    session_name: str,
    cloudtrail_event: dict,
):
    event = event_source.split(".")[0] + ":" + event_name
    blocks = [
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "An unauthorized Cloud Identity Modification was detected and automatically remediated.\n\n*<fakeLink.toEmployeeProfile.com|Click here to view CloudTrail Logs>*",
            },
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": f"*Identity:*\t<fakeLink.toNoqRole.com|{identity}>",
            },
        },
        {"type": "section", "text": {"type": "mrkdwn", "text": f"*Action:*\t{event}"}},
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": f"*Actor:*\t<fakeLink.toNoqRole.com|{actor}>",
            },
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": f"*Session Name:*\t{session_name}",
            },
        },
        {
            "type": "actions",
            "elements": [
                {
                    "type": "button",
                    "text": {
                        "type": "plain_text",
                        "emoji": True,
                        "text": "Approve and Submit Request",
                    },
                    "style": "primary",
                    "value": "approve",
                },
                {
                    "type": "button",
                    "text": {"type": "plain_text", "emoji": True, "text": "Ignore"},
                    "style": "danger",
                    "value": "ignore",
                },
            ],
        },
    ]

    await send_slack_notification_to_channel(
        config,
        config.slack["alert_channel_id"],
        "An unauthorized Cloud Identity Modification was detected and automatically remediated.",
        blocks,
    )


async def send_slack_notification_to_channel(
    config: Config, channel_id, text, blocks
) -> bool:
    if not (slack_app := config.slack_app):
        return False

    slack_app.client.chat_postMessage(channel=channel_id, text=text, blocks=blocks)
    return True

iambic/slack/__init__.py METASEP

iambic/request_handler/generate.py METASEP
import asyncio
import os

from iambic.aws.iam.role.template_generation import (
    ROLE_RESPONSE_DIR,
    generate_aws_role_templates,
)
from iambic.config.models import Config
from iambic.google.models import generate_group_templates


async def generate_templates(configs: list[Config], output_dir):
    # TODO: Create a setting to enable support for google groups
    # TODO: Ensure google_groups are not excluded from sync
    response_dir_list = [output_dir, ROLE_RESPONSE_DIR]
    for response_dir in response_dir_list:
        os.makedirs(str(response_dir), exist_ok=True)

    tasks = [generate_aws_role_templates(configs, output_dir)]
    for config in configs:
        if config.google and config.google.groups.enabled:
            tasks.extend(
                generate_group_templates(config, "noq.dev", output_dir) for config in configs
            )

    await asyncio.gather(*tasks)

iambic/request_handler/detect.py METASEP
# Intentionally hacky for demo
import json

from iambic.config.models import Config
from iambic.core.logger import log
from iambic.slack.notifications import send_iam_mutation_message


async def detect_changes(config: Config) -> bool:
    queue_arn = config.sqs.get("queues", {}).get("iam_mutation", {}).get("arn", "")
    if not queue_arn:
        log.info("No queue arn found. Returning")
        return False

    queue_name = queue_arn.split(":")[-1]
    session = config.get_boto_session_from_arn(queue_arn, "us-east-1")
    identity = session.client("sts").get_caller_identity()
    identity_arn_with_session_name = (
        identity["Arn"].replace(":sts:", ":iam:").replace("assumed-role", "role")
    )
    identity_arn = "/".join(identity_arn_with_session_name.split("/")[0:2])
    sqs = session.client("sqs", region_name="us-east-1")
    queue_url_res = sqs.get_queue_url(QueueName=queue_name)
    queue_url = queue_url_res.get("QueueUrl")
    messages = sqs.receive_message(QueueUrl=queue_url, MaxNumberOfMessages=10).get(
        "Messages", []
    )

    while messages:
        processed_messages = []
        for message in messages:
            try:
                processed_messages.append(
                    {
                        "Id": message["MessageId"],
                        "ReceiptHandle": message["ReceiptHandle"],
                    }
                )
                message_body = json.loads(message["Body"])
                try:
                    if "Message" in message_body:
                        decoded_message = json.loads(message_body["Message"])["detail"]
                    else:
                        decoded_message = message_body["detail"]
                except Exception as err:
                    log.debug(
                        "Unable to process message", error=str(err), message=message
                    )
                    processed_messages.append(
                        {
                            "Id": message["MessageId"],
                            "ReceiptHandle": message["ReceiptHandle"],
                        }
                    )
                    continue
                role_name = decoded_message["requestParameters"]["roleName"]
                role_account_id = decoded_message.get(
                    "account", decoded_message.get("recipientAccountId")
                )
                role_arn = f"arn:aws:iam::{role_account_id}:role/{role_name}"
                actor = (
                    decoded_message.get("userIdentity", {})
                    .get("sessionContext", {})
                    .get("sessionIssuer", {})
                    .get("arn", "")
                )
                session_name = (
                    decoded_message.get("userIdentity", {})
                    .get("principalId")
                    .split(":")[-1]
                )
                if actor != identity_arn:
                    await send_iam_mutation_message(
                        config=config,
                        identity=role_arn,
                        actor=actor,
                        event_source=decoded_message["eventSource"],
                        event_name=decoded_message["eventName"],
                        session_name=session_name,
                        cloudtrail_event=decoded_message,
                    )
            except Exception as err:
                log.debug("Unable to process message", error=str(err), message=message)
                continue

        sqs.delete_message_batch(QueueUrl=queue_url, Entries=processed_messages)
        messages = sqs.receive_message(QueueUrl=queue_url, MaxNumberOfMessages=10).get(
            "Messages", []
        )
    return True

iambic/request_handler/apply.py METASEP
import asyncio
import os

from iambic.config.models import Config
from iambic.core.context import ctx
from iambic.core.logger import log
from iambic.core.parser import load_templates
from iambic.core.utils import remove_expired_resources, yaml


async def apply_changes(config: Config, template_paths: list[str]) -> bool:
    changes_made = await asyncio.gather(
        *[template.apply_all(config) for template in load_templates(template_paths)]
    )
    changes_made = any(changes_made)
    if ctx.execute and changes_made:
        log.info("Finished applying changes.")
    elif not ctx.execute:
        log.info("Finished scanning for changes.")
    else:
        log.info("No changes found.")

    return changes_made


async def flag_expired_resources(template_paths: list[str]):
    log.info("Scanning for expired resources")
    templates = await asyncio.gather(
        *[
            remove_expired_resources(
                template, template.resource_type, template.resource_name
            )
            for template in load_templates(template_paths)
        ]
    )

    for template in templates:
        with open(
            os.path.join(os.path.dirname(__file__), template.file_path), "w"
        ) as f:
            f.write(yaml.dump(template.dict(exclude_none=True, exclude_unset=True)))

    log.info("Expired resource scan complete.")

iambic/request_handler/__init__.py METASEP

iambic/google/models.py METASEP
import json
import os
from enum import Enum
from typing import List, Optional

import googleapiclient.discovery
from google.oauth2 import service_account
from pydantic import Field

from iambic.config.models import AccountConfig, Config
from iambic.core.logger import log
from iambic.core.models import ExpiryModel, NoqTemplate
from iambic.core.utils import aio_wrapper, yaml


class WhoCanInvite(Enum):
    ALL_MANAGERS_CAN_INVITE = "ALL_MANAGERS_CAN_INVITE"
    ALL_MEMBERS_CAN_INVITE = "ALL_MEMBERS_CAN_INVITE"


class WhoCanJoin(Enum):
    ALL_IN_DOMAIN_CAN_JOIN = "ALL_IN_DOMAIN_CAN_JOIN"
    ANYONE_CAN_JOIN = "ANYONE_CAN_JOIN"
    CAN_REQUEST_TO_JOIN = "CAN_REQUEST_TO_JOIN"


class WhoCanPostMessage(Enum):
    ALL_IN_DOMAIN_CAN_POST = "ALL_IN_DOMAIN_CAN_POST"
    ALL_MANAGERS_CAN_POST = "ALL_MANAGERS_CAN_POST"
    ALL_MEMBERS_CAN_POST = "ALL_MEMBERS_CAN_POST"
    ANYONE_CAN_POST = "ANYONE_CAN_POST"
    NONE_CAN_POST = "NONE_CAN_POST"


class WhoCanViewGroup(Enum):
    ALL_IN_DOMAIN_CAN_VIEW = "ALL_IN_DOMAIN_CAN_VIEW"
    ALL_MANAGERS_CAN_VIEW = "ALL_MANAGERS_CAN_VIEW"
    ALL_MEMBERS_CAN_VIEW = "ALL_MEMBERS_CAN_VIEW"
    ANYONE_CAN_VIEW = "ANYONE_CAN_VIEW"


class WhoCanViewMembership(Enum):
    ALL_IN_DOMAIN_CAN_VIEW = "ALL_IN_DOMAIN_CAN_VIEW"
    ALL_MANAGERS_CAN_VIEW = "ALL_MANAGERS_CAN_VIEW"
    ALL_MEMBERS_CAN_VIEW = "ALL_MEMBERS_CAN_VIEW"
    ANYONE_CAN_VIEW = "ANYONE_CAN_VIEW"


class GroupMemberRole(Enum):
    OWNER = "OWNER"
    MANAGER = "MANAGER"
    MEMBER = "MEMBER"


class GroupMemberSubscription(Enum):
    EACH_EMAIL = "EACH_EMAIL"
    DIGEST = "DIGEST"
    ABRIDGED = "ABRIDGED"
    NO_EMAIL = "NO_EMAIL"


class Posting(Enum):
    ALLOWED = "ALLOWED"
    NOT_ALLOWED = "NOT_ALLOWED"
    MODERATED = "MODERATED"


class GroupMemberType(Enum):
    USER = "USER"
    GROUP = "GROUP"
    EXTERNAL = "EXTERNAL"


class GroupMemberStatus(Enum):
    ACTIVE = "ACTIVE"
    INACTIVE = "INACTIVE"
    PENDING = "PENDING"


class GroupMember(ExpiryModel):
    email: str
    expand: bool = Field(
        False,
        description="Expand the group into the members of the group. This is useful for nested groups.",
    )
    role: GroupMemberRole = GroupMemberRole.MEMBER
    type: GroupMemberType = GroupMemberType.USER
    status: GroupMemberStatus = GroupMemberStatus.ACTIVE
    subscription: GroupMemberSubscription = GroupMemberSubscription.EACH_EMAIL


class GroupTemplate(NoqTemplate):
    template_type = "NOQ::Google::GroupTemplate"
    name: str
    email: str
    description: str
    welcome_message: Optional[str]
    members: List[GroupMember]
    who_can_invite: WhoCanInvite = "ALL_MANAGERS_CAN_INVITE"
    who_can_join: WhoCanJoin = "CAN_REQUEST_TO_JOIN"
    who_can_post_message: WhoCanPostMessage = "NONE_CAN_POST"
    who_can_view_group: WhoCanViewGroup = "ALL_MANAGERS_CAN_VIEW"
    who_can_view_membership: WhoCanViewMembership = "ALL_MANAGERS_CAN_VIEW"
    # TODO: who_can_contact_group_members
    # TODO: who_can_view_member_email_addresses
    # TODO: allow_email_posting
    # TODO: allow_web_posting
    # TODO: conversation_history
    # TODO: There is more. Check google group settings page

    async def _apply_to_account(self, account_config: AccountConfig) -> bool:
        # The bool represents whether the resource was altered in any way in the cloud
        raise NotImplementedError

    def resource_type(self):
        return "google:group"

    def resource_name(self):
        return self.name


async def get_service(
    config: Config,
    service_name: str,
    service_path: str,
):
    """
    Get a service connection to Google. You'll need to generate a GCP service account first.

    Noq requires that you either have a service key file with content like below,
    and you've set the configuration for `google.service_key_file` to the full path of that file on disk,
    or you've just put the json for this in your Noq configuration in the `secrets.google.service_key_dict` configuration
    key.


    """
    if not config.secrets:
        return

    service_key = config.secrets.get("google", {}).get("service_key")
    credential_subjects = config.secrets.get("google", {}).get("credential_subjects")

    admin_credentials = service_account.Credentials.from_service_account_info(
        service_key,
        scopes=[
            "https://www.googleapis.com/auth/admin.directory.user.security",
            "https://www.googleapis.com/auth/admin.reports.audit.readonly",
            "https://www.googleapis.com/auth/admin.directory.user",
            "https://www.googleapis.com/auth/admin.directory.group",
            "https://www.googleapis.com/auth/admin.directory.group.member",
        ],
    )

    credential_subject = None
    for k, v in credential_subjects.items():
        # TODO: If we want to support multiple domains in the feature, such as
        # noq.dev and noqcontractors.dev, we would need to account for this here.
        credential_subject = v
        break

    admin_delegated_credentials = admin_credentials.with_subject(credential_subject)
    service = await aio_wrapper(
        googleapiclient.discovery.build,
        service_name,
        service_path,
        credentials=admin_delegated_credentials,
        thread_sensitive=True,
    )

    return service


async def generate_group_templates(config, domain, output_dir):
    """List all groups in the domain, along with members and
    settings"""
    groups = []

    try:
        service = await get_service(config, "admin", "directory_v1")
        if not service:
            return []
    except AttributeError as err:
        log.exception("Unable to process google groups.", error=err)
        return

    req = service.groups().list(domain=domain)  # TODO: Async
    res = req.execute()
    if res and "groups" in res:
        for group in res["groups"]:
            member_req = service.members().list(groupKey=group["email"])
            member_res = member_req.execute() or {}
            members = [
                GroupMember(
                    email=member["email"],
                    role=GroupMemberRole(member["role"]),
                    type=GroupMemberType(member["type"]),
                    status=GroupMemberStatus(member["status"]),
                )
                for member in member_res.get("members", [])
            ]
            file_name = f"{group['email'].split('@')[0]}.yaml"
            groups.append(
                GroupTemplate(
                    file_path=f"google_groups/{domain}/{file_name}",
                    name=group["name"],
                    email=group["email"],
                    description=group["description"],
                    members=members,
                )
            )
    base_path = os.path.expanduser(output_dir)
    for group in groups:
        file_path = os.path.expanduser(group.file_path)
        path = os.path.join(base_path, file_path)
        os.makedirs(os.path.dirname(path), exist_ok=True)
        with open(path, "w") as f:
            f.write(
                yaml.dump(
                    {
                        "template_type": group.template_type,
                        **json.loads(
                            group.json(
                                exclude_unset=True,
                                exclude_defaults=True,
                                exclude={"file_path"},
                            )
                        ),
                    }
                )
            )
    return groups

iambic/google/__init__.py METASEP

iambic/config/templates.py METASEP
from iambic.aws.iam.role.models import RoleTemplate

TEMPLATES = [RoleTemplate]
TEMPLATE_TYPE_MAP = {
    template.__fields__["template_type"].default: template for template in TEMPLATES
}

iambic/config/models.py METASEP
import base64
from enum import Enum
from typing import List, Optional

import boto3
import botocore
from pydantic import BaseModel, Field, constr
from slack_bolt import App as SlackBoltApp

from iambic.aws.models import RegionName
from iambic.core.logger import log
from iambic.core.utils import yaml


class Variable(BaseModel):
    key: str
    value: str


class AccountConfig(BaseModel):
    account_id: constr(min_length=12, max_length=12) = Field(
        None, description="The AWS Account ID"
    )
    org_id: Optional[str] = Field(
        None,
        description="A unique identifier designating the identity of the organization",
    )
    account_name: Optional[str] = None
    default_region: Optional[RegionName] = Field(
        RegionName.us_east_1,
        description="Default region to use when making AWS requests",
    )
    aws_profile: Optional[str] = Field(
        None,
        description="The AWS profile used when making calls to the account",
    )
    assume_role_arn: Optional[str] = Field(
        None,
        description="The role arn to assume into when making calls to the account",
    )
    external_id: Optional[str] = Field(
        None,
        description="The external id to use for assuming into a role when making calls to the account",
    )
    role_access_tag: Optional[str] = Field(
        None,
        description="The key of the tag used to store users and groups that can assume into the role the tag is on",
    )
    variables: Optional[List[Variable]] = Field(
        [],
        description="A list of variables to be used when creating templates",
    )
    boto3_session_map: Optional[dict] = None
    read_only: Optional[bool] = Field(
        False,
        description="If set to True, iambic will only log drift instead of apply changes when drift is detected.",
    )

    def get_boto3_session(self, region_name: str = None):
        region_name = region_name or self.default_region

        if self.boto3_session_map is None:
            self.boto3_session_map = {}
        elif boto3_session := self.boto3_session_map.get(region_name):
            return boto3_session

        if self.aws_profile:
            try:
                self.boto3_session_map[region_name] = boto3.Session(
                    profile_name=self.aws_profile, region_name=region_name
                )
            except Exception as err:
                log.exception(err)
            else:
                return self.boto3_session_map[region_name]

        session = boto3.Session(region_name=region_name)
        if self.assume_role_arn and self.external_id:
            try:
                sts = session.client("sts")
                role = sts.assume_role(
                    RoleArn=self.assume_role_arn,
                    ExternalId=self.external_id,
                    RoleSessionName="NoqForm",
                )
                self.boto3_session_map[region_name] = boto3.Session(
                    region_name=region_name,
                    aws_access_key_id=role["Credentials"]["AccessKeyId"],
                    aws_secret_access_key=role["Credentials"]["SecretAccessKey"],
                    aws_session_token=role["Credentials"]["SessionToken"],
                )
            except Exception as err:
                log.exception(err)
            else:
                return self.boto3_session_map[region_name]

        self.boto3_session_map[region_name] = session
        return self.boto3_session_map[region_name]

    def get_boto3_client(self, service: str, region_name: str = None):
        return self.get_boto3_session(region_name).client(
            service, config=botocore.client.Config(max_pool_connections=50)
        )

    def __str__(self):
        return f"{self.account_name} - ({self.account_id})"

    def __init__(self, **kwargs):
        super(AccountConfig, self).__init__(**kwargs)
        self.default_region = self.default_region.value


class ExtendsConfigKey(Enum):
    AWS_SECRETS_MANAGER = "AWS_SECRETS_MANAGER"


class ExtendsConfig(BaseModel):
    key: ExtendsConfigKey
    value: str

class GoogleGroupsConfig(BaseModel):
    enabled: Optional[bool] = False

class GoogleConfig(BaseModel):
    groups: Optional[GoogleGroupsConfig] = None

class Config(BaseModel):
    accounts: List[AccountConfig]
    google: Optional[GoogleConfig] = None
    extends: List[ExtendsConfig] = []
    secrets: Optional[dict] = None
    role_access_tag: Optional[str] = Field(
        "noq-authorized",
        description="The key of the tag used to store users and groups that can assume into the role the tag is on",
    )
    variables: Optional[List[Variable]] = Field(
        [],
        description="A list of variables to be used when creating templates. "
        "These apply to all accounts but can be overwritten by an account.",
    )
    slack_app: Optional[str] = None
    sqs: Optional[dict] = {}
    slack: Optional[dict] = {}

    class Config:
        arbitrary_types_allowed = True

    def set_account_defaults(self):
        for elem, account in enumerate(self.accounts):
            if not account.role_access_tag:
                self.accounts[elem].role_access_tag = self.role_access_tag

            for variable in self.variables:
                if variable.key not in [av.key for av in account.variables]:
                    self.accounts[elem].variables.append(variable)

    def get_aws_secret(self, secret_arn: str) -> dict:
        """TODO: Secrets should be moved to the account to prevent an anti-pattern
        It also makes it required for every account to have access to the account the secret exists on
        Example: If the secret is in prod
          A build in the staging account won't work unless it has access to the prod secret
        """
        session = self.get_boto_session_from_arn(secret_arn)
        client = session.client(service_name="secretsmanager")
        get_secret_value_response = client.get_secret_value(SecretId=secret_arn)
        if "SecretString" in get_secret_value_response:
            return_val = get_secret_value_response["SecretString"]
        else:
            return_val = base64.b64decode(get_secret_value_response["SecretBinary"])

        return yaml.load(return_val)

    def configure_slack(self):
        if self.secrets and (
            slack_bot_token := self.secrets.get("slack", {}).get("bot_token")
        ):
            self.slack_app = SlackBoltApp(token=slack_bot_token)

    def combine_extended_configs(self):
        if self.extends:
            for extend in self.extends:
                if extend.key == ExtendsConfigKey.AWS_SECRETS_MANAGER:
                    for k, v in self.get_aws_secret(extend.value).items():
                        if not getattr(self, k):
                            setattr(self, k, v)

    def get_boto_session_from_arn(self, arn: str, region_name: str = None):
        region_name = region_name or arn.split(":")[3]
        account_id = arn.split(":")[4]
        account_config_map = {account.account_id: account for account in self.accounts}
        account_config = account_config_map[account_id]
        return account_config.get_boto3_session(region_name)

    @classmethod
    def load(cls, file_path: str):
        c = cls(file_path=file_path, **yaml.load(open(file_path)))
        c.combine_extended_configs()
        c.configure_slack()
        return c

iambic/config/__init__.py METASEP

iambic/aws/utils.py METASEP
from iambic.core.utils import aio_wrapper


async def paginated_search(
    search_fnc, response_key: str, max_results: int = None, **search_kwargs
) -> list:
    """Retrieve and aggregate each paged response, returning a single list of each response object
    :param search_fnc:
    :param response_key:
    :param max_results:
    :return:
    """
    results = []

    while True:
        response = await aio_wrapper(search_fnc, **search_kwargs)
        results.extend(response.get(response_key, []))

        if not response["IsTruncated"] or (max_results and len(results) >= max_results):
            return results
        else:
            search_kwargs["Marker"] = response["Marker"]

iambic/aws/models.py METASEP
from enum import Enum

ARN_RE = r"(^arn:([^:]*):([^:]*):([^:]*):(|\*|[\d]{12}|cloudfront|aws|{{account_id}}):(.+)$)|^\*$"


class RegionName(Enum):
    us_east_1 = "us-east-1"
    us_west_1 = "us-west-1"
    us_west_2 = "us-west-2"
    eu_west_1 = "eu-west-1"
    eu_west_2 = "eu-west-2"
    eu_central_1 = "eu-central-1"
    ap_southeast_1 = "ap-southeast-1"
    ap_southeast_2 = "ap-southeast-2"
    ap_northeast_1 = "ap-northeast-1"
    ap_northeast_2 = "ap-northeast-2"
    sa_east_1 = "sa-east-1"
    cn_north_1 = "cn-north-1"

iambic/aws/__init__.py METASEP

iambic/main.py METASEP
import asyncio
import pathlib
import warnings

import click

from iambic.config.models import Config
from iambic.core.context import ctx
from iambic.core.utils import gather_templates
from iambic.request_handler.apply import apply_changes, flag_expired_resources
from iambic.request_handler.detect import detect_changes
from iambic.request_handler.generate import generate_templates

warnings.filterwarnings("ignore", category=FutureWarning, module="botocore.client")


@click.group()
def cli():
    ...


@cli.command()
@click.option(
    "--template",
    "-t",
    "templates",
    required=False,
    multiple=True,
    type=click.Path(exists=True),
    help="The template file path(s) to apply.",
)
def plan(templates: list[str]):
    asyncio.run(flag_expired_resources(templates))


@cli.command()
@click.option(
    "--config",
    "-c",
    "config_path",
    required=False,
    type=click.Path(exists=True),
    help="The config.yaml file path to apply. Example: ./prod/config.yaml",
)
def detect(config_path: str):
    config = Config.load(config_path)
    config.set_account_defaults()
    asyncio.run(detect_changes(config))


@cli.command()
@click.option(
    "--no-prompt",
    is_flag=True,
    show_default=True,
    help="Apply changes without asking for permission?",
)
@click.option(
    "--config",
    "-c",
    "config_path",
    required=False,
    type=click.Path(exists=True),
    help="The config.yaml file path to apply. Example: ./prod/config.yaml",
)
@click.option(
    "--template",
    "-t",
    "templates",
    required=False,
    multiple=True,
    type=click.Path(exists=True),
    help="The template file path(s) to apply. Example: ./aws/roles/engineering.yaml",
)
@click.option(
    "--template-repo-dir",
    "-d",
    "template_dir",
    required=False,
    type=click.Path(exists=True),
    help="The repo directory containing the templates. Example: ~/noq-templates",
)
def apply(no_prompt: bool, config_path: str, templates: list[str], template_dir: str):
    if not templates:
        templates = asyncio.run(
            gather_templates(template_dir or str(pathlib.Path.cwd()))
        )

    config = Config.load(config_path)
    config.set_account_defaults()
    ctx.eval_only = not no_prompt
    changes_made = asyncio.run(apply_changes(config, templates))
    if ctx.eval_only and changes_made and click.confirm("Proceed?"):
        ctx.eval_only = False
        asyncio.run(apply_changes(config, templates))
    asyncio.run(detect_changes(config))


@cli.command(name="import")
@click.option(
    "--config",
    "-c",
    "config_paths",
    required=False,
    multiple=True,
    type=click.Path(exists=True),
    help="The config.yaml file paths. Example: ./prod/config.yaml",
)
@click.option(
    "--output-dir",
    "-o",
    "output_dir",
    required=False,
    type=click.Path(exists=True),
    help="The output directory to export templates to. Example: ~/noq-templates",
)
def import_(config_paths: list[str], output_dir: str):
    if not output_dir:
        output_dir = str(pathlib.Path.cwd())

    configs = list()
    for config_path in config_paths:
        config = Config.load(config_path)
        config.set_account_defaults()
        configs.append(config)

    asyncio.run(generate_templates(configs, output_dir))


if __name__ == "__main__":
    cli()

iambic/__init__.py METASEP

iambic/core/utils.py METASEP
import asyncio
import glob
import os
import pathlib
import re
from datetime import datetime
from io import StringIO
from typing import Union

import aiofiles
from asgiref.sync import sync_to_async
from ruamel.yaml import YAML

from iambic.core import noq_json as json
from iambic.core.context import ctx
from iambic.core.logger import log

NOQ_TEMPLATE_REGEX = r".*template_type:\n?.*NOQ::"


def camel_to_snake(str_obj: str) -> str:
    return re.sub("([a-z0-9])([A-Z])", r"\1_\2", str_obj).lower()


def camel_to_kebab(str_obj: str) -> str:
    return re.sub("([a-z0-9])([A-Z])", r"\1-\2", str_obj).lower()


def snake_to_camelback(str_obj: str) -> str:
    return re.sub(r"_([a-z])", lambda x: x.group(1).upper(), str_obj)


def snake_to_camelcap(str_obj: str) -> str:
    str_obj = camel_to_snake(
        str_obj
    ).title()  # normalize string and add required case convention
    return str_obj.replace("_", "")  # Remove underscores


async def resource_file_upsert(
    file_path: Union[str | pathlib.Path],
    content_as_dict: dict,
    replace_file: bool = False,
):
    if not replace_file and os.path.exists(file_path):
        async with aiofiles.open(file_path, mode="r") as f:
            content_dict = json.loads(await f.read())
            content_as_dict = {**content_dict, **content_as_dict}

    async with aiofiles.open(file_path, mode="w") as f:
        await f.write(json.dumps(content_as_dict, indent=2))


def normalize_boto3_resp(obj):
    skip_formatting_for = ["condition"]
    if isinstance(obj, dict):
        new_obj = dict()
        for k, v in obj.items():
            k = camel_to_snake(k)
            if isinstance(v, list):
                new_obj[k] = [normalize_boto3_resp(x) for x in v]
            else:
                new_obj[k] = (
                    normalize_boto3_resp(v) if k not in skip_formatting_for else v
                )
        return new_obj
    elif isinstance(obj, list):
        return [normalize_boto3_resp(x) for x in obj]
    else:
        return obj


def get_closest_value(matching_values: list, account_config):
    if len(matching_values) == 1:
        return matching_values[0]

    account_value_hit = dict(returns=None, specificty=0)

    account_ids = [account_config.account_id]
    if account_name := account_config.account_name:
        account_ids.append(account_name.lower())

    for matching_value in matching_values:
        resource_accounts = sorted(matching_value.included_accounts, key=len)
        for resource_account in resource_accounts:
            for account_id in account_ids:
                if resource_account == "*" and account_value_hit["specificty"] == 0:
                    account_value_hit = {"returns": matching_value, "specificty": 1}
                elif re.match(resource_account.lower(), account_id) and len(
                    resource_account
                ) > account_value_hit.get("specificty", 0):
                    account_value_hit = {
                        "returns": matching_value,
                        "specificty": len(resource_account),
                    }
                    break

    return account_value_hit["returns"]


def evaluate_on_account(resource, account_config) -> bool:
    from iambic.core.models import AccessModel

    if ctx.execute and (
        account_config.read_only or getattr(resource, "read_only", False)
    ):
        return False
    if not issubclass(type(resource), AccessModel):
        return True

    if account_config.org_id:
        if account_config.org_id in resource.excluded_orgs:
            return False
        elif not any(
            re.match(org_id, account_config.org_id) for org_id in resource.included_orgs
        ):
            return False

    account_ids = [account_config.account_id]
    if account_name := account_config.account_name:
        account_ids.append(account_name.lower())

    for account_id in account_ids:
        if any(
            re.match(resource_account.lower(), account_id)
            for resource_account in resource.excluded_accounts
        ):
            return False

    for account_id in account_ids:
        if any(
            resource_account == "*" for resource_account in resource.included_accounts
        ):
            return True
        elif any(
            re.match(resource_account.lower(), account_id)
            for resource_account in resource.included_accounts
        ):
            return True

    return False


def apply_to_account(resource, account_config) -> bool:
    from iambic.core.models import Deleted

    if hasattr(resource, "deleted"):
        deleted_resource_type = isinstance(resource, Deleted)

        if isinstance(resource.deleted, bool):
            if resource.deleted and not deleted_resource_type:
                return False
        else:
            deleted_obj = resource.get_attribute_val_for_account(
                account_config, "deleted"
            )
            deleted_obj = get_closest_value(deleted_obj, account_config)
            if deleted_obj and deleted_obj.deleted and not deleted_resource_type:
                return False

    return evaluate_on_account(resource, account_config)


async def remove_expired_resources(
    resource, template_resource_type: str, template_resource_name: str
):
    from iambic.core.models import BaseModel

    if (
        not issubclass(type(resource), BaseModel)
        or not hasattr(resource, "expires_at")
        or getattr(resource, "deleted", None)
    ):
        return resource

    log_params = dict(
        resource_type=resource.resource_type, resource_name=resource.resource_name
    )
    if (
        template_resource_type != resource.resource_type
        or template_resource_name != resource.resource_name
    ):
        log_params["parent_resource_type"] = template_resource_type
        log_params["parent_resource_name"] = template_resource_name

    if hasattr(resource, "expires_at") and resource.expires_at:
        if resource.expires_at < datetime.utcnow():
            resource.deleted = True
            log.info("Expired resource found, marking for deletion", **log_params)
            return resource

    for field_name in resource.__fields__.keys():
        field_val = getattr(resource, field_name)
        if isinstance(field_val, list):
            resource.__fields__[field_name] = await asyncio.gather(
                *[
                    remove_expired_resources(
                        elem, template_resource_type, template_resource_name
                    )
                    for elem in field_val
                ]
            )
        else:
            resource.__fields__[field_name] = await remove_expired_resources(
                field_val, template_resource_type, template_resource_name
            )

    return resource


def get_account_config_map(configs: list) -> dict:
    """Returns a map containing all account configs across all provided config instances

    :param configs:
    :return: dict(account_id:str = AccountConfig)
    """
    account_config_map = dict()
    for config in configs:
        config.set_account_defaults()
        for account_config in config.accounts:
            if account_config_map.get(account_config.account_id):
                log.critical(
                    "Account definition found in multiple configs",
                    account_id=account_config.account_id,
                    account_name=account_config.account_name,
                )
                raise ValueError
            account_config_map[account_config.account_id] = account_config

    return account_config_map


async def file_regex_search(file_path: str, re_pattern: str) -> Union[str | None]:
    async with aiofiles.open(file_path, mode="r") as f:
        file_content = await f.read()
        if re.search(re_pattern, file_content):
            return file_path


async def gather_templates(repo_dir: str, template_type: str = None) -> list[str]:
    regex_pattern = (
        rf"{NOQ_TEMPLATE_REGEX}.*{template_type}"
        if template_type
        else NOQ_TEMPLATE_REGEX
    )
    file_paths = glob.glob(f"{repo_dir}/**/*.yaml", recursive=True)
    file_paths += glob.glob(f"{repo_dir}*.yaml", recursive=True)
    file_paths = await asyncio.gather(
        *[file_regex_search(fp, regex_pattern) for fp in file_paths]
    )
    return [fp for fp in file_paths if fp]


async def aio_wrapper(fnc, *args, **kwargs):
    thread_sensitive = kwargs.pop("thread_sensitive", False)
    return await sync_to_async(fnc, thread_sensitive=thread_sensitive)(*args, **kwargs)


class NoqYaml(YAML):
    def dump(self, data, stream=None, **kw):
        inefficient = False
        if stream is None:
            inefficient = True
            stream = StringIO()
        YAML.dump(self, data, stream, **kw)
        if inefficient:
            return stream.getvalue()


class NoqSemaphore:
    def __init__(
        self, callback_function: any, batch_size: int, callback_is_async: bool = True
    ):
        """Makes a reusable semaphore that wraps a provided function.
        Useful for batch processing things that could be rate limited.

        Example prints hello there 3 times in quick succession, waits 3 seconds then processes another 3:
            from datetime import datetime

            async def hello_there():
                print(f"Hello there - {datetime.utcnow()}")
                await asyncio.sleep(3)

            hello_there_semaphore = NoqSemaphore(hello_there, 3)
            asyncio.run(hello_there_semaphore.process([{} for _ in range(10)]))
        """
        self.limit = asyncio.Semaphore(batch_size)
        self.callback_function = callback_function
        self.callback_is_async = callback_is_async

    async def handle_message(self, **kwargs):
        async with self.limit:
            if self.callback_is_async:
                return await self.callback_function(**kwargs)

            return await aio_wrapper(self.callback_function, **kwargs)

    async def process(self, messages: list[dict]):
        return await asyncio.gather(
            *[asyncio.create_task(self.handle_message(**msg)) for msg in messages]
        )


typ = "rt"
yaml = NoqYaml(typ=typ)
yaml.preserve_quotes = True
yaml.indent(mapping=2, sequence=4, offset=2)
yaml.representer.ignore_aliases = lambda *data: True
yaml.width = 4096

iambic/core/template_generation.py METASEP
from collections import defaultdict
from typing import Union

import xxhash

from iambic.config.models import AccountConfig
from iambic.core import noq_json as json


def templatize_resource(account_config: AccountConfig, resource):
    resource_type = type(resource)

    if isinstance(resource, dict) or isinstance(resource, list):
        resource = json.dumps(resource)
    elif resource_type != str:
        resource = str(resource)

    resource = resource.replace(account_config.account_id, "{{account_id}}")
    resource = resource.replace(account_config.account_name, "{{account_name}}")
    for var in account_config.variables:
        resource = resource.replace(var.value, "{{{}}}".format(var.key))

    return (
        json.loads(resource)
        if resource_type == dict or resource_type == list
        else resource_type(resource)
    )


def base_group_int_attribute(account_vals: dict) -> dict[int, list[dict[str, str]]]:
    """Groups an int attribute by a shared name across of accounts

    Just call group_int_or_str_attribute

    :param account_vals: dict(resource_val: int = list[str])
    :return: dict(attribute_val: int = list[dict(account_id: str)])
    """
    response: dict[int, list[dict]] = defaultdict(list)

    for account_id, resource_val in account_vals.items():
        response[resource_val].append({"account_id": account_id})

    return response


async def base_group_str_attribute(
    account_config_map: dict[str, AccountConfig], account_resources: list[dict]
) -> dict[str, list]:
    """Groups a string attribute by a shared name across of accounts

    The ability to pass in and maintain arbitrary keys is necessary for
        parsing resource names related to a boto3 response

    Call group_int_or_str_attribute instead unless you need to transform this response.
    An example would be grouping role names for generating the template where you need to keep the file_path ref.

    :param account_config_map: dict(account_id:str = AccountConfig)
    :param account_resources: list[dict(account_id:str, resources=list[dict(resource_val: str, **)])]
    :return: dict(attribute_val: str = list[dict(resource_val: str, account_id: str, **)])
    """

    """
    Create map with different representations of a resource value for each account

    The purpose is to add the 2 ways look-ups are done and maintain o(1) performance.
    The resource_val to the corresponding list element in account_resources[elem]["resources"]
        Under resource_val_map
    The reverse of resource_val_map which is an int representing the elem with a list of all resource_val reprs
        Under elem_resource_val_map
    """
    for account_resource_elem, account_resource in enumerate(account_resources):
        account_resources[account_resource_elem]["resource_val_map"] = dict()
        account_resources[account_resource_elem]["elem_resource_val_map"] = dict()
        account_config = account_config_map[account_resource["account_id"]]
        for resource_elem, resource in enumerate(account_resource["resources"]):
            account_resource["resources"][resource_elem][
                "account_id"
            ] = account_resource["account_id"]
            resource_val = resource["resource_val"]
            templatized_resource_val = templatize_resource(account_config, resource_val)

            account_resources[account_resource_elem]["resource_val_map"][
                resource_val
            ] = resource_elem
            account_resources[account_resource_elem]["elem_resource_val_map"][
                resource_elem
            ] = [resource_val]
            if templatized_resource_val != resource_val:
                account_resources[account_resource_elem]["resource_val_map"][
                    templatized_resource_val
                ] = resource_elem
                account_resources[account_resource_elem]["elem_resource_val_map"][
                    resource_elem
                ].append(templatized_resource_val)

    grouped_resource_map = defaultdict(
        list
    )  # val:str = list(dict(name: str, path: str, account_id: str))
    # Iterate everything looking for shared names across accounts
    for outer_elem in range(len(account_resources)):
        for resource_val, outer_resource_elem in account_resources[outer_elem][
            "resource_val_map"
        ].items():
            if outer_resource_elem is None:  # It hit on something already
                continue
            for inner_elem in range(outer_elem + 1, len(account_resources)):
                if (
                    inner_resource_elem := account_resources[inner_elem][
                        "resource_val_map"
                    ].get(resource_val)
                ) is not None:
                    if not grouped_resource_map.get(resource_val):
                        # Null out the outer_resource_elem in all the places
                        for rn in account_resources[outer_elem][
                            "elem_resource_val_map"
                        ][outer_resource_elem]:
                            account_resources[outer_elem]["resource_val_map"][rn] = None
                        account_resources[outer_elem]["elem_resource_val_map"][
                            outer_resource_elem
                        ] = []

                        grouped_resource_map[resource_val] = [
                            account_resources[inner_elem]["resources"][
                                inner_resource_elem
                            ],
                            account_resources[outer_elem]["resources"][
                                outer_resource_elem
                            ],
                        ]
                    else:
                        grouped_resource_map[resource_val].append(
                            account_resources[inner_elem]["resources"][
                                inner_resource_elem
                            ]
                        )

                    for rn in account_resources[inner_elem]["elem_resource_val_map"][
                        inner_resource_elem
                    ]:
                        account_resources[inner_elem]["resource_val_map"][rn] = None
                    account_resources[inner_elem]["elem_resource_val_map"][
                        inner_resource_elem
                    ] = []

    # Set the remaining attributes unique attributes
    for account_resource in account_resources:
        for elem, resource_vals in account_resource["elem_resource_val_map"].items():
            if not resource_vals:
                continue
            elif len(resource_vals) == 1:
                resource_val = resource_vals[0]
            else:
                # Take priority over raw output
                resource_val = [rv for rv in resource_vals if "{{" not in rv][0]

            account_resource["resources"][elem]["account_id"] = account_resource[
                "account_id"
            ]
            grouped_resource_map[resource_val] = [account_resource["resources"][elem]]

    return grouped_resource_map


async def base_group_dict_attribute(
    account_config_map: dict[str, AccountConfig], account_resources: list[dict]
) -> list[dict]:
    """Groups an attribute that is a dict or list of dicts with matching accounts

    Call group_dict_attribute instead unless you need to transform this response.
    An example would be tags which also contain role_access.

    :param account_config_map: dict(account_id:str = AccountConfig)
    :param account_resources: list[dict(account_id:str, resources=list[dict])]
    :return: list[dict(included_accounts: str, resource_val=list[dict]|dict)]
    """
    """
    Create map with different representations of a resource value for each account

    Create a resource_hash to the corresponding list element in account_resources[elem]["resources"]
        Under resource_hash_map
    Create a reverse of resource_hash_map which is an int representing the elem with a list of all resource_hash reprs
        Under elem_resource_hash_map
    """
    hash_map = dict()

    for account_resource_elem, account_resource in enumerate(account_resources):
        account_resources[account_resource_elem]["resource_hash_map"] = dict()
        account_resources[account_resource_elem]["elem_resource_hash_map"] = dict()
        account_config = account_config_map[account_resource["account_id"]]
        for resource_elem, resource in enumerate(account_resource["resources"]):
            account_resource["resources"][resource_elem][
                "account_id"
            ] = account_resource["account_id"]
            # Set raw dict
            resource_hash = xxhash.xxh32(
                json.dumps(resource["resource_val"])
            ).hexdigest()
            hash_map[resource_hash] = resource["resource_val"]
            # Set dict with attempted interpolation
            templatized_dict = templatize_resource(
                account_config, resource["resource_val"]
            )
            templatized_resource_hash = xxhash.xxh32(
                json.dumps(templatized_dict)
            ).hexdigest()
            hash_map[templatized_resource_hash] = templatized_dict
            # Define resource hash mappings
            account_resources[account_resource_elem]["resource_hash_map"][
                resource_hash
            ] = resource_elem
            account_resources[account_resource_elem]["elem_resource_hash_map"][
                resource_elem
            ] = [resource_hash]
            if templatized_resource_hash != resource_hash:
                account_resources[account_resource_elem]["resource_hash_map"][
                    templatized_resource_hash
                ] = resource_elem
                account_resources[account_resource_elem]["elem_resource_hash_map"][
                    resource_elem
                ].append(templatized_resource_hash)

    grouped_resource_map = (
        dict()
    )  # val:str = list(dict(name: str, path: str, account_id: str))
    # Iterate everything looking for shared names across accounts
    for outer_elem in range(len(account_resources)):
        for resource_hash, outer_resource_elem in account_resources[outer_elem][
            "resource_hash_map"
        ].items():
            if outer_resource_elem is None:  # It hit on something already
                continue
            for inner_elem in range(outer_elem + 1, len(account_resources)):
                if (
                    inner_resource_elem := account_resources[inner_elem][
                        "resource_hash_map"
                    ].get(resource_hash)
                ) is not None:
                    if not grouped_resource_map.get(resource_hash):
                        grouped_resource_map[resource_hash] = {
                            "resource_val": hash_map[resource_hash],
                            "included_accounts": [
                                account_resources[inner_elem]["account_id"],
                                account_resources[outer_elem]["account_id"],
                            ],
                        }

                        # Null out the outer_resource_elem in all the places
                        for rn in account_resources[outer_elem][
                            "elem_resource_hash_map"
                        ][outer_resource_elem]:
                            account_resources[outer_elem]["resource_hash_map"][
                                rn
                            ] = None
                        account_resources[outer_elem]["elem_resource_hash_map"][
                            outer_resource_elem
                        ] = []
                    else:
                        grouped_resource_map[resource_hash]["included_accounts"].append(
                            account_resources[inner_elem]["account_id"]
                        )

                    # Null out the inner_resource_elem in all the places
                    for rn in account_resources[inner_elem]["elem_resource_hash_map"][
                        inner_resource_elem
                    ]:
                        account_resources[inner_elem]["resource_hash_map"][rn] = None
                    account_resources[inner_elem]["elem_resource_hash_map"][
                        inner_resource_elem
                    ] = []

    # Set the remaining attributes unique attributes
    for account_resource in account_resources:
        for elem, resource_hashes in account_resource["elem_resource_hash_map"].items():
            if not resource_hashes:
                continue
            elif len(resource_hashes) == 1:
                resource_hash = resource_hashes[0]
            else:
                # Take priority over raw output
                resource_hash = [
                    rv for rv in resource_hashes if "{{" not in str(hash_map[rv])
                ][0]

            grouped_resource_map[resource_hash] = {
                "resource_val": hash_map[resource_hash],
                "included_accounts": [account_resource["account_id"]],
            }

    return list(grouped_resource_map.values())


async def set_included_accounts_for_grouped_attribute(
    account_config_map: dict[str, AccountConfig],
    number_of_accounts_resource_on: int,
    grouped_attribute,
) -> Union[list | dict]:
    if isinstance(grouped_attribute, dict):  # via base_group_str_attribute
        for k, resource_vals in grouped_attribute.items():
            if len(resource_vals) == number_of_accounts_resource_on:
                included_accounts = ["*"]
            else:
                included_accounts = [
                    account_config_map[rv["account_id"]].account_name
                    for rv in resource_vals
                ]
            grouped_attribute[k] = included_accounts

        return grouped_attribute

    elif isinstance(grouped_attribute, list):  # Generated via base_group_dict_attribute
        for elem in range(len(grouped_attribute)):
            if (
                len(grouped_attribute[elem]["included_accounts"])
                == number_of_accounts_resource_on
            ):
                grouped_attribute[elem]["included_accounts"] = ["*"]
            else:
                included_accounts = [
                    account_config_map[rv].account_name
                    for rv in grouped_attribute[elem]["included_accounts"]
                ]
                grouped_attribute[elem]["included_accounts"] = included_accounts

        return grouped_attribute


async def group_int_or_str_attribute(
    account_config_map: dict[str, AccountConfig],
    number_of_accounts_resource_on: int,
    account_resources: Union[dict | list[dict]],
    key: Union[int | str],
) -> Union[int | str | list[dict]]:
    """Groups an attribute by accounts, formats the attribute and normalizes the included accounts.

    :param account_config_map:
    :param number_of_accounts_resource_on:
    :param account_resources: dict(account_id: str = int_val) | list[dict(account_id:str, resources=list[dict])]
    :param key: Used to form the list[dict] response when there are multiple values for the attribute.
    :return:
    """
    if isinstance(account_resources, list):
        grouped_attribute = await base_group_str_attribute(
            account_config_map, account_resources
        )
    else:
        grouped_attribute = base_group_int_attribute(account_resources)

    if len(grouped_attribute) == 1:
        return list(grouped_attribute.keys())[0]

    response = []
    grouped_attribute = await set_included_accounts_for_grouped_attribute(
        account_config_map, number_of_accounts_resource_on, grouped_attribute
    )

    for resource_val, included_accounts in grouped_attribute.items():
        if included_accounts[0] == "*":
            response.append({key: resource_val})
        else:
            response.append({key: resource_val, "included_accounts": included_accounts})

    return response


async def group_dict_attribute(
    account_config_map: dict[str, AccountConfig],
    number_of_accounts_resource_on: int,
    account_resources: list[dict],
    is_dict_attr: bool = True,
) -> Union[dict | list[dict]]:
    """Groups an attribute by accounts, formats the attribute and normalizes the included accounts.

    :param account_config_map:
    :param number_of_accounts_resource_on:
    :param account_resources: list[dict(account_id:str, resources=list[dict])]
    :param is_dict_attr: If false and only one hit, still return as a list. Useful for things like inline_policies.
    :return:
    """

    response = []
    grouped_attributes = await set_included_accounts_for_grouped_attribute(
        account_config_map,
        number_of_accounts_resource_on,
        (await base_group_dict_attribute(account_config_map, account_resources)),
    )

    if len(grouped_attributes) == 1 and is_dict_attr:
        attr_val = grouped_attributes[0]["resource_val"]
        included_accounts = grouped_attributes[0]["included_accounts"]
        if included_accounts != ["*"]:
            attr_val["included_accounts"] = included_accounts

        return attr_val

    for grouped_attr in grouped_attributes:
        attr_val = grouped_attr["resource_val"]
        included_accounts = grouped_attr["included_accounts"]
        if included_accounts != ["*"]:
            attr_val["included_accounts"] = included_accounts

        response.append(attr_val)

    return response

iambic/core/parser.py METASEP
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.models import NoqTemplate
from iambic.core.utils import yaml


def load_templates(template_paths: list[str]) -> list[NoqTemplate]:
    templates = []

    for template_path in template_paths:
        template_dict = yaml.load(open(template_path))
        template_cls = TEMPLATE_TYPE_MAP[template_dict["template_type"]]
        templates.append(template_cls(file_path=template_path, **template_dict))

    return templates

iambic/core/noq_json.py METASEP
import json
from datetime import datetime
from decimal import Decimal
from uuid import UUID

import ujson
from deepdiff.model import PrettyOrderedSet


class SetEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (frozenset, set, PrettyOrderedSet)):
            return list(obj)
        if isinstance(obj, Decimal):
            return float(obj)
        if isinstance(obj, datetime):
            return obj.timestamp()
        if isinstance(obj, Exception):
            return str(obj)
        if isinstance(obj, UUID):
            return str(obj)
        return json.JSONEncoder.default(self, obj)


def dumps(
    obj: any,
    ensure_ascii: bool = True,
    double_precision: int = 9,
    encode_html_chars: bool = False,
    escape_forward_slashes: bool = False,
    sort_keys: bool = False,
    indent: int = 0,
    **kwargs
) -> str:
    # Try fast "ujson is 3x faster than the standard json library" first
    try:
        result = ujson.dumps(
            obj,
            ensure_ascii,
            double_precision,
            encode_html_chars,
            escape_forward_slashes,
            sort_keys,
            indent,
            **kwargs
        )
    except TypeError:
        result = json.dumps(
            obj,
            cls=SetEncoder,
            ensure_ascii=ensure_ascii,
            sort_keys=sort_keys,
            indent=indent,
            **kwargs
        )
    return result


def loads(s: str, **kwargs) -> any:
    try:
        result = ujson.loads(s, **kwargs)
    except ValueError:
        result = json.loads(s, **kwargs)
    return result

iambic/core/models.py METASEP
import asyncio
import json
from datetime import datetime
from typing import List, Optional, Union

from jinja2 import BaseLoader, Environment
from pydantic import BaseModel as PydanticBaseModel
from pydantic import Field

from iambic.config.models import AccountConfig, Config
from iambic.core.context import ctx
from iambic.core.logger import log
from iambic.core.utils import (
    apply_to_account,
    evaluate_on_account,
    snake_to_camelcap,
    yaml,
)


class BaseModel(PydanticBaseModel):
    def get_attribute_val_for_account(
        self, account_config: AccountConfig, attr: str, as_boto_dict: bool = True
    ):
        attr_val = getattr(self, attr)

        if as_boto_dict and hasattr(attr_val, "_apply_resource_dict"):
            return attr_val._apply_resource_dict(account_config)
        elif not isinstance(attr_val, list):
            return attr_val

        matching_definitions = [
            val for val in attr_val if apply_to_account(val, account_config)
        ]
        if len(matching_definitions) == 0:
            # Fallback to the default definition
            return self.__fields__[attr].default
        elif as_boto_dict:
            return [
                match._apply_resource_dict(account_config)
                if hasattr(match, "_apply_resource_dict")
                else match
                for match in matching_definitions
            ]
        else:
            return matching_definitions

    def _apply_resource_dict(self, account_config: AccountConfig = None) -> dict:
        exclude_keys = {
            "deleted",
            "expires_at",
            "included_accounts",
            "excluded_accounts",
            "included_orgs",
            "excluded_orgs",
            "owner",
            "template_type",
            "file_path",
        }
        exclude_keys.update(self.exclude_keys)

        if account_config:
            resource_dict = {
                k: self.get_attribute_val_for_account(account_config, k)
                for k in self.__dict__.keys()
                if k not in exclude_keys
            }
            resource_dict = {k: v for k, v in resource_dict.items() if bool(v)}
        else:
            resource_dict = self.dict(
                exclude=exclude_keys, exclude_none=True, exclude_unset=False
            )

        return {self.case_convention(k): v for k, v in resource_dict.items()}

    def apply_resource_dict(self, account_config: AccountConfig) -> dict:
        response = self._apply_resource_dict(account_config)
        variables = {var.key: var.value for var in account_config.variables}
        variables["account_id"] = account_config.account_id
        variables["account_name"] = account_config.account_name
        if owner := getattr(self, "owner"):
            variables["owner"] = owner

        rtemplate = Environment(loader=BaseLoader()).from_string(json.dumps(response))
        data = rtemplate.render(**variables)
        return json.loads(data)

    @property
    def exclude_keys(self):
        return set()

    @property
    def case_convention(self):
        return snake_to_camelcap


class AccessModel(BaseModel):
    included_accounts: List = Field(
        ["*"],
        description="A list of account ids and/or account names this statement applies to. "
        "Account ids/names can be represented as a regex and string",
    )
    excluded_accounts: Optional[List] = Field(
        [],
        description="A list of account ids and/or account names this statement explicitly does not apply to. "
        "Account ids/names can be represented as a regex and string",
    )
    included_orgs: List = Field(
        ["*"],
        description="A list of AWS organization ids this statement applies to. "
        "Org ids can be represented as a regex and string",
    )
    excluded_orgs: Optional[List] = Field(
        [],
        description="A list of AWS organization ids this statement explicitly does not apply to. "
        "Org ids can be represented as a regex and string",
    )


class Deleted(AccessModel):
    deleted: bool = Field(
        description="Denotes whether the resource has been removed from AWS."
        "Upon being set to true, the resource will be deleted the next time iambic is ran.",
    )


class ExpiryModel(BaseModel):
    expires_at: Optional[datetime] = Field(
        None, description="The date and time the resource will be/was set to deleted."
    )
    deleted: Optional[Union[bool | List[Deleted]]] = Field(
        False,
        description="Denotes whether the resource has been removed from AWS."
        "Upon being set to true, the resource will be deleted the next time iambic is ran.",
    )

    @property
    def resource_type(self):
        raise NotImplementedError

    @property
    def resource_name(self):
        raise NotImplementedError


class Tag(ExpiryModel, AccessModel):
    key: str
    value: str

    @property
    def resource_type(self):
        return "Tag"

    @property
    def resource_name(self):
        return self.key


class NoqTemplate(ExpiryModel):
    template_type: str
    file_path: str
    read_only: Optional[bool] = Field(
        False,
        description="If set to True, iambic will only log drift instead of apply changes when drift is detected.",
    )

    def dict(
        self,
        *,
        include: Optional[
            Union["AbstractSetIntStr", "MappingIntStrAny"]  # noqa
        ] = None,
        exclude: Optional[
            Union["AbstractSetIntStr", "MappingIntStrAny"]  # noqa
        ] = None,
        by_alias: bool = False,
        skip_defaults: Optional[bool] = None,
        exclude_unset: bool = True,
        exclude_defaults: bool = False,
        exclude_none: bool = True,
    ) -> "DictStrAny":  # noqa
        if exclude:
            exclude.add("file_path")
        else:
            exclude = {"file_path"}

        template_dict = self.json(
            include=include,
            exclude=exclude,
            by_alias=by_alias,
            skip_defaults=skip_defaults,
            exclude_unset=exclude_unset,
            exclude_defaults=exclude_defaults,
            exclude_none=exclude_none,
        )
        template_dict = json.loads(template_dict)
        template_dict["template_type"] = self.template_type
        return template_dict

    def write(self):
        with open(self.file_path, "w") as f:
            f.write(yaml.dump(self.dict()))

    async def _apply_to_account(self, account_config: AccountConfig) -> bool:
        # The bool represents whether the resource was altered in any way in the cloud
        raise NotImplementedError

    async def apply_all(self, config: Config) -> bool:
        tasks = []
        log_params = dict(
            resource_type=self.resource_type, resource_name=self.resource_name
        )
        for account in config.accounts:
            if evaluate_on_account(self, account):
                if ctx.execute:
                    log_str = "Applying changes to resource."
                else:
                    log_str = "Detecting changes for resource."
                log.info(log_str, account=str(account), **log_params)
                tasks.append(self._apply_to_account(account))

        changes_made = await asyncio.gather(*tasks)
        changes_made = bool(any(changes_made))
        if changes_made and ctx.execute:
            log.info(
                "Successfully applied resource changes to all accounts.", **log_params
            )
        elif changes_made and not ctx.execute:
            log.info(
                "Successfully detected required resource changes on all accounts.",
                **log_params,
            )
        else:
            log.debug("No changes detected for resource on any account.", **log_params)

        return changes_made

    @classmethod
    def load(cls, file_path: str):
        return cls(file_path=file_path, **yaml.load(open(file_path)))

iambic/core/logger.py METASEP
import logging
import os

import boto3
import structlog

LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
logging.basicConfig(level=LOG_LEVEL)
structlog.configure(
    processors=[
        structlog.processors.add_log_level,
        structlog.processors.StackInfoRenderer(),
        structlog.dev.set_exc_info,
        structlog.processors.TimeStamper("%Y/%m/%d %H:%M:%S", utc=False),
        structlog.dev.ConsoleRenderer(),
    ],
    wrapper_class=structlog.make_filtering_bound_logger(
        logging.getLevelName(LOG_LEVEL)
    ),
    context_class=dict,
    logger_factory=structlog.PrintLoggerFactory(),
    cache_logger_on_first_use=False,
)
log = structlog.get_logger("NoqForm")

if LOG_LEVEL == "DEBUG":
    boto3.set_stream_logger("", "DEBUG")
else:
    default_logging_levels = {
        "boto3": "CRITICAL",
        "boto": "CRITICAL",
        "botocore": "CRITICAL",
    }
    for logger, level in default_logging_levels.items():
        logging.getLogger(logger).setLevel(level)

iambic/core/context.py METASEP
class ExecutionContext:
    eval_only: bool = False

    @property
    def execute(self):
        return not self.eval_only


ctx = ExecutionContext()

iambic/core/__init__.py METASEP

iambic/core/git.py METASEP

import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
infile
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
infile
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                is_deleted=True,
            )
            if re.search(NOQ_TEMPLATE_REGEX, file.content):
                files["deleted_files"].append(file)

    # Collect all modified files
    for file_obj in diff_index.iter_change_type("M"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
        ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):
            if (main_path := file_obj.a_path) != path:  # File was renamed
                deleted_file = GitDiff(
                    path=str(os.path.join(repo_dir, main_path)),
                    content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                    is_deleted=True,
                )

                if re.search(NOQ_TEMPLATE_REGEX, deleted_file.content):
                    template_dict = yaml.load(open(path))
                    main_template_dict = yaml.load(StringIO(deleted_file.content))
                    if not DeepDiff(
                        template_dict, main_template_dict, ignore_order=True
                    ):
                        continue  # Just renamed but no file changes

                    template_cls = TEMPLATE_TYPE_MAP[
                        main_template_dict["template_type"]
                    ]
                    main_template = template_cls(
                        file_path=deleted_file.path, **main_template_dict
                    )
                    template = template_cls(file_path=path, **template_dict)
                    if main_template.resource_name != template.resource_name:
                        files["deleted_files"].append(deleted_file)
infile
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                is_deleted=True,
            )
            if re.search(NOQ_TEMPLATE_REGEX, file.content):
                files["deleted_files"].append(file)

    # Collect all modified files
    for file_obj in diff_index.iter_change_type("M"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
        ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):
            if (main_path := file_obj.a_path) != path:  # File was renamed
infile
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                is_deleted=True,
            )
            if re.search(NOQ_TEMPLATE_REGEX, file.content):
                files["deleted_files"].append(file)

    # Collect all modified files
    for file_obj in diff_index.iter_change_type("M"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
        ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):
            if (main_path := file_obj.a_path) != path:  # File was renamed
                deleted_file = GitDiff(
                    path=str(os.path.join(repo_dir, main_path)),
                    content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                    is_deleted=True,
                )

                if re.search(NOQ_TEMPLATE_REGEX, deleted_file.content):
                    template_dict = yaml.load(open(path))
                    main_template_dict = yaml.load(StringIO(deleted_file.content))
                    if not DeepDiff(
                        template_dict, main_template_dict, ignore_order=True
                    ):
                        continue  # Just renamed but no file changes

                    template_cls = TEMPLATE_TYPE_MAP[
                        main_template_dict["template_type"]
                    ]
                    main_template = template_cls(
                        file_path=deleted_file.path, **main_template_dict
                    )
                    template = template_cls(file_path=path, **template_dict)
                    if main_template.resource_name != template.resource_name:
                        files["deleted_files"].append(deleted_file)
                        files["new_files"].append(GitDiff(path=path))
                        continue

            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
            )
            files["modified_files"].append(file)

    return files


def create_templates_for_deleted_files(deleted_files: list[GitDiff]) -> list:
    """
    Create a class instance of the deleted file content with its template type
    If it wasn't deleted, set it to deleted
    Add that instance to templates
    """
    templates = []
    for git_diff in deleted_files:
        template_dict = yaml.load(StringIO(git_diff.content))
        template_cls = TEMPLATE_TYPE_MAP[template_dict["template_type"]]
        template = template_cls(file_path=git_diff.path, **template_dict)
        if template.deleted is True:
            continue
        template.deleted = True
        log.info("Template marked as deleted", file_path=git_diff.path)
        templates.append(template)

    return templates


def create_templates_for_modified_files(
    config: Config, modified_files: list[GitDiff]
) -> list:

    """
    Create a class instance of the original file content and the new file content with its template type
    Check for accounts that were removed from included_accounts or added to excluded_accounts
    Update the template to be applied to delete the role from the accounts that hit on the above statement
    """
    templates = []
    for git_diff in modified_files:
        main_template_dict = yaml.load(StringIO(git_diff.content))
        template_cls = TEMPLATE_TYPE_MAP[main_template_dict["template_type"]]

        main_template = template_cls(file_path=git_diff.path, **main_template_dict)

        template_dict = yaml.load(open(git_diff.path))
        template = template_cls(file_path=git_diff.path, **template_dict)
        deleted_included_accounts = []
        # deleted_exclude_accounts are accounts that are included in the current commit so can't be deleted
        deleted_exclude_accounts = [*template.included_accounts]
        deleted_exclude_accounts_str = "\n".join(deleted_exclude_accounts)

        # Catch accounts that were in included accounts but have been removed
        if "*" not in deleted_exclude_accounts:
            if "*" in main_template.included_accounts:
                """
                Catch accounts that were implicitly removed from included_accounts.
                Example:
                    main branch included_accounts:
                        - *
                    current commit included_accounts:
                        - staging
                        - dev

                If config.accounts included prod, staging, and dev this will catch that prod is no longer included.
                    This means marking prod for deletion as it has been implicitly deleted.
                """
                for account_config in config.accounts:
                    account_regex = (
                        rf"({account_config.account_id}|{account_config.account_name})"
                    )
                    if re.search(account_regex, deleted_exclude_accounts_str):
                        log.debug(
                            "Resource on account not marked deletion.",
                            account=account_regex,
                            template=git_diff.path,
                        )
                        continue

                    log.info(
                        "Marking resource for deletion on account.",
                        reason="Implicitly removed from included_accounts",
                        account=account_regex,
                        template=git_diff.path,
                    )
                    deleted_included_accounts.append(account_regex)
                    template.included_accounts.append(account_regex)
            else:
                """
                Catch accounts that were explicitly removed from included_accounts.
                Example:
                    main branch included_accounts:
                        - prod
                        - staging
                        - dev
                    current commit included_accounts:
                        - staging
                        - dev

                This means marking prod for deletion as it has been implicitly deleted.
                """
                for account in main_template.included_accounts:
                    if re.search(account, deleted_exclude_accounts_str):
                        log.debug(
                            "Resource on account not marked deletion.",
                            account=account,
                            template=git_diff.path,
                        )
                        continue

                    log.info(
                        "Marking resource for deletion on account.",
                        reason="Explicitly removed from included_accounts.",
                        account=account,
                        template=git_diff.path,
                    )
                    deleted_included_accounts.append(account)
                    template.included_accounts.append(account)

        main_template_included_accounts_str = "\n".join(main_template.included_accounts)
        main_template_excluded_accounts_str = (
            "\n".join(main_template.excluded_accounts)
            if main_template.excluded_accounts
            else None
        )
        template_excluded_accounts = []
        """
        Catch accounts that have been implicitly excluded.
        Example:
            included_accounts:
                - *
            main branch excluded_accounts: []
            current commit excluded_accounts:
                - prod

        If config.accounts included prod, staging, and dev this will catch that prod is no longer included.
            This means marking prod for deletion as it has been implicitly deleted.
        """
        for account in template.excluded_accounts:
            if main_template_excluded_accounts_str and re.search(
                account, main_template_excluded_accounts_str
            ):
                # The account was already excluded so add it to the template_excluded_accounts
                log.debug(
                    "Resource already excluded on account.",
                    account=account,
                    template=git_diff.path,
                )
                template_excluded_accounts.append(account)
            elif (
                re.search(account, main_template_included_accounts_str)
                or "*" in main_template.included_accounts
            ):
                # The account was previously included so mark it for deletion
                log.info(
                    "Marking resource for deletion on account.",
                    reason="Account added to excluded_accounts for resource.",
                    account=account,
                    template=git_diff.path,
                )
                deleted_included_accounts.append(account)
                template.included_accounts.append(account)
            else:
                # The account wasn't included or excluded before this so add it back to template_excluded_accounts
                log.debug(
                    "Newly excluded account.", account=account, template=git_diff.path
                )
                template_excluded_accounts.append(account)

        template.excluded_accounts = template_excluded_accounts

        if deleted_included_accounts and template.deleted is not True:
inproject
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
inproject
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                is_deleted=True,
            )
            if re.search(NOQ_TEMPLATE_REGEX, file.content):
                files["deleted_files"].append(file)

    # Collect all modified files
    for file_obj in diff_index.iter_change_type("M"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
        ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):
            if (main_path := file_obj.a_path) != path:  # File was renamed
                deleted_file = GitDiff(
                    path=str(os.path.join(repo_dir, main_path)),
                    content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                    is_deleted=True,
                )

                if re.search(NOQ_TEMPLATE_REGEX, deleted_file.content):
                    template_dict = yaml.load(open(path))
                    main_template_dict = yaml.load(StringIO(deleted_file.content))
                    if not DeepDiff(
                        template_dict, main_template_dict, ignore_order=True
                    ):
                        continue  # Just renamed but no file changes

                    template_cls = TEMPLATE_TYPE_MAP[
                        main_template_dict["template_type"]
                    ]
                    main_template = template_cls(
                        file_path=deleted_file.path, **main_template_dict
                    )
                    template = template_cls(file_path=path, **template_dict)
inproject
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                is_deleted=True,
            )
            if re.search(NOQ_TEMPLATE_REGEX, file.content):
                files["deleted_files"].append(file)

    # Collect all modified files
    for file_obj in diff_index.iter_change_type("M"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
        ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):
            if (main_path := file_obj.a_path) != path:  # File was renamed
                deleted_file = GitDiff(
                    path=str(os.path.join(repo_dir, main_path)),
                    content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                    is_deleted=True,
                )

                if re.search(NOQ_TEMPLATE_REGEX, deleted_file.content):
                    template_dict = yaml.load(open(path))
                    main_template_dict = yaml.load(StringIO(deleted_file.content))
                    if not DeepDiff(
                        template_dict, main_template_dict, ignore_order=True
                    ):
                        continue  # Just renamed but no file changes

                    template_cls = TEMPLATE_TYPE_MAP[
                        main_template_dict["template_type"]
                    ]
                    main_template = template_cls(
                        file_path=deleted_file.path, **main_template_dict
                    )
                    template = template_cls(file_path=path, **template_dict)
                    if main_template.resource_name != template.resource_name:
                        files["deleted_files"].append(deleted_file)
                        files["new_files"].append(GitDiff(path=path))
                        continue

            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
            )
            files["modified_files"].append(file)

    return files


def create_templates_for_deleted_files(deleted_files: list[GitDiff]) -> list:
    """
    Create a class instance of the deleted file content with its template type
    If it wasn't deleted, set it to deleted
    Add that instance to templates
    """
    templates = []
    for git_diff in deleted_files:
        template_dict = yaml.load(StringIO(git_diff.content))
        template_cls = TEMPLATE_TYPE_MAP[template_dict["template_type"]]
        template = template_cls(file_path=git_diff.path, **template_dict)
        if template.deleted is True:
            continue
        template.deleted = True
        log.info("Template marked as deleted", file_path=git_diff.path)
        templates.append(template)

    return templates


def create_templates_for_modified_files(
inproject
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


inproject
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                is_deleted=True,
            )
            if re.search(NOQ_TEMPLATE_REGEX, file.content):
                files["deleted_files"].append(file)

    # Collect all modified files
    for file_obj in diff_index.iter_change_type("M"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
inproject
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                is_deleted=True,
            )
            if re.search(NOQ_TEMPLATE_REGEX, file.content):
                files["deleted_files"].append(file)

    # Collect all modified files
    for file_obj in diff_index.iter_change_type("M"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
        ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):
            if (main_path := file_obj.a_path) != path:  # File was renamed
                deleted_file = GitDiff(
                    path=str(os.path.join(repo_dir, main_path)),
                    content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                    is_deleted=True,
                )

                if re.search(NOQ_TEMPLATE_REGEX, deleted_file.content):
common
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                is_deleted=True,
            )
            if re.search(NOQ_TEMPLATE_REGEX, file.content):
                files["deleted_files"].append(file)

    # Collect all modified files
    for file_obj in diff_index.iter_change_type("M"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
        ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):
            if (main_path := file_obj.a_path) != path:  # File was renamed
                deleted_file = GitDiff(
                    path=str(os.path.join(repo_dir, main_path)),
                    content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                    is_deleted=True,
                )

                if re.search(NOQ_TEMPLATE_REGEX, deleted_file.content):
                    template_dict = yaml.load(open(path))
common
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                is_deleted=True,
            )
            if re.search(NOQ_TEMPLATE_REGEX, file.content):
                files["deleted_files"].append(file)

    # Collect all modified files
    for file_obj in diff_index.iter_change_type("M"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
        ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):
            if (main_path := file_obj.a_path) != path:  # File was renamed
                deleted_file = GitDiff(
                    path=str(os.path.join(repo_dir, main_path)),
                    content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                    is_deleted=True,
                )

                if re.search(NOQ_TEMPLATE_REGEX, deleted_file.content):
                    template_dict = yaml.load(open(path))
                    main_template_dict = yaml.load(StringIO(deleted_file.content))
                    if not DeepDiff(
                        template_dict, main_template_dict, ignore_order=True
                    ):
                        continue  # Just renamed but no file changes

                    template_cls = TEMPLATE_TYPE_MAP[
                        main_template_dict["template_type"]
                    ]
                    main_template = template_cls(
                        file_path=deleted_file.path, **main_template_dict
                    )
                    template = template_cls(file_path=path, **template_dict)
                    if main_template.resource_name != template.resource_name:
                        files["deleted_files"].append(deleted_file)
                        files["new_files"].append(GitDiff(path=path))
                        continue

            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
            )
            files["modified_files"].append(file)

    return files


def create_templates_for_deleted_files(deleted_files: list[GitDiff]) -> list:
    """
    Create a class instance of the deleted file content with its template type
    If it wasn't deleted, set it to deleted
    Add that instance to templates
    """
    templates = []
    for git_diff in deleted_files:
        template_dict = yaml.load(StringIO(git_diff.content))
        template_cls = TEMPLATE_TYPE_MAP[template_dict["template_type"]]
        template = template_cls(file_path=git_diff.path, **template_dict)
        if template.deleted is True:
            continue
        template.deleted = True
        log.info("Template marked as deleted", file_path=git_diff.path)
        templates.append(template)

    return templates


def create_templates_for_modified_files(
    config: Config, modified_files: list[GitDiff]
) -> list:

    """
    Create a class instance of the original file content and the new file content with its template type
    Check for accounts that were removed from included_accounts or added to excluded_accounts
    Update the template to be applied to delete the role from the accounts that hit on the above statement
    """
    templates = []
    for git_diff in modified_files:
        main_template_dict = yaml.load(StringIO(git_diff.content))
        template_cls = TEMPLATE_TYPE_MAP[main_template_dict["template_type"]]

        main_template = template_cls(file_path=git_diff.path, **main_template_dict)

common
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                is_deleted=True,
            )
            if re.search(NOQ_TEMPLATE_REGEX, file.content):
                files["deleted_files"].append(file)

    # Collect all modified files
    for file_obj in diff_index.iter_change_type("M"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
        ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):
            if (main_path := file_obj.a_path) != path:  # File was renamed
                deleted_file = GitDiff(
                    path=str(os.path.join(repo_dir, main_path)),
                    content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                    is_deleted=True,
                )

                if re.search(NOQ_TEMPLATE_REGEX, deleted_file.content):
                    template_dict = yaml.load(open(path))
                    main_template_dict = yaml.load(StringIO(deleted_file.content))
                    if not DeepDiff(
                        template_dict, main_template_dict, ignore_order=True
                    ):
                        continue  # Just renamed but no file changes

                    template_cls = TEMPLATE_TYPE_MAP[
                        main_template_dict["template_type"]
                    ]
                    main_template = template_cls(
                        file_path=deleted_file.path, **main_template_dict
                    )
                    template = template_cls(file_path=path, **template_dict)
                    if main_template.resource_name != template.resource_name:
                        files["deleted_files"].append(deleted_file)
                        files["new_files"].append(GitDiff(path=path))
                        continue

            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
            )
            files["modified_files"].append(file)

    return files


def create_templates_for_deleted_files(deleted_files: list[GitDiff]) -> list:
    """
    Create a class instance of the deleted file content with its template type
    If it wasn't deleted, set it to deleted
    Add that instance to templates
    """
    templates = []
    for git_diff in deleted_files:
common
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                is_deleted=True,
            )
            if re.search(NOQ_TEMPLATE_REGEX, file.content):
                files["deleted_files"].append(file)

    # Collect all modified files
    for file_obj in diff_index.iter_change_type("M"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
        ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):
            if (main_path := file_obj.a_path) != path:  # File was renamed
                deleted_file = GitDiff(
                    path=str(os.path.join(repo_dir, main_path)),
                    content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                    is_deleted=True,
                )

                if re.search(NOQ_TEMPLATE_REGEX, deleted_file.content):
                    template_dict = yaml.load(open(path))
                    main_template_dict = yaml.load(StringIO(deleted_file.content))
                    if not DeepDiff(
                        template_dict, main_template_dict, ignore_order=True
                    ):
                        continue  # Just renamed but no file changes

                    template_cls = TEMPLATE_TYPE_MAP[
                        main_template_dict["template_type"]
                    ]
                    main_template = template_cls(
                        file_path=deleted_file.path, **main_template_dict
                    )
                    template = template_cls(file_path=path, **template_dict)
                    if main_template.resource_name != template.resource_name:
                        files["deleted_files"].append(deleted_file)
                        files["new_files"].append(GitDiff(path=path))
                        continue

            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
            )
            files["modified_files"].append(file)

    return files


def create_templates_for_deleted_files(deleted_files: list[GitDiff]) -> list:
    """
    Create a class instance of the deleted file content with its template type
    If it wasn't deleted, set it to deleted
    Add that instance to templates
    """
    templates = []
    for git_diff in deleted_files:
        template_dict = yaml.load(StringIO(git_diff.content))
        template_cls = TEMPLATE_TYPE_MAP[template_dict["template_type"]]
        template = template_cls(file_path=git_diff.path, **template_dict)
        if template.deleted is True:
            continue
        template.deleted = True
        log.info("Template marked as deleted", file_path=git_diff.path)
        templates.append(template)

    return templates


def create_templates_for_modified_files(
    config: Config, modified_files: list[GitDiff]
) -> list:

    """
    Create a class instance of the original file content and the new file content with its template type
    Check for accounts that were removed from included_accounts or added to excluded_accounts
    Update the template to be applied to delete the role from the accounts that hit on the above statement
    """
    templates = []
    for git_diff in modified_files:
common
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                is_deleted=True,
            )
            if re.search(NOQ_TEMPLATE_REGEX, file.content):
                files["deleted_files"].append(file)

    # Collect all modified files
    for file_obj in diff_index.iter_change_type("M"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
        ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):
            if (main_path := file_obj.a_path) != path:  # File was renamed
                deleted_file = GitDiff(
                    path=str(os.path.join(repo_dir, main_path)),
                    content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                    is_deleted=True,
                )

                if re.search(NOQ_TEMPLATE_REGEX, deleted_file.content):
                    template_dict = yaml.load(open(path))
                    main_template_dict = yaml.load(StringIO(deleted_file.content))
                    if not DeepDiff(
                        template_dict, main_template_dict, ignore_order=True
                    ):
                        continue  # Just renamed but no file changes

                    template_cls = TEMPLATE_TYPE_MAP[
                        main_template_dict["template_type"]
                    ]
                    main_template = template_cls(
                        file_path=deleted_file.path, **main_template_dict
                    )
                    template = template_cls(file_path=path, **template_dict)
                    if main_template.resource_name != template.resource_name:
                        files["deleted_files"].append(deleted_file)
                        files["new_files"].append(GitDiff(path=path))
                        continue

            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
            )
            files["modified_files"].append(file)

    return files


non_informative
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                is_deleted=True,
            )
            if re.search(NOQ_TEMPLATE_REGEX, file.content):
                files["deleted_files"].append(file)

    # Collect all modified files
    for file_obj in diff_index.iter_change_type("M"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
        ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):
            if (main_path := file_obj.a_path) != path:  # File was renamed
                deleted_file = GitDiff(
                    path=str(os.path.join(repo_dir, main_path)),
                    content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                    is_deleted=True,
                )

                if re.search(NOQ_TEMPLATE_REGEX, deleted_file.content):
                    template_dict = yaml.load(open(path))
                    main_template_dict = yaml.load(StringIO(deleted_file.content))
                    if not DeepDiff(
                        template_dict, main_template_dict, ignore_order=True
                    ):
                        continue  # Just renamed but no file changes

                    template_cls = TEMPLATE_TYPE_MAP[
                        main_template_dict["template_type"]
                    ]
                    main_template = template_cls(
                        file_path=deleted_file.path, **main_template_dict
                    )
                    template = template_cls(file_path=path, **template_dict)
                    if main_template.resource_name != template.resource_name:
                        files["deleted_files"].append(deleted_file)
                        files["new_files"].append(GitDiff(path=path))
                        continue

            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
            )
            files["modified_files"].append(file)

    return files


def create_templates_for_deleted_files(deleted_files: list[GitDiff]) -> list:
    """
    Create a class instance of the deleted file content with its template type
    If it wasn't deleted, set it to deleted
    Add that instance to templates
    """
    templates = []
    for git_diff in deleted_files:
        template_dict = yaml.load(StringIO(git_diff.content))
        template_cls = TEMPLATE_TYPE_MAP[template_dict["template_type"]]
        template = template_cls(file_path=git_diff.path, **template_dict)
        if template.deleted is True:
            continue
        template.deleted = True
        log.info("Template marked as deleted", file_path=git_diff.path)
        templates.append(template)

    return templates


def create_templates_for_modified_files(
    config: Config, modified_files: list[GitDiff]
) -> list:

    """
    Create a class instance of the original file content and the new file content with its template type
    Check for accounts that were removed from included_accounts or added to excluded_accounts
    Update the template to be applied to delete the role from the accounts that hit on the above statement
    """
    templates = []
    for git_diff in modified_files:
        main_template_dict = yaml.load(StringIO(git_diff.content))
        template_cls = TEMPLATE_TYPE_MAP[main_template_dict["template_type"]]

        main_template = template_cls(file_path=git_diff.path, **main_template_dict)

        template_dict = yaml.load(open(git_diff.path))
        template = template_cls(file_path=git_diff.path, **template_dict)
        deleted_included_accounts = []
        # deleted_exclude_accounts are accounts that are included in the current commit so can't be deleted
        deleted_exclude_accounts = [*template.included_accounts]
        deleted_exclude_accounts_str = "\n".join(deleted_exclude_accounts)

        # Catch accounts that were in included accounts but have been removed
        if "*" not in deleted_exclude_accounts:
            if "*" in main_template.included_accounts:
                """
                Catch accounts that were implicitly removed from included_accounts.
                Example:
                    main branch included_accounts:
                        - *
                    current commit included_accounts:
                        - staging
                        - dev

                If config.accounts included prod, staging, and dev this will catch that prod is no longer included.
                    This means marking prod for deletion as it has been implicitly deleted.
                """
                for account_config in config.accounts:
                    account_regex = (
                        rf"({account_config.account_id}|{account_config.account_name})"
                    )
                    if re.search(account_regex, deleted_exclude_accounts_str):
                        log.debug(
                            "Resource on account not marked deletion.",
                            account=account_regex,
                            template=git_diff.path,
                        )
                        continue

                    log.info(
                        "Marking resource for deletion on account.",
                        reason="Implicitly removed from included_accounts",
                        account=account_regex,
                        template=git_diff.path,
                    )
                    deleted_included_accounts.append(account_regex)
                    template.included_accounts.append(account_regex)
            else:
                """
non_informative
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                is_deleted=True,
            )
            if re.search(NOQ_TEMPLATE_REGEX, file.content):
                files["deleted_files"].append(file)

    # Collect all modified files
    for file_obj in diff_index.iter_change_type("M"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
        ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):
            if (main_path := file_obj.a_path) != path:  # File was renamed
                deleted_file = GitDiff(
                    path=str(os.path.join(repo_dir, main_path)),
                    content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                    is_deleted=True,
                )

                if re.search(NOQ_TEMPLATE_REGEX, deleted_file.content):
                    template_dict = yaml.load(open(path))
                    main_template_dict = yaml.load(StringIO(deleted_file.content))
                    if not DeepDiff(
                        template_dict, main_template_dict, ignore_order=True
                    ):
                        continue  # Just renamed but no file changes

                    template_cls = TEMPLATE_TYPE_MAP[
                        main_template_dict["template_type"]
                    ]
                    main_template = template_cls(
                        file_path=deleted_file.path, **main_template_dict
                    )
                    template = template_cls(file_path=path, **template_dict)
                    if main_template.resource_name != template.resource_name:
                        files["deleted_files"].append(deleted_file)
                        files["new_files"].append(GitDiff(path=path))
                        continue

            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
            )
            files["modified_files"].append(file)

    return files


def create_templates_for_deleted_files(deleted_files: list[GitDiff]) -> list:
    """
    Create a class instance of the deleted file content with its template type
    If it wasn't deleted, set it to deleted
    Add that instance to templates
    """
    templates = []
    for git_diff in deleted_files:
        template_dict = yaml.load(StringIO(git_diff.content))
        template_cls = TEMPLATE_TYPE_MAP[template_dict["template_type"]]
        template = template_cls(file_path=git_diff.path, **template_dict)
        if template.deleted is True:
            continue
        template.deleted = True
        log.info("Template marked as deleted", file_path=git_diff.path)
        templates.append(template)

    return templates


def create_templates_for_modified_files(
    config: Config, modified_files: list[GitDiff]
) -> list:

    """
    Create a class instance of the original file content and the new file content with its template type
    Check for accounts that were removed from included_accounts or added to excluded_accounts
    Update the template to be applied to delete the role from the accounts that hit on the above statement
    """
    templates = []
    for git_diff in modified_files:
        main_template_dict = yaml.load(StringIO(git_diff.content))
        template_cls = TEMPLATE_TYPE_MAP[main_template_dict["template_type"]]

        main_template = template_cls(file_path=git_diff.path, **main_template_dict)

        template_dict = yaml.load(open(git_diff.path))
        template = template_cls(file_path=git_diff.path, **template_dict)
        deleted_included_accounts = []
        # deleted_exclude_accounts are accounts that are included in the current commit so can't be deleted
        deleted_exclude_accounts = [*template.included_accounts]
        deleted_exclude_accounts_str = "\n".join(deleted_exclude_accounts)

        # Catch accounts that were in included accounts but have been removed
        if "*" not in deleted_exclude_accounts:
            if "*" in main_template.included_accounts:
                """
                Catch accounts that were implicitly removed from included_accounts.
                Example:
                    main branch included_accounts:
                        - *
                    current commit included_accounts:
                        - staging
                        - dev

                If config.accounts included prod, staging, and dev this will catch that prod is no longer included.
                    This means marking prod for deletion as it has been implicitly deleted.
                """
                for account_config in config.accounts:
                    account_regex = (
                        rf"({account_config.account_id}|{account_config.account_name})"
                    )
                    if re.search(account_regex, deleted_exclude_accounts_str):
                        log.debug(
                            "Resource on account not marked deletion.",
                            account=account_regex,
                            template=git_diff.path,
                        )
                        continue

                    log.info(
                        "Marking resource for deletion on account.",
                        reason="Implicitly removed from included_accounts",
                        account=account_regex,
                        template=git_diff.path,
                    )
                    deleted_included_accounts.append(account_regex)
                    template.included_accounts.append(account_regex)
            else:
                """
                Catch accounts that were explicitly removed from included_accounts.
                Example:
                    main branch included_accounts:
                        - prod
                        - staging
                        - dev
                    current commit included_accounts:
                        - staging
                        - dev

                This means marking prod for deletion as it has been implicitly deleted.
                """
                for account in main_template.included_accounts:
                    if re.search(account, deleted_exclude_accounts_str):
                        log.debug(
                            "Resource on account not marked deletion.",
                            account=account,
                            template=git_diff.path,
                        )
                        continue
non_informative
non_informative
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                is_deleted=True,
            )
            if re.search(NOQ_TEMPLATE_REGEX, file.content):
                files["deleted_files"].append(file)

    # Collect all modified files
    for file_obj in diff_index.iter_change_type("M"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
        ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):
            if (main_path := file_obj.a_path) != path:  # File was renamed
                deleted_file = GitDiff(
                    path=str(os.path.join(repo_dir, main_path)),
                    content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                    is_deleted=True,
                )

                if re.search(NOQ_TEMPLATE_REGEX, deleted_file.content):
                    template_dict = yaml.load(open(path))
                    main_template_dict = yaml.load(StringIO(deleted_file.content))
                    if not DeepDiff(
                        template_dict, main_template_dict, ignore_order=True
                    ):
                        continue  # Just renamed but no file changes

                    template_cls = TEMPLATE_TYPE_MAP[
                        main_template_dict["template_type"]
                    ]
                    main_template = template_cls(
                        file_path=deleted_file.path, **main_template_dict
                    )
                    template = template_cls(file_path=path, **template_dict)
                    if main_template.resource_name != template.resource_name:
                        files["deleted_files"].append(deleted_file)
                        files["new_files"].append(GitDiff(path=path))
                        continue

            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
            )
            files["modified_files"].append(file)

    return files


def create_templates_for_deleted_files(deleted_files: list[GitDiff]) -> list:
    """
    Create a class instance of the deleted file content with its template type
    If it wasn't deleted, set it to deleted
    Add that instance to templates
    """
    templates = []
    for git_diff in deleted_files:
        template_dict = yaml.load(StringIO(git_diff.content))
        template_cls = TEMPLATE_TYPE_MAP[template_dict["template_type"]]
        template = template_cls(file_path=git_diff.path, **template_dict)
        if template.deleted is True:
            continue
        template.deleted = True
        log.info("Template marked as deleted", file_path=git_diff.path)
        templates.append(template)

    return templates


def create_templates_for_modified_files(
    config: Config, modified_files: list[GitDiff]
) -> list:

    """
    Create a class instance of the original file content and the new file content with its template type
    Check for accounts that were removed from included_accounts or added to excluded_accounts
    Update the template to be applied to delete the role from the accounts that hit on the above statement
    """
    templates = []
    for git_diff in modified_files:
        main_template_dict = yaml.load(StringIO(git_diff.content))
random
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                is_deleted=True,
            )
            if re.search(NOQ_TEMPLATE_REGEX, file.content):
                files["deleted_files"].append(file)

    # Collect all modified files
    for file_obj in diff_index.iter_change_type("M"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
        ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):
            if (main_path := file_obj.a_path) != path:  # File was renamed
                deleted_file = GitDiff(
                    path=str(os.path.join(repo_dir, main_path)),
                    content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                    is_deleted=True,
                )

                if re.search(NOQ_TEMPLATE_REGEX, deleted_file.content):
                    template_dict = yaml.load(open(path))
                    main_template_dict = yaml.load(StringIO(deleted_file.content))
                    if not DeepDiff(
                        template_dict, main_template_dict, ignore_order=True
                    ):
                        continue  # Just renamed but no file changes

                    template_cls = TEMPLATE_TYPE_MAP[
                        main_template_dict["template_type"]
                    ]
                    main_template = template_cls(
                        file_path=deleted_file.path, **main_template_dict
                    )
                    template = template_cls(file_path=path, **template_dict)
                    if main_template.resource_name != template.resource_name:
                        files["deleted_files"].append(deleted_file)
                        files["new_files"].append(GitDiff(path=path))
                        continue

            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
            )
            files["modified_files"].append(file)

    return files


def create_templates_for_deleted_files(deleted_files: list[GitDiff]) -> list:
    """
    Create a class instance of the deleted file content with its template type
    If it wasn't deleted, set it to deleted
    Add that instance to templates
    """
    templates = []
    for git_diff in deleted_files:
        template_dict = yaml.load(StringIO(git_diff.content))
        template_cls = TEMPLATE_TYPE_MAP[template_dict["template_type"]]
        template = template_cls(file_path=git_diff.path, **template_dict)
        if template.deleted is True:
            continue
        template.deleted = True
        log.info("Template marked as deleted", file_path=git_diff.path)
        templates.append(template)

    return templates


def create_templates_for_modified_files(
    config: Config, modified_files: list[GitDiff]
) -> list:

    """
    Create a class instance of the original file content and the new file content with its template type
    Check for accounts that were removed from included_accounts or added to excluded_accounts
    Update the template to be applied to delete the role from the accounts that hit on the above statement
    """
    templates = []
    for git_diff in modified_files:
        main_template_dict = yaml.load(StringIO(git_diff.content))
        template_cls = TEMPLATE_TYPE_MAP[main_template_dict["template_type"]]

        main_template = template_cls(file_path=git_diff.path, **main_template_dict)

        template_dict = yaml.load(open(git_diff.path))
random
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                is_deleted=True,
            )
            if re.search(NOQ_TEMPLATE_REGEX, file.content):
                files["deleted_files"].append(file)

    # Collect all modified files
    for file_obj in diff_index.iter_change_type("M"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
        ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):
            if (main_path := file_obj.a_path) != path:  # File was renamed
                deleted_file = GitDiff(
                    path=str(os.path.join(repo_dir, main_path)),
                    content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                    is_deleted=True,
                )

                if re.search(NOQ_TEMPLATE_REGEX, deleted_file.content):
                    template_dict = yaml.load(open(path))
                    main_template_dict = yaml.load(StringIO(deleted_file.content))
                    if not DeepDiff(
                        template_dict, main_template_dict, ignore_order=True
                    ):
                        continue  # Just renamed but no file changes

                    template_cls = TEMPLATE_TYPE_MAP[
                        main_template_dict["template_type"]
                    ]
                    main_template = template_cls(
                        file_path=deleted_file.path, **main_template_dict
                    )
                    template = template_cls(file_path=path, **template_dict)
                    if main_template.resource_name != template.resource_name:
                        files["deleted_files"].append(deleted_file)
                        files["new_files"].append(GitDiff(path=path))
                        continue

            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
            )
            files["modified_files"].append(file)

    return files


def create_templates_for_deleted_files(deleted_files: list[GitDiff]) -> list:
    """
    Create a class instance of the deleted file content with its template type
    If it wasn't deleted, set it to deleted
    Add that instance to templates
    """
    templates = []
    for git_diff in deleted_files:
        template_dict = yaml.load(StringIO(git_diff.content))
        template_cls = TEMPLATE_TYPE_MAP[template_dict["template_type"]]
        template = template_cls(file_path=git_diff.path, **template_dict)
        if template.deleted is True:
            continue
        template.deleted = True
        log.info("Template marked as deleted", file_path=git_diff.path)
        templates.append(template)

    return templates


def create_templates_for_modified_files(
    config: Config, modified_files: list[GitDiff]
) -> list:

    """
    Create a class instance of the original file content and the new file content with its template type
    Check for accounts that were removed from included_accounts or added to excluded_accounts
    Update the template to be applied to delete the role from the accounts that hit on the above statement
    """
    templates = []
    for git_diff in modified_files:
        main_template_dict = yaml.load(StringIO(git_diff.content))
        template_cls = TEMPLATE_TYPE_MAP[main_template_dict["template_type"]]

        main_template = template_cls(file_path=git_diff.path, **main_template_dict)

        template_dict = yaml.load(open(git_diff.path))
        template = template_cls(file_path=git_diff.path, **template_dict)
        deleted_included_accounts = []
        # deleted_exclude_accounts are accounts that are included in the current commit so can't be deleted
        deleted_exclude_accounts = [*template.included_accounts]
        deleted_exclude_accounts_str = "\n".join(deleted_exclude_accounts)

        # Catch accounts that were in included accounts but have been removed
        if "*" not in deleted_exclude_accounts:
            if "*" in main_template.included_accounts:
                """
                Catch accounts that were implicitly removed from included_accounts.
                Example:
                    main branch included_accounts:
                        - *
                    current commit included_accounts:
                        - staging
                        - dev

                If config.accounts included prod, staging, and dev this will catch that prod is no longer included.
                    This means marking prod for deletion as it has been implicitly deleted.
                """
                for account_config in config.accounts:
                    account_regex = (
                        rf"({account_config.account_id}|{account_config.account_name})"
                    )
                    if re.search(account_regex, deleted_exclude_accounts_str):
                        log.debug(
                            "Resource on account not marked deletion.",
                            account=account_regex,
                            template=git_diff.path,
                        )
                        continue

                    log.info(
                        "Marking resource for deletion on account.",
                        reason="Implicitly removed from included_accounts",
                        account=account_regex,
                        template=git_diff.path,
                    )
                    deleted_included_accounts.append(account_regex)
                    template.included_accounts.append(account_regex)
            else:
                """
                Catch accounts that were explicitly removed from included_accounts.
                Example:
                    main branch included_accounts:
                        - prod
                        - staging
                        - dev
                    current commit included_accounts:
                        - staging
                        - dev

                This means marking prod for deletion as it has been implicitly deleted.
                """
                for account in main_template.included_accounts:
                    if re.search(account, deleted_exclude_accounts_str):
                        log.debug(
                            "Resource on account not marked deletion.",
                            account=account,
                            template=git_diff.path,
                        )
                        continue

                    log.info(
                        "Marking resource for deletion on account.",
                        reason="Explicitly removed from included_accounts.",
                        account=account,
                        template=git_diff.path,
                    )
                    deleted_included_accounts.append(account)
                    template.included_accounts.append(account)

        main_template_included_accounts_str = "\n".join(main_template.included_accounts)
        main_template_excluded_accounts_str = (
            "\n".join(main_template.excluded_accounts)
            if main_template.excluded_accounts
            else None
        )
        template_excluded_accounts = []
        """
        Catch accounts that have been implicitly excluded.
        Example:
            included_accounts:
                - *
            main branch excluded_accounts: []
            current commit excluded_accounts:
                - prod

        If config.accounts included prod, staging, and dev this will catch that prod is no longer included.
            This means marking prod for deletion as it has been implicitly deleted.
        """
        for account in template.excluded_accounts:
            if main_template_excluded_accounts_str and re.search(
                account, main_template_excluded_accounts_str
            ):
                # The account was already excluded so add it to the template_excluded_accounts
                log.debug(
random
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
random
import os
import re
from io import StringIO
from typing import Optional

from deepdiff import DeepDiff
from git import Repo
from pydantic import BaseModel as PydanticBaseModel

from iambic.config.models import Config
from iambic.config.templates import TEMPLATE_TYPE_MAP
from iambic.core.logger import log
from iambic.core.models import Deleted
from iambic.core.utils import NOQ_TEMPLATE_REGEX, file_regex_search, yaml


class GitDiff(PydanticBaseModel):
    path: str
    content: Optional[str] = None
    is_deleted: Optional[bool] = False


async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:
    repo = Repo(repo_dir)
    # Last commit of the current branch
    commit_feature = repo.head.commit.tree
    # Comparing against main
    commit_origin_main = repo.commit("origin/main")
    diff_index = commit_origin_main.diff(commit_feature)
    files = {
        "new_files": [],
        "deleted_files": [],
        "modified_files": [],
    }

    # Collect all new files
    for file_obj in diff_index.iter_change_type("A"):
        if (path := file_obj.b_path).endswith(".yaml") and (
            await file_regex_search(path, NOQ_TEMPLATE_REGEX)
        ):
            file = GitDiff(path=str(os.path.join(repo_dir, path)))
            files["new_files"].append(file)

    # Collect all deleted files
    for file_obj in diff_index.iter_change_type("D"):
        if (path := file_obj.b_path).endswith(".yaml"):
            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                is_deleted=True,
            )
            if re.search(NOQ_TEMPLATE_REGEX, file.content):
                files["deleted_files"].append(file)

    # Collect all modified files
    for file_obj in diff_index.iter_change_type("M"):
        if (path := str(os.path.join(repo_dir, file_obj.b_path))).endswith(
            ".yaml"
        ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):
            if (main_path := file_obj.a_path) != path:  # File was renamed
                deleted_file = GitDiff(
                    path=str(os.path.join(repo_dir, main_path)),
                    content=file_obj.a_blob.data_stream.read().decode("utf-8"),
                    is_deleted=True,
                )

                if re.search(NOQ_TEMPLATE_REGEX, deleted_file.content):
                    template_dict = yaml.load(open(path))
                    main_template_dict = yaml.load(StringIO(deleted_file.content))
                    if not DeepDiff(
                        template_dict, main_template_dict, ignore_order=True
                    ):
                        continue  # Just renamed but no file changes

                    template_cls = TEMPLATE_TYPE_MAP[
                        main_template_dict["template_type"]
                    ]
                    main_template = template_cls(
                        file_path=deleted_file.path, **main_template_dict
                    )
                    template = template_cls(file_path=path, **template_dict)
                    if main_template.resource_name != template.resource_name:
                        files["deleted_files"].append(deleted_file)
                        files["new_files"].append(GitDiff(path=path))
                        continue

            file = GitDiff(
                path=str(os.path.join(repo_dir, path)),
                content=file_obj.a_blob.data_stream.read().decode("utf-8"),
            )
            files["modified_files"].append(file)

    return files


def create_templates_for_deleted_files(deleted_files: list[GitDiff]) -> list:
    """
    Create a class instance of the deleted file content with its template type
    If it wasn't deleted, set it to deleted
    Add that instance to templates
    """
    templates = []
    for git_diff in deleted_files:
        template_dict = yaml.load(StringIO(git_diff.content))
        template_cls = TEMPLATE_TYPE_MAP[template_dict["template_type"]]
        template = template_cls(file_path=git_diff.path, **template_dict)
        if template.deleted is True:
            continue
        template.deleted = True
        log.info("Template marked as deleted", file_path=git_diff.path)
        templates.append(template)

    return templates


def create_templates_for_modified_files(
    config: Config, modified_files: list[GitDiff]
) -> list:

    """
    Create a class instance of the original file content and the new file content with its template type
    Check for accounts that were removed from included_accounts or added to excluded_accounts
    Update the template to be applied to delete the role from the accounts that hit on the above statement
    """
    templates = []
    for git_diff in modified_files:
        main_template_dict = yaml.load(StringIO(git_diff.content))
        template_cls = TEMPLATE_TYPE_MAP[main_template_dict["template_type"]]

        main_template = template_cls(file_path=git_diff.path, **main_template_dict)

        template_dict = yaml.load(open(git_diff.path))
        template = template_cls(file_path=git_diff.path, **template_dict)
        deleted_included_accounts = []
        # deleted_exclude_accounts are accounts that are included in the current commit so can't be deleted
        deleted_exclude_accounts = [*template.included_accounts]
        deleted_exclude_accounts_str = "\n".join(deleted_exclude_accounts)

        # Catch accounts that were in included accounts but have been removed
        if "*" not in deleted_exclude_accounts:
            if "*" in main_template.included_accounts:
                """
                Catch accounts that were implicitly removed from included_accounts.
                Example:
                    main branch included_accounts:
                        - *
                    current commit included_accounts:
                        - staging
                        - dev

                If config.accounts included prod, staging, and dev this will catch that prod is no longer included.
                    This means marking prod for deletion as it has been implicitly deleted.
                """
                for account_config in config.accounts:
                    account_regex = (
                        rf"({account_config.account_id}|{account_config.account_name})"
                    )
                    if re.search(account_regex, deleted_exclude_accounts_str):
                        log.debug(
                            "Resource on account not marked deletion.",
                            account=account_regex,
                            template=git_diff.path,
                        )
                        continue

                    log.info(
                        "Marking resource for deletion on account.",
                        reason="Implicitly removed from included_accounts",
                        account=account_regex,
                        template=git_diff.path,
                    )
                    deleted_included_accounts.append(account_regex)
                    template.included_accounts.append(account_regex)
            else:
                """
                Catch accounts that were explicitly removed from included_accounts.
                Example:
                    main branch included_accounts:
                        - prod
                        - staging
                        - dev
                    current commit included_accounts:
                        - staging
                        - dev

                This means marking prod for deletion as it has been implicitly deleted.
                """
                for account in main_template.included_accounts:
                    if re.search(account, deleted_exclude_accounts_str):
random
            file = GitDiff(path=str(os.path.join(repo_dir, path)))             file = GitDiff(                         files["new_files"].append(GitDiff(path=path))                 deleted_file = GitDiff(             deleted_obj = Deleted(             await file_regex_search(path, NOQ_TEMPLATE_REGEX)                     if main_template.resource_name != template.resource_name:     config: Config, modified_files: list[GitDiff] async def retrieve_git_changes(repo_dir: str) -> dict[str, list[GitDiff]]:         ) and (await file_regex_search(path, NOQ_TEMPLATE_REGEX)):                     template_dict = yaml.load(open(path))                     main_template_dict = yaml.load(StringIO(deleted_file.content))         template_dict = yaml.load(open(git_diff.path))         template_dict = yaml.load(StringIO(git_diff.content))         main_template_dict = yaml.load(StringIO(git_diff.content)) def create_templates_for_deleted_files(deleted_files: list[GitDiff]) -> list:                 Catch accounts that were explicitly removed from included_accounts. import os         template_cls = TEMPLATE_TYPE_MAP[main_template_dict["template_type"]]         template = template_cls(file_path=git_diff.path, **template_dict)                     "Resource already excluded on account.",     content: Optional[str] = None                         log.debug(