From e580a4bbab1840205d84cb97a14721013a3598be Mon Sep 17 00:00:00 2001 From: jo Date: Mon, 8 Sep 2025 09:23:06 +0200 Subject: [PATCH] feat: per location server types [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}" ) ``` --- hcloud/server_types/__init__.py | 3 +- hcloud/server_types/client.py | 25 +++++++- hcloud/server_types/domain.py | 82 +++++++++++++++++++++++++- tests/unit/server_types/conftest.py | 27 +++++++-- tests/unit/server_types/test_client.py | 51 ++++++++++------ 5 files changed, 159 insertions(+), 29 deletions(-) diff --git a/hcloud/server_types/__init__.py b/hcloud/server_types/__init__.py index 2cdab177..6840e327 100644 --- a/hcloud/server_types/__init__.py +++ b/hcloud/server_types/__init__.py @@ -5,11 +5,12 @@ ServerTypesClient, ServerTypesPageResult, ) -from .domain import ServerType +from .domain import ServerType, ServerTypeLocation __all__ = [ "BoundServerType", "ServerType", + "ServerTypeLocation", "ServerTypesClient", "ServerTypesPageResult", ] diff --git a/hcloud/server_types/client.py b/hcloud/server_types/client.py index 085b66f8..bfa525c2 100644 --- a/hcloud/server_types/client.py +++ b/hcloud/server_types/client.py @@ -3,7 +3,8 @@ from typing import Any, NamedTuple from ..core import BoundModelBase, Meta, ResourceClientBase -from .domain import ServerType +from ..locations import BoundLocation +from .domain import ServerType, ServerTypeLocation class BoundServerType(BoundModelBase, ServerType): @@ -11,6 +12,28 @@ class BoundServerType(BoundModelBase, ServerType): model = ServerType + def __init__( + self, + client: ServerTypesClient, + data: dict, + complete: bool = True, + ): + raw = data.get("locations") + if raw is not None: + data["locations"] = [ + ServerTypeLocation.from_dict( + { + "location": BoundLocation( + client._parent.locations, o, complete=False + ), + **o, + } + ) + for o in raw + ] + + super().__init__(client, data, complete) + class ServerTypesPageResult(NamedTuple): server_types: list[BoundServerType] diff --git a/hcloud/server_types/domain.py b/hcloud/server_types/domain.py index ad5ca4b2..ff9e3fd4 100644 --- a/hcloud/server_types/domain.py +++ b/hcloud/server_types/domain.py @@ -4,6 +4,7 @@ from ..core import BaseDomain, DomainIdentityMixin from ..deprecation import DeprecationInfo +from ..locations import BoundLocation class ServerType(BaseDomain, DomainIdentityMixin): @@ -38,6 +39,7 @@ class ServerType(BaseDomain, DomainIdentityMixin): deprecated. If it has a value, it is considered deprecated. :param included_traffic: int Free traffic per month in bytes + :param locations: Supported Location of the Server Type. """ __properties__ = ( @@ -52,18 +54,22 @@ class ServerType(BaseDomain, DomainIdentityMixin): "storage_type", "cpu_type", "architecture", - "deprecated", - "deprecation", + "locations", ) __api_properties__ = ( *__properties__, + "deprecated", + "deprecation", "included_traffic", ) __slots__ = ( *__properties__, + "_deprecated", + "_deprecation", "_included_traffic", ) + # pylint: disable=too-many-locals def __init__( self, id: int | None = None, @@ -80,6 +86,7 @@ def __init__( deprecated: bool | None = None, deprecation: dict | None = None, included_traffic: int | None = None, + locations: list[ServerTypeLocation] | None = None, ): self.id = id self.name = name @@ -92,12 +99,58 @@ def __init__( self.storage_type = storage_type self.cpu_type = cpu_type self.architecture = architecture + self.locations = locations + self.deprecated = deprecated self.deprecation = ( DeprecationInfo.from_dict(deprecation) if deprecation is not None else None ) self.included_traffic = included_traffic + @property + def deprecated(self) -> bool | None: + """ + .. deprecated:: 2.6.0 + The 'deprecated' property is deprecated and will gradually be phased starting 24 September 2025. + Please refer to the '.locations[].deprecation' property instead. + + See https://docs.hetzner.cloud/changelog#2025-09-24-per-location-server-types. + """ + warnings.warn( + "The 'deprecated' property is deprecated and will gradually be phased starting 24 September 2025. " + "Please refer to the '.locations[].deprecation' property instead. " + "See https://docs.hetzner.cloud/changelog#2025-09-24-per-location-server-types", + DeprecationWarning, + stacklevel=2, + ) + return self._deprecated + + @deprecated.setter + def deprecated(self, value: bool | None) -> None: + self._deprecated = value + + @property + def deprecation(self) -> DeprecationInfo | None: + """ + .. deprecated:: 2.6.0 + The 'deprecation' property is deprecated and will gradually be phased starting 24 September 2025. + Please refer to the '.locations[].deprecation' property instead. + + See https://docs.hetzner.cloud/changelog#2025-09-24-per-location-server-types. + """ + warnings.warn( + "The 'deprecation' property is deprecated and will gradually be phased starting 24 September 2025. " + "Please refer to the '.locations[].deprecation' property instead. " + "See https://docs.hetzner.cloud/changelog#2025-09-24-per-location-server-types", + DeprecationWarning, + stacklevel=2, + ) + return self._deprecation + + @deprecation.setter + def deprecation(self, value: DeprecationInfo | None) -> None: + self._deprecation = value + @property def included_traffic(self) -> int | None: """ @@ -119,3 +172,28 @@ def included_traffic(self) -> int | None: @included_traffic.setter def included_traffic(self, value: int | None) -> None: self._included_traffic = value + + +class ServerTypeLocation(BaseDomain): + """Server Type Location Domain + + :param location: Location of the Server Type. + :param deprecation: Wether the Server Type is deprecated in this Location. + """ + + __api_properties__ = ( + "location", + "deprecation", + ) + __slots__ = __api_properties__ + + def __init__( + self, + *, + location: BoundLocation, + deprecation: dict | None, + ): + self.location = location + self.deprecation = ( + DeprecationInfo.from_dict(deprecation) if deprecation is not None else None + ) diff --git a/tests/unit/server_types/conftest.py b/tests/unit/server_types/conftest.py index 8bb794a1..5ff2b5e5 100644 --- a/tests/unit/server_types/conftest.py +++ b/tests/unit/server_types/conftest.py @@ -33,9 +33,24 @@ def server_type_response(): "included_traffic": 21990232555520, "deprecated": True, "deprecation": { - "announced": "2023-06-01T00:00:00+00:00", - "unavailable_after": "2023-09-01T00:00:00+00:00", + "announced": "2023-06-01T00:00:00Z", + "unavailable_after": "2023-09-01T00:00:00Z", }, + "locations": [ + { + "id": 1, + "name": "nbg1", + "deprecation": None, + }, + { + "id": 2, + "name": "fsn1", + "deprecation": { + "announced": "2023-06-01T00:00:00Z", + "unavailable_after": "2023-09-01T00:00:00Z", + }, + }, + ], } } @@ -70,8 +85,8 @@ def two_server_types_response(): "included_traffic": 21990232555520, "deprecated": True, "deprecation": { - "announced": "2023-06-01T00:00:00+00:00", - "unavailable_after": "2023-09-01T00:00:00+00:00", + "announced": "2023-06-01T00:00:00Z", + "unavailable_after": "2023-09-01T00:00:00Z", }, }, { @@ -146,8 +161,8 @@ def one_server_types_response(): "included_traffic": 21990232555520, "deprecated": True, "deprecation": { - "announced": "2023-06-01T00:00:00+00:00", - "unavailable_after": "2023-09-01T00:00:00+00:00", + "announced": "2023-06-01T00:00:00Z", + "unavailable_after": "2023-09-01T00:00:00Z", }, } ] diff --git a/tests/unit/server_types/test_client.py b/tests/unit/server_types/test_client.py index 5802c41b..cdba00f1 100644 --- a/tests/unit/server_types/test_client.py +++ b/tests/unit/server_types/test_client.py @@ -14,31 +14,44 @@ class TestBoundServerType: def bound_server_type(self, client: Client): return BoundServerType(client.server_types, data=dict(id=14)) - def test_bound_server_type_init(self, server_type_response): - bound_server_type = BoundServerType( + def test_init(self, server_type_response): + o = BoundServerType( client=mock.MagicMock(), data=server_type_response["server_type"] ) - assert bound_server_type.id == 1 - assert bound_server_type.name == "cx11" - assert bound_server_type.description == "CX11" - assert bound_server_type.category == "Shared vCPU" - assert bound_server_type.cores == 1 - assert bound_server_type.memory == 1 - assert bound_server_type.disk == 25 - assert bound_server_type.storage_type == "local" - assert bound_server_type.cpu_type == "shared" - assert bound_server_type.architecture == "x86" - assert bound_server_type.deprecated is True - assert bound_server_type.deprecation is not None - assert bound_server_type.deprecation.announced == datetime( - 2023, 6, 1, tzinfo=timezone.utc + assert o.id == 1 + assert o.name == "cx11" + assert o.description == "CX11" + assert o.category == "Shared vCPU" + assert o.cores == 1 + assert o.memory == 1 + assert o.disk == 25 + assert o.storage_type == "local" + assert o.cpu_type == "shared" + assert o.architecture == "x86" + assert len(o.locations) == 2 + assert o.locations[0].location.id == 1 + assert o.locations[0].location.name == "nbg1" + assert o.locations[0].deprecation is None + assert o.locations[1].location.id == 2 + assert o.locations[1].location.name == "fsn1" + assert ( + o.locations[1].deprecation.announced.isoformat() + == "2023-06-01T00:00:00+00:00" ) - assert bound_server_type.deprecation.unavailable_after == datetime( - 2023, 9, 1, tzinfo=timezone.utc + assert ( + o.locations[1].deprecation.unavailable_after.isoformat() + == "2023-09-01T00:00:00+00:00" ) + with pytest.deprecated_call(): - assert bound_server_type.included_traffic == 21990232555520 + assert o.deprecated is True + assert o.deprecation is not None + assert o.deprecation.announced == datetime(2023, 6, 1, tzinfo=timezone.utc) + assert o.deprecation.unavailable_after == datetime( + 2023, 9, 1, tzinfo=timezone.utc + ) + assert o.included_traffic == 21990232555520 class TestServerTypesClient: