diff --git a/hcloud/core/domain.py b/hcloud/core/domain.py index 30c2d3e..65b7ffe 100644 --- a/hcloud/core/domain.py +++ b/hcloud/core/domain.py @@ -1,5 +1,10 @@ from __future__ import annotations +from datetime import datetime +from typing import Any + +from .client import BoundModelBase + class BaseDomain: __slots__: tuple[str, ...] = () @@ -12,11 +17,34 @@ def from_dict(cls, data: dict): # type: ignore[no-untyped-def] supported_data = {k: v for k, v in data.items() if k in cls.__slots__} return cls(**supported_data) + def to_dict(self) -> dict: + """Recursively convert a domain object to a dict.""" + return _make_serializable(self) # type: ignore + def __repr__(self) -> str: kwargs = [f"{key}={getattr(self, key)!r}" for key in self.__slots__] return f"{self.__class__.__qualname__}({', '.join(kwargs)})" +def _make_serializable(value: Any) -> dict | list | str | int | float: + if isinstance(value, (BaseDomain, BoundModelBase)): + if isinstance(value, BoundModelBase): + value = value.data_model + + return {key: _make_serializable(getattr(value, key)) for key in value.__slots__} + + if isinstance(value, dict): + return {key: _make_serializable(value[key]) for key in value} + + if isinstance(value, list): + return [_make_serializable(child) for child in value] + + if isinstance(value, datetime): + return value.isoformat() + + return value + + class DomainIdentityMixin: __slots__ = () diff --git a/hcloud/datacenters/domain.py b/hcloud/datacenters/domain.py index 1c59bfa..05d5f79 100644 --- a/hcloud/datacenters/domain.py +++ b/hcloud/datacenters/domain.py @@ -36,7 +36,7 @@ def __init__( self.server_types = server_types -class DatacenterServerTypes: +class DatacenterServerTypes(BaseDomain): """DatacenterServerTypes Domain :param available: List[:class:`BoundServerTypes `] diff --git a/hcloud/firewalls/domain.py b/hcloud/firewalls/domain.py index d294fb8..22e8c51 100644 --- a/hcloud/firewalls/domain.py +++ b/hcloud/firewalls/domain.py @@ -48,7 +48,7 @@ def __init__( self.created = isoparse(created) if created else None -class FirewallRule: +class FirewallRule(BaseDomain): """Firewall Rule Domain :param direction: str @@ -125,7 +125,7 @@ def to_payload(self) -> dict[str, Any]: return payload -class FirewallResource: +class FirewallResource(BaseDomain): """Firewall Used By Domain :param type: str diff --git a/tests/unit/core/conftest.py b/tests/unit/core/conftest.py new file mode 100644 index 0000000..5919e0d --- /dev/null +++ b/tests/unit/core/conftest.py @@ -0,0 +1,193 @@ +from __future__ import annotations + +import pytest + + +@pytest.fixture() +def server_dict(): + return { + "id": 35555937, + "name": "tmp", + "status": "running", + "created": "2023-08-03T13:06:14+00:00", + "public_net": { + "ipv4": { + "ip": "95.217.11.207", + "blocked": False, + "dns_ptr": "static.207.11.217.95.clients.your-server.de", + "id": 36311011, + }, + "ipv6": { + "ip": "2a01:4f9:c012:63c9::/64", + "blocked": False, + "dns_ptr": [], + "id": 36311012, + }, + "floating_ips": [], + "firewalls": [], + }, + "private_net": [], + "server_type": { + "id": 1, + "name": "cx11", + "description": "CX11", + "cores": 1, + "memory": 2.0, + "disk": 20, + "deprecated": False, + "prices": [ + { + "location": "fsn1", + "price_hourly": { + "net": "0.0052000000", + "gross": "0.0061880000000000", + }, + "price_monthly": { + "net": "3.2900000000", + "gross": "3.9151000000000000", + }, + }, + { + "location": "hel1", + "price_hourly": { + "net": "0.0052000000", + "gross": "0.0061880000000000", + }, + "price_monthly": { + "net": "3.2900000000", + "gross": "3.9151000000000000", + }, + }, + { + "location": "nbg1", + "price_hourly": { + "net": "0.0052000000", + "gross": "0.0061880000000000", + }, + "price_monthly": { + "net": "3.2900000000", + "gross": "3.9151000000000000", + }, + }, + ], + "storage_type": "local", + "cpu_type": "shared", + "architecture": "x86", + "included_traffic": 21990232555520, + "deprecation": None, + }, + "datacenter": { + "id": 3, + "name": "hel1-dc2", + "description": "Helsinki 1 virtual DC 2", + "location": { + "id": 3, + "name": "hel1", + "description": "Helsinki DC Park 1", + "country": "FI", + "city": "Helsinki", + "latitude": 60.169855, + "longitude": 24.938379, + "network_zone": "eu-central", + }, + "server_types": { + "supported": [ + 1, + 3, + 5, + 7, + 9, + 22, + 23, + 24, + 25, + 26, + 33, + 34, + 35, + 36, + 37, + 38, + 45, + 93, + 94, + 95, + ], + "available": [ + 1, + 3, + 5, + 7, + 9, + 22, + 23, + 24, + 25, + 26, + 33, + 34, + 35, + 36, + 37, + 38, + ], + "available_for_migration": [ + 1, + 3, + 5, + 7, + 9, + 22, + 23, + 24, + 25, + 26, + 33, + 34, + 35, + 36, + 37, + 38, + 96, + 97, + 98, + 99, + 100, + 101, + ], + }, + }, + "image": { + "id": 114690387, + "type": "system", + "status": "available", + "name": "debian-12", + "description": "Debian 12", + "image_size": None, + "disk_size": 5, + "created": "2023-06-13T06:00:02+00:00", + "created_from": None, + "bound_to": None, + "os_flavor": "debian", + "os_version": "12", + "rapid_deploy": True, + "protection": {"delete": False}, + "deprecated": None, + "labels": {}, + "deleted": None, + "architecture": "x86", + }, + "iso": None, + "rescue_enabled": False, + "locked": False, + "backup_window": None, + "outgoing_traffic": None, + "ingoing_traffic": None, + "included_traffic": 21990232555520, + "protection": {"delete": False, "rebuild": False}, + "labels": {}, + "volumes": [], + # "load_balancers": [], + "primary_disk_size": 20, + "placement_group": None, + } diff --git a/tests/unit/core/test_domain.py b/tests/unit/core/test_domain.py index 456b3bb..9c7adf5 100644 --- a/tests/unit/core/test_domain.py +++ b/tests/unit/core/test_domain.py @@ -119,6 +119,44 @@ def test_from_dict_ok(self, data_dict, expected_result): for k, v in expected_result.items(): assert getattr(model, k) == v + @pytest.mark.parametrize( + "data,expected", + [ + ( + SomeOtherDomain(id=1, name="name1"), + {"id": 1, "name": "name1", "child": None}, + ), + ( + SomeOtherDomain( + id=2, name="name2", child=SomeOtherDomain(id=3, name="name3") + ), + { + "id": 2, + "name": "name2", + "child": {"id": 3, "name": "name3", "child": None}, + }, + ), + ( + SomeOtherDomain( + id=2, name="name2", child=[SomeOtherDomain(id=3, name="name3")] + ), + { + "id": 2, + "name": "name2", + "child": [{"id": 3, "name": "name3", "child": None}], + }, + ), + ], + ) + def test_to_dict_ok(self, data, expected): + assert data.to_dict() == expected + + def test_from_dict_and_to_dict_ok(self, server_dict: dict): + from hcloud.servers import Server + + server = Server.from_dict(server_dict) + assert server.to_dict() == server_dict + @pytest.mark.parametrize( "data,expected", [