diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index 314a8059..ef262384 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -1,8 +1,10 @@ from __future__ import annotations import time +from typing import NamedTuple from ..core.client import BoundModelBase, ClientEntityBase +from ..core.domain import Meta from .domain import Action, ActionFailedException, ActionTimeoutException @@ -29,9 +31,12 @@ def wait_until_finished(self, max_retries=100): raise ActionFailedException(action=self) -class ActionsClient(ClientEntityBase): - results_list_attribute_name = "actions" +class ActionsPageResult(NamedTuple): + actions: list[BoundAction] + meta: Meta | None + +class ActionsClient(ClientEntityBase): def get_by_id(self, id): # type: (int) -> BoundAction """Get a specific action by its ID. @@ -77,7 +82,7 @@ def get_list( actions = [ BoundAction(self, action_data) for action_data in response["actions"] ] - return self._add_meta_to_result(actions, response) + return ActionsPageResult(actions, Meta.parse_meta(response)) def get_all(self, status=None, sort=None): # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] diff --git a/hcloud/certificates/client.py b/hcloud/certificates/client.py index 548feb7e..2f60bd1e 100644 --- a/hcloud/certificates/client.py +++ b/hcloud/certificates/client.py @@ -1,8 +1,10 @@ from __future__ import annotations -from ..actions.client import BoundAction +from typing import NamedTuple + +from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import add_meta_to_result +from ..core.domain import Meta from .domain import ( Certificate, CreateManagedCertificateResponse, @@ -83,9 +85,12 @@ def retry_issuance(self): return self._client.retry_issuance(self) -class CertificatesClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "certificates" +class CertificatesPageResult(NamedTuple): + certificates: list[BoundCertificate] + meta: Meta | None + +class CertificatesClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundCertificate """Get a specific certificate by its ID. @@ -138,7 +143,7 @@ def get_list( for certificate_data in response["certificates"] ] - return self._add_meta_to_result(certificates, response) + return CertificatesPageResult(certificates, Meta.parse_meta(response)) def get_all(self, name=None, label_selector=None): # type: (Optional[str], Optional[str]) -> List[BoundCertificate] @@ -287,7 +292,7 @@ def get_actions_list( BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] - return add_meta_to_result(actions, response, "actions") + return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions(self, certificate, status=None, sort=None): # type: (Certificate, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] diff --git a/hcloud/core/client.py b/hcloud/core/client.py index 3224ac6b..b0bde8c1 100644 --- a/hcloud/core/client.py +++ b/hcloud/core/client.py @@ -1,7 +1,5 @@ from __future__ import annotations -from .domain import add_meta_to_result - class ClientEntityBase: max_per_page = 50 @@ -14,44 +12,25 @@ def __init__(self, client): """ self._client = client - def _is_list_attribute_implemented(self): - if self.results_list_attribute_name is None: - raise NotImplementedError( - "in order to get results list, 'results_list_attribute_name' attribute of {} has to be specified".format( - self.__class__.__name__ - ) - ) - - def _add_meta_to_result( - self, - results, # type: List[BoundModelBase] - response, # type: json - ): - # type: (...) -> PageResult - self._is_list_attribute_implemented() - return add_meta_to_result(results, response, self.results_list_attribute_name) - def _get_all( self, list_function, # type: function - results_list_attribute_name, # type: str *args, **kwargs, ): # type (...) -> List[BoundModelBase] - - page = 1 - results = [] + page = 1 while page: - page_result = list_function( + # The *PageResult tuples MUST have the following structure + # `(result: List[Bound*], meta: Meta)` + result, meta = list_function( page=page, per_page=self.max_per_page, *args, **kwargs ) - result = getattr(page_result, results_list_attribute_name) if result: results.extend(result) - meta = page_result.meta + if ( meta and meta.pagination @@ -66,17 +45,14 @@ def _get_all( def get_all(self, *args, **kwargs): # type: (...) -> List[BoundModelBase] - self._is_list_attribute_implemented() - return self._get_all( - self.get_list, self.results_list_attribute_name, *args, **kwargs - ) + return self._get_all(self.get_list, *args, **kwargs) def get_actions(self, *args, **kwargs): # type: (...) -> List[BoundModelBase] if not hasattr(self, "get_actions_list"): raise ValueError("this endpoint does not support get_actions method") - return self._get_all(self.get_actions_list, "actions", *args, **kwargs) + return self._get_all(self.get_actions_list, *args, **kwargs) class GetEntityByNameMixin: @@ -86,11 +62,8 @@ class GetEntityByNameMixin: def get_by_name(self, name): # type: (str) -> BoundModelBase - self._is_list_attribute_implemented() - response = self.get_list(name=name) - entities = getattr(response, self.results_list_attribute_name) - entity = entities[0] if entities else None - return entity + entities, _ = self.get_list(name=name) + return entities[0] if entities else None class BoundModelBase: diff --git a/hcloud/core/domain.py b/hcloud/core/domain.py index 302ca9ec..8b0a2574 100644 --- a/hcloud/core/domain.py +++ b/hcloud/core/domain.py @@ -1,7 +1,5 @@ from __future__ import annotations -from collections import namedtuple - class BaseDomain: __slots__ = () @@ -63,19 +61,13 @@ def __init__(self, pagination=None): self.pagination = pagination @classmethod - def parse_meta(cls, json_content): + def parse_meta(cls, response: dict) -> Meta | None: meta = None - if json_content and "meta" in json_content: + if response and "meta" in response: meta = cls() - pagination_json = json_content["meta"].get("pagination") - if pagination_json: - pagination = Pagination(**pagination_json) - meta.pagination = pagination - return meta + try: + meta.pagination = Pagination(**response["meta"]["pagination"]) + except KeyError: + pass - -def add_meta_to_result(result, json_content, attr_name): - # type: (List[BoundModelBase], json, string) -> PageResult - class_name = f"PageResults{attr_name.capitalize()}" - PageResults = namedtuple(class_name, [attr_name, "meta"]) - return PageResults(**{attr_name: result, "meta": Meta.parse_meta(json_content)}) + return meta diff --git a/hcloud/datacenters/client.py b/hcloud/datacenters/client.py index 9166648a..76e3c707 100644 --- a/hcloud/datacenters/client.py +++ b/hcloud/datacenters/client.py @@ -1,6 +1,9 @@ from __future__ import annotations +from typing import NamedTuple + from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import Meta from ..locations.client import BoundLocation from ..server_types.client import BoundServerType from .domain import Datacenter, DatacenterServerTypes @@ -43,9 +46,12 @@ def __init__(self, client, data): super().__init__(client, data) -class DatacentersClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "datacenters" +class DatacentersPageResult(NamedTuple): + datacenters: list[BoundDatacenter] + meta: Meta | None + +class DatacentersClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundDatacenter """Get a specific datacenter by its ID. @@ -90,7 +96,7 @@ def get_list( for datacenter_data in response["datacenters"] ] - return self._add_meta_to_result(datacenters, response) + return DatacentersPageResult(datacenters, Meta.parse_meta(response)) def get_all(self, name=None): # type: (Optional[str]) -> List[BoundDatacenter] diff --git a/hcloud/firewalls/client.py b/hcloud/firewalls/client.py index a3c76d13..118fc850 100644 --- a/hcloud/firewalls/client.py +++ b/hcloud/firewalls/client.py @@ -1,8 +1,10 @@ from __future__ import annotations -from ..actions.client import BoundAction +from typing import NamedTuple + +from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import add_meta_to_result +from ..core.domain import Meta from .domain import ( CreateFirewallResponse, Firewall, @@ -134,9 +136,12 @@ def remove_from_resources(self, resources): return self._client.remove_from_resources(self, resources) -class FirewallsClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "firewalls" +class FirewallsPageResult(NamedTuple): + firewalls: list[BoundFirewall] + meta: Meta | None + +class FirewallsClient(ClientEntityBase, GetEntityByNameMixin): def get_actions_list( self, firewall, # type: Firewall @@ -177,7 +182,7 @@ def get_actions_list( BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] - return add_meta_to_result(actions, response, "actions") + return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions( self, @@ -249,7 +254,7 @@ def get_list( for firewall_data in response["firewalls"] ] - return self._add_meta_to_result(firewalls, response) + return FirewallsPageResult(firewalls, Meta.parse_meta(response)) def get_all(self, label_selector=None, name=None, sort=None): # type: (Optional[str], Optional[str], Optional[List[str]]) -> List[BoundFirewall] diff --git a/hcloud/floating_ips/client.py b/hcloud/floating_ips/client.py index 16a1dd52..f6d56113 100644 --- a/hcloud/floating_ips/client.py +++ b/hcloud/floating_ips/client.py @@ -1,8 +1,10 @@ from __future__ import annotations -from ..actions.client import BoundAction +from typing import NamedTuple + +from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import add_meta_to_result +from ..core.domain import Meta from ..locations.client import BoundLocation from .domain import CreateFloatingIPResponse, FloatingIP @@ -119,9 +121,12 @@ def change_dns_ptr(self, ip, dns_ptr): return self._client.change_dns_ptr(self, ip, dns_ptr) -class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "floating_ips" +class FloatingIPsPageResult(NamedTuple): + floating_ips: list[BoundFloatingIP] + meta: Meta | None + +class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin): def get_actions_list( self, floating_ip, # type: FloatingIP @@ -164,7 +169,7 @@ def get_actions_list( BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] - return add_meta_to_result(actions, response, "actions") + return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions( self, @@ -234,7 +239,7 @@ def get_list( for floating_ip_data in response["floating_ips"] ] - return self._add_meta_to_result(floating_ips, response) + return FloatingIPsPageResult(floating_ips, Meta.parse_meta(response)) def get_all(self, label_selector=None, name=None): # type: (Optional[str], Optional[str]) -> List[BoundFloatingIP] diff --git a/hcloud/images/client.py b/hcloud/images/client.py index 7e099154..55729a0f 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -1,8 +1,10 @@ from __future__ import annotations -from ..actions.client import BoundAction +from typing import NamedTuple + +from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import add_meta_to_result +from ..core.domain import Meta from .domain import Image @@ -89,9 +91,12 @@ def change_protection(self, delete=None): return self._client.change_protection(self, delete) -class ImagesClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "images" +class ImagesPageResult(NamedTuple): + images: list[BoundImage] + meta: Meta | None + +class ImagesClient(ClientEntityBase, GetEntityByNameMixin): def get_actions_list( self, image, # type: Image @@ -132,7 +137,7 @@ def get_actions_list( BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] - return add_meta_to_result(actions, response, "actions") + return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions( self, @@ -224,7 +229,7 @@ def get_list( response = self._client.request(url="/images", method="GET", params=params) images = [BoundImage(self, image_data) for image_data in response["images"]] - return self._add_meta_to_result(images, response) + return ImagesPageResult(images, Meta.parse_meta(response)) def get_all( self, @@ -291,8 +296,7 @@ def get_by_name_and_architecture(self, name, architecture): Used to identify the image. :return: :class:`BoundImage ` """ - response = self.get_list(name=name, architecture=[architecture]) - entities = getattr(response, self.results_list_attribute_name) + entities, _ = self.get_list(name=name, architecture=[architecture]) entity = entities[0] if entities else None return entity diff --git a/hcloud/isos/client.py b/hcloud/isos/client.py index 9d0fcddb..fba3930e 100644 --- a/hcloud/isos/client.py +++ b/hcloud/isos/client.py @@ -1,8 +1,10 @@ from __future__ import annotations +from typing import NamedTuple from warnings import warn from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import Meta from .domain import Iso @@ -10,9 +12,12 @@ class BoundIso(BoundModelBase): model = Iso -class IsosClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "isos" +class IsosPageResult(NamedTuple): + isos: list[BoundIso] + meta: Meta | None + +class IsosClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundIso """Get a specific ISO by its id @@ -72,7 +77,7 @@ def get_list( response = self._client.request(url="/isos", method="GET", params=params) isos = [BoundIso(self, iso_data) for iso_data in response["isos"]] - return self._add_meta_to_result(isos, response) + return IsosPageResult(isos, Meta.parse_meta(response)) def get_all( self, diff --git a/hcloud/load_balancer_types/client.py b/hcloud/load_balancer_types/client.py index 12e5edd3..7dff45a3 100644 --- a/hcloud/load_balancer_types/client.py +++ b/hcloud/load_balancer_types/client.py @@ -1,6 +1,9 @@ from __future__ import annotations +from typing import NamedTuple + from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import Meta from .domain import LoadBalancerType @@ -8,9 +11,12 @@ class BoundLoadBalancerType(BoundModelBase): model = LoadBalancerType -class LoadBalancerTypesClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "load_balancer_types" +class LoadBalancerTypesPageResult(NamedTuple): + load_balancer_types: list[BoundLoadBalancerType] + meta: Meta | None + +class LoadBalancerTypesClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> load_balancer_types.client.BoundLoadBalancerType """Returns a specific Load Balancer Type. @@ -53,7 +59,9 @@ def get_list(self, name=None, page=None, per_page=None): BoundLoadBalancerType(self, load_balancer_type_data) for load_balancer_type_data in response["load_balancer_types"] ] - return self._add_meta_to_result(load_balancer_types, response) + return LoadBalancerTypesPageResult( + load_balancer_types, Meta.parse_meta(response) + ) def get_all(self, name=None): # type: (Optional[str]) -> List[BoundLoadBalancerType] diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index 4c913154..85e73d82 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -1,9 +1,11 @@ from __future__ import annotations -from ..actions.client import BoundAction +from typing import NamedTuple + +from ..actions.client import ActionsPageResult, BoundAction from ..certificates.client import BoundCertificate from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import add_meta_to_result +from ..core.domain import Meta from ..load_balancer_types.client import BoundLoadBalancerType from ..locations.client import BoundLocation from ..networks.client import BoundNetwork @@ -310,9 +312,12 @@ def change_type(self, load_balancer_type): return self._client.change_type(self, load_balancer_type) -class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "load_balancers" +class LoadBalancersPageResult(NamedTuple): + load_balancers: list[BoundLoadBalancer] + meta: Meta | None + +class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundLoadBalancer """Get a specific Load Balancer @@ -360,11 +365,11 @@ def get_list( url="/load_balancers", method="GET", params=params ) - ass_load_balancers = [ + load_balancers = [ BoundLoadBalancer(self, load_balancer_data) for load_balancer_data in response["load_balancers"] ] - return self._add_meta_to_result(ass_load_balancers, response) + return LoadBalancersPageResult(load_balancers, Meta.parse_meta(response)) def get_all(self, name=None, label_selector=None): # type: (Optional[str], Optional[str]) -> List[BoundLoadBalancer] @@ -550,7 +555,7 @@ def get_actions_list( BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] - return add_meta_to_result(actions, response, "actions") + return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions(self, load_balancer, status=None, sort=None): # type: (LoadBalancer, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] diff --git a/hcloud/locations/client.py b/hcloud/locations/client.py index 97b5fdfd..c21c6a75 100644 --- a/hcloud/locations/client.py +++ b/hcloud/locations/client.py @@ -1,6 +1,9 @@ from __future__ import annotations +from typing import NamedTuple + from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import Meta from .domain import Location @@ -8,9 +11,12 @@ class BoundLocation(BoundModelBase): model = Location -class LocationsClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "locations" +class LocationsPageResult(NamedTuple): + locations: list[BoundLocation] + meta: Meta | None + +class LocationsClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> locations.client.BoundLocation """Get a specific location by its ID. @@ -46,7 +52,7 @@ def get_list(self, name=None, page=None, per_page=None): BoundLocation(self, location_data) for location_data in response["locations"] ] - return self._add_meta_to_result(locations, response) + return LocationsPageResult(locations, Meta.parse_meta(response)) def get_all(self, name=None): # type: (Optional[str]) -> List[BoundLocation] diff --git a/hcloud/networks/client.py b/hcloud/networks/client.py index 46d25f5e..6a93c0cd 100644 --- a/hcloud/networks/client.py +++ b/hcloud/networks/client.py @@ -1,8 +1,10 @@ from __future__ import annotations -from ..actions.client import BoundAction +from typing import NamedTuple + +from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import add_meta_to_result +from ..core.domain import Meta from .domain import Network, NetworkRoute, NetworkSubnet @@ -153,9 +155,12 @@ def change_protection(self, delete=None): return self._client.change_protection(self, delete=delete) -class NetworksClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "networks" +class NetworksPageResult(NamedTuple): + networks: list[BoundNetwork] + meta: Meta | None + +class NetworksClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundNetwork """Get a specific network @@ -198,10 +203,10 @@ def get_list( response = self._client.request(url="/networks", method="GET", params=params) - ass_networks = [ + networks = [ BoundNetwork(self, network_data) for network_data in response["networks"] ] - return self._add_meta_to_result(ass_networks, response) + return NetworksPageResult(networks, Meta.parse_meta(response)) def get_all(self, name=None, label_selector=None): # type: (Optional[str], Optional[str]) -> List[BoundNetwork] @@ -359,7 +364,7 @@ def get_actions_list( BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] - return add_meta_to_result(actions, response, "actions") + return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions(self, network, status=None, sort=None): # type: (Network, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] diff --git a/hcloud/placement_groups/client.py b/hcloud/placement_groups/client.py index ca78bc82..5a97c7c5 100644 --- a/hcloud/placement_groups/client.py +++ b/hcloud/placement_groups/client.py @@ -1,7 +1,10 @@ from __future__ import annotations +from typing import NamedTuple + from ..actions.client import BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import Meta from .domain import CreatePlacementGroupResponse, PlacementGroup @@ -29,9 +32,12 @@ def delete(self): return self._client.delete(self) -class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "placement_groups" +class PlacementGroupsPageResult(NamedTuple): + placement_groups: list[BoundPlacementGroup] + meta: Meta | None + +class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundPlacementGroup """Returns a specific Placement Group object @@ -92,7 +98,7 @@ def get_list( for placement_group_data in response["placement_groups"] ] - return self._add_meta_to_result(placement_groups, response) + return PlacementGroupsPageResult(placement_groups, Meta.parse_meta(response)) def get_all(self, label_selector=None, name=None, sort=None): # type: (Optional[str], Optional[str], Optional[List[str]]) -> List[BoundPlacementGroup] diff --git a/hcloud/primary_ips/client.py b/hcloud/primary_ips/client.py index d33f0f20..717406a5 100644 --- a/hcloud/primary_ips/client.py +++ b/hcloud/primary_ips/client.py @@ -1,7 +1,10 @@ from __future__ import annotations +from typing import NamedTuple + from ..actions.client import BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import Meta from .domain import CreatePrimaryIPResponse, PrimaryIP @@ -84,9 +87,12 @@ def change_dns_ptr(self, ip, dns_ptr): return self._client.change_dns_ptr(self, ip, dns_ptr) -class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "primary_ips" +class PrimaryIPsPageResult(NamedTuple): + primary_ips: list[BoundPrimaryIP] + meta: Meta | None + +class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundPrimaryIP """Returns a specific Primary IP object. @@ -139,7 +145,7 @@ def get_list( for primary_ip_data in response["primary_ips"] ] - return self._add_meta_to_result(primary_ips, response) + return PrimaryIPsPageResult(primary_ips, Meta.parse_meta(response)) def get_all(self, label_selector=None, name=None): # type: (Optional[str], Optional[str]) -> List[BoundPrimaryIP] diff --git a/hcloud/server_types/client.py b/hcloud/server_types/client.py index 70e29e69..ed90dbb6 100644 --- a/hcloud/server_types/client.py +++ b/hcloud/server_types/client.py @@ -1,6 +1,9 @@ from __future__ import annotations +from typing import NamedTuple + from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import Meta from .domain import ServerType @@ -8,9 +11,12 @@ class BoundServerType(BoundModelBase): model = ServerType -class ServerTypesClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "server_types" +class ServerTypesPageResult(NamedTuple): + server_types: list[BoundServerType] + meta: Meta | None + +class ServerTypesClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundServerType """Returns a specific Server Type. @@ -48,7 +54,7 @@ def get_list(self, name=None, page=None, per_page=None): BoundServerType(self, server_type_data) for server_type_data in response["server_types"] ] - return self._add_meta_to_result(server_types, response) + return ServerTypesPageResult(server_types, Meta.parse_meta(response)) def get_all(self, name=None): # type: (Optional[str]) -> List[BoundServerType] diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index 99fe53b6..b9f7455e 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -1,8 +1,10 @@ from __future__ import annotations -from ..actions.client import BoundAction +from typing import NamedTuple + +from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import add_meta_to_result +from ..core.domain import Meta from ..datacenters.client import BoundDatacenter from ..firewalls.client import BoundFirewall from ..floating_ips.client import BoundFloatingIP @@ -409,9 +411,12 @@ def remove_from_placement_group(self): return self._client.remove_from_placement_group(self) -class ServersClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "servers" +class ServersPageResult(NamedTuple): + servers: list[BoundServer] + meta: Meta | None + +class ServersClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundServer """Get a specific server @@ -462,7 +467,7 @@ def get_list( ass_servers = [ BoundServer(self, server_data) for server_data in response["servers"] ] - return self._add_meta_to_result(ass_servers, response) + return ServersPageResult(ass_servers, Meta.parse_meta(response)) def get_all(self, name=None, label_selector=None, status=None): # type: (Optional[str], Optional[str], Optional[List[str]]) -> List[BoundServer] @@ -625,7 +630,7 @@ def get_actions_list( BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] - return add_meta_to_result(actions, response, "actions") + return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions(self, server, status=None, sort=None): # type: (Server, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] diff --git a/hcloud/ssh_keys/client.py b/hcloud/ssh_keys/client.py index 66c13fa5..0c89d6b6 100644 --- a/hcloud/ssh_keys/client.py +++ b/hcloud/ssh_keys/client.py @@ -1,6 +1,9 @@ from __future__ import annotations +from typing import NamedTuple + from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import Meta from .domain import SSHKey @@ -27,9 +30,12 @@ def delete(self): return self._client.delete(self) -class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "ssh_keys" +class SSHKeysPageResult(NamedTuple): + ssh_keys: list[BoundSSHKey] + meta: Meta | None + +class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundSSHKey """Get a specific SSH Key by its ID @@ -77,10 +83,10 @@ def get_list( response = self._client.request(url="/ssh_keys", method="GET", params=params) - ass_ssh_keys = [ + ssh_keys = [ BoundSSHKey(self, server_data) for server_data in response["ssh_keys"] ] - return self._add_meta_to_result(ass_ssh_keys, response) + return SSHKeysPageResult(ssh_keys, Meta.parse_meta(response)) def get_all(self, name=None, fingerprint=None, label_selector=None): # type: (Optional[str], Optional[str], Optional[str]) -> List[BoundSSHKey] diff --git a/hcloud/volumes/client.py b/hcloud/volumes/client.py index bd9dff55..687f145d 100644 --- a/hcloud/volumes/client.py +++ b/hcloud/volumes/client.py @@ -1,8 +1,10 @@ from __future__ import annotations -from ..actions.client import BoundAction +from typing import NamedTuple + +from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import add_meta_to_result +from ..core.domain import Meta from ..locations.client import BoundLocation from .domain import CreateVolumeResponse, Volume @@ -111,9 +113,12 @@ def change_protection(self, delete=None): return self._client.change_protection(self, delete) -class VolumesClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "volumes" +class VolumesPageResult(NamedTuple): + volumes: list[BoundVolume] + meta: Meta | None + +class VolumesClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> volumes.client.BoundVolume """Get a specific volume by its id @@ -158,7 +163,7 @@ def get_list( volumes = [ BoundVolume(self, volume_data) for volume_data in response["volumes"] ] - return self._add_meta_to_result(volumes, response) + return VolumesPageResult(volumes, Meta.parse_meta(response)) def get_all(self, label_selector=None, status=None): # type: (Optional[str], Optional[List[str]]) -> List[BoundVolume] @@ -277,7 +282,7 @@ def get_actions_list( BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] - return add_meta_to_result(actions, response, "actions") + return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions(self, volume, status=None, sort=None): # type: (Union[Volume, BoundVolume], Optional[List[str]], Optional[List[str]]) -> List[BoundAction] diff --git a/tests/unit/core/test_client.py b/tests/unit/core/test_client.py index 80bab5e3..6ea2cd63 100644 --- a/tests/unit/core/test_client.py +++ b/tests/unit/core/test_client.py @@ -1,11 +1,13 @@ from __future__ import annotations +from typing import Any, NamedTuple from unittest import mock import pytest +from hcloud.actions.client import ActionsPageResult from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.core.domain import BaseDomain, add_meta_to_result +from hcloud.core.domain import BaseDomain, Meta class TestBoundModelBase: @@ -84,15 +86,17 @@ class TestClientEntityBase: @pytest.fixture() def client_class_constructor(self): def constructor(json_content_function): - class CandiesClient(ClientEntityBase): - results_list_attribute_name = "candies" + class CandiesPageResult(NamedTuple): + candies: list[Any] + meta: Meta + class CandiesClient(ClientEntityBase): def get_list(self, status, page=None, per_page=None): json_content = json_content_function(page) results = [ (r, page, status, per_page) for r in json_content["candies"] ] - return self._add_meta_to_result(results, json_content) + return CandiesPageResult(results, Meta.parse_meta(json_content)) return CandiesClient(mock.MagicMock()) @@ -107,7 +111,7 @@ def get_actions_list(self, status, page=None, per_page=None): results = [ (r, page, status, per_page) for r in json_content["actions"] ] - return add_meta_to_result(results, json_content, "actions") + return ActionsPageResult(results, Meta.parse_meta(json_content)) return CandiesClient(mock.MagicMock()) @@ -205,44 +209,20 @@ def json_content_function(p): (23, 3, "sweet", 50), ] - def test_raise_exception_if_list_attribute_is_not_implemented( - self, client_class_with_actions_constructor - ): - def json_content_function(p): - return { - "actions": [10 + p, 20 + p], - "meta": { - "pagination": { - "page": p, - "per_page": 11, - "next_page": p + 1 if p < 3 else None, - } - }, - } - - candies_client = client_class_with_actions_constructor(json_content_function) - - with pytest.raises(NotImplementedError) as exception_info: - candies_client.get_all() - - error = exception_info.value - assert ( - str(error) - == "in order to get results list, 'results_list_attribute_name' attribute of CandiesClient has to be specified" - ) - class TestGetEntityByNameMixin: @pytest.fixture() def client_class_constructor(self): def constructor(json_content_function): - class CandiesClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "candies" + class CandiesPageResult(NamedTuple): + candies: list[Any] + meta: Meta + class CandiesClient(ClientEntityBase, GetEntityByNameMixin): def get_list(self, name, page=None, per_page=None): json_content = json_content_function(page) results = json_content["candies"] - return self._add_meta_to_result(results, json_content) + return CandiesPageResult(results, Meta.parse_meta(json_content)) return CandiesClient(mock.MagicMock()) diff --git a/tests/unit/core/test_domain.py b/tests/unit/core/test_domain.py index e9e06a35..97cb6f74 100644 --- a/tests/unit/core/test_domain.py +++ b/tests/unit/core/test_domain.py @@ -3,13 +3,7 @@ import pytest from dateutil.parser import isoparse -from hcloud.core.domain import ( - BaseDomain, - DomainIdentityMixin, - Meta, - Pagination, - add_meta_to_result, -) +from hcloud.core.domain import BaseDomain, DomainIdentityMixin, Meta, Pagination class TestMeta: @@ -46,27 +40,6 @@ def test_parse_meta_json_ok(self): assert result.pagination.last_page == 10 assert result.pagination.total_entries == 100 - def test_add_meta_to_result(self): - json_content = { - "meta": { - "pagination": { - "page": 2, - "per_page": 10, - "previous_page": 1, - "next_page": 3, - "last_page": 10, - "total_entries": 100, - } - } - } - result = add_meta_to_result([1, 2, 3], json_content, "id_list") - assert result.id_list == [1, 2, 3] - assert result.meta.pagination.page == 2 - assert result.meta.pagination.per_page == 10 - assert result.meta.pagination.next_page == 3 - assert result.meta.pagination.last_page == 10 - assert result.meta.pagination.total_entries == 100 - class SomeDomain(BaseDomain, DomainIdentityMixin): __slots__ = ("id", "name")