Skip to content

Commit fa417d5

Browse files
authored
python-sdk: Support regions/vpc-availability endpoints (#646)
1 parent d18b54e commit fa417d5

File tree

7 files changed

+311
-4
lines changed

7 files changed

+311
-4
lines changed

linode_api4/groups/region.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from linode_api4.groups import Group
22
from linode_api4.objects import Region
3-
from linode_api4.objects.region import RegionAvailabilityEntry
3+
from linode_api4.objects.region import (
4+
RegionAvailabilityEntry,
5+
RegionVPCAvailability,
6+
)
47

58

69
class RegionGroup(Group):
@@ -43,3 +46,34 @@ def availability(self, *filters):
4346
return self.client._get_and_filter(
4447
RegionAvailabilityEntry, *filters, endpoint="/regions/availability"
4548
)
49+
50+
def vpc_availability(self, *filters):
51+
"""
52+
Returns VPC availability data for all regions.
53+
54+
NOTE: IPv6 VPCs may not currently be available to all users.
55+
56+
This endpoint supports pagination with the following parameters:
57+
- page: Page number (>= 1)
58+
- page_size: Number of items per page (25-500)
59+
60+
Pagination is handled automatically by PaginatedList. To configure page_size,
61+
set it when creating the LinodeClient:
62+
63+
client = LinodeClient(token, page_size=100)
64+
65+
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-regions-vpc-availability
66+
67+
:param filters: Any number of filters to apply to this query.
68+
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
69+
for more details on filtering.
70+
71+
:returns: A list of VPC availability data for regions.
72+
:rtype: PaginatedList of RegionVPCAvailability
73+
"""
74+
75+
return self.client._get_and_filter(
76+
RegionVPCAvailability,
77+
*filters,
78+
endpoint="/regions/vpc-availability",
79+
)

linode_api4/objects/region.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,29 @@ def availability(self) -> List["RegionAvailabilityEntry"]:
125125

126126
return [RegionAvailabilityEntry.from_json(v) for v in result]
127127

128+
@property
129+
def vpc_availability(self) -> "RegionVPCAvailability":
130+
"""
131+
Returns VPC availability data for this region.
132+
133+
NOTE: IPv6 VPCs may not currently be available to all users.
134+
135+
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-region-vpc-availability
136+
137+
:returns: VPC availability data for this region.
138+
:rtype: RegionVPCAvailability
139+
"""
140+
result = self._client.get(
141+
f"{self.api_endpoint}/vpc-availability", model=self
142+
)
143+
144+
if result is None:
145+
raise UnexpectedResponseError(
146+
"Expected VPC availability data, got None."
147+
)
148+
149+
return RegionVPCAvailability.from_json(result)
150+
128151

129152
@dataclass
130153
class RegionAvailabilityEntry(JSONObject):
@@ -137,3 +160,18 @@ class RegionAvailabilityEntry(JSONObject):
137160
region: Optional[str] = None
138161
plan: Optional[str] = None
139162
available: bool = False
163+
164+
165+
@dataclass
166+
class RegionVPCAvailability(JSONObject):
167+
"""
168+
Represents the VPC availability data for a region.
169+
170+
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-regions-vpc-availability
171+
172+
NOTE: IPv6 VPCs may not currently be available to all users.
173+
"""
174+
175+
region: Optional[str] = None
176+
available: bool = False
177+
available_ipv6_prefix_lengths: Optional[List[int]] = None
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"region": "us-east",
3+
"available": true,
4+
"available_ipv6_prefix_lengths": [52, 48]
5+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
{
2+
"data": [
3+
{
4+
"region": "us-east",
5+
"available": true,
6+
"available_ipv6_prefix_lengths": [52, 48]
7+
},
8+
{
9+
"region": "us-west",
10+
"available": true,
11+
"available_ipv6_prefix_lengths": [56, 52, 48]
12+
},
13+
{
14+
"region": "nl-ams",
15+
"available": true,
16+
"available_ipv6_prefix_lengths": []
17+
},
18+
{
19+
"region": "us-ord",
20+
"available": true,
21+
"available_ipv6_prefix_lengths": []
22+
},
23+
{
24+
"region": "us-iad",
25+
"available": true,
26+
"available_ipv6_prefix_lengths": []
27+
},
28+
{
29+
"region": "fr-par",
30+
"available": true,
31+
"available_ipv6_prefix_lengths": []
32+
},
33+
{
34+
"region": "us-sea",
35+
"available": true,
36+
"available_ipv6_prefix_lengths": []
37+
},
38+
{
39+
"region": "br-gru",
40+
"available": true,
41+
"available_ipv6_prefix_lengths": []
42+
},
43+
{
44+
"region": "se-sto",
45+
"available": true,
46+
"available_ipv6_prefix_lengths": []
47+
},
48+
{
49+
"region": "es-mad",
50+
"available": true,
51+
"available_ipv6_prefix_lengths": []
52+
},
53+
{
54+
"region": "in-maa",
55+
"available": true,
56+
"available_ipv6_prefix_lengths": []
57+
},
58+
{
59+
"region": "jp-osa",
60+
"available": true,
61+
"available_ipv6_prefix_lengths": []
62+
},
63+
{
64+
"region": "it-mil",
65+
"available": true,
66+
"available_ipv6_prefix_lengths": []
67+
},
68+
{
69+
"region": "us-mia",
70+
"available": true,
71+
"available_ipv6_prefix_lengths": []
72+
},
73+
{
74+
"region": "id-cgk",
75+
"available": true,
76+
"available_ipv6_prefix_lengths": []
77+
},
78+
{
79+
"region": "us-lax",
80+
"available": true,
81+
"available_ipv6_prefix_lengths": []
82+
},
83+
{
84+
"region": "gb-lon",
85+
"available": true,
86+
"available_ipv6_prefix_lengths": []
87+
},
88+
{
89+
"region": "au-mel",
90+
"available": true,
91+
"available_ipv6_prefix_lengths": []
92+
},
93+
{
94+
"region": "in-bom-2",
95+
"available": true,
96+
"available_ipv6_prefix_lengths": []
97+
},
98+
{
99+
"region": "de-fra-2",
100+
"available": true,
101+
"available_ipv6_prefix_lengths": []
102+
},
103+
{
104+
"region": "sg-sin-2",
105+
"available": true,
106+
"available_ipv6_prefix_lengths": []
107+
},
108+
{
109+
"region": "jp-tyo-3",
110+
"available": true,
111+
"available_ipv6_prefix_lengths": []
112+
},
113+
{
114+
"region": "fr-par-2",
115+
"available": true,
116+
"available_ipv6_prefix_lengths": []
117+
},
118+
{
119+
"region": "ca-central",
120+
"available": false,
121+
"available_ipv6_prefix_lengths": []
122+
},
123+
{
124+
"region": "ap-southeast",
125+
"available": false,
126+
"available_ipv6_prefix_lengths": []
127+
}
128+
],
129+
"page": 1,
130+
"pages": 2,
131+
"results": 50
132+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import pytest
2+
3+
from linode_api4.objects import Region
4+
5+
6+
@pytest.mark.smoke
7+
def test_list_regions_vpc_availability(test_linode_client):
8+
"""
9+
Test listing VPC availability for all regions.
10+
"""
11+
client = test_linode_client
12+
13+
vpc_availability = client.regions.vpc_availability()
14+
15+
assert len(vpc_availability) > 0
16+
17+
for entry in vpc_availability:
18+
assert entry.region is not None
19+
assert len(entry.region) > 0
20+
assert entry.available is not None
21+
assert isinstance(entry.available, bool)
22+
# available_ipv6_prefix_lengths may be empty list but should exist
23+
assert entry.available_ipv6_prefix_lengths is not None
24+
assert isinstance(entry.available_ipv6_prefix_lengths, list)
25+
26+
27+
@pytest.mark.smoke
28+
def test_get_region_vpc_availability_via_object(test_linode_client):
29+
"""
30+
Test getting VPC availability via the Region object property.
31+
"""
32+
client = test_linode_client
33+
34+
# Get the first available region
35+
regions = client.regions()
36+
assert len(regions) > 0
37+
test_region_id = regions[0].id
38+
39+
region = Region(client, test_region_id)
40+
vpc_avail = region.vpc_availability
41+
42+
assert vpc_avail is not None
43+
assert vpc_avail.region == test_region_id
44+
assert vpc_avail.available is not None
45+
assert isinstance(vpc_avail.available, bool)
46+
assert vpc_avail.available_ipv6_prefix_lengths is not None
47+
assert isinstance(vpc_avail.available_ipv6_prefix_lengths, list)
48+
49+
50+
def test_vpc_availability_available_regions(test_linode_client):
51+
"""
52+
Test that some regions have VPC availability enabled.
53+
"""
54+
client = test_linode_client
55+
56+
vpc_availability = client.regions.vpc_availability()
57+
58+
# Filter for regions where VPC is available
59+
available_regions = [v for v in vpc_availability if v.available]
60+
61+
# There should be at least some regions with VPC available
62+
assert len(available_regions) > 0

test/unit/groups/region_test.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,7 @@ def test_list_availability(self):
2525
for entry in avail_entries:
2626
assert entry.region is not None
2727
assert len(entry.region) > 0
28-
29-
assert entry.plan is not None
3028
assert len(entry.plan) > 0
31-
3229
assert entry.available is not None
3330

3431
# Ensure all three pages are read
@@ -49,3 +46,30 @@ def test_list_availability(self):
4946
assert json.loads(call.get("headers").get("X-Filter")) == {
5047
"+and": [{"region": "us-east"}, {"plan": "premium4096.7"}]
5148
}
49+
50+
def test_list_vpc_availability(self):
51+
"""
52+
Tests that region VPC availability can be listed.
53+
"""
54+
55+
with self.mock_get("/regions/vpc-availability") as m:
56+
vpc_entries = self.client.regions.vpc_availability()
57+
58+
assert len(vpc_entries) > 0
59+
60+
for entry in vpc_entries:
61+
assert len(entry.region) > 0
62+
assert entry.available is not None
63+
# available_ipv6_prefix_lengths may be empty list but should exist
64+
assert entry.available_ipv6_prefix_lengths is not None
65+
66+
# Ensure both pages are read
67+
assert m.call_count == 2
68+
assert (
69+
m.mock.call_args_list[0].args[0] == "//regions/vpc-availability"
70+
)
71+
72+
assert (
73+
m.mock.call_args_list[1].args[0]
74+
== "//regions/vpc-availability?page=2&page_size=25"
75+
)

test/unit/objects/region_test.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,15 @@ def test_region_availability(self):
4949
assert len(entry.plan) > 0
5050

5151
assert entry.available is not None
52+
53+
def test_region_vpc_availability(self):
54+
"""
55+
Tests that VPC availability for a specific region can be retrieved.
56+
"""
57+
vpc_avail = Region(self.client, "us-east").vpc_availability
58+
59+
assert vpc_avail is not None
60+
assert vpc_avail.region == "us-east"
61+
assert vpc_avail.available is True
62+
assert vpc_avail.available_ipv6_prefix_lengths is not None
63+
assert isinstance(vpc_avail.available_ipv6_prefix_lengths, list)

0 commit comments

Comments
 (0)