diff --git a/linode_api4/groups/region.py b/linode_api4/groups/region.py index baf8697e4..ca7626b89 100644 --- a/linode_api4/groups/region.py +++ b/linode_api4/groups/region.py @@ -1,6 +1,9 @@ from linode_api4.groups import Group from linode_api4.objects import Region -from linode_api4.objects.region import RegionAvailabilityEntry +from linode_api4.objects.region import ( + RegionAvailabilityEntry, + RegionVPCAvailability, +) class RegionGroup(Group): @@ -43,3 +46,76 @@ def availability(self, *filters): return self.client._get_and_filter( RegionAvailabilityEntry, *filters, endpoint="/regions/availability" ) + + def availability_get(self, region_id): + """ + Returns availability data for a specific region. + + API Documentation: https://techdocs.akamai.com/linode-api/reference/get-region-availability + + :param region_id: The ID of the region to retrieve availability for. + :type region_id: str + + :returns: A list of availability entries for the specified region. + :rtype: list of RegionAvailabilityEntry + """ + + result = self.client.get(f"/regions/{region_id}/availability") + + if result is None: + return [] + + return [RegionAvailabilityEntry.from_json(v) for v in result] + + def vpc_availability(self, *filters): + """ + Returns VPC availability data for all regions. + + NOTE: IPv6 VPCs may not currently be available to all users. + + This endpoint supports pagination with the following parameters: + - page: Page number (>= 1) + - page_size: Number of items per page (25-500) + + Pagination is handled automatically by PaginatedList. To configure page_size, + set it when creating the LinodeClient: + + client = LinodeClient(token, page_size=100) + + API Documentation: https://techdocs.akamai.com/linode-api/reference/get-regions-vpc-availability + + :param filters: Any number of filters to apply to this query. + See :doc:`Filtering Collections` + for more details on filtering. + + :returns: A list of VPC availability data for regions. + :rtype: PaginatedList of RegionVPCAvailability + """ + + return self.client._get_and_filter( + RegionVPCAvailability, + *filters, + endpoint="/regions/vpc-availability", + ) + + def vpc_availability_get(self, region_id): + """ + Returns VPC availability data for a specific region. + + NOTE: IPv6 VPCs may not currently be available to all users. + + API Documentation: https://techdocs.akamai.com/linode-api/reference/get-region-vpc-availability + + :param region_id: The ID of the region to retrieve VPC availability for. + :type region_id: str + + :returns: VPC availability data for the specified region. + :rtype: RegionVPCAvailability + """ + + result = self.client.get(f"/regions/{region_id}/vpc-availability") + + if result is None: + return None + + return RegionVPCAvailability.from_json(result) diff --git a/linode_api4/objects/region.py b/linode_api4/objects/region.py index c9dc05099..232170fce 100644 --- a/linode_api4/objects/region.py +++ b/linode_api4/objects/region.py @@ -63,6 +63,29 @@ def availability(self) -> List["RegionAvailabilityEntry"]: return [RegionAvailabilityEntry.from_json(v) for v in result] + @property + def vpc_availability(self) -> "RegionVPCAvailability": + """ + Returns VPC availability data for this region. + + NOTE: IPv6 VPCs may not currently be available to all users. + + API Documentation: https://techdocs.akamai.com/linode-api/reference/get-region-vpc-availability + + :returns: VPC availability data for this region. + :rtype: RegionVPCAvailability + """ + result = self._client.get( + f"{self.api_endpoint}/vpc-availability", model=self + ) + + if result is None: + raise UnexpectedResponseError( + "Expected VPC availability data, got None." + ) + + return RegionVPCAvailability.from_json(result) + @dataclass class RegionAvailabilityEntry(JSONObject): @@ -75,3 +98,18 @@ class RegionAvailabilityEntry(JSONObject): region: Optional[str] = None plan: Optional[str] = None available: bool = False + + +@dataclass +class RegionVPCAvailability(JSONObject): + """ + Represents the VPC availability data for a region. + + API Documentation: https://techdocs.akamai.com/linode-api/reference/get-regions-vpc-availability + + NOTE: IPv6 VPCs may not currently be available to all users. + """ + + region: Optional[str] = None + available: bool = False + available_ipv6_prefix_lengths: Optional[List[int]] = None diff --git a/test/fixtures/regions_us-east_vpc-availability.json b/test/fixtures/regions_us-east_vpc-availability.json new file mode 100644 index 000000000..531dd649e --- /dev/null +++ b/test/fixtures/regions_us-east_vpc-availability.json @@ -0,0 +1,5 @@ +{ + "region": "us-east", + "available": true, + "available_ipv6_prefix_lengths": [52, 60] +} diff --git a/test/fixtures/regions_vpc-availability.json b/test/fixtures/regions_vpc-availability.json new file mode 100644 index 000000000..5e4d386df --- /dev/null +++ b/test/fixtures/regions_vpc-availability.json @@ -0,0 +1,132 @@ +{ + "data": [ + { + "region": "us-east", + "available": true, + "available_ipv6_prefix_lengths": [52, 48] + }, + { + "region": "us-west", + "available": true, + "available_ipv6_prefix_lengths": [56, 52, 48] + }, + { + "region": "nl-ams", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "us-ord", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "us-iad", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "fr-par", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "us-sea", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "br-gru", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "se-sto", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "es-mad", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "in-maa", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "jp-osa", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "it-mil", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "us-mia", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "id-cgk", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "us-lax", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "gb-lon", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "au-mel", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "in-bom-2", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "de-fra-2", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "sg-sin-2", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "jp-tyo-3", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "fr-par-2", + "available": true, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "ca-central", + "available": false, + "available_ipv6_prefix_lengths": [] + }, + { + "region": "ap-southeast", + "available": false, + "available_ipv6_prefix_lengths": [] + } + ], + "page": 1, + "pages": 2, + "results": 50 +} diff --git a/test/integration/models/region/test_region.py b/test/integration/models/region/test_region.py new file mode 100644 index 000000000..bd0ee93d4 --- /dev/null +++ b/test/integration/models/region/test_region.py @@ -0,0 +1,108 @@ +import pytest + +from linode_api4.objects import Region + + +@pytest.mark.smoke +def test_list_regions_vpc_availability(test_linode_client): + """ + Test listing VPC availability for all regions. + """ + client = test_linode_client + + vpc_availability = client.regions.vpc_availability() + + assert len(vpc_availability) > 0 + + for entry in vpc_availability: + assert entry.region is not None + assert len(entry.region) > 0 + assert entry.available is not None + assert isinstance(entry.available, bool) + # available_ipv6_prefix_lengths may be empty list but should exist + assert entry.available_ipv6_prefix_lengths is not None + assert isinstance(entry.available_ipv6_prefix_lengths, list) + + +@pytest.mark.smoke +def test_get_region_vpc_availability_via_group(test_linode_client): + """ + Test getting VPC availability for a specific region via RegionGroup. + """ + client = test_linode_client + + # Get the first available region + regions = client.regions() + assert len(regions) > 0 + test_region_id = regions[0].id + + vpc_avail = client.regions.vpc_availability_get(test_region_id) + + assert vpc_avail is not None + assert vpc_avail.region == test_region_id + assert vpc_avail.available is not None + assert isinstance(vpc_avail.available, bool) + assert vpc_avail.available_ipv6_prefix_lengths is not None + assert isinstance(vpc_avail.available_ipv6_prefix_lengths, list) + + +@pytest.mark.smoke +def test_get_region_vpc_availability_via_object(test_linode_client): + """ + Test getting VPC availability via the Region object property. + """ + client = test_linode_client + + # Get the first available region + regions = client.regions() + assert len(regions) > 0 + test_region_id = regions[0].id + + region = Region(client, test_region_id) + vpc_avail = region.vpc_availability + + assert vpc_avail is not None + assert vpc_avail.region == test_region_id + assert vpc_avail.available is not None + assert isinstance(vpc_avail.available, bool) + assert vpc_avail.available_ipv6_prefix_lengths is not None + assert isinstance(vpc_avail.available_ipv6_prefix_lengths, list) + + +@pytest.mark.smoke +def test_get_region_availability_via_group(test_linode_client): + """ + Test getting availability for a specific region via RegionGroup. + """ + client = test_linode_client + + # Get the first available region + regions = client.regions() + assert len(regions) > 0 + test_region_id = regions[0].id + + avail_entries = client.regions.availability_get(test_region_id) + + assert len(avail_entries) > 0 + + for entry in avail_entries: + assert entry.region == test_region_id + assert entry.plan is not None + assert len(entry.plan) > 0 + assert entry.available is not None + assert isinstance(entry.available, bool) + + +def test_vpc_availability_available_regions(test_linode_client): + """ + Test that some regions have VPC availability enabled. + """ + client = test_linode_client + + vpc_availability = client.regions.vpc_availability() + + # Filter for regions where VPC is available + available_regions = [v for v in vpc_availability if v.available] + + # There should be at least some regions with VPC available + assert len(available_regions) > 0 diff --git a/test/unit/groups/region_test.py b/test/unit/groups/region_test.py index fe44c13ab..019bf4797 100644 --- a/test/unit/groups/region_test.py +++ b/test/unit/groups/region_test.py @@ -23,12 +23,8 @@ def test_list_availability(self): assert len(avail_entries) > 0 for entry in avail_entries: - assert entry.region is not None assert len(entry.region) > 0 - - assert entry.plan is not None assert len(entry.plan) > 0 - assert entry.available is not None # Ensure all three pages are read @@ -49,3 +45,59 @@ def test_list_availability(self): assert json.loads(call.get("headers").get("X-Filter")) == { "+and": [{"region": "us-east"}, {"plan": "premium4096.7"}] } + + def test_list_vpc_availability(self): + """ + Tests that region VPC availability can be listed. + """ + + with self.mock_get("/regions/vpc-availability") as m: + vpc_entries = self.client.regions.vpc_availability() + + assert len(vpc_entries) > 0 + + for entry in vpc_entries: + assert len(entry.region) > 0 + assert entry.available is not None + # available_ipv6_prefix_lengths may be empty list but should exist + assert entry.available_ipv6_prefix_lengths is not None + + # Ensure both pages are read + assert m.call_count == 2 + assert ( + m.mock.call_args_list[0].args[0] == "//regions/vpc-availability" + ) + + assert ( + m.mock.call_args_list[1].args[0] + == "//regions/vpc-availability?page=2&page_size=25" + ) + + def test_get_vpc_availability(self): + """ + Tests that VPC availability for a specific region can be retrieved. + """ + + with self.mock_get("/regions/us-east/vpc-availability") as m: + vpc_avail = self.client.regions.vpc_availability_get("us-east") + + assert vpc_avail is not None + assert vpc_avail.region == "us-east" + assert vpc_avail.available is True + assert vpc_avail.available_ipv6_prefix_lengths == [52, 60] + + def test_get_availability(self): + """ + Tests that availability for a specific region can be retrieved. + """ + + with self.mock_get("/regions/us-east/availability") as m: + avail_entries = self.client.regions.availability_get("us-east") + + assert len(avail_entries) > 0 + + # Verify first entry to ensure deserialization works + first_entry = avail_entries[0] + assert first_entry.region == "us-east" + assert len(first_entry.plan) > 0 + assert first_entry.available is not None diff --git a/test/unit/objects/region_test.py b/test/unit/objects/region_test.py index 73fdc8f5d..7bc3ae9f8 100644 --- a/test/unit/objects/region_test.py +++ b/test/unit/objects/region_test.py @@ -49,3 +49,15 @@ def test_region_availability(self): assert len(entry.plan) > 0 assert entry.available is not None + + def test_region_vpc_availability(self): + """ + Tests that VPC availability for a specific region can be retrieved. + """ + vpc_avail = Region(self.client, "us-east").vpc_availability + + assert vpc_avail is not None + assert vpc_avail.region == "us-east" + assert vpc_avail.available is True + assert vpc_avail.available_ipv6_prefix_lengths is not None + assert isinstance(vpc_avail.available_ipv6_prefix_lengths, list)