From cde61e5808580b3ad83bfa319ca1e9d8dc8b74f7 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 6 Mar 2026 01:47:16 +1000 Subject: [PATCH 01/42] feat: parse nested load balancer `label_selector` targets (#633) When a load balancer has a label_selector type target, the API returns a nested "targets" array containing the resolved individual server targets with their health statuses. This data was previously discarded. - Add `targets` parameter to `LoadBalancerTarget.__init__` in domain.py - Parse nested targets in `BoundLoadBalancer.__init__` for label_selector targets, creating `LoadBalancerTarget` objects with server, health_status, type, and use_private_ip fields - Add test fixture and test case for nested target parsing --------- Co-authored-by: Claude Co-authored-by: jo --- hcloud/load_balancers/client.py | 71 ++++++++++++++---------- hcloud/load_balancers/domain.py | 5 ++ tests/unit/load_balancers/conftest.py | 18 +++++- tests/unit/load_balancers/test_client.py | 26 ++++++++- 4 files changed, 90 insertions(+), 30 deletions(-) diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index 1986905b..166e86dc 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -93,36 +93,51 @@ def __init__( ] data["private_net"] = private_nets - targets = data.get("targets") - if targets: - tmp_targets = [] - for target in targets: - tmp_target = LoadBalancerTarget(type=target["type"]) - if target["type"] == "server": - tmp_target.server = BoundServer( - client._parent.servers, data=target["server"], complete=False - ) - tmp_target.use_private_ip = target["use_private_ip"] - elif target["type"] == "label_selector": - tmp_target.label_selector = LoadBalancerTargetLabelSelector( - selector=target["label_selector"]["selector"] + def _load_balancer_targets( + raw_targets: list[dict[str, Any]], + ) -> list[LoadBalancerTarget]: + return [_load_balancer_target(raw_target) for raw_target in raw_targets] + + def _load_balancer_target( + raw_target: dict[str, Any], + ) -> LoadBalancerTarget: + result = LoadBalancerTarget(type=raw_target["type"]) + + if raw_target["type"] == "ip": + result.ip = LoadBalancerTargetIP( + ip=raw_target["ip"]["ip"], + ) + + elif raw_target["type"] == "server": + result.server = BoundServer( + client._parent.servers, # pylint: disable=protected-access + data=raw_target["server"], + complete=False, + ) + result.use_private_ip = raw_target["use_private_ip"] + + elif raw_target["type"] == "label_selector": + result.label_selector = LoadBalancerTargetLabelSelector( + selector=raw_target["label_selector"]["selector"] + ) + result.use_private_ip = raw_target["use_private_ip"] + + if (raw_nested_targets := raw_target.get("targets")) is not None: + result.targets = _load_balancer_targets(raw_nested_targets) + + if (raw_health_status := raw_target.get("health_status")) is not None: + result.health_status = [ + LoadBalancerTargetHealthStatus( + listen_port=item["listen_port"], + status=item["status"], ) - tmp_target.use_private_ip = target["use_private_ip"] - elif target["type"] == "ip": - tmp_target.ip = LoadBalancerTargetIP(ip=target["ip"]["ip"]) - - target_health_status = target.get("health_status") - if target_health_status is not None: - tmp_target.health_status = [ - LoadBalancerTargetHealthStatus( - listen_port=target_health_status_item["listen_port"], - status=target_health_status_item["status"], - ) - for target_health_status_item in target_health_status - ] + for item in raw_health_status + ] + + return result - tmp_targets.append(tmp_target) - data["targets"] = tmp_targets + if (raw_targets := data.get("targets")) is not None: + data["targets"] = _load_balancer_targets(raw_targets) services = data.get("services") if services: diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index c02e1100..8bad4ded 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -411,6 +411,8 @@ class LoadBalancerTarget(BaseDomain): use the private IP instead of primary public IP :param health_status: list List of health statuses of the services on this target. Only present for target types "server" and "ip". + :param targets: list + List of resolved label selector targets. Only present for target types "label_selector". """ __api_properties__ = ( @@ -420,6 +422,7 @@ class LoadBalancerTarget(BaseDomain): "ip", "use_private_ip", "health_status", + "targets", ) __slots__ = __api_properties__ @@ -431,6 +434,7 @@ def __init__( ip: LoadBalancerTargetIP | None = None, use_private_ip: bool | None = None, health_status: list[LoadBalancerTargetHealthStatus] | None = None, + targets: list[LoadBalancerTarget] | None = None, ): self.type = type self.server = server @@ -438,6 +442,7 @@ def __init__( self.ip = ip self.use_private_ip = use_private_ip self.health_status = health_status + self.targets = targets def to_payload(self) -> dict[str, Any]: """ diff --git a/tests/unit/load_balancers/conftest.py b/tests/unit/load_balancers/conftest.py index f19508ea..8f7f6e6d 100644 --- a/tests/unit/load_balancers/conftest.py +++ b/tests/unit/load_balancers/conftest.py @@ -86,7 +86,23 @@ def response_load_balancer(): "health_status": [{"listen_port": 443, "status": "healthy"}], "label_selector": None, "use_private_ip": False, - } + }, + { + "type": "label_selector", + "label_selector": {"selector": "env=prod"}, + "use_private_ip": True, + "targets": [ + { + "type": "server", + "server": {"id": 105054278}, + "use_private_ip": True, + "health_status": [ + {"listen_port": 443, "status": "healthy"}, + {"listen_port": 3000, "status": "healthy"}, + ], + } + ], + }, ], "algorithm": {"type": "round_robin"}, } diff --git a/tests/unit/load_balancers/test_client.py b/tests/unit/load_balancers/test_client.py index 7ac3290c..5945b977 100644 --- a/tests/unit/load_balancers/test_client.py +++ b/tests/unit/load_balancers/test_client.py @@ -19,7 +19,7 @@ ) from hcloud.locations import Location from hcloud.networks import Network -from hcloud.servers import Server +from hcloud.servers import BoundServer, Server from ..conftest import BoundModelTestCase @@ -60,6 +60,30 @@ def test_init(self, response_load_balancer): assert bound_load_balancer.id == 4711 assert bound_load_balancer.name == "Web Frontend" + def test_init_label_selector_nested_targets(self, response_load_balancer): + bound_load_balancer = BoundLoadBalancer( + client=mock.MagicMock(), data=response_load_balancer["load_balancer"] + ) + + label_selector_target = bound_load_balancer.targets[1] + assert label_selector_target.type == "label_selector" + assert label_selector_target.label_selector.selector == "env=prod" + assert label_selector_target.use_private_ip is True + assert label_selector_target.targets is not None + assert len(label_selector_target.targets) == 1 + + nested = label_selector_target.targets[0] + assert nested.type == "server" + assert isinstance(nested.server, BoundServer) + assert nested.server.id == 105054278 + assert nested.use_private_ip is True + assert nested.health_status is not None + assert len(nested.health_status) == 2 + assert nested.health_status[0].listen_port == 443 + assert nested.health_status[0].status == "healthy" + assert nested.health_status[1].listen_port == 3000 + assert nested.health_status[1].status == "healthy" + class TestLoadBalancerslient: @pytest.fixture() From 05ebdaa397ad485e032c7426ddd081160c0fa327 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:35:09 +0100 Subject: [PATCH 02/42] chore(main): release v2.17.0 (#634) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Features - parse nested load balancer `label_selector` targets (#633) ---

PR by releaser-pleaser 🤖

If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs. ## Release Notes ### Prefix / Start This will be added to the start of the release notes. ~~~~rp-prefix ~~~~ ### Suffix / End This will be added to the end of the release notes. ~~~~rp-suffix ~~~~
Co-authored-by: Hetzner Cloud Bot <> --- CHANGELOG.md | 6 ++++++ hcloud/_version.py | 2 +- setup.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1b5c8f4..12422643 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [v2.17.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.17.0) + +### Features + +- parse nested load balancer `label_selector` targets (#633) + ## [v2.16.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.16.0) ### Storage Boxes support is now generally available diff --git a/hcloud/_version.py b/hcloud/_version.py index aa056a4a..3d1f634a 100644 --- a/hcloud/_version.py +++ b/hcloud/_version.py @@ -1,3 +1,3 @@ from __future__ import annotations -__version__ = "2.16.0" # x-releaser-pleaser-version +__version__ = "2.17.0" # x-releaser-pleaser-version diff --git a/setup.py b/setup.py index 4ae4e5b9..b77498aa 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="hcloud", - version="2.16.0", # x-releaser-pleaser-version + version="2.17.0", # x-releaser-pleaser-version keywords="hcloud hetzner cloud", description="Official Hetzner Cloud python library", long_description=readme, From 8458a5269c02f353fa5712ca039bd89257d1d57c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:35:04 +0100 Subject: [PATCH 03/42] chore(deps): update pre-commit hook psf/black-pre-commit-mirror to v26.3.0 (#635) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [psf/black-pre-commit-mirror](https://redirect.github.com/psf/black-pre-commit-mirror) | repository | minor | `26.1.0` → `26.3.0` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://redirect.github.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
psf/black-pre-commit-mirror (psf/black-pre-commit-mirror) ### [`v26.3.0`](https://redirect.github.com/psf/black-pre-commit-mirror/compare/26.1.0...26.3.0) [Compare Source](https://redirect.github.com/psf/black-pre-commit-mirror/compare/26.1.0...26.3.0)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d737c4a9..a5789951 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: - id: isort - repo: https://github.com/psf/black-pre-commit-mirror - rev: 26.1.0 + rev: 26.3.0 hooks: - id: black From 740d169c16feca9f1edae775148c7e53bc080a18 Mon Sep 17 00:00:00 2001 From: "Jonas L." Date: Mon, 23 Mar 2026 11:28:28 +0100 Subject: [PATCH 04/42] fix: missing `__api_properties__` on LoadBalancerService (#639) Closes #638 Added a test to make sure all classes inheriting from BaseDomain implements the `__api_properties__` property. --- hcloud/load_balancers/domain.py | 10 ++++++++++ tests/unit/core/test_domain.py | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index 8bad4ded..17ad807e 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -160,6 +160,16 @@ class LoadBalancerService(BaseDomain): Configuration for http/https protocols, required when protocol is http/https """ + __api_properties__ = ( + "protocol", + "listen_port", + "destination_port", + "proxyprotocol", + "health_check", + "http", + ) + __slots__ = __api_properties__ + def __init__( self, protocol: str | None = None, diff --git a/tests/unit/core/test_domain.py b/tests/unit/core/test_domain.py index bfef1129..280dc3cb 100644 --- a/tests/unit/core/test_domain.py +++ b/tests/unit/core/test_domain.py @@ -191,3 +191,9 @@ def test_nested_list__eq__(self): d2.child = [ActionDomain(id=2, name="child2")] assert d1 != d2 + + +def test_base_domain_subclasses(): + for c in BaseDomain.__subclasses__(): + assert len(c.__api_properties__) > 0 + assert len(c.__slots__) > 0 From c265773a1a89e34477d1ed2728816598dccf9f17 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:28:45 +0100 Subject: [PATCH 05/42] chore(deps): update dependency pytest-cov to >=7,<7.2 (#637) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b77498aa..f9f36cef 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ "coverage>=7.13,<7.14", "pylint>=4,<4.1", "pytest>=9,<9.1", - "pytest-cov>=7,<7.1", + "pytest-cov>=7,<7.2", "mypy>=1.19,<1.20", "types-python-dateutil", "types-requests", From 63e975f017c35582ca6f959df1597e893b20950a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:29:00 +0100 Subject: [PATCH 06/42] chore(deps): update pre-commit hook psf/black-pre-commit-mirror to v26.3.1 (#636) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a5789951..4cb38974 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: - id: isort - repo: https://github.com/psf/black-pre-commit-mirror - rev: 26.3.0 + rev: 26.3.1 hooks: - id: black From 335d766f5254dea8b2d91cf404c365a8b189d23c Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:50:01 +0100 Subject: [PATCH 07/42] chore(main): release v2.17.1 (#640) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Bug Fixes - missing `__api_properties__` on LoadBalancerService (#639) ---

PR by releaser-pleaser 🤖

If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs. ## Release Notes ### Prefix / Start This will be added to the start of the release notes. ~~~~rp-prefix ~~~~ ### Suffix / End This will be added to the end of the release notes. ~~~~rp-suffix ~~~~
Co-authored-by: Hetzner Cloud Bot <> --- CHANGELOG.md | 6 ++++++ hcloud/_version.py | 2 +- setup.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12422643..29fed689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [v2.17.1](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.17.1) + +### Bug Fixes + +- missing `__api_properties__` on LoadBalancerService (#639) + ## [v2.17.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.17.0) ### Features diff --git a/hcloud/_version.py b/hcloud/_version.py index 3d1f634a..8f665861 100644 --- a/hcloud/_version.py +++ b/hcloud/_version.py @@ -1,3 +1,3 @@ from __future__ import annotations -__version__ = "2.17.0" # x-releaser-pleaser-version +__version__ = "2.17.1" # x-releaser-pleaser-version diff --git a/setup.py b/setup.py index f9f36cef..ccacf936 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="hcloud", - version="2.17.0", # x-releaser-pleaser-version + version="2.17.1", # x-releaser-pleaser-version keywords="hcloud hetzner cloud", description="Official Hetzner Cloud python library", long_description=readme, From b573867a1877d00d0aad8db8f5391357ee16384a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:59:25 +0100 Subject: [PATCH 08/42] chore(deps): update dependency mypy to >=1.20,<1.21 (#642) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ccacf936..a92828b2 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ "pylint>=4,<4.1", "pytest>=9,<9.1", "pytest-cov>=7,<7.2", - "mypy>=1.19,<1.20", + "mypy>=1.20,<1.21", "types-python-dateutil", "types-requests", ], From 2a187ed70736463c1c017923f0ac026d71ff8bd1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:00:17 +0100 Subject: [PATCH 09/42] chore(deps): update codecov/codecov-action action to v6 (#641) --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 379c1fc6..efb3c435 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,6 +37,6 @@ jobs: if: > !startsWith(github.head_ref, 'renovate/') && !startsWith(github.head_ref, 'releaser-pleaser--') - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} From 1262feb904e42bac5c619343ac51ec620f8108cb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:01:11 +0100 Subject: [PATCH 10/42] chore(deps): update pypa/gh-action-pypi-publish action to v1.14.0 (#643) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 03e44dbb..cc66d19a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,4 +53,4 @@ jobs: path: dist/ - name: Publish packages to PyPI - uses: pypa/gh-action-pypi-publish@v1.13.0 + uses: pypa/gh-action-pypi-publish@v1.14.0 From 8e364412319a8b7513c4d136e28aab057e18f258 Mon Sep 17 00:00:00 2001 From: "Jonas L." Date: Thu, 9 Apr 2026 17:50:21 +0100 Subject: [PATCH 11/42] test: modernize primary-ip tests (#644) Implement the latest testing patterns for the primary ips tests --- tests/unit/primary_ips/conftest.py | 278 +++------------------ tests/unit/primary_ips/test_client.py | 344 ++++++++++++++++---------- 2 files changed, 248 insertions(+), 374 deletions(-) diff --git a/tests/unit/primary_ips/conftest.py b/tests/unit/primary_ips/conftest.py index 4a7b23c6..361de307 100644 --- a/tests/unit/primary_ips/conftest.py +++ b/tests/unit/primary_ips/conftest.py @@ -4,252 +4,56 @@ @pytest.fixture() -def primary_ip_response(): +def primary_ip1(): return { - "primary_ip": { - "assignee_id": 17, - "assignee_type": "server", - "auto_delete": True, - "blocked": False, - "created": "2016-01-30T23:55:00+00:00", - "datacenter": { - "description": "Falkenstein DC Park 8", - "id": 42, - "location": { - "city": "Falkenstein", - "country": "DE", - "description": "Falkenstein DC Park 1", - "id": 1, - "latitude": 50.47612, - "longitude": 12.370071, - "name": "fsn1", - "network_zone": "eu-central", - }, - "name": "fsn1-dc8", - "server_types": { - "available": [1, 2, 3], - "available_for_migration": [1, 2, 3], - "supported": [1, 2, 3], - }, - }, - "dns_ptr": [{"dns_ptr": "server.example.com", "ip": "131.232.99.1"}], - "id": 42, - "ip": "131.232.99.1", - "labels": {}, - "name": "my-resource", - "protection": {"delete": False}, - "type": "ipv4", - } - } - - -@pytest.fixture() -def one_primary_ips_response(): - return { - "meta": { - "pagination": { - "last_page": 4, - "next_page": 4, - "page": 3, - "per_page": 25, - "previous_page": 2, - "total_entries": 100, - } + "id": 42, + "name": "primary-ip1", + "type": "ipv4", + "ip": "131.232.99.1", + "assignee_id": 17, + "assignee_type": "server", + "auto_delete": True, + "blocked": False, + "datacenter": { + "id": 4, + "name": "fsn1-dc14", }, - "primary_ips": [ - { - "assignee_id": 17, - "assignee_type": "server", - "auto_delete": True, - "blocked": False, - "created": "2016-01-30T23:55:00+00:00", - "datacenter": { - "description": "Falkenstein DC Park 8", - "id": 42, - "location": { - "city": "Falkenstein", - "country": "DE", - "description": "Falkenstein DC Park 1", - "id": 1, - "latitude": 50.47612, - "longitude": 12.370071, - "name": "fsn1", - "network_zone": "eu-central", - }, - "name": "fsn1-dc8", - "server_types": { - "available": [1, 2, 3], - "available_for_migration": [1, 2, 3], - "supported": [1, 2, 3], - }, - }, - "dns_ptr": [{"dns_ptr": "server.example.com", "ip": "131.232.99.1"}], - "id": 42, - "ip": "131.232.99.1", - "labels": {}, - "name": "my-resource", - "protection": {"delete": False}, - "type": "ipv4", - } - ], - } - - -@pytest.fixture() -def all_primary_ips_response(): - return { - "meta": { - "pagination": { - "last_page": 1, - "next_page": None, - "page": 1, - "per_page": 25, - "previous_page": None, - "total_entries": 1, - } + "location": { + "id": 1, + "name": "fsn1", }, - "primary_ips": [ - { - "assignee_id": 17, - "assignee_type": "server", - "auto_delete": True, - "blocked": False, - "created": "2016-01-30T23:55:00+00:00", - "datacenter": { - "description": "Falkenstein DC Park 8", - "id": 42, - "location": { - "city": "Falkenstein", - "country": "DE", - "description": "Falkenstein DC Park 1", - "id": 1, - "latitude": 50.47612, - "longitude": 12.370071, - "name": "fsn1", - "network_zone": "eu-central", - }, - "name": "fsn1-dc8", - "server_types": { - "available": [1, 2, 3], - "available_for_migration": [1, 2, 3], - "supported": [1, 2, 3], - }, - }, - "dns_ptr": [{"dns_ptr": "server.example.com", "ip": "131.232.99.1"}], - "id": 42, - "ip": "131.232.99.1", - "labels": {}, - "name": "my-resource", - "protection": {"delete": False}, - "type": "ipv4", - } + "dns_ptr": [ + {"dns_ptr": "server.example.com", "ip": "131.232.99.1"}, ], + "labels": {"key": "value"}, + "protection": {"delete": False}, + "created": "2016-01-30T23:55:00Z", } @pytest.fixture() -def primary_ip_create_response(): +def primary_ip2(): return { - "action": { - "command": "create_primary_ip", - "error": {"code": "action_failed", "message": "Action failed"}, - "finished": None, - "id": 13, - "progress": 0, - "resources": [{"id": 17, "type": "server"}], - "started": "2016-01-30T23:50:00+00:00", - "status": "running", + "id": 52, + "name": "primary-ip2", + "type": "ipv4", + "ip": "131.232.99.2", + "assignee_id": None, + "assignee_type": "server", + "auto_delete": True, + "blocked": False, + "datacenter": { + "id": 4, + "name": "fsn1-dc14", }, - "primary_ip": { - "assignee_id": 17, - "assignee_type": "server", - "auto_delete": True, - "blocked": False, - "created": "2016-01-30T23:50:00+00:00", - "datacenter": { - "description": "Falkenstein DC Park 8", - "id": 42, - "location": { - "city": "Falkenstein", - "country": "DE", - "description": "Falkenstein DC Park 1", - "id": 1, - "latitude": 50.47612, - "longitude": 12.370071, - "name": "fsn1", - "network_zone": "eu-central", - "server_types": { - "available": [1, 2, 3], - "available_for_migration": [1, 2, 3], - "supported": [1, 2, 3], - }, - }, - "name": "fsn1-dc8", - }, - "dns_ptr": [{"dns_ptr": "server.example.com", "ip": "2001:db8::1"}], - "id": 42, - "ip": "131.232.99.1", - "labels": {"labelkey": "value"}, - "name": "my-ip", - "protection": {"delete": False}, - "type": "ipv4", + "location": { + "id": 1, + "name": "fsn1", }, - } - - -@pytest.fixture() -def response_update_primary_ip(): - return { - "primary_ip": { - "assignee_id": 17, - "assignee_type": "server", - "auto_delete": True, - "blocked": False, - "created": "2016-01-30T23:55:00+00:00", - "datacenter": { - "description": "Falkenstein DC Park 8", - "id": 42, - "location": { - "city": "Falkenstein", - "country": "DE", - "description": "Falkenstein DC Park 1", - "id": 1, - "latitude": 50.47612, - "longitude": 12.370071, - "name": "fsn1", - "network_zone": "eu-central", - }, - "name": "fsn1-dc8", - "server_types": { - "available": [1, 2, 3], - "available_for_migration": [1, 2, 3], - "supported": [1, 2, 3], - }, - }, - "dns_ptr": [{"dns_ptr": "server.example.com", "ip": "131.232.99.1"}], - "id": 42, - "ip": "131.232.99.1", - "labels": {}, - "name": "my-resource", - "protection": {"delete": False}, - "type": "ipv4", - } - } - - -@pytest.fixture() -def response_get_actions(): - return { - "actions": [ - { - "id": 13, - "command": "assign_primary_ip", - "status": "success", - "progress": 100, - "started": "2016-01-30T23:55:00+00:00", - "finished": "2016-01-30T23:56:00+00:00", - "resources": [{"id": 42, "type": "server"}], - "error": {"code": "action_failed", "message": "Action failed"}, - } - ] + "dns_ptr": [ + {"dns_ptr": "server.example.com", "ip": "131.232.99.1"}, + ], + "labels": {"key": "value"}, + "protection": {"delete": False}, + "created": "2016-01-30T23:55:00Z", } diff --git a/tests/unit/primary_ips/test_client.py b/tests/unit/primary_ips/test_client.py index a4ffcc3a..04b5fc10 100644 --- a/tests/unit/primary_ips/test_client.py +++ b/tests/unit/primary_ips/test_client.py @@ -6,9 +6,24 @@ from hcloud import Client from hcloud.datacenters import BoundDatacenter, Datacenter +from hcloud.locations import BoundLocation, Location from hcloud.primary_ips import BoundPrimaryIP, PrimaryIP, PrimaryIPsClient -from ..conftest import BoundModelTestCase +from ..conftest import BoundModelTestCase, assert_bound_action1 + + +def assert_bound_primary_ip1(o: BoundPrimaryIP, client: PrimaryIPsClient): + assert isinstance(o, BoundPrimaryIP) + assert o._client is client + assert o.id == 42 + assert o.name == "primary-ip1" + + +def assert_bound_primary_ip2(o: BoundPrimaryIP, client: PrimaryIPsClient): + assert isinstance(o, BoundPrimaryIP) + assert o._client is client + assert o.id == 52 + assert o.name == "primary-ip2" class TestBoundPrimaryIP(BoundModelTestCase): @@ -29,87 +44,88 @@ def resource_client(self, client: Client): def bound_model(self, resource_client: PrimaryIPsClient): return BoundPrimaryIP(resource_client, data=dict(id=14)) - def test_init(self, primary_ip_response): - bound_primary_ip = BoundPrimaryIP( - client=mock.MagicMock(), data=primary_ip_response["primary_ip"] - ) + def test_init(self, primary_ip1): + o = BoundPrimaryIP(client=mock.MagicMock(), data=primary_ip1) + + assert o.id == 42 + assert o.name == "primary-ip1" + assert o.ip == "131.232.99.1" + assert o.type == "ipv4" + assert o.protection == {"delete": False} + assert o.labels == {"key": "value"} + assert o.blocked is False - assert bound_primary_ip.id == 42 - assert bound_primary_ip.name == "my-resource" - assert bound_primary_ip.ip == "131.232.99.1" - assert bound_primary_ip.type == "ipv4" - assert bound_primary_ip.protection == {"delete": False} - assert bound_primary_ip.labels == {} - assert bound_primary_ip.blocked is False + assert o.assignee_id == 17 + assert o.assignee_type == "server" - assert bound_primary_ip.assignee_id == 17 - assert bound_primary_ip.assignee_type == "server" + assert isinstance(o.location, BoundLocation) + assert o.location.id == 1 + assert o.location.name == "fsn1" with pytest.deprecated_call(): - datacenter = bound_primary_ip.datacenter + datacenter = o.datacenter assert isinstance(datacenter, BoundDatacenter) - assert datacenter.id == 42 - assert datacenter.name == "fsn1-dc8" - assert datacenter.description == "Falkenstein DC Park 8" - assert datacenter.location.country == "DE" - assert datacenter.location.city == "Falkenstein" - assert datacenter.location.latitude == 50.47612 - assert datacenter.location.longitude == 12.370071 + assert datacenter.id == 4 + assert datacenter.name == "fsn1-dc14" class TestPrimaryIPsClient: @pytest.fixture() - def primary_ips_client(self, client: Client): + def resource_client(self, client: Client): return PrimaryIPsClient(client) def test_get_by_id( self, request_mock: mock.MagicMock, - primary_ips_client: PrimaryIPsClient, - primary_ip_response, + resource_client: PrimaryIPsClient, + primary_ip1, ): - request_mock.return_value = primary_ip_response + request_mock.return_value = {"primary_ip": primary_ip1} - bound_primary_ip = primary_ips_client.get_by_id(1) + result = resource_client.get_by_id(1) request_mock.assert_called_with( method="GET", url="/primary_ips/1", ) - assert bound_primary_ip._client is primary_ips_client - assert bound_primary_ip.id == 42 + assert_bound_primary_ip1(result, resource_client) def test_get_by_name( self, request_mock: mock.MagicMock, - primary_ips_client: PrimaryIPsClient, - one_primary_ips_response, + resource_client: PrimaryIPsClient, + primary_ip1, ): - request_mock.return_value = one_primary_ips_response + request_mock.return_value = {"primary_ips": [primary_ip1]} - bound_primary_ip = primary_ips_client.get_by_name("my-resource") + result = resource_client.get_by_name("primary-ip1") request_mock.assert_called_with( method="GET", url="/primary_ips", - params={"name": "my-resource"}, + params={"name": "primary-ip1"}, ) - assert bound_primary_ip._client is primary_ips_client - assert bound_primary_ip.id == 42 - assert bound_primary_ip.name == "my-resource" + assert_bound_primary_ip1(result, resource_client) - @pytest.mark.parametrize("params", [{"label_selector": "label1"}]) + @pytest.mark.parametrize( + "params", + [ + {"name": "primary-ip1"}, + {"label_selector": "key=value"}, + ], + ) def test_get_all( self, request_mock: mock.MagicMock, - primary_ips_client: PrimaryIPsClient, - all_primary_ips_response, + resource_client: PrimaryIPsClient, + primary_ip1, + primary_ip2, params, ): - request_mock.return_value = all_primary_ips_response + request_mock.return_value = {"primary_ips": [primary_ip1, primary_ip2]} - bound_primary_ips = primary_ips_client.get_all(**params) + result = resource_client.get_all(**params) params.update({"page": 1, "per_page": 50}) @@ -119,60 +135,88 @@ def test_get_all( params=params, ) - assert len(bound_primary_ips) == 1 + assert len(result) == 2 + assert_bound_primary_ip1(result[0], resource_client) + assert_bound_primary_ip2(result[1], resource_client) - bound_primary_ip1 = bound_primary_ips[0] + def test_create_with_location( + self, + request_mock: mock.MagicMock, + resource_client: PrimaryIPsClient, + primary_ip1, + ): + request_mock.return_value = { + "primary_ip": primary_ip1, + "action": None, + } + + result = resource_client.create( + type="ipv4", + name="primary-ip1", + location=Location(name="fsn1"), + ) - assert bound_primary_ip1._client is primary_ips_client - assert bound_primary_ip1.id == 42 - assert bound_primary_ip1.name == "my-resource" + request_mock.assert_called_with( + method="POST", + url="/primary_ips", + json={ + "name": "primary-ip1", + "type": "ipv4", + "assignee_type": "server", + "location": "fsn1", + "auto_delete": False, + }, + ) + assert_bound_primary_ip1(result.primary_ip, resource_client) + assert result.action is None def test_create_with_datacenter( self, request_mock: mock.MagicMock, - primary_ips_client: PrimaryIPsClient, - primary_ip_response, + resource_client: PrimaryIPsClient, + primary_ip1, ): - request_mock.return_value = primary_ip_response + request_mock.return_value = { + "primary_ip": primary_ip1, + "action": None, + } with pytest.deprecated_call(): - response = primary_ips_client.create( - type="ipv6", - name="my-resource", - datacenter=Datacenter(name="datacenter"), + result = resource_client.create( + type="ipv4", + name="primary-ip1", + datacenter=Datacenter(name="fsn1-dc14"), ) request_mock.assert_called_with( method="POST", url="/primary_ips", json={ - "name": "my-resource", - "type": "ipv6", + "name": "primary-ip1", + "type": "ipv4", "assignee_type": "server", - "datacenter": "datacenter", + "datacenter": "fsn1-dc14", "auto_delete": False, }, ) - - bound_primary_ip = response.primary_ip - action = response.action - - assert bound_primary_ip._client is primary_ips_client - assert bound_primary_ip.id == 42 - assert bound_primary_ip.name == "my-resource" - assert action is None + assert_bound_primary_ip1(result.primary_ip, resource_client) + assert result.action is None def test_create_with_assignee_id( self, request_mock: mock.MagicMock, - primary_ips_client: PrimaryIPsClient, - primary_ip_create_response, + resource_client: PrimaryIPsClient, + primary_ip1, + action1_running, ): - request_mock.return_value = primary_ip_create_response - - response = primary_ips_client.create( - type="ipv6", - name="my-ip", + request_mock.return_value = { + "primary_ip": primary_ip1, + "action": action1_running, + } + + result = resource_client.create( + type="ipv4", + name="primary-ip1", assignee_id=17, assignee_type="server", ) @@ -181,161 +225,187 @@ def test_create_with_assignee_id( method="POST", url="/primary_ips", json={ - "name": "my-ip", - "type": "ipv6", + "name": "primary-ip1", + "type": "ipv4", "assignee_id": 17, "assignee_type": "server", "auto_delete": False, }, ) - bound_primary_ip = response.primary_ip - action = response.action - assert bound_primary_ip._client is primary_ips_client - assert bound_primary_ip.id == 42 - assert bound_primary_ip.name == "my-ip" - assert bound_primary_ip.assignee_id == 17 - assert action.id == 13 + assert_bound_primary_ip1(result.primary_ip, resource_client) + assert_bound_action1(result.action, resource_client._parent.actions) @pytest.mark.parametrize( - "primary_ip", [PrimaryIP(id=1), BoundPrimaryIP(mock.MagicMock(), dict(id=1))] + "primary_ip", + [ + PrimaryIP(id=42), + BoundPrimaryIP(client=mock.MagicMock(), data={"id": 42}), + ], ) def test_update( self, request_mock: mock.MagicMock, - primary_ips_client: PrimaryIPsClient, + resource_client: PrimaryIPsClient, primary_ip, - response_update_primary_ip, + primary_ip1, ): - request_mock.return_value = response_update_primary_ip + request_mock.return_value = {"primary_ip": primary_ip1} - primary_ip = primary_ips_client.update( - primary_ip, auto_delete=True, name="my-resource" + result = resource_client.update( + primary_ip, + name="changed", + auto_delete=True, ) request_mock.assert_called_with( method="PUT", - url="/primary_ips/1", - json={"auto_delete": True, "name": "my-resource"}, + url="/primary_ips/42", + json={ + "name": "changed", + "auto_delete": True, + }, ) - - assert primary_ip.id == 42 - assert primary_ip.auto_delete is True - assert primary_ip.name == "my-resource" + assert_bound_primary_ip1(result, resource_client) @pytest.mark.parametrize( - "primary_ip", [PrimaryIP(id=1), BoundPrimaryIP(mock.MagicMock(), dict(id=1))] + "primary_ip", + [ + PrimaryIP(id=42), + BoundPrimaryIP(client=mock.MagicMock(), data={"id": 42}), + ], ) - def test_change_protection( + def test_delete( self, request_mock: mock.MagicMock, - primary_ips_client: PrimaryIPsClient, + resource_client: PrimaryIPsClient, primary_ip, - action_response, ): - request_mock.return_value = action_response + request_mock.return_value = None - action = primary_ips_client.change_protection(primary_ip, True) + result = resource_client.delete(primary_ip) request_mock.assert_called_with( - method="POST", - url="/primary_ips/1/actions/change_protection", - json={"delete": True}, + method="DELETE", + url="/primary_ips/42", ) - assert action.id == 1 - assert action.progress == 0 + assert result is True @pytest.mark.parametrize( - "primary_ip", [PrimaryIP(id=1), BoundPrimaryIP(mock.MagicMock(), dict(id=1))] + "primary_ip", + [ + PrimaryIP(id=42), + BoundPrimaryIP(client=mock.MagicMock(), data={"id": 42}), + ], ) - def test_delete( + def test_change_protection( self, request_mock: mock.MagicMock, - primary_ips_client: PrimaryIPsClient, + resource_client: PrimaryIPsClient, primary_ip, action_response, ): request_mock.return_value = action_response - delete_success = primary_ips_client.delete(primary_ip) + result = resource_client.change_protection( + primary_ip, + delete=True, + ) request_mock.assert_called_with( - method="DELETE", - url="/primary_ips/1", + method="POST", + url="/primary_ips/42/actions/change_protection", + json={ + "delete": True, + }, ) - - assert delete_success is True + assert_bound_action1(result, resource_client._parent.actions) @pytest.mark.parametrize( - "assignee_id,assignee_type,primary_ip", + "primary_ip", [ - (1, "server", PrimaryIP(id=12)), - (1, "server", BoundPrimaryIP(mock.MagicMock(), dict(id=12))), + PrimaryIP(id=42), + BoundPrimaryIP(client=mock.MagicMock(), data={"id": 42}), ], ) def test_assign( self, request_mock: mock.MagicMock, - primary_ips_client: PrimaryIPsClient, - assignee_id, - assignee_type, + resource_client: PrimaryIPsClient, primary_ip, action_response, ): request_mock.return_value = action_response - action = primary_ips_client.assign(primary_ip, assignee_id, assignee_type) + result = resource_client.assign( + primary_ip, + assignee_id=17, + assignee_type="server", + ) request_mock.assert_called_with( method="POST", - url="/primary_ips/12/actions/assign", - json={"assignee_id": 1, "assignee_type": "server"}, + url="/primary_ips/42/actions/assign", + json={ + "assignee_id": 17, + "assignee_type": "server", + }, ) - assert action.id == 1 - assert action.progress == 0 + assert_bound_action1(result, resource_client._parent.actions) @pytest.mark.parametrize( - "primary_ip", [PrimaryIP(id=12), BoundPrimaryIP(mock.MagicMock(), dict(id=12))] + "primary_ip", + [ + PrimaryIP(id=42), + BoundPrimaryIP(client=mock.MagicMock(), data={"id": 42}), + ], ) def test_unassign( self, request_mock: mock.MagicMock, - primary_ips_client: PrimaryIPsClient, + resource_client: PrimaryIPsClient, primary_ip, action_response, ): request_mock.return_value = action_response - action = primary_ips_client.unassign(primary_ip) + result = resource_client.unassign(primary_ip) request_mock.assert_called_with( method="POST", - url="/primary_ips/12/actions/unassign", + url="/primary_ips/42/actions/unassign", ) - assert action.id == 1 - assert action.progress == 0 + assert_bound_action1(result, resource_client._parent.actions) @pytest.mark.parametrize( - "primary_ip", [PrimaryIP(id=12), BoundPrimaryIP(mock.MagicMock(), dict(id=12))] + "primary_ip", + [ + PrimaryIP(id=42), + BoundPrimaryIP(client=mock.MagicMock(), data={"id": 42}), + ], ) def test_change_dns_ptr( self, request_mock: mock.MagicMock, - primary_ips_client: PrimaryIPsClient, + resource_client: PrimaryIPsClient, primary_ip, action_response, ): request_mock.return_value = action_response - action = primary_ips_client.change_dns_ptr( - primary_ip, "1.2.3.4", "server02.example.com" + result = resource_client.change_dns_ptr( + primary_ip, + ip="1.2.3.4", + dns_ptr="server02.example.com", ) request_mock.assert_called_with( method="POST", - url="/primary_ips/12/actions/change_dns_ptr", - json={"ip": "1.2.3.4", "dns_ptr": "server02.example.com"}, + url="/primary_ips/42/actions/change_dns_ptr", + json={ + "ip": "1.2.3.4", + "dns_ptr": "server02.example.com", + }, ) - assert action.id == 1 - assert action.progress == 0 + assert_bound_action1(result, resource_client._parent.actions) From fb34c3eaa90a9045fdc5c7409ad24057f69c10c3 Mon Sep 17 00:00:00 2001 From: phm07 <22707808+phm07@users.noreply.github.com> Date: Thu, 23 Apr 2026 11:51:06 +0200 Subject: [PATCH 12/42] feat(datacenter, server_type): move available and recommended to server_type (#645) See https://docs.hetzner.cloud/changelog#2026-04-01-datacenter-deprecations --------- Co-authored-by: jo --- hcloud/datacenters/domain.py | 30 ++++++++++-- hcloud/server_types/domain.py | 8 ++++ tests/unit/datacenters/test_client.py | 64 ++++++++++++++------------ tests/unit/server_types/conftest.py | 4 ++ tests/unit/server_types/test_client.py | 4 ++ 5 files changed, 77 insertions(+), 33 deletions(-) diff --git a/hcloud/datacenters/domain.py b/hcloud/datacenters/domain.py index 4867915d..bdc7a27b 100644 --- a/hcloud/datacenters/domain.py +++ b/hcloud/datacenters/domain.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from typing import TYPE_CHECKING from ..core import BaseDomain, DomainIdentityMixin @@ -24,8 +25,9 @@ class Datacenter(BaseDomain, DomainIdentityMixin): :param server_types: :class:`DatacenterServerTypes ` """ - __api_properties__ = ("id", "name", "description", "location", "server_types") - __slots__ = __api_properties__ + __properties__ = ("id", "name", "description", "location") + __api_properties__ = (*__properties__, "server_types") + __slots__ = (*__properties__, "_server_types") def __init__( self, @@ -39,7 +41,29 @@ def __init__( self.name = name self.description = description self.location = location - self.server_types = server_types + self._server_types = server_types + + @property + def server_types(self) -> DatacenterServerTypes | None: + """ + .. deprecated:: 2.18.0 + The 'server_types' property is deprecated and will not be supported after 2026-10-01. + Please use 'server_type.locations[]' instead. + + See https://docs.hetzner.cloud/changelog#2026-04-01-datacenter-deprecations. + """ + warnings.warn( + "The 'server_types' property is deprecated and will not be supported after 2026-10-01. " + "Please use 'server_type.locations[]' instead. " + "See https://docs.hetzner.cloud/changelog#2026-04-01-datacenter-deprecations.", + DeprecationWarning, + stacklevel=2, + ) + return self._server_types + + @server_types.setter + def server_types(self, value: DatacenterServerTypes | None) -> None: + self._server_types = value class DatacenterServerTypes(BaseDomain): diff --git a/hcloud/server_types/domain.py b/hcloud/server_types/domain.py index 279d928d..cafa873a 100644 --- a/hcloud/server_types/domain.py +++ b/hcloud/server_types/domain.py @@ -185,11 +185,15 @@ class ServerTypeLocation(BaseDomain): :param location: Location of the Server Type. :param deprecation: Wether the Server Type is deprecated in this Location. + :param available: Whether the Server Type is currently available in this Location. + :param recommended: Whether the Server Type is currently recommended in this Location. """ __api_properties__ = ( "location", "deprecation", + "available", + "recommended", ) __slots__ = __api_properties__ @@ -198,8 +202,12 @@ def __init__( *, location: BoundLocation, deprecation: dict[str, Any] | None, + available: bool | None, + recommended: bool | None, ): self.location = location self.deprecation = ( DeprecationInfo.from_dict(deprecation) if deprecation is not None else None ) + self.available = available + self.recommended = recommended diff --git a/tests/unit/datacenters/test_client.py b/tests/unit/datacenters/test_client.py index 7f5ca96b..ef1aa840 100644 --- a/tests/unit/datacenters/test_client.py +++ b/tests/unit/datacenters/test_client.py @@ -25,36 +25,40 @@ def test_bound_datacenter_init(self, datacenter_response): assert bound_datacenter.location.name == "fsn1" assert bound_datacenter.location.complete is True - assert isinstance(bound_datacenter.server_types, DatacenterServerTypes) - assert len(bound_datacenter.server_types.supported) == 3 - assert bound_datacenter.server_types.supported[0].id == 1 - assert bound_datacenter.server_types.supported[0].complete is False - assert bound_datacenter.server_types.supported[1].id == 2 - assert bound_datacenter.server_types.supported[1].complete is False - assert bound_datacenter.server_types.supported[2].id == 3 - assert bound_datacenter.server_types.supported[2].complete is False - - assert len(bound_datacenter.server_types.available) == 3 - assert bound_datacenter.server_types.available[0].id == 1 - assert bound_datacenter.server_types.available[0].complete is False - assert bound_datacenter.server_types.available[1].id == 2 - assert bound_datacenter.server_types.available[1].complete is False - assert bound_datacenter.server_types.available[2].id == 3 - assert bound_datacenter.server_types.available[2].complete is False - - assert len(bound_datacenter.server_types.available_for_migration) == 3 - assert bound_datacenter.server_types.available_for_migration[0].id == 1 - assert ( - bound_datacenter.server_types.available_for_migration[0].complete is False - ) - assert bound_datacenter.server_types.available_for_migration[1].id == 2 - assert ( - bound_datacenter.server_types.available_for_migration[1].complete is False - ) - assert bound_datacenter.server_types.available_for_migration[2].id == 3 - assert ( - bound_datacenter.server_types.available_for_migration[2].complete is False - ) + with pytest.deprecated_call(): + assert isinstance(bound_datacenter.server_types, DatacenterServerTypes) + assert len(bound_datacenter.server_types.supported) == 3 + assert bound_datacenter.server_types.supported[0].id == 1 + assert bound_datacenter.server_types.supported[0].complete is False + assert bound_datacenter.server_types.supported[1].id == 2 + assert bound_datacenter.server_types.supported[1].complete is False + assert bound_datacenter.server_types.supported[2].id == 3 + assert bound_datacenter.server_types.supported[2].complete is False + + assert len(bound_datacenter.server_types.available) == 3 + assert bound_datacenter.server_types.available[0].id == 1 + assert bound_datacenter.server_types.available[0].complete is False + assert bound_datacenter.server_types.available[1].id == 2 + assert bound_datacenter.server_types.available[1].complete is False + assert bound_datacenter.server_types.available[2].id == 3 + assert bound_datacenter.server_types.available[2].complete is False + + assert len(bound_datacenter.server_types.available_for_migration) == 3 + assert bound_datacenter.server_types.available_for_migration[0].id == 1 + assert ( + bound_datacenter.server_types.available_for_migration[0].complete + is False + ) + assert bound_datacenter.server_types.available_for_migration[1].id == 2 + assert ( + bound_datacenter.server_types.available_for_migration[1].complete + is False + ) + assert bound_datacenter.server_types.available_for_migration[2].id == 3 + assert ( + bound_datacenter.server_types.available_for_migration[2].complete + is False + ) class TestDatacentersClient: diff --git a/tests/unit/server_types/conftest.py b/tests/unit/server_types/conftest.py index 5ff2b5e5..095ab7f4 100644 --- a/tests/unit/server_types/conftest.py +++ b/tests/unit/server_types/conftest.py @@ -41,6 +41,8 @@ def server_type_response(): "id": 1, "name": "nbg1", "deprecation": None, + "available": True, + "recommended": False, }, { "id": 2, @@ -49,6 +51,8 @@ def server_type_response(): "announced": "2023-06-01T00:00:00Z", "unavailable_after": "2023-09-01T00:00:00Z", }, + "available": True, + "recommended": True, }, ], } diff --git a/tests/unit/server_types/test_client.py b/tests/unit/server_types/test_client.py index cdba00f1..b8d266ff 100644 --- a/tests/unit/server_types/test_client.py +++ b/tests/unit/server_types/test_client.py @@ -33,6 +33,8 @@ def test_init(self, server_type_response): assert o.locations[0].location.id == 1 assert o.locations[0].location.name == "nbg1" assert o.locations[0].deprecation is None + assert o.locations[0].available is True + assert o.locations[0].recommended is False assert o.locations[1].location.id == 2 assert o.locations[1].location.name == "fsn1" assert ( @@ -43,6 +45,8 @@ def test_init(self, server_type_response): o.locations[1].deprecation.unavailable_after.isoformat() == "2023-09-01T00:00:00+00:00" ) + assert o.locations[1].available is True + assert o.locations[1].recommended is True with pytest.deprecated_call(): assert o.deprecated is True From b309d97e9ee198e339f62ab1b543299b43987bfe Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:02:27 +0200 Subject: [PATCH 13/42] chore(main): release v2.18.0 (#646) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Available and recommended Server Types have been moved `Datacenter.server_types` has been deprecated in favor of the new `ServerType.locations[].available` and `ServerType.locations[].recommended` properties. See the [changelog](https://docs.hetzner.cloud/changelog#2026-04-01-datacenter-deprecations) for more details. ### Features - **datacenter, server_type**: move available and recommended to server_type (#645) ---

PR by releaser-pleaser 🤖

If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs. ## Release Notes ### Prefix / Start This will be added to the start of the release notes. ~~~~rp-prefix ### Available and recommended Server Types have been moved `Datacenter.server_types` has been deprecated in favor of the new `ServerType.locations[].available` and `ServerType.locations[].recommended` properties. See the [changelog](https://docs.hetzner.cloud/changelog#2026-04-01-datacenter-deprecations) for more details. ~~~~ ### Suffix / End This will be added to the end of the release notes. ~~~~rp-suffix ~~~~
Co-authored-by: Hetzner Cloud Bot <> --- CHANGELOG.md | 12 ++++++++++++ hcloud/_version.py | 2 +- setup.py | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29fed689..1647669f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [v2.18.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.18.0) + +### Available and recommended Server Types have been moved + +`Datacenter.server_types` has been deprecated in favor of the new `ServerType.locations[].available` and `ServerType.locations[].recommended` properties. + +See the [changelog](https://docs.hetzner.cloud/changelog#2026-04-01-datacenter-deprecations) for more details. + +### Features + +- **datacenter, server_type**: move available and recommended to server_type (#645) + ## [v2.17.1](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.17.1) ### Bug Fixes diff --git a/hcloud/_version.py b/hcloud/_version.py index 8f665861..ecdea74b 100644 --- a/hcloud/_version.py +++ b/hcloud/_version.py @@ -1,3 +1,3 @@ from __future__ import annotations -__version__ = "2.17.1" # x-releaser-pleaser-version +__version__ = "2.18.0" # x-releaser-pleaser-version diff --git a/setup.py b/setup.py index a92828b2..00a5d9e5 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="hcloud", - version="2.17.1", # x-releaser-pleaser-version + version="2.18.0", # x-releaser-pleaser-version keywords="hcloud hetzner cloud", description="Official Hetzner Cloud python library", long_description=readme, From 09b3dcd273b22390def413149e404779789e9a26 Mon Sep 17 00:00:00 2001 From: Lukas Metzner Date: Tue, 28 Apr 2026 13:51:30 +0200 Subject: [PATCH 14/42] feat(primary-ip): `assignee_type` behavior changed when creating a primary ip (#647) In the create Primary IP call, the `assignee_type` argument is now only send when the `assignee_id` argument is set. The `assignee_type` argument will stop defaulting to 'server' in the near future, consider explicitly setting this argument when needed. #### Primary IPs `assignee_type` behavior change As of 1 August 2026, the behavior of the Primary IP `assignee_type` property will change, and will return `unassigned` when the Primary IP is not assigned (when `assignee_id` is `null`). The goal is to eventually assign Primary IPs to other resource types, not only to `server`. See the [changelog](https://docs.hetzner.cloud/changelog#2026-04-27-primary-ips-will-return-unassigned) for more details. In addition, the Primary IP request body `assignee_type` property of the operation [`POST /v1/primary_ips`](https://docs.hetzner.cloud/reference/cloud#tag/primary-ips/create_primary_ip) is now optional. Primary IPs created without `assignee_type` return `server` until 1 August 2026, after this date, its value will be `unassigned`. See the [changelog](https://docs.hetzner.cloud/changelog#2026-04-27-primary-ips-make-assignee_type-optional) for more details. Co-authored-by: jo --- hcloud/primary_ips/client.py | 21 +++++++++++++---- tests/unit/primary_ips/test_client.py | 33 +++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/hcloud/primary_ips/client.py b/hcloud/primary_ips/client.py index 6d6693dc..875032ae 100644 --- a/hcloud/primary_ips/client.py +++ b/hcloud/primary_ips/client.py @@ -311,7 +311,7 @@ def create( name: str, datacenter: Datacenter | BoundDatacenter | None = None, location: Location | BoundLocation | None = None, - assignee_type: str | None = "server", + assignee_type: str | None = None, assignee_id: int | None = None, auto_delete: bool | None = False, labels: dict[str, str] | None = None, @@ -328,13 +328,12 @@ def create( :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`CreatePrimaryIPResponse ` """ - data: dict[str, Any] = { "name": name, "type": type, - "assignee_type": assignee_type, "auto_delete": auto_delete, } + if datacenter is not None: warnings.warn( "The 'datacenter' argument is deprecated and will be removed after 1 July 2026. " @@ -348,10 +347,24 @@ def create( data["location"] = location.id_or_name if assignee_id is not None: data["assignee_id"] = assignee_id + if assignee_type is None: + assignee_type = "server" + warnings.warn( + "The 'assignee_type' argument will no longer default to 'server' " + "and will be required together with the 'assignee_id' argument. " + "Please explicitly set the 'assignee_type' argument.", + DeprecationWarning, + stacklevel=2, + ) + data["assignee_type"] = assignee_type if labels is not None: data["labels"] = labels - response = self._client.request(url=self._base_url, json=data, method="POST") + response = self._client.request( + method="POST", + url=self._base_url, + json=data, + ) action = None if response.get("action") is not None: diff --git a/tests/unit/primary_ips/test_client.py b/tests/unit/primary_ips/test_client.py index 04b5fc10..638acb2b 100644 --- a/tests/unit/primary_ips/test_client.py +++ b/tests/unit/primary_ips/test_client.py @@ -162,7 +162,6 @@ def test_create_with_location( json={ "name": "primary-ip1", "type": "ipv4", - "assignee_type": "server", "location": "fsn1", "auto_delete": False, }, @@ -194,7 +193,6 @@ def test_create_with_datacenter( json={ "name": "primary-ip1", "type": "ipv4", - "assignee_type": "server", "datacenter": "fsn1-dc14", "auto_delete": False, }, @@ -236,6 +234,37 @@ def test_create_with_assignee_id( assert_bound_primary_ip1(result.primary_ip, resource_client) assert_bound_action1(result.action, resource_client._parent.actions) + def test_create_with_assignee_type_deprecation( + self, + request_mock: mock.MagicMock, + resource_client: PrimaryIPsClient, + primary_ip1, + action1_running, + ): + request_mock.return_value = { + "primary_ip": primary_ip1, + "action": action1_running, + } + + with pytest.deprecated_call(): + resource_client.create( + type="ipv4", + name="primary-ip1", + assignee_id=17, + ) + + request_mock.assert_called_with( + method="POST", + url="/primary_ips", + json={ + "name": "primary-ip1", + "type": "ipv4", + "assignee_id": 17, + "assignee_type": "server", + "auto_delete": False, + }, + ) + @pytest.mark.parametrize( "primary_ip", [ From 5dd14c4f05d8fc16235a18cc86267036d25552d0 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:00:21 +0200 Subject: [PATCH 15/42] chore(main): release v2.19.0 (#648) ### Primary IPs `assignee_type` behavior change In the create Primary IP call, the `assignee_type` argument is now only send when the `assignee_id` argument is set. The `assignee_type` argument will stop defaulting to `server` in the near future, consider explicitly setting this argument when needed. As of 1 August 2026, the behavior of the Primary IP `assignee_type` property will change, and will return `unassigned` when the Primary IP is not assigned (when `assignee_id` is `null`). The goal is to eventually assign Primary IPs to other resource types, not only to `server`. See the [changelog](https://docs.hetzner.cloud/changelog#2026-04-27-primary-ips-will-return-unassigned) for more details. In addition, the Primary IP request body `assignee_type` property of the operation [`POST /v1/primary_ips`](https://docs.hetzner.cloud/reference/cloud#tag/primary-ips/create_primary_ip) is now optional. Primary IPs created without `assignee_type` return `server` until 1 August 2026, after this date, its value will be `unassigned`. See the [changelog](https://docs.hetzner.cloud/changelog#2026-04-27-primary-ips-make-assignee_type-optional) for more details. ### Features - **primary-ip**: `assignee_type` behavior changed when creating a primary ip (#647) --- CHANGELOG.md | 18 ++++++++++++++++++ hcloud/_version.py | 2 +- setup.py | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1647669f..050de6be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [v2.19.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.19.0) + +### Primary IPs `assignee_type` behavior change + +In the create Primary IP call, the `assignee_type` argument is now only send when the `assignee_id` argument is set. The `assignee_type` argument will stop defaulting to `server` in the near future, consider explicitly setting this argument when needed. + +As of 1 August 2026, the behavior of the Primary IP `assignee_type` property will change, and will return `unassigned` when the Primary IP is not assigned (when `assignee_id` is `null`). The goal is to eventually assign Primary IPs to other resource types, not only to `server`. + +See the [changelog](https://docs.hetzner.cloud/changelog#2026-04-27-primary-ips-will-return-unassigned) for more details. + +In addition, the Primary IP request body `assignee_type` property of the operation [`POST /v1/primary_ips`](https://docs.hetzner.cloud/reference/cloud#tag/primary-ips/create_primary_ip) is now optional. Primary IPs created without `assignee_type` return `server` until 1 August 2026, after this date, its value will be `unassigned`. + +See the [changelog](https://docs.hetzner.cloud/changelog#2026-04-27-primary-ips-make-assignee_type-optional) for more details. + +### Features + +- **primary-ip**: `assignee_type` behavior changed when creating a primary ip (#647) + ## [v2.18.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.18.0) ### Available and recommended Server Types have been moved diff --git a/hcloud/_version.py b/hcloud/_version.py index ecdea74b..6704eaaa 100644 --- a/hcloud/_version.py +++ b/hcloud/_version.py @@ -1,3 +1,3 @@ from __future__ import annotations -__version__ = "2.18.0" # x-releaser-pleaser-version +__version__ = "2.19.0" # x-releaser-pleaser-version diff --git a/setup.py b/setup.py index 00a5d9e5..e1df1d54 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="hcloud", - version="2.18.0", # x-releaser-pleaser-version + version="2.19.0", # x-releaser-pleaser-version keywords="hcloud hetzner cloud", description="Official Hetzner Cloud python library", long_description=readme, From 1669de263e8ed8176c2f94570176311568003b56 Mon Sep 17 00:00:00 2001 From: Marius Zander Date: Thu, 7 May 2026 15:12:50 +0200 Subject: [PATCH 16/42] feat(load-balancer): support `timeout_idle` http service field (#649) `timeout_idle` controls the time a http connection is allowed to idle before the load balancer drops the connection. See the [changelog](https://docs.hetzner.cloud/changelog#2026-04-30-load-balancers-http-idle-timeout-can-now-be-configured) for more information. --- hcloud/load_balancers/client.py | 1 + hcloud/load_balancers/domain.py | 7 ++++++ tests/unit/load_balancers/conftest.py | 5 ++++ tests/unit/load_balancers/test_client.py | 30 ++++++++++++++++++++++-- 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index 166e86dc..6dd4672e 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -155,6 +155,7 @@ def _load_balancer_target( redirect_http=service["http"]["redirect_http"], cookie_name=service["http"]["cookie_name"], cookie_lifetime=service["http"]["cookie_lifetime"], + timeout_idle=service["http"]["timeout_idle"], ) tmp_service.http.certificates = [ BoundCertificate( diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index 17ad807e..7561af2a 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -212,6 +212,8 @@ def to_payload(self) -> dict[str, Any]: http["redirect_http"] = self.http.redirect_http if self.http.sticky_sessions is not None: http["sticky_sessions"] = self.http.sticky_sessions + if self.http.timeout_idle is not None: + http["timeout_idle"] = self.http.timeout_idle http["certificates"] = [ certificate.id for certificate in self.http.certificates or [] @@ -272,6 +274,8 @@ class LoadBalancerServiceHttp(BaseDomain): Redirect traffic from http port 80 to port 443 :param sticky_sessions: bool Use sticky sessions. Only available if protocol is "http" or "https". + :param timeout_idle: int + Idle timeout in seconds for HTTP connections. Must be between 30 and 300 seconds. """ __api_properties__ = ( @@ -280,6 +284,7 @@ class LoadBalancerServiceHttp(BaseDomain): "certificates", "redirect_http", "sticky_sessions", + "timeout_idle", ) __slots__ = __api_properties__ @@ -290,12 +295,14 @@ def __init__( certificates: list[BoundCertificate] | None = None, redirect_http: bool | None = None, sticky_sessions: bool | None = None, + timeout_idle: int | None = None, ): self.cookie_name = cookie_name self.cookie_lifetime = cookie_lifetime self.certificates = certificates self.redirect_http = redirect_http self.sticky_sessions = sticky_sessions + self.timeout_idle = timeout_idle class LoadBalancerHealthCheck(BaseDomain): diff --git a/tests/unit/load_balancers/conftest.py b/tests/unit/load_balancers/conftest.py index 8f7f6e6d..d7fe2a90 100644 --- a/tests/unit/load_balancers/conftest.py +++ b/tests/unit/load_balancers/conftest.py @@ -62,6 +62,7 @@ def response_load_balancer(): "certificates": [897], "redirect_http": True, "sticky_sessions": True, + "timeout_idle": 60, }, "health_check": { "protocol": "http", @@ -155,6 +156,7 @@ def response_create_load_balancer(): "certificates": [897], "redirect_http": True, "sticky_sessions": True, + "timeout_idle": 60, }, "health_check": { "protocol": "http", @@ -253,6 +255,7 @@ def response_update_load_balancer(): "certificates": [897], "redirect_http": True, "sticky_sessions": True, + "timeout_idle": 60, }, "health_check": { "protocol": "http", @@ -344,6 +347,7 @@ def response_simple_load_balancers(): "cookie_lifetime": 300, "certificates": [897], "redirect_http": True, + "timeout_idle": 60, }, "health_check": { "protocol": "http", @@ -428,6 +432,7 @@ def response_simple_load_balancers(): "cookie_lifetime": 300, "certificates": [897], "redirect_http": True, + "timeout_idle": 60, }, "health_check": { "protocol": "http", diff --git a/tests/unit/load_balancers/test_client.py b/tests/unit/load_balancers/test_client.py index 5945b977..d9558f6d 100644 --- a/tests/unit/load_balancers/test_client.py +++ b/tests/unit/load_balancers/test_client.py @@ -13,6 +13,7 @@ LoadBalancerHealthCheck, LoadBalancersClient, LoadBalancerService, + LoadBalancerServiceHttp, LoadBalancerTarget, LoadBalancerTargetIP, LoadBalancerTargetLabelSelector, @@ -383,13 +384,38 @@ def test_add_service( ): request_mock.return_value = response_add_service - service = LoadBalancerService(listen_port=80, protocol="http") + service = LoadBalancerService( + listen_port=80, + protocol="http", + destination_port=8080, + proxyprotocol=False, + http=LoadBalancerServiceHttp( + cookie_name="HCLBSTICKY", + cookie_lifetime=300, + redirect_http=True, + sticky_sessions=True, + timeout_idle=60, + ), + ) action = resource_client.add_service(load_balancer, service) request_mock.assert_called_with( method="POST", url="/load_balancers/1/actions/add_service", - json={"protocol": "http", "listen_port": 80}, + json={ + "protocol": "http", + "listen_port": 80, + "destination_port": 8080, + "proxyprotocol": False, + "http": { + "cookie_name": "HCLBSTICKY", + "cookie_lifetime": 300, + "redirect_http": True, + "sticky_sessions": True, + "timeout_idle": 60, + "certificates": [], + }, + }, ) assert action.id == 13 From 0bbc9dd849300a7f1392393ce6964ace0682de69 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Thu, 7 May 2026 16:26:45 +0200 Subject: [PATCH 17/42] chore(main): release v2.20.0 (#651) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Load Balancer HTTP Services now support `timeout_idle` HTTP Services now support the field `timeout_idle`, which controls the time a HTTP connection is allowed to idle before it is being dropped. See the [changelog](https://docs.hetzner.cloud/changelog#2026-04-30-load-balancers-http-idle-timeout-can-now-be-configured) for more information. ### Features - **load-balancer**: support `timeout_idle` http service field (#649) ---

PR by releaser-pleaser 🤖

If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs. ## Release Notes ### Prefix / Start This will be added to the start of the release notes. ~~~~rp-prefix ### Load Balancer HTTP Services now support `timeout_idle` HTTP Services now support the field `timeout_idle`, which controls the time a HTTP connection is allowed to idle before it is being dropped. See the [changelog](https://docs.hetzner.cloud/changelog#2026-04-30-load-balancers-http-idle-timeout-can-now-be-configured) for more information. ~~~~ ### Suffix / End This will be added to the end of the release notes. ~~~~rp-suffix ~~~~
Co-authored-by: Hetzner Cloud Bot <> --- CHANGELOG.md | 12 ++++++++++++ hcloud/_version.py | 2 +- setup.py | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 050de6be..4d4b753d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [v2.20.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.20.0) + +### Load Balancer HTTP Services now support `timeout_idle` + +HTTP Services now support the field `timeout_idle`, which controls the time a HTTP connection is allowed to idle before it is being dropped. + +See the [changelog](https://docs.hetzner.cloud/changelog#2026-04-30-load-balancers-http-idle-timeout-can-now-be-configured) for more information. + +### Features + +- **load-balancer**: support `timeout_idle` http service field (#649) + ## [v2.19.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.19.0) ### Primary IPs `assignee_type` behavior change diff --git a/hcloud/_version.py b/hcloud/_version.py index 6704eaaa..f4f40e66 100644 --- a/hcloud/_version.py +++ b/hcloud/_version.py @@ -1,3 +1,3 @@ from __future__ import annotations -__version__ = "2.19.0" # x-releaser-pleaser-version +__version__ = "2.20.0" # x-releaser-pleaser-version diff --git a/setup.py b/setup.py index e1df1d54..618ac950 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="hcloud", - version="2.19.0", # x-releaser-pleaser-version + version="2.20.0", # x-releaser-pleaser-version keywords="hcloud hetzner cloud", description="Official Hetzner Cloud python library", long_description=readme, From 841dff907beffddcb37436876995898c5dd82e38 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 11:31:53 +0200 Subject: [PATCH 18/42] chore(deps): update dependency mypy to v2 (#650) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 618ac950..67140c77 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ "pylint>=4,<4.1", "pytest>=9,<9.1", "pytest-cov>=7,<7.2", - "mypy>=1.20,<1.21", + "mypy>=2.1,<2.2", "types-python-dateutil", "types-requests", ], From 940a30fb16aeede05d38e4eb1549604fdfb370cc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 15:08:54 +0200 Subject: [PATCH 19/42] chore(deps): update dependency coverage to >=7.14,<7.15 (#652) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 67140c77..16983147 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ "watchdog>=6,<6.1", ], "test": [ - "coverage>=7.13,<7.14", + "coverage>=7.14,<7.15", "pylint>=4,<4.1", "pytest>=9,<9.1", "pytest-cov>=7,<7.2", From 7778d20aff78dec27b6b8a4954336869c47f4565 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 14:48:26 +0200 Subject: [PATCH 20/42] chore(deps): update dependency myst-parser to >=5,<5.2 (#653) --- docs/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index df08f92c..7992a6ef 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -40,7 +40,7 @@ add_module_names = False # Myst Parser -myst_enable_extensions = ["colon_fence"] +myst_enable_extensions = ["colon_fence", "alert"] # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output diff --git a/setup.py b/setup.py index 16983147..a36ab35e 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ "docs": [ "sphinx>=9,<9.2", "sphinx-rtd-theme>=3,<3.2", - "myst-parser>=5,<5.1", + "myst-parser>=5,<5.2", "watchdog>=6,<6.1", ], "test": [ From cc39d444b37d3341335a10508b47280dc888b5ad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 13:13:02 +0200 Subject: [PATCH 21/42] chore(deps): update pre-commit hook psf/black-pre-commit-mirror to v26.5.1 (#654) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4cb38974..27bc959c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: - id: isort - repo: https://github.com/psf/black-pre-commit-mirror - rev: 26.3.1 + rev: 26.5.1 hooks: - id: black From d52319c40e5b1b901bcaf9c443da07fdcaad9bbc Mon Sep 17 00:00:00 2001 From: "Jonas L." Date: Mon, 8 Jun 2026 11:18:06 +0200 Subject: [PATCH 22/42] chore: exclude dns migration links from checker (#657) --- .github/workflows/links.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml index cf99d202..b707d7ee 100644 --- a/.github/workflows/links.yml +++ b/.github/workflows/links.yml @@ -35,5 +35,7 @@ jobs: --exclude 'https://docs.hetzner.cloud/reference/hetzner#' --exclude 'codecov.io' --exclude 'github.com' + --exclude 'https://docs.hetzner.com/networking/dns/faq/beta/' + --exclude 'https://docs.hetzner.com/networking/dns/migration-to-hetzner-console/process/' '**/*.md' '**/*.py' From 78301e010542b57df8c9bb6df5c492b120adfa22 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:18:34 +0200 Subject: [PATCH 23/42] chore(deps): pin dependencies (#655) --- .github/workflows/links.yml | 6 +++--- .github/workflows/lint.yml | 8 ++++---- .github/workflows/release.yml | 10 +++++----- .github/workflows/releaser-pleaser.yml | 2 +- .github/workflows/test.yml | 6 +++--- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml index b707d7ee..484e9626 100644 --- a/.github/workflows/links.yml +++ b/.github/workflows/links.yml @@ -11,15 +11,15 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - - uses: actions/cache@v5 + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: .lycheecache key: cache-lychee-${{ github.sha }} restore-keys: cache-lychee- - - uses: lycheeverse/lychee-action@v2 + - uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2 with: fail: true args: > diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d0a893a0..1686f3ec 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,10 +9,10 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Setup python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: 3.x @@ -25,10 +25,10 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Setup python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: 3.x diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc66d19a..d195d05f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,10 +11,10 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: 3.x @@ -29,7 +29,7 @@ jobs: - name: Upload packages artifact if: github.event_name == 'release' - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: python-packages path: dist/ @@ -47,10 +47,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Download packages artifact - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: python-packages path: dist/ - name: Publish packages to PyPI - uses: pypa/gh-action-pypi-publish@v1.14.0 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 diff --git a/.github/workflows/releaser-pleaser.yml b/.github/workflows/releaser-pleaser.yml index ebbf620c..7e552517 100644 --- a/.github/workflows/releaser-pleaser.yml +++ b/.github/workflows/releaser-pleaser.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: releaser-pleaser - uses: apricote/releaser-pleaser@v0.8.0 + uses: apricote/releaser-pleaser@a1ce9493fd3f3abe60f22c37249d257bc10081dc # v0.8.0 with: token: ${{ secrets.HCLOUD_BOT_TOKEN }} extra-files: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index efb3c435..e40fa076 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,10 +20,10 @@ jobs: name: Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Setup python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: ${{ matrix.python-version }} @@ -37,6 +37,6 @@ jobs: if: > !startsWith(github.head_ref, 'renovate/') && !startsWith(github.head_ref, 'releaser-pleaser--') - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6 with: token: ${{ secrets.CODECOV_TOKEN }} From 36f530260feb7f4e724731c65c956cb12a3a02d9 Mon Sep 17 00:00:00 2001 From: "Jonas L." Date: Fri, 12 Jun 2026 14:34:59 +0200 Subject: [PATCH 24/42] feat: retry requests on api bad_gateway error (#658) If the API returns an error code `bad_gateway`, retry the failed request. --- hcloud/_client.py | 2 ++ tests/unit/test_client.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/hcloud/_client.py b/hcloud/_client.py index 20f26c9d..fe37dc98 100644 --- a/hcloud/_client.py +++ b/hcloud/_client.py @@ -129,6 +129,7 @@ class Client: - ``conflict`` - ``rate_limit_exceeded`` + - ``bad_gateway`` - ``timeout`` Changes to the retry policy might occur between releases, and will not be considered @@ -420,6 +421,7 @@ def _retry_policy(self, exception: APIException) -> bool: return exception.code in ( "rate_limit_exceeded", "conflict", + "bad_gateway", "timeout", ) diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 41094a88..460a25da 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -106,6 +106,10 @@ def test_init(self, client: ClientBase): APIException(code="conflict", message="Error", details=None), True, ), + ( + APIException(code="bad_gateway", message="Error", details=None), + True, + ), ( APIException(code=409, message="Conflict", details=None), False, From 32ea380d2c04e8c5b7d07572babb3348f1e4d800 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:37:44 +0200 Subject: [PATCH 25/42] chore(main): release v2.21.0 (#659) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Features - retry requests on api bad_gateway error (#658) ---

PR by releaser-pleaser 🤖

If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs. ## Release Notes ### Prefix / Start This will be added to the start of the release notes. ~~~~rp-prefix ~~~~ ### Suffix / End This will be added to the end of the release notes. ~~~~rp-suffix ~~~~
Co-authored-by: Hetzner Cloud Bot <> --- CHANGELOG.md | 6 ++++++ hcloud/_version.py | 2 +- setup.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d4b753d..9fe8eb32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [v2.21.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.21.0) + +### Features + +- retry requests on api bad_gateway error (#658) + ## [v2.20.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.20.0) ### Load Balancer HTTP Services now support `timeout_idle` diff --git a/hcloud/_version.py b/hcloud/_version.py index f4f40e66..e5527186 100644 --- a/hcloud/_version.py +++ b/hcloud/_version.py @@ -1,3 +1,3 @@ from __future__ import annotations -__version__ = "2.20.0" # x-releaser-pleaser-version +__version__ = "2.21.0" # x-releaser-pleaser-version diff --git a/setup.py b/setup.py index a36ab35e..f2a279bf 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="hcloud", - version="2.20.0", # x-releaser-pleaser-version + version="2.21.0", # x-releaser-pleaser-version keywords="hcloud hetzner cloud", description="Official Hetzner Cloud python library", long_description=readme, From b4a25976bd8ab5a41e20700ea6bca11ee06e401f Mon Sep 17 00:00:00 2001 From: "Jonas L." Date: Thu, 18 Jun 2026 12:00:28 +0200 Subject: [PATCH 26/42] ci: update read the docs build config (#661) Bump the os version and python version used to build our documentation. --- .readthedocs.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index dcda10d7..e34d455d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,9 +7,9 @@ version: 2 # Set the OS, Python version and other tools you might need build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: - python: "3.11" + python: "3.14" # Build documentation in the "docs/" directory with Sphinx sphinx: From 9e1883a54627026d55fcac718a356ada2dc65103 Mon Sep 17 00:00:00 2001 From: "Jonas L." Date: Thu, 18 Jun 2026 15:34:32 +0200 Subject: [PATCH 27/42] feat: deprecate datacenters (#656) The API endpoints `GET /v1/datacenters` and `GET /v1/datacenters/{id}` are now deprecated and will be removed after 1 Oct. 2026. After this date, requests to these endpoints will return `HTTP 410 Gone`. See https://docs.hetzner.cloud/changelog#2026-06-02-datacenters-deprecated. --- hcloud/_client.py | 30 ++++++++++++++++++++---- hcloud/core/client.py | 5 +++- hcloud/datacenters/client.py | 33 ++++++++++++++++++++++++++- hcloud/datacenters/domain.py | 14 ++++++++++++ hcloud/primary_ips/client.py | 4 +++- hcloud/servers/client.py | 4 +++- tests/unit/actions/test_client.py | 14 ++++++++---- tests/unit/datacenters/test_client.py | 7 +++++- tests/unit/datacenters/test_domain.py | 4 ++++ 9 files changed, 102 insertions(+), 13 deletions(-) diff --git a/hcloud/_client.py b/hcloud/_client.py index fe37dc98..4a71bb0b 100644 --- a/hcloud/_client.py +++ b/hcloud/_client.py @@ -1,6 +1,7 @@ from __future__ import annotations import time +import warnings from http import HTTPStatus from random import uniform from typing import Any, Protocol @@ -181,11 +182,10 @@ def __init__( timeout=timeout, ) - self.datacenters = DatacentersClient(self) - """DatacentersClient Instance + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + self.datacenters = DatacentersClient(self) - :type: :class:`DatacentersClient ` - """ self.locations = LocationsClient(self) """LocationsClient Instance @@ -303,6 +303,28 @@ def request( # type: ignore[no-untyped-def] """ return self._client.request(method, url, **kwargs) + @property + def datacenters(self) -> DatacentersClient: + """DatacentersClient Instance + + .. deprecated:: 2.22.0 + The datacenters client is deprecated and will be removed after the 2026-10-01. + See https://docs.hetzner.cloud/changelog#2026-06-02-datacenters-deprecated. + + :type: :class:`DatacentersClient ` + """ + warnings.warn( + "The datacenters client is deprecated and will be removed after the 2026-10-01. " + "See https://docs.hetzner.cloud/changelog#2026-06-02-datacenters-deprecated.", + DeprecationWarning, + stacklevel=2, + ) + return self._datacenters + + @datacenters.setter + def datacenters(self, value: DatacentersClient) -> None: + self._datacenters = value + class ClientBase: def __init__( diff --git a/hcloud/core/client.py b/hcloud/core/client.py index 4ff7f240..45f5039c 100644 --- a/hcloud/core/client.py +++ b/hcloud/core/client.py @@ -109,7 +109,10 @@ def __init__( """ self._client = client self.complete = complete - self.data_model: Domain = self.model.from_dict(data) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + self.data_model: Domain = self.model.from_dict(data) def __getattr__(self, name: str): # type: ignore[no-untyped-def] """Allow magical access to the properties of the model diff --git a/hcloud/datacenters/client.py b/hcloud/datacenters/client.py index 5d7fe964..f8a7f3b0 100644 --- a/hcloud/datacenters/client.py +++ b/hcloud/datacenters/client.py @@ -1,12 +1,16 @@ from __future__ import annotations -from typing import Any, NamedTuple +import warnings +from typing import TYPE_CHECKING, Any, NamedTuple from ..core import BoundModelBase, Meta, ResourceClientBase from ..locations import BoundLocation from ..server_types import BoundServerType from .domain import Datacenter, DatacenterServerTypes +if TYPE_CHECKING: + from .._client import Client + __all__ = [ "BoundDatacenter", "DatacentersPageResult", @@ -15,6 +19,12 @@ class BoundDatacenter(BoundModelBase[Datacenter], Datacenter): + """ + .. deprecated:: 2.22.0 + The bound datacenter class is deprecated and will be removed after the 2026-10-01. + See https://docs.hetzner.cloud/changelog#2026-06-02-datacenters-deprecated. + """ + _client: DatacentersClient model = Datacenter @@ -54,13 +64,34 @@ def __init__(self, client: DatacentersClient, data: dict[str, Any]): class DatacentersPageResult(NamedTuple): + """ + .. deprecated:: 2.22.0 + The datacenters page result class is deprecated and will be removed after the 2026-10-01. + See https://docs.hetzner.cloud/changelog#2026-06-02-datacenters-deprecated. + """ + datacenters: list[BoundDatacenter] meta: Meta class DatacentersClient(ResourceClientBase): + """ + .. deprecated:: 2.22.0 + The datacenters client class is deprecated and will be removed after the 2026-10-01. + See https://docs.hetzner.cloud/changelog#2026-06-02-datacenters-deprecated. + """ + _base_url = "/datacenters" + def __init__(self, client: Client): + warnings.warn( + "The datacenters client class is deprecated and will be removed after the 2026-10-01. " + "See https://docs.hetzner.cloud/changelog#2026-06-02-datacenters-deprecated.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(client) + def get_by_id(self, id: int) -> BoundDatacenter: """Get a specific datacenter by its ID. diff --git a/hcloud/datacenters/domain.py b/hcloud/datacenters/domain.py index bdc7a27b..aef7d700 100644 --- a/hcloud/datacenters/domain.py +++ b/hcloud/datacenters/domain.py @@ -18,6 +18,10 @@ class Datacenter(BaseDomain, DomainIdentityMixin): """Datacenter Domain + .. deprecated:: 2.22.0 + The datacenters domain class is deprecated and will be removed after the 2026-10-01. + See https://docs.hetzner.cloud/changelog#2026-06-02-datacenters-deprecated. + :param id: int ID of Datacenter :param name: str Name of Datacenter :param description: str Description of Datacenter @@ -37,6 +41,12 @@ def __init__( location: Location | None = None, server_types: DatacenterServerTypes | None = None, ): + warnings.warn( + "The datacenter domain class is deprecated and will be removed after the 2026-10-01. " + "See https://docs.hetzner.cloud/changelog#2026-06-02-datacenters-deprecated.", + DeprecationWarning, + stacklevel=2, + ) self.id = id self.name = name self.description = description @@ -69,6 +79,10 @@ def server_types(self, value: DatacenterServerTypes | None) -> None: class DatacenterServerTypes(BaseDomain): """DatacenterServerTypes Domain + .. deprecated:: 2.22.0 + The datacenters domain class is deprecated and will be removed after the 2026-10-01. + See https://docs.hetzner.cloud/changelog#2026-06-02-datacenters-deprecated. + :param available: List[:class:`BoundServerTypes `] All available server types for this datacenter :param supported: List[:class:`BoundServerTypes `] diff --git a/hcloud/primary_ips/client.py b/hcloud/primary_ips/client.py index 875032ae..4a0c490d 100644 --- a/hcloud/primary_ips/client.py +++ b/hcloud/primary_ips/client.py @@ -44,7 +44,9 @@ def __init__( raw = data.get("datacenter", {}) if raw: - data["datacenter"] = BoundDatacenter(client._parent.datacenters, raw) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + data["datacenter"] = BoundDatacenter(client._parent.datacenters, raw) raw = data.get("location", {}) if raw: diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index 1128f420..6e406f2c 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -77,7 +77,9 @@ def __init__( ): raw = data.get("datacenter") if raw: - data["datacenter"] = BoundDatacenter(client._parent.datacenters, raw) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + data["datacenter"] = BoundDatacenter(client._parent.datacenters, raw) raw = data.get("location") if raw: diff --git a/tests/unit/actions/test_client.py b/tests/unit/actions/test_client.py index 2b9d8113..26930de4 100644 --- a/tests/unit/actions/test_client.py +++ b/tests/unit/actions/test_client.py @@ -1,6 +1,7 @@ from __future__ import annotations import inspect +import warnings from unittest import mock import pytest @@ -47,10 +48,15 @@ def test_resources_with_actions(client: Client): """ Ensure that the list of resource clients above is up to date. """ - members = inspect.getmembers( - client, - predicate=lambda p: isinstance(p, ResourceClientBase) and hasattr(p, "actions"), - ) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + + members = inspect.getmembers( + client, + predicate=lambda p: isinstance(p, ResourceClientBase) + and hasattr(p, "actions"), + ) + for name, member in members: assert name in resources_with_actions diff --git a/tests/unit/datacenters/test_client.py b/tests/unit/datacenters/test_client.py index ef1aa840..59d0d25b 100644 --- a/tests/unit/datacenters/test_client.py +++ b/tests/unit/datacenters/test_client.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from unittest import mock # noqa: F401 import pytest # noqa: F401 @@ -8,6 +9,8 @@ from hcloud.datacenters import BoundDatacenter, DatacentersClient, DatacenterServerTypes from hcloud.locations import BoundLocation +warnings.filterwarnings("ignore", category=DeprecationWarning) + class TestBoundDatacenter: def test_bound_datacenter_init(self, datacenter_response): @@ -64,7 +67,9 @@ def test_bound_datacenter_init(self, datacenter_response): class TestDatacentersClient: @pytest.fixture() def datacenters_client(self, client: Client): - return DatacentersClient(client) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + return DatacentersClient(client) def test_get_by_id( self, diff --git a/tests/unit/datacenters/test_domain.py b/tests/unit/datacenters/test_domain.py index 58666bf6..fbf2b156 100644 --- a/tests/unit/datacenters/test_domain.py +++ b/tests/unit/datacenters/test_domain.py @@ -1,9 +1,13 @@ from __future__ import annotations +import warnings + import pytest from hcloud.datacenters import Datacenter, DatacenterServerTypes +warnings.filterwarnings("ignore", category=DeprecationWarning) + @pytest.mark.parametrize( "value", From 6fdc2d73e7aaa58acdb6b3dcaf6b461154d908d5 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:32:36 +0200 Subject: [PATCH 28/42] chore(main): release v2.22.0 (#662) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Datacenters resource is now deprecated The endpoints `GET /v1/datacenters` and `GET /v1/datacenters/{id}` are now deprecated and will be removed after 1 Oct. 2026. After this date, requests to these endpoints will return `HTTP 410 Gone`. The `DatacentersClient`, `Datacenter` and related classes are now deprecated. See the [changelog](https://docs.hetzner.cloud/changelog#2026-06-02-datacenters-deprecated) for more details. ### Features - deprecate datacenters (#656) ---

PR by releaser-pleaser 🤖

If you want to modify the proposed release, add you overrides here. You can learn more about the options in the docs. ## Release Notes ### Prefix / Start This will be added to the start of the release notes. ~~~~rp-prefix ### Datacenters resource is now deprecated The endpoints `GET /v1/datacenters` and `GET /v1/datacenters/{id}` are now deprecated and will be removed after 1 Oct. 2026. After this date, requests to these endpoints will return `HTTP 410 Gone`. The `DatacentersClient`, `Datacenter` and related classes are now deprecated. See the [changelog](https://docs.hetzner.cloud/changelog#2026-06-02-datacenters-deprecated) for more details. ~~~~ ### Suffix / End This will be added to the end of the release notes. ~~~~rp-suffix ~~~~
Co-authored-by: Hetzner Cloud Bot <> --- CHANGELOG.md | 14 ++++++++++++++ hcloud/_version.py | 2 +- setup.py | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fe8eb32..6d40b4b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [v2.22.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.22.0) + +### Datacenters resource is now deprecated + +The endpoints `GET /v1/datacenters` and `GET /v1/datacenters/{id}` are now deprecated and will be removed after 1 Oct. 2026. After this date, requests to these endpoints will return `HTTP 410 Gone`. + +The `DatacentersClient`, `Datacenter` and related classes are now deprecated. + +See the [changelog](https://docs.hetzner.cloud/changelog#2026-06-02-datacenters-deprecated) for more details. + +### Features + +- deprecate datacenters (#656) + ## [v2.21.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.21.0) ### Features diff --git a/hcloud/_version.py b/hcloud/_version.py index e5527186..7576cdbd 100644 --- a/hcloud/_version.py +++ b/hcloud/_version.py @@ -1,3 +1,3 @@ from __future__ import annotations -__version__ = "2.21.0" # x-releaser-pleaser-version +__version__ = "2.22.0" # x-releaser-pleaser-version diff --git a/setup.py b/setup.py index f2a279bf..768aaa41 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="hcloud", - version="2.21.0", # x-releaser-pleaser-version + version="2.22.0", # x-releaser-pleaser-version keywords="hcloud hetzner cloud", description="Official Hetzner Cloud python library", long_description=readme, From 3c2e97243cfcb0441c424566914cf30ea9bb38ec Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:11:45 +0200 Subject: [PATCH 29/42] chore(deps): update actions/cache action to v6 (#667) --- .github/workflows/links.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml index 484e9626..321e991a 100644 --- a/.github/workflows/links.yml +++ b/.github/workflows/links.yml @@ -13,7 +13,7 @@ jobs: steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: .lycheecache key: cache-lychee-${{ github.sha }} From 557e5a1371934900c45dce4b681747bdbc22ddf6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:14:25 +0200 Subject: [PATCH 30/42] chore(deps): update actions/setup-python digest to ece7cb0 (#665) --- .github/workflows/lint.yml | 4 ++-- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1686f3ec..4c8e3094 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Setup python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: 3.x @@ -28,7 +28,7 @@ jobs: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Setup python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: 3.x diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d195d05f..240a89c6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: 3.x diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e40fa076..1f631995 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Setup python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: ${{ matrix.python-version }} From 24533d0b976b32df9f310aacdd23fad9d2e7fde3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:16:40 +0200 Subject: [PATCH 31/42] chore(deps): update actions/checkout action to v7 (#666) --- .github/workflows/links.yml | 2 +- .github/workflows/lint.yml | 4 ++-- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml index 321e991a..401746bd 100644 --- a/.github/workflows/links.yml +++ b/.github/workflows/links.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4c8e3094..f1910d8d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,7 +9,7 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Setup python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 @@ -25,7 +25,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Setup python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 240a89c6..08d3de84 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1f631995..325a0e5f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: name: Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Setup python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 From 725b093ac4de8df3df680401a174ce7ce6371ceb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:16:54 +0200 Subject: [PATCH 32/42] chore(deps): update codecov/codecov-action action to v7 (#664) --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 325a0e5f..90181486 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,6 +37,6 @@ jobs: if: > !startsWith(github.head_ref, 'renovate/') && !startsWith(github.head_ref, 'releaser-pleaser--') - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 with: token: ${{ secrets.CODECOV_TOKEN }} From 85abb34f9e4e66559284478f11d1f288fe49f49e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:17:05 +0200 Subject: [PATCH 33/42] chore(deps): update dependency pytest to >=9,<9.2 (#663) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 768aaa41..28cc7325 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ "test": [ "coverage>=7.14,<7.15", "pylint>=4,<4.1", - "pytest>=9,<9.1", + "pytest>=9,<9.2", "pytest-cov>=7,<7.2", "mypy>=2.1,<2.2", "types-python-dateutil", From 36656a585f3ffdee19c99a7e61c4cb3594a6e4bf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:13:22 +0200 Subject: [PATCH 34/42] chore(deps): update github actions (#671) --- .github/workflows/links.yml | 2 +- .github/workflows/releaser-pleaser.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml index 401746bd..2791863e 100644 --- a/.github/workflows/links.yml +++ b/.github/workflows/links.yml @@ -19,7 +19,7 @@ jobs: key: cache-lychee-${{ github.sha }} restore-keys: cache-lychee- - - uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2 + - uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2 with: fail: true args: > diff --git a/.github/workflows/releaser-pleaser.yml b/.github/workflows/releaser-pleaser.yml index 7e552517..e810632f 100644 --- a/.github/workflows/releaser-pleaser.yml +++ b/.github/workflows/releaser-pleaser.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: releaser-pleaser - uses: apricote/releaser-pleaser@a1ce9493fd3f3abe60f22c37249d257bc10081dc # v0.8.0 + uses: apricote/releaser-pleaser@93a3e2c401320206dcd9a9b7a5bbc0850072b3c6 # v0.9.0 with: token: ${{ secrets.HCLOUD_BOT_TOKEN }} extra-files: | From e29b12fb0f1e4f337e9d4e8a863ba43a44f47a95 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:22:20 +0200 Subject: [PATCH 35/42] chore(deps): update dependency mypy to >=2.2,<2.3 (#670) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 28cc7325..3e3e4315 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ "pylint>=4,<4.1", "pytest>=9,<9.2", "pytest-cov>=7,<7.2", - "mypy>=2.1,<2.2", + "mypy>=2.2,<2.3", "types-python-dateutil", "types-requests", ], From b733181f4cac54ddcbbca119e8018e631139ada1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:22:33 +0200 Subject: [PATCH 36/42] chore(deps): update dependency coverage to >=7.15,<7.16 (#669) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3e3e4315..ff037d56 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ "watchdog>=6,<6.1", ], "test": [ - "coverage>=7.14,<7.15", + "coverage>=7.15,<7.16", "pylint>=4,<4.1", "pytest>=9,<9.2", "pytest-cov>=7,<7.2", From abdadf3e7cc5d0edeb38f369955fa83f71b102e5 Mon Sep 17 00:00:00 2001 From: Lukas Metzner Date: Thu, 16 Jul 2026 08:54:38 +0200 Subject: [PATCH 37/42] feat: remove datacenter property from server and primary_ip (#668) Remove the `datacenter` property from Server and Primary IP resource where possible. The property was deprecated ~6 months ago, and because the property is removed from the API, removing it from our code is not considered a breaking change. Changelog entry: https://docs.hetzner.cloud/changelog#2026-07-01-removing-datacenters --- hcloud/primary_ips/client.py | 19 ----- hcloud/primary_ips/domain.py | 41 +-------- hcloud/servers/client.py | 20 ----- hcloud/servers/domain.py | 39 +-------- tests/unit/primary_ips/conftest.py | 8 -- tests/unit/primary_ips/test_client.py | 39 --------- tests/unit/servers/conftest.py | 114 -------------------------- tests/unit/servers/test_client.py | 49 ----------- 8 files changed, 4 insertions(+), 325 deletions(-) diff --git a/hcloud/primary_ips/client.py b/hcloud/primary_ips/client.py index 4a0c490d..08efdf34 100644 --- a/hcloud/primary_ips/client.py +++ b/hcloud/primary_ips/client.py @@ -16,7 +16,6 @@ if TYPE_CHECKING: from .._client import Client - from ..datacenters import BoundDatacenter, Datacenter from ..locations import BoundLocation, Location @@ -39,15 +38,8 @@ def __init__( complete: bool = True, ): # pylint: disable=import-outside-toplevel - from ..datacenters import BoundDatacenter from ..locations import BoundLocation - raw = data.get("datacenter", {}) - if raw: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - data["datacenter"] = BoundDatacenter(client._parent.datacenters, raw) - raw = data.get("location", {}) if raw: data["location"] = BoundLocation(client._parent.locations, raw) @@ -311,7 +303,6 @@ def create( self, type: str, name: str, - datacenter: Datacenter | BoundDatacenter | None = None, location: Location | BoundLocation | None = None, assignee_type: str | None = None, assignee_id: int | None = None, @@ -322,7 +313,6 @@ def create( :param type: str Primary IP type Choices: ipv4, ipv6 :param name: str - :param datacenter: Datacenter (optional) :param location: Location (optional) :param assignee_type: str (optional) :param assignee_id: int (optional) @@ -336,15 +326,6 @@ def create( "auto_delete": auto_delete, } - if datacenter is not None: - warnings.warn( - "The 'datacenter' argument is deprecated and will be removed after 1 July 2026. " - "Please use the 'location' argument instead. " - "See https://docs.hetzner.cloud/changelog#2025-12-16-phasing-out-datacenters", - DeprecationWarning, - stacklevel=2, - ) - data["datacenter"] = datacenter.id_or_name if location is not None: data["location"] = location.id_or_name if assignee_id is not None: diff --git a/hcloud/primary_ips/domain.py b/hcloud/primary_ips/domain.py index 96749be2..2e964498 100644 --- a/hcloud/primary_ips/domain.py +++ b/hcloud/primary_ips/domain.py @@ -1,13 +1,11 @@ from __future__ import annotations -import warnings from typing import TYPE_CHECKING, TypedDict from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction - from ..datacenters import BoundDatacenter from ..locations import BoundLocation from ..rdns import DNSPtr from .client import BoundPrimaryIP @@ -30,14 +28,6 @@ class PrimaryIP(BaseDomain, DomainIdentityMixin): Type of Primary IP. Choices: `ipv4`, `ipv6` :param dns_ptr: List[Dict] Array of reverse DNS entries - :param datacenter: :class:`Datacenter ` - Datacenter the Primary IP was created in. - - This property is deprecated and will be removed after 1 July 2026. - Please use the ``location`` property instead. - - See https://docs.hetzner.cloud/changelog#2025-12-16-phasing-out-datacenters. - :param location: :class:`Location ` Location the Primary IP was created in. :param blocked: boolean @@ -58,7 +48,7 @@ class PrimaryIP(BaseDomain, DomainIdentityMixin): Delete the Primary IP when the Assignee it is assigned to is deleted. """ - __properties__ = ( + __api_properties__ = ( "id", "ip", "type", @@ -73,14 +63,7 @@ class PrimaryIP(BaseDomain, DomainIdentityMixin): "assignee_type", "auto_delete", ) - __api_properties__ = ( - *__properties__, - "datacenter", - ) - __slots__ = ( - *__properties__, - "_datacenter", - ) + __slots__ = __api_properties__ def __init__( self, @@ -88,7 +71,6 @@ def __init__( type: str | None = None, ip: str | None = None, dns_ptr: list[DNSPtr] | None = None, - datacenter: BoundDatacenter | None = None, location: BoundLocation | None = None, blocked: bool | None = None, protection: PrimaryIPProtection | None = None, @@ -103,7 +85,6 @@ def __init__( self.type = type self.ip = ip self.dns_ptr = dns_ptr - self.datacenter = datacenter self.location = location self.blocked = blocked self.protection = protection @@ -114,24 +95,6 @@ def __init__( self.assignee_type = assignee_type self.auto_delete = auto_delete - @property - def datacenter(self) -> BoundDatacenter | None: - """ - :meta private: - """ - warnings.warn( - "The 'datacenter' property is deprecated and will be removed after 1 July 2026. " - "Please use the 'location' property instead. " - "See https://docs.hetzner.cloud/changelog#2025-12-16-phasing-out-datacenters.", - DeprecationWarning, - stacklevel=2, - ) - return self._datacenter - - @datacenter.setter - def datacenter(self, value: BoundDatacenter | None) -> None: - self._datacenter = value - class PrimaryIPProtection(TypedDict): delete: bool diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index 6e406f2c..68cbb9ce 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -1,6 +1,5 @@ from __future__ import annotations -import warnings from datetime import datetime from typing import TYPE_CHECKING, Any, NamedTuple @@ -15,7 +14,6 @@ ) from ..actions.client import ResourceClientBaseActionsMixin from ..core import BoundModelBase, Meta, ResourceClientBase -from ..datacenters import BoundDatacenter from ..firewalls import BoundFirewall from ..floating_ips import BoundFloatingIP from ..images import BoundImage, CreateImageResponse @@ -44,7 +42,6 @@ if TYPE_CHECKING: from .._client import Client - from ..datacenters import Datacenter from ..firewalls import Firewall from ..images import Image from ..isos import Iso @@ -75,12 +72,6 @@ def __init__( data: dict[str, Any], complete: bool = True, ): - raw = data.get("datacenter") - if raw: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - data["datacenter"] = BoundDatacenter(client._parent.datacenters, raw) - raw = data.get("location") if raw: data["location"] = BoundLocation(client._parent.locations, raw) @@ -636,7 +627,6 @@ def create( user_data: str | None = None, labels: dict[str, str] | None = None, location: Location | BoundLocation | None = None, - datacenter: Datacenter | BoundDatacenter | None = None, start_after_create: bool | None = True, automount: bool | None = None, placement_group: PlacementGroup | BoundPlacementGroup | None = None, @@ -661,7 +651,6 @@ def create( :param labels: Dict[str,str] (optional) User-defined labels (key-value pairs) :param location: :class:`BoundLocation ` or :class:`Location ` - :param datacenter: :class:`BoundDatacenter ` or :class:`Datacenter ` :param start_after_create: boolean (optional) Start Server right after creation. Defaults to True. :param automount: boolean (optional) @@ -681,15 +670,6 @@ def create( if location is not None: data["location"] = location.id_or_name - if datacenter is not None: - warnings.warn( - "The 'datacenter' argument is deprecated and will be removed after 1 July 2026. " - "Please use the 'location' argument instead. " - "See https://docs.hetzner.cloud/changelog#2025-12-16-phasing-out-datacenters", - DeprecationWarning, - stacklevel=2, - ) - data["datacenter"] = datacenter.id_or_name if ssh_keys is not None: data["ssh_keys"] = [ssh_key.id_or_name for ssh_key in ssh_keys] if volumes is not None: diff --git a/hcloud/servers/domain.py b/hcloud/servers/domain.py index ec019e4e..32ea4506 100644 --- a/hcloud/servers/domain.py +++ b/hcloud/servers/domain.py @@ -1,13 +1,11 @@ from __future__ import annotations -import warnings from typing import TYPE_CHECKING, Literal, TypedDict from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction - from ..datacenters import BoundDatacenter from ..firewalls import BoundFirewall from ..floating_ips import BoundFloatingIP from ..images import BoundImage @@ -56,12 +54,6 @@ class Server(BaseDomain, DomainIdentityMixin): :param public_net: :class:`PublicNetwork ` Public network information. :param server_type: :class:`BoundServerType ` - :param datacenter: :class:`BoundDatacenter ` - - This property is deprecated and will be removed after 1 July 2026. - Please use the ``location`` property instead. - - See https://docs.hetzner.cloud/changelog#2025-12-16-phasing-out-datacenters. :param location: :class:`BoundLocation ` :param image: :class:`BoundImage `, None :param iso: :class:`BoundIso `, None @@ -108,7 +100,7 @@ class Server(BaseDomain, DomainIdentityMixin): STATUS_UNKNOWN = "unknown" """Server Status unknown""" - __properties__ = ( + __api_properties__ = ( "id", "name", "status", @@ -131,14 +123,7 @@ class Server(BaseDomain, DomainIdentityMixin): "primary_disk_size", "placement_group", ) - __api_properties__ = ( - *__properties__, - "datacenter", - ) - __slots__ = ( - *__properties__, - "_datacenter", - ) + __slots__ = __api_properties__ # pylint: disable=too-many-locals def __init__( @@ -149,7 +134,6 @@ def __init__( created: str | None = None, public_net: PublicNetwork | None = None, server_type: BoundServerType | None = None, - datacenter: BoundDatacenter | None = None, location: BoundLocation | None = None, image: BoundImage | None = None, iso: BoundIso | None = None, @@ -172,7 +156,6 @@ def __init__( self.created = self._parse_datetime(created) self.public_net = public_net self.server_type = server_type - self.datacenter = datacenter self.location = location self.image = image self.iso = iso @@ -199,24 +182,6 @@ def private_net_for(self, network: BoundNetwork | Network) -> PrivateNet | None: return o return None - @property - def datacenter(self) -> BoundDatacenter | None: - """ - :meta private: - """ - warnings.warn( - "The 'datacenter' property is deprecated and will be removed after 1 July 2026. " - "Please use the 'location' property instead. " - "See https://docs.hetzner.cloud/changelog#2025-12-16-phasing-out-datacenters.", - DeprecationWarning, - stacklevel=2, - ) - return self._datacenter - - @datacenter.setter - def datacenter(self, value: BoundDatacenter | None) -> None: - self._datacenter = value - class ServerProtection(TypedDict): rebuild: bool diff --git a/tests/unit/primary_ips/conftest.py b/tests/unit/primary_ips/conftest.py index 361de307..3d84d871 100644 --- a/tests/unit/primary_ips/conftest.py +++ b/tests/unit/primary_ips/conftest.py @@ -14,10 +14,6 @@ def primary_ip1(): "assignee_type": "server", "auto_delete": True, "blocked": False, - "datacenter": { - "id": 4, - "name": "fsn1-dc14", - }, "location": { "id": 1, "name": "fsn1", @@ -42,10 +38,6 @@ def primary_ip2(): "assignee_type": "server", "auto_delete": True, "blocked": False, - "datacenter": { - "id": 4, - "name": "fsn1-dc14", - }, "location": { "id": 1, "name": "fsn1", diff --git a/tests/unit/primary_ips/test_client.py b/tests/unit/primary_ips/test_client.py index 638acb2b..f8df0225 100644 --- a/tests/unit/primary_ips/test_client.py +++ b/tests/unit/primary_ips/test_client.py @@ -5,7 +5,6 @@ import pytest from hcloud import Client -from hcloud.datacenters import BoundDatacenter, Datacenter from hcloud.locations import BoundLocation, Location from hcloud.primary_ips import BoundPrimaryIP, PrimaryIP, PrimaryIPsClient @@ -62,13 +61,6 @@ def test_init(self, primary_ip1): assert o.location.id == 1 assert o.location.name == "fsn1" - with pytest.deprecated_call(): - datacenter = o.datacenter - - assert isinstance(datacenter, BoundDatacenter) - assert datacenter.id == 4 - assert datacenter.name == "fsn1-dc14" - class TestPrimaryIPsClient: @pytest.fixture() @@ -169,37 +161,6 @@ def test_create_with_location( assert_bound_primary_ip1(result.primary_ip, resource_client) assert result.action is None - def test_create_with_datacenter( - self, - request_mock: mock.MagicMock, - resource_client: PrimaryIPsClient, - primary_ip1, - ): - request_mock.return_value = { - "primary_ip": primary_ip1, - "action": None, - } - - with pytest.deprecated_call(): - result = resource_client.create( - type="ipv4", - name="primary-ip1", - datacenter=Datacenter(name="fsn1-dc14"), - ) - - request_mock.assert_called_with( - method="POST", - url="/primary_ips", - json={ - "name": "primary-ip1", - "type": "ipv4", - "datacenter": "fsn1-dc14", - "auto_delete": False, - }, - ) - assert_bound_primary_ip1(result.primary_ip, resource_client) - assert result.action is None - def test_create_with_assignee_id( self, request_mock: mock.MagicMock, diff --git a/tests/unit/servers/conftest.py b/tests/unit/servers/conftest.py index 01649320..2ccf5d64 100644 --- a/tests/unit/servers/conftest.py +++ b/tests/unit/servers/conftest.py @@ -58,25 +58,6 @@ def response_simple_server(): "storage_type": "local", "cpu_type": "shared", }, - "datacenter": { - "id": 1, - "name": "fsn1-dc8", - "description": "Falkenstein 1 DC 8", - "location": { - "id": 1, - "name": "fsn1", - "description": "Falkenstein DC Park 1", - "country": "DE", - "city": "Falkenstein", - "latitude": 50.47612, - "longitude": 12.370071, - }, - "server_types": { - "supported": [1, 2, 3], - "available": [1, 2, 3], - "available_for_migration": [1, 2, 3], - }, - }, "image": { "id": 4711, "type": "snapshot", @@ -159,25 +140,6 @@ def response_create_simple_server(): "storage_type": "local", "cpu_type": "shared", }, - "datacenter": { - "id": 1, - "name": "fsn1-dc8", - "description": "Falkenstein 1 DC 8", - "location": { - "id": 1, - "name": "fsn1", - "description": "Falkenstein DC Park 1", - "country": "DE", - "city": "Falkenstein", - "latitude": 50.47612, - "longitude": 12.370071, - }, - "server_types": { - "supported": [1, 2, 3], - "available": [1, 2, 3], - "available_for_migration": [1, 2, 3], - }, - }, "image": { "id": 4711, "type": "snapshot", @@ -281,25 +243,6 @@ def response_update_server(): "storage_type": "local", "cpu_type": "shared", }, - "datacenter": { - "id": 1, - "name": "fsn1-dc8", - "description": "Falkenstein 1 DC 8", - "location": { - "id": 1, - "name": "fsn1", - "description": "Falkenstein DC Park 1", - "country": "DE", - "city": "Falkenstein", - "latitude": 50.47612, - "longitude": 12.370071, - }, - "server_types": { - "supported": [1, 2, 3], - "available": [1, 2, 3], - "available_for_migration": [1, 2, 3], - }, - }, "image": { "id": 4711, "type": "snapshot", @@ -444,25 +387,6 @@ def response_simple_servers(): "storage_type": "local", "cpu_type": "shared", }, - "datacenter": { - "id": 1, - "name": "fsn1-dc8", - "description": "Falkenstein 1 DC 8", - "location": { - "id": 1, - "name": "fsn1", - "description": "Falkenstein DC Park 1", - "country": "DE", - "city": "Falkenstein", - "latitude": 50.47612, - "longitude": 12.370071, - }, - "server_types": { - "supported": [1, 2, 3], - "available": [1, 2, 3], - "available_for_migration": [1, 2, 3], - }, - }, "image": { "id": 4711, "type": "snapshot", @@ -546,25 +470,6 @@ def response_simple_servers(): "storage_type": "local", "cpu_type": "shared", }, - "datacenter": { - "id": 1, - "name": "fsn1-dc8", - "description": "Falkenstein 1 DC 8", - "location": { - "id": 1, - "name": "fsn1", - "description": "Falkenstein DC Park 1", - "country": "DE", - "city": "Falkenstein", - "latitude": 50.47612, - "longitude": 12.370071, - }, - "server_types": { - "supported": [1, 2, 3], - "available": [1, 2, 3], - "available_for_migration": [1, 2, 3], - }, - }, "image": { "id": 4711, "type": "snapshot", @@ -643,25 +548,6 @@ def response_full_server(): "storage_type": "local", "cpu_type": "shared", }, - "datacenter": { - "id": 1, - "name": "fsn1-dc8", - "description": "Falkenstein 1 DC 8", - "location": { - "id": 1, - "name": "fsn1", - "description": "Falkenstein DC Park 1", - "country": "DE", - "city": "Falkenstein", - "latitude": 50.47612, - "longitude": 12.370071, - }, - "server_types": { - "supported": [1, 2, 3], - "available": [1, 2, 3], - "available_for_migration": [1, 2, 3], - }, - }, "image": { "id": 4711, "type": "snapshot", diff --git a/tests/unit/servers/test_client.py b/tests/unit/servers/test_client.py index 0a09544d..3d3ab085 100644 --- a/tests/unit/servers/test_client.py +++ b/tests/unit/servers/test_client.py @@ -6,7 +6,6 @@ from hcloud import Client from hcloud.actions import BoundAction -from hcloud.datacenters import BoundDatacenter, Datacenter from hcloud.firewalls import BoundFirewall, Firewall from hcloud.floating_ips import BoundFloatingIP from hcloud.images import BoundImage, Image @@ -100,14 +99,6 @@ def test_init(self, response_full_server): assert bound_server.public_net.floating_ips[0].id == 478 assert bound_server.public_net.floating_ips[0].complete is False - with pytest.deprecated_call(): - datacenter = bound_server.datacenter - - assert isinstance(datacenter, BoundDatacenter) - assert datacenter._client == bound_server._client._parent.datacenters - assert datacenter.id == 1 - assert datacenter.complete is True - assert isinstance(bound_server.server_type, BoundServerType) assert ( bound_server.server_type._client @@ -281,46 +272,6 @@ def test_get_by_name( assert bound_server.id == 1 assert bound_server.name == "my-server" - def test_create_with_datacenter( - self, - request_mock: mock.MagicMock, - servers_client: ServersClient, - response_create_simple_server, - ): - request_mock.return_value = response_create_simple_server - - with pytest.deprecated_call(): - response = servers_client.create( - "my-server", - server_type=ServerType(name="cx11"), - image=Image(id=4711), - datacenter=Datacenter(id=1), - ) - - request_mock.assert_called_with( - method="POST", - url="/servers", - json={ - "name": "my-server", - "server_type": "cx11", - "image": 4711, - "datacenter": 1, - "start_after_create": True, - }, - ) - - bound_server = response.server - bound_action = response.action - - assert bound_server._client is servers_client - assert bound_server.id == 1 - assert bound_server.name == "my-server" - - assert isinstance(bound_action, BoundAction) - assert bound_action._client == servers_client._parent.actions - assert bound_action.id == 1 - assert bound_action.command == "create_server" - def test_create_with_location( self, request_mock: mock.MagicMock, From d6148f4826255e2c9dfe3f2cfac31cc75a9e7eab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:52:53 +0200 Subject: [PATCH 38/42] chore(deps): update dependency mypy to >=2.3,<2.4 (#673) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ff037d56..d6c47860 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ "pylint>=4,<4.1", "pytest>=9,<9.2", "pytest-cov>=7,<7.2", - "mypy>=2.2,<2.3", + "mypy>=2.3,<2.4", "types-python-dateutil", "types-requests", ], From 629a8f8c4c00b2ca5b72db2e17cebc319cf10ffc Mon Sep 17 00:00:00 2001 From: "Jonas L." Date: Mon, 20 Jul 2026 18:29:19 +0200 Subject: [PATCH 39/42] test: update load balancer types tests (#675) --- tests/unit/load_balancer_types/conftest.py | 137 +++++------------- tests/unit/load_balancer_types/test_client.py | 103 +++++++------ 2 files changed, 95 insertions(+), 145 deletions(-) diff --git a/tests/unit/load_balancer_types/conftest.py b/tests/unit/load_balancer_types/conftest.py index 0736d787..f0a2ec5b 100644 --- a/tests/unit/load_balancer_types/conftest.py +++ b/tests/unit/load_balancer_types/conftest.py @@ -4,114 +4,49 @@ @pytest.fixture() -def load_balancer_type_response(): +def load_balancer_type1(): return { - "load_balancer_type": { - "id": 1, - "name": "LB11", - "description": "LB11", - "max_connections": 1, - "max_services": 1, - "max_targets": 1, - "max_assigned_certificates": 1, - "deprecated": None, - "prices": [ - { - "location": "fsn1", - "price_hourly": { - "net": "1.0000000000", - "gross": "1.1900000000000000", - }, - "price_monthly": { - "net": "1.0000000000", - "gross": "1.1900000000000000", - }, - } - ], - } - } - - -@pytest.fixture() -def two_load_balancer_types_response(): - return { - "load_balancer_types": [ - { - "id": 1, - "name": "LB11", - "description": "LB11D", - "max_connections": 1, - "max_services": 1, - "max_targets": 1, - "max_assigned_certificates": 1, - "deprecated": None, - "prices": [ - { - "location": "fsn1", - "price_hourly": { - "net": "1.0000000000", - "gross": "1.1900000000000000", - }, - "price_monthly": { - "net": "1.0000000000", - "gross": "1.1900000000000000", - }, - } - ], - }, + "id": 1, + "name": "lb11", + "description": "LB11", + "max_connections": 10000, + "max_services": 5, + "max_targets": 25, + "max_assigned_certificates": 10, + "deprecation": { + "announced": "2023-06-01T00:00:00Z", + "unavailable_after": "2023-09-01T00:00:00Z", + }, + "prices": [ { - "id": 2, - "name": "LB21", - "description": "LB21D", - "max_connections": 2, - "max_services": 2, - "max_targets": 2, - "max_assigned_certificates": 2, - "deprecated": None, - "prices": [ - { - "location": "fsn1", - "price_hourly": { - "net": "1.0000000000", - "gross": "1.1900000000000000", - }, - "price_monthly": { - "net": "1.0000000000", - "gross": "1.1900000000000000", - }, - } - ], + "location": "fsn1", + "price_hourly": {"net": "0.0120", "gross": "0.0120"}, + "price_monthly": {"net": "7.4900", "gross": "7.4900"}, + "price_per_tb_traffic": {"net": "1.0000", "gross": "1.0000"}, + "included_traffic": 21990232555520, }, - ] + ], } @pytest.fixture() -def one_load_balancer_types_response(): +def load_balancer_type2(): return { - "load_balancer_types": [ + "id": 2, + "name": "lb21", + "description": "LB21", + "max_connections": 20000, + "max_services": 15, + "max_targets": 75, + "max_assigned_certificates": 25, + "deprecation": None, + "prices": [ { - "id": 2, - "name": "LB21", - "description": "LB21D", - "max_connections": 2, - "max_services": 2, - "max_targets": 2, - "max_assigned_certificates": 2, - "deprecated": None, - "prices": [ - { - "location": "fsn1", - "price_hourly": { - "net": "1.0000000000", - "gross": "1.1900000000000000", - }, - "price_monthly": { - "net": "1.0000000000", - "gross": "1.1900000000000000", - }, - } - ], - } - ] + "location": "fsn1", + "price_hourly": {"net": "0.0344", "gross": "0.0344"}, + "price_monthly": {"net": "21.4900", "gross": "21.4900"}, + "price_per_tb_traffic": {"net": "1.0000", "gross": "1.0000"}, + "included_traffic": 21990232555520, + }, + ], } diff --git a/tests/unit/load_balancer_types/test_client.py b/tests/unit/load_balancer_types/test_client.py index c9f356dc..8a5bc628 100644 --- a/tests/unit/load_balancer_types/test_client.py +++ b/tests/unit/load_balancer_types/test_client.py @@ -17,31 +17,51 @@ def test_get_by_id( self, request_mock: mock.MagicMock, load_balancer_types_client: LoadBalancerTypesClient, - load_balancer_type_response, + load_balancer_type1, ): - request_mock.return_value = load_balancer_type_response + request_mock.return_value = { + "load_balancer_type": load_balancer_type1, + } - load_balancer_type = load_balancer_types_client.get_by_id(1) + result = load_balancer_types_client.get_by_id(1) request_mock.assert_called_with( method="GET", url="/load_balancer_types/1", ) - assert load_balancer_type._client is load_balancer_types_client - assert load_balancer_type.id == 1 - assert load_balancer_type.name == "LB11" + + assert result._client is load_balancer_types_client + assert result.id == 1 + assert result.name == "lb11" + assert result.description == "LB11" + assert result.max_connections == 10000 + assert result.max_services == 5 + assert result.max_targets == 25 + assert result.max_assigned_certificates == 10 + assert result.prices == [ + { + "location": "fsn1", + "price_hourly": {"net": "0.0120", "gross": "0.0120"}, + "price_monthly": {"net": "7.4900", "gross": "7.4900"}, + "price_per_tb_traffic": {"net": "1.0000", "gross": "1.0000"}, + "included_traffic": 21990232555520, + } + ] @pytest.mark.parametrize( - "params", [{"name": "LB11", "page": 1, "per_page": 10}, {"name": ""}, {}] + "params", [{"name": "lb11", "page": 1, "per_page": 10}, {"name": ""}, {}] ) def test_get_list( self, request_mock: mock.MagicMock, load_balancer_types_client: LoadBalancerTypesClient, - two_load_balancer_types_response, + load_balancer_type1, + load_balancer_type2, params, ): - request_mock.return_value = two_load_balancer_types_response + request_mock.return_value = { + "load_balancer_types": [load_balancer_type1, load_balancer_type2], + } result = load_balancer_types_client.get_list(**params) @@ -51,33 +71,31 @@ def test_get_list( params=params, ) - load_balancer_types = result.load_balancer_types assert result.meta is not None + assert len(result.load_balancer_types) == 2 - assert len(load_balancer_types) == 2 - - load_balancer_types1 = load_balancer_types[0] - load_balancer_types2 = load_balancer_types[1] - - assert load_balancer_types1._client is load_balancer_types_client - assert load_balancer_types1.id == 1 - assert load_balancer_types1.name == "LB11" + assert result.load_balancer_types[0]._client is load_balancer_types_client + assert result.load_balancer_types[0].id == 1 + assert result.load_balancer_types[0].name == "lb11" - assert load_balancer_types2._client is load_balancer_types_client - assert load_balancer_types2.id == 2 - assert load_balancer_types2.name == "LB21" + assert result.load_balancer_types[1]._client is load_balancer_types_client + assert result.load_balancer_types[1].id == 2 + assert result.load_balancer_types[1].name == "lb21" - @pytest.mark.parametrize("params", [{"name": "LB21"}]) + @pytest.mark.parametrize("params", [{"name": "lb11"}]) def test_get_all( self, request_mock: mock.MagicMock, load_balancer_types_client: LoadBalancerTypesClient, - two_load_balancer_types_response, + load_balancer_type1, + load_balancer_type2, params, ): - request_mock.return_value = two_load_balancer_types_response + request_mock.return_value = { + "load_balancer_types": [load_balancer_type1, load_balancer_type2] + } - load_balancer_types = load_balancer_types_client.get_all(**params) + result = load_balancer_types_client.get_all(**params) params.update({"page": 1, "per_page": 50}) @@ -87,37 +105,34 @@ def test_get_all( params=params, ) - assert len(load_balancer_types) == 2 + assert len(result) == 2 - load_balancer_types1 = load_balancer_types[0] - load_balancer_types2 = load_balancer_types[1] + assert result[0]._client is load_balancer_types_client + assert result[0].id == 1 + assert result[0].name == "lb11" - assert load_balancer_types1._client is load_balancer_types_client - assert load_balancer_types1.id == 1 - assert load_balancer_types1.name == "LB11" - - assert load_balancer_types2._client is load_balancer_types_client - assert load_balancer_types2.id == 2 - assert load_balancer_types2.name == "LB21" + assert result[1]._client is load_balancer_types_client + assert result[1].id == 2 + assert result[1].name == "lb21" def test_get_by_name( self, request_mock: mock.MagicMock, load_balancer_types_client: LoadBalancerTypesClient, - one_load_balancer_types_response, + load_balancer_type1, ): - request_mock.return_value = one_load_balancer_types_response - - load_balancer_type = load_balancer_types_client.get_by_name("LB21") + request_mock.return_value = { + "load_balancer_types": [load_balancer_type1], + } - params = {"name": "LB21"} + result = load_balancer_types_client.get_by_name("lb11") request_mock.assert_called_with( method="GET", url="/load_balancer_types", - params=params, + params={"name": "lb11"}, ) - assert load_balancer_type._client is load_balancer_types_client - assert load_balancer_type.id == 2 - assert load_balancer_type.name == "LB21" + assert result._client is load_balancer_types_client + assert result.id == 1 + assert result.name == "lb11" From aeebcf8bd32df390163089a33c57815bb7214763 Mon Sep 17 00:00:00 2001 From: "Jonas L." Date: Tue, 21 Jul 2026 09:57:30 +0200 Subject: [PATCH 40/42] feat: add deprecation info to load balancer type (#674) Add the `deprecation` object property to the `LoadBalancerType` domain. https://docs.hetzner.cloud/changelog#2026-06-05-deprecation-info-for-load-balancer-types --- hcloud/load_balancer_types/domain.py | 6 +++++- tests/unit/load_balancer_types/test_client.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/hcloud/load_balancer_types/domain.py b/hcloud/load_balancer_types/domain.py index 1e594e94..7814a4f4 100644 --- a/hcloud/load_balancer_types/domain.py +++ b/hcloud/load_balancer_types/domain.py @@ -3,6 +3,7 @@ from typing import Any from ..core import BaseDomain, DomainIdentityMixin +from ..deprecation import DeprecationInfo __all__ = [ "LoadBalancerType", @@ -28,7 +29,7 @@ class LoadBalancerType(BaseDomain, DomainIdentityMixin): Max amount of certificates the Load Balancer can serve :param prices: List of dict Prices in different locations - + :param deprecation: Define if and when the Load Balancer Type is deprecated. """ __api_properties__ = ( @@ -40,6 +41,7 @@ class LoadBalancerType(BaseDomain, DomainIdentityMixin): "max_targets", "max_assigned_certificates", "prices", + "deprecation", ) __slots__ = __api_properties__ @@ -53,6 +55,7 @@ def __init__( max_targets: int | None = None, max_assigned_certificates: int | None = None, prices: list[dict[str, Any]] | None = None, + deprecation: dict[str, Any] | None = None, ): self.id = id self.name = name @@ -62,3 +65,4 @@ def __init__( self.max_targets = max_targets self.max_assigned_certificates = max_assigned_certificates self.prices = prices + self.deprecation = deprecation and DeprecationInfo.from_dict(deprecation) diff --git a/tests/unit/load_balancer_types/test_client.py b/tests/unit/load_balancer_types/test_client.py index 8a5bc628..3c774a30 100644 --- a/tests/unit/load_balancer_types/test_client.py +++ b/tests/unit/load_balancer_types/test_client.py @@ -3,6 +3,7 @@ from unittest import mock import pytest +from dateutil.parser import isoparse from hcloud import Client from hcloud.load_balancer_types import LoadBalancerTypesClient @@ -47,6 +48,10 @@ def test_get_by_id( "included_traffic": 21990232555520, } ] + assert result.deprecation.announced == isoparse("2023-06-01T00:00:00+00:00") + assert result.deprecation.unavailable_after == isoparse( + "2023-09-01T00:00:00+00:00" + ) @pytest.mark.parametrize( "params", [{"name": "lb11", "page": 1, "per_page": 10}, {"name": ""}, {}] From 381d71a34994bc3123a16bde4321cf566f5f3793 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:28:31 +0200 Subject: [PATCH 41/42] chore(deps): replace pre-commit hook pre-commit/mirrors-prettier with jooola/pre-commit-prettier 3.9.6 (#676) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 27bc959c..c35de9e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,8 +22,8 @@ repos: args: [--fix=lf] - id: trailing-whitespace - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.1.0 + - repo: https://github.com/jooola/pre-commit-prettier + rev: 3.9.6 hooks: - id: prettier files: \.(md|ya?ml|js|css)$ From 48f3424b49ece33652cd8ca7163e857ca3fecbb9 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:30:42 +0200 Subject: [PATCH 42/42] chore(main): release v2.23.0 (#672) ### Removed deprecated Datacenter property from Server and PrimaryIP Removed the deprecated Datacenter property from the Server and PrimaryIP resources. Since the property was already removed from the Hetzner Cloud API, we do not consider this a breaking change (see [changelog entry](https://docs.hetzner.cloud/changelog#2026-07-01-removing-datacenters)). > [!IMPORTANT] > **Action required:** Please update all code that accesses `server.datacenter` or `primary_ip.datacenter` to use the `location` property instead, as shown below. **Before:** ```python server = client.server.get_by_id(5) print(server.datacenter) primary_ip = client.primary_ip.get_by_id(5) print(primary_ip.datacenter) ``` **After:** ```python server = client.server.get_by_id(5) print(server.location) primary_ip = client.primary_ip.get_by_id(5) print(primary_ip.location) ``` ### Features - remove datacenter property from server and primary_ip (#668) ([abdadf3](https://github.com/hetznercloud/hcloud-python/commit/abdadf3e7cc5d0edeb38f369955fa83f71b102e5)) - add deprecation info to load balancer type (#674) ([aeebcf8](https://github.com/hetznercloud/hcloud-python/commit/aeebcf8bd32df390163089a33c57815bb7214763)) --- CHANGELOG.md | 36 ++++++++++++++++++++++++++++++++++++ hcloud/_version.py | 2 +- setup.py | 2 +- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d40b4b6..2d744fe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,41 @@ # Changelog +## [v2.23.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.23.0) + +[Compare to previous version](https://github.com/hetznercloud/hcloud-python/compare/v2.22.0...v2.23.0) + +### Removed deprecated Datacenter property from Server and PrimaryIP + +Removed the deprecated Datacenter property from the Server and PrimaryIP resources. Since the property was already removed from the Hetzner Cloud API, we do not consider this a breaking change (see [changelog entry](https://docs.hetzner.cloud/changelog#2026-07-01-removing-datacenters)). + +> [!IMPORTANT] +> **Action required:** Please update all code that accesses `server.datacenter` or `primary_ip.datacenter` to use the `location` property instead, as shown below. + +**Before:** + +```python +server = client.server.get_by_id(5) +print(server.datacenter) + +primary_ip = client.primary_ip.get_by_id(5) +print(primary_ip.datacenter) +``` + +**After:** + +```python +server = client.server.get_by_id(5) +print(server.location) + +primary_ip = client.primary_ip.get_by_id(5) +print(primary_ip.location) +``` + +### Features + +- remove datacenter property from server and primary_ip (#668) ([abdadf3](https://github.com/hetznercloud/hcloud-python/commit/abdadf3e7cc5d0edeb38f369955fa83f71b102e5)) +- add deprecation info to load balancer type (#674) ([aeebcf8](https://github.com/hetznercloud/hcloud-python/commit/aeebcf8bd32df390163089a33c57815bb7214763)) + ## [v2.22.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.22.0) ### Datacenters resource is now deprecated diff --git a/hcloud/_version.py b/hcloud/_version.py index 7576cdbd..8408a231 100644 --- a/hcloud/_version.py +++ b/hcloud/_version.py @@ -1,3 +1,3 @@ from __future__ import annotations -__version__ = "2.22.0" # x-releaser-pleaser-version +__version__ = "2.23.0" # x-releaser-pleaser-version diff --git a/setup.py b/setup.py index d6c47860..93f2a6ab 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="hcloud", - version="2.22.0", # x-releaser-pleaser-version + version="2.23.0", # x-releaser-pleaser-version keywords="hcloud hetzner cloud", description="Official Hetzner Cloud python library", long_description=readme,