deps: update dependency hcloud to v1.27.1 (#290)

* deps: update dependency hcloud to v1.27.1

* chore: update vendored files

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: jo <ljonas@riseup.net>
This commit is contained in:
renovate[bot] 2023-08-08 18:17:22 +02:00 committed by GitHub
parent a035b7d4c2
commit ff539800aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 2489 additions and 1684 deletions

View file

@ -1,2 +1,4 @@
from __future__ import annotations
from ._client import Client # noqa from ._client import Client # noqa
from ._exceptions import APIException, HCloudException # noqa from ._exceptions import APIException, HCloudException # noqa

View file

@ -1,5 +1,7 @@
from __future__ import annotations
import time import time
from typing import Optional, Union from typing import NoReturn
try: try:
import requests import requests
@ -8,23 +10,23 @@ except ImportError:
from ._version import VERSION from ._version import VERSION
from ._exceptions import APIException from ._exceptions import APIException
from .actions.client import ActionsClient from .actions import ActionsClient
from .certificates.client import CertificatesClient from .certificates import CertificatesClient
from .datacenters.client import DatacentersClient from .datacenters import DatacentersClient
from .firewalls.client import FirewallsClient from .firewalls import FirewallsClient
from .floating_ips.client import FloatingIPsClient from .floating_ips import FloatingIPsClient
from .images.client import ImagesClient from .images import ImagesClient
from .isos.client import IsosClient from .isos import IsosClient
from .load_balancer_types.client import LoadBalancerTypesClient from .load_balancer_types import LoadBalancerTypesClient
from .load_balancers.client import LoadBalancersClient from .load_balancers import LoadBalancersClient
from .locations.client import LocationsClient from .locations import LocationsClient
from .networks.client import NetworksClient from .networks import NetworksClient
from .placement_groups.client import PlacementGroupsClient from .placement_groups import PlacementGroupsClient
from .primary_ips.client import PrimaryIPsClient from .primary_ips import PrimaryIPsClient
from .server_types.client import ServerTypesClient from .server_types import ServerTypesClient
from .servers.client import ServersClient from .servers import ServersClient
from .ssh_keys.client import SSHKeysClient from .ssh_keys import SSHKeysClient
from .volumes.client import VolumesClient from .volumes import VolumesClient
class Client: class Client:
@ -38,9 +40,10 @@ class Client:
self, self,
token: str, token: str,
api_endpoint: str = "https://api.hetzner.cloud/v1", api_endpoint: str = "https://api.hetzner.cloud/v1",
application_name: Optional[str] = None, application_name: str | None = None,
application_version: Optional[str] = None, application_version: str | None = None,
poll_interval: int = 1, poll_interval: int = 1,
timeout: float | tuple[float, float] | None = None,
): ):
"""Create an new Client instance """Create an new Client instance
@ -49,12 +52,14 @@ class Client:
:param application_name: Your application name :param application_name: Your application name
:param application_version: Your application _version :param application_version: Your application _version
:param poll_interval: Interval for polling information from Hetzner Cloud API in seconds :param poll_interval: Interval for polling information from Hetzner Cloud API in seconds
:param timeout: Requests timeout in seconds
""" """
self.token = token self.token = token
self._api_endpoint = api_endpoint self._api_endpoint = api_endpoint
self._application_name = application_name self._application_name = application_name
self._application_version = application_version self._application_version = application_version
self._requests_session = requests.Session() self._requests_session = requests.Session()
self._requests_timeout = timeout
self.poll_interval = poll_interval self.poll_interval = poll_interval
self.datacenters = DatacentersClient(self) self.datacenters = DatacentersClient(self)
@ -169,38 +174,42 @@ class Client:
} }
return headers return headers
def _raise_exception_from_response(self, response): def _raise_exception_from_response(self, response) -> NoReturn:
raise APIException( raise APIException(
code=response.status_code, code=response.status_code,
message=response.reason, message=response.reason,
details={"content": response.content}, details={"content": response.content},
) )
def _raise_exception_from_content(self, content: dict): def _raise_exception_from_content(self, content: dict) -> NoReturn:
raise APIException( raise APIException(
code=content["error"]["code"], code=content["error"]["code"],
message=content["error"]["message"], message=content["error"]["message"],
details=content["error"]["details"], details=content["error"]["details"],
) )
def request( def request( # type: ignore[no-untyped-def]
self, self,
method: str, method: str,
url: str, url: str,
tries: int = 1, tries: int = 1,
**kwargs, **kwargs,
) -> Union[bytes, dict]: ) -> dict:
"""Perform a request to the Hetzner Cloud API, wrapper around requests.request """Perform a request to the Hetzner Cloud API, wrapper around requests.request
:param method: HTTP Method to perform the Request :param method: HTTP Method to perform the Request
:param url: URL of the Endpoint :param url: URL of the Endpoint
:param tries: Tries of the request (used internally, should not be set by the user) :param tries: Tries of the request (used internally, should not be set by the user)
:param timeout: Requests timeout in seconds
:return: Response :return: Response
""" """
timeout = kwargs.pop("timeout", self._requests_timeout)
response = self._requests_session.request( response = self._requests_session.request(
method=method, method=method,
url=self._api_endpoint + url, url=self._api_endpoint + url,
headers=self._get_headers(), headers=self._get_headers(),
timeout=timeout,
**kwargs, **kwargs,
) )
@ -213,13 +222,15 @@ class Client:
if not response.ok: if not response.ok:
if content: if content:
assert isinstance(content, dict)
if content["error"]["code"] == "rate_limit_exceeded" and tries < 5: if content["error"]["code"] == "rate_limit_exceeded" and tries < 5:
time.sleep(tries * self._retry_wait_time) time.sleep(tries * self._retry_wait_time)
tries = tries + 1 tries = tries + 1
return self.request(method, url, tries, **kwargs) return self.request(method, url, tries, **kwargs)
else:
self._raise_exception_from_content(content) self._raise_exception_from_content(content)
else: else:
self._raise_exception_from_response(response) self._raise_exception_from_response(response)
return content # TODO: return an empty dict instead of an empty string when content == "".
return content # type: ignore[return-value]

View file

@ -1,3 +1,8 @@
from __future__ import annotations
from typing import Any
class HCloudException(Exception): class HCloudException(Exception):
"""There was an error while using the hcloud library""" """There was an error while using the hcloud library"""
@ -5,7 +10,7 @@ class HCloudException(Exception):
class APIException(HCloudException): class APIException(HCloudException):
"""There was an error while performing an API Request""" """There was an error while performing an API Request"""
def __init__(self, code, message, details): def __init__(self, code: int | str, message: str, details: Any):
super().__init__(message) super().__init__(message)
self.code = code self.code = code
self.message = message self.message = message

View file

@ -1 +1,3 @@
VERSION = "1.26.0" # x-release-please-version from __future__ import annotations
VERSION = "1.27.1" # x-release-please-version

View file

@ -0,0 +1,9 @@
from __future__ import annotations
from .client import ActionsClient, ActionsPageResult, BoundAction # noqa: F401
from .domain import ( # noqa: F401
Action,
ActionException,
ActionFailedException,
ActionTimeoutException,
)

View file

@ -1,13 +1,21 @@
import time from __future__ import annotations
from ..core.client import BoundModelBase, ClientEntityBase import time
from typing import TYPE_CHECKING, Any, NamedTuple
from ..core import BoundModelBase, ClientEntityBase, Meta
from .domain import Action, ActionFailedException, ActionTimeoutException from .domain import Action, ActionFailedException, ActionTimeoutException
if TYPE_CHECKING:
from .._client import Client
class BoundAction(BoundModelBase): class BoundAction(BoundModelBase):
_client: ActionsClient
model = Action model = Action
def wait_until_finished(self, max_retries=100): def wait_until_finished(self, max_retries: int = 100) -> None:
"""Wait until the specific action has status="finished" (set Client.poll_interval to specify a delay between checks) """Wait until the specific action has status="finished" (set Client.poll_interval to specify a delay between checks)
:param max_retries: int :param max_retries: int
@ -27,11 +35,15 @@ class BoundAction(BoundModelBase):
raise ActionFailedException(action=self) raise ActionFailedException(action=self)
class ActionsClient(ClientEntityBase): class ActionsPageResult(NamedTuple):
results_list_attribute_name = "actions" actions: list[BoundAction]
meta: Meta | None
def get_by_id(self, id):
# type: (int) -> BoundAction class ActionsClient(ClientEntityBase):
_client: Client
def get_by_id(self, id: int) -> BoundAction:
"""Get a specific action by its ID. """Get a specific action by its ID.
:param id: int :param id: int
@ -43,12 +55,11 @@ class ActionsClient(ClientEntityBase):
def get_list( def get_list(
self, self,
status=None, # type: Optional[List[str]] status: list[str] | None = None,
sort=None, # type: Optional[List[str]] sort: list[str] | None = None,
page=None, # type: Optional[int] page: int | None = None,
per_page=None, # type: Optional[int] per_page: int | None = None,
): ) -> ActionsPageResult:
# type: (...) -> PageResults[List[BoundAction]]
"""Get a list of actions from this account """Get a list of actions from this account
:param status: List[str] (optional) :param status: List[str] (optional)
@ -61,7 +72,7 @@ class ActionsClient(ClientEntityBase):
Specifies how many results are returned by page Specifies how many results are returned by page
:return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if status is not None: if status is not None:
params["status"] = status params["status"] = status
if sort is not None: if sort is not None:
@ -75,10 +86,13 @@ class ActionsClient(ClientEntityBase):
actions = [ actions = [
BoundAction(self, action_data) for action_data in response["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): def get_all(
# type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] self,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Get all actions of the account """Get all actions of the account
:param status: List[str] (optional) :param status: List[str] (optional)
@ -87,4 +101,4 @@ class ActionsClient(ClientEntityBase):
Specify how the results are sorted. Choices: `id` `command` `status` `progress` `started` `finished` . You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default) Specify how the results are sorted. Choices: `id` `command` `status` `progress` `started` `finished` . You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
""" """
return super().get_all(status=status, sort=sort) return self._iter_pages(self.get_list, status=status, sort=sort)

View file

@ -1,10 +1,17 @@
from __future__ import annotations
from typing import TYPE_CHECKING
try: try:
from dateutil.parser import isoparse from dateutil.parser import isoparse
except ImportError: except ImportError:
isoparse = None isoparse = None
from .._exceptions import HCloudException from .._exceptions import HCloudException
from ..core.domain import BaseDomain from ..core import BaseDomain
if TYPE_CHECKING:
from .client import BoundAction
class Action(BaseDomain): class Action(BaseDomain):
@ -40,14 +47,14 @@ class Action(BaseDomain):
def __init__( def __init__(
self, self,
id, id: int,
command=None, command: str | None = None,
status=None, status: str | None = None,
progress=None, progress: int | None = None,
started=None, started: str | None = None,
finished=None, finished: str | None = None,
resources=None, resources: list[dict] | None = None,
error=None, error: dict | None = None,
): ):
self.id = id self.id = id
self.command = command self.command = command
@ -63,7 +70,8 @@ class Action(BaseDomain):
class ActionException(HCloudException): class ActionException(HCloudException):
"""A generic action exception""" """A generic action exception"""
def __init__(self, action): def __init__(self, action: Action | BoundAction):
assert self.__doc__ is not None
message = self.__doc__ message = self.__doc__
if action.error is not None and "message" in action.error: if action.error is not None and "message" in action.error:
message += f": {action.error['message']}" message += f": {action.error['message']}"

View file

@ -0,0 +1,13 @@
from __future__ import annotations
from .client import ( # noqa: F401
BoundCertificate,
CertificatesClient,
CertificatesPageResult,
)
from .domain import ( # noqa: F401
Certificate,
CreateManagedCertificateResponse,
ManagedCertificateError,
ManagedCertificateStatus,
)

View file

@ -1,6 +1,9 @@
from ..actions.client import BoundAction from __future__ import annotations
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result from typing import TYPE_CHECKING, Any, NamedTuple
from ..actions import ActionsPageResult, BoundAction
from ..core import BoundModelBase, ClientEntityBase, Meta
from .domain import ( from .domain import (
Certificate, Certificate,
CreateManagedCertificateResponse, CreateManagedCertificateResponse,
@ -8,11 +11,16 @@ from .domain import (
ManagedCertificateStatus, ManagedCertificateStatus,
) )
if TYPE_CHECKING:
from .._client import Client
class BoundCertificate(BoundModelBase): class BoundCertificate(BoundModelBase):
_client: CertificatesClient
model = Certificate model = Certificate
def __init__(self, client, data, complete=True): def __init__(self, client: CertificatesClient, data: dict, complete: bool = True):
status = data.get("status") status = data.get("status")
if status is not None: if status is not None:
error_data = status.get("error") error_data = status.get("error")
@ -26,8 +34,13 @@ class BoundCertificate(BoundModelBase):
) )
super().__init__(client, data, complete) super().__init__(client, data, complete)
def get_actions_list(self, status=None, sort=None, page=None, per_page=None): def get_actions_list(
# type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]] self,
status: list[str] | None = None,
sort: list[str] | None = None,
page: int | None = None,
per_page: int | None = None,
) -> ActionsPageResult:
"""Returns all action objects for a Certificate. """Returns all action objects for a Certificate.
:param status: List[str] (optional) :param status: List[str] (optional)
@ -42,8 +55,11 @@ class BoundCertificate(BoundModelBase):
""" """
return self._client.get_actions_list(self, status, sort, page, per_page) return self._client.get_actions_list(self, status, sort, page, per_page)
def get_actions(self, status=None, sort=None): def get_actions(
# type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] self,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a Certificate. """Returns all action objects for a Certificate.
:param status: List[str] (optional) :param status: List[str] (optional)
@ -54,8 +70,11 @@ class BoundCertificate(BoundModelBase):
""" """
return self._client.get_actions(self, status, sort) return self._client.get_actions(self, status, sort)
def update(self, name=None, labels=None): def update(
# type: (Optional[str], Optional[Dict[str, str]]) -> BoundCertificate self,
name: str | None = None,
labels: dict[str, str] | None = None,
) -> BoundCertificate:
"""Updates an certificate. You can update an certificate name and the certificate labels. """Updates an certificate. You can update an certificate name and the certificate labels.
:param name: str (optional) :param name: str (optional)
@ -66,26 +85,28 @@ class BoundCertificate(BoundModelBase):
""" """
return self._client.update(self, name, labels) return self._client.update(self, name, labels)
def delete(self): def delete(self) -> bool:
# type: () -> bool
"""Deletes a certificate. """Deletes a certificate.
:return: boolean :return: boolean
""" """
return self._client.delete(self) return self._client.delete(self)
def retry_issuance(self): def retry_issuance(self) -> BoundAction:
# type: () -> BoundAction
"""Retry a failed Certificate issuance or renewal. """Retry a failed Certificate issuance or renewal.
:return: BoundAction :return: BoundAction
""" """
return self._client.retry_issuance(self) return self._client.retry_issuance(self)
class CertificatesClient(ClientEntityBase, GetEntityByNameMixin): class CertificatesPageResult(NamedTuple):
results_list_attribute_name = "certificates" certificates: list[BoundCertificate]
meta: Meta | None
def get_by_id(self, id):
# type: (int) -> BoundCertificate class CertificatesClient(ClientEntityBase):
_client: Client
def get_by_id(self, id: int) -> BoundCertificate:
"""Get a specific certificate by its ID. """Get a specific certificate by its ID.
:param id: int :param id: int
@ -96,12 +117,11 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
def get_list( def get_list(
self, self,
name=None, # type: Optional[str] name: str | None = None,
label_selector=None, # type: Optional[str] label_selector: str | None = None,
page=None, # type: Optional[int] page: int | None = None,
per_page=None, # type: Optional[int] per_page: int | None = None,
): ) -> CertificatesPageResult:
# type: (...) -> PageResults[List[BoundCertificate], Meta]
"""Get a list of certificates """Get a list of certificates
:param name: str (optional) :param name: str (optional)
@ -114,7 +134,7 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page Specifies how many results are returned by page
:return: (List[:class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if name is not None: if name is not None:
params["name"] = name params["name"] = name
@ -136,10 +156,13 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
for certificate_data in response["certificates"] 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): def get_all(
# type: (Optional[str], Optional[str]) -> List[BoundCertificate] self,
name: str | None = None,
label_selector: str | None = None,
) -> list[BoundCertificate]:
"""Get all certificates """Get all certificates
:param name: str (optional) :param name: str (optional)
@ -148,20 +171,24 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
Can be used to filter certificates by labels. The response will only contain certificates matching the label selector. Can be used to filter certificates by labels. The response will only contain certificates matching the label selector.
:return: List[:class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>`] :return: List[:class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>`]
""" """
return super().get_all(name=name, label_selector=label_selector) return self._iter_pages(self.get_list, name=name, label_selector=label_selector)
def get_by_name(self, name): def get_by_name(self, name: str) -> BoundCertificate | None:
# type: (str) -> BoundCertificate
"""Get certificate by name """Get certificate by name
:param name: str :param name: str
Used to get certificate by name. Used to get certificate by name.
:return: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` :return: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>`
""" """
return super().get_by_name(name) return self._get_first_by(name=name)
def create(self, name, certificate, private_key, labels=None): def create(
# type: (str, str, str, Optional[Dict[str, str]]) -> BoundCertificate self,
name: str,
certificate: str,
private_key: str,
labels: dict[str, str] | None = None,
) -> BoundCertificate:
"""Creates a new Certificate with the given name, certificate and private_key. This methods allows only creating """Creates a new Certificate with the given name, certificate and private_key. This methods allows only creating
custom uploaded certificates. If you want to create a managed certificate use :func:`~hcloud.certificates.client.CertificatesClient.create_managed` custom uploaded certificates. If you want to create a managed certificate use :func:`~hcloud.certificates.client.CertificatesClient.create_managed`
@ -174,7 +201,7 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
User-defined labels (key-value pairs) User-defined labels (key-value pairs)
:return: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` :return: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>`
""" """
data = { data: dict[str, Any] = {
"name": name, "name": name,
"certificate": certificate, "certificate": certificate,
"private_key": private_key, "private_key": private_key,
@ -185,8 +212,12 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
response = self._client.request(url="/certificates", method="POST", json=data) response = self._client.request(url="/certificates", method="POST", json=data)
return BoundCertificate(self, response["certificate"]) return BoundCertificate(self, response["certificate"])
def create_managed(self, name, domain_names, labels=None): def create_managed(
# type: (str, List[str], Optional[Dict[str, str]]) -> CreateManagedCertificateResponse self,
name: str,
domain_names: list[str],
labels: dict[str, str] | None = None,
) -> CreateManagedCertificateResponse:
"""Creates a new managed Certificate with the given name and domain names. This methods allows only creating """Creates a new managed Certificate with the given name and domain names. This methods allows only creating
managed certificates for domains that are using the Hetzner DNS service. If you want to create a custom uploaded certificate use :func:`~hcloud.certificates.client.CertificatesClient.create` managed certificates for domains that are using the Hetzner DNS service. If you want to create a custom uploaded certificate use :func:`~hcloud.certificates.client.CertificatesClient.create`
@ -197,7 +228,7 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
User-defined labels (key-value pairs) User-defined labels (key-value pairs)
:return: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` :return: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>`
""" """
data = { data: dict[str, Any] = {
"name": name, "name": name,
"type": Certificate.TYPE_MANAGED, "type": Certificate.TYPE_MANAGED,
"domain_names": domain_names, "domain_names": domain_names,
@ -210,8 +241,12 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
action=BoundAction(self._client.actions, response["action"]), action=BoundAction(self._client.actions, response["action"]),
) )
def update(self, certificate, name=None, labels=None): def update(
# type: (Certificate, Optional[str], Optional[Dict[str, str]]) -> BoundCertificate self,
certificate: Certificate | BoundCertificate,
name: str | None = None,
labels: dict[str, str] | None = None,
) -> BoundCertificate:
"""Updates a Certificate. You can update a certificate name and labels. """Updates a Certificate. You can update a certificate name and labels.
:param certificate: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.Certificate>` :param certificate: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.Certificate>`
@ -221,7 +256,7 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
User-defined labels (key-value pairs) User-defined labels (key-value pairs)
:return: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` :return: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>`
""" """
data = {} data: dict[str, Any] = {}
if name is not None: if name is not None:
data["name"] = name data["name"] = name
if labels is not None: if labels is not None:
@ -233,24 +268,27 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
) )
return BoundCertificate(self, response["certificate"]) return BoundCertificate(self, response["certificate"])
def delete(self, certificate): def delete(self, certificate: Certificate | BoundCertificate) -> bool:
# type: (Certificate) -> bool
self._client.request(
url=f"/certificates/{certificate.id}",
method="DELETE",
)
"""Deletes a certificate. """Deletes a certificate.
:param certificate: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.Certificate>` :param certificate: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.Certificate>`
:return: True :return: True
""" """
self._client.request(
url=f"/certificates/{certificate.id}",
method="DELETE",
)
# Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised
return True return True
def get_actions_list( def get_actions_list(
self, certificate, status=None, sort=None, page=None, per_page=None self,
): certificate: Certificate | BoundCertificate,
# type: (Certificate, Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta] status: list[str] | None = None,
sort: list[str] | None = None,
page: int | None = None,
per_page: int | None = None,
) -> ActionsPageResult:
"""Returns all action objects for a Certificate. """Returns all action objects for a Certificate.
:param certificate: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.Certificate>` :param certificate: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.Certificate>`
@ -264,7 +302,7 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page Specifies how many results are returned by page
:return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if status is not None: if status is not None:
params["status"] = status params["status"] = status
if sort is not None: if sort is not None:
@ -275,9 +313,7 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
params["per_page"] = per_page params["per_page"] = per_page
response = self._client.request( response = self._client.request(
url="/certificates/{certificate_id}/actions".format( url=f"/certificates/{certificate.id}/actions",
certificate_id=certificate.id
),
method="GET", method="GET",
params=params, params=params,
) )
@ -285,10 +321,14 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
BoundAction(self._client.actions, action_data) BoundAction(self._client.actions, action_data)
for action_data in response["actions"] 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): def get_actions(
# type: (Certificate, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] self,
certificate: Certificate | BoundCertificate,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a Certificate. """Returns all action objects for a Certificate.
:param certificate: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.Certificate>` :param certificate: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.Certificate>`
@ -298,19 +338,24 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc`
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
""" """
return super().get_actions(certificate, status=status, sort=sort) return self._iter_pages(
self.get_actions_list,
certificate,
status=status,
sort=sort,
)
def retry_issuance(self, certificate): def retry_issuance(
# type: (Certificate) -> BoundAction self,
certificate: Certificate | BoundCertificate,
) -> BoundAction:
"""Returns all action objects for a Certificate. """Returns all action objects for a Certificate.
:param certificate: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.Certificate>` :param certificate: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.Certificate>`
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
response = self._client.request( response = self._client.request(
url="/certificates/{certificate_id}/actions/retry".format( url=f"/certificates/{certificate.id}/actions/retry",
certificate_id=certificate.id
),
method="POST", method="POST",
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])

View file

@ -1,9 +1,17 @@
from __future__ import annotations
from typing import TYPE_CHECKING
try: try:
from dateutil.parser import isoparse from dateutil.parser import isoparse
except ImportError: except ImportError:
isoparse = None isoparse = None
from ..core.domain import BaseDomain, DomainIdentityMixin from ..core import BaseDomain, DomainIdentityMixin
if TYPE_CHECKING:
from ..actions import BoundAction
from .client import BoundCertificate
class Certificate(BaseDomain, DomainIdentityMixin): class Certificate(BaseDomain, DomainIdentityMixin):
@ -44,17 +52,17 @@ class Certificate(BaseDomain, DomainIdentityMixin):
def __init__( def __init__(
self, self,
id=None, id: int | None = None,
name=None, name: str | None = None,
certificate=None, certificate: str | None = None,
not_valid_before=None, not_valid_before: str | None = None,
not_valid_after=None, not_valid_after: str | None = None,
domain_names=None, domain_names: list[str] | None = None,
fingerprint=None, fingerprint: str | None = None,
created=None, created: str | None = None,
labels=None, labels: dict[str, str] | None = None,
type=None, type: str | None = None,
status=None, status: ManagedCertificateStatus | None = None,
): ):
self.id = id self.id = id
self.name = name self.name = name
@ -80,7 +88,12 @@ class ManagedCertificateStatus(BaseDomain):
If issuance or renewal reports failure, this property contains information about what happened If issuance or renewal reports failure, this property contains information about what happened
""" """
def __init__(self, issuance=None, renewal=None, error=None): def __init__(
self,
issuance: str | None = None,
renewal: str | None = None,
error: ManagedCertificateError | None = None,
):
self.issuance = issuance self.issuance = issuance
self.renewal = renewal self.renewal = renewal
self.error = error self.error = error
@ -95,7 +108,7 @@ class ManagedCertificateError(BaseDomain):
Message detailing the error Message detailing the error
""" """
def __init__(self, code=None, message=None): def __init__(self, code: str | None = None, message: str | None = None):
self.code = code self.code = code
self.message = message self.message = message
@ -113,8 +126,8 @@ class CreateManagedCertificateResponse(BaseDomain):
def __init__( def __init__(
self, self,
certificate, # type: BoundCertificate certificate: BoundCertificate,
action, # type: BoundAction action: BoundAction,
): ):
self.certificate = certificate self.certificate = certificate
self.action = action self.action = action

View file

@ -0,0 +1,4 @@
from __future__ import annotations
from .client import BoundModelBase, ClientEntityBase # noqa: F401
from .domain import BaseDomain, DomainIdentityMixin, Meta, Pagination # noqa: F401

View file

@ -1,102 +1,65 @@
from .domain import add_meta_to_result from __future__ import annotations
from typing import TYPE_CHECKING, Any, Callable
if TYPE_CHECKING:
from .._client import Client
class ClientEntityBase: class ClientEntityBase:
max_per_page = 50 _client: Client
results_list_attribute_name = None
def __init__(self, client): max_per_page: int = 50
def __init__(self, client: Client):
""" """
:param client: Client :param client: Client
:return self :return self
""" """
self._client = client self._client = client
def _is_list_attribute_implemented(self): def _iter_pages( # type: ignore[no-untyped-def]
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, self,
results, # type: List[BoundModelBase] list_function: Callable,
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, *args,
**kwargs **kwargs,
): ) -> list:
# type (...) -> List[BoundModelBase]
page = 1
results = [] results = []
page = 1
while page: while page:
page_result = list_function( # The *PageResult tuples MUST have the following structure
page=page, per_page=self.max_per_page, *args, **kwargs # `(result: List[Bound*], meta: Meta)`
result, meta = list_function(
*args, page=page, per_page=self.max_per_page, **kwargs
) )
result = getattr(page_result, results_list_attribute_name)
if result: if result:
results.extend(result) results.extend(result)
meta = page_result.meta
if ( if meta and meta.pagination and meta.pagination.next_page:
meta
and meta.pagination
and meta.pagination.next_page
and meta.pagination.next_page
):
page = meta.pagination.next_page page = meta.pagination.next_page
else: else:
page = None page = 0
return results return results
def get_all(self, *args, **kwargs): def _get_first_by(self, **kwargs): # type: ignore[no-untyped-def]
# type: (...) -> List[BoundModelBase] assert hasattr(self, "get_list")
self._is_list_attribute_implemented() entities, _ = self.get_list(**kwargs)
return self._get_all( return entities[0] if entities else None
self.get_list, self.results_list_attribute_name, *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)
class GetEntityByNameMixin:
"""
Use as a mixin for ClientEntityBase classes
"""
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
class BoundModelBase: class BoundModelBase:
"""Bound Model Base""" """Bound Model Base"""
model = None model: Any
def __init__(self, client, data={}, complete=True): def __init__(
self,
client: ClientEntityBase,
data: dict,
complete: bool = True,
):
""" """
:param client: :param client:
The client for the specific model to use The client for the specific model to use
@ -109,7 +72,7 @@ class BoundModelBase:
self.complete = complete self.complete = complete
self.data_model = self.model.from_dict(data) self.data_model = self.model.from_dict(data)
def __getattr__(self, name): def __getattr__(self, name: str): # type: ignore[no-untyped-def]
"""Allow magical access to the properties of the model """Allow magical access to the properties of the model
:param name: str :param name: str
:return: :return:
@ -120,8 +83,9 @@ class BoundModelBase:
value = getattr(self.data_model, name) value = getattr(self.data_model, name)
return value return value
def reload(self): def reload(self) -> None:
"""Reloads the model and tries to get all data from the APIx""" """Reloads the model and tries to get all data from the APIx"""
assert hasattr(self._client, "get_by_id")
bound_model = self._client.get_by_id(self.data_model.id) bound_model = self._client.get_by_id(self.data_model.id)
self.data_model = bound_model.data_model self.data_model = bound_model.data_model
self.complete = True self.complete = True

View file

@ -1,24 +1,27 @@
from collections import namedtuple from __future__ import annotations
class BaseDomain: class BaseDomain:
__slots__ = () __slots__ = ()
@classmethod @classmethod
def from_dict(cls, data): 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__} supported_data = {k: v for k, v in data.items() if k in cls.__slots__}
return cls(**supported_data) return cls(**supported_data)
def __repr__(self) -> str: def __repr__(self) -> str:
kwargs = [f"{key}={getattr(self, key)!r}" for key in self.__slots__] kwargs = [f"{key}={getattr(self, key)!r}" for key in self.__slots__] # type: ignore[var-annotated]
return f"{self.__class__.__qualname__}({', '.join(kwargs)})" return f"{self.__class__.__qualname__}({', '.join(kwargs)})"
class DomainIdentityMixin: class DomainIdentityMixin:
__slots__ = () __slots__ = ()
id: int | None
name: str | None
@property @property
def id_or_name(self): def id_or_name(self) -> int | str:
if self.id is not None: if self.id is not None:
return self.id return self.id
elif self.name is not None: elif self.name is not None:
@ -39,12 +42,12 @@ class Pagination(BaseDomain):
def __init__( def __init__(
self, self,
page, page: int,
per_page, per_page: int,
previous_page=None, previous_page: int | None = None,
next_page=None, next_page: int | None = None,
last_page=None, last_page: int | None = None,
total_entries=None, total_entries: int | None = None,
): ):
self.page = page self.page = page
self.per_page = per_page self.per_page = per_page
@ -57,23 +60,17 @@ class Pagination(BaseDomain):
class Meta(BaseDomain): class Meta(BaseDomain):
__slots__ = ("pagination",) __slots__ = ("pagination",)
def __init__(self, pagination=None): def __init__(self, pagination: Pagination | None = None):
self.pagination = pagination self.pagination = pagination
@classmethod @classmethod
def parse_meta(cls, json_content): def parse_meta(cls, response: dict) -> Meta | None:
meta = None meta = None
if json_content and "meta" in json_content: if response and "meta" in response:
meta = cls() meta = cls()
pagination_json = json_content["meta"].get("pagination") try:
if pagination_json: meta.pagination = Pagination(**response["meta"]["pagination"])
pagination = Pagination(**pagination_json) except KeyError:
meta.pagination = pagination pass
return meta return meta
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)})

View file

@ -0,0 +1,8 @@
from __future__ import annotations
from .client import ( # noqa: F401
BoundDatacenter,
DatacentersClient,
DatacentersPageResult,
)
from .domain import Datacenter, DatacenterServerTypes # noqa: F401

View file

@ -1,13 +1,22 @@
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from __future__ import annotations
from ..locations.client import BoundLocation
from ..server_types.client import BoundServerType from typing import TYPE_CHECKING, Any, NamedTuple
from ..core import BoundModelBase, ClientEntityBase, Meta
from ..locations import BoundLocation
from ..server_types import BoundServerType
from .domain import Datacenter, DatacenterServerTypes from .domain import Datacenter, DatacenterServerTypes
if TYPE_CHECKING:
from .._client import Client
class BoundDatacenter(BoundModelBase): class BoundDatacenter(BoundModelBase):
_client: DatacentersClient
model = Datacenter model = Datacenter
def __init__(self, client, data): def __init__(self, client: DatacentersClient, data: dict):
location = data.get("location") location = data.get("location")
if location is not None: if location is not None:
data["location"] = BoundLocation(client._client.locations, location) data["location"] = BoundLocation(client._client.locations, location)
@ -41,11 +50,15 @@ class BoundDatacenter(BoundModelBase):
super().__init__(client, data) super().__init__(client, data)
class DatacentersClient(ClientEntityBase, GetEntityByNameMixin): class DatacentersPageResult(NamedTuple):
results_list_attribute_name = "datacenters" datacenters: list[BoundDatacenter]
meta: Meta | None
def get_by_id(self, id):
# type: (int) -> BoundDatacenter class DatacentersClient(ClientEntityBase):
_client: Client
def get_by_id(self, id: int) -> BoundDatacenter:
"""Get a specific datacenter by its ID. """Get a specific datacenter by its ID.
:param id: int :param id: int
@ -56,11 +69,10 @@ class DatacentersClient(ClientEntityBase, GetEntityByNameMixin):
def get_list( def get_list(
self, self,
name=None, # type: Optional[str] name: str | None = None,
page=None, # type: Optional[int] page: int | None = None,
per_page=None, # type: Optional[int] per_page: int | None = None,
): ) -> DatacentersPageResult:
# type: (...) -> PageResults[List[BoundDatacenter], Meta]
"""Get a list of datacenters """Get a list of datacenters
:param name: str (optional) :param name: str (optional)
@ -71,7 +83,7 @@ class DatacentersClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page Specifies how many results are returned by page
:return: (List[:class:`BoundDatacenter <hcloud.datacenters.client.BoundDatacenter>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundDatacenter <hcloud.datacenters.client.BoundDatacenter>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if name is not None: if name is not None:
params["name"] = name params["name"] = name
@ -88,24 +100,22 @@ class DatacentersClient(ClientEntityBase, GetEntityByNameMixin):
for datacenter_data in response["datacenters"] 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): def get_all(self, name: str | None = None) -> list[BoundDatacenter]:
# type: (Optional[str]) -> List[BoundDatacenter]
"""Get all datacenters """Get all datacenters
:param name: str (optional) :param name: str (optional)
Can be used to filter datacenters by their name. Can be used to filter datacenters by their name.
:return: List[:class:`BoundDatacenter <hcloud.datacenters.client.BoundDatacenter>`] :return: List[:class:`BoundDatacenter <hcloud.datacenters.client.BoundDatacenter>`]
""" """
return super().get_all(name=name) return self._iter_pages(self.get_list, name=name)
def get_by_name(self, name): def get_by_name(self, name: str) -> BoundDatacenter | None:
# type: (str) -> BoundDatacenter
"""Get datacenter by name """Get datacenter by name
:param name: str :param name: str
Used to get datacenter by name. Used to get datacenter by name.
:return: :class:`BoundDatacenter <hcloud.datacenters.client.BoundDatacenter>` :return: :class:`BoundDatacenter <hcloud.datacenters.client.BoundDatacenter>`
""" """
return super().get_by_name(name) return self._get_first_by(name=name)

View file

@ -1,4 +1,12 @@
from ..core.domain import BaseDomain, DomainIdentityMixin from __future__ import annotations
from typing import TYPE_CHECKING
from ..core import BaseDomain, DomainIdentityMixin
if TYPE_CHECKING:
from ..locations import Location
from ..server_types import BoundServerType
class Datacenter(BaseDomain, DomainIdentityMixin): class Datacenter(BaseDomain, DomainIdentityMixin):
@ -14,7 +22,12 @@ class Datacenter(BaseDomain, DomainIdentityMixin):
__slots__ = ("id", "name", "description", "location", "server_types") __slots__ = ("id", "name", "description", "location", "server_types")
def __init__( def __init__(
self, id=None, name=None, description=None, location=None, server_types=None self,
id: int | None = None,
name: str | None = None,
description: str | None = None,
location: Location | None = None,
server_types: DatacenterServerTypes | None = None,
): ):
self.id = id self.id = id
self.name = name self.name = name
@ -36,7 +49,12 @@ class DatacenterServerTypes:
__slots__ = ("available", "supported", "available_for_migration") __slots__ = ("available", "supported", "available_for_migration")
def __init__(self, available, supported, available_for_migration): def __init__(
self,
available: list[BoundServerType],
supported: list[BoundServerType],
available_for_migration: list[BoundServerType],
):
self.available = available self.available = available
self.supported = supported self.supported = supported
self.available_for_migration = available_for_migration self.available_for_migration = available_for_migration

View file

@ -0,0 +1,3 @@
from __future__ import annotations
from .domain import DeprecationInfo # noqa: F401

View file

@ -1,9 +1,11 @@
from __future__ import annotations
try: try:
from dateutil.parser import isoparse from dateutil.parser import isoparse
except ImportError: except ImportError:
isoparse = None isoparse = None
from ..core.domain import BaseDomain from ..core import BaseDomain
class DeprecationInfo(BaseDomain): class DeprecationInfo(BaseDomain):
@ -25,8 +27,8 @@ class DeprecationInfo(BaseDomain):
def __init__( def __init__(
self, self,
announced=None, announced: str | None = None,
unavailable_after=None, unavailable_after: str | None = None,
): ):
self.announced = isoparse(announced) if announced else None self.announced = isoparse(announced) if announced else None
self.unavailable_after = ( self.unavailable_after = (

View file

@ -0,0 +1,10 @@
from __future__ import annotations
from .client import BoundFirewall, FirewallsClient, FirewallsPageResult # noqa: F401
from .domain import ( # noqa: F401
CreateFirewallResponse,
Firewall,
FirewallResource,
FirewallResourceLabelSelector,
FirewallRule,
)

View file

@ -1,6 +1,9 @@
from ..actions.client import BoundAction from __future__ import annotations
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result from typing import TYPE_CHECKING, Any, NamedTuple
from ..actions import ActionsPageResult, BoundAction
from ..core import BoundModelBase, ClientEntityBase, Meta
from .domain import ( from .domain import (
CreateFirewallResponse, CreateFirewallResponse,
Firewall, Firewall,
@ -9,11 +12,16 @@ from .domain import (
FirewallRule, FirewallRule,
) )
if TYPE_CHECKING:
from .._client import Client
class BoundFirewall(BoundModelBase): class BoundFirewall(BoundModelBase):
_client: FirewallsClient
model = Firewall model = Firewall
def __init__(self, client, data, complete=True): def __init__(self, client: FirewallsClient, data: dict, complete: bool = True):
rules = data.get("rules", []) rules = data.get("rules", [])
if rules: if rules:
rules = [ rules = [
@ -31,7 +39,7 @@ class BoundFirewall(BoundModelBase):
applied_to = data.get("applied_to", []) applied_to = data.get("applied_to", [])
if applied_to: if applied_to:
from ..servers.client import BoundServer from ..servers import BoundServer
ats = [] ats = []
for a in applied_to: for a in applied_to:
@ -57,8 +65,13 @@ class BoundFirewall(BoundModelBase):
super().__init__(client, data, complete) super().__init__(client, data, complete)
def get_actions_list(self, status=None, sort=None, page=None, per_page=None): def get_actions_list(
# type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResult[BoundAction, Meta] self,
status: list[str] | None = None,
sort: list[str] | None = None,
page: int | None = None,
per_page: int | None = None,
) -> ActionsPageResult:
"""Returns all action objects for a Firewall. """Returns all action objects for a Firewall.
:param status: List[str] (optional) :param status: List[str] (optional)
@ -73,8 +86,11 @@ class BoundFirewall(BoundModelBase):
""" """
return self._client.get_actions_list(self, status, sort, page, per_page) return self._client.get_actions_list(self, status, sort, page, per_page)
def get_actions(self, status=None, sort=None): def get_actions(
# type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] self,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a Firewall. """Returns all action objects for a Firewall.
:param status: List[str] (optional) :param status: List[str] (optional)
@ -86,8 +102,11 @@ class BoundFirewall(BoundModelBase):
""" """
return self._client.get_actions(self, status, sort) return self._client.get_actions(self, status, sort)
def update(self, name=None, labels=None): def update(
# type: (Optional[str], Optional[Dict[str, str]], Optional[str]) -> BoundFirewall self,
name: str | None = None,
labels: dict[str, str] | None = None,
) -> BoundFirewall:
"""Updates the name or labels of a Firewall. """Updates the name or labels of a Firewall.
:param labels: Dict[str, str] (optional) :param labels: Dict[str, str] (optional)
@ -98,16 +117,14 @@ class BoundFirewall(BoundModelBase):
""" """
return self._client.update(self, labels, name) return self._client.update(self, labels, name)
def delete(self): def delete(self) -> bool:
# type: () -> bool
"""Deletes a Firewall. """Deletes a Firewall.
:return: boolean :return: boolean
""" """
return self._client.delete(self) return self._client.delete(self)
def set_rules(self, rules): def set_rules(self, rules: list[FirewallRule]) -> list[BoundAction]:
# type: (List[FirewallRule]) -> List[BoundAction]
"""Sets the rules of a Firewall. All existing rules will be overwritten. Pass an empty rules array to remove all rules. """Sets the rules of a Firewall. All existing rules will be overwritten. Pass an empty rules array to remove all rules.
:param rules: List[:class:`FirewallRule <hcloud.firewalls.domain.FirewallRule>`] :param rules: List[:class:`FirewallRule <hcloud.firewalls.domain.FirewallRule>`]
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
@ -115,16 +132,20 @@ class BoundFirewall(BoundModelBase):
return self._client.set_rules(self, rules) return self._client.set_rules(self, rules)
def apply_to_resources(self, resources): def apply_to_resources(
# type: (List[FirewallResource]) -> List[BoundAction] self,
resources: list[FirewallResource],
) -> list[BoundAction]:
"""Applies one Firewall to multiple resources. """Applies one Firewall to multiple resources.
:param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`] :param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`]
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
""" """
return self._client.apply_to_resources(self, resources) return self._client.apply_to_resources(self, resources)
def remove_from_resources(self, resources): def remove_from_resources(
# type: (List[FirewallResource]) -> List[BoundAction] self,
resources: list[FirewallResource],
) -> list[BoundAction]:
"""Removes one Firewall from multiple resources. """Removes one Firewall from multiple resources.
:param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`] :param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`]
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
@ -132,18 +153,22 @@ class BoundFirewall(BoundModelBase):
return self._client.remove_from_resources(self, resources) return self._client.remove_from_resources(self, resources)
class FirewallsClient(ClientEntityBase, GetEntityByNameMixin): class FirewallsPageResult(NamedTuple):
results_list_attribute_name = "firewalls" firewalls: list[BoundFirewall]
meta: Meta | None
class FirewallsClient(ClientEntityBase):
_client: Client
def get_actions_list( def get_actions_list(
self, self,
firewall, # type: Firewall firewall: Firewall | BoundFirewall,
status=None, # type: Optional[List[str]] status: list[str] | None = None,
sort=None, # type: Optional[List[str]] sort: list[str] | None = None,
page=None, # type: Optional[int] page: int | None = None,
per_page=None, # type: Optional[int] per_page: int | None = None,
): ) -> ActionsPageResult:
# type: (...) -> PageResults[List[BoundAction], Meta]
"""Returns all action objects for a Firewall. """Returns all action objects for a Firewall.
:param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>` :param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>`
@ -157,7 +182,7 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page Specifies how many results are returned by page
:return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if status is not None: if status is not None:
params["status"] = status params["status"] = status
if sort is not None: if sort is not None:
@ -175,15 +200,14 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
BoundAction(self._client.actions, action_data) BoundAction(self._client.actions, action_data)
for action_data in response["actions"] for action_data in response["actions"]
] ]
return add_meta_to_result(actions, response, "actions") return ActionsPageResult(actions, Meta.parse_meta(response))
def get_actions( def get_actions(
self, self,
firewall, # type: Firewall firewall: Firewall | BoundFirewall,
status=None, # type: Optional[List[str]] status: list[str] | None = None,
sort=None, # type: Optional[List[str]] sort: list[str] | None = None,
): ) -> list[BoundAction]:
# type: (...) -> List[BoundAction]
"""Returns all action objects for a Firewall. """Returns all action objects for a Firewall.
:param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>` :param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>`
@ -194,10 +218,14 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
""" """
return super().get_actions(firewall, status=status, sort=sort) return self._iter_pages(
self.get_actions_list,
firewall,
status=status,
sort=sort,
)
def get_by_id(self, id): def get_by_id(self, id: int) -> BoundFirewall:
# type: (int) -> BoundFirewall
"""Returns a specific Firewall object. """Returns a specific Firewall object.
:param id: int :param id: int
@ -208,13 +236,12 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
def get_list( def get_list(
self, self,
label_selector=None, # type: Optional[str] label_selector: str | None = None,
page=None, # type: Optional[int] page: int | None = None,
per_page=None, # type: Optional[int] per_page: int | None = None,
name=None, # type: Optional[str] name: str | None = None,
sort=None, # type: Optional[List[str]] sort: list[str] | None = None,
): ) -> FirewallsPageResult:
# type: (...) -> PageResults[List[BoundFirewall]]
"""Get a list of floating ips from this account """Get a list of floating ips from this account
:param label_selector: str (optional) :param label_selector: str (optional)
@ -229,7 +256,7 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default))
:return: (List[:class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if label_selector is not None: if label_selector is not None:
params["label_selector"] = label_selector params["label_selector"] = label_selector
@ -247,10 +274,14 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
for firewall_data in response["firewalls"] 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): def get_all(
# type: (Optional[str], Optional[str], Optional[List[str]]) -> List[BoundFirewall] self,
label_selector: str | None = None,
name: str | None = None,
sort: list[str] | None = None,
) -> list[BoundFirewall]:
"""Get all floating ips from this account """Get all floating ips from this account
:param label_selector: str (optional) :param label_selector: str (optional)
@ -261,26 +292,29 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default))
:return: List[:class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>`] :return: List[:class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>`]
""" """
return super().get_all(label_selector=label_selector, name=name, sort=sort) return self._iter_pages(
self.get_list,
label_selector=label_selector,
name=name,
sort=sort,
)
def get_by_name(self, name): def get_by_name(self, name: str) -> BoundFirewall | None:
# type: (str) -> BoundFirewall
"""Get Firewall by name """Get Firewall by name
:param name: str :param name: str
Used to get Firewall by name. Used to get Firewall by name.
:return: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` :return: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>`
""" """
return super().get_by_name(name) return self._get_first_by(name=name)
def create( def create(
self, self,
name, # type: str name: str,
rules=None, # type: Optional[List[FirewallRule]] rules: list[FirewallRule] | None = None,
labels=None, # type: Optional[str] labels: str | None = None,
resources=None, # type: Optional[List[FirewallResource]] resources: list[FirewallResource] | None = None,
): ) -> CreateFirewallResponse:
# type: (...) -> CreateFirewallResponse
"""Creates a new Firewall. """Creates a new Firewall.
:param name: str :param name: str
@ -292,7 +326,7 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
:return: :class:`CreateFirewallResponse <hcloud.firewalls.domain.CreateFirewallResponse>` :return: :class:`CreateFirewallResponse <hcloud.firewalls.domain.CreateFirewallResponse>`
""" """
data = {"name": name} data: dict[str, Any] = {"name": name}
if labels is not None: if labels is not None:
data["labels"] = labels data["labels"] = labels
@ -309,7 +343,8 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
actions = [] actions = []
if response.get("actions") is not None: if response.get("actions") is not None:
actions = [ actions = [
BoundAction(self._client.actions, _) for _ in response["actions"] BoundAction(self._client.actions, action_data)
for action_data in response["actions"]
] ]
result = CreateFirewallResponse( result = CreateFirewallResponse(
@ -317,8 +352,12 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
) )
return result return result
def update(self, firewall, labels=None, name=None): def update(
# type: (Firewall, Optional[Dict[str, str]], Optional[str]) -> BoundFirewall self,
firewall: Firewall | BoundFirewall,
labels: dict[str, str] | None = None,
name: str | None = None,
) -> BoundFirewall:
"""Updates the description or labels of a Firewall. """Updates the description or labels of a Firewall.
:param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>` :param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>`
@ -328,7 +367,7 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
New name to set New name to set
:return: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` :return: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>`
""" """
data = {} data: dict[str, Any] = {}
if labels is not None: if labels is not None:
data["labels"] = labels data["labels"] = labels
if name is not None: if name is not None:
@ -341,8 +380,7 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
) )
return BoundFirewall(self, response["firewall"]) return BoundFirewall(self, response["firewall"])
def delete(self, firewall): def delete(self, firewall: Firewall | BoundFirewall) -> bool:
# type: (Firewall) -> bool
"""Deletes a Firewall. """Deletes a Firewall.
:param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>` :param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>`
@ -355,62 +393,74 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
# Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised
return True return True
def set_rules(self, firewall, rules): def set_rules(
# type: (Firewall, List[FirewallRule]) -> List[BoundAction] self,
firewall: Firewall | BoundFirewall,
rules: list[FirewallRule],
) -> list[BoundAction]:
"""Sets the rules of a Firewall. All existing rules will be overwritten. Pass an empty rules array to remove all rules. """Sets the rules of a Firewall. All existing rules will be overwritten. Pass an empty rules array to remove all rules.
:param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>` :param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>`
:param rules: List[:class:`FirewallRule <hcloud.firewalls.domain.FirewallRule>`] :param rules: List[:class:`FirewallRule <hcloud.firewalls.domain.FirewallRule>`]
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
""" """
data = {"rules": []} data: dict[str, Any] = {"rules": []}
for rule in rules: for rule in rules:
data["rules"].append(rule.to_payload()) data["rules"].append(rule.to_payload())
response = self._client.request( response = self._client.request(
url="/firewalls/{firewall_id}/actions/set_rules".format( url=f"/firewalls/{firewall.id}/actions/set_rules",
firewall_id=firewall.id
),
method="POST", method="POST",
json=data, json=data,
) )
return [BoundAction(self._client.actions, _) for _ in response["actions"]] return [
BoundAction(self._client.actions, action_data)
for action_data in response["actions"]
]
def apply_to_resources(self, firewall, resources): def apply_to_resources(
# type: (Firewall, List[FirewallResource]) -> List[BoundAction] self,
firewall: Firewall | BoundFirewall,
resources: list[FirewallResource],
) -> list[BoundAction]:
"""Applies one Firewall to multiple resources. """Applies one Firewall to multiple resources.
:param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>` :param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>`
:param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`] :param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`]
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
""" """
data = {"apply_to": []} data: dict[str, Any] = {"apply_to": []}
for resource in resources: for resource in resources:
data["apply_to"].append(resource.to_payload()) data["apply_to"].append(resource.to_payload())
response = self._client.request( response = self._client.request(
url="/firewalls/{firewall_id}/actions/apply_to_resources".format( url=f"/firewalls/{firewall.id}/actions/apply_to_resources",
firewall_id=firewall.id
),
method="POST", method="POST",
json=data, json=data,
) )
return [BoundAction(self._client.actions, _) for _ in response["actions"]] return [
BoundAction(self._client.actions, action_data)
for action_data in response["actions"]
]
def remove_from_resources(self, firewall, resources): def remove_from_resources(
# type: (Firewall, List[FirewallResource]) -> List[BoundAction] self,
firewall: Firewall | BoundFirewall,
resources: list[FirewallResource],
) -> list[BoundAction]:
"""Removes one Firewall from multiple resources. """Removes one Firewall from multiple resources.
:param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>` :param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>`
:param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`] :param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`]
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
""" """
data = {"remove_from": []} data: dict[str, Any] = {"remove_from": []}
for resource in resources: for resource in resources:
data["remove_from"].append(resource.to_payload()) data["remove_from"].append(resource.to_payload())
response = self._client.request( response = self._client.request(
url="/firewalls/{firewall_id}/actions/remove_from_resources".format( url=f"/firewalls/{firewall.id}/actions/remove_from_resources",
firewall_id=firewall.id
),
method="POST", method="POST",
json=data, json=data,
) )
return [BoundAction(self._client.actions, _) for _ in response["actions"]] return [
BoundAction(self._client.actions, action_data)
for action_data in response["actions"]
]

View file

@ -1,9 +1,18 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any
try: try:
from dateutil.parser import isoparse from dateutil.parser import isoparse
except ImportError: except ImportError:
isoparse = None isoparse = None
from ..core.domain import BaseDomain from ..core import BaseDomain
if TYPE_CHECKING:
from ..actions import BoundAction
from ..servers import BoundServer, Server
from .client import BoundFirewall
class Firewall(BaseDomain): class Firewall(BaseDomain):
@ -26,7 +35,13 @@ class Firewall(BaseDomain):
__slots__ = ("id", "name", "labels", "rules", "applied_to", "created") __slots__ = ("id", "name", "labels", "rules", "applied_to", "created")
def __init__( def __init__(
self, id=None, name=None, labels=None, rules=None, applied_to=None, created=None self,
id: int | None = None,
name: str | None = None,
labels: dict[str, str] | None = None,
rules: list[FirewallRule] | None = None,
applied_to: list[FirewallResource] | None = None,
created: str | None = None,
): ):
self.id = id self.id = id
self.name = name self.name = name
@ -81,12 +96,12 @@ class FirewallRule:
def __init__( def __init__(
self, self,
direction, # type: str direction: str,
protocol, # type: str protocol: str,
source_ips, # type: List[str] source_ips: list[str],
port=None, # type: Optional[str] port: str | None = None,
destination_ips=None, # type: Optional[List[str]] destination_ips: list[str] | None = None,
description=None, # type: Optional[str] description: str | None = None,
): ):
self.direction = direction self.direction = direction
self.port = port self.port = port
@ -95,18 +110,18 @@ class FirewallRule:
self.destination_ips = destination_ips or [] self.destination_ips = destination_ips or []
self.description = description self.description = description
def to_payload(self): def to_payload(self) -> dict[str, Any]:
payload = { payload: dict[str, Any] = {
"direction": self.direction, "direction": self.direction,
"protocol": self.protocol, "protocol": self.protocol,
"source_ips": self.source_ips, "source_ips": self.source_ips,
} }
if len(self.destination_ips) > 0: if len(self.destination_ips) > 0:
payload.update({"destination_ips": self.destination_ips}) payload["destination_ips"] = self.destination_ips
if self.port is not None: if self.port is not None:
payload.update({"port": self.port}) payload["port"] = self.port
if self.description is not None: if self.description is not None:
payload.update({"description": self.description}) payload["description"] = self.description
return payload return payload
@ -130,23 +145,21 @@ class FirewallResource:
def __init__( def __init__(
self, self,
type, # type: str type: str,
server=None, # type: Optional[Server] server: Server | BoundServer | None = None,
label_selector=None, # type: Optional[FirewallResourceLabelSelector] label_selector: FirewallResourceLabelSelector | None = None,
): ):
self.type = type self.type = type
self.server = server self.server = server
self.label_selector = label_selector self.label_selector = label_selector
def to_payload(self): def to_payload(self) -> dict[str, Any]:
payload = {"type": self.type} payload: dict[str, Any] = {"type": self.type}
if self.server is not None: if self.server is not None:
payload.update({"server": {"id": self.server.id}}) payload["server"] = {"id": self.server.id}
if self.label_selector is not None: if self.label_selector is not None:
payload.update( payload["label_selector"] = {"selector": self.label_selector.selector}
{"label_selector": {"selector": self.label_selector.selector}}
)
return payload return payload
@ -156,7 +169,7 @@ class FirewallResourceLabelSelector(BaseDomain):
:param selector: str Target label selector :param selector: str Target label selector
""" """
def __init__(self, selector=None): def __init__(self, selector: str | None = None):
self.selector = selector self.selector = selector
@ -173,8 +186,8 @@ class CreateFirewallResponse(BaseDomain):
def __init__( def __init__(
self, self,
firewall, # type: BoundFirewall firewall: BoundFirewall,
actions, # type: BoundAction actions: list[BoundAction] | None,
): ):
self.firewall = firewall self.firewall = firewall
self.actions = actions self.actions = actions

View file

@ -0,0 +1,8 @@
from __future__ import annotations
from .client import ( # noqa: F401
BoundFloatingIP,
FloatingIPsClient,
FloatingIPsPageResult,
)
from .domain import CreateFloatingIPResponse, FloatingIP # noqa: F401

View file

@ -1,15 +1,25 @@
from ..actions.client import BoundAction from __future__ import annotations
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result from typing import TYPE_CHECKING, Any, NamedTuple
from ..locations.client import BoundLocation
from ..actions import ActionsPageResult, BoundAction
from ..core import BoundModelBase, ClientEntityBase, Meta
from ..locations import BoundLocation
from .domain import CreateFloatingIPResponse, FloatingIP from .domain import CreateFloatingIPResponse, FloatingIP
if TYPE_CHECKING:
from .._client import Client
from ..locations import Location
from ..servers import BoundServer, Server
class BoundFloatingIP(BoundModelBase): class BoundFloatingIP(BoundModelBase):
_client: FloatingIPsClient
model = FloatingIP model = FloatingIP
def __init__(self, client, data, complete=True): def __init__(self, client: FloatingIPsClient, data: dict, complete: bool = True):
from ..servers.client import BoundServer from ..servers import BoundServer
server = data.get("server") server = data.get("server")
if server is not None: if server is not None:
@ -25,8 +35,13 @@ class BoundFloatingIP(BoundModelBase):
super().__init__(client, data, complete) super().__init__(client, data, complete)
def get_actions_list(self, status=None, sort=None, page=None, per_page=None): def get_actions_list(
# type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResult[BoundAction, Meta] self,
status: list[str] | None = None,
sort: list[str] | None = None,
page: int | None = None,
per_page: int | None = None,
) -> ActionsPageResult:
"""Returns all action objects for a Floating IP. """Returns all action objects for a Floating IP.
:param status: List[str] (optional) :param status: List[str] (optional)
@ -41,8 +56,11 @@ class BoundFloatingIP(BoundModelBase):
""" """
return self._client.get_actions_list(self, status, sort, page, per_page) return self._client.get_actions_list(self, status, sort, page, per_page)
def get_actions(self, status=None, sort=None): def get_actions(
# type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] self,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a Floating IP. """Returns all action objects for a Floating IP.
:param status: List[str] (optional) :param status: List[str] (optional)
@ -54,8 +72,12 @@ class BoundFloatingIP(BoundModelBase):
""" """
return self._client.get_actions(self, status, sort) return self._client.get_actions(self, status, sort)
def update(self, description=None, labels=None, name=None): def update(
# type: (Optional[str], Optional[Dict[str, str]], Optional[str]) -> BoundFloatingIP self,
description: str | None = None,
labels: dict[str, str] | None = None,
name: str | None = None,
) -> BoundFloatingIP:
"""Updates the description or labels of a Floating IP. """Updates the description or labels of a Floating IP.
:param description: str (optional) :param description: str (optional)
@ -68,16 +90,14 @@ class BoundFloatingIP(BoundModelBase):
""" """
return self._client.update(self, description, labels, name) return self._client.update(self, description, labels, name)
def delete(self): def delete(self) -> bool:
# type: () -> bool
"""Deletes a Floating IP. If it is currently assigned to a server it will automatically get unassigned. """Deletes a Floating IP. If it is currently assigned to a server it will automatically get unassigned.
:return: boolean :return: boolean
""" """
return self._client.delete(self) return self._client.delete(self)
def change_protection(self, delete=None): def change_protection(self, delete: bool | None = None) -> BoundAction:
# type: (Optional[bool]) -> BoundAction
"""Changes the protection configuration of the Floating IP. """Changes the protection configuration of the Floating IP.
:param delete: boolean :param delete: boolean
@ -86,8 +106,7 @@ class BoundFloatingIP(BoundModelBase):
""" """
return self._client.change_protection(self, delete) return self._client.change_protection(self, delete)
def assign(self, server): def assign(self, server: Server | BoundServer) -> BoundAction:
# type: (Server) -> BoundAction
"""Assigns a Floating IP to a server. """Assigns a Floating IP to a server.
:param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>`
@ -96,16 +115,14 @@ class BoundFloatingIP(BoundModelBase):
""" """
return self._client.assign(self, server) return self._client.assign(self, server)
def unassign(self): def unassign(self) -> BoundAction:
# type: () -> BoundAction
"""Unassigns a Floating IP, resulting in it being unreachable. You may assign it to a server again at a later time. """Unassigns a Floating IP, resulting in it being unreachable. You may assign it to a server again at a later time.
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
return self._client.unassign(self) return self._client.unassign(self)
def change_dns_ptr(self, ip, dns_ptr): def change_dns_ptr(self, ip: str, dns_ptr: str) -> BoundAction:
# type: (str, str) -> BoundAction
"""Changes the hostname that will appear when getting the hostname belonging to this Floating IP. """Changes the hostname that will appear when getting the hostname belonging to this Floating IP.
:param ip: str :param ip: str
@ -117,18 +134,22 @@ class BoundFloatingIP(BoundModelBase):
return self._client.change_dns_ptr(self, ip, dns_ptr) return self._client.change_dns_ptr(self, ip, dns_ptr)
class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin): class FloatingIPsPageResult(NamedTuple):
results_list_attribute_name = "floating_ips" floating_ips: list[BoundFloatingIP]
meta: Meta | None
class FloatingIPsClient(ClientEntityBase):
_client: Client
def get_actions_list( def get_actions_list(
self, self,
floating_ip, # type: FloatingIP floating_ip: FloatingIP | BoundFloatingIP,
status=None, # type: Optional[List[str]] status: list[str] | None = None,
sort=None, # type: Optional[List[str]] sort: list[str] | None = None,
page=None, # type: Optional[int] page: int | None = None,
per_page=None, # type: Optional[int] per_page: int | None = None,
): ) -> ActionsPageResult:
# type: (...) -> PageResults[List[BoundAction], Meta]
"""Returns all action objects for a Floating IP. """Returns all action objects for a Floating IP.
:param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>` :param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>`
@ -142,7 +163,7 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page Specifies how many results are returned by page
:return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if status is not None: if status is not None:
params["status"] = status params["status"] = status
if sort is not None: if sort is not None:
@ -152,9 +173,7 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
if per_page is not None: if per_page is not None:
params["per_page"] = per_page params["per_page"] = per_page
response = self._client.request( response = self._client.request(
url="/floating_ips/{floating_ip_id}/actions".format( url=f"/floating_ips/{floating_ip.id}/actions",
floating_ip_id=floating_ip.id
),
method="GET", method="GET",
params=params, params=params,
) )
@ -162,15 +181,14 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
BoundAction(self._client.actions, action_data) BoundAction(self._client.actions, action_data)
for action_data in response["actions"] for action_data in response["actions"]
] ]
return add_meta_to_result(actions, response, "actions") return ActionsPageResult(actions, Meta.parse_meta(response))
def get_actions( def get_actions(
self, self,
floating_ip, # type: FloatingIP floating_ip: FloatingIP | BoundFloatingIP,
status=None, # type: Optional[List[str]] status: list[str] | None = None,
sort=None, # type: Optional[List[str]] sort: list[str] | None = None,
): ) -> list[BoundAction]:
# type: (...) -> List[BoundAction]
"""Returns all action objects for a Floating IP. """Returns all action objects for a Floating IP.
:param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>` :param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>`
@ -181,10 +199,14 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
""" """
return super().get_actions(floating_ip, status=status, sort=sort) return self._iter_pages(
self.get_actions_list,
floating_ip,
status=status,
sort=sort,
)
def get_by_id(self, id): def get_by_id(self, id: int) -> BoundFloatingIP:
# type: (int) -> BoundFloatingIP
"""Returns a specific Floating IP object. """Returns a specific Floating IP object.
:param id: int :param id: int
@ -195,12 +217,11 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
def get_list( def get_list(
self, self,
label_selector=None, # type: Optional[str] label_selector: str | None = None,
page=None, # type: Optional[int] page: int | None = None,
per_page=None, # type: Optional[int] per_page: int | None = None,
name=None, # type: Optional[str] name: str | None = None,
): ) -> FloatingIPsPageResult:
# type: (...) -> PageResults[List[BoundFloatingIP]]
"""Get a list of floating ips from this account """Get a list of floating ips from this account
:param label_selector: str (optional) :param label_selector: str (optional)
@ -213,7 +234,7 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
Can be used to filter networks by their name. Can be used to filter networks by their name.
:return: (List[:class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if label_selector is not None: if label_selector is not None:
params["label_selector"] = label_selector params["label_selector"] = label_selector
@ -232,10 +253,13 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
for floating_ip_data in response["floating_ips"] 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): def get_all(
# type: (Optional[str], Optional[str]) -> List[BoundFloatingIP] self,
label_selector: str | None = None,
name: str | None = None,
) -> list[BoundFloatingIP]:
"""Get all floating ips from this account """Get all floating ips from this account
:param label_selector: str (optional) :param label_selector: str (optional)
@ -244,28 +268,26 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
Can be used to filter networks by their name. Can be used to filter networks by their name.
:return: List[:class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>`] :return: List[:class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>`]
""" """
return super().get_all(label_selector=label_selector, name=name) return self._iter_pages(self.get_list, label_selector=label_selector, name=name)
def get_by_name(self, name): def get_by_name(self, name: str) -> BoundFloatingIP | None:
# type: (str) -> BoundFloatingIP
"""Get Floating IP by name """Get Floating IP by name
:param name: str :param name: str
Used to get Floating IP by name. Used to get Floating IP by name.
:return: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` :return: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>`
""" """
return super().get_by_name(name) return self._get_first_by(name=name)
def create( def create(
self, self,
type, # type: str type: str,
description=None, # type: Optional[str] description: str | None = None,
labels=None, # type: Optional[str] labels: str | None = None,
home_location=None, # type: Optional[Location] home_location: Location | BoundLocation | None = None,
server=None, # type: Optional[Server] server: Server | BoundServer | None = None,
name=None, # type: Optional[str] name: str | None = None,
): ) -> CreateFloatingIPResponse:
# type: (...) -> CreateFloatingIPResponse
"""Creates a new Floating IP assigned to a server. """Creates a new Floating IP assigned to a server.
:param type: str :param type: str
@ -281,7 +303,7 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
:return: :class:`CreateFloatingIPResponse <hcloud.floating_ips.domain.CreateFloatingIPResponse>` :return: :class:`CreateFloatingIPResponse <hcloud.floating_ips.domain.CreateFloatingIPResponse>`
""" """
data = {"type": type} data: dict[str, Any] = {"type": type}
if description is not None: if description is not None:
data["description"] = description data["description"] = description
if labels is not None: if labels is not None:
@ -304,8 +326,13 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
) )
return result return result
def update(self, floating_ip, description=None, labels=None, name=None): def update(
# type: (FloatingIP, Optional[str], Optional[Dict[str, str]], Optional[str]) -> BoundFloatingIP self,
floating_ip: FloatingIP | BoundFloatingIP,
description: str | None = None,
labels: dict[str, str] | None = None,
name: str | None = None,
) -> BoundFloatingIP:
"""Updates the description or labels of a Floating IP. """Updates the description or labels of a Floating IP.
:param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>` :param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>`
@ -317,7 +344,7 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
New name to set New name to set
:return: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` :return: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>`
""" """
data = {} data: dict[str, Any] = {}
if description is not None: if description is not None:
data["description"] = description data["description"] = description
if labels is not None: if labels is not None:
@ -332,8 +359,7 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
) )
return BoundFloatingIP(self, response["floating_ip"]) return BoundFloatingIP(self, response["floating_ip"])
def delete(self, floating_ip): def delete(self, floating_ip: FloatingIP | BoundFloatingIP) -> bool:
# type: (FloatingIP) -> bool
"""Deletes a Floating IP. If it is currently assigned to a server it will automatically get unassigned. """Deletes a Floating IP. If it is currently assigned to a server it will automatically get unassigned.
:param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>` :param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>`
@ -346,8 +372,11 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
# Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised
return True return True
def change_protection(self, floating_ip, delete=None): def change_protection(
# type: (FloatingIP, Optional[bool]) -> BoundAction self,
floating_ip: FloatingIP | BoundFloatingIP,
delete: bool | None = None,
) -> BoundAction:
"""Changes the protection configuration of the Floating IP. """Changes the protection configuration of the Floating IP.
:param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>` :param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>`
@ -355,21 +384,22 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
If true, prevents the Floating IP from being deleted If true, prevents the Floating IP from being deleted
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {} data: dict[str, Any] = {}
if delete is not None: if delete is not None:
data.update({"delete": delete}) data.update({"delete": delete})
response = self._client.request( response = self._client.request(
url="/floating_ips/{floating_ip_id}/actions/change_protection".format( url=f"/floating_ips/{floating_ip.id}/actions/change_protection",
floating_ip_id=floating_ip.id
),
method="POST", method="POST",
json=data, json=data,
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def assign(self, floating_ip, server): def assign(
# type: (FloatingIP, Server) -> BoundAction self,
floating_ip: FloatingIP | BoundFloatingIP,
server: Server | BoundServer,
) -> BoundAction:
"""Assigns a Floating IP to a server. """Assigns a Floating IP to a server.
:param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>` :param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>`
@ -378,31 +408,30 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
response = self._client.request( response = self._client.request(
url="/floating_ips/{floating_ip_id}/actions/assign".format( url=f"/floating_ips/{floating_ip.id}/actions/assign",
floating_ip_id=floating_ip.id
),
method="POST", method="POST",
json={"server": server.id}, json={"server": server.id},
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def unassign(self, floating_ip): def unassign(self, floating_ip: FloatingIP | BoundFloatingIP) -> BoundAction:
# type: (FloatingIP) -> BoundAction
"""Unassigns a Floating IP, resulting in it being unreachable. You may assign it to a server again at a later time. """Unassigns a Floating IP, resulting in it being unreachable. You may assign it to a server again at a later time.
:param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>` :param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>`
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
response = self._client.request( response = self._client.request(
url="/floating_ips/{floating_ip_id}/actions/unassign".format( url=f"/floating_ips/{floating_ip.id}/actions/unassign",
floating_ip_id=floating_ip.id
),
method="POST", method="POST",
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def change_dns_ptr(self, floating_ip, ip, dns_ptr): def change_dns_ptr(
# type: (FloatingIP, str, str) -> BoundAction self,
floating_ip: FloatingIP | BoundFloatingIP,
ip: str,
dns_ptr: str,
) -> BoundAction:
"""Changes the hostname that will appear when getting the hostname belonging to this Floating IP. """Changes the hostname that will appear when getting the hostname belonging to this Floating IP.
:param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>` :param floating_ip: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>` or :class:`FloatingIP <hcloud.floating_ips.domain.FloatingIP>`
@ -413,9 +442,7 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
response = self._client.request( response = self._client.request(
url="/floating_ips/{floating_ip_id}/actions/change_dns_ptr".format( url=f"/floating_ips/{floating_ip.id}/actions/change_dns_ptr",
floating_ip_id=floating_ip.id
),
method="POST", method="POST",
json={"ip": ip, "dns_ptr": dns_ptr}, json={"ip": ip, "dns_ptr": dns_ptr},
) )

View file

@ -1,9 +1,19 @@
from __future__ import annotations
from typing import TYPE_CHECKING
try: try:
from dateutil.parser import isoparse from dateutil.parser import isoparse
except ImportError: except ImportError:
isoparse = None isoparse = None
from ..core.domain import BaseDomain from ..core import BaseDomain
if TYPE_CHECKING:
from ..actions import BoundAction
from ..locations import BoundLocation
from ..servers import BoundServer
from .client import BoundFloatingIP
class FloatingIP(BaseDomain): class FloatingIP(BaseDomain):
@ -52,18 +62,18 @@ class FloatingIP(BaseDomain):
def __init__( def __init__(
self, self,
id=None, id: int | None = None,
type=None, type: str | None = None,
description=None, description: str | None = None,
ip=None, ip: str | None = None,
server=None, server: BoundServer | None = None,
dns_ptr=None, dns_ptr: list[dict] | None = None,
home_location=None, home_location: BoundLocation | None = None,
blocked=None, blocked: bool | None = None,
protection=None, protection: dict | None = None,
labels=None, labels: dict[str, str] | None = None,
created=None, created: str | None = None,
name=None, name: str | None = None,
): ):
self.id = id self.id = id
self.type = type self.type = type
@ -92,8 +102,8 @@ class CreateFloatingIPResponse(BaseDomain):
def __init__( def __init__(
self, self,
floating_ip, # type: BoundFloatingIP floating_ip: BoundFloatingIP,
action, # type: BoundAction action: BoundAction | None,
): ):
self.floating_ip = floating_ip self.floating_ip = floating_ip
self.action = action self.action = action

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import warnings import warnings
warnings.warn( warnings.warn(

View file

@ -0,0 +1,3 @@
from __future__ import annotations
from .labels import LabelValidator # noqa: F401

View file

@ -1,5 +1,6 @@
from __future__ import annotations
import re import re
from typing import Dict
class LabelValidator: class LabelValidator:
@ -11,7 +12,7 @@ class LabelValidator:
) )
@staticmethod @staticmethod
def validate(labels: Dict[str, str]) -> bool: def validate(labels: dict[str, str]) -> bool:
"""Validates Labels. If you want to know which key/value pair of the dict is not correctly formatted """Validates Labels. If you want to know which key/value pair of the dict is not correctly formatted
use :func:`~hcloud.helpers.labels.validate_verbose`. use :func:`~hcloud.helpers.labels.validate_verbose`.
@ -25,7 +26,7 @@ class LabelValidator:
return True return True
@staticmethod @staticmethod
def validate_verbose(labels: Dict[str, str]) -> (bool, str): def validate_verbose(labels: dict[str, str]) -> tuple[bool, str]:
"""Validates Labels and returns the corresponding error message if something is wrong. Returns True, <empty string> """Validates Labels and returns the corresponding error message if something is wrong. Returns True, <empty string>
if everything is fine. if everything is fine.

View file

@ -0,0 +1,4 @@
from __future__ import annotations
from .client import BoundImage, ImagesClient, ImagesPageResult # noqa: F401
from .domain import CreateImageResponse, Image # noqa: F401

View file

@ -1,14 +1,22 @@
from ..actions.client import BoundAction from __future__ import annotations
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result from typing import TYPE_CHECKING, Any, NamedTuple
from ..actions import ActionsPageResult, BoundAction
from ..core import BoundModelBase, ClientEntityBase, Meta
from .domain import Image from .domain import Image
if TYPE_CHECKING:
from .._client import Client
class BoundImage(BoundModelBase): class BoundImage(BoundModelBase):
_client: ImagesClient
model = Image model = Image
def __init__(self, client, data): def __init__(self, client: ImagesClient, data: dict):
from ..servers.client import BoundServer from ..servers import BoundServer
created_from = data.get("created_from") created_from = data.get("created_from")
if created_from is not None: if created_from is not None:
@ -23,8 +31,13 @@ class BoundImage(BoundModelBase):
super().__init__(client, data) super().__init__(client, data)
def get_actions_list(self, sort=None, page=None, per_page=None, status=None): def get_actions_list(
# type: (Optional[List[str]], Optional[int], Optional[int], Optional[List[str]]) -> PageResult[BoundAction, Meta] self,
sort: list[str] | None = None,
page: int | None = None,
per_page: int | None = None,
status: list[str] | None = None,
) -> ActionsPageResult:
"""Returns a list of action objects for the image. """Returns a list of action objects for the image.
:param status: List[str] (optional) :param status: List[str] (optional)
@ -41,8 +54,11 @@ class BoundImage(BoundModelBase):
self, sort=sort, page=page, per_page=per_page, status=status self, sort=sort, page=page, per_page=per_page, status=status
) )
def get_actions(self, sort=None, status=None): def get_actions(
# type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] self,
sort: list[str] | None = None,
status: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for the image. """Returns all action objects for the image.
:param status: List[str] (optional) :param status: List[str] (optional)
@ -53,8 +69,12 @@ class BoundImage(BoundModelBase):
""" """
return self._client.get_actions(self, status=status, sort=sort) return self._client.get_actions(self, status=status, sort=sort)
def update(self, description=None, type=None, labels=None): def update(
# type: (Optional[str], Optional[str], Optional[Dict[str, str]]) -> BoundImage self,
description: str | None = None,
type: str | None = None,
labels: dict[str, str] | None = None,
) -> BoundImage:
"""Updates the Image. You may change the description, convert a Backup image to a Snapshot Image or change the image labels. """Updates the Image. You may change the description, convert a Backup image to a Snapshot Image or change the image labels.
:param description: str (optional) :param description: str (optional)
@ -68,16 +88,14 @@ class BoundImage(BoundModelBase):
""" """
return self._client.update(self, description, type, labels) return self._client.update(self, description, type, labels)
def delete(self): def delete(self) -> bool:
# type: () -> bool
"""Deletes an Image. Only images of type snapshot and backup can be deleted. """Deletes an Image. Only images of type snapshot and backup can be deleted.
:return: bool :return: bool
""" """
return self._client.delete(self) return self._client.delete(self)
def change_protection(self, delete=None): def change_protection(self, delete: bool | None = None) -> BoundAction:
# type: (Optional[bool]) -> BoundAction
"""Changes the protection configuration of the image. Can only be used on snapshots. """Changes the protection configuration of the image. Can only be used on snapshots.
:param delete: bool :param delete: bool
@ -87,18 +105,22 @@ class BoundImage(BoundModelBase):
return self._client.change_protection(self, delete) return self._client.change_protection(self, delete)
class ImagesClient(ClientEntityBase, GetEntityByNameMixin): class ImagesPageResult(NamedTuple):
results_list_attribute_name = "images" images: list[BoundImage]
meta: Meta | None
class ImagesClient(ClientEntityBase):
_client: Client
def get_actions_list( def get_actions_list(
self, self,
image, # type: Image image: Image | BoundImage,
sort=None, # type: Optional[List[str]] sort: list[str] | None = None,
page=None, # type: Optional[int] page: int | None = None,
per_page=None, # type: Optional[int] per_page: int | None = None,
status=None, # type: Optional[List[str]] status: list[str] | None = None,
): ) -> ActionsPageResult:
# type: (...) -> PageResults[List[BoundAction], Meta]
"""Returns a list of action objects for an image. """Returns a list of action objects for an image.
:param image: :class:`BoundImage <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.Image>` :param image: :class:`BoundImage <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.Image>`
@ -112,7 +134,7 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page Specifies how many results are returned by page
:return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if sort is not None: if sort is not None:
params["sort"] = sort params["sort"] = sort
if status is not None: if status is not None:
@ -130,15 +152,14 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
BoundAction(self._client.actions, action_data) BoundAction(self._client.actions, action_data)
for action_data in response["actions"] for action_data in response["actions"]
] ]
return add_meta_to_result(actions, response, "actions") return ActionsPageResult(actions, Meta.parse_meta(response))
def get_actions( def get_actions(
self, self,
image, # type: Image image: Image | BoundImage,
sort=None, # type: Optional[List[str]] sort: list[str] | None = None,
status=None, # type: Optional[List[str]] status: list[str] | None = None,
): ) -> list[BoundAction]:
# type: (...) -> List[BoundAction]
"""Returns all action objects for an image. """Returns all action objects for an image.
:param image: :class:`BoundImage <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.Image>` :param image: :class:`BoundImage <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.Image>`
@ -148,10 +169,14 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
Specify how the results are sorted. Choices: `id` `command` `status` `progress` `started` `finished` . You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default) Specify how the results are sorted. Choices: `id` `command` `status` `progress` `started` `finished` . You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
""" """
return super().get_actions(image, sort=sort, status=status) return self._iter_pages(
self.get_actions_list,
image,
sort=sort,
status=status,
)
def get_by_id(self, id): def get_by_id(self, id: int) -> BoundImage:
# type: (int) -> BoundImage
"""Get a specific Image """Get a specific Image
:param id: int :param id: int
@ -162,18 +187,17 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
def get_list( def get_list(
self, self,
name=None, # type: Optional[str] name: str | None = None,
label_selector=None, # type: Optional[str] label_selector: str | None = None,
bound_to=None, # type: Optional[List[str]] bound_to: list[str] | None = None,
type=None, # type: Optional[List[str]] type: list[str] | None = None,
architecture=None, # type: Optional[List[str]] architecture: list[str] | None = None,
sort=None, # type: Optional[List[str]] sort: list[str] | None = None,
page=None, # type: Optional[int] page: int | None = None,
per_page=None, # type: Optional[int] per_page: int | None = None,
status=None, # type: Optional[List[str]] status: list[str] | None = None,
include_deprecated=None, # type: Optional[bool] include_deprecated: bool | None = None,
): ) -> ImagesPageResult:
# type: (...) -> PageResults[List[BoundImage]]
"""Get all images """Get all images
:param name: str (optional) :param name: str (optional)
@ -198,7 +222,7 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page Specifies how many results are returned by page
:return: (List[:class:`BoundImage <hcloud.images.client.BoundImage>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundImage <hcloud.images.client.BoundImage>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if name is not None: if name is not None:
params["name"] = name params["name"] = name
if label_selector is not None: if label_selector is not None:
@ -222,20 +246,19 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
response = self._client.request(url="/images", method="GET", params=params) response = self._client.request(url="/images", method="GET", params=params)
images = [BoundImage(self, image_data) for image_data in response["images"]] 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( def get_all(
self, self,
name=None, # type: Optional[str] name: str | None = None,
label_selector=None, # type: Optional[str] label_selector: str | None = None,
bound_to=None, # type: Optional[List[str]] bound_to: list[str] | None = None,
type=None, # type: Optional[List[str]] type: list[str] | None = None,
architecture=None, # type: Optional[List[str]] architecture: list[str] | None = None,
sort=None, # type: Optional[List[str]] sort: list[str] | None = None,
status=None, # type: Optional[List[str]] status: list[str] | None = None,
include_deprecated=None, # type: Optional[bool] include_deprecated: bool | None = None,
): ) -> list[BoundImage]:
# type: (...) -> List[BoundImage]
"""Get all images """Get all images
:param name: str (optional) :param name: str (optional)
@ -256,7 +279,8 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
Include deprecated images in the response. Default: False Include deprecated images in the response. Default: False
:return: List[:class:`BoundImage <hcloud.images.client.BoundImage>`] :return: List[:class:`BoundImage <hcloud.images.client.BoundImage>`]
""" """
return super().get_all( return self._iter_pages(
self.get_list,
name=name, name=name,
label_selector=label_selector, label_selector=label_selector,
bound_to=bound_to, bound_to=bound_to,
@ -267,8 +291,7 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
include_deprecated=include_deprecated, include_deprecated=include_deprecated,
) )
def get_by_name(self, name): def get_by_name(self, name: str) -> BoundImage | None:
# type: (str) -> BoundImage
"""Get image by name """Get image by name
Deprecated: Use get_by_name_and_architecture instead. Deprecated: Use get_by_name_and_architecture instead.
@ -277,10 +300,13 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
Used to get image by name. Used to get image by name.
:return: :class:`BoundImage <hcloud.images.client.BoundImage>` :return: :class:`BoundImage <hcloud.images.client.BoundImage>`
""" """
return super().get_by_name(name) return self._get_first_by(name=name)
def get_by_name_and_architecture(self, name, architecture): def get_by_name_and_architecture(
# type: (str, str) -> BoundImage self,
name: str,
architecture: str,
) -> BoundImage | None:
"""Get image by name """Get image by name
:param name: str :param name: str
@ -289,13 +315,15 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
Used to identify the image. Used to identify the image.
:return: :class:`BoundImage <hcloud.images.client.BoundImage>` :return: :class:`BoundImage <hcloud.images.client.BoundImage>`
""" """
response = self.get_list(name=name, architecture=[architecture]) return self._get_first_by(name=name, architecture=[architecture])
entities = getattr(response, self.results_list_attribute_name)
entity = entities[0] if entities else None
return entity
def update(self, image, description=None, type=None, labels=None): def update(
# type:(Image, Optional[str], Optional[str], Optional[Dict[str, str]]) -> BoundImage self,
image: Image | BoundImage,
description: str | None = None,
type: str | None = None,
labels: dict[str, str] | None = None,
) -> BoundImage:
"""Updates the Image. You may change the description, convert a Backup image to a Snapshot Image or change the image labels. """Updates the Image. You may change the description, convert a Backup image to a Snapshot Image or change the image labels.
:param image: :class:`BoundImage <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.Image>` :param image: :class:`BoundImage <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.Image>`
@ -308,7 +336,7 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
User-defined labels (key-value pairs) User-defined labels (key-value pairs)
:return: :class:`BoundImage <hcloud.images.client.BoundImage>` :return: :class:`BoundImage <hcloud.images.client.BoundImage>`
""" """
data = {} data: dict[str, Any] = {}
if description is not None: if description is not None:
data.update({"description": description}) data.update({"description": description})
if type is not None: if type is not None:
@ -320,8 +348,7 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
) )
return BoundImage(self, response["image"]) return BoundImage(self, response["image"])
def delete(self, image): def delete(self, image: Image | BoundImage) -> bool:
# type: (Image) -> bool
"""Deletes an Image. Only images of type snapshot and backup can be deleted. """Deletes an Image. Only images of type snapshot and backup can be deleted.
:param :class:`BoundImage <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.Image>` :param :class:`BoundImage <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.Image>`
@ -331,8 +358,11 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
# Return allays true, because the API does not return an action for it. When an error occurs a APIException will be raised # Return allays true, because the API does not return an action for it. When an error occurs a APIException will be raised
return True return True
def change_protection(self, image, delete=None): def change_protection(
# type: (Image, Optional[bool]) -> BoundAction self,
image: Image | BoundImage,
delete: bool | None = None,
) -> BoundAction:
"""Changes the protection configuration of the image. Can only be used on snapshots. """Changes the protection configuration of the image. Can only be used on snapshots.
:param image: :class:`BoundImage <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.Image>` :param image: :class:`BoundImage <hcloud.images.client.BoundImage>` or :class:`Image <hcloud.images.domain.Image>`
@ -340,14 +370,12 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
If true, prevents the snapshot from being deleted If true, prevents the snapshot from being deleted
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {} data: dict[str, Any] = {}
if delete is not None: if delete is not None:
data.update({"delete": delete}) data.update({"delete": delete})
response = self._client.request( response = self._client.request(
url="/images/{image_id}/actions/change_protection".format( url=f"/images/{image.id}/actions/change_protection",
image_id=image.id
),
method="POST", method="POST",
json=data, json=data,
) )

View file

@ -1,9 +1,18 @@
from __future__ import annotations
from typing import TYPE_CHECKING
try: try:
from dateutil.parser import isoparse from dateutil.parser import isoparse
except ImportError: except ImportError:
isoparse = None isoparse = None
from ..core.domain import BaseDomain, DomainIdentityMixin from ..core import BaseDomain, DomainIdentityMixin
if TYPE_CHECKING:
from ..actions import BoundAction
from ..servers import BoundServer, Server
from .client import BoundImage
class Image(BaseDomain, DomainIdentityMixin): class Image(BaseDomain, DomainIdentityMixin):
@ -67,23 +76,23 @@ class Image(BaseDomain, DomainIdentityMixin):
def __init__( def __init__(
self, self,
id=None, id: int | None = None,
name=None, name: str | None = None,
type=None, type: str | None = None,
created=None, created: str | None = None,
description=None, description: str | None = None,
image_size=None, image_size: int | None = None,
disk_size=None, disk_size: int | None = None,
deprecated=None, deprecated: str | None = None,
bound_to=None, bound_to: Server | BoundServer | None = None,
os_flavor=None, os_flavor: str | None = None,
os_version=None, os_version: str | None = None,
architecture=None, architecture: str | None = None,
rapid_deploy=None, rapid_deploy: bool | None = None,
created_from=None, created_from: Server | BoundServer | None = None,
protection=None, protection: dict | None = None,
labels=None, labels: dict[str, str] | None = None,
status=None, status: str | None = None,
): ):
self.id = id self.id = id
self.name = name self.name = name
@ -117,8 +126,8 @@ class CreateImageResponse(BaseDomain):
def __init__( def __init__(
self, self,
action, # type: BoundAction action: BoundAction,
image, # type: BoundImage image: BoundImage,
): ):
self.action = action self.action = action
self.image = image self.image = image

View file

@ -0,0 +1,4 @@
from __future__ import annotations
from .client import BoundIso, IsosClient, IsosPageResult # noqa: F401
from .domain import Iso # noqa: F401

View file

@ -1,18 +1,30 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, NamedTuple
from warnings import warn from warnings import warn
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from ..core import BoundModelBase, ClientEntityBase, Meta
from .domain import Iso from .domain import Iso
if TYPE_CHECKING:
from .._client import Client
class BoundIso(BoundModelBase): class BoundIso(BoundModelBase):
_client: IsosClient
model = Iso model = Iso
class IsosClient(ClientEntityBase, GetEntityByNameMixin): class IsosPageResult(NamedTuple):
results_list_attribute_name = "isos" isos: list[BoundIso]
meta: Meta | None
def get_by_id(self, id):
# type: (int) -> BoundIso class IsosClient(ClientEntityBase):
_client: Client
def get_by_id(self, id: int) -> BoundIso:
"""Get a specific ISO by its id """Get a specific ISO by its id
:param id: int :param id: int
@ -23,14 +35,13 @@ class IsosClient(ClientEntityBase, GetEntityByNameMixin):
def get_list( def get_list(
self, self,
name=None, # type: Optional[str] name: str | None = None,
architecture=None, # type: Optional[List[str]] architecture: list[str] | None = None,
include_wildcard_architecture=None, # type: Optional[bool] include_wildcard_architecture: bool | None = None,
include_architecture_wildcard=None, # type: Optional[bool] include_architecture_wildcard: bool | None = None,
page=None, # type: Optional[int] page: int | None = None,
per_page=None, # type: Optional[int] per_page: int | None = None,
): ) -> IsosPageResult:
# type: (...) -> PageResults[List[BoundIso], Meta]
"""Get a list of ISOs """Get a list of ISOs
:param name: str (optional) :param name: str (optional)
@ -56,7 +67,7 @@ class IsosClient(ClientEntityBase, GetEntityByNameMixin):
) )
include_architecture_wildcard = include_wildcard_architecture include_architecture_wildcard = include_wildcard_architecture
params = {} params: dict[str, Any] = {}
if name is not None: if name is not None:
params["name"] = name params["name"] = name
if architecture is not None: if architecture is not None:
@ -70,16 +81,15 @@ class IsosClient(ClientEntityBase, GetEntityByNameMixin):
response = self._client.request(url="/isos", method="GET", params=params) response = self._client.request(url="/isos", method="GET", params=params)
isos = [BoundIso(self, iso_data) for iso_data in response["isos"]] 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( def get_all(
self, self,
name=None, # type: Optional[str] name: str | None = None,
architecture=None, # type: Optional[List[str]] architecture: list[str] | None = None,
include_wildcard_architecture=None, # type: Optional[bool] include_wildcard_architecture: bool | None = None,
include_architecture_wildcard=None, # type: Optional[bool] include_architecture_wildcard: bool | None = None,
): ) -> list[BoundIso]:
# type: (...) -> List[BoundIso]
"""Get all ISOs """Get all ISOs
:param name: str (optional) :param name: str (optional)
@ -101,18 +111,18 @@ class IsosClient(ClientEntityBase, GetEntityByNameMixin):
) )
include_architecture_wildcard = include_wildcard_architecture include_architecture_wildcard = include_wildcard_architecture
return super().get_all( return self._iter_pages(
self.get_list,
name=name, name=name,
architecture=architecture, architecture=architecture,
include_architecture_wildcard=include_architecture_wildcard, include_architecture_wildcard=include_architecture_wildcard,
) )
def get_by_name(self, name): def get_by_name(self, name: str) -> BoundIso | None:
# type: (str) -> BoundIso
"""Get iso by name """Get iso by name
:param name: str :param name: str
Used to get iso by name. Used to get iso by name.
:return: :class:`BoundIso <hcloud.isos.client.BoundIso>` :return: :class:`BoundIso <hcloud.isos.client.BoundIso>`
""" """
return super().get_by_name(name) return self._get_first_by(name=name)

View file

@ -1,9 +1,11 @@
from __future__ import annotations
try: try:
from dateutil.parser import isoparse from dateutil.parser import isoparse
except ImportError: except ImportError:
isoparse = None isoparse = None
from ..core.domain import BaseDomain, DomainIdentityMixin from ..core import BaseDomain, DomainIdentityMixin
class Iso(BaseDomain, DomainIdentityMixin): class Iso(BaseDomain, DomainIdentityMixin):
@ -27,12 +29,12 @@ class Iso(BaseDomain, DomainIdentityMixin):
def __init__( def __init__(
self, self,
id=None, id: int | None = None,
name=None, name: str | None = None,
type=None, type: str | None = None,
architecture=None, architecture: str | None = None,
description=None, description: str | None = None,
deprecated=None, deprecated: str | None = None,
): ):
self.id = id self.id = id
self.name = name self.name = name

View file

@ -0,0 +1,8 @@
from __future__ import annotations
from .client import ( # noqa: F401
BoundLoadBalancerType,
LoadBalancerTypesClient,
LoadBalancerTypesPageResult,
)
from .domain import LoadBalancerType # noqa: F401

View file

@ -1,31 +1,46 @@
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from __future__ import annotations
from typing import TYPE_CHECKING, Any, NamedTuple
from ..core import BoundModelBase, ClientEntityBase, Meta
from .domain import LoadBalancerType from .domain import LoadBalancerType
if TYPE_CHECKING:
from .._client import Client
class BoundLoadBalancerType(BoundModelBase): class BoundLoadBalancerType(BoundModelBase):
_client: LoadBalancerTypesClient
model = LoadBalancerType model = LoadBalancerType
class LoadBalancerTypesClient(ClientEntityBase, GetEntityByNameMixin): class LoadBalancerTypesPageResult(NamedTuple):
results_list_attribute_name = "load_balancer_types" load_balancer_types: list[BoundLoadBalancerType]
meta: Meta | None
def get_by_id(self, id):
# type: (int) -> load_balancer_types.client.BoundLoadBalancerType class LoadBalancerTypesClient(ClientEntityBase):
_client: Client
def get_by_id(self, id: int) -> BoundLoadBalancerType:
"""Returns a specific Load Balancer Type. """Returns a specific Load Balancer Type.
:param id: int :param id: int
:return: :class:`BoundLoadBalancerType <hcloud.load_balancer_type.client.BoundLoadBalancerType>` :return: :class:`BoundLoadBalancerType <hcloud.load_balancer_type.client.BoundLoadBalancerType>`
""" """
response = self._client.request( response = self._client.request(
url="/load_balancer_types/{load_balancer_type_id}".format( url=f"/load_balancer_types/{id}",
load_balancer_type_id=id
),
method="GET", method="GET",
) )
return BoundLoadBalancerType(self, response["load_balancer_type"]) return BoundLoadBalancerType(self, response["load_balancer_type"])
def get_list(self, name=None, page=None, per_page=None): def get_list(
# type: (Optional[str], Optional[int], Optional[int]) -> PageResults[List[BoundLoadBalancerType], Meta] self,
name: str | None = None,
page: int | None = None,
per_page: int | None = None,
) -> LoadBalancerTypesPageResult:
"""Get a list of Load Balancer types """Get a list of Load Balancer types
:param name: str (optional) :param name: str (optional)
@ -36,7 +51,7 @@ class LoadBalancerTypesClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page Specifies how many results are returned by page
:return: (List[:class:`BoundLoadBalancerType <hcloud.load_balancer_types.client.BoundLoadBalancerType>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundLoadBalancerType <hcloud.load_balancer_types.client.BoundLoadBalancerType>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if name is not None: if name is not None:
params["name"] = name params["name"] = name
if page is not None: if page is not None:
@ -51,24 +66,24 @@ class LoadBalancerTypesClient(ClientEntityBase, GetEntityByNameMixin):
BoundLoadBalancerType(self, load_balancer_type_data) BoundLoadBalancerType(self, load_balancer_type_data)
for load_balancer_type_data in response["load_balancer_types"] 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): def get_all(self, name: str | None = None) -> list[BoundLoadBalancerType]:
# type: (Optional[str]) -> List[BoundLoadBalancerType]
"""Get all Load Balancer types """Get all Load Balancer types
:param name: str (optional) :param name: str (optional)
Can be used to filter Load Balancer type by their name. Can be used to filter Load Balancer type by their name.
:return: List[:class:`BoundLoadBalancerType <hcloud.load_balancer_types.client.BoundLoadBalancerType>`] :return: List[:class:`BoundLoadBalancerType <hcloud.load_balancer_types.client.BoundLoadBalancerType>`]
""" """
return super().get_all(name=name) return self._iter_pages(self.get_list, name=name)
def get_by_name(self, name): def get_by_name(self, name: str) -> BoundLoadBalancerType | None:
# type: (str) -> BoundLoadBalancerType
"""Get Load Balancer type by name """Get Load Balancer type by name
:param name: str :param name: str
Used to get Load Balancer type by name. Used to get Load Balancer type by name.
:return: :class:`BoundLoadBalancerType <hcloud.load_balancer_types.client.BoundLoadBalancerType>` :return: :class:`BoundLoadBalancerType <hcloud.load_balancer_types.client.BoundLoadBalancerType>`
""" """
return super().get_by_name(name) return self._get_first_by(name=name)

View file

@ -1,4 +1,6 @@
from ..core.domain import BaseDomain, DomainIdentityMixin from __future__ import annotations
from ..core import BaseDomain, DomainIdentityMixin
class LoadBalancerType(BaseDomain, DomainIdentityMixin): class LoadBalancerType(BaseDomain, DomainIdentityMixin):
@ -36,14 +38,14 @@ class LoadBalancerType(BaseDomain, DomainIdentityMixin):
def __init__( def __init__(
self, self,
id=None, id: int | None = None,
name=None, name: str | None = None,
description=None, description: str | None = None,
max_connections=None, max_connections: int | None = None,
max_services=None, max_services: int | None = None,
max_targets=None, max_targets: int | None = None,
max_assigned_certificates=None, max_assigned_certificates: int | None = None,
prices=None, prices: dict | None = None,
): ):
self.id = id self.id = id
self.name = name self.name = name

View file

@ -0,0 +1,23 @@
from __future__ import annotations
from .client import ( # noqa: F401
BoundLoadBalancer,
LoadBalancersClient,
LoadBalancersPageResult,
)
from .domain import ( # noqa: F401
CreateLoadBalancerResponse,
IPv4Address,
IPv6Network,
LoadBalancer,
LoadBalancerAlgorithm,
LoadBalancerHealtCheckHttp,
LoadBalancerHealthCheck,
LoadBalancerService,
LoadBalancerServiceHttp,
LoadBalancerTarget,
LoadBalancerTargetIP,
LoadBalancerTargetLabelSelector,
PrivateNet,
PublicNetwork,
)

View file

@ -1,11 +1,14 @@
from ..actions.client import BoundAction from __future__ import annotations
from ..certificates.client import BoundCertificate
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from typing import TYPE_CHECKING, Any, NamedTuple
from ..core.domain import add_meta_to_result
from ..load_balancer_types.client import BoundLoadBalancerType from ..actions import ActionsPageResult, BoundAction
from ..locations.client import BoundLocation from ..certificates import BoundCertificate
from ..networks.client import BoundNetwork from ..core import BoundModelBase, ClientEntityBase, Meta
from ..servers.client import BoundServer from ..load_balancer_types import BoundLoadBalancerType
from ..locations import BoundLocation
from ..networks import BoundNetwork
from ..servers import BoundServer
from .domain import ( from .domain import (
CreateLoadBalancerResponse, CreateLoadBalancerResponse,
IPv4Address, IPv4Address,
@ -23,11 +26,19 @@ from .domain import (
PublicNetwork, PublicNetwork,
) )
if TYPE_CHECKING:
from .._client import Client
from ..load_balancer_types import LoadBalancerType
from ..locations import Location
from ..networks import Network
class BoundLoadBalancer(BoundModelBase): class BoundLoadBalancer(BoundModelBase):
_client: LoadBalancersClient
model = LoadBalancer model = LoadBalancer
def __init__(self, client, data, complete=True): def __init__(self, client: LoadBalancersClient, data: dict, complete: bool = True):
algorithm = data.get("algorithm") algorithm = data.get("algorithm")
if algorithm: if algorithm:
data["algorithm"] = LoadBalancerAlgorithm(type=algorithm["type"]) data["algorithm"] = LoadBalancerAlgorithm(type=algorithm["type"])
@ -131,8 +142,11 @@ class BoundLoadBalancer(BoundModelBase):
super().__init__(client, data, complete) super().__init__(client, data, complete)
def update(self, name=None, labels=None): def update(
# type: (Optional[str], Optional[Dict[str, str]]) -> BoundLoadBalancer self,
name: str | None = None,
labels: dict[str, str] | None = None,
) -> BoundLoadBalancer:
"""Updates a Load Balancer. You can update a Load Balancers name and a Load Balancers labels. """Updates a Load Balancer. You can update a Load Balancers name and a Load Balancers labels.
:param name: str (optional) :param name: str (optional)
@ -143,16 +157,20 @@ class BoundLoadBalancer(BoundModelBase):
""" """
return self._client.update(self, name, labels) return self._client.update(self, name, labels)
def delete(self): def delete(self) -> bool:
# type: () -> BoundAction
"""Deletes a Load Balancer. """Deletes a Load Balancer.
:return: boolean :return: boolean
""" """
return self._client.delete(self) return self._client.delete(self)
def get_actions_list(self, status=None, sort=None, page=None, per_page=None): def get_actions_list(
# type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]] self,
status: list[str] | None = None,
sort: list[str] | None = None,
page: int | None = None,
per_page: int | None = None,
) -> ActionsPageResult:
"""Returns all action objects for a Load Balancer. """Returns all action objects for a Load Balancer.
:param status: List[str] (optional) :param status: List[str] (optional)
@ -167,8 +185,11 @@ class BoundLoadBalancer(BoundModelBase):
""" """
return self._client.get_actions_list(self, status, sort, page, per_page) return self._client.get_actions_list(self, status, sort, page, per_page)
def get_actions(self, status=None, sort=None): def get_actions(
# type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] self,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a Load Balancer. """Returns all action objects for a Load Balancer.
:param status: List[str] (optional) :param status: List[str] (optional)
@ -179,8 +200,7 @@ class BoundLoadBalancer(BoundModelBase):
""" """
return self._client.get_actions(self, status, sort) return self._client.get_actions(self, status, sort)
def add_service(self, service): def add_service(self, service: LoadBalancerService) -> BoundAction:
# type: (LoadBalancerService) -> List[BoundAction]
"""Adds a service to a Load Balancer. """Adds a service to a Load Balancer.
:param service: :class:`LoadBalancerService <hcloud.load_balancers.domain.LoadBalancerService>` :param service: :class:`LoadBalancerService <hcloud.load_balancers.domain.LoadBalancerService>`
@ -189,8 +209,7 @@ class BoundLoadBalancer(BoundModelBase):
""" """
return self._client.add_service(self, service=service) return self._client.add_service(self, service=service)
def update_service(self, service): def update_service(self, service: LoadBalancerService) -> BoundAction:
# type: (LoadBalancerService) -> List[BoundAction]
"""Updates a service of an Load Balancer. """Updates a service of an Load Balancer.
:param service: :class:`LoadBalancerService <hcloud.load_balancers.domain.LoadBalancerService>` :param service: :class:`LoadBalancerService <hcloud.load_balancers.domain.LoadBalancerService>`
@ -199,8 +218,7 @@ class BoundLoadBalancer(BoundModelBase):
""" """
return self._client.update_service(self, service=service) return self._client.update_service(self, service=service)
def delete_service(self, service): def delete_service(self, service: LoadBalancerService) -> BoundAction:
# type: (LoadBalancerService) -> List[BoundAction]
"""Deletes a service from a Load Balancer. """Deletes a service from a Load Balancer.
:param service: :class:`LoadBalancerService <hcloud.load_balancers.domain.LoadBalancerService>` :param service: :class:`LoadBalancerService <hcloud.load_balancers.domain.LoadBalancerService>`
@ -209,8 +227,7 @@ class BoundLoadBalancer(BoundModelBase):
""" """
return self._client.delete_service(self, service) return self._client.delete_service(self, service)
def add_target(self, target): def add_target(self, target: LoadBalancerTarget) -> BoundAction:
# type: (LoadBalancerTarget) -> List[BoundAction]
"""Adds a target to a Load Balancer. """Adds a target to a Load Balancer.
:param target: :class:`LoadBalancerTarget <hcloud.load_balancers.domain.LoadBalancerTarget>` :param target: :class:`LoadBalancerTarget <hcloud.load_balancers.domain.LoadBalancerTarget>`
@ -219,8 +236,7 @@ class BoundLoadBalancer(BoundModelBase):
""" """
return self._client.add_target(self, target) return self._client.add_target(self, target)
def remove_target(self, target): def remove_target(self, target: LoadBalancerTarget) -> BoundAction:
# type: (LoadBalancerTarget) -> List[BoundAction]
"""Removes a target from a Load Balancer. """Removes a target from a Load Balancer.
:param target: :class:`LoadBalancerTarget <hcloud.load_balancers.domain.LoadBalancerTarget>` :param target: :class:`LoadBalancerTarget <hcloud.load_balancers.domain.LoadBalancerTarget>`
@ -229,8 +245,7 @@ class BoundLoadBalancer(BoundModelBase):
""" """
return self._client.remove_target(self, target) return self._client.remove_target(self, target)
def change_algorithm(self, algorithm): def change_algorithm(self, algorithm: LoadBalancerAlgorithm) -> BoundAction:
# type: (LoadBalancerAlgorithm) -> List[BoundAction]
"""Changes the algorithm used by the Load Balancer """Changes the algorithm used by the Load Balancer
:param algorithm: :class:`LoadBalancerAlgorithm <hcloud.load_balancers.domain.LoadBalancerAlgorithm>` :param algorithm: :class:`LoadBalancerAlgorithm <hcloud.load_balancers.domain.LoadBalancerAlgorithm>`
@ -239,8 +254,7 @@ class BoundLoadBalancer(BoundModelBase):
""" """
return self._client.change_algorithm(self, algorithm) return self._client.change_algorithm(self, algorithm)
def change_dns_ptr(self, ip, dns_ptr): def change_dns_ptr(self, ip: str, dns_ptr: str) -> BoundAction:
# type: (str, str) -> BoundAction
"""Changes the hostname that will appear when getting the hostname belonging to the public IPs (IPv4 and IPv6) of this Load Balancer. """Changes the hostname that will appear when getting the hostname belonging to the public IPs (IPv4 and IPv6) of this Load Balancer.
:param ip: str :param ip: str
@ -251,8 +265,7 @@ class BoundLoadBalancer(BoundModelBase):
""" """
return self._client.change_dns_ptr(self, ip, dns_ptr) return self._client.change_dns_ptr(self, ip, dns_ptr)
def change_protection(self, delete): def change_protection(self, delete: bool) -> BoundAction:
# type: (LoadBalancerService) -> List[BoundAction]
"""Changes the protection configuration of a Load Balancer. """Changes the protection configuration of a Load Balancer.
:param delete: boolean :param delete: boolean
@ -261,8 +274,11 @@ class BoundLoadBalancer(BoundModelBase):
""" """
return self._client.change_protection(self, delete) return self._client.change_protection(self, delete)
def attach_to_network(self, network, ip=None): def attach_to_network(
# type: (Union[Network,BoundNetwork],Optional[str]) -> BoundAction self,
network: Network | BoundNetwork,
ip: str | None = None,
) -> BoundAction:
"""Attaches a Load Balancer to a Network """Attaches a Load Balancer to a Network
:param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
@ -272,8 +288,7 @@ class BoundLoadBalancer(BoundModelBase):
""" """
return self._client.attach_to_network(self, network, ip) return self._client.attach_to_network(self, network, ip)
def detach_from_network(self, network): def detach_from_network(self, network: Network | BoundNetwork) -> BoundAction:
# type: ( Union[Network,BoundNetwork]) -> BoundAction
"""Detaches a Load Balancer from a Network. """Detaches a Load Balancer from a Network.
:param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
@ -281,24 +296,24 @@ class BoundLoadBalancer(BoundModelBase):
""" """
return self._client.detach_from_network(self, network) return self._client.detach_from_network(self, network)
def enable_public_interface(self): def enable_public_interface(self) -> BoundAction:
# type: () -> BoundAction
"""Enables the public interface of a Load Balancer. """Enables the public interface of a Load Balancer.
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
return self._client.enable_public_interface(self) return self._client.enable_public_interface(self)
def disable_public_interface(self): def disable_public_interface(self) -> BoundAction:
# type: () -> BoundAction
"""Disables the public interface of a Load Balancer. """Disables the public interface of a Load Balancer.
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
return self._client.disable_public_interface(self) return self._client.disable_public_interface(self)
def change_type(self, load_balancer_type): def change_type(
# type: (Union[LoadBalancerType,BoundLoadBalancerType]) -> BoundAction self,
load_balancer_type: LoadBalancerType | BoundLoadBalancerType,
) -> BoundAction:
"""Changes the type of a Load Balancer. """Changes the type of a Load Balancer.
:param load_balancer_type: :class:`BoundLoadBalancerType <hcloud.load_balancer_types.client.BoundLoadBalancerType>` or :class:`LoadBalancerType <hcloud.load_balancer_types.domain.LoadBalancerType>` :param load_balancer_type: :class:`BoundLoadBalancerType <hcloud.load_balancer_types.client.BoundLoadBalancerType>` or :class:`LoadBalancerType <hcloud.load_balancer_types.domain.LoadBalancerType>`
@ -308,11 +323,15 @@ class BoundLoadBalancer(BoundModelBase):
return self._client.change_type(self, load_balancer_type) return self._client.change_type(self, load_balancer_type)
class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin): class LoadBalancersPageResult(NamedTuple):
results_list_attribute_name = "load_balancers" load_balancers: list[BoundLoadBalancer]
meta: Meta | None
def get_by_id(self, id):
# type: (int) -> BoundLoadBalancer class LoadBalancersClient(ClientEntityBase):
_client: Client
def get_by_id(self, id: int) -> BoundLoadBalancer:
"""Get a specific Load Balancer """Get a specific Load Balancer
:param id: int :param id: int
@ -326,12 +345,11 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
def get_list( def get_list(
self, self,
name=None, # type: Optional[str] name: str | None = None,
label_selector=None, # type: Optional[str] label_selector: str | None = None,
page=None, # type: Optional[int] page: int | None = None,
per_page=None, # type: Optional[int] per_page: int | None = None,
): ) -> LoadBalancersPageResult:
# type: (...) -> PageResults[List[BoundLoadBalancer], Meta]
"""Get a list of Load Balancers from this account """Get a list of Load Balancers from this account
:param name: str (optional) :param name: str (optional)
@ -344,7 +362,7 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page Specifies how many results are returned by page
:return: (List[:class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if name is not None: if name is not None:
params["name"] = name params["name"] = name
if label_selector is not None: if label_selector is not None:
@ -358,14 +376,17 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
url="/load_balancers", method="GET", params=params url="/load_balancers", method="GET", params=params
) )
ass_load_balancers = [ load_balancers = [
BoundLoadBalancer(self, load_balancer_data) BoundLoadBalancer(self, load_balancer_data)
for load_balancer_data in response["load_balancers"] 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): def get_all(
# type: (Optional[str], Optional[str]) -> List[BoundLoadBalancer] self,
name: str | None = None,
label_selector: str | None = None,
) -> list[BoundLoadBalancer]:
"""Get all Load Balancers from this account """Get all Load Balancers from this account
:param name: str (optional) :param name: str (optional)
@ -374,32 +395,30 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
Can be used to filter Load Balancers by labels. The response will only contain Load Balancers matching the label selector. Can be used to filter Load Balancers by labels. The response will only contain Load Balancers matching the label selector.
:return: List[:class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>`] :return: List[:class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>`]
""" """
return super().get_all(name=name, label_selector=label_selector) return self._iter_pages(self.get_list, name=name, label_selector=label_selector)
def get_by_name(self, name): def get_by_name(self, name: str) -> BoundLoadBalancer | None:
# type: (str) -> BoundLoadBalancer
"""Get Load Balancer by name """Get Load Balancer by name
:param name: str :param name: str
Used to get Load Balancer by name. Used to get Load Balancer by name.
:return: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` :return: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>`
""" """
return super().get_by_name(name) return self._get_first_by(name=name)
def create( def create(
self, self,
name, # type: str name: str,
load_balancer_type, # type: LoadBalancerType load_balancer_type: LoadBalancerType | BoundLoadBalancerType,
algorithm=None, # type: Optional[LoadBalancerAlgorithm] algorithm: LoadBalancerAlgorithm | None = None,
services=None, # type: Optional[List[LoadBalancerService]] services: list[LoadBalancerService] | None = None,
targets=None, # type: Optional[List[LoadBalancerTarget]] targets: list[LoadBalancerTarget] | None = None,
labels=None, # type: Optional[Dict[str, str]] labels: dict[str, str] | None = None,
location=None, # type: Optional[Location] location: Location | BoundLocation | None = None,
network_zone=None, # type: Optional[str] network_zone: str | None = None,
public_interface=None, # type: Optional[bool] public_interface: bool | None = None,
network=None, # type: Optional[Union[Network,BoundNetwork]] network: Network | BoundNetwork | None = None,
): ) -> CreateLoadBalancerResponse:
# type: (...) -> CreateLoadBalancerResponse
"""Creates a Load Balancer . """Creates a Load Balancer .
:param name: str :param name: str
@ -424,7 +443,10 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
Adds the Load Balancer to a Network Adds the Load Balancer to a Network
:return: :class:`CreateLoadBalancerResponse <hcloud.load_balancers.domain.CreateLoadBalancerResponse>` :return: :class:`CreateLoadBalancerResponse <hcloud.load_balancers.domain.CreateLoadBalancerResponse>`
""" """
data = {"name": name, "load_balancer_type": load_balancer_type.id_or_name} data: dict[str, Any] = {
"name": name,
"load_balancer_type": load_balancer_type.id_or_name,
}
if network is not None: if network is not None:
data["network"] = network.id data["network"] = network.id
if public_interface is not None: if public_interface is not None:
@ -434,30 +456,9 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
if algorithm is not None: if algorithm is not None:
data["algorithm"] = {"type": algorithm.type} data["algorithm"] = {"type": algorithm.type}
if services is not None: if services is not None:
service_list = [] data["services"] = [service.to_payload() for service in services]
for service in services:
service_list.append(self.get_service_parameters(service))
data["services"] = service_list
if targets is not None: if targets is not None:
target_list = [] data["targets"] = [target.to_payload() for target in targets]
for target in targets:
target_data = {
"type": target.type,
"use_private_ip": target.use_private_ip,
}
if target.type == "server":
target_data["server"] = {"id": target.server.id}
elif target.type == "label_selector":
target_data["label_selector"] = {
"selector": target.label_selector.selector
}
elif target.type == "ip":
target_data["ip"] = {"ip": target.ip.ip}
target_list.append(target_data)
data["targets"] = target_list
if network_zone is not None: if network_zone is not None:
data["network_zone"] = network_zone data["network_zone"] = network_zone
if location is not None: if location is not None:
@ -470,8 +471,12 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
action=BoundAction(self._client.actions, response["action"]), action=BoundAction(self._client.actions, response["action"]),
) )
def update(self, load_balancer, name=None, labels=None): def update(
# type:(LoadBalancer, Optional[str], Optional[Dict[str, str]]) -> BoundLoadBalancer self,
load_balancer: LoadBalancer | BoundLoadBalancer,
name: str | None = None,
labels: dict[str, str] | None = None,
) -> BoundLoadBalancer:
"""Updates a LoadBalancer. You can update a LoadBalancers name and a LoadBalancers labels. """Updates a LoadBalancer. You can update a LoadBalancers name and a LoadBalancers labels.
:param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
@ -481,39 +486,38 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
User-defined labels (key-value pairs) User-defined labels (key-value pairs)
:return: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` :return: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>`
""" """
data = {} data: dict[str, Any] = {}
if name is not None: if name is not None:
data.update({"name": name}) data.update({"name": name})
if labels is not None: if labels is not None:
data.update({"labels": labels}) data.update({"labels": labels})
response = self._client.request( response = self._client.request(
url="/load_balancers/{load_balancer_id}".format( url=f"/load_balancers/{load_balancer.id}",
load_balancer_id=load_balancer.id
),
method="PUT", method="PUT",
json=data, json=data,
) )
return BoundLoadBalancer(self, response["load_balancer"]) return BoundLoadBalancer(self, response["load_balancer"])
def delete(self, load_balancer): def delete(self, load_balancer: LoadBalancer | BoundLoadBalancer) -> bool:
# type: (LoadBalancer) -> BoundAction
"""Deletes a Load Balancer. """Deletes a Load Balancer.
:param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
:return: boolean :return: boolean
""" """
self._client.request( self._client.request(
url="/load_balancers/{load_balancer_id}".format( url=f"/load_balancers/{load_balancer.id}",
load_balancer_id=load_balancer.id
),
method="DELETE", method="DELETE",
) )
return True return True
def get_actions_list( def get_actions_list(
self, load_balancer, status=None, sort=None, page=None, per_page=None self,
): load_balancer: LoadBalancer | BoundLoadBalancer,
# type: (LoadBalancer, Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta] status: list[str] | None = None,
sort: list[str] | None = None,
page: int | None = None,
per_page: int | None = None,
) -> ActionsPageResult:
"""Returns all action objects for a Load Balancer. """Returns all action objects for a Load Balancer.
:param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
@ -527,7 +531,7 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page Specifies how many results are returned by page
:return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if status is not None: if status is not None:
params["status"] = status params["status"] = status
if sort is not None: if sort is not None:
@ -538,9 +542,7 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
params["per_page"] = per_page params["per_page"] = per_page
response = self._client.request( response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions".format( url=f"/load_balancers/{load_balancer.id}/actions",
load_balancer_id=load_balancer.id
),
method="GET", method="GET",
params=params, params=params,
) )
@ -548,10 +550,14 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
BoundAction(self._client.actions, action_data) BoundAction(self._client.actions, action_data)
for action_data in response["actions"] 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): def get_actions(
# type: (LoadBalancer, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] self,
load_balancer: LoadBalancer | BoundLoadBalancer,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a Load Balancer. """Returns all action objects for a Load Balancer.
:param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
@ -561,10 +567,18 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc`
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
""" """
return super().get_actions(load_balancer, status=status, sort=sort) return self._iter_pages(
self.get_actions_list,
load_balancer,
status=status,
sort=sort,
)
def add_service(self, load_balancer, service): def add_service(
# type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerService) -> List[BoundAction] self,
load_balancer: LoadBalancer | BoundLoadBalancer,
service: LoadBalancerService,
) -> BoundAction:
"""Adds a service to a Load Balancer. """Adds a service to a Load Balancer.
:param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
@ -572,84 +586,20 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
The LoadBalancerService you want to add to the Load Balancer The LoadBalancerService you want to add to the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = self.get_service_parameters(service) data: dict[str, Any] = service.to_payload()
response = self._client.request( response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/add_service".format( url=f"/load_balancers/{load_balancer.id}/actions/add_service",
load_balancer_id=load_balancer.id
),
method="POST", method="POST",
json=data, json=data,
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def get_service_parameters(self, service): def update_service(
data = {} self,
if service.protocol is not None: load_balancer: LoadBalancer | BoundLoadBalancer,
data["protocol"] = service.protocol service: LoadBalancerService,
if service.listen_port is not None: ) -> BoundAction:
data["listen_port"] = service.listen_port
if service.destination_port is not None:
data["destination_port"] = service.destination_port
if service.proxyprotocol is not None:
data["proxyprotocol"] = service.proxyprotocol
if service.http is not None:
data["http"] = {}
if service.http.cookie_name is not None:
data["http"]["cookie_name"] = service.http.cookie_name
if service.http.cookie_lifetime is not None:
data["http"]["cookie_lifetime"] = service.http.cookie_lifetime
if service.http.redirect_http is not None:
data["http"]["redirect_http"] = service.http.redirect_http
if service.http.sticky_sessions is not None:
data["http"]["sticky_sessions"] = service.http.sticky_sessions
certificate_ids = []
for certificate in service.http.certificates:
certificate_ids.append(certificate.id)
data["http"]["certificates"] = certificate_ids
if service.health_check is not None:
data["health_check"] = {
"protocol": service.health_check.protocol,
"port": service.health_check.port,
"interval": service.health_check.interval,
"timeout": service.health_check.timeout,
"retries": service.health_check.retries,
}
data["health_check"] = {}
if service.health_check.protocol is not None:
data["health_check"]["protocol"] = service.health_check.protocol
if service.health_check.port is not None:
data["health_check"]["port"] = service.health_check.port
if service.health_check.interval is not None:
data["health_check"]["interval"] = service.health_check.interval
if service.health_check.timeout is not None:
data["health_check"]["timeout"] = service.health_check.timeout
if service.health_check.retries is not None:
data["health_check"]["retries"] = service.health_check.retries
if service.health_check.http is not None:
data["health_check"]["http"] = {}
if service.health_check.http.domain is not None:
data["health_check"]["http"][
"domain"
] = service.health_check.http.domain
if service.health_check.http.path is not None:
data["health_check"]["http"][
"path"
] = service.health_check.http.path
if service.health_check.http.response is not None:
data["health_check"]["http"][
"response"
] = service.health_check.http.response
if service.health_check.http.status_codes is not None:
data["health_check"]["http"][
"status_codes"
] = service.health_check.http.status_codes
if service.health_check.http.tls is not None:
data["health_check"]["http"]["tls"] = service.health_check.http.tls
return data
def update_service(self, load_balancer, service):
# type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerService) -> List[BoundAction]
"""Updates a service of an Load Balancer. """Updates a service of an Load Balancer.
:param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
@ -657,18 +607,19 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
The LoadBalancerService with updated values within for the Load Balancer The LoadBalancerService with updated values within for the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = self.get_service_parameters(service) data: dict[str, Any] = service.to_payload()
response = self._client.request( response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/update_service".format( url=f"/load_balancers/{load_balancer.id}/actions/update_service",
load_balancer_id=load_balancer.id
),
method="POST", method="POST",
json=data, json=data,
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def delete_service(self, load_balancer, service): def delete_service(
# type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerService) -> List[BoundAction] self,
load_balancer: LoadBalancer | BoundLoadBalancer,
service: LoadBalancerService,
) -> BoundAction:
"""Deletes a service from a Load Balancer. """Deletes a service from a Load Balancer.
:param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
@ -676,19 +627,20 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
The LoadBalancerService you want to delete from the Load Balancer The LoadBalancerService you want to delete from the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {"listen_port": service.listen_port} data: dict[str, Any] = {"listen_port": service.listen_port}
response = self._client.request( response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/delete_service".format( url=f"/load_balancers/{load_balancer.id}/actions/delete_service",
load_balancer_id=load_balancer.id
),
method="POST", method="POST",
json=data, json=data,
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def add_target(self, load_balancer, target): def add_target(
# type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerTarget) -> List[BoundAction] self,
load_balancer: LoadBalancer | BoundLoadBalancer,
target: LoadBalancerTarget,
) -> BoundAction:
"""Adds a target to a Load Balancer. """Adds a target to a Load Balancer.
:param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
@ -696,25 +648,20 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
The LoadBalancerTarget you want to add to the Load Balancer The LoadBalancerTarget you want to add to the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {"type": target.type, "use_private_ip": target.use_private_ip} data: dict[str, Any] = target.to_payload()
if target.type == "server":
data["server"] = {"id": target.server.id}
elif target.type == "label_selector":
data["label_selector"] = {"selector": target.label_selector.selector}
elif target.type == "ip":
data["ip"] = {"ip": target.ip.ip}
response = self._client.request( response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/add_target".format( url=f"/load_balancers/{load_balancer.id}/actions/add_target",
load_balancer_id=load_balancer.id
),
method="POST", method="POST",
json=data, json=data,
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def remove_target(self, load_balancer, target): def remove_target(
# type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerTarget) -> List[BoundAction] self,
load_balancer: LoadBalancer | BoundLoadBalancer,
target: LoadBalancerTarget,
) -> BoundAction:
"""Removes a target from a Load Balancer. """Removes a target from a Load Balancer.
:param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
@ -722,25 +669,22 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
The LoadBalancerTarget you want to remove from the Load Balancer The LoadBalancerTarget you want to remove from the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {"type": target.type} data: dict[str, Any] = target.to_payload()
if target.type == "server": # Do not send use_private_ip on remove_target
data["server"] = {"id": target.server.id} data.pop("use_private_ip", None)
elif target.type == "label_selector":
data["label_selector"] = {"selector": target.label_selector.selector}
elif target.type == "ip":
data["ip"] = {"ip": target.ip.ip}
response = self._client.request( response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/remove_target".format( url=f"/load_balancers/{load_balancer.id}/actions/remove_target",
load_balancer_id=load_balancer.id
),
method="POST", method="POST",
json=data, json=data,
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def change_algorithm(self, load_balancer, algorithm): def change_algorithm(
# type: (Union[LoadBalancer, BoundLoadBalancer], Optional[bool]) -> BoundAction self,
load_balancer: LoadBalancer | BoundLoadBalancer,
algorithm: LoadBalancerAlgorithm,
) -> BoundAction:
"""Changes the algorithm used by the Load Balancer """Changes the algorithm used by the Load Balancer
:param load_balancer: :class:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` :param load_balancer: :class:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
@ -748,19 +692,21 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
The LoadBalancerSubnet you want to add to the Load Balancer The LoadBalancerSubnet you want to add to the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {"type": algorithm.type} data: dict[str, Any] = {"type": algorithm.type}
response = self._client.request( response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/change_algorithm".format( url=f"/load_balancers/{load_balancer.id}/actions/change_algorithm",
load_balancer_id=load_balancer.id
),
method="POST", method="POST",
json=data, json=data,
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def change_dns_ptr(self, load_balancer, ip, dns_ptr): def change_dns_ptr(
# type: (Union[LoadBalancer, BoundLoadBalancer], str, str) -> BoundAction self,
load_balancer: LoadBalancer | BoundLoadBalancer,
ip: str,
dns_ptr: str,
) -> BoundAction:
"""Changes the hostname that will appear when getting the hostname belonging to the public IPs (IPv4 and IPv6) of this Load Balancer. """Changes the hostname that will appear when getting the hostname belonging to the public IPs (IPv4 and IPv6) of this Load Balancer.
:param ip: str :param ip: str
@ -771,16 +717,17 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
""" """
response = self._client.request( response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/change_dns_ptr".format( url=f"/load_balancers/{load_balancer.id}/actions/change_dns_ptr",
load_balancer_id=load_balancer.id
),
method="POST", method="POST",
json={"ip": ip, "dns_ptr": dns_ptr}, json={"ip": ip, "dns_ptr": dns_ptr},
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def change_protection(self, load_balancer, delete=None): def change_protection(
# type: (Union[LoadBalancer, BoundLoadBalancer], Optional[bool]) -> BoundAction self,
load_balancer: LoadBalancer | BoundLoadBalancer,
delete: bool | None = None,
) -> BoundAction:
"""Changes the protection configuration of a Load Balancer. """Changes the protection configuration of a Load Balancer.
:param load_balancer: :class:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` :param load_balancer: :class:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
@ -788,14 +735,12 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
If True, prevents the Load Balancer from being deleted If True, prevents the Load Balancer from being deleted
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {} data: dict[str, Any] = {}
if delete is not None: if delete is not None:
data.update({"delete": delete}) data.update({"delete": delete})
response = self._client.request( response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/change_protection".format( url=f"/load_balancers/{load_balancer.id}/actions/change_protection",
load_balancer_id=load_balancer.id
),
method="POST", method="POST",
json=data, json=data,
) )
@ -803,10 +748,10 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
def attach_to_network( def attach_to_network(
self, self,
load_balancer, # type: Union[LoadBalancer, BoundLoadBalancer] load_balancer: LoadBalancer | BoundLoadBalancer,
network, # type: Union[Network, BoundNetwork] network: Network | BoundNetwork,
ip=None, # type: Optional[str] ip: str | None = None,
): ) -> BoundAction:
"""Attach a Load Balancer to a Network. """Attach a Load Balancer to a Network.
:param load_balancer: :class:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` :param load_balancer: :class:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
@ -815,39 +760,40 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
IP to request to be assigned to this Load Balancer IP to request to be assigned to this Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {"network": network.id} data: dict[str, Any] = {"network": network.id}
if ip is not None: if ip is not None:
data.update({"ip": ip}) data.update({"ip": ip})
response = self._client.request( response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/attach_to_network".format( url=f"/load_balancers/{load_balancer.id}/actions/attach_to_network",
load_balancer_id=load_balancer.id
),
method="POST", method="POST",
json=data, json=data,
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def detach_from_network(self, load_balancer, network): def detach_from_network(
# type: (Union[LoadBalancer, BoundLoadBalancer], Union[Network,BoundNetwork]) -> BoundAction self,
load_balancer: LoadBalancer | BoundLoadBalancer,
network: Network | BoundNetwork,
) -> BoundAction:
"""Detaches a Load Balancer from a Network. """Detaches a Load Balancer from a Network.
:param load_balancer: :class:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` :param load_balancer: :class:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
:param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {"network": network.id} data: dict[str, Any] = {"network": network.id}
response = self._client.request( response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/detach_from_network".format( url=f"/load_balancers/{load_balancer.id}/actions/detach_from_network",
load_balancer_id=load_balancer.id
),
method="POST", method="POST",
json=data, json=data,
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def enable_public_interface(self, load_balancer): def enable_public_interface(
# type: (Union[LoadBalancer, BoundLoadBalancer]) -> BoundAction self,
load_balancer: LoadBalancer | BoundLoadBalancer,
) -> BoundAction:
"""Enables the public interface of a Load Balancer. """Enables the public interface of a Load Balancer.
:param load_balancer: :class:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` :param load_balancer: :class:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
@ -856,15 +802,15 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
""" """
response = self._client.request( response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/enable_public_interface".format( url=f"/load_balancers/{load_balancer.id}/actions/enable_public_interface",
load_balancer_id=load_balancer.id
),
method="POST", method="POST",
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def disable_public_interface(self, load_balancer): def disable_public_interface(
# type: (Union[LoadBalancer, BoundLoadBalancer]) -> BoundAction self,
load_balancer: LoadBalancer | BoundLoadBalancer,
) -> BoundAction:
"""Disables the public interface of a Load Balancer. """Disables the public interface of a Load Balancer.
:param load_balancer: :class:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` :param load_balancer: :class:` <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
@ -873,15 +819,16 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
""" """
response = self._client.request( response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/disable_public_interface".format( url=f"/load_balancers/{load_balancer.id}/actions/disable_public_interface",
load_balancer_id=load_balancer.id
),
method="POST", method="POST",
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def change_type(self, load_balancer, load_balancer_type): def change_type(
# type: ([LoadBalancer, BoundLoadBalancer], [LoadBalancerType, BoundLoadBalancerType]) ->BoundAction self,
load_balancer: LoadBalancer | BoundLoadBalancer,
load_balancer_type: LoadBalancerType | BoundLoadBalancerType,
) -> BoundAction:
"""Changes the type of a Load Balancer. """Changes the type of a Load Balancer.
:param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>` :param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
@ -889,11 +836,9 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
Load Balancer type the Load Balancer should migrate to Load Balancer type the Load Balancer should migrate to
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {"load_balancer_type": load_balancer_type.id_or_name} data: dict[str, Any] = {"load_balancer_type": load_balancer_type.id_or_name}
response = self._client.request( response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/change_type".format( url=f"/load_balancers/{load_balancer.id}/actions/change_type",
load_balancer_id=load_balancer.id
),
method="POST", method="POST",
json=data, json=data,
) )

View file

@ -1,9 +1,22 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any
try: try:
from dateutil.parser import isoparse from dateutil.parser import isoparse
except ImportError: except ImportError:
isoparse = None isoparse = None
from ..core.domain import BaseDomain from ..core import BaseDomain
if TYPE_CHECKING:
from ..actions import BoundAction
from ..certificates import BoundCertificate
from ..load_balancer_types import BoundLoadBalancerType
from ..locations import BoundLocation
from ..networks import BoundNetwork
from ..servers import BoundServer
from .client import BoundLoadBalancer
class LoadBalancer(BaseDomain): class LoadBalancer(BaseDomain):
@ -61,21 +74,21 @@ class LoadBalancer(BaseDomain):
def __init__( def __init__(
self, self,
id, id: int,
name=None, name: str | None = None,
public_net=None, public_net: PublicNetwork | None = None,
private_net=None, private_net: PrivateNet | None = None,
location=None, location: BoundLocation | None = None,
algorithm=None, algorithm: LoadBalancerAlgorithm | None = None,
services=None, services: list[LoadBalancerService] | None = None,
load_balancer_type=None, load_balancer_type: BoundLoadBalancerType | None = None,
protection=None, protection: dict | None = None,
labels=None, labels: dict[str, str] | None = None,
targets=None, targets: list[LoadBalancerTarget] | None = None,
created=None, created: str | None = None,
outgoing_traffic=None, outgoing_traffic: int | None = None,
ingoing_traffic=None, ingoing_traffic: int | None = None,
included_traffic=None, included_traffic: int | None = None,
): ):
self.id = id self.id = id
self.name = name self.name = name
@ -113,12 +126,12 @@ class LoadBalancerService(BaseDomain):
def __init__( def __init__(
self, self,
protocol=None, protocol: str | None = None,
listen_port=None, listen_port: int | None = None,
destination_port=None, destination_port: int | None = None,
proxyprotocol=None, proxyprotocol: bool | None = None,
health_check=None, health_check: LoadBalancerHealthCheck | None = None,
http=None, http: LoadBalancerServiceHttp | None = None,
): ):
self.protocol = protocol self.protocol = protocol
self.listen_port = listen_port self.listen_port = listen_port
@ -127,6 +140,74 @@ class LoadBalancerService(BaseDomain):
self.health_check = health_check self.health_check = health_check
self.http = http self.http = http
def to_payload(self) -> dict[str, Any]:
payload: dict[str, Any] = {}
if self.protocol is not None:
payload["protocol"] = self.protocol
if self.listen_port is not None:
payload["listen_port"] = self.listen_port
if self.destination_port is not None:
payload["destination_port"] = self.destination_port
if self.proxyprotocol is not None:
payload["proxyprotocol"] = self.proxyprotocol
if self.http is not None:
http: dict[str, Any] = {}
if self.http.cookie_name is not None:
http["cookie_name"] = self.http.cookie_name
if self.http.cookie_lifetime is not None:
http["cookie_lifetime"] = self.http.cookie_lifetime
if self.http.redirect_http is not None:
http["redirect_http"] = self.http.redirect_http
if self.http.sticky_sessions is not None:
http["sticky_sessions"] = self.http.sticky_sessions
http["certificates"] = [
certificate.id for certificate in self.http.certificates or []
]
payload["http"] = http
if self.health_check is not None:
health_check: dict[str, Any] = {
"protocol": self.health_check.protocol,
"port": self.health_check.port,
"interval": self.health_check.interval,
"timeout": self.health_check.timeout,
"retries": self.health_check.retries,
}
if self.health_check.protocol is not None:
health_check["protocol"] = self.health_check.protocol
if self.health_check.port is not None:
health_check["port"] = self.health_check.port
if self.health_check.interval is not None:
health_check["interval"] = self.health_check.interval
if self.health_check.timeout is not None:
health_check["timeout"] = self.health_check.timeout
if self.health_check.retries is not None:
health_check["retries"] = self.health_check.retries
if self.health_check.http is not None:
health_check_http: dict[str, Any] = {}
if self.health_check.http.domain is not None:
health_check_http["domain"] = self.health_check.http.domain
if self.health_check.http.path is not None:
health_check_http["path"] = self.health_check.http.path
if self.health_check.http.response is not None:
health_check_http["response"] = self.health_check.http.response
if self.health_check.http.status_codes is not None:
health_check_http[
"status_codes"
] = self.health_check.http.status_codes
if self.health_check.http.tls is not None:
health_check_http["tls"] = self.health_check.http.tls
health_check["http"] = health_check_http
payload["health_check"] = health_check
return payload
class LoadBalancerServiceHttp(BaseDomain): class LoadBalancerServiceHttp(BaseDomain):
"""LoadBalancerServiceHttp Domain """LoadBalancerServiceHttp Domain
@ -145,11 +226,11 @@ class LoadBalancerServiceHttp(BaseDomain):
def __init__( def __init__(
self, self,
cookie_name=None, cookie_name: str | None = None,
cookie_lifetime=None, cookie_lifetime: str | None = None,
certificates=None, certificates: list[BoundCertificate] | None = None,
redirect_http=None, redirect_http: bool | None = None,
sticky_sessions=None, sticky_sessions: bool | None = None,
): ):
self.cookie_name = cookie_name self.cookie_name = cookie_name
self.cookie_lifetime = cookie_lifetime self.cookie_lifetime = cookie_lifetime
@ -177,12 +258,12 @@ class LoadBalancerHealthCheck(BaseDomain):
def __init__( def __init__(
self, self,
protocol=None, protocol: str | None = None,
port=None, port: int | None = None,
interval=None, interval: int | None = None,
timeout=None, timeout: int | None = None,
retries=None, retries: int | None = None,
http=None, http: LoadBalancerHealtCheckHttp | None = None,
): ):
self.protocol = protocol self.protocol = protocol
self.port = port self.port = port
@ -208,7 +289,12 @@ class LoadBalancerHealtCheckHttp(BaseDomain):
""" """
def __init__( def __init__(
self, domain=None, path=None, response=None, status_codes=None, tls=None self,
domain: str | None = None,
path: str | None = None,
response: str | None = None,
status_codes: list | None = None,
tls: bool | None = None,
): ):
self.domain = domain self.domain = domain
self.path = path self.path = path
@ -233,7 +319,12 @@ class LoadBalancerTarget(BaseDomain):
""" """
def __init__( def __init__(
self, type=None, server=None, label_selector=None, ip=None, use_private_ip=None self,
type: str | None = None,
server: BoundServer | None = None,
label_selector: LoadBalancerTargetLabelSelector | None = None,
ip: LoadBalancerTargetIP | None = None,
use_private_ip: bool | None = None,
): ):
self.type = type self.type = type
self.server = server self.server = server
@ -241,6 +332,30 @@ class LoadBalancerTarget(BaseDomain):
self.ip = ip self.ip = ip
self.use_private_ip = use_private_ip self.use_private_ip = use_private_ip
def to_payload(self) -> dict[str, Any]:
payload: dict[str, Any] = {
"type": self.type,
}
if self.use_private_ip is not None:
payload["use_private_ip"] = self.use_private_ip
if self.type == "server":
if self.server is None:
raise ValueError(f"server is not defined in target {self!r}")
payload["server"] = {"id": self.server.id}
elif self.type == "label_selector":
if self.label_selector is None:
raise ValueError(f"label_selector is not defined in target {self!r}")
payload["label_selector"] = {"selector": self.label_selector.selector}
elif self.type == "ip":
if self.ip is None:
raise ValueError(f"ip is not defined in target {self!r}")
payload["ip"] = {"ip": self.ip.ip}
return payload
class LoadBalancerTargetLabelSelector(BaseDomain): class LoadBalancerTargetLabelSelector(BaseDomain):
"""LoadBalancerTargetLabelSelector Domain """LoadBalancerTargetLabelSelector Domain
@ -248,7 +363,7 @@ class LoadBalancerTargetLabelSelector(BaseDomain):
:param selector: str Target label selector :param selector: str Target label selector
""" """
def __init__(self, selector=None): def __init__(self, selector: str | None = None):
self.selector = selector self.selector = selector
@ -258,7 +373,7 @@ class LoadBalancerTargetIP(BaseDomain):
:param ip: str Target IP :param ip: str Target IP
""" """
def __init__(self, ip=None): def __init__(self, ip: str | None = None):
self.ip = ip self.ip = ip
@ -269,7 +384,7 @@ class LoadBalancerAlgorithm(BaseDomain):
Algorithm of the Load Balancer. Choices: round_robin, least_connections Algorithm of the Load Balancer. Choices: round_robin, least_connections
""" """
def __init__(self, type=None): def __init__(self, type: str | None = None):
self.type = type self.type = type
@ -285,9 +400,9 @@ class PublicNetwork(BaseDomain):
def __init__( def __init__(
self, self,
ipv4, # type: IPv4Address ipv4: IPv4Address,
ipv6, # type: IPv6Network ipv6: IPv6Network,
enabled, # type: bool enabled: bool,
): ):
self.ipv4 = ipv4 self.ipv4 = ipv4
self.ipv6 = ipv6 self.ipv6 = ipv6
@ -305,8 +420,8 @@ class IPv4Address(BaseDomain):
def __init__( def __init__(
self, self,
ip, # type: str ip: str,
dns_ptr, # type: str dns_ptr: str,
): ):
self.ip = ip self.ip = ip
self.dns_ptr = dns_ptr self.dns_ptr = dns_ptr
@ -323,8 +438,8 @@ class IPv6Network(BaseDomain):
def __init__( def __init__(
self, self,
ip, # type: str ip: str,
dns_ptr, # type: str dns_ptr: str,
): ):
self.ip = ip self.ip = ip
self.dns_ptr = dns_ptr self.dns_ptr = dns_ptr
@ -343,8 +458,8 @@ class PrivateNet(BaseDomain):
def __init__( def __init__(
self, self,
network, # type: BoundNetwork network: BoundNetwork,
ip, # type: str ip: str,
): ):
self.network = network self.network = network
self.ip = ip self.ip = ip
@ -363,8 +478,8 @@ class CreateLoadBalancerResponse(BaseDomain):
def __init__( def __init__(
self, self,
load_balancer, # type: BoundLoadBalancer load_balancer: BoundLoadBalancer,
action, # type: BoundAction action: BoundAction,
): ):
self.load_balancer = load_balancer self.load_balancer = load_balancer
self.action = action self.action = action

View file

@ -0,0 +1,4 @@
from __future__ import annotations
from .client import BoundLocation, LocationsClient, LocationsPageResult # noqa: F401
from .domain import Location # noqa: F401

View file

@ -1,16 +1,29 @@
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from __future__ import annotations
from typing import TYPE_CHECKING, Any, NamedTuple
from ..core import BoundModelBase, ClientEntityBase, Meta
from .domain import Location from .domain import Location
if TYPE_CHECKING:
from .._client import Client
class BoundLocation(BoundModelBase): class BoundLocation(BoundModelBase):
_client: LocationsClient
model = Location model = Location
class LocationsClient(ClientEntityBase, GetEntityByNameMixin): class LocationsPageResult(NamedTuple):
results_list_attribute_name = "locations" locations: list[BoundLocation]
meta: Meta | None
def get_by_id(self, id):
# type: (int) -> locations.client.BoundLocation class LocationsClient(ClientEntityBase):
_client: Client
def get_by_id(self, id: int) -> BoundLocation:
"""Get a specific location by its ID. """Get a specific location by its ID.
:param id: int :param id: int
@ -19,8 +32,12 @@ class LocationsClient(ClientEntityBase, GetEntityByNameMixin):
response = self._client.request(url=f"/locations/{id}", method="GET") response = self._client.request(url=f"/locations/{id}", method="GET")
return BoundLocation(self, response["location"]) return BoundLocation(self, response["location"])
def get_list(self, name=None, page=None, per_page=None): def get_list(
# type: (Optional[str], Optional[int], Optional[int]) -> PageResult[List[BoundLocation], Meta] self,
name: str | None = None,
page: int | None = None,
per_page: int | None = None,
) -> LocationsPageResult:
"""Get a list of locations """Get a list of locations
:param name: str (optional) :param name: str (optional)
@ -31,7 +48,7 @@ class LocationsClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page Specifies how many results are returned by page
:return: (List[:class:`BoundLocation <hcloud.locations.client.BoundLocation>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundLocation <hcloud.locations.client.BoundLocation>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if name is not None: if name is not None:
params["name"] = name params["name"] = name
if page is not None: if page is not None:
@ -44,24 +61,22 @@ class LocationsClient(ClientEntityBase, GetEntityByNameMixin):
BoundLocation(self, location_data) BoundLocation(self, location_data)
for location_data in response["locations"] 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): def get_all(self, name: str | None = None) -> list[BoundLocation]:
# type: (Optional[str]) -> List[BoundLocation]
"""Get all locations """Get all locations
:param name: str (optional) :param name: str (optional)
Can be used to filter locations by their name. Can be used to filter locations by their name.
:return: List[:class:`BoundLocation <hcloud.locations.client.BoundLocation>`] :return: List[:class:`BoundLocation <hcloud.locations.client.BoundLocation>`]
""" """
return super().get_all(name=name) return self._iter_pages(self.get_list, name=name)
def get_by_name(self, name): def get_by_name(self, name: str) -> BoundLocation | None:
# type: (str) -> BoundLocation
"""Get location by name """Get location by name
:param name: str :param name: str
Used to get location by name. Used to get location by name.
:return: :class:`BoundLocation <hcloud.locations.client.BoundLocation>` :return: :class:`BoundLocation <hcloud.locations.client.BoundLocation>`
""" """
return super().get_by_name(name) return self._get_first_by(name=name)

View file

@ -1,4 +1,6 @@
from ..core.domain import BaseDomain, DomainIdentityMixin from __future__ import annotations
from ..core import BaseDomain, DomainIdentityMixin
class Location(BaseDomain, DomainIdentityMixin): class Location(BaseDomain, DomainIdentityMixin):
@ -35,14 +37,14 @@ class Location(BaseDomain, DomainIdentityMixin):
def __init__( def __init__(
self, self,
id=None, id: int | None = None,
name=None, name: str | None = None,
description=None, description: str | None = None,
country=None, country: str | None = None,
city=None, city: str | None = None,
latitude=None, latitude: float | None = None,
longitude=None, longitude: float | None = None,
network_zone=None, network_zone: str | None = None,
): ):
self.id = id self.id = id
self.name = name self.name = name

View file

@ -0,0 +1,9 @@
from __future__ import annotations
from .client import BoundNetwork, NetworksClient, NetworksPageResult # noqa: F401
from .domain import ( # noqa: F401
CreateNetworkResponse,
Network,
NetworkRoute,
NetworkSubnet,
)

View file

@ -1,13 +1,21 @@
from ..actions.client import BoundAction from __future__ import annotations
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result from typing import TYPE_CHECKING, Any, NamedTuple
from ..actions import ActionsPageResult, BoundAction
from ..core import BoundModelBase, ClientEntityBase, Meta
from .domain import Network, NetworkRoute, NetworkSubnet from .domain import Network, NetworkRoute, NetworkSubnet
if TYPE_CHECKING:
from .._client import Client
class BoundNetwork(BoundModelBase): class BoundNetwork(BoundModelBase):
_client: NetworksClient
model = Network model = Network
def __init__(self, client, data, complete=True): def __init__(self, client: NetworksClient, data: dict, complete: bool = True):
subnets = data.get("subnets", []) subnets = data.get("subnets", [])
if subnets is not None: if subnets is not None:
subnets = [NetworkSubnet.from_dict(subnet) for subnet in subnets] subnets = [NetworkSubnet.from_dict(subnet) for subnet in subnets]
@ -18,7 +26,7 @@ class BoundNetwork(BoundModelBase):
routes = [NetworkRoute.from_dict(route) for route in routes] routes = [NetworkRoute.from_dict(route) for route in routes]
data["routes"] = routes data["routes"] = routes
from ..servers.client import BoundServer from ..servers import BoundServer
servers = data.get("servers", []) servers = data.get("servers", [])
if servers is not None: if servers is not None:
@ -32,10 +40,10 @@ class BoundNetwork(BoundModelBase):
def update( def update(
self, self,
name=None, # type: Optional[str] name: str | None = None,
expose_routes_to_vswitch=None, # type: Optional[bool] expose_routes_to_vswitch: bool | None = None,
labels=None, # type: Optional[Dict[str, str]] labels: dict[str, str] | None = None,
): # type: (...) -> BoundNetwork ) -> BoundNetwork:
"""Updates a network. You can update a networks name and a networkss labels. """Updates a network. You can update a networks name and a networkss labels.
:param name: str (optional) :param name: str (optional)
@ -54,16 +62,20 @@ class BoundNetwork(BoundModelBase):
labels=labels, labels=labels,
) )
def delete(self): def delete(self) -> bool:
# type: () -> BoundAction
"""Deletes a network. """Deletes a network.
:return: boolean :return: boolean
""" """
return self._client.delete(self) return self._client.delete(self)
def get_actions_list(self, status=None, sort=None, page=None, per_page=None): def get_actions_list(
# type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]] self,
status: list[str] | None = None,
sort: list[str] | None = None,
page: int | None = None,
per_page: int | None = None,
) -> ActionsPageResult:
"""Returns all action objects for a network. """Returns all action objects for a network.
:param status: List[str] (optional) :param status: List[str] (optional)
@ -78,8 +90,11 @@ class BoundNetwork(BoundModelBase):
""" """
return self._client.get_actions_list(self, status, sort, page, per_page) return self._client.get_actions_list(self, status, sort, page, per_page)
def get_actions(self, status=None, sort=None): def get_actions(
# type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] self,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a network. """Returns all action objects for a network.
:param status: List[str] (optional) :param status: List[str] (optional)
@ -90,8 +105,7 @@ class BoundNetwork(BoundModelBase):
""" """
return self._client.get_actions(self, status, sort) return self._client.get_actions(self, status, sort)
def add_subnet(self, subnet): def add_subnet(self, subnet: NetworkSubnet) -> BoundAction:
# type: (NetworkSubnet) -> List[BoundAction]
"""Adds a subnet entry to a network. """Adds a subnet entry to a network.
:param subnet: :class:`NetworkSubnet <hcloud.networks.domain.NetworkSubnet>` :param subnet: :class:`NetworkSubnet <hcloud.networks.domain.NetworkSubnet>`
@ -100,8 +114,7 @@ class BoundNetwork(BoundModelBase):
""" """
return self._client.add_subnet(self, subnet=subnet) return self._client.add_subnet(self, subnet=subnet)
def delete_subnet(self, subnet): def delete_subnet(self, subnet: NetworkSubnet) -> BoundAction:
# type: (NetworkSubnet) -> List[BoundAction]
"""Removes a subnet entry from a network """Removes a subnet entry from a network
:param subnet: :class:`NetworkSubnet <hcloud.networks.domain.NetworkSubnet>` :param subnet: :class:`NetworkSubnet <hcloud.networks.domain.NetworkSubnet>`
@ -110,8 +123,7 @@ class BoundNetwork(BoundModelBase):
""" """
return self._client.delete_subnet(self, subnet=subnet) return self._client.delete_subnet(self, subnet=subnet)
def add_route(self, route): def add_route(self, route: NetworkRoute) -> BoundAction:
# type: (NetworkRoute) -> List[BoundAction]
"""Adds a route entry to a network. """Adds a route entry to a network.
:param route: :class:`NetworkRoute <hcloud.networks.domain.NetworkRoute>` :param route: :class:`NetworkRoute <hcloud.networks.domain.NetworkRoute>`
@ -120,8 +132,7 @@ class BoundNetwork(BoundModelBase):
""" """
return self._client.add_route(self, route=route) return self._client.add_route(self, route=route)
def delete_route(self, route): def delete_route(self, route: NetworkRoute) -> BoundAction:
# type: (NetworkRoute) -> List[BoundAction]
"""Removes a route entry to a network. """Removes a route entry to a network.
:param route: :class:`NetworkRoute <hcloud.networks.domain.NetworkRoute>` :param route: :class:`NetworkRoute <hcloud.networks.domain.NetworkRoute>`
@ -130,8 +141,7 @@ class BoundNetwork(BoundModelBase):
""" """
return self._client.delete_route(self, route=route) return self._client.delete_route(self, route=route)
def change_ip_range(self, ip_range): def change_ip_range(self, ip_range: str) -> BoundAction:
# type: (str) -> List[BoundAction]
"""Changes the IP range of a network. """Changes the IP range of a network.
:param ip_range: str :param ip_range: str
@ -140,8 +150,7 @@ class BoundNetwork(BoundModelBase):
""" """
return self._client.change_ip_range(self, ip_range=ip_range) return self._client.change_ip_range(self, ip_range=ip_range)
def change_protection(self, delete=None): def change_protection(self, delete: bool | None = None) -> BoundAction:
# type: (Optional[bool]) -> BoundAction
"""Changes the protection configuration of a network. """Changes the protection configuration of a network.
:param delete: boolean :param delete: boolean
@ -151,11 +160,15 @@ class BoundNetwork(BoundModelBase):
return self._client.change_protection(self, delete=delete) return self._client.change_protection(self, delete=delete)
class NetworksClient(ClientEntityBase, GetEntityByNameMixin): class NetworksPageResult(NamedTuple):
results_list_attribute_name = "networks" networks: list[BoundNetwork]
meta: Meta | None
def get_by_id(self, id):
# type: (int) -> BoundNetwork class NetworksClient(ClientEntityBase):
_client: Client
def get_by_id(self, id: int) -> BoundNetwork:
"""Get a specific network """Get a specific network
:param id: int :param id: int
@ -166,12 +179,11 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
def get_list( def get_list(
self, self,
name=None, # type: Optional[str] name: str | None = None,
label_selector=None, # type: Optional[str] label_selector: str | None = None,
page=None, # type: Optional[int] page: int | None = None,
per_page=None, # type: Optional[int] per_page: int | None = None,
): ) -> NetworksPageResult:
# type: (...) -> PageResults[List[BoundNetwork], Meta]
"""Get a list of networks from this account """Get a list of networks from this account
:param name: str (optional) :param name: str (optional)
@ -184,7 +196,7 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page Specifies how many results are returned by page
:return: (List[:class:`BoundNetwork <hcloud.networks.client.BoundNetwork>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundNetwork <hcloud.networks.client.BoundNetwork>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if name is not None: if name is not None:
params["name"] = name params["name"] = name
if label_selector is not None: if label_selector is not None:
@ -196,13 +208,16 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
response = self._client.request(url="/networks", method="GET", params=params) response = self._client.request(url="/networks", method="GET", params=params)
ass_networks = [ networks = [
BoundNetwork(self, network_data) for network_data in response["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): def get_all(
# type: (Optional[str], Optional[str]) -> List[BoundNetwork] self,
name: str | None = None,
label_selector: str | None = None,
) -> list[BoundNetwork]:
"""Get all networks from this account """Get all networks from this account
:param name: str (optional) :param name: str (optional)
@ -211,27 +226,26 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
Can be used to filter networks by labels. The response will only contain networks matching the label selector. Can be used to filter networks by labels. The response will only contain networks matching the label selector.
:return: List[:class:`BoundNetwork <hcloud.networks.client.BoundNetwork>`] :return: List[:class:`BoundNetwork <hcloud.networks.client.BoundNetwork>`]
""" """
return super().get_all(name=name, label_selector=label_selector) return self._iter_pages(self.get_list, name=name, label_selector=label_selector)
def get_by_name(self, name): def get_by_name(self, name: str) -> BoundNetwork | None:
# type: (str) -> BoundNetwork
"""Get network by name """Get network by name
:param name: str :param name: str
Used to get network by name. Used to get network by name.
:return: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` :return: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>`
""" """
return super().get_by_name(name) return self._get_first_by(name=name)
def create( def create(
self, self,
name, # type: str name: str,
ip_range, # type: str ip_range: str,
subnets=None, # type: Optional[List[NetworkSubnet]] subnets: list[NetworkSubnet] | None = None,
routes=None, # type: Optional[List[NetworkRoute]] routes: list[NetworkRoute] | None = None,
expose_routes_to_vswitch=None, # type: Optional[bool] expose_routes_to_vswitch: bool | None = None,
labels=None, # type: Optional[Dict[str, str]] labels: dict[str, str] | None = None,
): ) -> BoundNetwork:
"""Creates a network with range ip_range. """Creates a network with range ip_range.
:param name: str :param name: str
@ -249,11 +263,11 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
User-defined labels (key-value pairs) User-defined labels (key-value pairs)
:return: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` :return: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>`
""" """
data = {"name": name, "ip_range": ip_range} data: dict[str, Any] = {"name": name, "ip_range": ip_range}
if subnets is not None: if subnets is not None:
data_subnets = [] data_subnets = []
for subnet in subnets: for subnet in subnets:
data_subnet = { data_subnet: dict[str, Any] = {
"type": subnet.type, "type": subnet.type,
"ip_range": subnet.ip_range, "ip_range": subnet.ip_range,
"network_zone": subnet.network_zone, "network_zone": subnet.network_zone,
@ -280,8 +294,13 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
return BoundNetwork(self, response["network"]) return BoundNetwork(self, response["network"])
def update(self, network, name=None, expose_routes_to_vswitch=None, labels=None): def update(
# type:(Network, Optional[str], Optional[bool], Optional[Dict[str, str]]) -> BoundNetwork self,
network: Network | BoundNetwork,
name: str | None = None,
expose_routes_to_vswitch: bool | None = None,
labels: dict[str, str] | None = None,
) -> BoundNetwork:
"""Updates a network. You can update a networks name and a networks labels. """Updates a network. You can update a networks name and a networks labels.
:param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
@ -294,7 +313,7 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
User-defined labels (key-value pairs) User-defined labels (key-value pairs)
:return: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` :return: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>`
""" """
data = {} data: dict[str, Any] = {}
if name is not None: if name is not None:
data.update({"name": name}) data.update({"name": name})
@ -311,8 +330,7 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
) )
return BoundNetwork(self, response["network"]) return BoundNetwork(self, response["network"])
def delete(self, network): def delete(self, network: Network | BoundNetwork) -> bool:
# type: (Network) -> BoundAction
"""Deletes a network. """Deletes a network.
:param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
@ -322,9 +340,13 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
return True return True
def get_actions_list( def get_actions_list(
self, network, status=None, sort=None, page=None, per_page=None self,
): network: Network | BoundNetwork,
# type: (Network, Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta] status: list[str] | None = None,
sort: list[str] | None = None,
page: int | None = None,
per_page: int | None = None,
) -> ActionsPageResult:
"""Returns all action objects for a network. """Returns all action objects for a network.
:param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
@ -338,7 +360,7 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page Specifies how many results are returned by page
:return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if status is not None: if status is not None:
params["status"] = status params["status"] = status
if sort is not None: if sort is not None:
@ -357,10 +379,14 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
BoundAction(self._client.actions, action_data) BoundAction(self._client.actions, action_data)
for action_data in response["actions"] 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): def get_actions(
# type: (Network, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] self,
network: Network | BoundNetwork,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a network. """Returns all action objects for a network.
:param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
@ -370,10 +396,18 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc`
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
""" """
return super().get_actions(network, status=status, sort=sort) return self._iter_pages(
self.get_actions_list,
network,
status=status,
sort=sort,
)
def add_subnet(self, network, subnet): def add_subnet(
# type: (Union[Network, BoundNetwork], NetworkSubnet) -> List[BoundAction] self,
network: Network | BoundNetwork,
subnet: NetworkSubnet,
) -> BoundAction:
"""Adds a subnet entry to a network. """Adds a subnet entry to a network.
:param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
@ -381,23 +415,27 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
The NetworkSubnet you want to add to the Network The NetworkSubnet you want to add to the Network
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {"type": subnet.type, "network_zone": subnet.network_zone} data: dict[str, Any] = {
"type": subnet.type,
"network_zone": subnet.network_zone,
}
if subnet.ip_range is not None: if subnet.ip_range is not None:
data["ip_range"] = subnet.ip_range data["ip_range"] = subnet.ip_range
if subnet.vswitch_id is not None: if subnet.vswitch_id is not None:
data["vswitch_id"] = subnet.vswitch_id data["vswitch_id"] = subnet.vswitch_id
response = self._client.request( response = self._client.request(
url="/networks/{network_id}/actions/add_subnet".format( url=f"/networks/{network.id}/actions/add_subnet",
network_id=network.id
),
method="POST", method="POST",
json=data, json=data,
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def delete_subnet(self, network, subnet): def delete_subnet(
# type: (Union[Network, BoundNetwork], NetworkSubnet) -> List[BoundAction] self,
network: Network | BoundNetwork,
subnet: NetworkSubnet,
) -> BoundAction:
"""Removes a subnet entry from a network """Removes a subnet entry from a network
:param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
@ -405,19 +443,20 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
The NetworkSubnet you want to remove from the Network The NetworkSubnet you want to remove from the Network
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {"ip_range": subnet.ip_range} data: dict[str, Any] = {"ip_range": subnet.ip_range}
response = self._client.request( response = self._client.request(
url="/networks/{network_id}/actions/delete_subnet".format( url=f"/networks/{network.id}/actions/delete_subnet",
network_id=network.id
),
method="POST", method="POST",
json=data, json=data,
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def add_route(self, network, route): def add_route(
# type: (Union[Network, BoundNetwork], NetworkRoute) -> List[BoundAction] self,
network: Network | BoundNetwork,
route: NetworkRoute,
) -> BoundAction:
"""Adds a route entry to a network. """Adds a route entry to a network.
:param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
@ -425,19 +464,23 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
The NetworkRoute you want to add to the Network The NetworkRoute you want to add to the Network
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {"destination": route.destination, "gateway": route.gateway} data: dict[str, Any] = {
"destination": route.destination,
"gateway": route.gateway,
}
response = self._client.request( response = self._client.request(
url="/networks/{network_id}/actions/add_route".format( url=f"/networks/{network.id}/actions/add_route",
network_id=network.id
),
method="POST", method="POST",
json=data, json=data,
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def delete_route(self, network, route): def delete_route(
# type: (Union[Network, BoundNetwork], NetworkRoute) -> List[BoundAction] self,
network: Network | BoundNetwork,
route: NetworkRoute,
) -> BoundAction:
"""Removes a route entry to a network. """Removes a route entry to a network.
:param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
@ -445,19 +488,23 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
The NetworkRoute you want to remove from the Network The NetworkRoute you want to remove from the Network
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {"destination": route.destination, "gateway": route.gateway} data: dict[str, Any] = {
"destination": route.destination,
"gateway": route.gateway,
}
response = self._client.request( response = self._client.request(
url="/networks/{network_id}/actions/delete_route".format( url=f"/networks/{network.id}/actions/delete_route",
network_id=network.id
),
method="POST", method="POST",
json=data, json=data,
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def change_ip_range(self, network, ip_range): def change_ip_range(
# type: (Union[Network, BoundNetwork], str) -> List[BoundAction] self,
network: Network | BoundNetwork,
ip_range: str,
) -> BoundAction:
"""Changes the IP range of a network. """Changes the IP range of a network.
:param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
@ -465,19 +512,20 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
The new prefix for the whole network. The new prefix for the whole network.
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {"ip_range": ip_range} data: dict[str, Any] = {"ip_range": ip_range}
response = self._client.request( response = self._client.request(
url="/networks/{network_id}/actions/change_ip_range".format( url=f"/networks/{network.id}/actions/change_ip_range",
network_id=network.id
),
method="POST", method="POST",
json=data, json=data,
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def change_protection(self, network, delete=None): def change_protection(
# type: (Union[Network, BoundNetwork], Optional[bool]) -> BoundAction self,
network: Network | BoundNetwork,
delete: bool | None = None,
) -> BoundAction:
"""Changes the protection configuration of a network. """Changes the protection configuration of a network.
:param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>` :param network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
@ -485,14 +533,12 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
If True, prevents the network from being deleted If True, prevents the network from being deleted
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {} data: dict[str, Any] = {}
if delete is not None: if delete is not None:
data.update({"delete": delete}) data.update({"delete": delete})
response = self._client.request( response = self._client.request(
url="/networks/{network_id}/actions/change_protection".format( url=f"/networks/{network.id}/actions/change_protection",
network_id=network.id
),
method="POST", method="POST",
json=data, json=data,
) )

View file

@ -1,9 +1,18 @@
from __future__ import annotations
from typing import TYPE_CHECKING
try: try:
from dateutil.parser import isoparse from dateutil.parser import isoparse
except ImportError: except ImportError:
isoparse = None isoparse = None
from ..core.domain import BaseDomain from ..core import BaseDomain
if TYPE_CHECKING:
from ..actions import BoundAction
from ..servers import BoundServer
from .client import BoundNetwork
class Network(BaseDomain): class Network(BaseDomain):
@ -44,16 +53,16 @@ class Network(BaseDomain):
def __init__( def __init__(
self, self,
id, id: int,
name=None, name: str | None = None,
created=None, created: str | None = None,
ip_range=None, ip_range: str | None = None,
subnets=None, subnets: list[NetworkSubnet] | None = None,
routes=None, routes: list[NetworkRoute] | None = None,
expose_routes_to_vswitch=None, expose_routes_to_vswitch: bool | None = None,
servers=None, servers: list[BoundServer] | None = None,
protection=None, protection: dict | None = None,
labels=None, labels: dict[str, str] | None = None,
): ):
self.id = id self.id = id
self.name = name self.name = name
@ -91,7 +100,12 @@ class NetworkSubnet(BaseDomain):
__slots__ = ("type", "ip_range", "network_zone", "gateway", "vswitch_id") __slots__ = ("type", "ip_range", "network_zone", "gateway", "vswitch_id")
def __init__( def __init__(
self, ip_range, type=None, network_zone=None, gateway=None, vswitch_id=None self,
ip_range: str,
type: str | None = None,
network_zone: str | None = None,
gateway: str | None = None,
vswitch_id: int | None = None,
): ):
self.type = type self.type = type
self.ip_range = ip_range self.ip_range = ip_range
@ -111,7 +125,7 @@ class NetworkRoute(BaseDomain):
__slots__ = ("destination", "gateway") __slots__ = ("destination", "gateway")
def __init__(self, destination, gateway): def __init__(self, destination: str, gateway: str):
self.destination = destination self.destination = destination
self.gateway = gateway self.gateway = gateway
@ -129,8 +143,8 @@ class CreateNetworkResponse(BaseDomain):
def __init__( def __init__(
self, self,
network, # type: BoundNetwork network: BoundNetwork,
action, # type: BoundAction action: BoundAction,
): ):
self.network = network self.network = network
self.action = action self.action = action

View file

@ -0,0 +1,8 @@
from __future__ import annotations
from .client import ( # noqa: F401
BoundPlacementGroup,
PlacementGroupsClient,
PlacementGroupsPageResult,
)
from .domain import CreatePlacementGroupResponse, PlacementGroup # noqa: F401

View file

@ -1,13 +1,25 @@
from ..actions.client import BoundAction from __future__ import annotations
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from typing import TYPE_CHECKING, Any, NamedTuple
from ..actions import BoundAction
from ..core import BoundModelBase, ClientEntityBase, Meta
from .domain import CreatePlacementGroupResponse, PlacementGroup from .domain import CreatePlacementGroupResponse, PlacementGroup
if TYPE_CHECKING:
from .._client import Client
class BoundPlacementGroup(BoundModelBase): class BoundPlacementGroup(BoundModelBase):
_client: PlacementGroupsClient
model = PlacementGroup model = PlacementGroup
def update(self, labels=None, name=None): def update(
# type: (Optional[str], Optional[Dict[str, str]], Optional[str]) -> BoundPlacementGroup self,
labels: dict[str, str] | None = None,
name: str | None = None,
) -> BoundPlacementGroup:
"""Updates the name or labels of a Placement Group """Updates the name or labels of a Placement Group
:param labels: Dict[str, str] (optional) :param labels: Dict[str, str] (optional)
@ -18,8 +30,7 @@ class BoundPlacementGroup(BoundModelBase):
""" """
return self._client.update(self, labels, name) return self._client.update(self, labels, name)
def delete(self): def delete(self) -> bool:
# type: () -> bool
"""Deletes a Placement Group """Deletes a Placement Group
:return: boolean :return: boolean
@ -27,11 +38,15 @@ class BoundPlacementGroup(BoundModelBase):
return self._client.delete(self) return self._client.delete(self)
class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin): class PlacementGroupsPageResult(NamedTuple):
results_list_attribute_name = "placement_groups" placement_groups: list[BoundPlacementGroup]
meta: Meta | None
def get_by_id(self, id):
# type: (int) -> BoundPlacementGroup class PlacementGroupsClient(ClientEntityBase):
_client: Client
def get_by_id(self, id: int) -> BoundPlacementGroup:
"""Returns a specific Placement Group object """Returns a specific Placement Group object
:param id: int :param id: int
@ -45,14 +60,13 @@ class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin):
def get_list( def get_list(
self, self,
label_selector=None, # type: Optional[str] label_selector: str | None = None,
page=None, # type: Optional[int] page: int | None = None,
per_page=None, # type: Optional[int] per_page: int | None = None,
name=None, # type: Optional[str] name: str | None = None,
sort=None, # type: Optional[List[str]] sort: list[str] | None = None,
type=None, # type: Optional[str] type: str | None = None,
): ) -> PlacementGroupsPageResult:
# type: (...) -> PageResults[List[BoundPlacementGroup]]
"""Get a list of Placement Groups """Get a list of Placement Groups
:param label_selector: str (optional) :param label_selector: str (optional)
@ -68,7 +82,7 @@ class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin):
:return: (List[:class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if label_selector is not None: if label_selector is not None:
params["label_selector"] = label_selector params["label_selector"] = label_selector
@ -90,10 +104,14 @@ class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin):
for placement_group_data in response["placement_groups"] 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): def get_all(
# type: (Optional[str], Optional[str], Optional[List[str]]) -> List[BoundPlacementGroup] self,
label_selector: str | None = None,
name: str | None = None,
sort: list[str] | None = None,
) -> list[BoundPlacementGroup]:
"""Get all Placement Groups """Get all Placement Groups
:param label_selector: str (optional) :param label_selector: str (optional)
@ -104,25 +122,28 @@ class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin):
Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default))
:return: List[:class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>`] :return: List[:class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>`]
""" """
return super().get_all(label_selector=label_selector, name=name, sort=sort) return self._iter_pages(
self.get_list,
label_selector=label_selector,
name=name,
sort=sort,
)
def get_by_name(self, name): def get_by_name(self, name: str) -> BoundPlacementGroup | None:
# type: (str) -> BoundPlacementGroup
"""Get Placement Group by name """Get Placement Group by name
:param name: str :param name: str
Used to get Placement Group by name Used to get Placement Group by name
:return: class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>` :return: class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>`
""" """
return super().get_by_name(name) return self._get_first_by(name=name)
def create( def create(
self, self,
name, # type: str name: str,
type, # type: str type: str,
labels=None, # type: Optional[Dict[str, str]] labels: dict[str, str] | None = None,
): ) -> CreatePlacementGroupResponse:
# type: (...) -> CreatePlacementGroupResponse
"""Creates a new Placement Group. """Creates a new Placement Group.
:param name: str :param name: str
@ -134,7 +155,7 @@ class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin):
:return: :class:`CreatePlacementGroupResponse <hcloud.placement_groups.domain.CreatePlacementGroupResponse>` :return: :class:`CreatePlacementGroupResponse <hcloud.placement_groups.domain.CreatePlacementGroupResponse>`
""" """
data = {"name": name, "type": type} data: dict[str, Any] = {"name": name, "type": type}
if labels is not None: if labels is not None:
data["labels"] = labels data["labels"] = labels
response = self._client.request( response = self._client.request(
@ -143,7 +164,7 @@ class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin):
action = None action = None
if response.get("action") is not None: if response.get("action") is not None:
action = BoundAction(self._client.action, response["action"]) action = BoundAction(self._client.actions, response["action"])
result = CreatePlacementGroupResponse( result = CreatePlacementGroupResponse(
placement_group=BoundPlacementGroup(self, response["placement_group"]), placement_group=BoundPlacementGroup(self, response["placement_group"]),
@ -151,8 +172,12 @@ class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin):
) )
return result return result
def update(self, placement_group, labels=None, name=None): def update(
# type: (PlacementGroup, Optional[Dict[str, str]], Optional[str]) -> BoundPlacementGroup self,
placement_group: PlacementGroup | BoundPlacementGroup,
labels: dict[str, str] | None = None,
name: str | None = None,
) -> BoundPlacementGroup:
"""Updates the description or labels of a Placement Group. """Updates the description or labels of a Placement Group.
:param placement_group: :class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>` or :class:`PlacementGroup <hcloud.placement_groups.domain.PlacementGroup>` :param placement_group: :class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>` or :class:`PlacementGroup <hcloud.placement_groups.domain.PlacementGroup>`
@ -163,32 +188,27 @@ class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin):
:return: :class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>` :return: :class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>`
""" """
data = {} data: dict[str, Any] = {}
if labels is not None: if labels is not None:
data["labels"] = labels data["labels"] = labels
if name is not None: if name is not None:
data["name"] = name data["name"] = name
response = self._client.request( response = self._client.request(
url="/placement_groups/{placement_group_id}".format( url=f"/placement_groups/{placement_group.id}",
placement_group_id=placement_group.id
),
method="PUT", method="PUT",
json=data, json=data,
) )
return BoundPlacementGroup(self, response["placement_group"]) return BoundPlacementGroup(self, response["placement_group"])
def delete(self, placement_group): def delete(self, placement_group: PlacementGroup | BoundPlacementGroup) -> bool:
# type: (PlacementGroup) -> bool
"""Deletes a Placement Group. """Deletes a Placement Group.
:param placement_group: :class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>` or :class:`PlacementGroup <hcloud.placement_groups.domain.PlacementGroup>` :param placement_group: :class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>` or :class:`PlacementGroup <hcloud.placement_groups.domain.PlacementGroup>`
:return: boolean :return: boolean
""" """
self._client.request( self._client.request(
url="/placement_groups/{placement_group_id}".format( url=f"/placement_groups/{placement_group.id}",
placement_group_id=placement_group.id
),
method="DELETE", method="DELETE",
) )
return True return True

View file

@ -1,9 +1,17 @@
from __future__ import annotations
from typing import TYPE_CHECKING
try: try:
from dateutil.parser import isoparse from dateutil.parser import isoparse
except ImportError: except ImportError:
isoparse = None isoparse = None
from ..core.domain import BaseDomain from ..core import BaseDomain
if TYPE_CHECKING:
from ..actions import BoundAction
from .client import BoundPlacementGroup
class PlacementGroup(BaseDomain): class PlacementGroup(BaseDomain):
@ -31,7 +39,13 @@ class PlacementGroup(BaseDomain):
TYPE_SPREAD = "spread" TYPE_SPREAD = "spread"
def __init__( def __init__(
self, id=None, name=None, labels=None, servers=None, type=None, created=None self,
id: int | None = None,
name: str | None = None,
labels: dict[str, str] | None = None,
servers: list[int] | None = None,
type: str | None = None,
created: str | None = None,
): ):
self.id = id self.id = id
self.name = name self.name = name
@ -54,8 +68,8 @@ class CreatePlacementGroupResponse(BaseDomain):
def __init__( def __init__(
self, self,
placement_group, # type: BoundPlacementGroup placement_group: BoundPlacementGroup,
action, # type: BoundAction action: BoundAction | None,
): ):
self.placement_group = placement_group self.placement_group = placement_group
self.action = action self.action = action

View file

@ -0,0 +1,4 @@
from __future__ import annotations
from .client import BoundPrimaryIP, PrimaryIPsClient, PrimaryIPsPageResult # noqa: F401
from .domain import CreatePrimaryIPResponse, PrimaryIP # noqa: F401

View file

@ -1,13 +1,23 @@
from ..actions.client import BoundAction from __future__ import annotations
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from typing import TYPE_CHECKING, Any, NamedTuple
from ..actions import BoundAction
from ..core import BoundModelBase, ClientEntityBase, Meta
from .domain import CreatePrimaryIPResponse, PrimaryIP from .domain import CreatePrimaryIPResponse, PrimaryIP
if TYPE_CHECKING:
from .._client import Client
from ..datacenters import BoundDatacenter, Datacenter
class BoundPrimaryIP(BoundModelBase): class BoundPrimaryIP(BoundModelBase):
_client: PrimaryIPsClient
model = PrimaryIP model = PrimaryIP
def __init__(self, client, data, complete=True): def __init__(self, client: PrimaryIPsClient, data: dict, complete: bool = True):
from ..datacenters.client import BoundDatacenter from ..datacenters import BoundDatacenter
datacenter = data.get("datacenter", {}) datacenter = data.get("datacenter", {})
if datacenter: if datacenter:
@ -15,8 +25,12 @@ class BoundPrimaryIP(BoundModelBase):
super().__init__(client, data, complete) super().__init__(client, data, complete)
def update(self, auto_delete=None, labels=None, name=None): def update(
# type: (Optional[bool], Optional[Dict[str, str]], Optional[str]) -> BoundPrimaryIP self,
auto_delete: bool | None = None,
labels: dict[str, str] | None = None,
name: str | None = None,
) -> BoundPrimaryIP:
"""Updates the description or labels of a Primary IP. """Updates the description or labels of a Primary IP.
:param auto_delete: bool (optional) :param auto_delete: bool (optional)
@ -31,16 +45,14 @@ class BoundPrimaryIP(BoundModelBase):
self, auto_delete=auto_delete, labels=labels, name=name self, auto_delete=auto_delete, labels=labels, name=name
) )
def delete(self): def delete(self) -> bool:
# type: () -> bool
"""Deletes a Primary IP. If it is currently assigned to a server it will automatically get unassigned. """Deletes a Primary IP. If it is currently assigned to a server it will automatically get unassigned.
:return: boolean :return: boolean
""" """
return self._client.delete(self) return self._client.delete(self)
def change_protection(self, delete=None): def change_protection(self, delete: bool | None = None) -> BoundAction:
# type: (Optional[bool]) -> BoundAction
"""Changes the protection configuration of the Primary IP. """Changes the protection configuration of the Primary IP.
:param delete: boolean :param delete: boolean
@ -49,8 +61,7 @@ class BoundPrimaryIP(BoundModelBase):
""" """
return self._client.change_protection(self, delete) return self._client.change_protection(self, delete)
def assign(self, assignee_id, assignee_type): def assign(self, assignee_id: int, assignee_type: str) -> BoundAction:
# type: (int,str) -> BoundAction
"""Assigns a Primary IP to a assignee. """Assigns a Primary IP to a assignee.
:param assignee_id: int` :param assignee_id: int`
@ -61,16 +72,14 @@ class BoundPrimaryIP(BoundModelBase):
""" """
return self._client.assign(self, assignee_id, assignee_type) return self._client.assign(self, assignee_id, assignee_type)
def unassign(self): def unassign(self) -> BoundAction:
# type: () -> BoundAction
"""Unassigns a Primary IP, resulting in it being unreachable. You may assign it to a server again at a later time. """Unassigns a Primary IP, resulting in it being unreachable. You may assign it to a server again at a later time.
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
return self._client.unassign(self) return self._client.unassign(self)
def change_dns_ptr(self, ip, dns_ptr): def change_dns_ptr(self, ip: str, dns_ptr: str) -> BoundAction:
# type: (str, str) -> BoundAction
"""Changes the hostname that will appear when getting the hostname belonging to this Primary IP. """Changes the hostname that will appear when getting the hostname belonging to this Primary IP.
:param ip: str :param ip: str
@ -82,11 +91,15 @@ class BoundPrimaryIP(BoundModelBase):
return self._client.change_dns_ptr(self, ip, dns_ptr) return self._client.change_dns_ptr(self, ip, dns_ptr)
class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin): class PrimaryIPsPageResult(NamedTuple):
results_list_attribute_name = "primary_ips" primary_ips: list[BoundPrimaryIP]
meta: Meta | None
def get_by_id(self, id):
# type: (int) -> BoundPrimaryIP class PrimaryIPsClient(ClientEntityBase):
_client: Client
def get_by_id(self, id: int) -> BoundPrimaryIP:
"""Returns a specific Primary IP object. """Returns a specific Primary IP object.
:param id: int :param id: int
@ -97,13 +110,12 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
def get_list( def get_list(
self, self,
label_selector=None, # type: Optional[str] label_selector: str | None = None,
page=None, # type: Optional[int] page: int | None = None,
per_page=None, # type: Optional[int] per_page: int | None = None,
name=None, # type: Optional[str] name: str | None = None,
ip=None, # type: Optional[ip] ip: str | None = None,
): ) -> PrimaryIPsPageResult:
# type: (...) -> PageResults[List[BoundPrimaryIP]]
"""Get a list of primary ips from this account """Get a list of primary ips from this account
:param label_selector: str (optional) :param label_selector: str (optional)
@ -118,7 +130,7 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
Can be used to filter resources by their ip. The response will only contain the resources matching the specified ip. Can be used to filter resources by their ip. The response will only contain the resources matching the specified ip.
:return: (List[:class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if label_selector is not None: if label_selector is not None:
params["label_selector"] = label_selector params["label_selector"] = label_selector
@ -137,10 +149,13 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
for primary_ip_data in response["primary_ips"] 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): def get_all(
# type: (Optional[str], Optional[str]) -> List[BoundPrimaryIP] self,
label_selector: str | None = None,
name: str | None = None,
) -> list[BoundPrimaryIP]:
"""Get all primary ips from this account """Get all primary ips from this account
:param label_selector: str (optional) :param label_selector: str (optional)
@ -149,29 +164,28 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
Can be used to filter networks by their name. Can be used to filter networks by their name.
:return: List[:class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>`] :return: List[:class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>`]
""" """
return super().get_all(label_selector=label_selector, name=name) return self._iter_pages(self.get_list, label_selector=label_selector, name=name)
def get_by_name(self, name): def get_by_name(self, name: str) -> BoundPrimaryIP | None:
# type: (str) -> BoundPrimaryIP
"""Get Primary IP by name """Get Primary IP by name
:param name: str :param name: str
Used to get Primary IP by name. Used to get Primary IP by name.
:return: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` :return: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>`
""" """
return super().get_by_name(name) return self._get_first_by(name=name)
def create( def create(
self, self,
type, # type: str type: str,
datacenter, # type: Datacenter # TODO: Make the datacenter argument optional
name, # type: str datacenter: Datacenter | BoundDatacenter | None,
assignee_type="server", # type: Optional[str] name: str,
assignee_id=None, # type: Optional[int] assignee_type: str | None = "server",
auto_delete=False, # type: Optional[bool] assignee_id: int | None = None,
labels=None, # type: Optional[dict] auto_delete: bool | None = False,
): labels: dict | None = None,
# type: (...) -> CreatePrimaryIPResponse ) -> CreatePrimaryIPResponse:
"""Creates a new Primary IP assigned to a server. """Creates a new Primary IP assigned to a server.
:param type: str :param type: str
@ -186,14 +200,15 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
:return: :class:`CreatePrimaryIPResponse <hcloud.primary_ips.domain.CreatePrimaryIPResponse>` :return: :class:`CreatePrimaryIPResponse <hcloud.primary_ips.domain.CreatePrimaryIPResponse>`
""" """
data = { data: dict[str, Any] = {
"type": type, "type": type,
"assignee_type": assignee_type, "assignee_type": assignee_type,
"auto_delete": auto_delete, "auto_delete": auto_delete,
"datacenter": datacenter.id_or_name,
"name": name, "name": name,
} }
if assignee_id: if datacenter is not None:
data["datacenter"] = datacenter.id_or_name
if assignee_id is not None:
data["assignee_id"] = assignee_id data["assignee_id"] = assignee_id
if labels is not None: if labels is not None:
data["labels"] = labels data["labels"] = labels
@ -209,8 +224,13 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
) )
return result return result
def update(self, primary_ip, auto_delete=None, labels=None, name=None): def update(
# type: (PrimaryIP, Optional[bool], Optional[Dict[str, str]], Optional[str]) -> BoundPrimaryIP self,
primary_ip: PrimaryIP | BoundPrimaryIP,
auto_delete: bool | None = None,
labels: dict[str, str] | None = None,
name: str | None = None,
) -> BoundPrimaryIP:
"""Updates the name, auto_delete or labels of a Primary IP. """Updates the name, auto_delete or labels of a Primary IP.
:param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>` :param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>`
@ -222,7 +242,7 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
New name to set New name to set
:return: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` :return: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>`
""" """
data = {} data: dict[str, Any] = {}
if auto_delete is not None: if auto_delete is not None:
data["auto_delete"] = auto_delete data["auto_delete"] = auto_delete
if labels is not None: if labels is not None:
@ -237,8 +257,7 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
) )
return BoundPrimaryIP(self, response["primary_ip"]) return BoundPrimaryIP(self, response["primary_ip"])
def delete(self, primary_ip): def delete(self, primary_ip: PrimaryIP | BoundPrimaryIP) -> bool:
# type: (PrimaryIP) -> bool
"""Deletes a Primary IP. If it is currently assigned to an assignee it will automatically get unassigned. """Deletes a Primary IP. If it is currently assigned to an assignee it will automatically get unassigned.
:param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>` :param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>`
@ -251,8 +270,11 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
# Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised
return True return True
def change_protection(self, primary_ip, delete=None): def change_protection(
# type: (PrimaryIP, Optional[bool]) -> BoundAction self,
primary_ip: PrimaryIP | BoundPrimaryIP,
delete: bool | None = None,
) -> BoundAction:
"""Changes the protection configuration of the Primary IP. """Changes the protection configuration of the Primary IP.
:param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>` :param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>`
@ -260,21 +282,23 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
If true, prevents the Primary IP from being deleted If true, prevents the Primary IP from being deleted
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {} data: dict[str, Any] = {}
if delete is not None: if delete is not None:
data.update({"delete": delete}) data.update({"delete": delete})
response = self._client.request( response = self._client.request(
url="/primary_ips/{primary_ip_id}/actions/change_protection".format( url=f"/primary_ips/{primary_ip.id}/actions/change_protection",
primary_ip_id=primary_ip.id
),
method="POST", method="POST",
json=data, json=data,
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def assign(self, primary_ip, assignee_id, assignee_type="server"): def assign(
# type: (PrimaryIP, int, str) -> BoundAction self,
primary_ip: PrimaryIP | BoundPrimaryIP,
assignee_id: int,
assignee_type: str = "server",
) -> BoundAction:
"""Assigns a Primary IP to a assignee_id. """Assigns a Primary IP to a assignee_id.
:param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>` :param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>`
@ -285,31 +309,30 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
response = self._client.request( response = self._client.request(
url="/primary_ips/{primary_ip_id}/actions/assign".format( url=f"/primary_ips/{primary_ip.id}/actions/assign",
primary_ip_id=primary_ip.id
),
method="POST", method="POST",
json={"assignee_id": assignee_id, "assignee_type": assignee_type}, json={"assignee_id": assignee_id, "assignee_type": assignee_type},
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def unassign(self, primary_ip): def unassign(self, primary_ip: PrimaryIP | BoundPrimaryIP) -> BoundAction:
# type: (PrimaryIP) -> BoundAction
"""Unassigns a Primary IP, resulting in it being unreachable. You may assign it to a server again at a later time. """Unassigns a Primary IP, resulting in it being unreachable. You may assign it to a server again at a later time.
:param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>` :param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>`
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
response = self._client.request( response = self._client.request(
url="/primary_ips/{primary_ip_id}/actions/unassign".format( url=f"/primary_ips/{primary_ip.id}/actions/unassign",
primary_ip_id=primary_ip.id
),
method="POST", method="POST",
) )
return BoundAction(self._client.actions, response["action"]) return BoundAction(self._client.actions, response["action"])
def change_dns_ptr(self, primary_ip, ip, dns_ptr): def change_dns_ptr(
# type: (PrimaryIP, str, str) -> BoundAction self,
primary_ip: PrimaryIP | BoundPrimaryIP,
ip: str,
dns_ptr: str,
) -> BoundAction:
"""Changes the dns ptr that will appear when getting the dns ptr belonging to this Primary IP. """Changes the dns ptr that will appear when getting the dns ptr belonging to this Primary IP.
:param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>` :param primary_ip: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>` or :class:`PrimaryIP <hcloud.primary_ips.domain.PrimaryIP>`
@ -320,9 +343,7 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
response = self._client.request( response = self._client.request(
url="/primary_ips/{primary_ip_id}/actions/change_dns_ptr".format( url=f"/primary_ips/{primary_ip.id}/actions/change_dns_ptr",
primary_ip_id=primary_ip.id
),
method="POST", method="POST",
json={"ip": ip, "dns_ptr": dns_ptr}, json={"ip": ip, "dns_ptr": dns_ptr},
) )

View file

@ -1,9 +1,18 @@
from __future__ import annotations
from typing import TYPE_CHECKING
try: try:
from dateutil.parser import isoparse from dateutil.parser import isoparse
except ImportError: except ImportError:
isoparse = None isoparse = None
from ..core.domain import BaseDomain from ..core import BaseDomain
if TYPE_CHECKING:
from ..actions import BoundAction
from ..datacenters import BoundDatacenter
from .client import BoundPrimaryIP
class PrimaryIP(BaseDomain): class PrimaryIP(BaseDomain):
@ -55,19 +64,19 @@ class PrimaryIP(BaseDomain):
def __init__( def __init__(
self, self,
id=None, id: int | None = None,
type=None, type: str | None = None,
ip=None, ip: str | None = None,
dns_ptr=None, dns_ptr: list[dict] | None = None,
datacenter=None, datacenter: BoundDatacenter | None = None,
blocked=None, blocked: bool | None = None,
protection=None, protection: dict | None = None,
labels=None, labels: dict[str, dict] | None = None,
created=None, created: str | None = None,
name=None, name: str | None = None,
assignee_id=None, assignee_id: int | None = None,
assignee_type=None, assignee_type: str | None = None,
auto_delete=None, auto_delete: bool | None = None,
): ):
self.id = id self.id = id
self.type = type self.type = type
@ -97,8 +106,8 @@ class CreatePrimaryIPResponse(BaseDomain):
def __init__( def __init__(
self, self,
primary_ip, # type: BoundPrimaryIP primary_ip: BoundPrimaryIP,
action, # type: BoundAction action: BoundAction | None,
): ):
self.primary_ip = primary_ip self.primary_ip = primary_ip
self.action = action self.action = action

View file

@ -0,0 +1 @@
# Marker file for PEP 561.

View file

@ -0,0 +1,8 @@
from __future__ import annotations
from .client import ( # noqa: F401
BoundServerType,
ServerTypesClient,
ServerTypesPageResult,
)
from .domain import ServerType # noqa: F401

View file

@ -1,16 +1,29 @@
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from __future__ import annotations
from typing import TYPE_CHECKING, Any, NamedTuple
from ..core import BoundModelBase, ClientEntityBase, Meta
from .domain import ServerType from .domain import ServerType
if TYPE_CHECKING:
from .._client import Client
class BoundServerType(BoundModelBase): class BoundServerType(BoundModelBase):
_client: ServerTypesClient
model = ServerType model = ServerType
class ServerTypesClient(ClientEntityBase, GetEntityByNameMixin): class ServerTypesPageResult(NamedTuple):
results_list_attribute_name = "server_types" server_types: list[BoundServerType]
meta: Meta | None
def get_by_id(self, id):
# type: (int) -> BoundServerType class ServerTypesClient(ClientEntityBase):
_client: Client
def get_by_id(self, id: int) -> BoundServerType:
"""Returns a specific Server Type. """Returns a specific Server Type.
:param id: int :param id: int
@ -19,8 +32,12 @@ class ServerTypesClient(ClientEntityBase, GetEntityByNameMixin):
response = self._client.request(url=f"/server_types/{id}", method="GET") response = self._client.request(url=f"/server_types/{id}", method="GET")
return BoundServerType(self, response["server_type"]) return BoundServerType(self, response["server_type"])
def get_list(self, name=None, page=None, per_page=None): def get_list(
# type: (Optional[str], Optional[int], Optional[int]) -> PageResults[List[BoundServerType], Meta] self,
name: str | None = None,
page: int | None = None,
per_page: int | None = None,
) -> ServerTypesPageResult:
"""Get a list of Server types """Get a list of Server types
:param name: str (optional) :param name: str (optional)
@ -31,7 +48,7 @@ class ServerTypesClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page Specifies how many results are returned by page
:return: (List[:class:`BoundServerType <hcloud.server_types.client.BoundServerType>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundServerType <hcloud.server_types.client.BoundServerType>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if name is not None: if name is not None:
params["name"] = name params["name"] = name
if page is not None: if page is not None:
@ -46,24 +63,22 @@ class ServerTypesClient(ClientEntityBase, GetEntityByNameMixin):
BoundServerType(self, server_type_data) BoundServerType(self, server_type_data)
for server_type_data in response["server_types"] 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): def get_all(self, name: str | None = None) -> list[BoundServerType]:
# type: (Optional[str]) -> List[BoundServerType]
"""Get all Server types """Get all Server types
:param name: str (optional) :param name: str (optional)
Can be used to filter server type by their name. Can be used to filter server type by their name.
:return: List[:class:`BoundServerType <hcloud.server_types.client.BoundServerType>`] :return: List[:class:`BoundServerType <hcloud.server_types.client.BoundServerType>`]
""" """
return super().get_all(name=name) return self._iter_pages(self.get_list, name=name)
def get_by_name(self, name): def get_by_name(self, name: str) -> BoundServerType | None:
# type: (str) -> BoundServerType
"""Get Server type by name """Get Server type by name
:param name: str :param name: str
Used to get Server type by name. Used to get Server type by name.
:return: :class:`BoundServerType <hcloud.server_types.client.BoundServerType>` :return: :class:`BoundServerType <hcloud.server_types.client.BoundServerType>`
""" """
return super().get_by_name(name) return self._get_first_by(name=name)

View file

@ -1,5 +1,7 @@
from ..core.domain import BaseDomain, DomainIdentityMixin from __future__ import annotations
from ..deprecation.domain import DeprecationInfo
from ..core import BaseDomain, DomainIdentityMixin
from ..deprecation import DeprecationInfo
class ServerType(BaseDomain, DomainIdentityMixin): class ServerType(BaseDomain, DomainIdentityMixin):
@ -52,19 +54,19 @@ class ServerType(BaseDomain, DomainIdentityMixin):
def __init__( def __init__(
self, self,
id=None, id: int | None = None,
name=None, name: str | None = None,
description=None, description: str | None = None,
cores=None, cores: int | None = None,
memory=None, memory: int | None = None,
disk=None, disk: int | None = None,
prices=None, prices: dict | None = None,
storage_type=None, storage_type: str | None = None,
cpu_type=None, cpu_type: str | None = None,
architecture=None, architecture: str | None = None,
deprecated=None, deprecated: bool | None = None,
deprecation=None, deprecation: dict | None = None,
included_traffic=None, included_traffic: int | None = None,
): ):
self.id = id self.id = id
self.name = name self.name = name

View file

@ -0,0 +1,16 @@
from __future__ import annotations
from .client import BoundServer, ServersClient, ServersPageResult # noqa: F401
from .domain import ( # noqa: F401
CreateServerResponse,
EnableRescueResponse,
IPv4Address,
IPv6Network,
PrivateNet,
PublicNetwork,
PublicNetworkFirewall,
RequestConsoleResponse,
ResetPasswordResponse,
Server,
ServerCreatePublicNetwork,
)

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,27 @@
from __future__ import annotations
from typing import TYPE_CHECKING
try: try:
from dateutil.parser import isoparse from dateutil.parser import isoparse
except ImportError: except ImportError:
isoparse = None isoparse = None
from ..core.domain import BaseDomain from ..core import BaseDomain
if TYPE_CHECKING:
from ..actions import BoundAction
from ..datacenters import BoundDatacenter
from ..firewalls import BoundFirewall
from ..floating_ips import BoundFloatingIP
from ..images import BoundImage
from ..isos import BoundIso
from ..networks import BoundNetwork
from ..placement_groups import BoundPlacementGroup
from ..primary_ips import BoundPrimaryIP, PrimaryIP
from ..server_types import BoundServerType
from ..volumes import BoundVolume
from .client import BoundServer
class Server(BaseDomain): class Server(BaseDomain):
@ -91,27 +109,27 @@ class Server(BaseDomain):
def __init__( def __init__(
self, self,
id, id: int,
name=None, name: str | None = None,
status=None, status: str | None = None,
created=None, created: str | None = None,
public_net=None, public_net: PublicNetwork | None = None,
server_type=None, server_type: BoundServerType | None = None,
datacenter=None, datacenter: BoundDatacenter | None = None,
image=None, image: BoundImage | None = None,
iso=None, iso: BoundIso | None = None,
rescue_enabled=None, rescue_enabled: bool | None = None,
locked=None, locked: bool | None = None,
backup_window=None, backup_window: str | None = None,
outgoing_traffic=None, outgoing_traffic: int | None = None,
ingoing_traffic=None, ingoing_traffic: int | None = None,
included_traffic=None, included_traffic: int | None = None,
protection=None, protection: dict | None = None,
labels=None, labels: dict[str, str] | None = None,
volumes=None, volumes: list[BoundVolume] | None = None,
private_net=None, private_net: list[PrivateNet] | None = None,
primary_disk_size=None, primary_disk_size: int | None = None,
placement_group=None, placement_group: BoundPlacementGroup | None = None,
): ):
self.id = id self.id = id
self.name = name self.name = name
@ -153,10 +171,10 @@ class CreateServerResponse(BaseDomain):
def __init__( def __init__(
self, self,
server, # type: BoundServer server: BoundServer,
action, # type: BoundAction action: BoundAction,
next_actions, # type: List[Action] next_actions: list[BoundAction],
root_password, # type: str root_password: str | None,
): ):
self.server = server self.server = server
self.action = action self.action = action
@ -177,8 +195,8 @@ class ResetPasswordResponse(BaseDomain):
def __init__( def __init__(
self, self,
action, # type: BoundAction action: BoundAction,
root_password, # type: str root_password: str,
): ):
self.action = action self.action = action
self.root_password = root_password self.root_password = root_password
@ -197,8 +215,8 @@ class EnableRescueResponse(BaseDomain):
def __init__( def __init__(
self, self,
action, # type: BoundAction action: BoundAction,
root_password, # type: str root_password: str,
): ):
self.action = action self.action = action
self.root_password = root_password self.root_password = root_password
@ -219,9 +237,9 @@ class RequestConsoleResponse(BaseDomain):
def __init__( def __init__(
self, self,
action, # type: BoundAction action: BoundAction,
wss_url, # type: str wss_url: str,
password, # type: str password: str,
): ):
self.action = action self.action = action
self.wss_url = wss_url self.wss_url = wss_url
@ -250,12 +268,12 @@ class PublicNetwork(BaseDomain):
def __init__( def __init__(
self, self,
ipv4, # type: IPv4Address ipv4: IPv4Address,
ipv6, # type: IPv6Network ipv6: IPv6Network,
floating_ips, # type: List[BoundFloatingIP] floating_ips: list[BoundFloatingIP],
primary_ipv4, # type: BoundPrimaryIP primary_ipv4: BoundPrimaryIP | None,
primary_ipv6, # type: BoundPrimaryIP primary_ipv6: BoundPrimaryIP | None,
firewalls=None, # type: List[PublicNetworkFirewall] firewalls: list[PublicNetworkFirewall] | None = None,
): ):
self.ipv4 = ipv4 self.ipv4 = ipv4
self.ipv6 = ipv6 self.ipv6 = ipv6
@ -281,8 +299,8 @@ class PublicNetworkFirewall(BaseDomain):
def __init__( def __init__(
self, self,
firewall, # type: BoundFirewall firewall: BoundFirewall,
status, # type: str status: str,
): ):
self.firewall = firewall self.firewall = firewall
self.status = status self.status = status
@ -303,9 +321,9 @@ class IPv4Address(BaseDomain):
def __init__( def __init__(
self, self,
ip, # type: str ip: str,
blocked, # type: bool blocked: bool,
dns_ptr, # type: str dns_ptr: str,
): ):
self.ip = ip self.ip = ip
self.blocked = blocked self.blocked = blocked
@ -331,9 +349,9 @@ class IPv6Network(BaseDomain):
def __init__( def __init__(
self, self,
ip, # type: str ip: str,
blocked, # type: bool blocked: bool,
dns_ptr, # type: list dns_ptr: list,
): ):
self.ip = ip self.ip = ip
self.blocked = blocked self.blocked = blocked
@ -360,10 +378,10 @@ class PrivateNet(BaseDomain):
def __init__( def __init__(
self, self,
network, # type: BoundNetwork network: BoundNetwork,
ip, # type: str ip: str,
alias_ips, # type: List[str] alias_ips: list[str],
mac_address, # type: str mac_address: str,
): ):
self.network = network self.network = network
self.ip = ip self.ip = ip
@ -384,10 +402,10 @@ class ServerCreatePublicNetwork(BaseDomain):
def __init__( def __init__(
self, self,
ipv4=None, # type: hcloud.primary_ips.domain.PrimaryIP ipv4: PrimaryIP | None = None,
ipv6=None, # type: hcloud.primary_ips.domain.PrimaryIP ipv6: PrimaryIP | None = None,
enable_ipv4=True, # type: bool enable_ipv4: bool = True,
enable_ipv6=True, # type: bool enable_ipv6: bool = True,
): ):
self.ipv4 = ipv4 self.ipv4 = ipv4
self.ipv6 = ipv6 self.ipv6 = ipv6

View file

@ -0,0 +1,4 @@
from __future__ import annotations
from .client import BoundSSHKey, SSHKeysClient, SSHKeysPageResult # noqa: F401
from .domain import SSHKey # noqa: F401

View file

@ -1,12 +1,24 @@
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from __future__ import annotations
from typing import TYPE_CHECKING, Any, NamedTuple
from ..core import BoundModelBase, ClientEntityBase, Meta
from .domain import SSHKey from .domain import SSHKey
if TYPE_CHECKING:
from .._client import Client
class BoundSSHKey(BoundModelBase): class BoundSSHKey(BoundModelBase):
_client: SSHKeysClient
model = SSHKey model = SSHKey
def update(self, name=None, labels=None): def update(
# type: (Optional[str], Optional[Dict[str, str]]) -> BoundSSHKey self,
name: str | None = None,
labels: dict[str, str] | None = None,
) -> BoundSSHKey:
"""Updates an SSH key. You can update an SSH key name and an SSH key labels. """Updates an SSH key. You can update an SSH key name and an SSH key labels.
:param description: str (optional) :param description: str (optional)
@ -17,19 +29,22 @@ class BoundSSHKey(BoundModelBase):
""" """
return self._client.update(self, name, labels) return self._client.update(self, name, labels)
def delete(self): def delete(self) -> bool:
# type: () -> bool
"""Deletes an SSH key. It cannot be used anymore. """Deletes an SSH key. It cannot be used anymore.
:return: boolean :return: boolean
""" """
return self._client.delete(self) return self._client.delete(self)
class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin): class SSHKeysPageResult(NamedTuple):
results_list_attribute_name = "ssh_keys" ssh_keys: list[BoundSSHKey]
meta: Meta | None
def get_by_id(self, id):
# type: (int) -> BoundSSHKey class SSHKeysClient(ClientEntityBase):
_client: Client
def get_by_id(self, id: int) -> BoundSSHKey:
"""Get a specific SSH Key by its ID """Get a specific SSH Key by its ID
:param id: int :param id: int
@ -40,13 +55,12 @@ class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin):
def get_list( def get_list(
self, self,
name=None, # type: Optional[str] name: str | None = None,
fingerprint=None, # type: Optional[str] fingerprint: str | None = None,
label_selector=None, # type: Optional[str] label_selector: str | None = None,
page=None, # type: Optional[int] page: int | None = None,
per_page=None, # type: Optional[int] per_page: int | None = None,
): ) -> SSHKeysPageResult:
# type: (...) -> PageResults[List[BoundSSHKey], Meta]
"""Get a list of SSH keys from the account """Get a list of SSH keys from the account
:param name: str (optional) :param name: str (optional)
@ -61,7 +75,7 @@ class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page Specifies how many results are returned by page
:return: (List[:class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if name is not None: if name is not None:
params["name"] = name params["name"] = name
if fingerprint is not None: if fingerprint is not None:
@ -75,13 +89,17 @@ class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin):
response = self._client.request(url="/ssh_keys", method="GET", params=params) 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"] 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): def get_all(
# type: (Optional[str], Optional[str], Optional[str]) -> List[BoundSSHKey] self,
name: str | None = None,
fingerprint: str | None = None,
label_selector: str | None = None,
) -> list[BoundSSHKey]:
"""Get all SSH keys from the account """Get all SSH keys from the account
:param name: str (optional) :param name: str (optional)
@ -92,34 +110,37 @@ class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin):
Can be used to filter SSH keys by labels. The response will only contain SSH keys matching the label selector. Can be used to filter SSH keys by labels. The response will only contain SSH keys matching the label selector.
:return: List[:class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>`] :return: List[:class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>`]
""" """
return super().get_all( return self._iter_pages(
name=name, fingerprint=fingerprint, label_selector=label_selector self.get_list,
name=name,
fingerprint=fingerprint,
label_selector=label_selector,
) )
def get_by_name(self, name): def get_by_name(self, name: str) -> BoundSSHKey | None:
# type: (str) -> SSHKeysClient
"""Get ssh key by name """Get ssh key by name
:param name: str :param name: str
Used to get ssh key by name. Used to get ssh key by name.
:return: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>` :return: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>`
""" """
return super().get_by_name(name) return self._get_first_by(name=name)
def get_by_fingerprint(self, fingerprint): def get_by_fingerprint(self, fingerprint: str) -> BoundSSHKey | None:
# type: (str) -> BoundSSHKey
"""Get ssh key by fingerprint """Get ssh key by fingerprint
:param fingerprint: str :param fingerprint: str
Used to get ssh key by fingerprint. Used to get ssh key by fingerprint.
:return: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>` :return: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>`
""" """
response = self.get_list(fingerprint=fingerprint) return self._get_first_by(fingerprint=fingerprint)
sshkeys = response.ssh_keys
return sshkeys[0] if sshkeys else None
def create(self, name, public_key, labels=None): def create(
# type: (str, str, Optional[Dict[str, str]]) -> BoundSSHKey self,
name: str,
public_key: str,
labels: dict[str, str] | None = None,
) -> BoundSSHKey:
"""Creates a new SSH key with the given name and public_key. """Creates a new SSH key with the given name and public_key.
:param name: str :param name: str
@ -129,14 +150,18 @@ class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin):
User-defined labels (key-value pairs) User-defined labels (key-value pairs)
:return: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>` :return: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>`
""" """
data = {"name": name, "public_key": public_key} data: dict[str, Any] = {"name": name, "public_key": public_key}
if labels is not None: if labels is not None:
data["labels"] = labels data["labels"] = labels
response = self._client.request(url="/ssh_keys", method="POST", json=data) response = self._client.request(url="/ssh_keys", method="POST", json=data)
return BoundSSHKey(self, response["ssh_key"]) return BoundSSHKey(self, response["ssh_key"])
def update(self, ssh_key, name=None, labels=None): def update(
# type: (SSHKey, Optional[str], Optional[Dict[str, str]]) -> BoundSSHKey self,
ssh_key: SSHKey | BoundSSHKey,
name: str | None = None,
labels: dict[str, str] | None = None,
) -> BoundSSHKey:
"""Updates an SSH key. You can update an SSH key name and an SSH key labels. """Updates an SSH key. You can update an SSH key name and an SSH key labels.
:param ssh_key: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>` or :class:`SSHKey <hcloud.ssh_keys.domain.SSHKey>` :param ssh_key: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>` or :class:`SSHKey <hcloud.ssh_keys.domain.SSHKey>`
@ -146,7 +171,7 @@ class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin):
User-defined labels (key-value pairs) User-defined labels (key-value pairs)
:return: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>` :return: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>`
""" """
data = {} data: dict[str, Any] = {}
if name is not None: if name is not None:
data["name"] = name data["name"] = name
if labels is not None: if labels is not None:
@ -158,13 +183,12 @@ class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin):
) )
return BoundSSHKey(self, response["ssh_key"]) return BoundSSHKey(self, response["ssh_key"])
def delete(self, ssh_key): def delete(self, ssh_key: SSHKey | BoundSSHKey) -> bool:
# type: (SSHKey) -> bool
self._client.request(url=f"/ssh_keys/{ssh_key.id}", method="DELETE")
"""Deletes an SSH key. It cannot be used anymore. """Deletes an SSH key. It cannot be used anymore.
:param ssh_key: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>` or :class:`SSHKey <hcloud.ssh_keys.domain.SSHKey>` :param ssh_key: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>` or :class:`SSHKey <hcloud.ssh_keys.domain.SSHKey>`
:return: True :return: True
""" """
self._client.request(url=f"/ssh_keys/{ssh_key.id}", method="DELETE")
# Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised
return True return True

View file

@ -1,9 +1,11 @@
from __future__ import annotations
try: try:
from dateutil.parser import isoparse from dateutil.parser import isoparse
except ImportError: except ImportError:
isoparse = None isoparse = None
from ..core.domain import BaseDomain, DomainIdentityMixin from ..core import BaseDomain, DomainIdentityMixin
class SSHKey(BaseDomain, DomainIdentityMixin): class SSHKey(BaseDomain, DomainIdentityMixin):
@ -27,12 +29,12 @@ class SSHKey(BaseDomain, DomainIdentityMixin):
def __init__( def __init__(
self, self,
id=None, id: int | None = None,
name=None, name: str | None = None,
fingerprint=None, fingerprint: str | None = None,
public_key=None, public_key: str | None = None,
labels=None, labels: dict[str, str] | None = None,
created=None, created: str | None = None,
): ):
self.id = id self.id = id
self.name = name self.name = name

View file

@ -0,0 +1,4 @@
from __future__ import annotations
from .client import BoundVolume, VolumesClient, VolumesPageResult # noqa: F401
from .domain import CreateVolumeResponse, Volume # noqa: F401

View file

@ -1,19 +1,29 @@
from ..actions.client import BoundAction from __future__ import annotations
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result from typing import TYPE_CHECKING, Any, NamedTuple
from ..locations.client import BoundLocation
from ..actions import ActionsPageResult, BoundAction
from ..core import BoundModelBase, ClientEntityBase, Meta
from ..locations import BoundLocation
from .domain import CreateVolumeResponse, Volume from .domain import CreateVolumeResponse, Volume
if TYPE_CHECKING:
from .._client import Client
from ..locations import Location
from ..servers import BoundServer, Server
class BoundVolume(BoundModelBase): class BoundVolume(BoundModelBase):
_client: VolumesClient
model = Volume model = Volume
def __init__(self, client, data, complete=True): def __init__(self, client: VolumesClient, data: dict, complete: bool = True):
location = data.get("location") location = data.get("location")
if location is not None: if location is not None:
data["location"] = BoundLocation(client._client.locations, location) data["location"] = BoundLocation(client._client.locations, location)
from ..servers.client import BoundServer from ..servers import BoundServer
server = data.get("server") server = data.get("server")
if server is not None: if server is not None:
@ -22,8 +32,13 @@ class BoundVolume(BoundModelBase):
) )
super().__init__(client, data, complete) super().__init__(client, data, complete)
def get_actions_list(self, status=None, sort=None, page=None, per_page=None): def get_actions_list(
# type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]] self,
status: list[str] | None = None,
sort: list[str] | None = None,
page: int | None = None,
per_page: int | None = None,
) -> ActionsPageResult:
"""Returns all action objects for a volume. """Returns all action objects for a volume.
:param status: List[str] (optional) :param status: List[str] (optional)
@ -38,8 +53,11 @@ class BoundVolume(BoundModelBase):
""" """
return self._client.get_actions_list(self, status, sort, page, per_page) return self._client.get_actions_list(self, status, sort, page, per_page)
def get_actions(self, status=None, sort=None): def get_actions(
# type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] self,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a volume. """Returns all action objects for a volume.
:param status: List[str] (optional) :param status: List[str] (optional)
@ -50,8 +68,11 @@ class BoundVolume(BoundModelBase):
""" """
return self._client.get_actions(self, status, sort) return self._client.get_actions(self, status, sort)
def update(self, name=None, labels=None): def update(
# type: (Optional[str], Optional[Dict[str, str]]) -> BoundAction self,
name: str | None = None,
labels: dict[str, str] | None = None,
) -> BoundVolume:
"""Updates the volume properties. """Updates the volume properties.
:param name: str (optional) :param name: str (optional)
@ -62,16 +83,18 @@ class BoundVolume(BoundModelBase):
""" """
return self._client.update(self, name, labels) return self._client.update(self, name, labels)
def delete(self): def delete(self) -> bool:
# type: () -> BoundAction
"""Deletes a volume. All volume data is irreversibly destroyed. The volume must not be attached to a server and it must not have delete protection enabled. """Deletes a volume. All volume data is irreversibly destroyed. The volume must not be attached to a server and it must not have delete protection enabled.
:return: boolean :return: boolean
""" """
return self._client.delete(self) return self._client.delete(self)
def attach(self, server, automount=None): def attach(
# type: (Union[Server, BoundServer], Optional[bool]) -> BoundAction self,
server: Server | BoundServer,
automount: bool | None = None,
) -> BoundAction:
"""Attaches a volume to a server. Works only if the server is in the same location as the volume. """Attaches a volume to a server. Works only if the server is in the same location as the volume.
:param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>` :param server: :class:`BoundServer <hcloud.servers.client.BoundServer>` or :class:`Server <hcloud.servers.domain.Server>`
@ -80,16 +103,14 @@ class BoundVolume(BoundModelBase):
""" """
return self._client.attach(self, server, automount) return self._client.attach(self, server, automount)
def detach(self): def detach(self) -> BoundAction:
# type: () -> BoundAction
"""Detaches a volume from the server its attached to. You may attach it to a server again at a later time. """Detaches a volume from the server its attached to. You may attach it to a server again at a later time.
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
return self._client.detach(self) return self._client.detach(self)
def resize(self, size): def resize(self, size: int) -> BoundAction:
# type: (int) -> BoundAction
"""Changes the size of a volume. Note that downsizing a volume is not possible. """Changes the size of a volume. Note that downsizing a volume is not possible.
:param size: int :param size: int
@ -98,8 +119,7 @@ class BoundVolume(BoundModelBase):
""" """
return self._client.resize(self, size) return self._client.resize(self, size)
def change_protection(self, delete=None): def change_protection(self, delete: bool | None = None) -> BoundAction:
# type: (Optional[bool]) -> BoundAction
"""Changes the protection configuration of a volume. """Changes the protection configuration of a volume.
:param delete: boolean :param delete: boolean
@ -109,11 +129,15 @@ class BoundVolume(BoundModelBase):
return self._client.change_protection(self, delete) return self._client.change_protection(self, delete)
class VolumesClient(ClientEntityBase, GetEntityByNameMixin): class VolumesPageResult(NamedTuple):
results_list_attribute_name = "volumes" volumes: list[BoundVolume]
meta: Meta | None
def get_by_id(self, id):
# type: (int) -> volumes.client.BoundVolume class VolumesClient(ClientEntityBase):
_client: Client
def get_by_id(self, id: int) -> BoundVolume:
"""Get a specific volume by its id """Get a specific volume by its id
:param id: int :param id: int
@ -123,9 +147,13 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
return BoundVolume(self, response["volume"]) return BoundVolume(self, response["volume"])
def get_list( def get_list(
self, name=None, label_selector=None, page=None, per_page=None, status=None self,
): name: str | None = None,
# type: (Optional[str], Optional[str], Optional[int], Optional[int], Optional[List[str]]) -> PageResults[List[BoundVolume], Meta] label_selector: str | None = None,
page: int | None = None,
per_page: int | None = None,
status: list[str] | None = None,
) -> VolumesPageResult:
"""Get a list of volumes from this account """Get a list of volumes from this account
:param name: str (optional) :param name: str (optional)
@ -140,7 +168,7 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page Specifies how many results are returned by page
:return: (List[:class:`BoundVolume <hcloud.volumes.client.BoundVolume>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundVolume <hcloud.volumes.client.BoundVolume>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if name is not None: if name is not None:
params["name"] = name params["name"] = name
if label_selector is not None: if label_selector is not None:
@ -156,10 +184,13 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
volumes = [ volumes = [
BoundVolume(self, volume_data) for volume_data in response["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): def get_all(
# type: (Optional[str], Optional[List[str]]) -> List[BoundVolume] self,
label_selector: str | None = None,
status: list[str] | None = None,
) -> list[BoundVolume]:
"""Get all volumes from this account """Get all volumes from this account
:param label_selector: :param label_selector:
@ -168,29 +199,31 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
Can be used to filter volumes by their status. The response will only contain volumes matching the status. Can be used to filter volumes by their status. The response will only contain volumes matching the status.
:return: List[:class:`BoundVolume <hcloud.volumes.client.BoundVolume>`] :return: List[:class:`BoundVolume <hcloud.volumes.client.BoundVolume>`]
""" """
return super().get_all(label_selector=label_selector, status=status) return self._iter_pages(
self.get_list,
label_selector=label_selector,
status=status,
)
def get_by_name(self, name): def get_by_name(self, name: str) -> BoundVolume | None:
# type: (str) -> BoundVolume
"""Get volume by name """Get volume by name
:param name: str :param name: str
Used to get volume by name. Used to get volume by name.
:return: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` :return: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>`
""" """
return super().get_by_name(name) return self._get_first_by(name=name)
def create( def create(
self, self,
size, # type: int size: int,
name, # type: str name: str,
labels=None, # type: Optional[str] labels: str | None = None,
location=None, # type: Optional[Location] location: Location | None = None,
server=None, # type: Optional[Server], server: Server | None = None,
automount=None, # type: Optional[bool], automount: bool | None = None,
format=None, # type: Optional[str], format: str | None = None,
): ) -> CreateVolumeResponse:
# type: (...) -> CreateVolumeResponse
"""Creates a new volume attached to a server. """Creates a new volume attached to a server.
:param size: int :param size: int
@ -214,7 +247,7 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
if not (bool(location) ^ bool(server)): if not (bool(location) ^ bool(server)):
raise ValueError("only one of server or location must be provided") raise ValueError("only one of server or location must be provided")
data = {"name": name, "size": size} data: dict[str, Any] = {"name": name, "size": size}
if labels is not None: if labels is not None:
data["labels"] = labels data["labels"] = labels
if location is not None: if location is not None:
@ -240,9 +273,13 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
return result return result
def get_actions_list( def get_actions_list(
self, volume, status=None, sort=None, page=None, per_page=None self,
): volume: Volume | BoundVolume,
# type: (Volume, Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta] status: list[str] | None = None,
sort: list[str] | None = None,
page: int | None = None,
per_page: int | None = None,
) -> ActionsPageResult:
"""Returns all action objects for a volume. """Returns all action objects for a volume.
:param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>` :param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>`
@ -256,7 +293,7 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page Specifies how many results are returned by page
:return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`) :return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`)
""" """
params = {} params: dict[str, Any] = {}
if status is not None: if status is not None:
params["status"] = status params["status"] = status
if sort is not None: if sort is not None:
@ -275,10 +312,14 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
BoundAction(self._client.actions, action_data) BoundAction(self._client.actions, action_data)
for action_data in response["actions"] 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): def get_actions(
# type: (Union[Volume, BoundVolume], Optional[List[str]], Optional[List[str]]) -> List[BoundAction] self,
volume: Volume | BoundVolume,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a volume. """Returns all action objects for a volume.
:param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>` :param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>`
@ -288,10 +329,19 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc`
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`] :return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
""" """
return super().get_actions(volume, status=status, sort=sort) return self._iter_pages(
self.get_actions_list,
volume,
status=status,
sort=sort,
)
def update(self, volume, name=None, labels=None): def update(
# type:(Union[Volume, BoundVolume], Optional[str], Optional[Dict[str, str]]) -> BoundVolume self,
volume: Volume | BoundVolume,
name: str | None = None,
labels: dict[str, str] | None = None,
) -> BoundVolume:
"""Updates the volume properties. """Updates the volume properties.
:param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>` :param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>`
@ -301,7 +351,7 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
User-defined labels (key-value pairs) User-defined labels (key-value pairs)
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {} data: dict[str, Any] = {}
if name is not None: if name is not None:
data.update({"name": name}) data.update({"name": name})
if labels is not None: if labels is not None:
@ -313,8 +363,7 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
) )
return BoundVolume(self, response["volume"]) return BoundVolume(self, response["volume"])
def delete(self, volume): def delete(self, volume: Volume | BoundVolume) -> bool:
# type: (Union[Volume, BoundVolume]) -> BoundAction
"""Deletes a volume. All volume data is irreversibly destroyed. The volume must not be attached to a server and it must not have delete protection enabled. """Deletes a volume. All volume data is irreversibly destroyed. The volume must not be attached to a server and it must not have delete protection enabled.
:param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>` :param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>`
@ -323,8 +372,7 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
self._client.request(url=f"/volumes/{volume.id}", method="DELETE") self._client.request(url=f"/volumes/{volume.id}", method="DELETE")
return True return True
def resize(self, volume, size): def resize(self, volume: Volume | BoundVolume, size: int) -> BoundAction:
# type: (Union[Volume, BoundVolume], int) -> BoundAction
"""Changes the size of a volume. Note that downsizing a volume is not possible. """Changes the size of a volume. Note that downsizing a volume is not possible.
:param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>` :param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>`
@ -339,8 +387,12 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
) )
return BoundAction(self._client.actions, data["action"]) return BoundAction(self._client.actions, data["action"])
def attach(self, volume, server, automount=None): def attach(
# type: (Union[Volume, BoundVolume], Union[Server, BoundServer], Optional[bool]) -> BoundAction self,
volume: Volume | BoundVolume,
server: Server | BoundServer,
automount: bool | None = None,
) -> BoundAction:
"""Attaches a volume to a server. Works only if the server is in the same location as the volume. """Attaches a volume to a server. Works only if the server is in the same location as the volume.
:param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>` :param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>`
@ -348,7 +400,7 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
:param automount: boolean :param automount: boolean
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {"server": server.id} data: dict[str, Any] = {"server": server.id}
if automount is not None: if automount is not None:
data["automount"] = automount data["automount"] = automount
@ -359,8 +411,7 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
) )
return BoundAction(self._client.actions, data["action"]) return BoundAction(self._client.actions, data["action"])
def detach(self, volume): def detach(self, volume: Volume | BoundVolume) -> BoundAction:
# type: (Union[Volume, BoundVolume]) -> BoundAction
"""Detaches a volume from the server its attached to. You may attach it to a server again at a later time. """Detaches a volume from the server its attached to. You may attach it to a server again at a later time.
:param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>` :param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>`
@ -372,8 +423,11 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
) )
return BoundAction(self._client.actions, data["action"]) return BoundAction(self._client.actions, data["action"])
def change_protection(self, volume, delete=None): def change_protection(
# type: (Union[Volume, BoundVolume], Optional[bool], Optional[bool]) -> BoundAction self,
volume: Volume | BoundVolume,
delete: bool | None = None,
) -> BoundAction:
"""Changes the protection configuration of a volume. """Changes the protection configuration of a volume.
:param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>` :param volume: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>` or :class:`Volume <hcloud.volumes.domain.Volume>`
@ -381,14 +435,12 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
If True, prevents the volume from being deleted If True, prevents the volume from being deleted
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>` :return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
""" """
data = {} data: dict[str, Any] = {}
if delete is not None: if delete is not None:
data.update({"delete": delete}) data.update({"delete": delete})
response = self._client.request( response = self._client.request(
url="/volumes/{volume_id}/actions/change_protection".format( url=f"/volumes/{volume.id}/actions/change_protection",
volume_id=volume.id
),
method="POST", method="POST",
json=data, json=data,
) )

View file

@ -1,9 +1,19 @@
from __future__ import annotations
from typing import TYPE_CHECKING
try: try:
from dateutil.parser import isoparse from dateutil.parser import isoparse
except ImportError: except ImportError:
isoparse = None isoparse = None
from ..core.domain import BaseDomain, DomainIdentityMixin from ..core import BaseDomain, DomainIdentityMixin
if TYPE_CHECKING:
from ..actions import BoundAction
from ..locations import BoundLocation, Location
from ..servers import BoundServer, Server
from .client import BoundVolume
class Volume(BaseDomain, DomainIdentityMixin): class Volume(BaseDomain, DomainIdentityMixin):
@ -54,17 +64,17 @@ class Volume(BaseDomain, DomainIdentityMixin):
def __init__( def __init__(
self, self,
id, id: int,
name=None, name: str | None = None,
server=None, server: Server | BoundServer | None = None,
created=None, created: str | None = None,
location=None, location: Location | BoundLocation | None = None,
size=None, size: int | None = None,
linux_device=None, linux_device: str | None = None,
format=None, format: str | None = None,
protection=None, protection: dict | None = None,
labels=None, labels: dict[str, str] | None = None,
status=None, status: str | None = None,
): ):
self.id = id self.id = id
self.name = name self.name = name
@ -94,9 +104,9 @@ class CreateVolumeResponse(BaseDomain):
def __init__( def __init__(
self, self,
volume, # type: BoundVolume volume: BoundVolume,
action, # type: BoundAction action: BoundAction,
next_actions, # type: List[BoundAction] next_actions: list[BoundAction],
): ):
self.volume = volume self.volume = volume
self.action = action self.action = action

View file

@ -19,7 +19,7 @@ from textwrap import dedent
logger = logging.getLogger("vendor") logger = logging.getLogger("vendor")
HCLOUD_SOURCE_URL = "https://github.com/hetznercloud/hcloud-python" HCLOUD_SOURCE_URL = "https://github.com/hetznercloud/hcloud-python"
HCLOUD_VERSION = "v1.26.0" HCLOUD_VERSION = "v1.27.1"
HCLOUD_VENDOR_PATH = "plugins/module_utils/vendor/hcloud" HCLOUD_VENDOR_PATH = "plugins/module_utils/vendor/hcloud"