From fc5d5b00867e321eaad0b70db0956cc45566f1ea Mon Sep 17 00:00:00 2001 From: Michal Wojcik Date: Fri, 13 Feb 2026 15:48:57 +0100 Subject: [PATCH 1/2] python-sdk: Support regions/vpc-availability endpoints --- linode_api4/groups/region.py | 76 +++++++- linode_api4/objects/region.py | 38 ++++ .../regions_us-east_vpc-availability.json | 5 + test/fixtures/regions_vpc-availability.json | 132 ++++++++++++++ test/integration/models/region/test_region.py | 168 ++++++++++++++++++ test/unit/groups/region_test.py | 63 ++++++- test/unit/objects/region_test.py | 12 ++ 7 files changed, 488 insertions(+), 6 deletions(-) create mode 100644 test/fixtures/regions_us-east_vpc-availability.json create mode 100644 test/fixtures/regions_vpc-availability.json create mode 100644 test/integration/models/region/test_region.py diff --git a/linode_api4/groups/region.py b/linode_api4/groups/region.py index baf8697e4..481b4f660 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,74 @@ 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..f3ce2f4fd --- /dev/null +++ b/test/integration/models/region/test_region.py @@ -0,0 +1,168 @@ +import pytest + +from linode_api4.objects import Region +from linode_api4.objects.region import RegionVPCAvailability + + +@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_matches_region_id(test_linode_client): + """ + Test that VPC availability region field matches the requested region. + """ + client = test_linode_client + + # Test with a known region + region_id = "us-east" + vpc_avail = client.regions.vpc_availability_get(region_id) + + assert vpc_avail.region == region_id + + +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 + + # Check that available regions have proper data + for entry in available_regions: + assert entry.region is not None + assert entry.available is True + + +def test_vpc_availability_pagination(test_linode_client): + """ + Test that VPC availability listing returns all entries. + """ + client = test_linode_client + + # Get all VPC availability entries + all_entries = client.regions.vpc_availability() + + # Verify we got results + assert len(all_entries) > 0 + + # Each entry should have required fields + for entry in all_entries: + assert entry.region is not None + assert entry.available is not None + assert entry.available_ipv6_prefix_lengths is not None + + +def test_vpc_availability_list_complete(test_linode_client): + """ + Test that vpc_availability endpoint returns complete list of all regions. + """ + client = test_linode_client + + # Get VPC availability for all regions + vpc_availability = client.regions.vpc_availability() + + # Get all regions + all_regions = client.regions() + + # VPC availability should contain entries for all regions + assert len(vpc_availability) == len(all_regions) + + # Verify each region has a VPC availability entry + vpc_region_ids = {v.region for v in vpc_availability} + all_region_ids = {r.id for r in all_regions} + + assert vpc_region_ids == all_region_ids diff --git a/test/unit/groups/region_test.py b/test/unit/groups/region_test.py index fe44c13ab..54ca5262a 100644 --- a/test/unit/groups/region_test.py +++ b/test/unit/groups/region_test.py @@ -1,7 +1,10 @@ import json from test.unit.base import ClientBaseCase -from linode_api4.objects.region import RegionAvailabilityEntry +from linode_api4.objects.region import ( + RegionAvailabilityEntry, + RegionVPCAvailability, +) class RegionTest(ClientBaseCase): @@ -23,12 +26,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 +48,57 @@ 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 all entries have valid data + for entry in avail_entries: + assert entry.region == "us-east" + assert len(entry.plan) > 0 + assert 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) From 37ad44ef4eb6b624c149e29fa4ef922fbb5a8f17 Mon Sep 17 00:00:00 2001 From: Michal Wojcik Date: Fri, 13 Feb 2026 16:29:04 +0100 Subject: [PATCH 2/2] python-sdk: Support regions/vpc-availability endpoints --- linode_api4/groups/region.py | 4 +- test/integration/models/region/test_region.py | 60 ------------------- test/unit/groups/region_test.py | 19 +++--- 3 files changed, 12 insertions(+), 71 deletions(-) diff --git a/linode_api4/groups/region.py b/linode_api4/groups/region.py index 481b4f660..ca7626b89 100644 --- a/linode_api4/groups/region.py +++ b/linode_api4/groups/region.py @@ -93,7 +93,9 @@ def vpc_availability(self, *filters): """ return self.client._get_and_filter( - RegionVPCAvailability, *filters, endpoint="/regions/vpc-availability" + RegionVPCAvailability, + *filters, + endpoint="/regions/vpc-availability", ) def vpc_availability_get(self, region_id): diff --git a/test/integration/models/region/test_region.py b/test/integration/models/region/test_region.py index f3ce2f4fd..bd0ee93d4 100644 --- a/test/integration/models/region/test_region.py +++ b/test/integration/models/region/test_region.py @@ -1,7 +1,6 @@ import pytest from linode_api4.objects import Region -from linode_api4.objects.region import RegionVPCAvailability @pytest.mark.smoke @@ -94,19 +93,6 @@ def test_get_region_availability_via_group(test_linode_client): assert isinstance(entry.available, bool) -def test_vpc_availability_matches_region_id(test_linode_client): - """ - Test that VPC availability region field matches the requested region. - """ - client = test_linode_client - - # Test with a known region - region_id = "us-east" - vpc_avail = client.regions.vpc_availability_get(region_id) - - assert vpc_avail.region == region_id - - def test_vpc_availability_available_regions(test_linode_client): """ Test that some regions have VPC availability enabled. @@ -120,49 +106,3 @@ def test_vpc_availability_available_regions(test_linode_client): # There should be at least some regions with VPC available assert len(available_regions) > 0 - - # Check that available regions have proper data - for entry in available_regions: - assert entry.region is not None - assert entry.available is True - - -def test_vpc_availability_pagination(test_linode_client): - """ - Test that VPC availability listing returns all entries. - """ - client = test_linode_client - - # Get all VPC availability entries - all_entries = client.regions.vpc_availability() - - # Verify we got results - assert len(all_entries) > 0 - - # Each entry should have required fields - for entry in all_entries: - assert entry.region is not None - assert entry.available is not None - assert entry.available_ipv6_prefix_lengths is not None - - -def test_vpc_availability_list_complete(test_linode_client): - """ - Test that vpc_availability endpoint returns complete list of all regions. - """ - client = test_linode_client - - # Get VPC availability for all regions - vpc_availability = client.regions.vpc_availability() - - # Get all regions - all_regions = client.regions() - - # VPC availability should contain entries for all regions - assert len(vpc_availability) == len(all_regions) - - # Verify each region has a VPC availability entry - vpc_region_ids = {v.region for v in vpc_availability} - all_region_ids = {r.id for r in all_regions} - - assert vpc_region_ids == all_region_ids diff --git a/test/unit/groups/region_test.py b/test/unit/groups/region_test.py index 54ca5262a..019bf4797 100644 --- a/test/unit/groups/region_test.py +++ b/test/unit/groups/region_test.py @@ -1,10 +1,7 @@ import json from test.unit.base import ClientBaseCase -from linode_api4.objects.region import ( - RegionAvailabilityEntry, - RegionVPCAvailability, -) +from linode_api4.objects.region import RegionAvailabilityEntry class RegionTest(ClientBaseCase): @@ -67,7 +64,9 @@ def test_list_vpc_availability(self): # 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[0].args[0] == "//regions/vpc-availability" + ) assert ( m.mock.call_args_list[1].args[0] @@ -97,8 +96,8 @@ def test_get_availability(self): assert len(avail_entries) > 0 - # Verify all entries have valid data - for entry in avail_entries: - assert entry.region == "us-east" - assert len(entry.plan) > 0 - assert entry.available is not None + # 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