Skip to content

Commit a77b0cc

Browse files
authored
feat: per location server types (#558)
[Server Types](https://docs.hetzner.cloud/reference/cloud#server-types) now depend on [Locations](https://docs.hetzner.cloud/reference/cloud#locations). - We added a new `locations` property to the [Server Types](https://docs.hetzner.cloud/reference/cloud#server-types) resource. The new property defines a list of supported [Locations](https://docs.hetzner.cloud/reference/cloud#locations) and additional per [Locations](https://docs.hetzner.cloud/reference/cloud#locations) details such as deprecations information. - We deprecated the `deprecation` property from the [Server Types](https://docs.hetzner.cloud/reference/cloud#server-types) resource. The property will gradually be phased out as per [Locations](https://docs.hetzner.cloud/reference/cloud#locations) deprecations are being announced. Please use the new per [Locations](https://docs.hetzner.cloud/reference/cloud#locations) deprecation information instead. See our [changelog](https://docs.hetzner.cloud/changelog#2025-09-24-per-location-server-types) for more details. **Upgrading** ```py def validate_server_type(server_type: ServerType): if server_type.deprecation is not None: raise ValueError(f"server type {server_type.name} is deprecated") ``` ```py def validate_server_type(server_type: ServerType, location: Location): found = [o for o in server_type.locations if location.name == o.location.name] if not found: raise ValueError( f"server type {server_type.name} is not supported in location {location.name}" ) server_type_location = found[0] if server_type_location.deprecation is not None: raise ValueError( f"server type {server_type.name} is deprecated in location {location.name}" ) ```
1 parent 628aa6d commit a77b0cc

File tree

5 files changed

+159
-29
lines changed

5 files changed

+159
-29
lines changed

hcloud/server_types/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
ServerTypesClient,
66
ServerTypesPageResult,
77
)
8-
from .domain import ServerType
8+
from .domain import ServerType, ServerTypeLocation
99

1010
__all__ = [
1111
"BoundServerType",
1212
"ServerType",
13+
"ServerTypeLocation",
1314
"ServerTypesClient",
1415
"ServerTypesPageResult",
1516
]

hcloud/server_types/client.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,37 @@
33
from typing import Any, NamedTuple
44

55
from ..core import BoundModelBase, Meta, ResourceClientBase
6-
from .domain import ServerType
6+
from ..locations import BoundLocation
7+
from .domain import ServerType, ServerTypeLocation
78

89

910
class BoundServerType(BoundModelBase, ServerType):
1011
_client: ServerTypesClient
1112

1213
model = ServerType
1314

15+
def __init__(
16+
self,
17+
client: ServerTypesClient,
18+
data: dict,
19+
complete: bool = True,
20+
):
21+
raw = data.get("locations")
22+
if raw is not None:
23+
data["locations"] = [
24+
ServerTypeLocation.from_dict(
25+
{
26+
"location": BoundLocation(
27+
client._parent.locations, o, complete=False
28+
),
29+
**o,
30+
}
31+
)
32+
for o in raw
33+
]
34+
35+
super().__init__(client, data, complete)
36+
1437

1538
class ServerTypesPageResult(NamedTuple):
1639
server_types: list[BoundServerType]

hcloud/server_types/domain.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from ..core import BaseDomain, DomainIdentityMixin
66
from ..deprecation import DeprecationInfo
7+
from ..locations import BoundLocation
78

89

910
class ServerType(BaseDomain, DomainIdentityMixin):
@@ -38,6 +39,7 @@ class ServerType(BaseDomain, DomainIdentityMixin):
3839
deprecated. If it has a value, it is considered deprecated.
3940
:param included_traffic: int
4041
Free traffic per month in bytes
42+
:param locations: Supported Location of the Server Type.
4143
"""
4244

4345
__properties__ = (
@@ -52,18 +54,22 @@ class ServerType(BaseDomain, DomainIdentityMixin):
5254
"storage_type",
5355
"cpu_type",
5456
"architecture",
55-
"deprecated",
56-
"deprecation",
57+
"locations",
5758
)
5859
__api_properties__ = (
5960
*__properties__,
61+
"deprecated",
62+
"deprecation",
6063
"included_traffic",
6164
)
6265
__slots__ = (
6366
*__properties__,
67+
"_deprecated",
68+
"_deprecation",
6469
"_included_traffic",
6570
)
6671

72+
# pylint: disable=too-many-locals
6773
def __init__(
6874
self,
6975
id: int | None = None,
@@ -80,6 +86,7 @@ def __init__(
8086
deprecated: bool | None = None,
8187
deprecation: dict | None = None,
8288
included_traffic: int | None = None,
89+
locations: list[ServerTypeLocation] | None = None,
8390
):
8491
self.id = id
8592
self.name = name
@@ -92,12 +99,58 @@ def __init__(
9299
self.storage_type = storage_type
93100
self.cpu_type = cpu_type
94101
self.architecture = architecture
102+
self.locations = locations
103+
95104
self.deprecated = deprecated
96105
self.deprecation = (
97106
DeprecationInfo.from_dict(deprecation) if deprecation is not None else None
98107
)
99108
self.included_traffic = included_traffic
100109

110+
@property
111+
def deprecated(self) -> bool | None:
112+
"""
113+
.. deprecated:: 2.6.0
114+
The 'deprecated' property is deprecated and will gradually be phased starting 24 September 2025.
115+
Please refer to the '.locations[].deprecation' property instead.
116+
117+
See https://docs.hetzner.cloud/changelog#2025-09-24-per-location-server-types.
118+
"""
119+
warnings.warn(
120+
"The 'deprecated' property is deprecated and will gradually be phased starting 24 September 2025. "
121+
"Please refer to the '.locations[].deprecation' property instead. "
122+
"See https://docs.hetzner.cloud/changelog#2025-09-24-per-location-server-types",
123+
DeprecationWarning,
124+
stacklevel=2,
125+
)
126+
return self._deprecated
127+
128+
@deprecated.setter
129+
def deprecated(self, value: bool | None) -> None:
130+
self._deprecated = value
131+
132+
@property
133+
def deprecation(self) -> DeprecationInfo | None:
134+
"""
135+
.. deprecated:: 2.6.0
136+
The 'deprecation' property is deprecated and will gradually be phased starting 24 September 2025.
137+
Please refer to the '.locations[].deprecation' property instead.
138+
139+
See https://docs.hetzner.cloud/changelog#2025-09-24-per-location-server-types.
140+
"""
141+
warnings.warn(
142+
"The 'deprecation' property is deprecated and will gradually be phased starting 24 September 2025. "
143+
"Please refer to the '.locations[].deprecation' property instead. "
144+
"See https://docs.hetzner.cloud/changelog#2025-09-24-per-location-server-types",
145+
DeprecationWarning,
146+
stacklevel=2,
147+
)
148+
return self._deprecation
149+
150+
@deprecation.setter
151+
def deprecation(self, value: DeprecationInfo | None) -> None:
152+
self._deprecation = value
153+
101154
@property
102155
def included_traffic(self) -> int | None:
103156
"""
@@ -119,3 +172,28 @@ def included_traffic(self) -> int | None:
119172
@included_traffic.setter
120173
def included_traffic(self, value: int | None) -> None:
121174
self._included_traffic = value
175+
176+
177+
class ServerTypeLocation(BaseDomain):
178+
"""Server Type Location Domain
179+
180+
:param location: Location of the Server Type.
181+
:param deprecation: Wether the Server Type is deprecated in this Location.
182+
"""
183+
184+
__api_properties__ = (
185+
"location",
186+
"deprecation",
187+
)
188+
__slots__ = __api_properties__
189+
190+
def __init__(
191+
self,
192+
*,
193+
location: BoundLocation,
194+
deprecation: dict | None,
195+
):
196+
self.location = location
197+
self.deprecation = (
198+
DeprecationInfo.from_dict(deprecation) if deprecation is not None else None
199+
)

tests/unit/server_types/conftest.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,24 @@ def server_type_response():
3333
"included_traffic": 21990232555520,
3434
"deprecated": True,
3535
"deprecation": {
36-
"announced": "2023-06-01T00:00:00+00:00",
37-
"unavailable_after": "2023-09-01T00:00:00+00:00",
36+
"announced": "2023-06-01T00:00:00Z",
37+
"unavailable_after": "2023-09-01T00:00:00Z",
3838
},
39+
"locations": [
40+
{
41+
"id": 1,
42+
"name": "nbg1",
43+
"deprecation": None,
44+
},
45+
{
46+
"id": 2,
47+
"name": "fsn1",
48+
"deprecation": {
49+
"announced": "2023-06-01T00:00:00Z",
50+
"unavailable_after": "2023-09-01T00:00:00Z",
51+
},
52+
},
53+
],
3954
}
4055
}
4156

@@ -70,8 +85,8 @@ def two_server_types_response():
7085
"included_traffic": 21990232555520,
7186
"deprecated": True,
7287
"deprecation": {
73-
"announced": "2023-06-01T00:00:00+00:00",
74-
"unavailable_after": "2023-09-01T00:00:00+00:00",
88+
"announced": "2023-06-01T00:00:00Z",
89+
"unavailable_after": "2023-09-01T00:00:00Z",
7590
},
7691
},
7792
{
@@ -146,8 +161,8 @@ def one_server_types_response():
146161
"included_traffic": 21990232555520,
147162
"deprecated": True,
148163
"deprecation": {
149-
"announced": "2023-06-01T00:00:00+00:00",
150-
"unavailable_after": "2023-09-01T00:00:00+00:00",
164+
"announced": "2023-06-01T00:00:00Z",
165+
"unavailable_after": "2023-09-01T00:00:00Z",
151166
},
152167
}
153168
]

tests/unit/server_types/test_client.py

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,44 @@ class TestBoundServerType:
1414
def bound_server_type(self, client: Client):
1515
return BoundServerType(client.server_types, data=dict(id=14))
1616

17-
def test_bound_server_type_init(self, server_type_response):
18-
bound_server_type = BoundServerType(
17+
def test_init(self, server_type_response):
18+
o = BoundServerType(
1919
client=mock.MagicMock(), data=server_type_response["server_type"]
2020
)
2121

22-
assert bound_server_type.id == 1
23-
assert bound_server_type.name == "cx11"
24-
assert bound_server_type.description == "CX11"
25-
assert bound_server_type.category == "Shared vCPU"
26-
assert bound_server_type.cores == 1
27-
assert bound_server_type.memory == 1
28-
assert bound_server_type.disk == 25
29-
assert bound_server_type.storage_type == "local"
30-
assert bound_server_type.cpu_type == "shared"
31-
assert bound_server_type.architecture == "x86"
32-
assert bound_server_type.deprecated is True
33-
assert bound_server_type.deprecation is not None
34-
assert bound_server_type.deprecation.announced == datetime(
35-
2023, 6, 1, tzinfo=timezone.utc
22+
assert o.id == 1
23+
assert o.name == "cx11"
24+
assert o.description == "CX11"
25+
assert o.category == "Shared vCPU"
26+
assert o.cores == 1
27+
assert o.memory == 1
28+
assert o.disk == 25
29+
assert o.storage_type == "local"
30+
assert o.cpu_type == "shared"
31+
assert o.architecture == "x86"
32+
assert len(o.locations) == 2
33+
assert o.locations[0].location.id == 1
34+
assert o.locations[0].location.name == "nbg1"
35+
assert o.locations[0].deprecation is None
36+
assert o.locations[1].location.id == 2
37+
assert o.locations[1].location.name == "fsn1"
38+
assert (
39+
o.locations[1].deprecation.announced.isoformat()
40+
== "2023-06-01T00:00:00+00:00"
3641
)
37-
assert bound_server_type.deprecation.unavailable_after == datetime(
38-
2023, 9, 1, tzinfo=timezone.utc
42+
assert (
43+
o.locations[1].deprecation.unavailable_after.isoformat()
44+
== "2023-09-01T00:00:00+00:00"
3945
)
46+
4047
with pytest.deprecated_call():
41-
assert bound_server_type.included_traffic == 21990232555520
48+
assert o.deprecated is True
49+
assert o.deprecation is not None
50+
assert o.deprecation.announced == datetime(2023, 6, 1, tzinfo=timezone.utc)
51+
assert o.deprecation.unavailable_after == datetime(
52+
2023, 9, 1, tzinfo=timezone.utc
53+
)
54+
assert o.included_traffic == 21990232555520
4255

4356

4457
class TestServerTypesClient:

0 commit comments

Comments
 (0)