Skip to content
This repository was archived by the owner on Nov 24, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ message: If you use this software, please cite it using these metadata.
title: Get AWS Regions Package
abstract: A simple package for retrieving a list of AWS regions.
type: software
version: 0.1.0
version: 0.1.1
date-released: 2024-06-27
repository-code: https://github.com/AWSToolbox/get-aws-regions-package
keywords:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ for all of the tools in our [AWS Toolbox](https://github.com/AWSToolbox).
- `exclude_list`: Optional list of regions to exclude.
- `all_regions`: Boolean flag to include all regions (default: True).
- `details`: Boolean flag to return detailed information about each region (default: False).
- `profile_name`: String to specify the name of the profile to use.
- Returns:
- If `details=True`: Sorted list of dictionaries containing detailed region information.
- If `details=False`: Sorted list of region names as strings.
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

setup(
name='wolfsoftware.get-aws-regions',
version='0.1.0',
version='0.1.1',
author='Wolf Software',
author_email='pypi@wolfsoftware.com',
description='A simple package for retrieving a list of AWS regions.',
Expand Down
43 changes: 21 additions & 22 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@
Configuration for pytest tests, including fixtures and mock data for testing the wolfsoftware.get-aws-regions package.

Fixtures:
- boto3_client_mock: Mocks the boto3 client for AWS interactions.
- boto3_session_mock: Mocks the boto3 session for AWS interactions.
- boto3_session_mock_with_exception: Mocks the boto3 session raising an exception during describe_regions.

Mock Data:
- mock_regions: A list of dictionaries representing mock AWS regions.
- mock_description: A dictionary representing the mock description of a specific AWS region.
"""
from typing import Any, Dict, Generator, List, Union

from unittest.mock import AsyncMock, MagicMock, patch

from typing import Any, Dict, Generator, List
from unittest.mock import MagicMock, patch
import pytest


# Mock data
mock_regions: List[Dict[str, str]] = [
{"RegionName": "us-east-1", "OptInStatus": "opt-in-not-required"},
Expand All @@ -30,14 +29,16 @@


@pytest.fixture
def boto3_client_mock(mock_exception: Union[Exception, None] = None) -> Generator[Union[MagicMock, AsyncMock], Any, None]:
def boto3_session_mock() -> Generator[MagicMock, None, None]:
"""
Fixture to mock the boto3 client.
Fixture to mock the boto3 session.

Yields:
MagicMock: A mock of the boto3 client.
MagicMock: A mock of the boto3 session.
"""
with patch("boto3.client") as mock_client:
with patch("boto3.Session") as mock_session:
mock_session_instance: Any = mock_session.return_value

regions_mock = MagicMock()
ssm_mock = MagicMock()

Expand All @@ -47,27 +48,25 @@ def mock_get_parameter(Name) -> Dict[str, Dict[str, str]]:
raise ValueError(f"Unknown parameter name: {Name}")

ssm_mock.get_parameter.side_effect = mock_get_parameter
mock_client.side_effect = lambda service_name, *args, **kwargs: ssm_mock if service_name == "ssm" else regions_mock

# Conditionally set side effect for describe_regions based on mock_exception
if mock_exception:
print("HERE")
regions_mock.describe_regions.side_effect = mock_exception
regions_mock.describe_regions.side_effect = mock_exception
mock_session_instance.client.side_effect = lambda service_name, *args, **kwargs: ssm_mock if service_name == "ssm" else regions_mock

regions_mock.describe_regions.return_value = {"Regions": mock_regions}

yield mock_client
yield mock_session


@pytest.fixture
def boto3_client_mock_with_exception() -> Generator[MagicMock, None, None]:
def boto3_session_mock_with_exception() -> Generator[MagicMock, None, None]:
"""
Fixture to mock the boto3 client raising an exception during describe_regions.
Fixture to mock the boto3 session raising an exception during describe_regions.

Yields:
MagicMock: A mock of the boto3 client.
MagicMock: A mock of the boto3 session.
"""
with patch("boto3.client") as mock_client:
with patch("boto3.Session") as mock_session:
mock_session_instance: Any = mock_session.return_value

regions_mock = MagicMock()
ssm_mock = MagicMock()

Expand All @@ -80,6 +79,6 @@ def mock_get_parameter(Name) -> Dict[str, Dict[str, str]]:

regions_mock.describe_regions.side_effect = Exception("Test Exception")

mock_client.side_effect = lambda service_name, *args, **kwargs: ssm_mock if service_name == "ssm" else regions_mock
mock_session_instance.client.side_effect = lambda service_name, *args, **kwargs: ssm_mock if service_name == "ssm" else regions_mock

yield mock_client
yield mock_session
30 changes: 14 additions & 16 deletions tests/test_get_aws_regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,39 +37,37 @@ def test_version() -> None:
assert version != 'unknown', f"Expected version, but got {version}" # nosec: B101


def test_get_region_list_all_regions(boto3_client_mock) -> None:
def test_get_region_list_all_regions(boto3_session_mock) -> None:
"""
Test fetching all regions with detailed information.

Arguments:
boto3_client_mock (fixture): The mocked boto3 client.
boto3_session_mock (fixture): The mocked boto3 session.
"""
regions_mock: Any = boto3_client_mock.return_value
regions_mock: Any = boto3_session_mock.return_value.client.return_value
regions_mock.describe_regions.return_value = {"Regions": mock_regions}

result: List[Dict[str, str | bool]] | List[str] = get_region_list(details=True)
result.sort(key=lambda x: x["RegionName"]) # Sort the result for consistent ordering
print("Filtered Regions with Details:", result)

expected_result: List[Dict[str, str]] = [
{"RegionName": "us-east-1", "OptInStatus": "opt-in-not-required", "GeographicalLocation": "US East (N. Virginia)"},
{"RegionName": "us-west-1", "OptInStatus": "opt-in-not-required", "GeographicalLocation": "US West (N. California)"},
{"RegionName": "eu-west-1", "OptInStatus": "opted-in", "GeographicalLocation": "EU West (Ireland)"}
]
expected_result.sort(key=lambda x: x["RegionName"]) # Sort the expected result for consistent ordering
print("Expected Result:", expected_result)

assert result == expected_result # nosec: B101


def test_get_region_list_include_filter(boto3_client_mock) -> None:
def test_get_region_list_include_filter(boto3_session_mock) -> None:
"""
Test fetching regions with an include filter.

Arguments:
boto3_client_mock (fixture): The mocked boto3 client.
boto3_session_mock (fixture): The mocked boto3 session.
"""
regions_mock: Any = boto3_client_mock.return_value
regions_mock: Any = boto3_session_mock.return_value.client.return_value

regions_mock.describe_regions.return_value = {"Regions": mock_regions}

Expand All @@ -85,14 +83,14 @@ def test_get_region_list_include_filter(boto3_client_mock) -> None:
assert result == expected_result # nosec: B101


def test_get_region_list_exclude_filter(boto3_client_mock) -> None:
def test_get_region_list_exclude_filter(boto3_session_mock) -> None:
"""
Test fetching regions with an exclude filter.

Arguments:
boto3_client_mock (fixture): The mocked boto3 client.
boto3_session_mock (fixture): The mocked boto3 session.
"""
regions_mock: Any = boto3_client_mock.return_value
regions_mock: Any = boto3_session_mock.return_value.client.return_value

regions_mock.describe_regions.return_value = {"Regions": mock_regions}

Expand All @@ -108,14 +106,14 @@ def test_get_region_list_exclude_filter(boto3_client_mock) -> None:
assert result == expected_result # nosec: B101


def test_get_region_list_no_details(boto3_client_mock) -> None:
def test_get_region_list_no_details(boto3_session_mock) -> None:
"""
Test fetching region names without details.

Arguments:
boto3_client_mock (fixture): The mocked boto3 client.
boto3_session_mock (fixture): The mocked boto3 session.
"""
regions_mock: Any = boto3_client_mock.return_value
regions_mock: Any = boto3_session_mock.return_value.client.return_value

regions_mock.describe_regions.return_value = {"Regions": mock_regions}

Expand All @@ -128,12 +126,12 @@ def test_get_region_list_no_details(boto3_client_mock) -> None:
assert result == expected_result # nosec: B101


def test_get_region_list_exceptions(boto3_client_mock_with_exception) -> None: # pylint: disable=unused-argument
def test_get_region_list_exceptions(boto3_session_mock_with_exception) -> None: # pylint: disable=unused-argument
"""
Test exception handling when an error occurs in fetching regions.

Arguments:
boto3_client_mock (fixture): The mocked boto3 client.
boto3_session_mock (fixture): The mocked boto3 session.
"""
# Use pytest.raises to catch RegionListingError
with pytest.raises(RegionListingError) as excinfo:
Expand Down
58 changes: 37 additions & 21 deletions wolfsoftware/get_aws_regions/functions.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
"""
This module provides functionalities for retrieving and processing AWS region information.
This module provides functionalities for retrieving and processing AWS region information with support for multiple AWS profiles.

It includes the following capabilities:
Capabilities:
- Fetching a list of all AWS regions, with the option to include or exclude regions based on account opt-in status.
- Retrieving the geographical location description for a specific AWS region from the AWS Systems Manager (SSM) Parameter Store.
- Fetching geographical locations for multiple AWS regions using concurrent threading for improved performance.
- Applying include and exclude filters to the list of regions to customize the output.
- Generating a comprehensive list of AWS regions, with optional detailed information about each region.
- Utilizing different AWS profiles for the retrieval of region information.

Functions:
- get_region_list: Main function to retrieve a list of AWS regions, optionally filtering by include and exclude lists and returning
detailed information if required.
- get_region_list: Main function to retrieve a list of AWS regions, optionally filtering by include and exclude lists,
and returning detailed information if required.

Private Functions:
Private Functions:
- _fetch_all_regions: Retrieves a list of all AWS regions.
- _fetch_region_description: Fetches the geographical location for a specific AWS region.
- _fetch_region_descriptions: Fetches geographical locations for multiple AWS regions using threading.
Expand All @@ -24,28 +25,34 @@
Dependencies:
- boto3: AWS SDK for Python to interact with various AWS services like EC2 and SSM.
- concurrent.futures: Standard library module to enable asynchronous execution using threading.
- botocore.exceptions: Exceptions for handling errors during boto3 operations.

Usage:
This module is intended to be used as part of the wolfsoftware.get-aws-regions package. The main entry point is the `get_region_list` function,
which provides flexibility in retrieving and customizing the list of AWS regions based on user-defined criteria.
This module is intended to be used as part of the wolfsoftware.get-aws-regions package.
The main entry point is the `get_region_list` function, which provides flexibility in retrieving
and customizing the list of AWS regions based on user-defined criteria, including the ability
to specify different AWS profiles.
"""

from typing import Any, List, Dict, Optional, Union

from concurrent.futures._base import Future
from concurrent.futures import ThreadPoolExecutor, as_completed

import boto3 # pylint: disable=import-error
from botocore.exceptions import BotoCoreError, ClientError

from .exceptions import RegionListingError


def _fetch_all_regions(all_regions: bool = True) -> List[Dict[str, Union[str, bool]]]:
def _fetch_all_regions(all_regions: bool = True, profile_name: Optional[str] = None) -> List[Dict[str, Union[str, bool]]]:
"""
Retrieve a list of all AWS regions.

Arguments:
all_regions (bool): If True, list all available regions, including those not opted into.
If False, list only regions opted into by the account.
profile_name (Optional[str]): The name of the AWS profile to use.

Returns:
List[Dict[str, Union[str, bool]]]: A list of dictionaries containing information about each region.
Expand All @@ -54,8 +61,9 @@ def _fetch_all_regions(all_regions: bool = True) -> List[Dict[str, Union[str, bo
RegionListingError: If there is an error in retrieving the regions.
"""
try:
# Initialize a session using Amazon EC2
ec2: Any = boto3.client('ec2')
# Initialize a session using Amazon EC2 with the specified profile
session = boto3.Session(profile_name=profile_name) if profile_name else boto3.Session()
ec2: Any = session.client('ec2')

# Retrieve a list of all available regions or only opted-in regions based on the flag
if all_regions:
Expand All @@ -71,18 +79,19 @@ def _fetch_all_regions(all_regions: bool = True) -> List[Dict[str, Union[str, bo

return regions

except boto3.exceptions.Boto3Error as e:
except (BotoCoreError, ClientError) as e:
raise RegionListingError(f"An error occurred while listing regions: {str(e)}") from e
except Exception as e:
raise RegionListingError(f"An unexpected error occurred: {str(e)}") from e


def _fetch_region_description(region_name: str) -> Dict[str, str]:
def _fetch_region_description(region_name: str, profile_name: Optional[str] = None) -> Dict[str, str]:
"""
Fetch the geographical location for a specific AWS region from SSM Parameter Store.

Arguments:
region_name (str): The name of the region to fetch the geographical location for.
profile_name (Optional[str]): The name of the AWS profile to use.

Returns:
Dict[str, str]: A dictionary containing the region name and its geographical location.
Expand All @@ -91,27 +100,29 @@ def _fetch_region_description(region_name: str) -> Dict[str, str]:
RegionListingError: If there is an error in retrieving the region geographical location.
"""
try:
# Initialize a session using Amazon SSM
ssm: Any = boto3.client('ssm')
# Initialize a session using Amazon SSM with the specified profile
session = boto3.Session(profile_name=profile_name) if profile_name else boto3.Session()
ssm: Any = session.client('ssm')

# Retrieve the parameter for the region description
parameter_name: str = f"/aws/service/global-infrastructure/regions/{region_name}/longName"
response: Any = ssm.get_parameter(Name=parameter_name)

return {region_name: response['Parameter']['Value']}

except boto3.exceptions.Boto3Error as e:
except (BotoCoreError, ClientError) as e:
raise RegionListingError(f"An error occurred while retrieving geographical location for region {region_name}: {str(e)}") from e
except Exception as e:
raise RegionListingError(f"An unexpected error occurred: {str(e)}") from e


def _fetch_region_descriptions(region_names: List[str]) -> Dict[str, str]:
def _fetch_region_descriptions(region_names: List[str], profile_name: Optional[str] = None) -> Dict[str, str]:
"""
Fetch geographical locations for multiple AWS regions from SSM Parameter Store using threading for better performance.

Arguments:
region_names (List[str]): A list of region names to fetch geographical locations for.
profile_name (Optional[str]): The name of the AWS profile to use.

Returns:
Dict[str, str]: A dictionary mapping region codes to their geographical locations.
Expand All @@ -122,7 +133,10 @@ def _fetch_region_descriptions(region_names: List[str]) -> Dict[str, str]:
descriptions: Dict = {}

with ThreadPoolExecutor() as executor:
future_to_region: Dict[Future[Dict[str, str]], str] = {executor.submit(_fetch_region_description, region): region for region in region_names}
future_to_region: Dict[Future[Dict[str, str]], str] = {
executor.submit(_fetch_region_description, region, profile_name): region
for region in region_names
}

for future in as_completed(future_to_region):
region: str = future_to_region[future]
Expand Down Expand Up @@ -166,8 +180,9 @@ def _apply_region_filters(
def get_region_list(
include_list: Optional[List[str]] = None,
exclude_list: Optional[List[str]] = None,
all_regions: bool = True,
details: bool = False
all_regions: Optional[bool] = True,
details: Optional[bool] = False,
profile_name: Optional[str] = None
) -> Union[List[Dict[str, Union[str, bool]]], List[str]]:
"""
Retrieve a list of AWS regions, optionally filtering by include and exclude lists.
Expand All @@ -179,6 +194,7 @@ def get_region_list(
exclude_list (Optional[List[str]]): A list of regions to exclude. These regions will be omitted from the returned list if specified.
all_regions (bool): If True, list all available regions, including those not opted into. If False, list only regions opted into by the account.
details (bool): If True, return detailed information about each region. If False, return only the region names.
profile_name (Optional[str]): The name of the AWS profile to use.

Returns:
Union[List[Dict[str, Union[str, bool]]], List[str]]: A sorted list of regions with detailed information or just the region names.
Expand All @@ -187,15 +203,15 @@ def get_region_list(
RegionListingError: If there is an error in retrieving the regions.
"""
try:
all_regions_list: List[Dict[str, str | bool]] = _fetch_all_regions(all_regions)
all_regions_list: List[Dict[str, str | bool]] = _fetch_all_regions(all_regions, profile_name)
except Exception as e:
raise RegionListingError(f"An error occurred while retrieving regions: {str(e)}") from e

filtered_regions: List[Dict[str, str | bool]] = _apply_region_filters(all_regions_list, include_list, exclude_list)

if details:
region_names: List[str | bool] = [region['RegionName'] for region in filtered_regions]
region_descriptions: Dict[str, str] = _fetch_region_descriptions(region_names)
region_descriptions: Dict[str, str] = _fetch_region_descriptions(region_names, profile_name)
for region in filtered_regions:
region['GeographicalLocation'] = region_descriptions.get(region['RegionName'], "Unknown")
print("Filtered Regions with Details:", filtered_regions) # Debug print
Expand Down