Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/api.clients.storage_box_types.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
StorageBoxTypesClient
=====================

.. autoclass:: hcloud.storage_box_types.client.StorageBoxTypesClient
:members:

.. autoclass:: hcloud.storage_box_types.client.BoundStorageBoxType
:members:

.. autoclass:: hcloud.storage_box_types.client.StorageBoxType
:members:
29 changes: 29 additions & 0 deletions docs/api.clients.storage_boxes.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
StorageBoxesClient
=====================

.. autoclass:: hcloud.storage_boxes.client.StorageBoxesClient
:members:

.. autoclass:: hcloud.storage_boxes.client.BoundStorageBox
:members:

.. autoclass:: hcloud.storage_boxes.client.StorageBox
:members:

.. autoclass:: hcloud.storage_boxes.client.StorageBoxSnapshotPlan
:members:

.. autoclass:: hcloud.storage_boxes.client.StorageBoxStats
:members:

.. autoclass:: hcloud.storage_boxes.client.StorageBoxAccessSettings
:members:

.. autoclass:: hcloud.storage_boxes.client.CreateStorageBoxResponse
:members:

.. autoclass:: hcloud.storage_boxes.client.DeleteStorageBoxResponse
:members:

.. autoclass:: hcloud.storage_boxes.client.StorageBoxFoldersResponse
:members:
161 changes: 113 additions & 48 deletions hcloud/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
from .server_types import ServerTypesClient
from .servers import ServersClient
from .ssh_keys import SSHKeysClient
from .storage_box_types import StorageBoxTypesClient
from .storage_boxes import StorageBoxesClient
from .volumes import VolumesClient


Expand Down Expand Up @@ -78,6 +80,25 @@ def func(retries: int) -> float:
return func


def _build_user_agent(
application_name: str | None,
application_version: str | None,
) -> str:
"""Build the user agent of the hcloud-python instance with the user application name (if specified)

:return: The user agent of this hcloud-python instance
"""
parts = []
for name, version in [
(application_name, application_version),
("hcloud-python", __version__),
]:
if name is not None:
parts.append(name if version is None else f"{name}/{version}")

return " ".join(parts)


class Client:
"""
Client for the Hetzner Cloud API.
Expand Down Expand Up @@ -112,14 +133,6 @@ class Client:
breaking changes.
"""

_version = __version__
__user_agent_prefix = "hcloud-python"

_retry_interval = staticmethod(
exponential_backoff_function(base=1.0, multiplier=2, cap=60.0, jitter=True)
)
_retry_max_retries = 5

def __init__(
self,
token: str,
Expand All @@ -129,11 +142,14 @@ def __init__(
poll_interval: int | float | BackoffFunction = 1.0,
poll_max_retries: int = 120,
timeout: float | tuple[float, float] | None = None,
*,
api_endpoint_hetzner: str = "https://api.hetzner.com/v1",
):
"""Create a new Client instance

:param token: Hetzner Cloud API token
:param api_endpoint: Hetzner Cloud API endpoint
:param api_endpoint_hetzner: Hetzner API endpoint.
:param application_name: Your application name
:param application_version: Your application _version
:param poll_interval:
Expand All @@ -143,18 +159,24 @@ def __init__(
Max retries before timeout when polling actions from the API.
: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

if isinstance(poll_interval, (int, float)):
self._poll_interval_func = constant_backoff_function(poll_interval)
else:
self._poll_interval_func = poll_interval
self._poll_max_retries = poll_max_retries
self._client = ClientBase(
token=token,
endpoint=api_endpoint,
application_name=application_name,
application_version=application_version,
poll_interval=poll_interval,
poll_max_retries=poll_max_retries,
timeout=timeout,
)
self._client_hetzner = ClientBase(
token=token,
endpoint=api_endpoint_hetzner,
application_name=application_name,
application_version=application_version,
poll_interval=poll_interval,
poll_max_retries=poll_max_retries,
timeout=timeout,
)

self.datacenters = DatacentersClient(self)
"""DatacentersClient Instance
Expand Down Expand Up @@ -246,50 +268,93 @@ def __init__(
:type: :class:`PlacementGroupsClient <hcloud.placement_groups.client.PlacementGroupsClient>`
"""

def _get_user_agent(self) -> str:
"""Get the user agent of the hcloud-python instance with the user application name (if specified)
self.storage_box_types = StorageBoxTypesClient(self)
"""StorageBoxTypesClient Instance

:type: :class:`StorageBoxTypesClient <hcloud.storage_box_types.client.StorageBoxTypesClient>`
"""

self.storage_boxes = StorageBoxesClient(self)
"""StorageBoxesClient Instance

:type: :class:`StorageBoxesClient <hcloud.storage_boxes.client.StorageBoxesClient>`
"""

def request( # type: ignore[no-untyped-def]
self,
method: str,
url: str,
**kwargs,
) -> dict:
"""Perform a request to the Hetzner Cloud API.

:return: The user agent of this hcloud-python instance
:param method: Method to perform the request.
:param url: URL to perform the request.
:param timeout: Requests timeout in seconds.
"""
user_agents = []
for name, version in [
(self._application_name, self._application_version),
(self.__user_agent_prefix, self._version),
]:
if name is not None:
user_agents.append(name if version is None else f"{name}/{version}")

return " ".join(user_agents)

def _get_headers(self) -> dict:
headers = {
"User-Agent": self._get_user_agent(),
"Authorization": f"Bearer {self.token}",
return self._client.request(method, url, **kwargs)


class ClientBase:
def __init__(
self,
token: str,
*,
endpoint: str,
application_name: str | None = None,
application_version: str | None = None,
poll_interval: int | float | BackoffFunction = 1.0,
poll_max_retries: int = 120,
timeout: float | tuple[float, float] | None = None,
):
self._token = token
self._endpoint = endpoint

self._user_agent = _build_user_agent(application_name, application_version)
self._headers = {
"User-Agent": self._user_agent,
"Authorization": f"Bearer {self._token}",
"Accept": "application/json",
}
return headers

if isinstance(poll_interval, (int, float)):
poll_interval_func = constant_backoff_function(poll_interval)
else:
poll_interval_func = poll_interval

self._poll_interval_func = poll_interval_func
self._poll_max_retries = poll_max_retries

self._retry_interval_func = exponential_backoff_function(
base=1.0, multiplier=2, cap=60.0, jitter=True
)
self._retry_max_retries = 5

self._timeout = timeout
self._session = requests.Session()

def request( # type: ignore[no-untyped-def]
self,
method: str,
url: str,
**kwargs,
) -> dict:
"""Perform a request to the Hetzner Cloud API, wrapper around requests.request
"""Perform a request to the provided URL.

:param method: HTTP Method to perform the Request
:param url: URL of the Endpoint
:param timeout: Requests timeout in seconds
:param method: Method to perform the request.
:param url: URL to perform the request.
:param timeout: Requests timeout in seconds.
:return: Response
"""
kwargs.setdefault("timeout", self._requests_timeout)
kwargs.setdefault("timeout", self._timeout)

url = self._api_endpoint + url
headers = self._get_headers()
url = self._endpoint + url
headers = self._headers

retries = 0
while True:
try:
response = self._requests_session.request(
response = self._session.request(
method=method,
url=url,
headers=headers,
Expand All @@ -298,13 +363,13 @@ def request( # type: ignore[no-untyped-def]
return self._read_response(response)
except APIException as exception:
if retries < self._retry_max_retries and self._retry_policy(exception):
time.sleep(self._retry_interval(retries))
time.sleep(self._retry_interval_func(retries))
retries += 1
continue
raise
except requests.exceptions.Timeout:
if retries < self._retry_max_retries:
time.sleep(self._retry_interval(retries))
time.sleep(self._retry_interval_func(retries))
retries += 1
continue
raise
Expand Down
16 changes: 12 additions & 4 deletions hcloud/actions/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,16 @@ class ActionsPageResult(NamedTuple):
class ResourceActionsClient(ResourceClientBase):
_resource: str

def __init__(self, client: Client, resource: str | None):
super().__init__(client)
def __init__(self, client: ResourceClientBase | Client, resource: str | None):
if isinstance(client, ResourceClientBase):
super().__init__(client._parent)
# Use the same base client as the the resource base client. Allows us to
# choose the base client outside of the ResourceActionsClient.
self._client = client._client
else:
# Backward compatibility, defaults to the parent ("top level") base client (`_client`).
super().__init__(client)

self._resource = resource or ""

def get_by_id(self, id: int) -> BoundAction:
Expand All @@ -67,7 +75,7 @@ def get_by_id(self, id: int) -> BoundAction:
url=f"{self._resource}/actions/{id}",
method="GET",
)
return BoundAction(self._client.actions, response["action"])
return BoundAction(self._parent.actions, response["action"])

def get_list(
self,
Expand Down Expand Up @@ -104,7 +112,7 @@ def get_list(
params=params,
)
actions = [
BoundAction(self._client.actions, action_data)
BoundAction(self._parent.actions, action_data)
for action_data in response["actions"]
]
return ActionsPageResult(actions, Meta.parse_meta(response))
Expand Down
7 changes: 3 additions & 4 deletions hcloud/certificates/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ class CertificatesPageResult(NamedTuple):


class CertificatesClient(ResourceClientBase):
_client: Client

actions: ResourceActionsClient
"""Certificates scoped actions client
Expand Down Expand Up @@ -248,7 +247,7 @@ def create_managed(
response = self._client.request(url="/certificates", method="POST", json=data)
return CreateManagedCertificateResponse(
certificate=BoundCertificate(self, response["certificate"]),
action=BoundAction(self._client.actions, response["action"]),
action=BoundAction(self._parent.actions, response["action"]),
)

def update(
Expand Down Expand Up @@ -328,7 +327,7 @@ def get_actions_list(
params=params,
)
actions = [
BoundAction(self._client.actions, action_data)
BoundAction(self._parent.actions, action_data)
for action_data in response["actions"]
]
return ActionsPageResult(actions, Meta.parse_meta(response))
Expand Down Expand Up @@ -368,4 +367,4 @@ def retry_issuance(
url=f"/certificates/{certificate.id}/actions/retry",
method="POST",
)
return BoundAction(self._client.actions, response["action"])
return BoundAction(self._parent.actions, response["action"])
13 changes: 6 additions & 7 deletions hcloud/core/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,20 @@
from typing import TYPE_CHECKING, Any, Callable

if TYPE_CHECKING:
from .._client import Client
from .._client import Client, ClientBase
from .domain import BaseDomain


class ResourceClientBase:
_client: Client
_parent: Client
_client: ClientBase

max_per_page: int = 50

def __init__(self, client: Client):
"""
:param client: Client
:return self
"""
self._client = client
self._parent = client
# Use the parent "default" base client.
self._client = client._client

def _iter_pages( # type: ignore[no-untyped-def]
self,
Expand Down
Loading