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 ._exceptions import APIException, HCloudException # noqa

View file

@ -1,5 +1,7 @@
from __future__ import annotations
import time
from typing import Optional, Union
from typing import NoReturn
try:
import requests
@ -8,23 +10,23 @@ except ImportError:
from ._version import VERSION
from ._exceptions import APIException
from .actions.client import ActionsClient
from .certificates.client import CertificatesClient
from .datacenters.client import DatacentersClient
from .firewalls.client import FirewallsClient
from .floating_ips.client import FloatingIPsClient
from .images.client import ImagesClient
from .isos.client import IsosClient
from .load_balancer_types.client import LoadBalancerTypesClient
from .load_balancers.client import LoadBalancersClient
from .locations.client import LocationsClient
from .networks.client import NetworksClient
from .placement_groups.client import PlacementGroupsClient
from .primary_ips.client import PrimaryIPsClient
from .server_types.client import ServerTypesClient
from .servers.client import ServersClient
from .ssh_keys.client import SSHKeysClient
from .volumes.client import VolumesClient
from .actions import ActionsClient
from .certificates import CertificatesClient
from .datacenters import DatacentersClient
from .firewalls import FirewallsClient
from .floating_ips import FloatingIPsClient
from .images import ImagesClient
from .isos import IsosClient
from .load_balancer_types import LoadBalancerTypesClient
from .load_balancers import LoadBalancersClient
from .locations import LocationsClient
from .networks import NetworksClient
from .placement_groups import PlacementGroupsClient
from .primary_ips import PrimaryIPsClient
from .server_types import ServerTypesClient
from .servers import ServersClient
from .ssh_keys import SSHKeysClient
from .volumes import VolumesClient
class Client:
@ -38,9 +40,10 @@ class Client:
self,
token: str,
api_endpoint: str = "https://api.hetzner.cloud/v1",
application_name: Optional[str] = None,
application_version: Optional[str] = None,
application_name: str | None = None,
application_version: str | None = None,
poll_interval: int = 1,
timeout: float | tuple[float, float] | None = None,
):
"""Create an new Client instance
@ -49,12 +52,14 @@ class Client:
:param application_name: Your application name
:param application_version: Your application _version
:param poll_interval: Interval for polling information from Hetzner Cloud API in seconds
:param timeout: Requests timeout in seconds
"""
self.token = token
self._api_endpoint = api_endpoint
self._application_name = application_name
self._application_version = application_version
self._requests_session = requests.Session()
self._requests_timeout = timeout
self.poll_interval = poll_interval
self.datacenters = DatacentersClient(self)
@ -169,38 +174,42 @@ class Client:
}
return headers
def _raise_exception_from_response(self, response):
def _raise_exception_from_response(self, response) -> NoReturn:
raise APIException(
code=response.status_code,
message=response.reason,
details={"content": response.content},
)
def _raise_exception_from_content(self, content: dict):
def _raise_exception_from_content(self, content: dict) -> NoReturn:
raise APIException(
code=content["error"]["code"],
message=content["error"]["message"],
details=content["error"]["details"],
)
def request(
def request( # type: ignore[no-untyped-def]
self,
method: str,
url: str,
tries: int = 1,
**kwargs,
) -> Union[bytes, dict]:
) -> dict:
"""Perform a request to the Hetzner Cloud API, wrapper around requests.request
:param method: HTTP Method to perform the Request
:param url: URL of the Endpoint
:param tries: Tries of the request (used internally, should not be set by the user)
:param timeout: Requests timeout in seconds
:return: Response
"""
timeout = kwargs.pop("timeout", self._requests_timeout)
response = self._requests_session.request(
method=method,
url=self._api_endpoint + url,
headers=self._get_headers(),
timeout=timeout,
**kwargs,
)
@ -213,13 +222,15 @@ class Client:
if not response.ok:
if content:
assert isinstance(content, dict)
if content["error"]["code"] == "rate_limit_exceeded" and tries < 5:
time.sleep(tries * self._retry_wait_time)
tries = tries + 1
return self.request(method, url, tries, **kwargs)
else:
self._raise_exception_from_content(content)
self._raise_exception_from_content(content)
else:
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):
"""There was an error while using the hcloud library"""
@ -5,7 +10,7 @@ class HCloudException(Exception):
class APIException(HCloudException):
"""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)
self.code = code
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
if TYPE_CHECKING:
from .._client import Client
class BoundAction(BoundModelBase):
_client: ActionsClient
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)
:param max_retries: int
@ -27,11 +35,15 @@ class BoundAction(BoundModelBase):
raise ActionFailedException(action=self)
class ActionsClient(ClientEntityBase):
results_list_attribute_name = "actions"
class ActionsPageResult(NamedTuple):
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.
:param id: int
@ -43,12 +55,11 @@ class ActionsClient(ClientEntityBase):
def get_list(
self,
status=None, # type: Optional[List[str]]
sort=None, # type: Optional[List[str]]
page=None, # type: Optional[int]
per_page=None, # type: Optional[int]
):
# type: (...) -> PageResults[List[BoundAction]]
status: list[str] | None = None,
sort: list[str] | None = None,
page: int | None = None,
per_page: int | None = None,
) -> ActionsPageResult:
"""Get a list of actions from this account
:param status: List[str] (optional)
@ -61,7 +72,7 @@ class ActionsClient(ClientEntityBase):
Specifies how many results are returned by page
:return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`)
"""
params = {}
params: dict[str, Any] = {}
if status is not None:
params["status"] = status
if sort is not None:
@ -75,10 +86,13 @@ class ActionsClient(ClientEntityBase):
actions = [
BoundAction(self, action_data) for action_data in response["actions"]
]
return self._add_meta_to_result(actions, response)
return ActionsPageResult(actions, Meta.parse_meta(response))
def get_all(self, status=None, sort=None):
# type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction]
def get_all(
self,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Get all actions of the account
: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)
: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:
from dateutil.parser import isoparse
except ImportError:
isoparse = None
from .._exceptions import HCloudException
from ..core.domain import BaseDomain
from ..core import BaseDomain
if TYPE_CHECKING:
from .client import BoundAction
class Action(BaseDomain):
@ -40,14 +47,14 @@ class Action(BaseDomain):
def __init__(
self,
id,
command=None,
status=None,
progress=None,
started=None,
finished=None,
resources=None,
error=None,
id: int,
command: str | None = None,
status: str | None = None,
progress: int | None = None,
started: str | None = None,
finished: str | None = None,
resources: list[dict] | None = None,
error: dict | None = None,
):
self.id = id
self.command = command
@ -63,7 +70,8 @@ class Action(BaseDomain):
class ActionException(HCloudException):
"""A generic action exception"""
def __init__(self, action):
def __init__(self, action: Action | BoundAction):
assert self.__doc__ is not None
message = self.__doc__
if action.error is not None and "message" in action.error:
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 ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result
from __future__ import annotations
from typing import TYPE_CHECKING, Any, NamedTuple
from ..actions import ActionsPageResult, BoundAction
from ..core import BoundModelBase, ClientEntityBase, Meta
from .domain import (
Certificate,
CreateManagedCertificateResponse,
@ -8,11 +11,16 @@ from .domain import (
ManagedCertificateStatus,
)
if TYPE_CHECKING:
from .._client import Client
class BoundCertificate(BoundModelBase):
_client: CertificatesClient
model = Certificate
def __init__(self, client, data, complete=True):
def __init__(self, client: CertificatesClient, data: dict, complete: bool = True):
status = data.get("status")
if status is not None:
error_data = status.get("error")
@ -26,8 +34,13 @@ class BoundCertificate(BoundModelBase):
)
super().__init__(client, data, complete)
def get_actions_list(self, status=None, sort=None, page=None, per_page=None):
# type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]]
def get_actions_list(
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.
:param status: List[str] (optional)
@ -42,8 +55,11 @@ class BoundCertificate(BoundModelBase):
"""
return self._client.get_actions_list(self, status, sort, page, per_page)
def get_actions(self, status=None, sort=None):
# type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction]
def get_actions(
self,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a Certificate.
:param status: List[str] (optional)
@ -54,8 +70,11 @@ class BoundCertificate(BoundModelBase):
"""
return self._client.get_actions(self, status, sort)
def update(self, name=None, labels=None):
# type: (Optional[str], Optional[Dict[str, str]]) -> BoundCertificate
def update(
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.
:param name: str (optional)
@ -66,26 +85,28 @@ class BoundCertificate(BoundModelBase):
"""
return self._client.update(self, name, labels)
def delete(self):
# type: () -> bool
def delete(self) -> bool:
"""Deletes a certificate.
:return: boolean
"""
return self._client.delete(self)
def retry_issuance(self):
# type: () -> BoundAction
def retry_issuance(self) -> BoundAction:
"""Retry a failed Certificate issuance or renewal.
:return: BoundAction
"""
return self._client.retry_issuance(self)
class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
results_list_attribute_name = "certificates"
class CertificatesPageResult(NamedTuple):
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.
:param id: int
@ -96,12 +117,11 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
def get_list(
self,
name=None, # type: Optional[str]
label_selector=None, # type: Optional[str]
page=None, # type: Optional[int]
per_page=None, # type: Optional[int]
):
# type: (...) -> PageResults[List[BoundCertificate], Meta]
name: str | None = None,
label_selector: str | None = None,
page: int | None = None,
per_page: int | None = None,
) -> CertificatesPageResult:
"""Get a list of certificates
:param name: str (optional)
@ -114,7 +134,7 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page
:return: (List[:class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>`], :class:`Meta <hcloud.core.domain.Meta>`)
"""
params = {}
params: dict[str, Any] = {}
if name is not None:
params["name"] = name
@ -136,10 +156,13 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
for certificate_data in response["certificates"]
]
return self._add_meta_to_result(certificates, response)
return CertificatesPageResult(certificates, Meta.parse_meta(response))
def get_all(self, name=None, label_selector=None):
# type: (Optional[str], Optional[str]) -> List[BoundCertificate]
def get_all(
self,
name: str | None = None,
label_selector: str | None = None,
) -> list[BoundCertificate]:
"""Get all certificates
: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.
: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):
# type: (str) -> BoundCertificate
def get_by_name(self, name: str) -> BoundCertificate | None:
"""Get certificate by name
:param name: str
Used to get certificate by name.
: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):
# type: (str, str, str, Optional[Dict[str, str]]) -> BoundCertificate
def create(
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
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)
:return: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>`
"""
data = {
data: dict[str, Any] = {
"name": name,
"certificate": certificate,
"private_key": private_key,
@ -185,8 +212,12 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
response = self._client.request(url="/certificates", method="POST", json=data)
return BoundCertificate(self, response["certificate"])
def create_managed(self, name, domain_names, labels=None):
# type: (str, List[str], Optional[Dict[str, str]]) -> CreateManagedCertificateResponse
def create_managed(
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
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)
:return: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>`
"""
data = {
data: dict[str, Any] = {
"name": name,
"type": Certificate.TYPE_MANAGED,
"domain_names": domain_names,
@ -210,8 +241,12 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
action=BoundAction(self._client.actions, response["action"]),
)
def update(self, certificate, name=None, labels=None):
# type: (Certificate, Optional[str], Optional[Dict[str, str]]) -> BoundCertificate
def update(
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.
: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)
:return: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>`
"""
data = {}
data: dict[str, Any] = {}
if name is not None:
data["name"] = name
if labels is not None:
@ -233,24 +268,27 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
)
return BoundCertificate(self, response["certificate"])
def delete(self, certificate):
# type: (Certificate) -> bool
self._client.request(
url=f"/certificates/{certificate.id}",
method="DELETE",
)
def delete(self, certificate: Certificate | BoundCertificate) -> bool:
"""Deletes a certificate.
:param certificate: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.Certificate>`
: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 True
def get_actions_list(
self, certificate, status=None, sort=None, page=None, per_page=None
):
# type: (Certificate, Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta]
self,
certificate: Certificate | BoundCertificate,
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.
: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
:return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`)
"""
params = {}
params: dict[str, Any] = {}
if status is not None:
params["status"] = status
if sort is not None:
@ -275,9 +313,7 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
params["per_page"] = per_page
response = self._client.request(
url="/certificates/{certificate_id}/actions".format(
certificate_id=certificate.id
),
url=f"/certificates/{certificate.id}/actions",
method="GET",
params=params,
)
@ -285,10 +321,14 @@ class CertificatesClient(ClientEntityBase, GetEntityByNameMixin):
BoundAction(self._client.actions, action_data)
for action_data in response["actions"]
]
return add_meta_to_result(actions, response, "actions")
return ActionsPageResult(actions, Meta.parse_meta(response))
def get_actions(self, certificate, status=None, sort=None):
# type: (Certificate, Optional[List[str]], Optional[List[str]]) -> List[BoundAction]
def get_actions(
self,
certificate: Certificate | BoundCertificate,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a 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`
: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):
# type: (Certificate) -> BoundAction
def retry_issuance(
self,
certificate: Certificate | BoundCertificate,
) -> BoundAction:
"""Returns all action objects for a Certificate.
:param certificate: :class:`BoundCertificate <hcloud.certificates.client.BoundCertificate>` or :class:`Certificate <hcloud.certificates.domain.Certificate>`
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
response = self._client.request(
url="/certificates/{certificate_id}/actions/retry".format(
certificate_id=certificate.id
),
url=f"/certificates/{certificate.id}/actions/retry",
method="POST",
)
return BoundAction(self._client.actions, response["action"])

View file

@ -1,9 +1,17 @@
from __future__ import annotations
from typing import TYPE_CHECKING
try:
from dateutil.parser import isoparse
except ImportError:
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):
@ -44,17 +52,17 @@ class Certificate(BaseDomain, DomainIdentityMixin):
def __init__(
self,
id=None,
name=None,
certificate=None,
not_valid_before=None,
not_valid_after=None,
domain_names=None,
fingerprint=None,
created=None,
labels=None,
type=None,
status=None,
id: int | None = None,
name: str | None = None,
certificate: str | None = None,
not_valid_before: str | None = None,
not_valid_after: str | None = None,
domain_names: list[str] | None = None,
fingerprint: str | None = None,
created: str | None = None,
labels: dict[str, str] | None = None,
type: str | None = None,
status: ManagedCertificateStatus | None = None,
):
self.id = id
self.name = name
@ -80,7 +88,12 @@ class ManagedCertificateStatus(BaseDomain):
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.renewal = renewal
self.error = error
@ -95,7 +108,7 @@ class ManagedCertificateError(BaseDomain):
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.message = message
@ -113,8 +126,8 @@ class CreateManagedCertificateResponse(BaseDomain):
def __init__(
self,
certificate, # type: BoundCertificate
action, # type: BoundAction
certificate: BoundCertificate,
action: BoundAction,
):
self.certificate = certificate
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:
max_per_page = 50
results_list_attribute_name = None
_client: Client
def __init__(self, client):
max_per_page: int = 50
def __init__(self, client: Client):
"""
:param client: Client
:return self
"""
self._client = client
def _is_list_attribute_implemented(self):
if self.results_list_attribute_name is None:
raise NotImplementedError(
"in order to get results list, 'results_list_attribute_name' attribute of {} has to be specified".format(
self.__class__.__name__
)
)
def _add_meta_to_result(
def _iter_pages( # type: ignore[no-untyped-def]
self,
results, # type: List[BoundModelBase]
response, # type: json
):
# type: (...) -> PageResult
self._is_list_attribute_implemented()
return add_meta_to_result(results, response, self.results_list_attribute_name)
def _get_all(
self,
list_function, # type: function
results_list_attribute_name, # type: str
list_function: Callable,
*args,
**kwargs
):
# type (...) -> List[BoundModelBase]
page = 1
**kwargs,
) -> list:
results = []
page = 1
while page:
page_result = list_function(
page=page, per_page=self.max_per_page, *args, **kwargs
# The *PageResult tuples MUST have the following structure
# `(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:
results.extend(result)
meta = page_result.meta
if (
meta
and meta.pagination
and meta.pagination.next_page
and meta.pagination.next_page
):
if meta and meta.pagination and meta.pagination.next_page:
page = meta.pagination.next_page
else:
page = None
page = 0
return results
def get_all(self, *args, **kwargs):
# type: (...) -> List[BoundModelBase]
self._is_list_attribute_implemented()
return self._get_all(
self.get_list, self.results_list_attribute_name, *args, **kwargs
)
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
def _get_first_by(self, **kwargs): # type: ignore[no-untyped-def]
assert hasattr(self, "get_list")
entities, _ = self.get_list(**kwargs)
return entities[0] if entities else None
class BoundModelBase:
"""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:
The client for the specific model to use
@ -109,7 +72,7 @@ class BoundModelBase:
self.complete = complete
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
:param name: str
:return:
@ -120,8 +83,9 @@ class BoundModelBase:
value = getattr(self.data_model, name)
return value
def reload(self):
def reload(self) -> None:
"""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)
self.data_model = bound_model.data_model
self.complete = True

View file

@ -1,24 +1,27 @@
from collections import namedtuple
from __future__ import annotations
class BaseDomain:
__slots__ = ()
@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__}
return cls(**supported_data)
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)})"
class DomainIdentityMixin:
__slots__ = ()
id: int | None
name: str | None
@property
def id_or_name(self):
def id_or_name(self) -> int | str:
if self.id is not None:
return self.id
elif self.name is not None:
@ -39,12 +42,12 @@ class Pagination(BaseDomain):
def __init__(
self,
page,
per_page,
previous_page=None,
next_page=None,
last_page=None,
total_entries=None,
page: int,
per_page: int,
previous_page: int | None = None,
next_page: int | None = None,
last_page: int | None = None,
total_entries: int | None = None,
):
self.page = page
self.per_page = per_page
@ -57,23 +60,17 @@ class Pagination(BaseDomain):
class Meta(BaseDomain):
__slots__ = ("pagination",)
def __init__(self, pagination=None):
def __init__(self, pagination: Pagination | None = None):
self.pagination = pagination
@classmethod
def parse_meta(cls, json_content):
def parse_meta(cls, response: dict) -> Meta | None:
meta = None
if json_content and "meta" in json_content:
if response and "meta" in response:
meta = cls()
pagination_json = json_content["meta"].get("pagination")
if pagination_json:
pagination = Pagination(**pagination_json)
meta.pagination = pagination
try:
meta.pagination = Pagination(**response["meta"]["pagination"])
except KeyError:
pass
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 ..locations.client import BoundLocation
from ..server_types.client import BoundServerType
from __future__ import annotations
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
if TYPE_CHECKING:
from .._client import Client
class BoundDatacenter(BoundModelBase):
_client: DatacentersClient
model = Datacenter
def __init__(self, client, data):
def __init__(self, client: DatacentersClient, data: dict):
location = data.get("location")
if location is not None:
data["location"] = BoundLocation(client._client.locations, location)
@ -41,11 +50,15 @@ class BoundDatacenter(BoundModelBase):
super().__init__(client, data)
class DatacentersClient(ClientEntityBase, GetEntityByNameMixin):
results_list_attribute_name = "datacenters"
class DatacentersPageResult(NamedTuple):
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.
:param id: int
@ -56,11 +69,10 @@ class DatacentersClient(ClientEntityBase, GetEntityByNameMixin):
def get_list(
self,
name=None, # type: Optional[str]
page=None, # type: Optional[int]
per_page=None, # type: Optional[int]
):
# type: (...) -> PageResults[List[BoundDatacenter], Meta]
name: str | None = None,
page: int | None = None,
per_page: int | None = None,
) -> DatacentersPageResult:
"""Get a list of datacenters
:param name: str (optional)
@ -71,7 +83,7 @@ class DatacentersClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page
:return: (List[:class:`BoundDatacenter <hcloud.datacenters.client.BoundDatacenter>`], :class:`Meta <hcloud.core.domain.Meta>`)
"""
params = {}
params: dict[str, Any] = {}
if name is not None:
params["name"] = name
@ -88,24 +100,22 @@ class DatacentersClient(ClientEntityBase, GetEntityByNameMixin):
for datacenter_data in response["datacenters"]
]
return self._add_meta_to_result(datacenters, response)
return DatacentersPageResult(datacenters, Meta.parse_meta(response))
def get_all(self, name=None):
# type: (Optional[str]) -> List[BoundDatacenter]
def get_all(self, name: str | None = None) -> list[BoundDatacenter]:
"""Get all datacenters
:param name: str (optional)
Can be used to filter datacenters by their name.
: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):
# type: (str) -> BoundDatacenter
def get_by_name(self, name: str) -> BoundDatacenter | None:
"""Get datacenter by name
:param name: str
Used to get datacenter by name.
: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):
@ -14,7 +22,12 @@ class Datacenter(BaseDomain, DomainIdentityMixin):
__slots__ = ("id", "name", "description", "location", "server_types")
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.name = name
@ -36,7 +49,12 @@ class DatacenterServerTypes:
__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.supported = supported
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:
from dateutil.parser import isoparse
except ImportError:
isoparse = None
from ..core.domain import BaseDomain
from ..core import BaseDomain
class DeprecationInfo(BaseDomain):
@ -25,8 +27,8 @@ class DeprecationInfo(BaseDomain):
def __init__(
self,
announced=None,
unavailable_after=None,
announced: str | None = None,
unavailable_after: str | None = None,
):
self.announced = isoparse(announced) if announced else None
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 ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result
from __future__ import annotations
from typing import TYPE_CHECKING, Any, NamedTuple
from ..actions import ActionsPageResult, BoundAction
from ..core import BoundModelBase, ClientEntityBase, Meta
from .domain import (
CreateFirewallResponse,
Firewall,
@ -9,11 +12,16 @@ from .domain import (
FirewallRule,
)
if TYPE_CHECKING:
from .._client import Client
class BoundFirewall(BoundModelBase):
_client: FirewallsClient
model = Firewall
def __init__(self, client, data, complete=True):
def __init__(self, client: FirewallsClient, data: dict, complete: bool = True):
rules = data.get("rules", [])
if rules:
rules = [
@ -31,7 +39,7 @@ class BoundFirewall(BoundModelBase):
applied_to = data.get("applied_to", [])
if applied_to:
from ..servers.client import BoundServer
from ..servers import BoundServer
ats = []
for a in applied_to:
@ -57,8 +65,13 @@ class BoundFirewall(BoundModelBase):
super().__init__(client, data, complete)
def get_actions_list(self, status=None, sort=None, page=None, per_page=None):
# type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResult[BoundAction, Meta]
def get_actions_list(
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.
:param status: List[str] (optional)
@ -73,8 +86,11 @@ class BoundFirewall(BoundModelBase):
"""
return self._client.get_actions_list(self, status, sort, page, per_page)
def get_actions(self, status=None, sort=None):
# type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction]
def get_actions(
self,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a Firewall.
:param status: List[str] (optional)
@ -86,8 +102,11 @@ class BoundFirewall(BoundModelBase):
"""
return self._client.get_actions(self, status, sort)
def update(self, name=None, labels=None):
# type: (Optional[str], Optional[Dict[str, str]], Optional[str]) -> BoundFirewall
def update(
self,
name: str | None = None,
labels: dict[str, str] | None = None,
) -> BoundFirewall:
"""Updates the name or labels of a Firewall.
:param labels: Dict[str, str] (optional)
@ -98,16 +117,14 @@ class BoundFirewall(BoundModelBase):
"""
return self._client.update(self, labels, name)
def delete(self):
# type: () -> bool
def delete(self) -> bool:
"""Deletes a Firewall.
:return: boolean
"""
return self._client.delete(self)
def set_rules(self, rules):
# type: (List[FirewallRule]) -> List[BoundAction]
def set_rules(self, 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.
:param rules: List[:class:`FirewallRule <hcloud.firewalls.domain.FirewallRule>`]
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
@ -115,16 +132,20 @@ class BoundFirewall(BoundModelBase):
return self._client.set_rules(self, rules)
def apply_to_resources(self, resources):
# type: (List[FirewallResource]) -> List[BoundAction]
def apply_to_resources(
self,
resources: list[FirewallResource],
) -> list[BoundAction]:
"""Applies one Firewall to multiple resources.
:param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`]
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
"""
return self._client.apply_to_resources(self, resources)
def remove_from_resources(self, resources):
# type: (List[FirewallResource]) -> List[BoundAction]
def remove_from_resources(
self,
resources: list[FirewallResource],
) -> list[BoundAction]:
"""Removes one Firewall from multiple resources.
:param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`]
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
@ -132,18 +153,22 @@ class BoundFirewall(BoundModelBase):
return self._client.remove_from_resources(self, resources)
class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
results_list_attribute_name = "firewalls"
class FirewallsPageResult(NamedTuple):
firewalls: list[BoundFirewall]
meta: Meta | None
class FirewallsClient(ClientEntityBase):
_client: Client
def get_actions_list(
self,
firewall, # type: Firewall
status=None, # type: Optional[List[str]]
sort=None, # type: Optional[List[str]]
page=None, # type: Optional[int]
per_page=None, # type: Optional[int]
):
# type: (...) -> PageResults[List[BoundAction], Meta]
firewall: Firewall | BoundFirewall,
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.
: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
:return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`)
"""
params = {}
params: dict[str, Any] = {}
if status is not None:
params["status"] = status
if sort is not None:
@ -175,15 +200,14 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
BoundAction(self._client.actions, action_data)
for action_data in response["actions"]
]
return add_meta_to_result(actions, response, "actions")
return ActionsPageResult(actions, Meta.parse_meta(response))
def get_actions(
self,
firewall, # type: Firewall
status=None, # type: Optional[List[str]]
sort=None, # type: Optional[List[str]]
):
# type: (...) -> List[BoundAction]
firewall: Firewall | BoundFirewall,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a 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 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):
# type: (int) -> BoundFirewall
def get_by_id(self, id: int) -> BoundFirewall:
"""Returns a specific Firewall object.
:param id: int
@ -208,13 +236,12 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
def get_list(
self,
label_selector=None, # type: Optional[str]
page=None, # type: Optional[int]
per_page=None, # type: Optional[int]
name=None, # type: Optional[str]
sort=None, # type: Optional[List[str]]
):
# type: (...) -> PageResults[List[BoundFirewall]]
label_selector: str | None = None,
page: int | None = None,
per_page: int | None = None,
name: str | None = None,
sort: list[str] | None = None,
) -> FirewallsPageResult:
"""Get a list of floating ips from this account
: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))
: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:
params["label_selector"] = label_selector
@ -247,10 +274,14 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
for firewall_data in response["firewalls"]
]
return self._add_meta_to_result(firewalls, response)
return FirewallsPageResult(firewalls, Meta.parse_meta(response))
def get_all(self, label_selector=None, name=None, sort=None):
# type: (Optional[str], Optional[str], Optional[List[str]]) -> List[BoundFirewall]
def get_all(
self,
label_selector: str | None = None,
name: str | None = None,
sort: list[str] | None = None,
) -> list[BoundFirewall]:
"""Get all floating ips from this account
: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))
: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):
# type: (str) -> BoundFirewall
def get_by_name(self, name: str) -> BoundFirewall | None:
"""Get Firewall by name
:param name: str
Used to get Firewall by name.
:return: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>`
"""
return super().get_by_name(name)
return self._get_first_by(name=name)
def create(
self,
name, # type: str
rules=None, # type: Optional[List[FirewallRule]]
labels=None, # type: Optional[str]
resources=None, # type: Optional[List[FirewallResource]]
):
# type: (...) -> CreateFirewallResponse
name: str,
rules: list[FirewallRule] | None = None,
labels: str | None = None,
resources: list[FirewallResource] | None = None,
) -> CreateFirewallResponse:
"""Creates a new Firewall.
:param name: str
@ -292,7 +326,7 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
:return: :class:`CreateFirewallResponse <hcloud.firewalls.domain.CreateFirewallResponse>`
"""
data = {"name": name}
data: dict[str, Any] = {"name": name}
if labels is not None:
data["labels"] = labels
@ -309,7 +343,8 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
actions = []
if response.get("actions") is not None:
actions = [
BoundAction(self._client.actions, _) for _ in response["actions"]
BoundAction(self._client.actions, action_data)
for action_data in response["actions"]
]
result = CreateFirewallResponse(
@ -317,8 +352,12 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
)
return result
def update(self, firewall, labels=None, name=None):
# type: (Firewall, Optional[Dict[str, str]], Optional[str]) -> BoundFirewall
def update(
self,
firewall: Firewall | BoundFirewall,
labels: dict[str, str] | None = None,
name: str | None = None,
) -> BoundFirewall:
"""Updates the description or labels of a 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
:return: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>`
"""
data = {}
data: dict[str, Any] = {}
if labels is not None:
data["labels"] = labels
if name is not None:
@ -341,8 +380,7 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin):
)
return BoundFirewall(self, response["firewall"])
def delete(self, firewall):
# type: (Firewall) -> bool
def delete(self, firewall: Firewall | BoundFirewall) -> bool:
"""Deletes a 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 True
def set_rules(self, firewall, rules):
# type: (Firewall, List[FirewallRule]) -> List[BoundAction]
def set_rules(
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.
:param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>`
:param rules: List[:class:`FirewallRule <hcloud.firewalls.domain.FirewallRule>`]
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
"""
data = {"rules": []}
data: dict[str, Any] = {"rules": []}
for rule in rules:
data["rules"].append(rule.to_payload())
response = self._client.request(
url="/firewalls/{firewall_id}/actions/set_rules".format(
firewall_id=firewall.id
),
url=f"/firewalls/{firewall.id}/actions/set_rules",
method="POST",
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):
# type: (Firewall, List[FirewallResource]) -> List[BoundAction]
def apply_to_resources(
self,
firewall: Firewall | BoundFirewall,
resources: list[FirewallResource],
) -> list[BoundAction]:
"""Applies one Firewall to multiple resources.
:param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>`
:param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`]
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
"""
data = {"apply_to": []}
data: dict[str, Any] = {"apply_to": []}
for resource in resources:
data["apply_to"].append(resource.to_payload())
response = self._client.request(
url="/firewalls/{firewall_id}/actions/apply_to_resources".format(
firewall_id=firewall.id
),
url=f"/firewalls/{firewall.id}/actions/apply_to_resources",
method="POST",
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):
# type: (Firewall, List[FirewallResource]) -> List[BoundAction]
def remove_from_resources(
self,
firewall: Firewall | BoundFirewall,
resources: list[FirewallResource],
) -> list[BoundAction]:
"""Removes one Firewall from multiple resources.
:param firewall: :class:`BoundFirewall <hcloud.firewalls.client.BoundFirewall>` or :class:`Firewall <hcloud.firewalls.domain.Firewall>`
:param resources: List[:class:`FirewallResource <hcloud.firewalls.domain.FirewallResource>`]
:return: List[:class:`BoundAction <hcloud.actions.client.BoundAction>`]
"""
data = {"remove_from": []}
data: dict[str, Any] = {"remove_from": []}
for resource in resources:
data["remove_from"].append(resource.to_payload())
response = self._client.request(
url="/firewalls/{firewall_id}/actions/remove_from_resources".format(
firewall_id=firewall.id
),
url=f"/firewalls/{firewall.id}/actions/remove_from_resources",
method="POST",
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:
from dateutil.parser import isoparse
except ImportError:
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):
@ -26,7 +35,13 @@ class Firewall(BaseDomain):
__slots__ = ("id", "name", "labels", "rules", "applied_to", "created")
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.name = name
@ -81,12 +96,12 @@ class FirewallRule:
def __init__(
self,
direction, # type: str
protocol, # type: str
source_ips, # type: List[str]
port=None, # type: Optional[str]
destination_ips=None, # type: Optional[List[str]]
description=None, # type: Optional[str]
direction: str,
protocol: str,
source_ips: list[str],
port: str | None = None,
destination_ips: list[str] | None = None,
description: str | None = None,
):
self.direction = direction
self.port = port
@ -95,18 +110,18 @@ class FirewallRule:
self.destination_ips = destination_ips or []
self.description = description
def to_payload(self):
payload = {
def to_payload(self) -> dict[str, Any]:
payload: dict[str, Any] = {
"direction": self.direction,
"protocol": self.protocol,
"source_ips": self.source_ips,
}
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:
payload.update({"port": self.port})
payload["port"] = self.port
if self.description is not None:
payload.update({"description": self.description})
payload["description"] = self.description
return payload
@ -130,23 +145,21 @@ class FirewallResource:
def __init__(
self,
type, # type: str
server=None, # type: Optional[Server]
label_selector=None, # type: Optional[FirewallResourceLabelSelector]
type: str,
server: Server | BoundServer | None = None,
label_selector: FirewallResourceLabelSelector | None = None,
):
self.type = type
self.server = server
self.label_selector = label_selector
def to_payload(self):
payload = {"type": self.type}
def to_payload(self) -> dict[str, Any]:
payload: dict[str, Any] = {"type": self.type}
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:
payload.update(
{"label_selector": {"selector": self.label_selector.selector}}
)
payload["label_selector"] = {"selector": self.label_selector.selector}
return payload
@ -156,7 +169,7 @@ class FirewallResourceLabelSelector(BaseDomain):
:param selector: str Target label selector
"""
def __init__(self, selector=None):
def __init__(self, selector: str | None = None):
self.selector = selector
@ -173,8 +186,8 @@ class CreateFirewallResponse(BaseDomain):
def __init__(
self,
firewall, # type: BoundFirewall
actions, # type: BoundAction
firewall: BoundFirewall,
actions: list[BoundAction] | None,
):
self.firewall = firewall
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 ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result
from ..locations.client import BoundLocation
from __future__ import annotations
from typing import TYPE_CHECKING, Any, NamedTuple
from ..actions import ActionsPageResult, BoundAction
from ..core import BoundModelBase, ClientEntityBase, Meta
from ..locations import BoundLocation
from .domain import CreateFloatingIPResponse, FloatingIP
if TYPE_CHECKING:
from .._client import Client
from ..locations import Location
from ..servers import BoundServer, Server
class BoundFloatingIP(BoundModelBase):
_client: FloatingIPsClient
model = FloatingIP
def __init__(self, client, data, complete=True):
from ..servers.client import BoundServer
def __init__(self, client: FloatingIPsClient, data: dict, complete: bool = True):
from ..servers import BoundServer
server = data.get("server")
if server is not None:
@ -25,8 +35,13 @@ class BoundFloatingIP(BoundModelBase):
super().__init__(client, data, complete)
def get_actions_list(self, status=None, sort=None, page=None, per_page=None):
# type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResult[BoundAction, Meta]
def get_actions_list(
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.
:param status: List[str] (optional)
@ -41,8 +56,11 @@ class BoundFloatingIP(BoundModelBase):
"""
return self._client.get_actions_list(self, status, sort, page, per_page)
def get_actions(self, status=None, sort=None):
# type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction]
def get_actions(
self,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a Floating IP.
:param status: List[str] (optional)
@ -54,8 +72,12 @@ class BoundFloatingIP(BoundModelBase):
"""
return self._client.get_actions(self, status, sort)
def update(self, description=None, labels=None, name=None):
# type: (Optional[str], Optional[Dict[str, str]], Optional[str]) -> BoundFloatingIP
def update(
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.
:param description: str (optional)
@ -68,16 +90,14 @@ class BoundFloatingIP(BoundModelBase):
"""
return self._client.update(self, description, labels, name)
def delete(self):
# type: () -> bool
def delete(self) -> bool:
"""Deletes a Floating IP. If it is currently assigned to a server it will automatically get unassigned.
:return: boolean
"""
return self._client.delete(self)
def change_protection(self, delete=None):
# type: (Optional[bool]) -> BoundAction
def change_protection(self, delete: bool | None = None) -> BoundAction:
"""Changes the protection configuration of the Floating IP.
:param delete: boolean
@ -86,8 +106,7 @@ class BoundFloatingIP(BoundModelBase):
"""
return self._client.change_protection(self, delete)
def assign(self, server):
# type: (Server) -> BoundAction
def assign(self, server: Server | BoundServer) -> BoundAction:
"""Assigns a Floating IP to a 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)
def unassign(self):
# type: () -> BoundAction
def unassign(self) -> BoundAction:
"""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 self._client.unassign(self)
def change_dns_ptr(self, ip, dns_ptr):
# type: (str, str) -> BoundAction
def change_dns_ptr(self, ip: str, dns_ptr: str) -> BoundAction:
"""Changes the hostname that will appear when getting the hostname belonging to this Floating IP.
:param ip: str
@ -117,18 +134,22 @@ class BoundFloatingIP(BoundModelBase):
return self._client.change_dns_ptr(self, ip, dns_ptr)
class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
results_list_attribute_name = "floating_ips"
class FloatingIPsPageResult(NamedTuple):
floating_ips: list[BoundFloatingIP]
meta: Meta | None
class FloatingIPsClient(ClientEntityBase):
_client: Client
def get_actions_list(
self,
floating_ip, # type: FloatingIP
status=None, # type: Optional[List[str]]
sort=None, # type: Optional[List[str]]
page=None, # type: Optional[int]
per_page=None, # type: Optional[int]
):
# type: (...) -> PageResults[List[BoundAction], Meta]
floating_ip: FloatingIP | BoundFloatingIP,
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.
: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
:return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`)
"""
params = {}
params: dict[str, Any] = {}
if status is not None:
params["status"] = status
if sort is not None:
@ -152,9 +173,7 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
if per_page is not None:
params["per_page"] = per_page
response = self._client.request(
url="/floating_ips/{floating_ip_id}/actions".format(
floating_ip_id=floating_ip.id
),
url=f"/floating_ips/{floating_ip.id}/actions",
method="GET",
params=params,
)
@ -162,15 +181,14 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
BoundAction(self._client.actions, action_data)
for action_data in response["actions"]
]
return add_meta_to_result(actions, response, "actions")
return ActionsPageResult(actions, Meta.parse_meta(response))
def get_actions(
self,
floating_ip, # type: FloatingIP
status=None, # type: Optional[List[str]]
sort=None, # type: Optional[List[str]]
):
# type: (...) -> List[BoundAction]
floating_ip: FloatingIP | BoundFloatingIP,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""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>`
@ -181,10 +199,14 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
: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):
# type: (int) -> BoundFloatingIP
def get_by_id(self, id: int) -> BoundFloatingIP:
"""Returns a specific Floating IP object.
:param id: int
@ -195,12 +217,11 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
def get_list(
self,
label_selector=None, # type: Optional[str]
page=None, # type: Optional[int]
per_page=None, # type: Optional[int]
name=None, # type: Optional[str]
):
# type: (...) -> PageResults[List[BoundFloatingIP]]
label_selector: str | None = None,
page: int | None = None,
per_page: int | None = None,
name: str | None = None,
) -> FloatingIPsPageResult:
"""Get a list of floating ips from this account
:param label_selector: str (optional)
@ -213,7 +234,7 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
Can be used to filter networks by their name.
: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:
params["label_selector"] = label_selector
@ -232,10 +253,13 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
for floating_ip_data in response["floating_ips"]
]
return self._add_meta_to_result(floating_ips, response)
return FloatingIPsPageResult(floating_ips, Meta.parse_meta(response))
def get_all(self, label_selector=None, name=None):
# type: (Optional[str], Optional[str]) -> List[BoundFloatingIP]
def get_all(
self,
label_selector: str | None = None,
name: str | None = None,
) -> list[BoundFloatingIP]:
"""Get all floating ips from this account
:param label_selector: str (optional)
@ -244,28 +268,26 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
Can be used to filter networks by their name.
: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):
# type: (str) -> BoundFloatingIP
def get_by_name(self, name: str) -> BoundFloatingIP | None:
"""Get Floating IP by name
:param name: str
Used to get Floating IP by name.
:return: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>`
"""
return super().get_by_name(name)
return self._get_first_by(name=name)
def create(
self,
type, # type: str
description=None, # type: Optional[str]
labels=None, # type: Optional[str]
home_location=None, # type: Optional[Location]
server=None, # type: Optional[Server]
name=None, # type: Optional[str]
):
# type: (...) -> CreateFloatingIPResponse
type: str,
description: str | None = None,
labels: str | None = None,
home_location: Location | BoundLocation | None = None,
server: Server | BoundServer | None = None,
name: str | None = None,
) -> CreateFloatingIPResponse:
"""Creates a new Floating IP assigned to a server.
:param type: str
@ -281,7 +303,7 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
:return: :class:`CreateFloatingIPResponse <hcloud.floating_ips.domain.CreateFloatingIPResponse>`
"""
data = {"type": type}
data: dict[str, Any] = {"type": type}
if description is not None:
data["description"] = description
if labels is not None:
@ -304,8 +326,13 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
)
return result
def update(self, floating_ip, description=None, labels=None, name=None):
# type: (FloatingIP, Optional[str], Optional[Dict[str, str]], Optional[str]) -> BoundFloatingIP
def update(
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.
: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
:return: :class:`BoundFloatingIP <hcloud.floating_ips.client.BoundFloatingIP>`
"""
data = {}
data: dict[str, Any] = {}
if description is not None:
data["description"] = description
if labels is not None:
@ -332,8 +359,7 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
)
return BoundFloatingIP(self, response["floating_ip"])
def delete(self, floating_ip):
# type: (FloatingIP) -> bool
def delete(self, floating_ip: FloatingIP | BoundFloatingIP) -> bool:
"""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>`
@ -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 True
def change_protection(self, floating_ip, delete=None):
# type: (FloatingIP, Optional[bool]) -> BoundAction
def change_protection(
self,
floating_ip: FloatingIP | BoundFloatingIP,
delete: bool | None = None,
) -> BoundAction:
"""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>`
@ -355,21 +384,22 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
If true, prevents the Floating IP from being deleted
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = {}
data: dict[str, Any] = {}
if delete is not None:
data.update({"delete": delete})
response = self._client.request(
url="/floating_ips/{floating_ip_id}/actions/change_protection".format(
floating_ip_id=floating_ip.id
),
url=f"/floating_ips/{floating_ip.id}/actions/change_protection",
method="POST",
json=data,
)
return BoundAction(self._client.actions, response["action"])
def assign(self, floating_ip, server):
# type: (FloatingIP, Server) -> BoundAction
def assign(
self,
floating_ip: FloatingIP | BoundFloatingIP,
server: Server | BoundServer,
) -> BoundAction:
"""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>`
@ -378,31 +408,30 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin):
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
response = self._client.request(
url="/floating_ips/{floating_ip_id}/actions/assign".format(
floating_ip_id=floating_ip.id
),
url=f"/floating_ips/{floating_ip.id}/actions/assign",
method="POST",
json={"server": server.id},
)
return BoundAction(self._client.actions, response["action"])
def unassign(self, floating_ip):
# type: (FloatingIP) -> BoundAction
def unassign(self, floating_ip: FloatingIP | BoundFloatingIP) -> BoundAction:
"""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>`
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
response = self._client.request(
url="/floating_ips/{floating_ip_id}/actions/unassign".format(
floating_ip_id=floating_ip.id
),
url=f"/floating_ips/{floating_ip.id}/actions/unassign",
method="POST",
)
return BoundAction(self._client.actions, response["action"])
def change_dns_ptr(self, floating_ip, ip, dns_ptr):
# type: (FloatingIP, str, str) -> BoundAction
def change_dns_ptr(
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.
: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>`
"""
response = self._client.request(
url="/floating_ips/{floating_ip_id}/actions/change_dns_ptr".format(
floating_ip_id=floating_ip.id
),
url=f"/floating_ips/{floating_ip.id}/actions/change_dns_ptr",
method="POST",
json={"ip": ip, "dns_ptr": dns_ptr},
)

View file

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

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import warnings
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
from typing import Dict
class LabelValidator:
@ -11,7 +12,7 @@ class LabelValidator:
)
@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
use :func:`~hcloud.helpers.labels.validate_verbose`.
@ -25,7 +26,7 @@ class LabelValidator:
return True
@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>
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 ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result
from __future__ import annotations
from typing import TYPE_CHECKING, Any, NamedTuple
from ..actions import ActionsPageResult, BoundAction
from ..core import BoundModelBase, ClientEntityBase, Meta
from .domain import Image
if TYPE_CHECKING:
from .._client import Client
class BoundImage(BoundModelBase):
_client: ImagesClient
model = Image
def __init__(self, client, data):
from ..servers.client import BoundServer
def __init__(self, client: ImagesClient, data: dict):
from ..servers import BoundServer
created_from = data.get("created_from")
if created_from is not None:
@ -23,8 +31,13 @@ class BoundImage(BoundModelBase):
super().__init__(client, data)
def get_actions_list(self, sort=None, page=None, per_page=None, status=None):
# type: (Optional[List[str]], Optional[int], Optional[int], Optional[List[str]]) -> PageResult[BoundAction, Meta]
def get_actions_list(
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.
:param status: List[str] (optional)
@ -41,8 +54,11 @@ class BoundImage(BoundModelBase):
self, sort=sort, page=page, per_page=per_page, status=status
)
def get_actions(self, sort=None, status=None):
# type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction]
def get_actions(
self,
sort: list[str] | None = None,
status: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for the image.
:param status: List[str] (optional)
@ -53,8 +69,12 @@ class BoundImage(BoundModelBase):
"""
return self._client.get_actions(self, status=status, sort=sort)
def update(self, description=None, type=None, labels=None):
# type: (Optional[str], Optional[str], Optional[Dict[str, str]]) -> BoundImage
def update(
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.
:param description: str (optional)
@ -68,16 +88,14 @@ class BoundImage(BoundModelBase):
"""
return self._client.update(self, description, type, labels)
def delete(self):
# type: () -> bool
def delete(self) -> bool:
"""Deletes an Image. Only images of type snapshot and backup can be deleted.
:return: bool
"""
return self._client.delete(self)
def change_protection(self, delete=None):
# type: (Optional[bool]) -> BoundAction
def change_protection(self, delete: bool | None = None) -> BoundAction:
"""Changes the protection configuration of the image. Can only be used on snapshots.
:param delete: bool
@ -87,18 +105,22 @@ class BoundImage(BoundModelBase):
return self._client.change_protection(self, delete)
class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
results_list_attribute_name = "images"
class ImagesPageResult(NamedTuple):
images: list[BoundImage]
meta: Meta | None
class ImagesClient(ClientEntityBase):
_client: Client
def get_actions_list(
self,
image, # type: Image
sort=None, # type: Optional[List[str]]
page=None, # type: Optional[int]
per_page=None, # type: Optional[int]
status=None, # type: Optional[List[str]]
):
# type: (...) -> PageResults[List[BoundAction], Meta]
image: Image | BoundImage,
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 an 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
:return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`)
"""
params = {}
params: dict[str, Any] = {}
if sort is not None:
params["sort"] = sort
if status is not None:
@ -130,15 +152,14 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
BoundAction(self._client.actions, action_data)
for action_data in response["actions"]
]
return add_meta_to_result(actions, response, "actions")
return ActionsPageResult(actions, Meta.parse_meta(response))
def get_actions(
self,
image, # type: Image
sort=None, # type: Optional[List[str]]
status=None, # type: Optional[List[str]]
):
# type: (...) -> List[BoundAction]
image: Image | BoundImage,
sort: list[str] | None = None,
status: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for an 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)
: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):
# type: (int) -> BoundImage
def get_by_id(self, id: int) -> BoundImage:
"""Get a specific Image
:param id: int
@ -162,18 +187,17 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
def get_list(
self,
name=None, # type: Optional[str]
label_selector=None, # type: Optional[str]
bound_to=None, # type: Optional[List[str]]
type=None, # type: Optional[List[str]]
architecture=None, # type: Optional[List[str]]
sort=None, # type: Optional[List[str]]
page=None, # type: Optional[int]
per_page=None, # type: Optional[int]
status=None, # type: Optional[List[str]]
include_deprecated=None, # type: Optional[bool]
):
# type: (...) -> PageResults[List[BoundImage]]
name: str | None = None,
label_selector: str | None = None,
bound_to: list[str] | None = None,
type: list[str] | None = None,
architecture: list[str] | None = None,
sort: list[str] | None = None,
page: int | None = None,
per_page: int | None = None,
status: list[str] | None = None,
include_deprecated: bool | None = None,
) -> ImagesPageResult:
"""Get all images
:param name: str (optional)
@ -198,7 +222,7 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page
:return: (List[:class:`BoundImage <hcloud.images.client.BoundImage>`], :class:`Meta <hcloud.core.domain.Meta>`)
"""
params = {}
params: dict[str, Any] = {}
if name is not None:
params["name"] = name
if label_selector is not None:
@ -222,20 +246,19 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
response = self._client.request(url="/images", method="GET", params=params)
images = [BoundImage(self, image_data) for image_data in response["images"]]
return self._add_meta_to_result(images, response)
return ImagesPageResult(images, Meta.parse_meta(response))
def get_all(
self,
name=None, # type: Optional[str]
label_selector=None, # type: Optional[str]
bound_to=None, # type: Optional[List[str]]
type=None, # type: Optional[List[str]]
architecture=None, # type: Optional[List[str]]
sort=None, # type: Optional[List[str]]
status=None, # type: Optional[List[str]]
include_deprecated=None, # type: Optional[bool]
):
# type: (...) -> List[BoundImage]
name: str | None = None,
label_selector: str | None = None,
bound_to: list[str] | None = None,
type: list[str] | None = None,
architecture: list[str] | None = None,
sort: list[str] | None = None,
status: list[str] | None = None,
include_deprecated: bool | None = None,
) -> list[BoundImage]:
"""Get all images
:param name: str (optional)
@ -256,7 +279,8 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
Include deprecated images in the response. Default: False
:return: List[:class:`BoundImage <hcloud.images.client.BoundImage>`]
"""
return super().get_all(
return self._iter_pages(
self.get_list,
name=name,
label_selector=label_selector,
bound_to=bound_to,
@ -267,8 +291,7 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
include_deprecated=include_deprecated,
)
def get_by_name(self, name):
# type: (str) -> BoundImage
def get_by_name(self, name: str) -> BoundImage | None:
"""Get image by name
Deprecated: Use get_by_name_and_architecture instead.
@ -277,10 +300,13 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
Used to get image by name.
: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):
# type: (str, str) -> BoundImage
def get_by_name_and_architecture(
self,
name: str,
architecture: str,
) -> BoundImage | None:
"""Get image by name
:param name: str
@ -289,13 +315,15 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
Used to identify the image.
:return: :class:`BoundImage <hcloud.images.client.BoundImage>`
"""
response = self.get_list(name=name, architecture=[architecture])
entities = getattr(response, self.results_list_attribute_name)
entity = entities[0] if entities else None
return entity
return self._get_first_by(name=name, architecture=[architecture])
def update(self, image, description=None, type=None, labels=None):
# type:(Image, Optional[str], Optional[str], Optional[Dict[str, str]]) -> BoundImage
def update(
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.
: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)
:return: :class:`BoundImage <hcloud.images.client.BoundImage>`
"""
data = {}
data: dict[str, Any] = {}
if description is not None:
data.update({"description": description})
if type is not None:
@ -320,8 +348,7 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
)
return BoundImage(self, response["image"])
def delete(self, image):
# type: (Image) -> bool
def delete(self, image: Image | BoundImage) -> bool:
"""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>`
@ -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 True
def change_protection(self, image, delete=None):
# type: (Image, Optional[bool]) -> BoundAction
def change_protection(
self,
image: Image | BoundImage,
delete: bool | None = None,
) -> BoundAction:
"""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>`
@ -340,14 +370,12 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin):
If true, prevents the snapshot from being deleted
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = {}
data: dict[str, Any] = {}
if delete is not None:
data.update({"delete": delete})
response = self._client.request(
url="/images/{image_id}/actions/change_protection".format(
image_id=image.id
),
url=f"/images/{image.id}/actions/change_protection",
method="POST",
json=data,
)

View file

@ -1,9 +1,18 @@
from __future__ import annotations
from typing import TYPE_CHECKING
try:
from dateutil.parser import isoparse
except ImportError:
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):
@ -67,23 +76,23 @@ class Image(BaseDomain, DomainIdentityMixin):
def __init__(
self,
id=None,
name=None,
type=None,
created=None,
description=None,
image_size=None,
disk_size=None,
deprecated=None,
bound_to=None,
os_flavor=None,
os_version=None,
architecture=None,
rapid_deploy=None,
created_from=None,
protection=None,
labels=None,
status=None,
id: int | None = None,
name: str | None = None,
type: str | None = None,
created: str | None = None,
description: str | None = None,
image_size: int | None = None,
disk_size: int | None = None,
deprecated: str | None = None,
bound_to: Server | BoundServer | None = None,
os_flavor: str | None = None,
os_version: str | None = None,
architecture: str | None = None,
rapid_deploy: bool | None = None,
created_from: Server | BoundServer | None = None,
protection: dict | None = None,
labels: dict[str, str] | None = None,
status: str | None = None,
):
self.id = id
self.name = name
@ -117,8 +126,8 @@ class CreateImageResponse(BaseDomain):
def __init__(
self,
action, # type: BoundAction
image, # type: BoundImage
action: BoundAction,
image: BoundImage,
):
self.action = action
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 ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core import BoundModelBase, ClientEntityBase, Meta
from .domain import Iso
if TYPE_CHECKING:
from .._client import Client
class BoundIso(BoundModelBase):
_client: IsosClient
model = Iso
class IsosClient(ClientEntityBase, GetEntityByNameMixin):
results_list_attribute_name = "isos"
class IsosPageResult(NamedTuple):
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
:param id: int
@ -23,14 +35,13 @@ class IsosClient(ClientEntityBase, GetEntityByNameMixin):
def get_list(
self,
name=None, # type: Optional[str]
architecture=None, # type: Optional[List[str]]
include_wildcard_architecture=None, # type: Optional[bool]
include_architecture_wildcard=None, # type: Optional[bool]
page=None, # type: Optional[int]
per_page=None, # type: Optional[int]
):
# type: (...) -> PageResults[List[BoundIso], Meta]
name: str | None = None,
architecture: list[str] | None = None,
include_wildcard_architecture: bool | None = None,
include_architecture_wildcard: bool | None = None,
page: int | None = None,
per_page: int | None = None,
) -> IsosPageResult:
"""Get a list of ISOs
:param name: str (optional)
@ -56,7 +67,7 @@ class IsosClient(ClientEntityBase, GetEntityByNameMixin):
)
include_architecture_wildcard = include_wildcard_architecture
params = {}
params: dict[str, Any] = {}
if name is not None:
params["name"] = name
if architecture is not None:
@ -70,16 +81,15 @@ class IsosClient(ClientEntityBase, GetEntityByNameMixin):
response = self._client.request(url="/isos", method="GET", params=params)
isos = [BoundIso(self, iso_data) for iso_data in response["isos"]]
return self._add_meta_to_result(isos, response)
return IsosPageResult(isos, Meta.parse_meta(response))
def get_all(
self,
name=None, # type: Optional[str]
architecture=None, # type: Optional[List[str]]
include_wildcard_architecture=None, # type: Optional[bool]
include_architecture_wildcard=None, # type: Optional[bool]
):
# type: (...) -> List[BoundIso]
name: str | None = None,
architecture: list[str] | None = None,
include_wildcard_architecture: bool | None = None,
include_architecture_wildcard: bool | None = None,
) -> list[BoundIso]:
"""Get all ISOs
:param name: str (optional)
@ -101,18 +111,18 @@ class IsosClient(ClientEntityBase, GetEntityByNameMixin):
)
include_architecture_wildcard = include_wildcard_architecture
return super().get_all(
return self._iter_pages(
self.get_list,
name=name,
architecture=architecture,
include_architecture_wildcard=include_architecture_wildcard,
)
def get_by_name(self, name):
# type: (str) -> BoundIso
def get_by_name(self, name: str) -> BoundIso | None:
"""Get iso by name
:param name: str
Used to get iso by name.
: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:
from dateutil.parser import isoparse
except ImportError:
isoparse = None
from ..core.domain import BaseDomain, DomainIdentityMixin
from ..core import BaseDomain, DomainIdentityMixin
class Iso(BaseDomain, DomainIdentityMixin):
@ -27,12 +29,12 @@ class Iso(BaseDomain, DomainIdentityMixin):
def __init__(
self,
id=None,
name=None,
type=None,
architecture=None,
description=None,
deprecated=None,
id: int | None = None,
name: str | None = None,
type: str | None = None,
architecture: str | None = None,
description: str | None = None,
deprecated: str | None = None,
):
self.id = id
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
if TYPE_CHECKING:
from .._client import Client
class BoundLoadBalancerType(BoundModelBase):
_client: LoadBalancerTypesClient
model = LoadBalancerType
class LoadBalancerTypesClient(ClientEntityBase, GetEntityByNameMixin):
results_list_attribute_name = "load_balancer_types"
class LoadBalancerTypesPageResult(NamedTuple):
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.
:param id: int
:return: :class:`BoundLoadBalancerType <hcloud.load_balancer_type.client.BoundLoadBalancerType>`
"""
response = self._client.request(
url="/load_balancer_types/{load_balancer_type_id}".format(
load_balancer_type_id=id
),
url=f"/load_balancer_types/{id}",
method="GET",
)
return BoundLoadBalancerType(self, response["load_balancer_type"])
def get_list(self, name=None, page=None, per_page=None):
# type: (Optional[str], Optional[int], Optional[int]) -> PageResults[List[BoundLoadBalancerType], Meta]
def get_list(
self,
name: str | None = None,
page: int | None = None,
per_page: int | None = None,
) -> LoadBalancerTypesPageResult:
"""Get a list of Load Balancer types
:param name: str (optional)
@ -36,7 +51,7 @@ class LoadBalancerTypesClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page
: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:
params["name"] = name
if page is not None:
@ -51,24 +66,24 @@ class LoadBalancerTypesClient(ClientEntityBase, GetEntityByNameMixin):
BoundLoadBalancerType(self, load_balancer_type_data)
for load_balancer_type_data in response["load_balancer_types"]
]
return self._add_meta_to_result(load_balancer_types, response)
return LoadBalancerTypesPageResult(
load_balancer_types, Meta.parse_meta(response)
)
def get_all(self, name=None):
# type: (Optional[str]) -> List[BoundLoadBalancerType]
def get_all(self, name: str | None = None) -> list[BoundLoadBalancerType]:
"""Get all Load Balancer types
:param name: str (optional)
Can be used to filter Load Balancer type by their name.
: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):
# type: (str) -> BoundLoadBalancerType
def get_by_name(self, name: str) -> BoundLoadBalancerType | None:
"""Get Load Balancer type by name
:param name: str
Used to get Load Balancer type by name.
: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):
@ -36,14 +38,14 @@ class LoadBalancerType(BaseDomain, DomainIdentityMixin):
def __init__(
self,
id=None,
name=None,
description=None,
max_connections=None,
max_services=None,
max_targets=None,
max_assigned_certificates=None,
prices=None,
id: int | None = None,
name: str | None = None,
description: str | None = None,
max_connections: int | None = None,
max_services: int | None = None,
max_targets: int | None = None,
max_assigned_certificates: int | None = None,
prices: dict | None = None,
):
self.id = id
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 ..certificates.client import BoundCertificate
from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result
from ..load_balancer_types.client import BoundLoadBalancerType
from ..locations.client import BoundLocation
from ..networks.client import BoundNetwork
from ..servers.client import BoundServer
from __future__ import annotations
from typing import TYPE_CHECKING, Any, NamedTuple
from ..actions import ActionsPageResult, BoundAction
from ..certificates import BoundCertificate
from ..core import BoundModelBase, ClientEntityBase, Meta
from ..load_balancer_types import BoundLoadBalancerType
from ..locations import BoundLocation
from ..networks import BoundNetwork
from ..servers import BoundServer
from .domain import (
CreateLoadBalancerResponse,
IPv4Address,
@ -23,11 +26,19 @@ from .domain import (
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):
_client: LoadBalancersClient
model = LoadBalancer
def __init__(self, client, data, complete=True):
def __init__(self, client: LoadBalancersClient, data: dict, complete: bool = True):
algorithm = data.get("algorithm")
if algorithm:
data["algorithm"] = LoadBalancerAlgorithm(type=algorithm["type"])
@ -131,8 +142,11 @@ class BoundLoadBalancer(BoundModelBase):
super().__init__(client, data, complete)
def update(self, name=None, labels=None):
# type: (Optional[str], Optional[Dict[str, str]]) -> BoundLoadBalancer
def update(
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.
:param name: str (optional)
@ -143,16 +157,20 @@ class BoundLoadBalancer(BoundModelBase):
"""
return self._client.update(self, name, labels)
def delete(self):
# type: () -> BoundAction
def delete(self) -> bool:
"""Deletes a Load Balancer.
:return: boolean
"""
return self._client.delete(self)
def get_actions_list(self, status=None, sort=None, page=None, per_page=None):
# type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]]
def get_actions_list(
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.
:param status: List[str] (optional)
@ -167,8 +185,11 @@ class BoundLoadBalancer(BoundModelBase):
"""
return self._client.get_actions_list(self, status, sort, page, per_page)
def get_actions(self, status=None, sort=None):
# type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction]
def get_actions(
self,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a Load Balancer.
:param status: List[str] (optional)
@ -179,8 +200,7 @@ class BoundLoadBalancer(BoundModelBase):
"""
return self._client.get_actions(self, status, sort)
def add_service(self, service):
# type: (LoadBalancerService) -> List[BoundAction]
def add_service(self, service: LoadBalancerService) -> BoundAction:
"""Adds a service to a Load Balancer.
:param service: :class:`LoadBalancerService <hcloud.load_balancers.domain.LoadBalancerService>`
@ -189,8 +209,7 @@ class BoundLoadBalancer(BoundModelBase):
"""
return self._client.add_service(self, service=service)
def update_service(self, service):
# type: (LoadBalancerService) -> List[BoundAction]
def update_service(self, service: LoadBalancerService) -> BoundAction:
"""Updates a service of an Load Balancer.
:param service: :class:`LoadBalancerService <hcloud.load_balancers.domain.LoadBalancerService>`
@ -199,8 +218,7 @@ class BoundLoadBalancer(BoundModelBase):
"""
return self._client.update_service(self, service=service)
def delete_service(self, service):
# type: (LoadBalancerService) -> List[BoundAction]
def delete_service(self, service: LoadBalancerService) -> BoundAction:
"""Deletes a service from a Load Balancer.
:param service: :class:`LoadBalancerService <hcloud.load_balancers.domain.LoadBalancerService>`
@ -209,8 +227,7 @@ class BoundLoadBalancer(BoundModelBase):
"""
return self._client.delete_service(self, service)
def add_target(self, target):
# type: (LoadBalancerTarget) -> List[BoundAction]
def add_target(self, target: LoadBalancerTarget) -> BoundAction:
"""Adds a target to a Load Balancer.
:param target: :class:`LoadBalancerTarget <hcloud.load_balancers.domain.LoadBalancerTarget>`
@ -219,8 +236,7 @@ class BoundLoadBalancer(BoundModelBase):
"""
return self._client.add_target(self, target)
def remove_target(self, target):
# type: (LoadBalancerTarget) -> List[BoundAction]
def remove_target(self, target: LoadBalancerTarget) -> BoundAction:
"""Removes a target from a Load Balancer.
:param target: :class:`LoadBalancerTarget <hcloud.load_balancers.domain.LoadBalancerTarget>`
@ -229,8 +245,7 @@ class BoundLoadBalancer(BoundModelBase):
"""
return self._client.remove_target(self, target)
def change_algorithm(self, algorithm):
# type: (LoadBalancerAlgorithm) -> List[BoundAction]
def change_algorithm(self, algorithm: LoadBalancerAlgorithm) -> BoundAction:
"""Changes the algorithm used by the Load Balancer
:param algorithm: :class:`LoadBalancerAlgorithm <hcloud.load_balancers.domain.LoadBalancerAlgorithm>`
@ -239,8 +254,7 @@ class BoundLoadBalancer(BoundModelBase):
"""
return self._client.change_algorithm(self, algorithm)
def change_dns_ptr(self, ip, dns_ptr):
# type: (str, str) -> BoundAction
def change_dns_ptr(self, 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.
:param ip: str
@ -251,8 +265,7 @@ class BoundLoadBalancer(BoundModelBase):
"""
return self._client.change_dns_ptr(self, ip, dns_ptr)
def change_protection(self, delete):
# type: (LoadBalancerService) -> List[BoundAction]
def change_protection(self, delete: bool) -> BoundAction:
"""Changes the protection configuration of a Load Balancer.
:param delete: boolean
@ -261,8 +274,11 @@ class BoundLoadBalancer(BoundModelBase):
"""
return self._client.change_protection(self, delete)
def attach_to_network(self, network, ip=None):
# type: (Union[Network,BoundNetwork],Optional[str]) -> BoundAction
def attach_to_network(
self,
network: Network | BoundNetwork,
ip: str | None = None,
) -> BoundAction:
"""Attaches a Load Balancer to a 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)
def detach_from_network(self, network):
# type: ( Union[Network,BoundNetwork]) -> BoundAction
def detach_from_network(self, network: Network | BoundNetwork) -> BoundAction:
"""Detaches a Load Balancer from a 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)
def enable_public_interface(self):
# type: () -> BoundAction
def enable_public_interface(self) -> BoundAction:
"""Enables the public interface of a Load Balancer.
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
return self._client.enable_public_interface(self)
def disable_public_interface(self):
# type: () -> BoundAction
def disable_public_interface(self) -> BoundAction:
"""Disables the public interface of a Load Balancer.
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
return self._client.disable_public_interface(self)
def change_type(self, load_balancer_type):
# type: (Union[LoadBalancerType,BoundLoadBalancerType]) -> BoundAction
def change_type(
self,
load_balancer_type: LoadBalancerType | BoundLoadBalancerType,
) -> BoundAction:
"""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>`
@ -308,11 +323,15 @@ class BoundLoadBalancer(BoundModelBase):
return self._client.change_type(self, load_balancer_type)
class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
results_list_attribute_name = "load_balancers"
class LoadBalancersPageResult(NamedTuple):
load_balancers: list[BoundLoadBalancer]
meta: Meta | None
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
:param id: int
@ -326,12 +345,11 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
def get_list(
self,
name=None, # type: Optional[str]
label_selector=None, # type: Optional[str]
page=None, # type: Optional[int]
per_page=None, # type: Optional[int]
):
# type: (...) -> PageResults[List[BoundLoadBalancer], Meta]
name: str | None = None,
label_selector: str | None = None,
page: int | None = None,
per_page: int | None = None,
) -> LoadBalancersPageResult:
"""Get a list of Load Balancers from this account
:param name: str (optional)
@ -344,7 +362,7 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page
: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:
params["name"] = name
if label_selector is not None:
@ -358,14 +376,17 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
url="/load_balancers", method="GET", params=params
)
ass_load_balancers = [
load_balancers = [
BoundLoadBalancer(self, load_balancer_data)
for load_balancer_data in response["load_balancers"]
]
return self._add_meta_to_result(ass_load_balancers, response)
return LoadBalancersPageResult(load_balancers, Meta.parse_meta(response))
def get_all(self, name=None, label_selector=None):
# type: (Optional[str], Optional[str]) -> List[BoundLoadBalancer]
def get_all(
self,
name: str | None = None,
label_selector: str | None = None,
) -> list[BoundLoadBalancer]:
"""Get all Load Balancers from this account
: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.
: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):
# type: (str) -> BoundLoadBalancer
def get_by_name(self, name: str) -> BoundLoadBalancer | None:
"""Get Load Balancer by name
:param name: str
Used to get Load Balancer by name.
:return: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>`
"""
return super().get_by_name(name)
return self._get_first_by(name=name)
def create(
self,
name, # type: str
load_balancer_type, # type: LoadBalancerType
algorithm=None, # type: Optional[LoadBalancerAlgorithm]
services=None, # type: Optional[List[LoadBalancerService]]
targets=None, # type: Optional[List[LoadBalancerTarget]]
labels=None, # type: Optional[Dict[str, str]]
location=None, # type: Optional[Location]
network_zone=None, # type: Optional[str]
public_interface=None, # type: Optional[bool]
network=None, # type: Optional[Union[Network,BoundNetwork]]
):
# type: (...) -> CreateLoadBalancerResponse
name: str,
load_balancer_type: LoadBalancerType | BoundLoadBalancerType,
algorithm: LoadBalancerAlgorithm | None = None,
services: list[LoadBalancerService] | None = None,
targets: list[LoadBalancerTarget] | None = None,
labels: dict[str, str] | None = None,
location: Location | BoundLocation | None = None,
network_zone: str | None = None,
public_interface: bool | None = None,
network: Network | BoundNetwork | None = None,
) -> CreateLoadBalancerResponse:
"""Creates a Load Balancer .
:param name: str
@ -424,7 +443,10 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
Adds the Load Balancer to a Network
: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:
data["network"] = network.id
if public_interface is not None:
@ -434,30 +456,9 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
if algorithm is not None:
data["algorithm"] = {"type": algorithm.type}
if services is not None:
service_list = []
for service in services:
service_list.append(self.get_service_parameters(service))
data["services"] = service_list
data["services"] = [service.to_payload() for service in services]
if targets is not None:
target_list = []
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
data["targets"] = [target.to_payload() for target in targets]
if network_zone is not None:
data["network_zone"] = network_zone
if location is not None:
@ -470,8 +471,12 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
action=BoundAction(self._client.actions, response["action"]),
)
def update(self, load_balancer, name=None, labels=None):
# type:(LoadBalancer, Optional[str], Optional[Dict[str, str]]) -> BoundLoadBalancer
def update(
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.
: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)
:return: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>`
"""
data = {}
data: dict[str, Any] = {}
if name is not None:
data.update({"name": name})
if labels is not None:
data.update({"labels": labels})
response = self._client.request(
url="/load_balancers/{load_balancer_id}".format(
load_balancer_id=load_balancer.id
),
url=f"/load_balancers/{load_balancer.id}",
method="PUT",
json=data,
)
return BoundLoadBalancer(self, response["load_balancer"])
def delete(self, load_balancer):
# type: (LoadBalancer) -> BoundAction
def delete(self, load_balancer: LoadBalancer | BoundLoadBalancer) -> bool:
"""Deletes a Load Balancer.
:param load_balancer: :class:`BoundLoadBalancer <hcloud.load_balancers.client.BoundLoadBalancer>` or :class:`LoadBalancer <hcloud.load_balancers.domain.LoadBalancer>`
:return: boolean
"""
self._client.request(
url="/load_balancers/{load_balancer_id}".format(
load_balancer_id=load_balancer.id
),
url=f"/load_balancers/{load_balancer.id}",
method="DELETE",
)
return True
def get_actions_list(
self, load_balancer, status=None, sort=None, page=None, per_page=None
):
# type: (LoadBalancer, Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta]
self,
load_balancer: LoadBalancer | BoundLoadBalancer,
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.
: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
:return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`)
"""
params = {}
params: dict[str, Any] = {}
if status is not None:
params["status"] = status
if sort is not None:
@ -538,9 +542,7 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
params["per_page"] = per_page
response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions".format(
load_balancer_id=load_balancer.id
),
url=f"/load_balancers/{load_balancer.id}/actions",
method="GET",
params=params,
)
@ -548,10 +550,14 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
BoundAction(self._client.actions, action_data)
for action_data in response["actions"]
]
return add_meta_to_result(actions, response, "actions")
return ActionsPageResult(actions, Meta.parse_meta(response))
def get_actions(self, load_balancer, status=None, sort=None):
# type: (LoadBalancer, Optional[List[str]], Optional[List[str]]) -> List[BoundAction]
def get_actions(
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.
: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`
: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):
# type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerService) -> List[BoundAction]
def add_service(
self,
load_balancer: LoadBalancer | BoundLoadBalancer,
service: LoadBalancerService,
) -> BoundAction:
"""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>`
@ -572,84 +586,20 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
The LoadBalancerService you want to add to the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = self.get_service_parameters(service)
data: dict[str, Any] = service.to_payload()
response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/add_service".format(
load_balancer_id=load_balancer.id
),
url=f"/load_balancers/{load_balancer.id}/actions/add_service",
method="POST",
json=data,
)
return BoundAction(self._client.actions, response["action"])
def get_service_parameters(self, service):
data = {}
if service.protocol is not None:
data["protocol"] = service.protocol
if service.listen_port is not None:
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]
def update_service(
self,
load_balancer: LoadBalancer | BoundLoadBalancer,
service: LoadBalancerService,
) -> BoundAction:
"""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>`
@ -657,18 +607,19 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
The LoadBalancerService with updated values within for the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = self.get_service_parameters(service)
data: dict[str, Any] = service.to_payload()
response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/update_service".format(
load_balancer_id=load_balancer.id
),
url=f"/load_balancers/{load_balancer.id}/actions/update_service",
method="POST",
json=data,
)
return BoundAction(self._client.actions, response["action"])
def delete_service(self, load_balancer, service):
# type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerService) -> List[BoundAction]
def delete_service(
self,
load_balancer: LoadBalancer | BoundLoadBalancer,
service: LoadBalancerService,
) -> BoundAction:
"""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>`
@ -676,19 +627,20 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
The LoadBalancerService you want to delete from the Load Balancer
: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(
url="/load_balancers/{load_balancer_id}/actions/delete_service".format(
load_balancer_id=load_balancer.id
),
url=f"/load_balancers/{load_balancer.id}/actions/delete_service",
method="POST",
json=data,
)
return BoundAction(self._client.actions, response["action"])
def add_target(self, load_balancer, target):
# type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerTarget) -> List[BoundAction]
def add_target(
self,
load_balancer: LoadBalancer | BoundLoadBalancer,
target: LoadBalancerTarget,
) -> BoundAction:
"""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>`
@ -696,25 +648,20 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
The LoadBalancerTarget you want to add to the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = {"type": target.type, "use_private_ip": target.use_private_ip}
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}
data: dict[str, Any] = target.to_payload()
response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/add_target".format(
load_balancer_id=load_balancer.id
),
url=f"/load_balancers/{load_balancer.id}/actions/add_target",
method="POST",
json=data,
)
return BoundAction(self._client.actions, response["action"])
def remove_target(self, load_balancer, target):
# type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerTarget) -> List[BoundAction]
def remove_target(
self,
load_balancer: LoadBalancer | BoundLoadBalancer,
target: LoadBalancerTarget,
) -> BoundAction:
"""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>`
@ -722,25 +669,22 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
The LoadBalancerTarget you want to remove from the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = {"type": target.type}
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}
data: dict[str, Any] = target.to_payload()
# Do not send use_private_ip on remove_target
data.pop("use_private_ip", None)
response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/remove_target".format(
load_balancer_id=load_balancer.id
),
url=f"/load_balancers/{load_balancer.id}/actions/remove_target",
method="POST",
json=data,
)
return BoundAction(self._client.actions, response["action"])
def change_algorithm(self, load_balancer, algorithm):
# type: (Union[LoadBalancer, BoundLoadBalancer], Optional[bool]) -> BoundAction
def change_algorithm(
self,
load_balancer: LoadBalancer | BoundLoadBalancer,
algorithm: LoadBalancerAlgorithm,
) -> BoundAction:
"""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>`
@ -748,19 +692,21 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
The LoadBalancerSubnet you want to add to the Load Balancer
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = {"type": algorithm.type}
data: dict[str, Any] = {"type": algorithm.type}
response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/change_algorithm".format(
load_balancer_id=load_balancer.id
),
url=f"/load_balancers/{load_balancer.id}/actions/change_algorithm",
method="POST",
json=data,
)
return BoundAction(self._client.actions, response["action"])
def change_dns_ptr(self, load_balancer, ip, dns_ptr):
# type: (Union[LoadBalancer, BoundLoadBalancer], str, str) -> BoundAction
def change_dns_ptr(
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.
:param ip: str
@ -771,16 +717,17 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
"""
response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/change_dns_ptr".format(
load_balancer_id=load_balancer.id
),
url=f"/load_balancers/{load_balancer.id}/actions/change_dns_ptr",
method="POST",
json={"ip": ip, "dns_ptr": dns_ptr},
)
return BoundAction(self._client.actions, response["action"])
def change_protection(self, load_balancer, delete=None):
# type: (Union[LoadBalancer, BoundLoadBalancer], Optional[bool]) -> BoundAction
def change_protection(
self,
load_balancer: LoadBalancer | BoundLoadBalancer,
delete: bool | None = None,
) -> BoundAction:
"""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>`
@ -788,14 +735,12 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
If True, prevents the Load Balancer from being deleted
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = {}
data: dict[str, Any] = {}
if delete is not None:
data.update({"delete": delete})
response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/change_protection".format(
load_balancer_id=load_balancer.id
),
url=f"/load_balancers/{load_balancer.id}/actions/change_protection",
method="POST",
json=data,
)
@ -803,10 +748,10 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
def attach_to_network(
self,
load_balancer, # type: Union[LoadBalancer, BoundLoadBalancer]
network, # type: Union[Network, BoundNetwork]
ip=None, # type: Optional[str]
):
load_balancer: LoadBalancer | BoundLoadBalancer,
network: Network | BoundNetwork,
ip: str | None = None,
) -> BoundAction:
"""Attach a Load Balancer to a Network.
: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
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = {"network": network.id}
data: dict[str, Any] = {"network": network.id}
if ip is not None:
data.update({"ip": ip})
response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/attach_to_network".format(
load_balancer_id=load_balancer.id
),
url=f"/load_balancers/{load_balancer.id}/actions/attach_to_network",
method="POST",
json=data,
)
return BoundAction(self._client.actions, response["action"])
def detach_from_network(self, load_balancer, network):
# type: (Union[LoadBalancer, BoundLoadBalancer], Union[Network,BoundNetwork]) -> BoundAction
def detach_from_network(
self,
load_balancer: LoadBalancer | BoundLoadBalancer,
network: Network | BoundNetwork,
) -> BoundAction:
"""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 network: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>` or :class:`Network <hcloud.networks.domain.Network>`
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = {"network": network.id}
data: dict[str, Any] = {"network": network.id}
response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/detach_from_network".format(
load_balancer_id=load_balancer.id
),
url=f"/load_balancers/{load_balancer.id}/actions/detach_from_network",
method="POST",
json=data,
)
return BoundAction(self._client.actions, response["action"])
def enable_public_interface(self, load_balancer):
# type: (Union[LoadBalancer, BoundLoadBalancer]) -> BoundAction
def enable_public_interface(
self,
load_balancer: LoadBalancer | BoundLoadBalancer,
) -> BoundAction:
"""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>`
@ -856,15 +802,15 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
"""
response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/enable_public_interface".format(
load_balancer_id=load_balancer.id
),
url=f"/load_balancers/{load_balancer.id}/actions/enable_public_interface",
method="POST",
)
return BoundAction(self._client.actions, response["action"])
def disable_public_interface(self, load_balancer):
# type: (Union[LoadBalancer, BoundLoadBalancer]) -> BoundAction
def disable_public_interface(
self,
load_balancer: LoadBalancer | BoundLoadBalancer,
) -> BoundAction:
"""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>`
@ -873,15 +819,16 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
"""
response = self._client.request(
url="/load_balancers/{load_balancer_id}/actions/disable_public_interface".format(
load_balancer_id=load_balancer.id
),
url=f"/load_balancers/{load_balancer.id}/actions/disable_public_interface",
method="POST",
)
return BoundAction(self._client.actions, response["action"])
def change_type(self, load_balancer, load_balancer_type):
# type: ([LoadBalancer, BoundLoadBalancer], [LoadBalancerType, BoundLoadBalancerType]) ->BoundAction
def change_type(
self,
load_balancer: LoadBalancer | BoundLoadBalancer,
load_balancer_type: LoadBalancerType | BoundLoadBalancerType,
) -> BoundAction:
"""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>`
@ -889,11 +836,9 @@ class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin):
Load Balancer type the Load Balancer should migrate to
: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(
url="/load_balancers/{load_balancer_id}/actions/change_type".format(
load_balancer_id=load_balancer.id
),
url=f"/load_balancers/{load_balancer.id}/actions/change_type",
method="POST",
json=data,
)

View file

@ -1,9 +1,22 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any
try:
from dateutil.parser import isoparse
except ImportError:
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):
@ -61,21 +74,21 @@ class LoadBalancer(BaseDomain):
def __init__(
self,
id,
name=None,
public_net=None,
private_net=None,
location=None,
algorithm=None,
services=None,
load_balancer_type=None,
protection=None,
labels=None,
targets=None,
created=None,
outgoing_traffic=None,
ingoing_traffic=None,
included_traffic=None,
id: int,
name: str | None = None,
public_net: PublicNetwork | None = None,
private_net: PrivateNet | None = None,
location: BoundLocation | None = None,
algorithm: LoadBalancerAlgorithm | None = None,
services: list[LoadBalancerService] | None = None,
load_balancer_type: BoundLoadBalancerType | None = None,
protection: dict | None = None,
labels: dict[str, str] | None = None,
targets: list[LoadBalancerTarget] | None = None,
created: str | None = None,
outgoing_traffic: int | None = None,
ingoing_traffic: int | None = None,
included_traffic: int | None = None,
):
self.id = id
self.name = name
@ -113,12 +126,12 @@ class LoadBalancerService(BaseDomain):
def __init__(
self,
protocol=None,
listen_port=None,
destination_port=None,
proxyprotocol=None,
health_check=None,
http=None,
protocol: str | None = None,
listen_port: int | None = None,
destination_port: int | None = None,
proxyprotocol: bool | None = None,
health_check: LoadBalancerHealthCheck | None = None,
http: LoadBalancerServiceHttp | None = None,
):
self.protocol = protocol
self.listen_port = listen_port
@ -127,6 +140,74 @@ class LoadBalancerService(BaseDomain):
self.health_check = health_check
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):
"""LoadBalancerServiceHttp Domain
@ -145,11 +226,11 @@ class LoadBalancerServiceHttp(BaseDomain):
def __init__(
self,
cookie_name=None,
cookie_lifetime=None,
certificates=None,
redirect_http=None,
sticky_sessions=None,
cookie_name: str | None = None,
cookie_lifetime: str | None = None,
certificates: list[BoundCertificate] | None = None,
redirect_http: bool | None = None,
sticky_sessions: bool | None = None,
):
self.cookie_name = cookie_name
self.cookie_lifetime = cookie_lifetime
@ -177,12 +258,12 @@ class LoadBalancerHealthCheck(BaseDomain):
def __init__(
self,
protocol=None,
port=None,
interval=None,
timeout=None,
retries=None,
http=None,
protocol: str | None = None,
port: int | None = None,
interval: int | None = None,
timeout: int | None = None,
retries: int | None = None,
http: LoadBalancerHealtCheckHttp | None = None,
):
self.protocol = protocol
self.port = port
@ -208,7 +289,12 @@ class LoadBalancerHealtCheckHttp(BaseDomain):
"""
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.path = path
@ -233,7 +319,12 @@ class LoadBalancerTarget(BaseDomain):
"""
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.server = server
@ -241,6 +332,30 @@ class LoadBalancerTarget(BaseDomain):
self.ip = 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):
"""LoadBalancerTargetLabelSelector Domain
@ -248,7 +363,7 @@ class LoadBalancerTargetLabelSelector(BaseDomain):
:param selector: str Target label selector
"""
def __init__(self, selector=None):
def __init__(self, selector: str | None = None):
self.selector = selector
@ -258,7 +373,7 @@ class LoadBalancerTargetIP(BaseDomain):
:param ip: str Target IP
"""
def __init__(self, ip=None):
def __init__(self, ip: str | None = None):
self.ip = ip
@ -269,7 +384,7 @@ class LoadBalancerAlgorithm(BaseDomain):
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
@ -285,9 +400,9 @@ class PublicNetwork(BaseDomain):
def __init__(
self,
ipv4, # type: IPv4Address
ipv6, # type: IPv6Network
enabled, # type: bool
ipv4: IPv4Address,
ipv6: IPv6Network,
enabled: bool,
):
self.ipv4 = ipv4
self.ipv6 = ipv6
@ -305,8 +420,8 @@ class IPv4Address(BaseDomain):
def __init__(
self,
ip, # type: str
dns_ptr, # type: str
ip: str,
dns_ptr: str,
):
self.ip = ip
self.dns_ptr = dns_ptr
@ -323,8 +438,8 @@ class IPv6Network(BaseDomain):
def __init__(
self,
ip, # type: str
dns_ptr, # type: str
ip: str,
dns_ptr: str,
):
self.ip = ip
self.dns_ptr = dns_ptr
@ -343,8 +458,8 @@ class PrivateNet(BaseDomain):
def __init__(
self,
network, # type: BoundNetwork
ip, # type: str
network: BoundNetwork,
ip: str,
):
self.network = network
self.ip = ip
@ -363,8 +478,8 @@ class CreateLoadBalancerResponse(BaseDomain):
def __init__(
self,
load_balancer, # type: BoundLoadBalancer
action, # type: BoundAction
load_balancer: BoundLoadBalancer,
action: BoundAction,
):
self.load_balancer = load_balancer
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
if TYPE_CHECKING:
from .._client import Client
class BoundLocation(BoundModelBase):
_client: LocationsClient
model = Location
class LocationsClient(ClientEntityBase, GetEntityByNameMixin):
results_list_attribute_name = "locations"
class LocationsPageResult(NamedTuple):
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.
:param id: int
@ -19,8 +32,12 @@ class LocationsClient(ClientEntityBase, GetEntityByNameMixin):
response = self._client.request(url=f"/locations/{id}", method="GET")
return BoundLocation(self, response["location"])
def get_list(self, name=None, page=None, per_page=None):
# type: (Optional[str], Optional[int], Optional[int]) -> PageResult[List[BoundLocation], Meta]
def get_list(
self,
name: str | None = None,
page: int | None = None,
per_page: int | None = None,
) -> LocationsPageResult:
"""Get a list of locations
:param name: str (optional)
@ -31,7 +48,7 @@ class LocationsClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page
:return: (List[:class:`BoundLocation <hcloud.locations.client.BoundLocation>`], :class:`Meta <hcloud.core.domain.Meta>`)
"""
params = {}
params: dict[str, Any] = {}
if name is not None:
params["name"] = name
if page is not None:
@ -44,24 +61,22 @@ class LocationsClient(ClientEntityBase, GetEntityByNameMixin):
BoundLocation(self, location_data)
for location_data in response["locations"]
]
return self._add_meta_to_result(locations, response)
return LocationsPageResult(locations, Meta.parse_meta(response))
def get_all(self, name=None):
# type: (Optional[str]) -> List[BoundLocation]
def get_all(self, name: str | None = None) -> list[BoundLocation]:
"""Get all locations
:param name: str (optional)
Can be used to filter locations by their name.
: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):
# type: (str) -> BoundLocation
def get_by_name(self, name: str) -> BoundLocation | None:
"""Get location by name
:param name: str
Used to get location by name.
: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):
@ -35,14 +37,14 @@ class Location(BaseDomain, DomainIdentityMixin):
def __init__(
self,
id=None,
name=None,
description=None,
country=None,
city=None,
latitude=None,
longitude=None,
network_zone=None,
id: int | None = None,
name: str | None = None,
description: str | None = None,
country: str | None = None,
city: str | None = None,
latitude: float | None = None,
longitude: float | None = None,
network_zone: str | None = None,
):
self.id = id
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 ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result
from __future__ import annotations
from typing import TYPE_CHECKING, Any, NamedTuple
from ..actions import ActionsPageResult, BoundAction
from ..core import BoundModelBase, ClientEntityBase, Meta
from .domain import Network, NetworkRoute, NetworkSubnet
if TYPE_CHECKING:
from .._client import Client
class BoundNetwork(BoundModelBase):
_client: NetworksClient
model = Network
def __init__(self, client, data, complete=True):
def __init__(self, client: NetworksClient, data: dict, complete: bool = True):
subnets = data.get("subnets", [])
if subnets is not None:
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]
data["routes"] = routes
from ..servers.client import BoundServer
from ..servers import BoundServer
servers = data.get("servers", [])
if servers is not None:
@ -32,10 +40,10 @@ class BoundNetwork(BoundModelBase):
def update(
self,
name=None, # type: Optional[str]
expose_routes_to_vswitch=None, # type: Optional[bool]
labels=None, # type: Optional[Dict[str, str]]
): # type: (...) -> 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 networkss labels.
:param name: str (optional)
@ -54,16 +62,20 @@ class BoundNetwork(BoundModelBase):
labels=labels,
)
def delete(self):
# type: () -> BoundAction
def delete(self) -> bool:
"""Deletes a network.
:return: boolean
"""
return self._client.delete(self)
def get_actions_list(self, status=None, sort=None, page=None, per_page=None):
# type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]]
def get_actions_list(
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.
:param status: List[str] (optional)
@ -78,8 +90,11 @@ class BoundNetwork(BoundModelBase):
"""
return self._client.get_actions_list(self, status, sort, page, per_page)
def get_actions(self, status=None, sort=None):
# type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction]
def get_actions(
self,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a network.
:param status: List[str] (optional)
@ -90,8 +105,7 @@ class BoundNetwork(BoundModelBase):
"""
return self._client.get_actions(self, status, sort)
def add_subnet(self, subnet):
# type: (NetworkSubnet) -> List[BoundAction]
def add_subnet(self, subnet: NetworkSubnet) -> BoundAction:
"""Adds a subnet entry to a network.
:param subnet: :class:`NetworkSubnet <hcloud.networks.domain.NetworkSubnet>`
@ -100,8 +114,7 @@ class BoundNetwork(BoundModelBase):
"""
return self._client.add_subnet(self, subnet=subnet)
def delete_subnet(self, subnet):
# type: (NetworkSubnet) -> List[BoundAction]
def delete_subnet(self, subnet: NetworkSubnet) -> BoundAction:
"""Removes a subnet entry from a network
:param subnet: :class:`NetworkSubnet <hcloud.networks.domain.NetworkSubnet>`
@ -110,8 +123,7 @@ class BoundNetwork(BoundModelBase):
"""
return self._client.delete_subnet(self, subnet=subnet)
def add_route(self, route):
# type: (NetworkRoute) -> List[BoundAction]
def add_route(self, route: NetworkRoute) -> BoundAction:
"""Adds a route entry to a network.
:param route: :class:`NetworkRoute <hcloud.networks.domain.NetworkRoute>`
@ -120,8 +132,7 @@ class BoundNetwork(BoundModelBase):
"""
return self._client.add_route(self, route=route)
def delete_route(self, route):
# type: (NetworkRoute) -> List[BoundAction]
def delete_route(self, route: NetworkRoute) -> BoundAction:
"""Removes a route entry to a network.
:param route: :class:`NetworkRoute <hcloud.networks.domain.NetworkRoute>`
@ -130,8 +141,7 @@ class BoundNetwork(BoundModelBase):
"""
return self._client.delete_route(self, route=route)
def change_ip_range(self, ip_range):
# type: (str) -> List[BoundAction]
def change_ip_range(self, ip_range: str) -> BoundAction:
"""Changes the IP range of a network.
:param ip_range: str
@ -140,8 +150,7 @@ class BoundNetwork(BoundModelBase):
"""
return self._client.change_ip_range(self, ip_range=ip_range)
def change_protection(self, delete=None):
# type: (Optional[bool]) -> BoundAction
def change_protection(self, delete: bool | None = None) -> BoundAction:
"""Changes the protection configuration of a network.
:param delete: boolean
@ -151,11 +160,15 @@ class BoundNetwork(BoundModelBase):
return self._client.change_protection(self, delete=delete)
class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
results_list_attribute_name = "networks"
class NetworksPageResult(NamedTuple):
networks: list[BoundNetwork]
meta: Meta | None
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
:param id: int
@ -166,12 +179,11 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
def get_list(
self,
name=None, # type: Optional[str]
label_selector=None, # type: Optional[str]
page=None, # type: Optional[int]
per_page=None, # type: Optional[int]
):
# type: (...) -> PageResults[List[BoundNetwork], Meta]
name: str | None = None,
label_selector: str | None = None,
page: int | None = None,
per_page: int | None = None,
) -> NetworksPageResult:
"""Get a list of networks from this account
:param name: str (optional)
@ -184,7 +196,7 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page
:return: (List[:class:`BoundNetwork <hcloud.networks.client.BoundNetwork>`], :class:`Meta <hcloud.core.domain.Meta>`)
"""
params = {}
params: dict[str, Any] = {}
if name is not None:
params["name"] = name
if label_selector is not None:
@ -196,13 +208,16 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
response = self._client.request(url="/networks", method="GET", params=params)
ass_networks = [
networks = [
BoundNetwork(self, network_data) for network_data in response["networks"]
]
return self._add_meta_to_result(ass_networks, response)
return NetworksPageResult(networks, Meta.parse_meta(response))
def get_all(self, name=None, label_selector=None):
# type: (Optional[str], Optional[str]) -> List[BoundNetwork]
def get_all(
self,
name: str | None = None,
label_selector: str | None = None,
) -> list[BoundNetwork]:
"""Get all networks from this account
: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.
: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):
# type: (str) -> BoundNetwork
def get_by_name(self, name: str) -> BoundNetwork | None:
"""Get network by name
:param name: str
Used to get network by name.
:return: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>`
"""
return super().get_by_name(name)
return self._get_first_by(name=name)
def create(
self,
name, # type: str
ip_range, # type: str
subnets=None, # type: Optional[List[NetworkSubnet]]
routes=None, # type: Optional[List[NetworkRoute]]
expose_routes_to_vswitch=None, # type: Optional[bool]
labels=None, # type: Optional[Dict[str, str]]
):
name: str,
ip_range: str,
subnets: list[NetworkSubnet] | None = None,
routes: list[NetworkRoute] | None = None,
expose_routes_to_vswitch: bool | None = None,
labels: dict[str, str] | None = None,
) -> BoundNetwork:
"""Creates a network with range ip_range.
:param name: str
@ -249,11 +263,11 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
User-defined labels (key-value pairs)
: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:
data_subnets = []
for subnet in subnets:
data_subnet = {
data_subnet: dict[str, Any] = {
"type": subnet.type,
"ip_range": subnet.ip_range,
"network_zone": subnet.network_zone,
@ -280,8 +294,13 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
return BoundNetwork(self, response["network"])
def update(self, network, name=None, expose_routes_to_vswitch=None, labels=None):
# type:(Network, Optional[str], Optional[bool], Optional[Dict[str, str]]) -> BoundNetwork
def update(
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.
: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)
:return: :class:`BoundNetwork <hcloud.networks.client.BoundNetwork>`
"""
data = {}
data: dict[str, Any] = {}
if name is not None:
data.update({"name": name})
@ -311,8 +330,7 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
)
return BoundNetwork(self, response["network"])
def delete(self, network):
# type: (Network) -> BoundAction
def delete(self, network: Network | BoundNetwork) -> bool:
"""Deletes a 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
def get_actions_list(
self, network, status=None, sort=None, page=None, per_page=None
):
# type: (Network, Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta]
self,
network: Network | BoundNetwork,
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.
: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
:return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`)
"""
params = {}
params: dict[str, Any] = {}
if status is not None:
params["status"] = status
if sort is not None:
@ -357,10 +379,14 @@ class NetworksClient(ClientEntityBase, GetEntityByNameMixin):
BoundAction(self._client.actions, action_data)
for action_data in response["actions"]
]
return add_meta_to_result(actions, response, "actions")
return ActionsPageResult(actions, Meta.parse_meta(response))
def get_actions(self, network, status=None, sort=None):
# type: (Network, Optional[List[str]], Optional[List[str]]) -> List[BoundAction]
def get_actions(
self,
network: Network | BoundNetwork,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a 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`
: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):
# type: (Union[Network, BoundNetwork], NetworkSubnet) -> List[BoundAction]
def add_subnet(
self,
network: Network | BoundNetwork,
subnet: NetworkSubnet,
) -> BoundAction:
"""Adds a subnet entry to a 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
: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:
data["ip_range"] = subnet.ip_range
if subnet.vswitch_id is not None:
data["vswitch_id"] = subnet.vswitch_id
response = self._client.request(
url="/networks/{network_id}/actions/add_subnet".format(
network_id=network.id
),
url=f"/networks/{network.id}/actions/add_subnet",
method="POST",
json=data,
)
return BoundAction(self._client.actions, response["action"])
def delete_subnet(self, network, subnet):
# type: (Union[Network, BoundNetwork], NetworkSubnet) -> List[BoundAction]
def delete_subnet(
self,
network: Network | BoundNetwork,
subnet: NetworkSubnet,
) -> BoundAction:
"""Removes a subnet entry from a 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
: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(
url="/networks/{network_id}/actions/delete_subnet".format(
network_id=network.id
),
url=f"/networks/{network.id}/actions/delete_subnet",
method="POST",
json=data,
)
return BoundAction(self._client.actions, response["action"])
def add_route(self, network, route):
# type: (Union[Network, BoundNetwork], NetworkRoute) -> List[BoundAction]
def add_route(
self,
network: Network | BoundNetwork,
route: NetworkRoute,
) -> BoundAction:
"""Adds a route entry to a 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
: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(
url="/networks/{network_id}/actions/add_route".format(
network_id=network.id
),
url=f"/networks/{network.id}/actions/add_route",
method="POST",
json=data,
)
return BoundAction(self._client.actions, response["action"])
def delete_route(self, network, route):
# type: (Union[Network, BoundNetwork], NetworkRoute) -> List[BoundAction]
def delete_route(
self,
network: Network | BoundNetwork,
route: NetworkRoute,
) -> BoundAction:
"""Removes a route entry to a 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
: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(
url="/networks/{network_id}/actions/delete_route".format(
network_id=network.id
),
url=f"/networks/{network.id}/actions/delete_route",
method="POST",
json=data,
)
return BoundAction(self._client.actions, response["action"])
def change_ip_range(self, network, ip_range):
# type: (Union[Network, BoundNetwork], str) -> List[BoundAction]
def change_ip_range(
self,
network: Network | BoundNetwork,
ip_range: str,
) -> BoundAction:
"""Changes the IP range of a 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.
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = {"ip_range": ip_range}
data: dict[str, Any] = {"ip_range": ip_range}
response = self._client.request(
url="/networks/{network_id}/actions/change_ip_range".format(
network_id=network.id
),
url=f"/networks/{network.id}/actions/change_ip_range",
method="POST",
json=data,
)
return BoundAction(self._client.actions, response["action"])
def change_protection(self, network, delete=None):
# type: (Union[Network, BoundNetwork], Optional[bool]) -> BoundAction
def change_protection(
self,
network: Network | BoundNetwork,
delete: bool | None = None,
) -> BoundAction:
"""Changes the protection configuration of a 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
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = {}
data: dict[str, Any] = {}
if delete is not None:
data.update({"delete": delete})
response = self._client.request(
url="/networks/{network_id}/actions/change_protection".format(
network_id=network.id
),
url=f"/networks/{network.id}/actions/change_protection",
method="POST",
json=data,
)

View file

@ -1,9 +1,18 @@
from __future__ import annotations
from typing import TYPE_CHECKING
try:
from dateutil.parser import isoparse
except ImportError:
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):
@ -44,16 +53,16 @@ class Network(BaseDomain):
def __init__(
self,
id,
name=None,
created=None,
ip_range=None,
subnets=None,
routes=None,
expose_routes_to_vswitch=None,
servers=None,
protection=None,
labels=None,
id: int,
name: str | None = None,
created: str | None = None,
ip_range: str | None = None,
subnets: list[NetworkSubnet] | None = None,
routes: list[NetworkRoute] | None = None,
expose_routes_to_vswitch: bool | None = None,
servers: list[BoundServer] | None = None,
protection: dict | None = None,
labels: dict[str, str] | None = None,
):
self.id = id
self.name = name
@ -91,7 +100,12 @@ class NetworkSubnet(BaseDomain):
__slots__ = ("type", "ip_range", "network_zone", "gateway", "vswitch_id")
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.ip_range = ip_range
@ -111,7 +125,7 @@ class NetworkRoute(BaseDomain):
__slots__ = ("destination", "gateway")
def __init__(self, destination, gateway):
def __init__(self, destination: str, gateway: str):
self.destination = destination
self.gateway = gateway
@ -129,8 +143,8 @@ class CreateNetworkResponse(BaseDomain):
def __init__(
self,
network, # type: BoundNetwork
action, # type: BoundAction
network: BoundNetwork,
action: BoundAction,
):
self.network = network
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 ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from __future__ import annotations
from typing import TYPE_CHECKING, Any, NamedTuple
from ..actions import BoundAction
from ..core import BoundModelBase, ClientEntityBase, Meta
from .domain import CreatePlacementGroupResponse, PlacementGroup
if TYPE_CHECKING:
from .._client import Client
class BoundPlacementGroup(BoundModelBase):
_client: PlacementGroupsClient
model = PlacementGroup
def update(self, labels=None, name=None):
# type: (Optional[str], Optional[Dict[str, str]], Optional[str]) -> BoundPlacementGroup
def update(
self,
labels: dict[str, str] | None = None,
name: str | None = None,
) -> BoundPlacementGroup:
"""Updates the name or labels of a Placement Group
:param labels: Dict[str, str] (optional)
@ -18,8 +30,7 @@ class BoundPlacementGroup(BoundModelBase):
"""
return self._client.update(self, labels, name)
def delete(self):
# type: () -> bool
def delete(self) -> bool:
"""Deletes a Placement Group
:return: boolean
@ -27,11 +38,15 @@ class BoundPlacementGroup(BoundModelBase):
return self._client.delete(self)
class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin):
results_list_attribute_name = "placement_groups"
class PlacementGroupsPageResult(NamedTuple):
placement_groups: list[BoundPlacementGroup]
meta: Meta | None
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
:param id: int
@ -45,14 +60,13 @@ class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin):
def get_list(
self,
label_selector=None, # type: Optional[str]
page=None, # type: Optional[int]
per_page=None, # type: Optional[int]
name=None, # type: Optional[str]
sort=None, # type: Optional[List[str]]
type=None, # type: Optional[str]
):
# type: (...) -> PageResults[List[BoundPlacementGroup]]
label_selector: str | None = None,
page: int | None = None,
per_page: int | None = None,
name: str | None = None,
sort: list[str] | None = None,
type: str | None = None,
) -> PlacementGroupsPageResult:
"""Get a list of Placement Groups
: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>`)
"""
params = {}
params: dict[str, Any] = {}
if label_selector is not None:
params["label_selector"] = label_selector
@ -90,10 +104,14 @@ class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin):
for placement_group_data in response["placement_groups"]
]
return self._add_meta_to_result(placement_groups, response)
return PlacementGroupsPageResult(placement_groups, Meta.parse_meta(response))
def get_all(self, label_selector=None, name=None, sort=None):
# type: (Optional[str], Optional[str], Optional[List[str]]) -> List[BoundPlacementGroup]
def get_all(
self,
label_selector: str | None = None,
name: str | None = None,
sort: list[str] | None = None,
) -> list[BoundPlacementGroup]:
"""Get all Placement Groups
: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))
: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):
# type: (str) -> BoundPlacementGroup
def get_by_name(self, name: str) -> BoundPlacementGroup | None:
"""Get Placement Group by name
:param name: str
Used to get Placement Group by name
:return: class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>`
"""
return super().get_by_name(name)
return self._get_first_by(name=name)
def create(
self,
name, # type: str
type, # type: str
labels=None, # type: Optional[Dict[str, str]]
):
# type: (...) -> CreatePlacementGroupResponse
name: str,
type: str,
labels: dict[str, str] | None = None,
) -> CreatePlacementGroupResponse:
"""Creates a new Placement Group.
:param name: str
@ -134,7 +155,7 @@ class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin):
: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:
data["labels"] = labels
response = self._client.request(
@ -143,7 +164,7 @@ class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin):
action = None
if response.get("action") is not None:
action = BoundAction(self._client.action, response["action"])
action = BoundAction(self._client.actions, response["action"])
result = CreatePlacementGroupResponse(
placement_group=BoundPlacementGroup(self, response["placement_group"]),
@ -151,8 +172,12 @@ class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin):
)
return result
def update(self, placement_group, labels=None, name=None):
# type: (PlacementGroup, Optional[Dict[str, str]], Optional[str]) -> BoundPlacementGroup
def update(
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.
: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>`
"""
data = {}
data: dict[str, Any] = {}
if labels is not None:
data["labels"] = labels
if name is not None:
data["name"] = name
response = self._client.request(
url="/placement_groups/{placement_group_id}".format(
placement_group_id=placement_group.id
),
url=f"/placement_groups/{placement_group.id}",
method="PUT",
json=data,
)
return BoundPlacementGroup(self, response["placement_group"])
def delete(self, placement_group):
# type: (PlacementGroup) -> bool
def delete(self, placement_group: PlacementGroup | BoundPlacementGroup) -> bool:
"""Deletes a Placement Group.
:param placement_group: :class:`BoundPlacementGroup <hcloud.placement_groups.client.BoundPlacementGroup>` or :class:`PlacementGroup <hcloud.placement_groups.domain.PlacementGroup>`
:return: boolean
"""
self._client.request(
url="/placement_groups/{placement_group_id}".format(
placement_group_id=placement_group.id
),
url=f"/placement_groups/{placement_group.id}",
method="DELETE",
)
return True

View file

@ -1,9 +1,17 @@
from __future__ import annotations
from typing import TYPE_CHECKING
try:
from dateutil.parser import isoparse
except ImportError:
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):
@ -31,7 +39,13 @@ class PlacementGroup(BaseDomain):
TYPE_SPREAD = "spread"
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.name = name
@ -54,8 +68,8 @@ class CreatePlacementGroupResponse(BaseDomain):
def __init__(
self,
placement_group, # type: BoundPlacementGroup
action, # type: BoundAction
placement_group: BoundPlacementGroup,
action: BoundAction | None,
):
self.placement_group = placement_group
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 ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from __future__ import annotations
from typing import TYPE_CHECKING, Any, NamedTuple
from ..actions import BoundAction
from ..core import BoundModelBase, ClientEntityBase, Meta
from .domain import CreatePrimaryIPResponse, PrimaryIP
if TYPE_CHECKING:
from .._client import Client
from ..datacenters import BoundDatacenter, Datacenter
class BoundPrimaryIP(BoundModelBase):
_client: PrimaryIPsClient
model = PrimaryIP
def __init__(self, client, data, complete=True):
from ..datacenters.client import BoundDatacenter
def __init__(self, client: PrimaryIPsClient, data: dict, complete: bool = True):
from ..datacenters import BoundDatacenter
datacenter = data.get("datacenter", {})
if datacenter:
@ -15,8 +25,12 @@ class BoundPrimaryIP(BoundModelBase):
super().__init__(client, data, complete)
def update(self, auto_delete=None, labels=None, name=None):
# type: (Optional[bool], Optional[Dict[str, str]], Optional[str]) -> BoundPrimaryIP
def update(
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.
:param auto_delete: bool (optional)
@ -31,16 +45,14 @@ class BoundPrimaryIP(BoundModelBase):
self, auto_delete=auto_delete, labels=labels, name=name
)
def delete(self):
# type: () -> bool
def delete(self) -> bool:
"""Deletes a Primary IP. If it is currently assigned to a server it will automatically get unassigned.
:return: boolean
"""
return self._client.delete(self)
def change_protection(self, delete=None):
# type: (Optional[bool]) -> BoundAction
def change_protection(self, delete: bool | None = None) -> BoundAction:
"""Changes the protection configuration of the Primary IP.
:param delete: boolean
@ -49,8 +61,7 @@ class BoundPrimaryIP(BoundModelBase):
"""
return self._client.change_protection(self, delete)
def assign(self, assignee_id, assignee_type):
# type: (int,str) -> BoundAction
def assign(self, assignee_id: int, assignee_type: str) -> BoundAction:
"""Assigns a Primary IP to a assignee.
:param assignee_id: int`
@ -61,16 +72,14 @@ class BoundPrimaryIP(BoundModelBase):
"""
return self._client.assign(self, assignee_id, assignee_type)
def unassign(self):
# type: () -> BoundAction
def unassign(self) -> BoundAction:
"""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 self._client.unassign(self)
def change_dns_ptr(self, ip, dns_ptr):
# type: (str, str) -> BoundAction
def change_dns_ptr(self, ip: str, dns_ptr: str) -> BoundAction:
"""Changes the hostname that will appear when getting the hostname belonging to this Primary IP.
:param ip: str
@ -82,11 +91,15 @@ class BoundPrimaryIP(BoundModelBase):
return self._client.change_dns_ptr(self, ip, dns_ptr)
class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
results_list_attribute_name = "primary_ips"
class PrimaryIPsPageResult(NamedTuple):
primary_ips: list[BoundPrimaryIP]
meta: Meta | None
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.
:param id: int
@ -97,13 +110,12 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
def get_list(
self,
label_selector=None, # type: Optional[str]
page=None, # type: Optional[int]
per_page=None, # type: Optional[int]
name=None, # type: Optional[str]
ip=None, # type: Optional[ip]
):
# type: (...) -> PageResults[List[BoundPrimaryIP]]
label_selector: str | None = None,
page: int | None = None,
per_page: int | None = None,
name: str | None = None,
ip: str | None = None,
) -> PrimaryIPsPageResult:
"""Get a list of primary ips from this account
: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.
: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:
params["label_selector"] = label_selector
@ -137,10 +149,13 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
for primary_ip_data in response["primary_ips"]
]
return self._add_meta_to_result(primary_ips, response)
return PrimaryIPsPageResult(primary_ips, Meta.parse_meta(response))
def get_all(self, label_selector=None, name=None):
# type: (Optional[str], Optional[str]) -> List[BoundPrimaryIP]
def get_all(
self,
label_selector: str | None = None,
name: str | None = None,
) -> list[BoundPrimaryIP]:
"""Get all primary ips from this account
:param label_selector: str (optional)
@ -149,29 +164,28 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
Can be used to filter networks by their name.
: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):
# type: (str) -> BoundPrimaryIP
def get_by_name(self, name: str) -> BoundPrimaryIP | None:
"""Get Primary IP by name
:param name: str
Used to get Primary IP by name.
:return: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>`
"""
return super().get_by_name(name)
return self._get_first_by(name=name)
def create(
self,
type, # type: str
datacenter, # type: Datacenter
name, # type: str
assignee_type="server", # type: Optional[str]
assignee_id=None, # type: Optional[int]
auto_delete=False, # type: Optional[bool]
labels=None, # type: Optional[dict]
):
# type: (...) -> CreatePrimaryIPResponse
type: str,
# TODO: Make the datacenter argument optional
datacenter: Datacenter | BoundDatacenter | None,
name: str,
assignee_type: str | None = "server",
assignee_id: int | None = None,
auto_delete: bool | None = False,
labels: dict | None = None,
) -> CreatePrimaryIPResponse:
"""Creates a new Primary IP assigned to a server.
:param type: str
@ -186,14 +200,15 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
:return: :class:`CreatePrimaryIPResponse <hcloud.primary_ips.domain.CreatePrimaryIPResponse>`
"""
data = {
data: dict[str, Any] = {
"type": type,
"assignee_type": assignee_type,
"auto_delete": auto_delete,
"datacenter": datacenter.id_or_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
if labels is not None:
data["labels"] = labels
@ -209,8 +224,13 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
)
return result
def update(self, primary_ip, auto_delete=None, labels=None, name=None):
# type: (PrimaryIP, Optional[bool], Optional[Dict[str, str]], Optional[str]) -> BoundPrimaryIP
def update(
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.
: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
:return: :class:`BoundPrimaryIP <hcloud.primary_ips.client.BoundPrimaryIP>`
"""
data = {}
data: dict[str, Any] = {}
if auto_delete is not None:
data["auto_delete"] = auto_delete
if labels is not None:
@ -237,8 +257,7 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
)
return BoundPrimaryIP(self, response["primary_ip"])
def delete(self, primary_ip):
# type: (PrimaryIP) -> bool
def delete(self, primary_ip: PrimaryIP | BoundPrimaryIP) -> bool:
"""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>`
@ -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 True
def change_protection(self, primary_ip, delete=None):
# type: (PrimaryIP, Optional[bool]) -> BoundAction
def change_protection(
self,
primary_ip: PrimaryIP | BoundPrimaryIP,
delete: bool | None = None,
) -> BoundAction:
"""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>`
@ -260,21 +282,23 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
If true, prevents the Primary IP from being deleted
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = {}
data: dict[str, Any] = {}
if delete is not None:
data.update({"delete": delete})
response = self._client.request(
url="/primary_ips/{primary_ip_id}/actions/change_protection".format(
primary_ip_id=primary_ip.id
),
url=f"/primary_ips/{primary_ip.id}/actions/change_protection",
method="POST",
json=data,
)
return BoundAction(self._client.actions, response["action"])
def assign(self, primary_ip, assignee_id, assignee_type="server"):
# type: (PrimaryIP, int, str) -> BoundAction
def assign(
self,
primary_ip: PrimaryIP | BoundPrimaryIP,
assignee_id: int,
assignee_type: str = "server",
) -> BoundAction:
"""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>`
@ -285,31 +309,30 @@ class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin):
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
response = self._client.request(
url="/primary_ips/{primary_ip_id}/actions/assign".format(
primary_ip_id=primary_ip.id
),
url=f"/primary_ips/{primary_ip.id}/actions/assign",
method="POST",
json={"assignee_id": assignee_id, "assignee_type": assignee_type},
)
return BoundAction(self._client.actions, response["action"])
def unassign(self, primary_ip):
# type: (PrimaryIP) -> BoundAction
def unassign(self, primary_ip: PrimaryIP | BoundPrimaryIP) -> BoundAction:
"""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>`
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
response = self._client.request(
url="/primary_ips/{primary_ip_id}/actions/unassign".format(
primary_ip_id=primary_ip.id
),
url=f"/primary_ips/{primary_ip.id}/actions/unassign",
method="POST",
)
return BoundAction(self._client.actions, response["action"])
def change_dns_ptr(self, primary_ip, ip, dns_ptr):
# type: (PrimaryIP, str, str) -> BoundAction
def change_dns_ptr(
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.
: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>`
"""
response = self._client.request(
url="/primary_ips/{primary_ip_id}/actions/change_dns_ptr".format(
primary_ip_id=primary_ip.id
),
url=f"/primary_ips/{primary_ip.id}/actions/change_dns_ptr",
method="POST",
json={"ip": ip, "dns_ptr": dns_ptr},
)

View file

@ -1,9 +1,18 @@
from __future__ import annotations
from typing import TYPE_CHECKING
try:
from dateutil.parser import isoparse
except ImportError:
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):
@ -55,19 +64,19 @@ class PrimaryIP(BaseDomain):
def __init__(
self,
id=None,
type=None,
ip=None,
dns_ptr=None,
datacenter=None,
blocked=None,
protection=None,
labels=None,
created=None,
name=None,
assignee_id=None,
assignee_type=None,
auto_delete=None,
id: int | None = None,
type: str | None = None,
ip: str | None = None,
dns_ptr: list[dict] | None = None,
datacenter: BoundDatacenter | None = None,
blocked: bool | None = None,
protection: dict | None = None,
labels: dict[str, dict] | None = None,
created: str | None = None,
name: str | None = None,
assignee_id: int | None = None,
assignee_type: str | None = None,
auto_delete: bool | None = None,
):
self.id = id
self.type = type
@ -97,8 +106,8 @@ class CreatePrimaryIPResponse(BaseDomain):
def __init__(
self,
primary_ip, # type: BoundPrimaryIP
action, # type: BoundAction
primary_ip: BoundPrimaryIP,
action: BoundAction | None,
):
self.primary_ip = primary_ip
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
if TYPE_CHECKING:
from .._client import Client
class BoundServerType(BoundModelBase):
_client: ServerTypesClient
model = ServerType
class ServerTypesClient(ClientEntityBase, GetEntityByNameMixin):
results_list_attribute_name = "server_types"
class ServerTypesPageResult(NamedTuple):
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.
:param id: int
@ -19,8 +32,12 @@ class ServerTypesClient(ClientEntityBase, GetEntityByNameMixin):
response = self._client.request(url=f"/server_types/{id}", method="GET")
return BoundServerType(self, response["server_type"])
def get_list(self, name=None, page=None, per_page=None):
# type: (Optional[str], Optional[int], Optional[int]) -> PageResults[List[BoundServerType], Meta]
def get_list(
self,
name: str | None = None,
page: int | None = None,
per_page: int | None = None,
) -> ServerTypesPageResult:
"""Get a list of Server types
:param name: str (optional)
@ -31,7 +48,7 @@ class ServerTypesClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page
: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:
params["name"] = name
if page is not None:
@ -46,24 +63,22 @@ class ServerTypesClient(ClientEntityBase, GetEntityByNameMixin):
BoundServerType(self, server_type_data)
for server_type_data in response["server_types"]
]
return self._add_meta_to_result(server_types, response)
return ServerTypesPageResult(server_types, Meta.parse_meta(response))
def get_all(self, name=None):
# type: (Optional[str]) -> List[BoundServerType]
def get_all(self, name: str | None = None) -> list[BoundServerType]:
"""Get all Server types
:param name: str (optional)
Can be used to filter server type by their name.
: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):
# type: (str) -> BoundServerType
def get_by_name(self, name: str) -> BoundServerType | None:
"""Get Server type by name
:param name: str
Used to get Server type by name.
: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 ..deprecation.domain import DeprecationInfo
from __future__ import annotations
from ..core import BaseDomain, DomainIdentityMixin
from ..deprecation import DeprecationInfo
class ServerType(BaseDomain, DomainIdentityMixin):
@ -52,19 +54,19 @@ class ServerType(BaseDomain, DomainIdentityMixin):
def __init__(
self,
id=None,
name=None,
description=None,
cores=None,
memory=None,
disk=None,
prices=None,
storage_type=None,
cpu_type=None,
architecture=None,
deprecated=None,
deprecation=None,
included_traffic=None,
id: int | None = None,
name: str | None = None,
description: str | None = None,
cores: int | None = None,
memory: int | None = None,
disk: int | None = None,
prices: dict | None = None,
storage_type: str | None = None,
cpu_type: str | None = None,
architecture: str | None = None,
deprecated: bool | None = None,
deprecation: dict | None = None,
included_traffic: int | None = None,
):
self.id = id
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:
from dateutil.parser import isoparse
except ImportError:
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):
@ -91,27 +109,27 @@ class Server(BaseDomain):
def __init__(
self,
id,
name=None,
status=None,
created=None,
public_net=None,
server_type=None,
datacenter=None,
image=None,
iso=None,
rescue_enabled=None,
locked=None,
backup_window=None,
outgoing_traffic=None,
ingoing_traffic=None,
included_traffic=None,
protection=None,
labels=None,
volumes=None,
private_net=None,
primary_disk_size=None,
placement_group=None,
id: int,
name: str | None = None,
status: str | None = None,
created: str | None = None,
public_net: PublicNetwork | None = None,
server_type: BoundServerType | None = None,
datacenter: BoundDatacenter | None = None,
image: BoundImage | None = None,
iso: BoundIso | None = None,
rescue_enabled: bool | None = None,
locked: bool | None = None,
backup_window: str | None = None,
outgoing_traffic: int | None = None,
ingoing_traffic: int | None = None,
included_traffic: int | None = None,
protection: dict | None = None,
labels: dict[str, str] | None = None,
volumes: list[BoundVolume] | None = None,
private_net: list[PrivateNet] | None = None,
primary_disk_size: int | None = None,
placement_group: BoundPlacementGroup | None = None,
):
self.id = id
self.name = name
@ -153,10 +171,10 @@ class CreateServerResponse(BaseDomain):
def __init__(
self,
server, # type: BoundServer
action, # type: BoundAction
next_actions, # type: List[Action]
root_password, # type: str
server: BoundServer,
action: BoundAction,
next_actions: list[BoundAction],
root_password: str | None,
):
self.server = server
self.action = action
@ -177,8 +195,8 @@ class ResetPasswordResponse(BaseDomain):
def __init__(
self,
action, # type: BoundAction
root_password, # type: str
action: BoundAction,
root_password: str,
):
self.action = action
self.root_password = root_password
@ -197,8 +215,8 @@ class EnableRescueResponse(BaseDomain):
def __init__(
self,
action, # type: BoundAction
root_password, # type: str
action: BoundAction,
root_password: str,
):
self.action = action
self.root_password = root_password
@ -219,9 +237,9 @@ class RequestConsoleResponse(BaseDomain):
def __init__(
self,
action, # type: BoundAction
wss_url, # type: str
password, # type: str
action: BoundAction,
wss_url: str,
password: str,
):
self.action = action
self.wss_url = wss_url
@ -250,12 +268,12 @@ class PublicNetwork(BaseDomain):
def __init__(
self,
ipv4, # type: IPv4Address
ipv6, # type: IPv6Network
floating_ips, # type: List[BoundFloatingIP]
primary_ipv4, # type: BoundPrimaryIP
primary_ipv6, # type: BoundPrimaryIP
firewalls=None, # type: List[PublicNetworkFirewall]
ipv4: IPv4Address,
ipv6: IPv6Network,
floating_ips: list[BoundFloatingIP],
primary_ipv4: BoundPrimaryIP | None,
primary_ipv6: BoundPrimaryIP | None,
firewalls: list[PublicNetworkFirewall] | None = None,
):
self.ipv4 = ipv4
self.ipv6 = ipv6
@ -281,8 +299,8 @@ class PublicNetworkFirewall(BaseDomain):
def __init__(
self,
firewall, # type: BoundFirewall
status, # type: str
firewall: BoundFirewall,
status: str,
):
self.firewall = firewall
self.status = status
@ -303,9 +321,9 @@ class IPv4Address(BaseDomain):
def __init__(
self,
ip, # type: str
blocked, # type: bool
dns_ptr, # type: str
ip: str,
blocked: bool,
dns_ptr: str,
):
self.ip = ip
self.blocked = blocked
@ -331,9 +349,9 @@ class IPv6Network(BaseDomain):
def __init__(
self,
ip, # type: str
blocked, # type: bool
dns_ptr, # type: list
ip: str,
blocked: bool,
dns_ptr: list,
):
self.ip = ip
self.blocked = blocked
@ -360,10 +378,10 @@ class PrivateNet(BaseDomain):
def __init__(
self,
network, # type: BoundNetwork
ip, # type: str
alias_ips, # type: List[str]
mac_address, # type: str
network: BoundNetwork,
ip: str,
alias_ips: list[str],
mac_address: str,
):
self.network = network
self.ip = ip
@ -384,10 +402,10 @@ class ServerCreatePublicNetwork(BaseDomain):
def __init__(
self,
ipv4=None, # type: hcloud.primary_ips.domain.PrimaryIP
ipv6=None, # type: hcloud.primary_ips.domain.PrimaryIP
enable_ipv4=True, # type: bool
enable_ipv6=True, # type: bool
ipv4: PrimaryIP | None = None,
ipv6: PrimaryIP | None = None,
enable_ipv4: bool = True,
enable_ipv6: bool = True,
):
self.ipv4 = ipv4
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
if TYPE_CHECKING:
from .._client import Client
class BoundSSHKey(BoundModelBase):
_client: SSHKeysClient
model = SSHKey
def update(self, name=None, labels=None):
# type: (Optional[str], Optional[Dict[str, str]]) -> BoundSSHKey
def update(
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.
:param description: str (optional)
@ -17,19 +29,22 @@ class BoundSSHKey(BoundModelBase):
"""
return self._client.update(self, name, labels)
def delete(self):
# type: () -> bool
def delete(self) -> bool:
"""Deletes an SSH key. It cannot be used anymore.
:return: boolean
"""
return self._client.delete(self)
class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin):
results_list_attribute_name = "ssh_keys"
class SSHKeysPageResult(NamedTuple):
ssh_keys: list[BoundSSHKey]
meta: Meta | None
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
:param id: int
@ -40,13 +55,12 @@ class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin):
def get_list(
self,
name=None, # type: Optional[str]
fingerprint=None, # type: Optional[str]
label_selector=None, # type: Optional[str]
page=None, # type: Optional[int]
per_page=None, # type: Optional[int]
):
# type: (...) -> PageResults[List[BoundSSHKey], Meta]
name: str | None = None,
fingerprint: str | None = None,
label_selector: str | None = None,
page: int | None = None,
per_page: int | None = None,
) -> SSHKeysPageResult:
"""Get a list of SSH keys from the account
:param name: str (optional)
@ -61,7 +75,7 @@ class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page
: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:
params["name"] = name
if fingerprint is not None:
@ -75,13 +89,17 @@ class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin):
response = self._client.request(url="/ssh_keys", method="GET", params=params)
ass_ssh_keys = [
ssh_keys = [
BoundSSHKey(self, server_data) for server_data in response["ssh_keys"]
]
return self._add_meta_to_result(ass_ssh_keys, response)
return SSHKeysPageResult(ssh_keys, Meta.parse_meta(response))
def get_all(self, name=None, fingerprint=None, label_selector=None):
# type: (Optional[str], Optional[str], Optional[str]) -> List[BoundSSHKey]
def get_all(
self,
name: str | None = None,
fingerprint: str | None = None,
label_selector: str | None = None,
) -> list[BoundSSHKey]:
"""Get all SSH keys from the account
: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.
:return: List[:class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>`]
"""
return super().get_all(
name=name, fingerprint=fingerprint, label_selector=label_selector
return self._iter_pages(
self.get_list,
name=name,
fingerprint=fingerprint,
label_selector=label_selector,
)
def get_by_name(self, name):
# type: (str) -> SSHKeysClient
def get_by_name(self, name: str) -> BoundSSHKey | None:
"""Get ssh key by name
:param name: str
Used to get ssh key by name.
: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):
# type: (str) -> BoundSSHKey
def get_by_fingerprint(self, fingerprint: str) -> BoundSSHKey | None:
"""Get ssh key by fingerprint
:param fingerprint: str
Used to get ssh key by fingerprint.
:return: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>`
"""
response = self.get_list(fingerprint=fingerprint)
sshkeys = response.ssh_keys
return sshkeys[0] if sshkeys else None
return self._get_first_by(fingerprint=fingerprint)
def create(self, name, public_key, labels=None):
# type: (str, str, Optional[Dict[str, str]]) -> BoundSSHKey
def create(
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.
:param name: str
@ -129,14 +150,18 @@ class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin):
User-defined labels (key-value pairs)
: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:
data["labels"] = labels
response = self._client.request(url="/ssh_keys", method="POST", json=data)
return BoundSSHKey(self, response["ssh_key"])
def update(self, ssh_key, name=None, labels=None):
# type: (SSHKey, Optional[str], Optional[Dict[str, str]]) -> BoundSSHKey
def update(
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.
: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)
:return: :class:`BoundSSHKey <hcloud.ssh_keys.client.BoundSSHKey>`
"""
data = {}
data: dict[str, Any] = {}
if name is not None:
data["name"] = name
if labels is not None:
@ -158,13 +183,12 @@ class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin):
)
return BoundSSHKey(self, response["ssh_key"])
def delete(self, ssh_key):
# type: (SSHKey) -> bool
self._client.request(url=f"/ssh_keys/{ssh_key.id}", method="DELETE")
def delete(self, ssh_key: SSHKey | BoundSSHKey) -> bool:
"""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>`
: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 True

View file

@ -1,9 +1,11 @@
from __future__ import annotations
try:
from dateutil.parser import isoparse
except ImportError:
isoparse = None
from ..core.domain import BaseDomain, DomainIdentityMixin
from ..core import BaseDomain, DomainIdentityMixin
class SSHKey(BaseDomain, DomainIdentityMixin):
@ -27,12 +29,12 @@ class SSHKey(BaseDomain, DomainIdentityMixin):
def __init__(
self,
id=None,
name=None,
fingerprint=None,
public_key=None,
labels=None,
created=None,
id: int | None = None,
name: str | None = None,
fingerprint: str | None = None,
public_key: str | None = None,
labels: dict[str, str] | None = None,
created: str | None = None,
):
self.id = id
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 ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin
from ..core.domain import add_meta_to_result
from ..locations.client import BoundLocation
from __future__ import annotations
from typing import TYPE_CHECKING, Any, NamedTuple
from ..actions import ActionsPageResult, BoundAction
from ..core import BoundModelBase, ClientEntityBase, Meta
from ..locations import BoundLocation
from .domain import CreateVolumeResponse, Volume
if TYPE_CHECKING:
from .._client import Client
from ..locations import Location
from ..servers import BoundServer, Server
class BoundVolume(BoundModelBase):
_client: VolumesClient
model = Volume
def __init__(self, client, data, complete=True):
def __init__(self, client: VolumesClient, data: dict, complete: bool = True):
location = data.get("location")
if location is not None:
data["location"] = BoundLocation(client._client.locations, location)
from ..servers.client import BoundServer
from ..servers import BoundServer
server = data.get("server")
if server is not None:
@ -22,8 +32,13 @@ class BoundVolume(BoundModelBase):
)
super().__init__(client, data, complete)
def get_actions_list(self, status=None, sort=None, page=None, per_page=None):
# type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]]
def get_actions_list(
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.
:param status: List[str] (optional)
@ -38,8 +53,11 @@ class BoundVolume(BoundModelBase):
"""
return self._client.get_actions_list(self, status, sort, page, per_page)
def get_actions(self, status=None, sort=None):
# type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction]
def get_actions(
self,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a volume.
:param status: List[str] (optional)
@ -50,8 +68,11 @@ class BoundVolume(BoundModelBase):
"""
return self._client.get_actions(self, status, sort)
def update(self, name=None, labels=None):
# type: (Optional[str], Optional[Dict[str, str]]) -> BoundAction
def update(
self,
name: str | None = None,
labels: dict[str, str] | None = None,
) -> BoundVolume:
"""Updates the volume properties.
:param name: str (optional)
@ -62,16 +83,18 @@ class BoundVolume(BoundModelBase):
"""
return self._client.update(self, name, labels)
def delete(self):
# type: () -> BoundAction
def delete(self) -> bool:
"""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 self._client.delete(self)
def attach(self, server, automount=None):
# type: (Union[Server, BoundServer], Optional[bool]) -> BoundAction
def attach(
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.
: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)
def detach(self):
# type: () -> BoundAction
def detach(self) -> BoundAction:
"""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 self._client.detach(self)
def resize(self, size):
# type: (int) -> BoundAction
def resize(self, size: int) -> BoundAction:
"""Changes the size of a volume. Note that downsizing a volume is not possible.
:param size: int
@ -98,8 +119,7 @@ class BoundVolume(BoundModelBase):
"""
return self._client.resize(self, size)
def change_protection(self, delete=None):
# type: (Optional[bool]) -> BoundAction
def change_protection(self, delete: bool | None = None) -> BoundAction:
"""Changes the protection configuration of a volume.
:param delete: boolean
@ -109,11 +129,15 @@ class BoundVolume(BoundModelBase):
return self._client.change_protection(self, delete)
class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
results_list_attribute_name = "volumes"
class VolumesPageResult(NamedTuple):
volumes: list[BoundVolume]
meta: Meta | None
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
:param id: int
@ -123,9 +147,13 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
return BoundVolume(self, response["volume"])
def get_list(
self, name=None, label_selector=None, page=None, per_page=None, status=None
):
# type: (Optional[str], Optional[str], Optional[int], Optional[int], Optional[List[str]]) -> PageResults[List[BoundVolume], Meta]
self,
name: str | None = None,
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
:param name: str (optional)
@ -140,7 +168,7 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
Specifies how many results are returned by page
:return: (List[:class:`BoundVolume <hcloud.volumes.client.BoundVolume>`], :class:`Meta <hcloud.core.domain.Meta>`)
"""
params = {}
params: dict[str, Any] = {}
if name is not None:
params["name"] = name
if label_selector is not None:
@ -156,10 +184,13 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
volumes = [
BoundVolume(self, volume_data) for volume_data in response["volumes"]
]
return self._add_meta_to_result(volumes, response)
return VolumesPageResult(volumes, Meta.parse_meta(response))
def get_all(self, label_selector=None, status=None):
# type: (Optional[str], Optional[List[str]]) -> List[BoundVolume]
def get_all(
self,
label_selector: str | None = None,
status: list[str] | None = None,
) -> list[BoundVolume]:
"""Get all volumes from this account
: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.
: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):
# type: (str) -> BoundVolume
def get_by_name(self, name: str) -> BoundVolume | None:
"""Get volume by name
:param name: str
Used to get volume by name.
:return: :class:`BoundVolume <hcloud.volumes.client.BoundVolume>`
"""
return super().get_by_name(name)
return self._get_first_by(name=name)
def create(
self,
size, # type: int
name, # type: str
labels=None, # type: Optional[str]
location=None, # type: Optional[Location]
server=None, # type: Optional[Server],
automount=None, # type: Optional[bool],
format=None, # type: Optional[str],
):
# type: (...) -> CreateVolumeResponse
size: int,
name: str,
labels: str | None = None,
location: Location | None = None,
server: Server | None = None,
automount: bool | None = None,
format: str | None = None,
) -> CreateVolumeResponse:
"""Creates a new volume attached to a server.
:param size: int
@ -214,7 +247,7 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
if not (bool(location) ^ bool(server)):
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:
data["labels"] = labels
if location is not None:
@ -240,9 +273,13 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
return result
def get_actions_list(
self, volume, status=None, sort=None, page=None, per_page=None
):
# type: (Volume, Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta]
self,
volume: Volume | BoundVolume,
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.
: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
:return: (List[:class:`BoundAction <hcloud.actions.client.BoundAction>`], :class:`Meta <hcloud.core.domain.Meta>`)
"""
params = {}
params: dict[str, Any] = {}
if status is not None:
params["status"] = status
if sort is not None:
@ -275,10 +312,14 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
BoundAction(self._client.actions, action_data)
for action_data in response["actions"]
]
return add_meta_to_result(actions, response, "actions")
return ActionsPageResult(actions, Meta.parse_meta(response))
def get_actions(self, volume, status=None, sort=None):
# type: (Union[Volume, BoundVolume], Optional[List[str]], Optional[List[str]]) -> List[BoundAction]
def get_actions(
self,
volume: Volume | BoundVolume,
status: list[str] | None = None,
sort: list[str] | None = None,
) -> list[BoundAction]:
"""Returns all action objects for a 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`
: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):
# type:(Union[Volume, BoundVolume], Optional[str], Optional[Dict[str, str]]) -> BoundVolume
def update(
self,
volume: Volume | BoundVolume,
name: str | None = None,
labels: dict[str, str] | None = None,
) -> BoundVolume:
"""Updates the volume properties.
: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)
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = {}
data: dict[str, Any] = {}
if name is not None:
data.update({"name": name})
if labels is not None:
@ -313,8 +363,7 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
)
return BoundVolume(self, response["volume"])
def delete(self, volume):
# type: (Union[Volume, BoundVolume]) -> BoundAction
def delete(self, volume: Volume | BoundVolume) -> bool:
"""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>`
@ -323,8 +372,7 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
self._client.request(url=f"/volumes/{volume.id}", method="DELETE")
return True
def resize(self, volume, size):
# type: (Union[Volume, BoundVolume], int) -> BoundAction
def resize(self, volume: Volume | BoundVolume, size: int) -> BoundAction:
"""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>`
@ -339,8 +387,12 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
)
return BoundAction(self._client.actions, data["action"])
def attach(self, volume, server, automount=None):
# type: (Union[Volume, BoundVolume], Union[Server, BoundServer], Optional[bool]) -> BoundAction
def attach(
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.
: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
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = {"server": server.id}
data: dict[str, Any] = {"server": server.id}
if automount is not None:
data["automount"] = automount
@ -359,8 +411,7 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
)
return BoundAction(self._client.actions, data["action"])
def detach(self, volume):
# type: (Union[Volume, BoundVolume]) -> BoundAction
def detach(self, volume: Volume | BoundVolume) -> BoundAction:
"""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>`
@ -372,8 +423,11 @@ class VolumesClient(ClientEntityBase, GetEntityByNameMixin):
)
return BoundAction(self._client.actions, data["action"])
def change_protection(self, volume, delete=None):
# type: (Union[Volume, BoundVolume], Optional[bool], Optional[bool]) -> BoundAction
def change_protection(
self,
volume: Volume | BoundVolume,
delete: bool | None = None,
) -> BoundAction:
"""Changes the protection configuration of a 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
:return: :class:`BoundAction <hcloud.actions.client.BoundAction>`
"""
data = {}
data: dict[str, Any] = {}
if delete is not None:
data.update({"delete": delete})
response = self._client.request(
url="/volumes/{volume_id}/actions/change_protection".format(
volume_id=volume.id
),
url=f"/volumes/{volume.id}/actions/change_protection",
method="POST",
json=data,
)

View file

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

View file

@ -19,7 +19,7 @@ from textwrap import dedent
logger = logging.getLogger("vendor")
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"