From 08f3a40670c8b45248b33dcd7a40553e88223420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Wed, 29 Jun 2022 08:55:08 +0200 Subject: [PATCH 001/406] Add primary IP support (#160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lukas Kämmerling Co-authored-by: Vanderson Mota dos Santos --- .github/workflows/unit_test.yml | 2 +- docs/api.clients.primary_ips.rst | 13 + docs/api.clients.servers.rst | 3 + docs/conf.py | 22 +- hcloud/certificates/domain.py | 5 +- hcloud/core/client.py | 1 + hcloud/core/domain.py | 5 +- hcloud/datacenters/domain.py | 8 +- hcloud/firewalls/domain.py | 4 +- hcloud/hcloud.py | 6 + hcloud/isos/domain.py | 7 +- hcloud/load_balancers/client.py | 16 +- hcloud/load_balancers/domain.py | 20 +- hcloud/networks/client.py | 23 +- hcloud/primary_ips/__init__.py | 1 + hcloud/primary_ips/client.py | 335 +++++++++++++++++++++++++ hcloud/primary_ips/domain.py | 102 ++++++++ hcloud/servers/client.py | 59 ++++- hcloud/servers/domain.py | 39 ++- hcloud/volumes/client.py | 5 +- tests/unit/firewalls/conftest.py | 2 +- tests/unit/floating_ips/conftest.py | 2 +- tests/unit/floating_ips/test_client.py | 4 +- tests/unit/images/conftest.py | 2 +- tests/unit/networks/test_client.py | 19 +- tests/unit/primary_ips/__init__.py | 0 tests/unit/primary_ips/conftest.py | 253 +++++++++++++++++++ tests/unit/primary_ips/test_client.py | 296 ++++++++++++++++++++++ tests/unit/primary_ips/test_domain.py | 12 + tests/unit/servers/conftest.py | 16 +- tox.ini | 2 +- 31 files changed, 1159 insertions(+), 125 deletions(-) create mode 100644 docs/api.clients.primary_ips.rst create mode 100644 hcloud/primary_ips/__init__.py create mode 100644 hcloud/primary_ips/client.py create mode 100644 hcloud/primary_ips/domain.py create mode 100644 tests/unit/primary_ips/__init__.py create mode 100644 tests/unit/primary_ips/conftest.py create mode 100644 tests/unit/primary_ips/test_client.py create mode 100644 tests/unit/primary_ips/test_domain.py diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index ba7c79f1..29871179 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ 3.6, 3.7, 3.8, 3.9, "3.10.0-beta.1" ] + python-version: [ 3.6, 3.7, 3.8, 3.9, "3.10" ] name: Python ${{ matrix.python-version }} steps: - uses: actions/checkout@v2 diff --git a/docs/api.clients.primary_ips.rst b/docs/api.clients.primary_ips.rst new file mode 100644 index 00000000..237e2237 --- /dev/null +++ b/docs/api.clients.primary_ips.rst @@ -0,0 +1,13 @@ +PrimaryIPsClient +================== + + +.. autoclass:: hcloud.primary_ips.client.PrimaryIPsClient + :members: + +.. autoclass:: hcloud.primary_ips.client.BoundPrimaryIP + :members: + +.. autoclass:: hcloud.primary_ips.domain.PrimaryIP + :members: + diff --git a/docs/api.clients.servers.rst b/docs/api.clients.servers.rst index 6f6c2869..58362561 100644 --- a/docs/api.clients.servers.rst +++ b/docs/api.clients.servers.rst @@ -23,6 +23,9 @@ ServersClient .. autoclass:: hcloud.servers.domain.CreateServerResponse :members: +.. autoclass:: hcloud.servers.domain.ServerCreatePublicNetwork + :members: + .. autoclass:: hcloud.servers.domain.ResetPasswordResponse :members: diff --git a/docs/conf.py b/docs/conf.py index 6ec4f486..dd5b85e7 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -47,9 +47,9 @@ master_doc = "index" # General information about the project. -project = u"Hetzner Cloud Python" -copyright = u"2019, Hetzner Cloud GmbH" -author = u"Hetzner Cloud GmbH" +project = "Hetzner Cloud Python" +copyright = "2019, Hetzner Cloud GmbH" +author = "Hetzner Cloud GmbH" # The version info for the project you're documenting, acts as replacement # for |version| and |release|, also used in various other places throughout @@ -93,9 +93,7 @@ # theme further. For a list of options available for each theme, see the # documentation. # -html_theme_options = { - "logo_only": True, -} +html_theme_options = {"logo_only": True} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -131,17 +129,17 @@ ( master_doc, "hcloud.tex", - u"Hetzner Cloud Python Documentation", - u"Hetzner Cloud GmbH", + "Hetzner Cloud Python Documentation", + "Hetzner Cloud GmbH", "manual", - ), + ) ] # -- Options for manual page output ------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [(master_doc, u"Hetzner Cloud Python Documentation", [author], 1)] +man_pages = [(master_doc, "Hetzner Cloud Python Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------- @@ -151,11 +149,11 @@ texinfo_documents = [ ( master_doc, - u"Hetzner Cloud Python Documentation", + "Hetzner Cloud Python Documentation", author, "HCloud-python is a library for the Hetzner Cloud API.", "Miscellaneous", - ), + ) ] source_suffix = [".rst"] diff --git a/hcloud/certificates/domain.py b/hcloud/certificates/domain.py index bde10fea..448e4b41 100644 --- a/hcloud/certificates/domain.py +++ b/hcloud/certificates/domain.py @@ -107,10 +107,7 @@ class CreateManagedCertificateResponse(BaseDomain): Shows the progress of the certificate creation """ - __slots__ = ( - "certificate", - "action", - ) + __slots__ = ("certificate", "action") def __init__( self, diff --git a/hcloud/core/client.py b/hcloud/core/client.py index f024c9be..f9fa5408 100644 --- a/hcloud/core/client.py +++ b/hcloud/core/client.py @@ -38,6 +38,7 @@ def _get_all( **kwargs ): # type (...) -> List[BoundModelBase] + page = 1 results = [] diff --git a/hcloud/core/domain.py b/hcloud/core/domain.py index 1703ae2f..e08c78b1 100644 --- a/hcloud/core/domain.py +++ b/hcloud/core/domain.py @@ -55,10 +55,7 @@ class Meta(BaseDomain): __slots__ = ("pagination",) - def __init__( - self, - pagination=None, - ): + def __init__(self, pagination=None): self.pagination = pagination @classmethod diff --git a/hcloud/datacenters/domain.py b/hcloud/datacenters/domain.py index 1335adca..e19a2b26 100644 --- a/hcloud/datacenters/domain.py +++ b/hcloud/datacenters/domain.py @@ -12,13 +12,7 @@ class Datacenter(BaseDomain, DomainIdentityMixin): :param server_types: :class:`DatacenterServerTypes ` """ - __slots__ = ( - "id", - "name", - "description", - "location", - "server_types", - ) + __slots__ = ("id", "name", "description", "location", "server_types") def __init__( self, id=None, name=None, description=None, location=None, server_types=None diff --git a/hcloud/firewalls/domain.py b/hcloud/firewalls/domain.py index bcdcf07a..ee26b471 100644 --- a/hcloud/firewalls/domain.py +++ b/hcloud/firewalls/domain.py @@ -137,9 +137,7 @@ def __init__( self.label_selector = label_selector def to_payload(self): - payload = { - "type": self.type, - } + payload = {"type": self.type} if self.server is not None: payload.update({"server": {"id": self.server.id}}) diff --git a/hcloud/hcloud.py b/hcloud/hcloud.py index c8aea488..c172b704 100644 --- a/hcloud/hcloud.py +++ b/hcloud/hcloud.py @@ -7,6 +7,7 @@ from hcloud.actions.client import ActionsClient from hcloud.certificates.client import CertificatesClient from hcloud.floating_ips.client import FloatingIPsClient +from hcloud.primary_ips.client import PrimaryIPsClient from hcloud.networks.client import NetworksClient from hcloud.isos.client import IsosClient from hcloud.servers.client import ServersClient @@ -121,6 +122,11 @@ def __init__( :type: :class:`FloatingIPsClient ` """ + self.primary_ips = PrimaryIPsClient(self) + """PrimaryIPsClient Instance + + :type: :class:`PrimaryIPsClient ` + """ self.networks = NetworksClient(self) """NetworksClient Instance diff --git a/hcloud/isos/domain.py b/hcloud/isos/domain.py index aea06b32..2551b0a5 100644 --- a/hcloud/isos/domain.py +++ b/hcloud/isos/domain.py @@ -22,12 +22,7 @@ class Iso(BaseDomain, DomainIdentityMixin): __slots__ = ("id", "name", "type", "description", "deprecated") def __init__( - self, - id=None, - name=None, - type=None, - description=None, - deprecated=None, + self, id=None, name=None, type=None, description=None, deprecated=None ): self.id = id self.name = name diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index d23acf94..4caba88b 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -684,9 +684,7 @@ def delete_service(self, load_balancer, service): The LoadBalancerService you want to delete from the Load Balancer :return: :class:`BoundAction ` """ - data = { - "listen_port": service.listen_port, - } + data = {"listen_port": service.listen_port} response = self._client.request( url="/load_balancers/{load_balancer_id}/actions/delete_service".format( @@ -732,9 +730,7 @@ def remove_target(self, load_balancer, target): The LoadBalancerTarget you want to remove from the Load Balancer :return: :class:`BoundAction ` """ - data = { - "type": target.type, - } + data = {"type": target.type} if target.type == "server": data["server"] = {"id": target.server.id} elif target.type == "label_selector": @@ -848,9 +844,7 @@ def detach_from_network(self, load_balancer, network): :param network: :class:`BoundNetwork ` or :class:`Network ` :return: :class:`BoundAction ` """ - data = { - "network": network.id, - } + data = {"network": network.id} response = self._client.request( url="/load_balancers/{load_balancer_id}/actions/detach_from_network".format( load_balancer_id=load_balancer.id @@ -903,9 +897,7 @@ def change_type(self, load_balancer, load_balancer_type): Load Balancer type the Load Balancer should migrate to :return: :class:`BoundAction ` """ - data = { - "load_balancer_type": load_balancer_type.id_or_name, - } + data = {"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 diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index 04302924..11db2e7a 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -299,10 +299,7 @@ class IPv4Address(BaseDomain): The IPv4 Address """ - __slots__ = ( - "ip", - "dns_ptr", - ) + __slots__ = ("ip", "dns_ptr") def __init__( self, @@ -320,10 +317,7 @@ class IPv6Network(BaseDomain): The IPv6 Network as CIDR Notation """ - __slots__ = ( - "ip", - "dns_ptr", - ) + __slots__ = ("ip", "dns_ptr") def __init__( self, @@ -343,10 +337,7 @@ class PrivateNet(BaseDomain): The main IP Address of the LoadBalancer in the Network """ - __slots__ = ( - "network", - "ip", - ) + __slots__ = ("network", "ip") def __init__( self, @@ -366,10 +357,7 @@ class CreateLoadBalancerResponse(BaseDomain): Shows the progress of the Load Balancer creation """ - __slots__ = ( - "load_balancer", - "action", - ) + __slots__ = ("load_balancer", "action") def __init__( self, diff --git a/hcloud/networks/client.py b/hcloud/networks/client.py index 5adf41b2..60b85bd4 100644 --- a/hcloud/networks/client.py +++ b/hcloud/networks/client.py @@ -358,10 +358,7 @@ def add_subnet(self, network, subnet): The NetworkSubnet you want to add to the Network :return: :class:`BoundAction ` """ - data = { - "type": subnet.type, - "network_zone": subnet.network_zone, - } + data = {"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: @@ -385,9 +382,7 @@ def delete_subnet(self, network, subnet): The NetworkSubnet you want to remove from the Network :return: :class:`BoundAction ` """ - data = { - "ip_range": subnet.ip_range, - } + data = {"ip_range": subnet.ip_range} response = self._client.request( url="/networks/{network_id}/actions/delete_subnet".format( @@ -407,10 +402,7 @@ def add_route(self, network, route): The NetworkRoute you want to add to the Network :return: :class:`BoundAction ` """ - data = { - "destination": route.destination, - "gateway": route.gateway, - } + data = {"destination": route.destination, "gateway": route.gateway} response = self._client.request( url="/networks/{network_id}/actions/add_route".format( @@ -430,10 +422,7 @@ def delete_route(self, network, route): The NetworkRoute you want to remove from the Network :return: :class:`BoundAction ` """ - data = { - "destination": route.destination, - "gateway": route.gateway, - } + data = {"destination": route.destination, "gateway": route.gateway} response = self._client.request( url="/networks/{network_id}/actions/delete_route".format( @@ -453,9 +442,7 @@ def change_ip_range(self, network, ip_range): The new prefix for the whole network. :return: :class:`BoundAction ` """ - data = { - "ip_range": ip_range, - } + data = {"ip_range": ip_range} response = self._client.request( url="/networks/{network_id}/actions/change_ip_range".format( diff --git a/hcloud/primary_ips/__init__.py b/hcloud/primary_ips/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/hcloud/primary_ips/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/hcloud/primary_ips/client.py b/hcloud/primary_ips/client.py new file mode 100644 index 00000000..7d8cb496 --- /dev/null +++ b/hcloud/primary_ips/client.py @@ -0,0 +1,335 @@ +# -*- coding: utf-8 -*- +from hcloud.actions.client import BoundAction +from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin + +from hcloud.primary_ips.domain import PrimaryIP, CreatePrimaryIPResponse + + +class BoundPrimaryIP(BoundModelBase): + model = PrimaryIP + + def __init__(self, client, data, complete=True): + from hcloud.datacenters.client import BoundDatacenter + + datacenter = data.get("datacenter", {}) + if datacenter: + data["datacenter"] = BoundDatacenter(client._client.datacenters, datacenter) + + super(BoundPrimaryIP, self).__init__(client, data, complete) + + def update(self, auto_delete=None, labels=None, name=None): + # type: (Optional[bool], Optional[Dict[str, str]], Optional[str]) -> BoundPrimaryIP + """Updates the description or labels of a Primary IP. + + :param auto_delete: bool (optional) + Auto delete IP when assignee gets deleted + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str (optional) + New Name to set + :return: :class:`BoundPrimaryIP ` + """ + return self._client.update( + self, auto_delete=auto_delete, labels=labels, name=name + ) + + def delete(self): + # type: () -> 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 + """Changes the protection configuration of the Primary IP. + + :param delete: boolean + If true, prevents the Primary IP from being deleted + :return: :class:`BoundAction ` + """ + return self._client.change_protection(self, delete) + + def assign(self, assignee_id, assignee_type): + # type: (int,str) -> BoundAction + """Assigns a Primary IP to a assignee. + + :param assignee_id: int` + Id of an assignee the Primary IP shall be assigned to + :param assignee_type: string` + Assignee type (e.g server) the Primary IP shall be assigned to + :return: :class:`BoundAction ` + """ + return self._client.assign(self, assignee_id, assignee_type) + + def unassign(self): + # type: () -> 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 ` + """ + return self._client.unassign(self) + + def change_dns_ptr(self, ip, dns_ptr): + # type: (str, str) -> BoundAction + """Changes the hostname that will appear when getting the hostname belonging to this Primary IP. + + :param ip: str + The IP address for which to set the reverse DNS entry + :param dns_ptr: str + Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` + :return: :class:`BoundAction ` + """ + return self._client.change_dns_ptr(self, ip, dns_ptr) + + +class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin): + results_list_attribute_name = "primary_ips" + + def get_by_id(self, id): + # type: (int) -> BoundPrimaryIP + """Returns a specific Primary IP object. + + :param id: int + :return: :class:`BoundPrimaryIP ` + """ + response = self._client.request( + url="/primary_ips/{primary_ip_id}".format(primary_ip_id=id), method="GET" + ) + return BoundPrimaryIP(self, response["primary_ip"]) + + 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]] + """Get a list of primary ips from this account + + :param label_selector: str (optional) + Can be used to filter Primary IPs by labels. The response will only contain Primary IPs matching the label selectorable values. + :param page: int (optional) + Specifies the page to fetch + :param per_page: int (optional) + Specifies how many results are returned by page + :param name: str (optional) + Can be used to filter networks by their name. + :param ip: str (optional) + Can be used to filter resources by their ip. The response will only contain the resources matching the specified ip. + :return: (List[:class:`BoundPrimaryIP `], :class:`Meta `) + """ + params = {} + + if label_selector is not None: + params["label_selector"] = label_selector + if page is not None: + params["page"] = page + if per_page is not None: + params["per_page"] = per_page + if name is not None: + params["name"] = name + if ip is not None: + params["ip"] = ip + + response = self._client.request(url="/primary_ips", method="GET", params=params) + primary_ips = [ + BoundPrimaryIP(self, primary_ip_data) + for primary_ip_data in response["primary_ips"] + ] + + return self._add_meta_to_result(primary_ips, response) + + def get_all(self, label_selector=None, name=None): + # type: (Optional[str], Optional[str]) -> List[BoundPrimaryIP] + """Get all primary ips from this account + + :param label_selector: str (optional) + Can be used to filter Primary IPs by labels. The response will only contain Primary IPs matching the label selector.able values. + :param name: str (optional) + Can be used to filter networks by their name. + :return: List[:class:`BoundPrimaryIP `] + """ + return super(PrimaryIPsClient, self).get_all( + label_selector=label_selector, name=name + ) + + def get_by_name(self, name): + # type: (str) -> BoundPrimaryIP + """Get Primary IP by name + + :param name: str + Used to get Primary IP by name. + :return: :class:`BoundPrimaryIP ` + """ + return super(PrimaryIPsClient, self).get_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 + """Creates a new Primary IP assigned to a server. + + :param type: str + Primary IP type Choices: ipv4, ipv6 + :param assignee_type: str + :param assignee_id: int (optional) + :param datacenter: Datacenter + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str + :param auto_delete: bool (optional) + :return: :class:`CreatePrimaryIPResponse ` + """ + + data = { + "type": type, + "assignee_type": assignee_type, + "auto_delete": auto_delete, + "datacenter": datacenter.id_or_name, + "name": name, + } + if assignee_id: + data["assignee_id"] = assignee_id + if labels is not None: + data["labels"] = labels + + response = self._client.request(url="/primary_ips", json=data, method="POST") + + action = None + if response.get("action") is not None: + action = BoundAction(self._client.actions, response["action"]) + + result = CreatePrimaryIPResponse( + primary_ip=BoundPrimaryIP(self, response["primary_ip"]), action=action + ) + 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 + """Updates the name, auto_delete or labels of a Primary IP. + + :param primary_ip: :class:`BoundPrimaryIP ` or :class:`PrimaryIP ` + :param auto_delete: bool (optional) + Delete this Primary IP when the resource it is assigned to is deleted + :param labels: Dict[str, str] (optional) + User-defined labels (key-value pairs) + :param name: str (optional) + New name to set + :return: :class:`BoundPrimaryIP ` + """ + data = {} + if auto_delete is not None: + data["auto_delete"] = auto_delete + if labels is not None: + data["labels"] = labels + if name is not None: + data["name"] = name + + response = self._client.request( + url="/primary_ips/{primary_ip_id}".format(primary_ip_id=primary_ip.id), + method="PUT", + json=data, + ) + return BoundPrimaryIP(self, response["primary_ip"]) + + def delete(self, primary_ip): + # type: (PrimaryIP) -> bool + """Deletes a Primary IP. If it is currently assigned to an assignee it will automatically get unassigned. + + :param primary_ip: :class:`BoundPrimaryIP ` or :class:`PrimaryIP ` + :return: boolean + """ + self._client.request( + url="/primary_ips/{primary_ip_id}".format(primary_ip_id=primary_ip.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 change_protection(self, primary_ip, delete=None): + # type: (PrimaryIP, Optional[bool]) -> BoundAction + """Changes the protection configuration of the Primary IP. + + :param primary_ip: :class:`BoundPrimaryIP ` or :class:`PrimaryIP ` + :param delete: boolean + If true, prevents the Primary IP from being deleted + :return: :class:`BoundAction ` + """ + data = {} + 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 + ), + 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 + """Assigns a Primary IP to a assignee_id. + + :param primary_ip: :class:`BoundPrimaryIP ` or :class:`PrimaryIP ` + :param assignee_id: int + Assignee the Primary IP shall be assigned to + :param assignee_type: str + Assignee the Primary IP shall be assigned to + :return: :class:`BoundAction ` + """ + response = self._client.request( + url="/primary_ips/{primary_ip_id}/actions/assign".format( + primary_ip_id=primary_ip.id + ), + 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 + """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 ` or :class:`PrimaryIP ` + :return: :class:`BoundAction ` + """ + response = self._client.request( + url="/primary_ips/{primary_ip_id}/actions/unassign".format( + primary_ip_id=primary_ip.id + ), + method="POST", + ) + return BoundAction(self._client.actions, response["action"]) + + def change_dns_ptr(self, primary_ip, ip, dns_ptr): + # type: (PrimaryIP, str, str) -> BoundAction + """Changes the dns ptr that will appear when getting the dns ptr belonging to this Primary IP. + + :param primary_ip: :class:`BoundPrimaryIP ` or :class:`PrimaryIP ` + :param ip: str + The IP address for which to set the reverse DNS entry + :param dns_ptr: str + Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` + :return: :class:`BoundAction ` + """ + response = self._client.request( + url="/primary_ips/{primary_ip_id}/actions/change_dns_ptr".format( + primary_ip_id=primary_ip.id + ), + method="POST", + json={"ip": ip, "dns_ptr": dns_ptr}, + ) + return BoundAction(self._client.actions, response["action"]) diff --git a/hcloud/primary_ips/domain.py b/hcloud/primary_ips/domain.py new file mode 100644 index 00000000..077c7106 --- /dev/null +++ b/hcloud/primary_ips/domain.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +from dateutil.parser import isoparse + +from hcloud.core.domain import BaseDomain + + +class PrimaryIP(BaseDomain): + """Primary IP Domain + + :param id: int + ID of the Primary IP + :param ip: str + IP address of the Primary IP + :param type: str + Type of Primary IP. Choices: `ipv4`, `ipv6` + :param dns_ptr: List[Dict] + Array of reverse DNS entries + :param datacenter: :class:`Datacenter ` + Datacenter the Primary IP was created in. + :param blocked: boolean + Whether the IP is blocked + :param protection: dict + Protection configuration for the Primary IP + :param labels: dict + User-defined labels (key-value pairs) + :param created: datetime + Point in time when the Primary IP was created + :param name: str + Name of the Primary IP + :param assignee_id: int + Assignee ID the Primary IP is assigned to + :param assignee_type: str + Assignee Type of entity the Primary IP is assigned to + :param auto_delete: bool + Delete the Primary IP when the Assignee it is assigned to is deleted. + """ + + __slots__ = ( + "id", + "ip", + "type", + "dns_ptr", + "datacenter", + "blocked", + "protection", + "labels", + "created", + "name", + "assignee_id", + "assignee_type", + "auto_delete", + ) + + 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, + ): + self.id = id + self.type = type + self.ip = ip + self.dns_ptr = dns_ptr + self.datacenter = datacenter + self.blocked = blocked + self.protection = protection + self.labels = labels + self.created = isoparse(created) if created else None + self.name = name + self.assignee_id = assignee_id + self.assignee_type = assignee_type + self.auto_delete = auto_delete + + +class CreatePrimaryIPResponse(BaseDomain): + """Create Primary IP Response Domain + + :param primary_ip: :class:`BoundPrimaryIP ` + The Primary IP which was created + :param action: :class:`BoundAction ` + The Action which shows the progress of the Primary IP Creation + """ + + __slots__ = ("primary_ip", "action") + + def __init__( + self, + primary_ip, # type: BoundPrimaryIP + action, # type: BoundAction + ): + self.primary_ip = primary_ip + self.action = action diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index 414527c8..08d02f07 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -6,6 +6,7 @@ from hcloud.firewalls.client import BoundFirewall from hcloud.floating_ips.client import BoundFloatingIP from hcloud.isos.client import BoundIso +from hcloud.primary_ips.client import BoundPrimaryIP from hcloud.servers.domain import ( Server, CreateServerResponse, @@ -61,8 +62,34 @@ def __init__(self, client, data, complete=True): public_net = data.get("public_net") if public_net: - ipv4_address = IPv4Address.from_dict(public_net["ipv4"]) - ipv6_network = IPv6Network.from_dict(public_net["ipv6"]) + ipv4_address = ( + IPv4Address.from_dict(public_net["ipv4"]) + if public_net["ipv4"] is not None + else None + ) + ipv4_primary_ip = ( + BoundPrimaryIP( + client._client.primary_ips, + {"id": public_net["ipv4"]["id"]}, + complete=False, + ) + if public_net["ipv4"] is not None + else None + ) + ipv6_network = ( + IPv6Network.from_dict(public_net["ipv6"]) + if public_net["ipv6"] is not None + else None + ) + ipv6_primary_ip = ( + BoundPrimaryIP( + client._client.primary_ips, + {"id": public_net["ipv6"]["id"]}, + complete=False, + ) + if public_net["ipv6"] is not None + else None + ) floating_ips = [ BoundFloatingIP( client._client.floating_ips, {"id": floating_ip}, complete=False @@ -81,6 +108,8 @@ def __init__(self, client, data, complete=True): data["public_net"] = PublicNetwork( ipv4=ipv4_address, ipv6=ipv6_network, + primary_ipv4=ipv4_primary_ip, + primary_ipv6=ipv6_primary_ip, floating_ips=floating_ips, firewalls=firewalls, ) @@ -480,6 +509,7 @@ def create( start_after_create=True, # type: Optional[bool] automount=None, # type: Optional[bool] placement_group=None, # type: Optional[PlacementGroup] + public_net=None, # type: Optional[ServerCreatePublicNetwork] ): # type: (...) -> CreateServerResponse """Creates a new server. Returns preliminary information about the server as well as an action that covers progress of creation. @@ -508,6 +538,8 @@ def create( Auto mount volumes after attach. :param placement_group: :class:`BoundPlacementGroup ` or :class:`Location ` Placement Group where server should be added during creation + :param public_net: :class:`ServerCreatePublicNetwork ` + Options to configure the public network of a server on creation :return: :class:`CreateServerResponse ` """ data = { @@ -538,6 +570,17 @@ def create( if placement_group is not None: data["placement_group"] = placement_group.id + if public_net is not None: + pn = { + "enable_ipv4": public_net.enable_ipv4, + "enable_ipv6": public_net.enable_ipv6, + } + if public_net.ipv4 is not None: + pn.update({"ipv4": public_net.ipv4.id}) + if public_net.ipv6 is not None: + pn.update({"ipv6": public_net.ipv6.id}) + data["public_net"] = pn + response = self._client.request(url="/servers", method="POST", json=data) result = CreateServerResponse( @@ -969,9 +1012,7 @@ def attach_to_network(self, server, network, ip=None, alias_ips=None): New alias IPs to set for this server. :return: :class:`BoundAction ` """ - data = { - "network": network.id, - } + data = {"network": network.id} if ip is not None: data.update({"ip": ip}) if alias_ips is not None: @@ -993,9 +1034,7 @@ def detach_from_network(self, server, network): :param network: :class:`BoundNetwork ` or :class:`Network ` :return: :class:`BoundAction ` """ - data = { - "network": network.id, - } + data = {"network": network.id} response = self._client.request( url="/servers/{server_id}/actions/detach_from_network".format( server_id=server.id @@ -1033,9 +1072,7 @@ def add_to_placement_group(self, server, placement_group): :param placement_group: :class:`BoundPlacementGroup ` or :class:`Network ` :return: :class:`BoundAction ` """ - data = { - "placement_group": str(placement_group.id), - } + data = {"placement_group": str(placement_group.id)} response = self._client.request( url="/servers/{server_id}/actions/add_to_placement_group".format( server_id=server.id diff --git a/hcloud/servers/domain.py b/hcloud/servers/domain.py index 1ba7fb43..ea222ebe 100644 --- a/hcloud/servers/domain.py +++ b/hcloud/servers/domain.py @@ -232,22 +232,35 @@ class PublicNetwork(BaseDomain): :param ipv4: :class:`IPv4Address ` :param ipv6: :class:`IPv6Network ` :param floating_ips: List[:class:`BoundFloatingIP `] + :param primary_ipv4: :class:`BoundPrimaryIP ` + :param primary_ipv6: :class:`BoundPrimaryIP ` :param firewalls: List[:class:`PublicNetworkFirewall `] """ - __slots__ = ("ipv4", "ipv6", "floating_ips", "firewalls") + __slots__ = ( + "ipv4", + "ipv6", + "floating_ips", + "firewalls", + "primary_ipv4", + "primary_ipv6", + ) 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] ): self.ipv4 = ipv4 self.ipv6 = ipv6 self.floating_ips = floating_ips self.firewalls = firewalls + self.primary_ipv4 = primary_ipv4 + self.primary_ipv6 = primary_ipv6 class PublicNetworkFirewall(BaseDomain): @@ -354,3 +367,27 @@ def __init__( self.ip = ip self.alias_ips = alias_ips self.mac_address = mac_address + + +class ServerCreatePublicNetwork(BaseDomain): + """Server Create Public Network Domain + + :param ipv4: Optional[:class:`PrimaryIP `] + :param ipv6: Optional[:class:`PrimaryIP `] + :param enable_ipv4: bool + :param enable_ipv6: bool + """ + + __slots__ = ("ipv4", "ipv6", "enable_ipv4", "enable_ipv6") + + 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 + ): + self.ipv4 = ipv4 + self.ipv6 = ipv6 + self.enable_ipv4 = enable_ipv4 + self.enable_ipv6 = enable_ipv6 diff --git a/hcloud/volumes/client.py b/hcloud/volumes/client.py index aed318e3..093184e8 100644 --- a/hcloud/volumes/client.py +++ b/hcloud/volumes/client.py @@ -220,10 +220,7 @@ def create( if not (bool(location) ^ bool(server)): raise ValueError("only one of server or location must be provided") - data = { - "name": name, - "size": size, - } + data = {"name": name, "size": size} if labels is not None: data["labels"] = labels if location is not None: diff --git a/tests/unit/firewalls/conftest.py b/tests/unit/firewalls/conftest.py index 9bee2d81..922acd42 100644 --- a/tests/unit/firewalls/conftest.py +++ b/tests/unit/firewalls/conftest.py @@ -190,7 +190,7 @@ def one_firewalls_response(): } ], "applied_to": [{"server": {"id": 42}, "type": "server"}], - }, + } ] } diff --git a/tests/unit/floating_ips/conftest.py b/tests/unit/floating_ips/conftest.py index d4641e80..755889ef 100644 --- a/tests/unit/floating_ips/conftest.py +++ b/tests/unit/floating_ips/conftest.py @@ -54,7 +54,7 @@ def one_floating_ips_response(): "blocked": False, "protection": {"delete": False}, "labels": {}, - }, + } ] } diff --git a/tests/unit/floating_ips/test_client.py b/tests/unit/floating_ips/test_client.py index e8adcca7..79ce8dfa 100644 --- a/tests/unit/floating_ips/test_client.py +++ b/tests/unit/floating_ips/test_client.py @@ -206,9 +206,7 @@ def test_get_all(self, floating_ips_client, two_floating_ips_response, params): def test_create_with_location(self, floating_ips_client, floating_ip_response): floating_ips_client._client.request.return_value = floating_ip_response response = floating_ips_client.create( - "ipv6", - "Web Frontend", - home_location=Location(name="location"), + "ipv6", "Web Frontend", home_location=Location(name="location") ) floating_ips_client._client.request.assert_called_with( url="/floating_ips", diff --git a/tests/unit/images/conftest.py b/tests/unit/images/conftest.py index 0c3e56bd..a6156e9a 100644 --- a/tests/unit/images/conftest.py +++ b/tests/unit/images/conftest.py @@ -90,7 +90,7 @@ def one_images_response(): "protection": {"delete": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, - }, + } ] } diff --git a/tests/unit/networks/test_client.py b/tests/unit/networks/test_client.py index 03249264..062d156f 100644 --- a/tests/unit/networks/test_client.py +++ b/tests/unit/networks/test_client.py @@ -264,10 +264,7 @@ def test_create(self, networks_client, network_create_response): networks_client._client.request.assert_called_with( url="/networks", method="POST", - json={ - "name": "mynet", - "ip_range": "10.0.0.0/8", - }, + json={"name": "mynet", "ip_range": "10.0.0.0/8"}, ) def test_create_with_subnet( @@ -306,12 +303,7 @@ def test_create_with_route( json={ "name": "mynet", "ip_range": "10.0.0.0/8", - "routes": [ - { - "destination": "10.100.1.0/24", - "gateway": "10.0.1.1", - } - ], + "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], }, ) @@ -338,12 +330,7 @@ def test_create_with_route_and_subnet( "network_zone": "eu-central", } ], - "routes": [ - { - "destination": "10.100.1.0/24", - "gateway": "10.0.1.1", - } - ], + "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], }, ) diff --git a/tests/unit/primary_ips/__init__.py b/tests/unit/primary_ips/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/primary_ips/conftest.py b/tests/unit/primary_ips/conftest.py new file mode 100644 index 00000000..8885535f --- /dev/null +++ b/tests/unit/primary_ips/conftest.py @@ -0,0 +1,253 @@ +import pytest + + +@pytest.fixture() +def primary_ip_response(): + return { + "primary_ip": { + "assignee_id": 17, + "assignee_type": "server", + "auto_delete": True, + "blocked": False, + "created": "2016-01-30T23:55:00+00:00", + "datacenter": { + "description": "Falkenstein DC Park 8", + "id": 42, + "location": { + "city": "Falkenstein", + "country": "DE", + "description": "Falkenstein DC Park 1", + "id": 1, + "latitude": 50.47612, + "longitude": 12.370071, + "name": "fsn1", + "network_zone": "eu-central", + }, + "name": "fsn1-dc8", + "server_types": { + "available": [1, 2, 3], + "available_for_migration": [1, 2, 3], + "supported": [1, 2, 3], + }, + }, + "dns_ptr": [{"dns_ptr": "server.example.com", "ip": "131.232.99.1"}], + "id": 42, + "ip": "131.232.99.1", + "labels": {}, + "name": "my-resource", + "protection": {"delete": False}, + "type": "ipv4", + } + } + + +@pytest.fixture() +def one_primary_ips_response(): + return { + "meta": { + "pagination": { + "last_page": 4, + "next_page": 4, + "page": 3, + "per_page": 25, + "previous_page": 2, + "total_entries": 100, + } + }, + "primary_ips": [ + { + "assignee_id": 17, + "assignee_type": "server", + "auto_delete": True, + "blocked": False, + "created": "2016-01-30T23:55:00+00:00", + "datacenter": { + "description": "Falkenstein DC Park 8", + "id": 42, + "location": { + "city": "Falkenstein", + "country": "DE", + "description": "Falkenstein DC Park 1", + "id": 1, + "latitude": 50.47612, + "longitude": 12.370071, + "name": "fsn1", + "network_zone": "eu-central", + }, + "name": "fsn1-dc8", + "server_types": { + "available": [1, 2, 3], + "available_for_migration": [1, 2, 3], + "supported": [1, 2, 3], + }, + }, + "dns_ptr": [{"dns_ptr": "server.example.com", "ip": "131.232.99.1"}], + "id": 42, + "ip": "131.232.99.1", + "labels": {}, + "name": "my-resource", + "protection": {"delete": False}, + "type": "ipv4", + } + ], + } + + +@pytest.fixture() +def all_primary_ips_response(): + return { + "meta": { + "pagination": { + "last_page": 1, + "next_page": None, + "page": 1, + "per_page": 25, + "previous_page": None, + "total_entries": 1, + } + }, + "primary_ips": [ + { + "assignee_id": 17, + "assignee_type": "server", + "auto_delete": True, + "blocked": False, + "created": "2016-01-30T23:55:00+00:00", + "datacenter": { + "description": "Falkenstein DC Park 8", + "id": 42, + "location": { + "city": "Falkenstein", + "country": "DE", + "description": "Falkenstein DC Park 1", + "id": 1, + "latitude": 50.47612, + "longitude": 12.370071, + "name": "fsn1", + "network_zone": "eu-central", + }, + "name": "fsn1-dc8", + "server_types": { + "available": [1, 2, 3], + "available_for_migration": [1, 2, 3], + "supported": [1, 2, 3], + }, + }, + "dns_ptr": [{"dns_ptr": "server.example.com", "ip": "131.232.99.1"}], + "id": 42, + "ip": "131.232.99.1", + "labels": {}, + "name": "my-resource", + "protection": {"delete": False}, + "type": "ipv4", + } + ], + } + + +@pytest.fixture() +def primary_ip_create_response(): + return { + "action": { + "command": "create_primary_ip", + "error": {"code": "action_failed", "message": "Action failed"}, + "finished": None, + "id": 13, + "progress": 0, + "resources": [{"id": 17, "type": "server"}], + "started": "2016-01-30T23:50:00+00:00", + "status": "running", + }, + "primary_ip": { + "assignee_id": 17, + "assignee_type": "server", + "auto_delete": True, + "blocked": False, + "created": "2016-01-30T23:50:00+00:00", + "datacenter": { + "description": "Falkenstein DC Park 8", + "id": 42, + "location": { + "city": "Falkenstein", + "country": "DE", + "description": "Falkenstein DC Park 1", + "id": 1, + "latitude": 50.47612, + "longitude": 12.370071, + "name": "fsn1", + "network_zone": "eu-central", + "server_types": { + "available": [1, 2, 3], + "available_for_migration": [1, 2, 3], + "supported": [1, 2, 3], + }, + }, + "name": "fsn1-dc8", + }, + "dns_ptr": [{"dns_ptr": "server.example.com", "ip": "2001:db8::1"}], + "id": 42, + "ip": "131.232.99.1", + "labels": {"labelkey": "value"}, + "name": "my-ip", + "protection": {"delete": False}, + "type": "ipv4", + }, + } + + +@pytest.fixture() +def response_update_primary_ip(): + return { + "primary_ip": { + "assignee_id": 17, + "assignee_type": "server", + "auto_delete": True, + "blocked": False, + "created": "2016-01-30T23:55:00+00:00", + "datacenter": { + "description": "Falkenstein DC Park 8", + "id": 42, + "location": { + "city": "Falkenstein", + "country": "DE", + "description": "Falkenstein DC Park 1", + "id": 1, + "latitude": 50.47612, + "longitude": 12.370071, + "name": "fsn1", + "network_zone": "eu-central", + }, + "name": "fsn1-dc8", + "server_types": { + "available": [1, 2, 3], + "available_for_migration": [1, 2, 3], + "supported": [1, 2, 3], + }, + }, + "dns_ptr": [{"dns_ptr": "server.example.com", "ip": "131.232.99.1"}], + "id": 42, + "ip": "131.232.99.1", + "labels": {}, + "name": "my-resource", + "protection": {"delete": False}, + "type": "ipv4", + } + } + + +@pytest.fixture() +def response_get_actions(): + return { + "actions": [ + { + "id": 13, + "command": "assign_primary_ip", + "status": "success", + "progress": 100, + "started": "2016-01-30T23:55:00+00:00", + "finished": "2016-01-30T23:56:00+00:00", + "resources": [{"id": 42, "type": "server"}], + "error": {"code": "action_failed", "message": "Action failed"}, + } + ] + } diff --git a/tests/unit/primary_ips/test_client.py b/tests/unit/primary_ips/test_client.py new file mode 100644 index 00000000..1611cc38 --- /dev/null +++ b/tests/unit/primary_ips/test_client.py @@ -0,0 +1,296 @@ +import pytest +import mock + +from hcloud.primary_ips.client import PrimaryIPsClient, BoundPrimaryIP +from hcloud.primary_ips.domain import PrimaryIP +from hcloud.datacenters.client import BoundDatacenter +from hcloud.datacenters.domain import Datacenter + + +class TestBoundPrimaryIP(object): + @pytest.fixture() + def bound_primary_ip(self, hetzner_client): + return BoundPrimaryIP(client=hetzner_client.primary_ips, data=dict(id=14)) + + def test_bound_primary_ip_init(self, primary_ip_response): + bound_primary_ip = BoundPrimaryIP( + client=mock.MagicMock(), data=primary_ip_response["primary_ip"] + ) + + assert bound_primary_ip.id == 42 + assert bound_primary_ip.name == "my-resource" + assert bound_primary_ip.ip == "131.232.99.1" + assert bound_primary_ip.type == "ipv4" + assert bound_primary_ip.protection == {"delete": False} + assert bound_primary_ip.labels == {} + assert bound_primary_ip.blocked is False + + assert bound_primary_ip.assignee_id == 17 + assert bound_primary_ip.assignee_type == "server" + + assert isinstance(bound_primary_ip.datacenter, BoundDatacenter) + assert bound_primary_ip.datacenter.id == 42 + assert bound_primary_ip.datacenter.name == "fsn1-dc8" + assert bound_primary_ip.datacenter.description == "Falkenstein DC Park 8" + assert bound_primary_ip.datacenter.location.country == "DE" + assert bound_primary_ip.datacenter.location.city == "Falkenstein" + assert bound_primary_ip.datacenter.location.latitude == 50.47612 + assert bound_primary_ip.datacenter.location.longitude == 12.370071 + + def test_update(self, hetzner_client, bound_primary_ip, response_update_primary_ip): + hetzner_client.request.return_value = response_update_primary_ip + primary_ip = bound_primary_ip.update(auto_delete=True, name="my-resource") + hetzner_client.request.assert_called_with( + url="/primary_ips/14", + method="PUT", + json={"auto_delete": True, "name": "my-resource"}, + ) + + assert primary_ip.id == 42 + assert primary_ip.auto_delete is True + + def test_delete(self, hetzner_client, bound_primary_ip, generic_action): + hetzner_client.request.return_value = generic_action + delete_success = bound_primary_ip.delete() + hetzner_client.request.assert_called_with( + url="/primary_ips/14", method="DELETE" + ) + + assert delete_success is True + + def test_change_protection(self, hetzner_client, bound_primary_ip, generic_action): + hetzner_client.request.return_value = generic_action + action = bound_primary_ip.change_protection(True) + hetzner_client.request.assert_called_with( + url="/primary_ips/14/actions/change_protection", + method="POST", + json={"delete": True}, + ) + + assert action.id == 1 + assert action.progress == 0 + + def test_assign(self, hetzner_client, bound_primary_ip, generic_action): + hetzner_client.request.return_value = generic_action + action = bound_primary_ip.assign(assignee_id=12, assignee_type="server") + hetzner_client.request.assert_called_with( + url="/primary_ips/14/actions/assign", + method="POST", + json={"assignee_id": 12, "assignee_type": "server"}, + ) + assert action.id == 1 + assert action.progress == 0 + + def test_unassign(self, hetzner_client, bound_primary_ip, generic_action): + hetzner_client.request.return_value = generic_action + action = bound_primary_ip.unassign() + hetzner_client.request.assert_called_with( + url="/primary_ips/14/actions/unassign", method="POST" + ) + assert action.id == 1 + assert action.progress == 0 + + def test_change_dns_ptr(self, hetzner_client, bound_primary_ip, generic_action): + hetzner_client.request.return_value = generic_action + action = bound_primary_ip.change_dns_ptr("1.2.3.4", "server02.example.com") + hetzner_client.request.assert_called_with( + url="/primary_ips/14/actions/change_dns_ptr", + method="POST", + json={"ip": "1.2.3.4", "dns_ptr": "server02.example.com"}, + ) + assert action.id == 1 + assert action.progress == 0 + + +class TestPrimaryIPsClient(object): + @pytest.fixture() + def primary_ips_client(self): + return PrimaryIPsClient(client=mock.MagicMock()) + + def test_get_by_id(self, primary_ips_client, primary_ip_response): + primary_ips_client._client.request.return_value = primary_ip_response + bound_primary_ip = primary_ips_client.get_by_id(1) + primary_ips_client._client.request.assert_called_with( + url="/primary_ips/1", method="GET" + ) + assert bound_primary_ip._client is primary_ips_client + assert bound_primary_ip.id == 42 + + def test_get_by_name(self, primary_ips_client, one_primary_ips_response): + primary_ips_client._client.request.return_value = one_primary_ips_response + bound_primary_ip = primary_ips_client.get_by_name("my-resource") + primary_ips_client._client.request.assert_called_with( + url="/primary_ips", method="GET", params={"name": "my-resource"} + ) + assert bound_primary_ip._client is primary_ips_client + assert bound_primary_ip.id == 42 + assert bound_primary_ip.name == "my-resource" + + @pytest.mark.parametrize("params", [{"label_selector": "label1"}]) + def test_get_all(self, primary_ips_client, all_primary_ips_response, params): + primary_ips_client._client.request.return_value = all_primary_ips_response + bound_primary_ips = primary_ips_client.get_all(**params) + + params.update({"page": 1, "per_page": 50}) + + primary_ips_client._client.request.assert_called_with( + url="/primary_ips", method="GET", params=params + ) + + assert len(bound_primary_ips) == 1 + + bound_primary_ip1 = bound_primary_ips[0] + + assert bound_primary_ip1._client is primary_ips_client + assert bound_primary_ip1.id == 42 + assert bound_primary_ip1.name == "my-resource" + + def test_create_with_datacenter(self, primary_ips_client, primary_ip_response): + primary_ips_client._client.request.return_value = primary_ip_response + response = primary_ips_client.create( + type="ipv6", name="my-resource", datacenter=Datacenter(name="datacenter") + ) + primary_ips_client._client.request.assert_called_with( + url="/primary_ips", + method="POST", + json={ + "name": "my-resource", + "type": "ipv6", + "datacenter": "datacenter", + "auto_delete": False, + "assignee_type": "server", + }, + ) + + bound_primary_ip = response.primary_ip + action = response.action + + assert bound_primary_ip._client is primary_ips_client + assert bound_primary_ip.id == 42 + assert bound_primary_ip.name == "my-resource" + assert action is None + + def test_create_with_assignee_id( + self, primary_ips_client, primary_ip_create_response + ): + primary_ips_client._client.request.return_value = primary_ip_create_response + response = primary_ips_client.create( + type="ipv6", + name="my-ip", + assignee_id=1, + assignee_type="server", + datacenter=Datacenter(name="datacenter"), + ) + primary_ips_client._client.request.assert_called_with( + url="/primary_ips", + method="POST", + json={ + "type": "ipv6", + "assignee_id": 1, + "assignee_type": "server", + "name": "my-ip", + "auto_delete": False, + "datacenter": "datacenter", + }, + ) + bound_primary_ip = response.primary_ip + action = response.action + + assert bound_primary_ip._client is primary_ips_client + assert bound_primary_ip.id == 42 + assert bound_primary_ip.name == "my-ip" + assert bound_primary_ip.assignee_id == 17 + assert action.id == 13 + + @pytest.mark.parametrize( + "primary_ip", [PrimaryIP(id=1), BoundPrimaryIP(mock.MagicMock(), dict(id=1))] + ) + def test_update(self, primary_ips_client, primary_ip, response_update_primary_ip): + primary_ips_client._client.request.return_value = response_update_primary_ip + primary_ip = primary_ips_client.update( + primary_ip, auto_delete=True, name="my-resource" + ) + primary_ips_client._client.request.assert_called_with( + url="/primary_ips/1", + method="PUT", + json={"auto_delete": True, "name": "my-resource"}, + ) + + assert primary_ip.id == 42 + assert primary_ip.auto_delete is True + assert primary_ip.name == "my-resource" + + @pytest.mark.parametrize( + "primary_ip", [PrimaryIP(id=1), BoundPrimaryIP(mock.MagicMock(), dict(id=1))] + ) + def test_change_protection(self, primary_ips_client, primary_ip, generic_action): + primary_ips_client._client.request.return_value = generic_action + action = primary_ips_client.change_protection(primary_ip, True) + primary_ips_client._client.request.assert_called_with( + url="/primary_ips/1/actions/change_protection", + method="POST", + json={"delete": True}, + ) + + assert action.id == 1 + assert action.progress == 0 + + @pytest.mark.parametrize( + "primary_ip", [PrimaryIP(id=1), BoundPrimaryIP(mock.MagicMock(), dict(id=1))] + ) + def test_delete(self, primary_ips_client, primary_ip, generic_action): + primary_ips_client._client.request.return_value = generic_action + delete_success = primary_ips_client.delete(primary_ip) + primary_ips_client._client.request.assert_called_with( + url="/primary_ips/1", method="DELETE" + ) + + assert delete_success is True + + @pytest.mark.parametrize( + "assignee_id,assignee_type,primary_ip", + [ + (1, "server", PrimaryIP(id=12)), + (1, "server", BoundPrimaryIP(mock.MagicMock(), dict(id=12))), + ], + ) + def test_assign( + self, primary_ips_client, assignee_id, assignee_type, primary_ip, generic_action + ): + primary_ips_client._client.request.return_value = generic_action + action = primary_ips_client.assign(primary_ip, assignee_id, assignee_type) + primary_ips_client._client.request.assert_called_with( + url="/primary_ips/12/actions/assign", + method="POST", + json={"assignee_id": 1, "assignee_type": "server"}, + ) + assert action.id == 1 + assert action.progress == 0 + + @pytest.mark.parametrize( + "primary_ip", [PrimaryIP(id=12), BoundPrimaryIP(mock.MagicMock(), dict(id=12))] + ) + def test_unassign(self, primary_ips_client, primary_ip, generic_action): + primary_ips_client._client.request.return_value = generic_action + action = primary_ips_client.unassign(primary_ip) + primary_ips_client._client.request.assert_called_with( + url="/primary_ips/12/actions/unassign", method="POST" + ) + assert action.id == 1 + assert action.progress == 0 + + @pytest.mark.parametrize( + "primary_ip", [PrimaryIP(id=12), BoundPrimaryIP(mock.MagicMock(), dict(id=12))] + ) + def test_change_dns_ptr(self, primary_ips_client, primary_ip, generic_action): + primary_ips_client._client.request.return_value = generic_action + action = primary_ips_client.change_dns_ptr( + primary_ip, "1.2.3.4", "server02.example.com" + ) + primary_ips_client._client.request.assert_called_with( + url="/primary_ips/12/actions/change_dns_ptr", + method="POST", + json={"ip": "1.2.3.4", "dns_ptr": "server02.example.com"}, + ) + assert action.id == 1 + assert action.progress == 0 diff --git a/tests/unit/primary_ips/test_domain.py b/tests/unit/primary_ips/test_domain.py new file mode 100644 index 00000000..65648904 --- /dev/null +++ b/tests/unit/primary_ips/test_domain.py @@ -0,0 +1,12 @@ +import datetime +from dateutil.tz import tzoffset + +from hcloud.primary_ips.domain import PrimaryIP + + +class TestPrimaryIP(object): + def test_created_is_datetime(self): + primaryIP = PrimaryIP(id=1, created="2016-01-30T23:50+00:00") + assert primaryIP.created == datetime.datetime( + 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) + ) diff --git a/tests/unit/servers/conftest.py b/tests/unit/servers/conftest.py index cbb992d8..43d047f5 100644 --- a/tests/unit/servers/conftest.py +++ b/tests/unit/servers/conftest.py @@ -12,12 +12,14 @@ def response_simple_server(): "public_net": { "ipv4": { "ip": "1.2.3.4", + "id": 1, "blocked": False, "dns_ptr": "server01.example.com", }, "ipv6": { "ip": "2001:db8::/64", "blocked": False, + "id": 2, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], }, "floating_ips": [478], @@ -119,11 +121,13 @@ def response_create_simple_server(): "ipv4": { "ip": "1.2.3.4", "blocked": False, + "id": 1, "dns_ptr": "server01.example.com", }, "ipv6": { "ip": "2001:db8::/64", "blocked": False, + "id": 2, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], }, "floating_ips": [], @@ -190,9 +194,7 @@ def response_create_simple_server(): "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, }, - "iso": { - "id": 4711, - }, + "iso": {"id": 4711}, "rescue_enabled": False, "locked": False, "backup_window": "22-02", @@ -241,11 +243,13 @@ def response_update_server(): "ipv4": { "ip": "1.2.3.4", "blocked": False, + "id": 1, "dns_ptr": "server01.example.com", }, "ipv6": { "ip": "2001:db8::/64", "blocked": False, + "id": 2, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], }, "floating_ips": [478], @@ -345,11 +349,13 @@ def response_simple_servers(): "ipv4": { "ip": "1.2.3.4", "blocked": False, + "id": 2, "dns_ptr": "server01.example.com", }, "ipv6": { "ip": "2001:db8::/64", "blocked": False, + "id": 1, "dns_ptr": [ {"ip": "2001:db8::1", "dns_ptr": "server.example.com"} ], @@ -445,11 +451,13 @@ def response_simple_servers(): "ipv4": { "ip": "1.2.3.4", "blocked": False, + "id": 3, "dns_ptr": "server01.example.com", }, "ipv6": { "ip": "2001:db8::/64", "blocked": False, + "id": 4, "dns_ptr": [ {"ip": "2001:db8::1", "dns_ptr": "server.example.com"} ], @@ -554,11 +562,13 @@ def response_full_server(): "ipv4": { "ip": "1.2.3.4", "blocked": False, + "id": 1, "dns_ptr": "server01.example.com", }, "ipv6": { "ip": "2001:db8::/64", "blocked": False, + "id": 2, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], }, "floating_ips": [478], diff --git a/tox.ini b/tox.ini index d3d8780b..8fa58248 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ commands = flake8 hcloud tests setup.py [testenv:black] basepython = python -deps = black==21.7b0 +deps = black==22.6.0 commands = black . --check --diff [testenv] From 3cb10ab4891e6966758c118fdfe40d07e79ac0c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Wed, 29 Jun 2022 08:56:57 +0200 Subject: [PATCH 002/406] Bump Version --- hcloud/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index 781742db..b32887e0 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1 +1 @@ -VERSION = "1.16.0" +VERSION = "1.17.0" From b55756ff9590a77ab32cb69b9f7c60fdd759ade3 Mon Sep 17 00:00:00 2001 From: Steve Kowalik Date: Fri, 5 Aug 2022 15:32:29 +1000 Subject: [PATCH 003/406] Remove use of external mock module (#162) Python since 3.4 has included mock in the standard library, under the unittest module. Since the lowest version supported is greater than that, we can switch to using it and drop one external requirement. --- requirements/test.txt | 1 - tests/unit/actions/test_client.py | 2 +- tests/unit/certificates/test_client.py | 2 +- tests/unit/conftest.py | 2 +- tests/unit/core/test_client.py | 2 +- tests/unit/datacenters/test_client.py | 2 +- tests/unit/firewalls/test_client.py | 2 +- tests/unit/floating_ips/test_client.py | 2 +- tests/unit/images/test_client.py | 2 +- tests/unit/isos/test_client.py | 2 +- tests/unit/load_balancer_types/test_client.py | 2 +- tests/unit/load_balancers/test_client.py | 2 +- tests/unit/locations/test_client.py | 2 +- tests/unit/networks/test_client.py | 2 +- tests/unit/placement_groups/test_client.py | 2 +- tests/unit/primary_ips/test_client.py | 2 +- tests/unit/server_types/test_client.py | 2 +- tests/unit/servers/test_client.py | 2 +- tests/unit/ssh_keys/test_client.py | 2 +- tests/unit/volumes/test_client.py | 2 +- 20 files changed, 19 insertions(+), 20 deletions(-) diff --git a/requirements/test.txt b/requirements/test.txt index 5e90782d..a0fe53ac 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -2,7 +2,6 @@ flake8==3.6.0 isort==4.3.4 -mock==2.0.0 pytest tox==3.23.1 black==21.7b0 diff --git a/tests/unit/actions/test_client.py b/tests/unit/actions/test_client.py index d16ec4a1..8ee7fe60 100644 --- a/tests/unit/actions/test_client.py +++ b/tests/unit/actions/test_client.py @@ -1,4 +1,4 @@ -import mock +from unittest import mock import pytest from hcloud.actions.client import ActionsClient, BoundAction diff --git a/tests/unit/certificates/test_client.py b/tests/unit/certificates/test_client.py index b2acd4e5..3090ff24 100644 --- a/tests/unit/certificates/test_client.py +++ b/tests/unit/certificates/test_client.py @@ -1,5 +1,5 @@ import pytest -import mock +from unittest import mock from hcloud.actions.client import BoundAction from hcloud.certificates.client import CertificatesClient, BoundCertificate diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 17c9d624..f40734d4 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,4 +1,4 @@ -import mock +from unittest import mock import pytest from hcloud import Client diff --git a/tests/unit/core/test_client.py b/tests/unit/core/test_client.py index feabf820..7c5aa36c 100644 --- a/tests/unit/core/test_client.py +++ b/tests/unit/core/test_client.py @@ -1,4 +1,4 @@ -import mock +from unittest import mock import pytest from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin diff --git a/tests/unit/datacenters/test_client.py b/tests/unit/datacenters/test_client.py index 3e24e92f..a34fe8ad 100644 --- a/tests/unit/datacenters/test_client.py +++ b/tests/unit/datacenters/test_client.py @@ -1,5 +1,5 @@ import pytest # noqa: F401 -import mock # noqa: F401 +from unittest import mock # noqa: F401 from hcloud.datacenters.client import DatacentersClient, BoundDatacenter from hcloud.datacenters.domain import DatacenterServerTypes diff --git a/tests/unit/firewalls/test_client.py b/tests/unit/firewalls/test_client.py index ab564eaf..95311c21 100644 --- a/tests/unit/firewalls/test_client.py +++ b/tests/unit/firewalls/test_client.py @@ -1,5 +1,5 @@ import pytest -import mock +from unittest import mock from hcloud.firewalls.client import FirewallsClient, BoundFirewall from hcloud.actions.client import BoundAction diff --git a/tests/unit/floating_ips/test_client.py b/tests/unit/floating_ips/test_client.py index 79ce8dfa..97fcd28f 100644 --- a/tests/unit/floating_ips/test_client.py +++ b/tests/unit/floating_ips/test_client.py @@ -1,5 +1,5 @@ import pytest -import mock +from unittest import mock from hcloud.actions.client import BoundAction from hcloud.servers.client import BoundServer diff --git a/tests/unit/images/test_client.py b/tests/unit/images/test_client.py index 521ad1f8..55ec9ba4 100644 --- a/tests/unit/images/test_client.py +++ b/tests/unit/images/test_client.py @@ -1,5 +1,5 @@ import pytest -import mock +from unittest import mock import datetime from dateutil.tz import tzoffset diff --git a/tests/unit/isos/test_client.py b/tests/unit/isos/test_client.py index 32a2a51a..4bb4d37d 100644 --- a/tests/unit/isos/test_client.py +++ b/tests/unit/isos/test_client.py @@ -1,5 +1,5 @@ import pytest -import mock +from unittest import mock import datetime from dateutil.tz import tzoffset diff --git a/tests/unit/load_balancer_types/test_client.py b/tests/unit/load_balancer_types/test_client.py index 565ce61b..b9686ad0 100644 --- a/tests/unit/load_balancer_types/test_client.py +++ b/tests/unit/load_balancer_types/test_client.py @@ -1,5 +1,5 @@ import pytest -import mock +from unittest import mock from hcloud.load_balancer_types.client import LoadBalancerTypesClient diff --git a/tests/unit/load_balancers/test_client.py b/tests/unit/load_balancers/test_client.py index 97a765a4..b2c05a5b 100644 --- a/tests/unit/load_balancers/test_client.py +++ b/tests/unit/load_balancers/test_client.py @@ -1,4 +1,4 @@ -import mock +from unittest import mock import pytest from hcloud.load_balancer_types.domain import LoadBalancerType diff --git a/tests/unit/locations/test_client.py b/tests/unit/locations/test_client.py index 8b4d5abd..217d6ae7 100644 --- a/tests/unit/locations/test_client.py +++ b/tests/unit/locations/test_client.py @@ -1,5 +1,5 @@ import pytest # noqa: F401 -import mock # noqa: F401 +from unittest import mock # noqa: F401 from hcloud.locations.client import LocationsClient diff --git a/tests/unit/networks/test_client.py b/tests/unit/networks/test_client.py index 062d156f..81c12dcb 100644 --- a/tests/unit/networks/test_client.py +++ b/tests/unit/networks/test_client.py @@ -1,6 +1,6 @@ import pytest from dateutil.parser import isoparse -import mock +from unittest import mock from hcloud.actions.client import BoundAction from hcloud.networks.client import BoundNetwork, NetworksClient diff --git a/tests/unit/placement_groups/test_client.py b/tests/unit/placement_groups/test_client.py index 674ce328..aecea55d 100644 --- a/tests/unit/placement_groups/test_client.py +++ b/tests/unit/placement_groups/test_client.py @@ -1,5 +1,5 @@ import pytest -import mock +from unittest import mock from hcloud.placement_groups.client import BoundPlacementGroup, PlacementGroupsClient diff --git a/tests/unit/primary_ips/test_client.py b/tests/unit/primary_ips/test_client.py index 1611cc38..2624f923 100644 --- a/tests/unit/primary_ips/test_client.py +++ b/tests/unit/primary_ips/test_client.py @@ -1,5 +1,5 @@ import pytest -import mock +from unittest import mock from hcloud.primary_ips.client import PrimaryIPsClient, BoundPrimaryIP from hcloud.primary_ips.domain import PrimaryIP diff --git a/tests/unit/server_types/test_client.py b/tests/unit/server_types/test_client.py index a12a3e8f..10c07c2b 100644 --- a/tests/unit/server_types/test_client.py +++ b/tests/unit/server_types/test_client.py @@ -1,5 +1,5 @@ import pytest -import mock +from unittest import mock from hcloud.server_types.client import ServerTypesClient diff --git a/tests/unit/servers/test_client.py b/tests/unit/servers/test_client.py index b92e9ff8..c8bf2493 100644 --- a/tests/unit/servers/test_client.py +++ b/tests/unit/servers/test_client.py @@ -1,4 +1,4 @@ -import mock +from unittest import mock import pytest from hcloud.firewalls.client import BoundFirewall diff --git a/tests/unit/ssh_keys/test_client.py b/tests/unit/ssh_keys/test_client.py index 6562304f..01ab9e0e 100644 --- a/tests/unit/ssh_keys/test_client.py +++ b/tests/unit/ssh_keys/test_client.py @@ -1,5 +1,5 @@ import pytest -import mock +from unittest import mock from hcloud.ssh_keys.client import SSHKeysClient, BoundSSHKey from hcloud.ssh_keys.domain import SSHKey diff --git a/tests/unit/volumes/test_client.py b/tests/unit/volumes/test_client.py index a075ad5d..7145194c 100644 --- a/tests/unit/volumes/test_client.py +++ b/tests/unit/volumes/test_client.py @@ -1,6 +1,6 @@ import pytest from dateutil.parser import isoparse -import mock +from unittest import mock from hcloud.actions.client import BoundAction from hcloud.servers.client import BoundServer From cdf0ce5f91f96013003ee1c45b0334f00d18c250 Mon Sep 17 00:00:00 2001 From: sme Date: Fri, 5 Aug 2022 09:22:43 +0200 Subject: [PATCH 004/406] document installation path via conda-forge (#149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * install via conda-forge Co-authored-by: Lukas Kämmerling --- docs/installation.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/installation.rst b/docs/installation.rst index 50448b81..96096b0f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -23,6 +23,18 @@ you through the process. .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ +Via conda (Third-Party) +--------- + +Hetzner Cloud Python is also available as a ``conda``-package via `conda-forge`. This package is not maintained by Hetzner Cloud and might be outdated._: + +.. code-block:: console + + $ conda install -c conda-forge hcloud + +.. _conda-forge: https://conda-forge.org/ + + From sources ------------ From 32bd2ecea280aebee62305fb7e4a2d0aa90540b8 Mon Sep 17 00:00:00 2001 From: jonasdlindner <42033762+jonasdlindner@users.noreply.github.com> Date: Fri, 5 Aug 2022 09:23:15 +0200 Subject: [PATCH 005/406] Drop # -*- coding: utf-8 -*- from files (#154) --- docs/conf.py | 1 - hcloud/__init__.py | 2 -- hcloud/actions/__init__.py | 1 - hcloud/actions/client.py | 1 - hcloud/actions/domain.py | 1 - hcloud/certificates/__init__.py | 1 - hcloud/certificates/client.py | 1 - hcloud/certificates/domain.py | 1 - hcloud/core/__init__.py | 1 - hcloud/core/client.py | 1 - hcloud/core/domain.py | 1 - hcloud/datacenters/__init__.py | 1 - hcloud/datacenters/client.py | 1 - hcloud/datacenters/domain.py | 1 - hcloud/firewalls/__init__.py | 1 - hcloud/firewalls/client.py | 1 - hcloud/firewalls/domain.py | 1 - hcloud/floating_ips/__init__.py | 1 - hcloud/floating_ips/client.py | 1 - hcloud/floating_ips/domain.py | 1 - hcloud/hcloud.py | 1 - hcloud/helpers/__init__.py | 1 - hcloud/images/__init__.py | 1 - hcloud/images/client.py | 1 - hcloud/images/domain.py | 1 - hcloud/isos/__init__.py | 1 - hcloud/isos/client.py | 1 - hcloud/isos/domain.py | 1 - hcloud/load_balancer_types/__init__.py | 1 - hcloud/load_balancer_types/domain.py | 1 - hcloud/load_balancers/client.py | 1 - hcloud/load_balancers/domain.py | 1 - hcloud/locations/__init__.py | 1 - hcloud/locations/client.py | 1 - hcloud/locations/domain.py | 1 - hcloud/networks/__init__.py | 1 - hcloud/networks/client.py | 1 - hcloud/networks/domain.py | 1 - hcloud/placement_groups/__init__.py | 1 - hcloud/placement_groups/client.py | 1 - hcloud/placement_groups/domain.py | 1 - hcloud/server_types/__init__.py | 1 - hcloud/server_types/domain.py | 1 - hcloud/servers/__init__.py | 1 - hcloud/servers/client.py | 1 - hcloud/servers/domain.py | 1 - hcloud/ssh_keys/__init__.py | 1 - hcloud/ssh_keys/client.py | 1 - hcloud/ssh_keys/domain.py | 1 - hcloud/volumes/__init__.py | 1 - hcloud/volumes/client.py | 1 - hcloud/volumes/domain.py | 1 - setup.py | 1 - tests/unit/test_hcloud.py | 1 - 54 files changed, 55 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index dd5b85e7..80205c98 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # # hcloud documentation build configuration file, created by # sphinx-quickstart on Fri Jun 9 13:47:02 2017. diff --git a/hcloud/__init__.py b/hcloud/__init__.py index ceb1a6c2..5ef06e2f 100644 --- a/hcloud/__init__.py +++ b/hcloud/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - from .hcloud import Client, APIException # noqa diff --git a/hcloud/actions/__init__.py b/hcloud/actions/__init__.py index 40a96afc..e69de29b 100644 --- a/hcloud/actions/__init__.py +++ b/hcloud/actions/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index 70fa0bbf..02caa6e5 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import time from hcloud.core.client import ClientEntityBase, BoundModelBase diff --git a/hcloud/actions/domain.py b/hcloud/actions/domain.py index e40c62a7..d0837055 100644 --- a/hcloud/actions/domain.py +++ b/hcloud/actions/domain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain diff --git a/hcloud/certificates/__init__.py b/hcloud/certificates/__init__.py index 40a96afc..e69de29b 100644 --- a/hcloud/certificates/__init__.py +++ b/hcloud/certificates/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/hcloud/certificates/client.py b/hcloud/certificates/client.py index d94e5507..adb7c215 100644 --- a/hcloud/certificates/client.py +++ b/hcloud/certificates/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from hcloud.actions.client import BoundAction from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin diff --git a/hcloud/certificates/domain.py b/hcloud/certificates/domain.py index 448e4b41..46f13392 100644 --- a/hcloud/certificates/domain.py +++ b/hcloud/certificates/domain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain, DomainIdentityMixin diff --git a/hcloud/core/__init__.py b/hcloud/core/__init__.py index 40a96afc..e69de29b 100644 --- a/hcloud/core/__init__.py +++ b/hcloud/core/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/hcloud/core/client.py b/hcloud/core/client.py index f9fa5408..319f6368 100644 --- a/hcloud/core/client.py +++ b/hcloud/core/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from hcloud.core.domain import add_meta_to_result diff --git a/hcloud/core/domain.py b/hcloud/core/domain.py index e08c78b1..a58765fa 100644 --- a/hcloud/core/domain.py +++ b/hcloud/core/domain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from collections import namedtuple diff --git a/hcloud/datacenters/__init__.py b/hcloud/datacenters/__init__.py index 40a96afc..e69de29b 100644 --- a/hcloud/datacenters/__init__.py +++ b/hcloud/datacenters/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/hcloud/datacenters/client.py b/hcloud/datacenters/client.py index 71548900..68c7a180 100644 --- a/hcloud/datacenters/client.py +++ b/hcloud/datacenters/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin from hcloud.datacenters.domain import Datacenter, DatacenterServerTypes diff --git a/hcloud/datacenters/domain.py b/hcloud/datacenters/domain.py index e19a2b26..52f9ccde 100644 --- a/hcloud/datacenters/domain.py +++ b/hcloud/datacenters/domain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from hcloud.core.domain import BaseDomain, DomainIdentityMixin diff --git a/hcloud/firewalls/__init__.py b/hcloud/firewalls/__init__.py index 40a96afc..e69de29b 100644 --- a/hcloud/firewalls/__init__.py +++ b/hcloud/firewalls/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/hcloud/firewalls/client.py b/hcloud/firewalls/client.py index 76797732..7fa1b1ec 100644 --- a/hcloud/firewalls/client.py +++ b/hcloud/firewalls/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from hcloud.actions.client import BoundAction from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.core.domain import add_meta_to_result diff --git a/hcloud/firewalls/domain.py b/hcloud/firewalls/domain.py index ee26b471..07cfadde 100644 --- a/hcloud/firewalls/domain.py +++ b/hcloud/firewalls/domain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain diff --git a/hcloud/floating_ips/__init__.py b/hcloud/floating_ips/__init__.py index 40a96afc..e69de29b 100644 --- a/hcloud/floating_ips/__init__.py +++ b/hcloud/floating_ips/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/hcloud/floating_ips/client.py b/hcloud/floating_ips/client.py index 6283cb61..a14d3dff 100644 --- a/hcloud/floating_ips/client.py +++ b/hcloud/floating_ips/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from hcloud.actions.client import BoundAction from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.core.domain import add_meta_to_result diff --git a/hcloud/floating_ips/domain.py b/hcloud/floating_ips/domain.py index bd5bf301..01fce179 100644 --- a/hcloud/floating_ips/domain.py +++ b/hcloud/floating_ips/domain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain diff --git a/hcloud/hcloud.py b/hcloud/hcloud.py index c172b704..e290cd44 100644 --- a/hcloud/hcloud.py +++ b/hcloud/hcloud.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from __future__ import absolute_import import time diff --git a/hcloud/helpers/__init__.py b/hcloud/helpers/__init__.py index 40a96afc..e69de29b 100644 --- a/hcloud/helpers/__init__.py +++ b/hcloud/helpers/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/hcloud/images/__init__.py b/hcloud/images/__init__.py index 40a96afc..e69de29b 100644 --- a/hcloud/images/__init__.py +++ b/hcloud/images/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/hcloud/images/client.py b/hcloud/images/client.py index 4b37a5f7..2a2345c7 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from hcloud.actions.client import BoundAction from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.core.domain import add_meta_to_result diff --git a/hcloud/images/domain.py b/hcloud/images/domain.py index cd65673c..4a2c2b05 100644 --- a/hcloud/images/domain.py +++ b/hcloud/images/domain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain, DomainIdentityMixin diff --git a/hcloud/isos/__init__.py b/hcloud/isos/__init__.py index 40a96afc..e69de29b 100644 --- a/hcloud/isos/__init__.py +++ b/hcloud/isos/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/hcloud/isos/client.py b/hcloud/isos/client.py index b7637435..a59b80b9 100644 --- a/hcloud/isos/client.py +++ b/hcloud/isos/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.isos.domain import Iso diff --git a/hcloud/isos/domain.py b/hcloud/isos/domain.py index 2551b0a5..e85b30dc 100644 --- a/hcloud/isos/domain.py +++ b/hcloud/isos/domain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain, DomainIdentityMixin diff --git a/hcloud/load_balancer_types/__init__.py b/hcloud/load_balancer_types/__init__.py index 40a96afc..e69de29b 100644 --- a/hcloud/load_balancer_types/__init__.py +++ b/hcloud/load_balancer_types/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/hcloud/load_balancer_types/domain.py b/hcloud/load_balancer_types/domain.py index 3356679e..ecfffc0b 100644 --- a/hcloud/load_balancer_types/domain.py +++ b/hcloud/load_balancer_types/domain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from hcloud.core.domain import BaseDomain, DomainIdentityMixin diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index 4caba88b..6c1fb59d 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from hcloud.certificates.client import BoundCertificate from hcloud.servers.client import BoundServer diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index 11db2e7a..07297344 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain diff --git a/hcloud/locations/__init__.py b/hcloud/locations/__init__.py index 40a96afc..e69de29b 100644 --- a/hcloud/locations/__init__.py +++ b/hcloud/locations/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/hcloud/locations/client.py b/hcloud/locations/client.py index e3e0ffa1..79cd53f6 100644 --- a/hcloud/locations/client.py +++ b/hcloud/locations/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin from hcloud.locations.domain import Location diff --git a/hcloud/locations/domain.py b/hcloud/locations/domain.py index ee0bb45c..366631dd 100644 --- a/hcloud/locations/domain.py +++ b/hcloud/locations/domain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from hcloud.core.domain import BaseDomain, DomainIdentityMixin diff --git a/hcloud/networks/__init__.py b/hcloud/networks/__init__.py index 40a96afc..e69de29b 100644 --- a/hcloud/networks/__init__.py +++ b/hcloud/networks/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/hcloud/networks/client.py b/hcloud/networks/client.py index 60b85bd4..9cc369ec 100644 --- a/hcloud/networks/client.py +++ b/hcloud/networks/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin from hcloud.core.domain import add_meta_to_result diff --git a/hcloud/networks/domain.py b/hcloud/networks/domain.py index 8fc2c4c9..8fc39901 100644 --- a/hcloud/networks/domain.py +++ b/hcloud/networks/domain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain diff --git a/hcloud/placement_groups/__init__.py b/hcloud/placement_groups/__init__.py index 40a96afc..e69de29b 100644 --- a/hcloud/placement_groups/__init__.py +++ b/hcloud/placement_groups/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/hcloud/placement_groups/client.py b/hcloud/placement_groups/client.py index 932edc5e..81426d10 100644 --- a/hcloud/placement_groups/client.py +++ b/hcloud/placement_groups/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from hcloud.actions.client import BoundAction from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin diff --git a/hcloud/placement_groups/domain.py b/hcloud/placement_groups/domain.py index 0e4e8f5a..e3ee0c6b 100644 --- a/hcloud/placement_groups/domain.py +++ b/hcloud/placement_groups/domain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain diff --git a/hcloud/server_types/__init__.py b/hcloud/server_types/__init__.py index 40a96afc..e69de29b 100644 --- a/hcloud/server_types/__init__.py +++ b/hcloud/server_types/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/hcloud/server_types/domain.py b/hcloud/server_types/domain.py index 8421a0b7..7f03ee47 100644 --- a/hcloud/server_types/domain.py +++ b/hcloud/server_types/domain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from hcloud.core.domain import BaseDomain, DomainIdentityMixin diff --git a/hcloud/servers/__init__.py b/hcloud/servers/__init__.py index 40a96afc..e69de29b 100644 --- a/hcloud/servers/__init__.py +++ b/hcloud/servers/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index 08d02f07..f52a0e53 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin from hcloud.actions.client import BoundAction diff --git a/hcloud/servers/domain.py b/hcloud/servers/domain.py index ea222ebe..4792a88c 100644 --- a/hcloud/servers/domain.py +++ b/hcloud/servers/domain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain diff --git a/hcloud/ssh_keys/__init__.py b/hcloud/ssh_keys/__init__.py index 40a96afc..e69de29b 100644 --- a/hcloud/ssh_keys/__init__.py +++ b/hcloud/ssh_keys/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/hcloud/ssh_keys/client.py b/hcloud/ssh_keys/client.py index d0e7c02d..d9c93e1e 100644 --- a/hcloud/ssh_keys/client.py +++ b/hcloud/ssh_keys/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin from hcloud.ssh_keys.domain import SSHKey diff --git a/hcloud/ssh_keys/domain.py b/hcloud/ssh_keys/domain.py index c37e63ce..84d89f42 100644 --- a/hcloud/ssh_keys/domain.py +++ b/hcloud/ssh_keys/domain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain, DomainIdentityMixin diff --git a/hcloud/volumes/__init__.py b/hcloud/volumes/__init__.py index 40a96afc..e69de29b 100644 --- a/hcloud/volumes/__init__.py +++ b/hcloud/volumes/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/hcloud/volumes/client.py b/hcloud/volumes/client.py index 093184e8..e6327c38 100644 --- a/hcloud/volumes/client.py +++ b/hcloud/volumes/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin from hcloud.actions.client import BoundAction diff --git a/hcloud/volumes/domain.py b/hcloud/volumes/domain.py index 137f3df7..71abeaee 100644 --- a/hcloud/volumes/domain.py +++ b/hcloud/volumes/domain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain, DomainIdentityMixin diff --git a/setup.py b/setup.py index 13f42bca..1c46557f 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """The setup script.""" diff --git a/tests/unit/test_hcloud.py b/tests/unit/test_hcloud.py index c680ec1d..a4f2e61a 100644 --- a/tests/unit/test_hcloud.py +++ b/tests/unit/test_hcloud.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- import json from unittest.mock import MagicMock From 76ec7edfccb36fbf824c6c565f72da91f37e223b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Fri, 5 Aug 2022 13:45:40 +0200 Subject: [PATCH 006/406] Simplify Requirement Constraints (#163) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This includes the remove of the upper boundary. Signed-off-by: Lukas Kämmerling --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 1c46557f..eef60fb9 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ with open("CHANGELOG.rst") as changelog_file: changelog = changelog_file.read() -requirements = ["future>=0.17.1,<1", "python-dateutil>=2.7.5,<3", "requests>=2.20,<3"] +requirements = ["future>=0.17.1", "python-dateutil>=2.7.5", "requests>=2.20"] extras_require = {"docs": ["Sphinx==1.8.1", "sphinx-rtd-theme==0.4.2"]} @@ -33,7 +33,7 @@ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", ], - python_requires="!=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.11", + python_requires=">3.5", description="Official Hetzner Cloud python library", install_requires=requirements, extras_require=extras_require, From 090be270264f54a2e99ecaa069780b92ec3e6331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Wed, 17 Aug 2022 09:05:57 +0200 Subject: [PATCH 007/406] Add validation helper for Label Values/Keys (#164) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add validation helper for Label Values/Keys This implementation is similar to the implementation in the hcloud-go. To give the "user" as much as verbosity as possible i introduced two methods. - `validate` which is a simple validator that just returns True/False.add-validator-for-labels - `validate_verbose` which also returns the corresponding error message (like which key/value is not correctly formatted) I decided against calling one of both function in the other and instead duplicate the code. This is based on my feeling that the code would be more complex when we as a sample call validate_verbose in validate. Also, it is slightly more readable imho, but we can discuss this :) Closes https://github.com/hetznercloud/hcloud-python/issues/153 Signed-off-by: Lukas Kämmerling * Fix tox Signed-off-by: Lukas Kämmerling Signed-off-by: Lukas Kämmerling --- docs/api.helpers.rst | 7 ++ hcloud/helpers/labels.py | 39 ++++++++++ hcloud/load_balancers/client.py | 2 +- tests/unit/helpers/test_labels.py | 119 ++++++++++++++++++++++++++++++ tox.ini | 4 +- 5 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 docs/api.helpers.rst create mode 100644 hcloud/helpers/labels.py create mode 100644 tests/unit/helpers/test_labels.py diff --git a/docs/api.helpers.rst b/docs/api.helpers.rst new file mode 100644 index 00000000..93699443 --- /dev/null +++ b/docs/api.helpers.rst @@ -0,0 +1,7 @@ +Helpers +================== + + +.. autoclass:: hcloud.helpers.labels.LabelValidator + :members: + diff --git a/hcloud/helpers/labels.py b/hcloud/helpers/labels.py new file mode 100644 index 00000000..eb7527dd --- /dev/null +++ b/hcloud/helpers/labels.py @@ -0,0 +1,39 @@ +import re +from typing import Dict + + +class LabelValidator: + KEY_REGEX = re.compile( + "^([a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]){0,253}[a-z0-9A-Z])?/)?[a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]|){0,62}[a-z0-9A-Z])?$" + ) + VALUE_REGEX = re.compile( + "^(([a-z0-9A-Z](?:[\-_.]|[a-z0-9A-Z]){0,62})?[a-z0-9A-Z]$|$)" + ) + + @staticmethod + 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`. + + :return: bool + """ + for k, v in labels.items(): + if LabelValidator.KEY_REGEX.match(k) is None: + return False + if LabelValidator.VALUE_REGEX.match(v) is None: + return False + return True + + @staticmethod + def validate_verbose(labels: Dict[str, str]) -> (bool, str): + """Validates Labels and returns the corresponding error message if something is wrong. Returns True, + if everything is fine. + + :return: bool, str + """ + for k, v in labels.items(): + if LabelValidator.KEY_REGEX.match(k) is None: + return False, f"label key {k} is not correctly formatted" + if LabelValidator.VALUE_REGEX.match(v) is None: + return False, f"label value {v} (key: {k}) is not correctly formatted" + return True, "" diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index 6c1fb59d..cb7ea3a4 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -404,7 +404,7 @@ def create( public_interface=None, # type: Optional[bool] network=None, # type: Optional[Union[Network,BoundNetwork]] ): - # type: (...) -> CreateLoadBalancerResponse: + # type: (...) -> CreateLoadBalancerResponse """Creates a Load Balancer . :param name: str diff --git a/tests/unit/helpers/test_labels.py b/tests/unit/helpers/test_labels.py new file mode 100644 index 00000000..927564cf --- /dev/null +++ b/tests/unit/helpers/test_labels.py @@ -0,0 +1,119 @@ +import pytest + +from hcloud.helpers.labels import LabelValidator + + +@pytest.mark.parametrize( + "labels,expected", + [ + # valid combinations + ({"label1": "correct.de"}, True), + ({"empty/label": ""}, True), + ({"label3-test.de/hallo.welt": "233344444443"}, True), + ({"label2.de/hallo": "1correct2.de"}, True), + # invalid value + ({"valid_key": "incorrect .com"}, False), + ({"valid_key": "-incorrect.com"}, False), + ({"valid_key": "incorrect.com-"}, False), + ({"valid_key": "incorr,ect.com-"}, False), + ( + { + "valid_key": "incorrect-111111111111111111111111111111111111111111111111111111111111.com" + }, + False, + ), + # invalid keys + ({"incorrect.de/": "correct.de"}, False), + ({"incor rect.de/": "correct.de"}, False), + ({"incorrect.de/+": "correct.de"}, False), + ({"-incorrect.de": "correct.de"}, False), + ({"incorrect.de-": "correct.de"}, False), + ({"incorrect.de/tes t": "correct.de"}, False), + ({"incorrect.de/test-": "correct.de"}, False), + ( + { + "incorrect.de/test-dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd": "correct.de" + }, + False, + ), + ( + { + "incorrect-11111111111111111111111111111111111111111111111111111111111111111111111111111111" + + "11111111111111111111111111111111111111111111111111111111111111111111111111111111" + + "11111111111111111111111111111111111111111111111111111111111111111111111111111111" + + "11111111111111111111111111111111111111111111111111111111111111111111111111111111" + + "11111111111111111111111111111111111111111111111111111111111111111111111111111111" + + "11111111111111111111111111111111111111111111111111111111111111111111111111111111" + + "11111111111111111111111111111111111111111111111111111111111111111111111111111111" + + ".de/test": "correct.de" + }, + False, + ), + ], +) +def test_validate(labels, expected): + assert LabelValidator.validate(labels=labels) == expected + + +@pytest.mark.parametrize( + "labels,expected,type", + [ + # valid combinations + ({"label1": "correct.de"}, True, ""), + ({"empty/label": ""}, True, ""), + ({"label3-test.de/hallo.welt": "233344444443"}, True, ""), + ({"label2.de/hallo": "1correct2.de"}, True, ""), + # invalid value + ({"valid_key": "incorrect .com"}, False, "value"), + ({"valid_key": "-incorrect.com"}, False, "value"), + ({"valid_key": "incorrect.com-"}, False, "value"), + ({"valid_key": "incorr,ect.com-"}, False, "value"), + ( + { + "valid_key": "incorrect-111111111111111111111111111111111111111111111111111111111111.com" + }, + False, + "value", + ), + # invalid keys + ({"incorrect.de/": "correct.de"}, False, "key"), + ({"incor rect.de/": "correct.de"}, False, "key"), + ({"incorrect.de/+": "correct.de"}, False, "key"), + ({"-incorrect.de": "correct.de"}, False, "key"), + ({"incorrect.de-": "correct.de"}, False, "key"), + ({"incorrect.de/tes t": "correct.de"}, False, "key"), + ({"incorrect.de/test-": "correct.de"}, False, "key"), + ( + { + "incorrect.de/test-dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd": "correct.de" + }, + False, + "key", + ), + ( + { + "incorrect-11111111111111111111111111111111111111111111111111111111111111111111111111111111" + + "11111111111111111111111111111111111111111111111111111111111111111111111111111111" + + "11111111111111111111111111111111111111111111111111111111111111111111111111111111" + + "11111111111111111111111111111111111111111111111111111111111111111111111111111111" + + "11111111111111111111111111111111111111111111111111111111111111111111111111111111" + + "11111111111111111111111111111111111111111111111111111111111111111111111111111111" + + "11111111111111111111111111111111111111111111111111111111111111111111111111111111" + + ".de/test": "correct.de" + }, + False, + "key", + ), + ], +) +def test_validate_verbose(labels, expected, type): + result, error = LabelValidator.validate_verbose(labels=labels) + if type == "key" and expected is False: + assert error == f"label key {list(labels.keys())[0]} is not correctly formatted" + elif type == "value" and expected is False: + assert ( + error + == f"label value {list(labels.values())[0]} (key: {list(labels.keys())[0]}) is not correctly formatted" + ) + + assert result == expected diff --git a/tox.ini b/tox.ini index 8fa58248..8c599180 100644 --- a/tox.ini +++ b/tox.ini @@ -3,8 +3,8 @@ envlist = py36, py37, py38, py39, py310, flake8 [testenv:flake8] basepython = python -deps = flake8==3.6.0 -commands = flake8 hcloud tests setup.py +deps = flake8==5.0.4 +commands = flake8 --ignore F821,E501,W605,W503 hcloud tests setup.py [testenv:black] basepython = python From c2de84e1a2353a89acc8647101ca14d6e152ebbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Wed, 17 Aug 2022 10:06:33 +0200 Subject: [PATCH 008/406] Bum Version --- hcloud/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index b32887e0..4c0681d4 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1 +1 @@ -VERSION = "1.17.0" +VERSION = "1.18.0" From 842321553ae007651491dd6e9a9b1850afa8957b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Tue, 25 Oct 2022 07:05:39 +0200 Subject: [PATCH 009/406] Update Github Actions (#165) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lukas Kämmerling Signed-off-by: Lukas Kämmerling --- .github/workflows/code_style.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- .github/workflows/unit_test.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/code_style.yml b/.github/workflows/code_style.yml index 132aa94f..eb49a0ff 100644 --- a/.github/workflows/code_style.yml +++ b/.github/workflows/code_style.yml @@ -6,9 +6,9 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: 3.7 architecture: x64 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7cd05973..5e9c4ad7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,9 +8,9 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: '3.x' - name: Install dependencies diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 29871179..8e68ae5c 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -10,9 +10,9 @@ jobs: python-version: [ 3.6, 3.7, 3.8, 3.9, "3.10" ] name: Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} architecture: x64 From 05f13162937669ee67069f46f7715ca41f1d619b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Tue, 25 Oct 2022 07:05:50 +0200 Subject: [PATCH 010/406] Add tests for Python 3.11 (#167) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Additionally based on our Version policy drop testes for Python 3.6 Signed-off-by: Lukas Kämmerling Signed-off-by: Lukas Kämmerling --- .github/workflows/code_style.yml | 2 +- .github/workflows/unit_test.yml | 2 +- .gitlab-ci.yml | 14 +++++++------- setup.py | 2 +- tox.ini | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/code_style.yml b/.github/workflows/code_style.yml index eb49a0ff..b9562fba 100644 --- a/.github/workflows/code_style.yml +++ b/.github/workflows/code_style.yml @@ -10,7 +10,7 @@ jobs: - name: Setup python uses: actions/setup-python@v3 with: - python-version: 3.7 + python-version: 3.9 architecture: x64 - name: Install dependencies run: | diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 8e68ae5c..e3f51bad 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ 3.6, 3.7, 3.8, 3.9, "3.10" ] + python-version: [ 3.7, 3.8, 3.9, "3.10", "3.11.0-rc.2" ] name: Python ${{ matrix.python-version }} steps: - uses: actions/checkout@v3 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c64963ff..5a99a251 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,11 +11,6 @@ stages: tags: - hc-bladerunner -python36: - <<: *tests_template - image: python:3.6-alpine - script: tox -e py36 - python37: <<: *tests_template image: python:3.7-alpine @@ -33,12 +28,17 @@ python39: python310: <<: *tests_template - image: python:3.10-rc-alpine + image: python:3.10-alpine script: tox -e py310 +python311: + <<: *tests_template + image: python:3.1-rc-alpine + script: tox -e py311 + test-style: <<: *tests_template - image: python:3.7-alpine + image: python:3.9-alpine script: - tox -e flake8 - tox -e black diff --git a/setup.py b/setup.py index eef60fb9..322666a0 100644 --- a/setup.py +++ b/setup.py @@ -27,11 +27,11 @@ "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ], python_requires=">3.5", description="Official Hetzner Cloud python library", diff --git a/tox.ini b/tox.ini index 8c599180..a48f6f7e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36, py37, py38, py39, py310, flake8 +envlist = py37, py38, py39, py310, py311, flake8 [testenv:flake8] basepython = python @@ -20,8 +20,8 @@ commands = [gh-actions] python = - 3.6: py36 3.7: py37 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 From ed98de05e1fa928bdabd14d584673806b8e6b0f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Tue, 25 Oct 2022 07:06:22 +0200 Subject: [PATCH 011/406] Bump version --- hcloud/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index 4c0681d4..a56ae3a4 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1 +1 @@ -VERSION = "1.18.0" +VERSION = "1.18.1" From 381545d2addfa625d4a53cca0705397e6e5e4672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Tue, 27 Dec 2022 09:34:10 +0100 Subject: [PATCH 012/406] fix: remove unused future dependency (#173) As far as I can tell, we never used this dependency. We only support Python 3+, and the dependency has a known vulnerability and is not maintained. There is not reason for us to keep it, and we were requested to remove it in #172. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 322666a0..96879fb5 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ with open("CHANGELOG.rst") as changelog_file: changelog = changelog_file.read() -requirements = ["future>=0.17.1", "python-dateutil>=2.7.5", "requests>=2.20"] +requirements = ["python-dateutil>=2.7.5", "requests>=2.20"] extras_require = {"docs": ["Sphinx==1.8.1", "sphinx-rtd-theme==0.4.2"]} From 4ee0a243c0763d8dc327ab454e5713e60f6f31b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Tue, 27 Dec 2022 09:41:42 +0100 Subject: [PATCH 013/406] chore: update tests to use release python-3.11 (#175) --- .github/workflows/unit_test.yml | 2 +- .gitlab-ci.yml | 2 +- hcloud/hcloud.py | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index e3f51bad..a7ac659a 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ 3.7, 3.8, 3.9, "3.10", "3.11.0-rc.2" ] + python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ] name: Python ${{ matrix.python-version }} steps: - uses: actions/checkout@v3 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5a99a251..96e645a0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -33,7 +33,7 @@ python310: python311: <<: *tests_template - image: python:3.1-rc-alpine + image: python:3.11-alpine script: tox -e py311 test-style: diff --git a/hcloud/hcloud.py b/hcloud/hcloud.py index e290cd44..ef6b57f8 100644 --- a/hcloud/hcloud.py +++ b/hcloud/hcloud.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import time import requests From 5adcfb0a303d60b88abce837be3437f6f62b63c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Tue, 27 Dec 2022 09:42:12 +0100 Subject: [PATCH 014/406] chore: prepare release 1.18.2 (#174) --- hcloud/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index a56ae3a4..cca3a688 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1 +1 @@ -VERSION = "1.18.1" +VERSION = "1.18.2" From be383443f3c6fb5ddb214f4a6460e9f1ae18a00c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Fri, 27 Jan 2023 09:22:04 +0100 Subject: [PATCH 015/406] docs: link to PrivateNet broken (#177) --- hcloud/servers/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hcloud/servers/domain.py b/hcloud/servers/domain.py index 4792a88c..a2aabd3b 100644 --- a/hcloud/servers/domain.py +++ b/hcloud/servers/domain.py @@ -40,7 +40,7 @@ class Server(BaseDomain): User-defined labels (key-value pairs) :param volumes: List[:class:`BoundVolume `] Volumes assigned to this server. - :param private_net: List[:class:`PrivateNet `] Private networks information. """ From 9d5afe001d6dc19ebbad06b26abec1880f05aec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Wed, 12 Apr 2023 09:59:17 +0200 Subject: [PATCH 016/406] feat: add support for ARM APIs (#182) * feat: add field Architecture to ISO, ServerType & Image * feat: add architecture filter to image & iso clients * feat(image): add new method to get image by name&architecture * chore: prepare release 1.19.0 --- hcloud/__version__.py | 2 +- hcloud/images/client.py | 28 ++++++++++++++++++++++- hcloud/images/domain.py | 5 +++++ hcloud/isos/client.py | 31 +++++++++++++++++++++++--- hcloud/isos/domain.py | 13 +++++++++-- hcloud/server_types/domain.py | 5 +++++ tests/unit/images/conftest.py | 5 +++++ tests/unit/images/test_client.py | 16 +++++++++++++ tests/unit/isos/conftest.py | 4 ++++ tests/unit/isos/test_client.py | 1 + tests/unit/server_types/conftest.py | 4 ++++ tests/unit/server_types/test_client.py | 23 ++++++++++++++++++- 12 files changed, 129 insertions(+), 8 deletions(-) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index cca3a688..ccdbf7c1 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1 +1 @@ -VERSION = "1.18.2" +VERSION = "1.19.0" diff --git a/hcloud/images/client.py b/hcloud/images/client.py index 2a2345c7..c5a65dfd 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -169,6 +169,7 @@ def get_list( 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] @@ -186,6 +187,8 @@ def get_list( Server Id linked to the image. Only available for images of type backup :param type: List[str] (optional) Choices: system snapshot backup + :param architecture: List[str] (optional) + Choices: x86 arm :param status: List[str] (optional) Can be used to filter images by their status. The response will only contain images matching the status. :param sort: List[str] (optional) @@ -207,6 +210,8 @@ def get_list( params["bound_to"] = bound_to if type is not None: params["type"] = type + if architecture is not None: + params["architecture"] = architecture if sort is not None: params["sort"] = sort if page is not None: @@ -228,6 +233,7 @@ def get_all( 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] @@ -243,6 +249,8 @@ def get_all( Server Id linked to the image. Only available for images of type backup :param type: List[str] (optional) Choices: system snapshot backup + :param architecture: List[str] (optional) + Choices: x86 arm :param status: List[str] (optional) Can be used to filter images by their status. The response will only contain images matching the status. :param sort: List[str] (optional) @@ -256,6 +264,7 @@ def get_all( label_selector=label_selector, bound_to=bound_to, type=type, + architecture=architecture, sort=sort, status=status, include_deprecated=include_deprecated, @@ -265,12 +274,29 @@ def get_by_name(self, name): # type: (str) -> BoundImage """Get image by name + Deprecated: Use get_by_name_and_architecture instead. + :param name: str Used to get image by name. :return: :class:`BoundImage ` """ return super(ImagesClient, self).get_by_name(name) + def get_by_name_and_architecture(self, name, architecture): + # type: (str, str) -> BoundImage + """Get image by name + + :param name: str + Used to identify the image. + :param architecture: str + Used to identify the image. + :return: :class:`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 + def update(self, image, description=None, type=None, labels=None): # type:(Image, Optional[str], Optional[str], Optional[Dict[str, str]]) -> BoundImage """Updates the Image. You may change the description, convert a Backup image to a Snapshot Image or change the image labels. @@ -311,7 +337,7 @@ def delete(self, image): return True def change_protection(self, image, delete=None): - # type: (Image, Optional[bool], Optional[bool]) -> BoundAction + # type: (Image, Optional[bool]) -> BoundAction """Changes the protection configuration of the image. Can only be used on snapshots. :param image: :class:`BoundImage ` or :class:`Image ` diff --git a/hcloud/images/domain.py b/hcloud/images/domain.py index 4a2c2b05..6e387b6a 100644 --- a/hcloud/images/domain.py +++ b/hcloud/images/domain.py @@ -30,6 +30,8 @@ class Image(BaseDomain, DomainIdentityMixin): Flavor of operating system contained in the image Choices: `ubuntu`, `centos`, `debian`, `fedora`, `unknown` :param os_version: str, None Operating system version + :param architecture: str + CPU Architecture that the image is compatible with. Choices: `x86`, `arm` :param rapid_deploy: bool Indicates that rapid deploy of the image is available :param protection: dict @@ -50,6 +52,7 @@ class Image(BaseDomain, DomainIdentityMixin): "bound_to", "os_flavor", "os_version", + "architecture", "rapid_deploy", "created_from", "status", @@ -72,6 +75,7 @@ def __init__( bound_to=None, os_flavor=None, os_version=None, + architecture=None, rapid_deploy=None, created_from=None, protection=None, @@ -89,6 +93,7 @@ def __init__( self.bound_to = bound_to self.os_flavor = os_flavor self.os_version = os_version + self.architecture = architecture self.rapid_deploy = rapid_deploy self.created_from = created_from self.protection = protection diff --git a/hcloud/isos/client.py b/hcloud/isos/client.py index a59b80b9..c28b632a 100644 --- a/hcloud/isos/client.py +++ b/hcloud/isos/client.py @@ -25,6 +25,8 @@ def get_by_id(self, id): def get_list( self, name=None, # type: Optional[str] + architecture=None, # type: Optional[List[str]] + include_wildcard_architecture=None, # type: Optional[bool] page=None, # type: Optional[int] per_page=None, # type: Optional[int] ): @@ -33,6 +35,11 @@ def get_list( :param name: str (optional) Can be used to filter ISOs by their name. + :param architecture: List[str] (optional) + Can be used to filter ISOs by their architecture. Choices: x86 arm + :param include_wildcard_architecture: bool (optional) + Custom ISOs do not have an architecture set. You must also set this flag to True if you are filtering by + architecture and also want custom ISOs. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) @@ -42,6 +49,10 @@ def get_list( params = {} if name is not None: params["name"] = name + if architecture is not None: + params["architecture"] = architecture + if include_wildcard_architecture is not None: + params["include_wildcard_architecture"] = include_wildcard_architecture if page is not None: params["page"] = page if per_page is not None: @@ -51,15 +62,29 @@ def get_list( isos = [BoundIso(self, iso_data) for iso_data in response["isos"]] return self._add_meta_to_result(isos, response) - def get_all(self, name=None): - # type: (Optional[str]) -> List[BoundIso] + def get_all( + self, + name=None, # type: Optional[str] + architecture=None, # type: Optional[List[str]] + include_wildcard_architecture=None, # type: Optional[bool] + ): + # type: (...) -> List[BoundIso] """Get all ISOs :param name: str (optional) Can be used to filter ISOs by their name. + :param architecture: List[str] (optional) + Can be used to filter ISOs by their architecture. Choices: x86 arm + :param include_wildcard_architecture: bool (optional) + Custom ISOs do not have an architecture set. You must also set this flag to True if you are filtering by + architecture and also want custom ISOs. :return: List[:class:`BoundIso `] """ - return super(IsosClient, self).get_all(name=name) + return super(IsosClient, self).get_all( + name=name, + architecture=architecture, + include_wildcard_architecture=include_wildcard_architecture, + ) def get_by_name(self, name): # type: (str) -> BoundIso diff --git a/hcloud/isos/domain.py b/hcloud/isos/domain.py index e85b30dc..906b42d5 100644 --- a/hcloud/isos/domain.py +++ b/hcloud/isos/domain.py @@ -14,17 +14,26 @@ class Iso(BaseDomain, DomainIdentityMixin): Description of the ISO :param type: str Type of the ISO. Choices: `public`, `private` + :param architecture: str, None + CPU Architecture that the ISO is compatible with. None means that the compatibility is unknown. Choices: `x86`, `arm` :param deprecated: datetime, None ISO 8601 timestamp of deprecation, None if ISO is still available. After the deprecation time it will no longer be possible to attach the ISO to servers. """ - __slots__ = ("id", "name", "type", "description", "deprecated") + __slots__ = ("id", "name", "type", "architecture", "description", "deprecated") def __init__( - self, id=None, name=None, type=None, description=None, deprecated=None + self, + id=None, + name=None, + type=None, + architecture=None, + description=None, + deprecated=None, ): self.id = id self.name = name self.type = type + self.architecture = architecture self.description = description self.deprecated = isoparse(deprecated) if deprecated else None diff --git a/hcloud/server_types/domain.py b/hcloud/server_types/domain.py index 7f03ee47..0b0891e4 100644 --- a/hcloud/server_types/domain.py +++ b/hcloud/server_types/domain.py @@ -22,6 +22,8 @@ class ServerType(BaseDomain, DomainIdentityMixin): Type of server boot drive. Local has higher speed. Network has better availability. Choices: `local`, `network` :param cpu_type: string Type of cpu. Choices: `shared`, `dedicated` + :param architecture: string + Architecture of cpu. Choices: `x86`, `arm` :param deprecated: bool True if server type is deprecated """ @@ -36,6 +38,7 @@ class ServerType(BaseDomain, DomainIdentityMixin): "prices", "storage_type", "cpu_type", + "architecture", "deprecated", ) @@ -50,6 +53,7 @@ def __init__( prices=None, storage_type=None, cpu_type=None, + architecture=None, deprecated=None, ): self.id = id @@ -61,4 +65,5 @@ def __init__( self.prices = prices self.storage_type = storage_type self.cpu_type = cpu_type + self.architecture = architecture self.deprecated = deprecated diff --git a/tests/unit/images/conftest.py b/tests/unit/images/conftest.py index a6156e9a..7ba4dbc1 100644 --- a/tests/unit/images/conftest.py +++ b/tests/unit/images/conftest.py @@ -17,6 +17,7 @@ def image_response(): "bound_to": 1, "os_flavor": "ubuntu", "os_version": "16.04", + "architecture": "x86", "rapid_deploy": False, "protection": {"delete": False}, "deprecated": "2018-02-28T00:00:00+00:00", @@ -42,6 +43,7 @@ def two_images_response(): "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", + "architecture": "x86", "rapid_deploy": False, "protection": {"delete": False}, "deprecated": "2018-02-28T00:00:00+00:00", @@ -60,6 +62,7 @@ def two_images_response(): "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", + "architecture": "x86", "rapid_deploy": False, "protection": {"delete": False}, "deprecated": "2018-02-28T00:00:00+00:00", @@ -86,6 +89,7 @@ def one_images_response(): "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", + "architecture": "x86", "rapid_deploy": False, "protection": {"delete": False}, "deprecated": "2018-02-28T00:00:00+00:00", @@ -111,6 +115,7 @@ def response_update_image(): "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", + "architecture": "arm", "rapid_deploy": False, "protection": {"delete": False}, "deprecated": "2018-02-28T00:00:00+00:00", diff --git a/tests/unit/images/test_client.py b/tests/unit/images/test_client.py index 55ec9ba4..bba1416c 100644 --- a/tests/unit/images/test_client.py +++ b/tests/unit/images/test_client.py @@ -29,6 +29,7 @@ def test_bound_image_init(self, image_response): ) assert bound_image.os_flavor == "ubuntu" assert bound_image.os_version == "16.04" + assert bound_image.architecture == "x86" assert bound_image.rapid_deploy is False assert bound_image.deprecated == datetime.datetime( 2018, 2, 28, 0, 0, tzinfo=tzoffset(None, 0) @@ -223,6 +224,21 @@ def test_get_by_name(self, images_client, one_images_response): assert image.id == 4711 assert image.name == "ubuntu-20.04" + def test_get_by_name_and_architecture(self, images_client, one_images_response): + images_client._client.request.return_value = one_images_response + image = images_client.get_by_name_and_architecture("ubuntu-20.04", "x86") + + params = {"name": "ubuntu-20.04", "architecture": ["x86"]} + + images_client._client.request.assert_called_with( + url="/images", method="GET", params=params + ) + + assert image._client is images_client + assert image.id == 4711 + assert image.name == "ubuntu-20.04" + assert image.architecture == "x86" + @pytest.mark.parametrize( "image", [Image(id=1), BoundImage(mock.MagicMock(), dict(id=1))] ) diff --git a/tests/unit/isos/conftest.py b/tests/unit/isos/conftest.py index 4c27b019..76f20738 100644 --- a/tests/unit/isos/conftest.py +++ b/tests/unit/isos/conftest.py @@ -9,6 +9,7 @@ def iso_response(): "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "type": "public", + "architecture": "x86", "deprecated": "2018-02-28T00:00:00+00:00", } } @@ -23,6 +24,7 @@ def two_isos_response(): "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "type": "public", + "architecture": "x86", "deprecated": "2018-02-28T00:00:00+00:00", }, { @@ -30,6 +32,7 @@ def two_isos_response(): "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "type": "public", + "architecture": "x86", "deprecated": "2018-02-28T00:00:00+00:00", }, ] @@ -45,6 +48,7 @@ def one_isos_response(): "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "type": "public", + "architecture": "x86", "deprecated": "2018-02-28T00:00:00+00:00", } ] diff --git a/tests/unit/isos/test_client.py b/tests/unit/isos/test_client.py index 4bb4d37d..34f052e2 100644 --- a/tests/unit/isos/test_client.py +++ b/tests/unit/isos/test_client.py @@ -18,6 +18,7 @@ def test_bound_iso_init(self, iso_response): assert bound_iso.name == "FreeBSD-11.0-RELEASE-amd64-dvd1" assert bound_iso.description == "FreeBSD 11.0 x64" assert bound_iso.type == "public" + assert bound_iso.architecture == "x86" assert bound_iso.deprecated == datetime.datetime( 2018, 2, 28, 0, 0, tzinfo=tzoffset(None, 0) ) diff --git a/tests/unit/server_types/conftest.py b/tests/unit/server_types/conftest.py index dd2fbb2d..0588252a 100644 --- a/tests/unit/server_types/conftest.py +++ b/tests/unit/server_types/conftest.py @@ -26,6 +26,7 @@ def server_type_response(): ], "storage_type": "local", "cpu_type": "shared", + "architecture": "x86", } } @@ -56,6 +57,7 @@ def two_server_types_response(): ], "storage_type": "local", "cpu_type": "shared", + "architecture": "x86", }, { "id": 2, @@ -90,6 +92,7 @@ def two_server_types_response(): ], "storage_type": "local", "cpu_type": "shared", + "architecture": "x86", }, ] } @@ -121,6 +124,7 @@ def one_server_types_response(): ], "storage_type": "local", "cpu_type": "shared", + "architecture": "x86", } ] } diff --git a/tests/unit/server_types/test_client.py b/tests/unit/server_types/test_client.py index 10c07c2b..6ddaba17 100644 --- a/tests/unit/server_types/test_client.py +++ b/tests/unit/server_types/test_client.py @@ -2,7 +2,28 @@ from unittest import mock -from hcloud.server_types.client import ServerTypesClient +from hcloud.server_types.client import ServerTypesClient, BoundServerType + + +class TestBoundIso(object): + @pytest.fixture() + def bound_server_type(self, hetzner_client): + return BoundServerType(client=hetzner_client.server_types, data=dict(id=14)) + + def test_bound_server_type_init(self, server_type_response): + bound_server_type = BoundServerType( + client=mock.MagicMock(), data=server_type_response["server_type"] + ) + + assert bound_server_type.id == 1 + assert bound_server_type.name == "cx11" + assert bound_server_type.description == "CX11" + assert bound_server_type.cores == 1 + assert bound_server_type.memory == 1 + assert bound_server_type.disk == 25 + assert bound_server_type.storage_type == "local" + assert bound_server_type.cpu_type == "shared" + assert bound_server_type.architecture == "x86" class TestServerTypesClient(object): From 8ae0bc6e032440538f3aeb2222a9bee34adab04b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Thu, 11 May 2023 13:28:47 +0200 Subject: [PATCH 017/406] feat(server_type): add field for included traffic (#185) Field was recently added to the API: https://docs.hetzner.cloud/#server-types --- hcloud/server_types/domain.py | 7 ++++++- tests/unit/server_types/conftest.py | 4 ++++ tests/unit/server_types/test_client.py | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/hcloud/server_types/domain.py b/hcloud/server_types/domain.py index 0b0891e4..5293b8d8 100644 --- a/hcloud/server_types/domain.py +++ b/hcloud/server_types/domain.py @@ -23,9 +23,11 @@ class ServerType(BaseDomain, DomainIdentityMixin): :param cpu_type: string Type of cpu. Choices: `shared`, `dedicated` :param architecture: string - Architecture of cpu. Choices: `x86`, `arm` + Architecture of cpu. Choices: `x86`, `arm` :param deprecated: bool True if server type is deprecated + :param included_traffic: int + Free traffic per month in bytes """ __slots__ = ( @@ -40,6 +42,7 @@ class ServerType(BaseDomain, DomainIdentityMixin): "cpu_type", "architecture", "deprecated", + "included_traffic", ) def __init__( @@ -55,6 +58,7 @@ def __init__( cpu_type=None, architecture=None, deprecated=None, + included_traffic=None, ): self.id = id self.name = name @@ -67,3 +71,4 @@ def __init__( self.cpu_type = cpu_type self.architecture = architecture self.deprecated = deprecated + self.included_traffic = included_traffic diff --git a/tests/unit/server_types/conftest.py b/tests/unit/server_types/conftest.py index 0588252a..559e8d2e 100644 --- a/tests/unit/server_types/conftest.py +++ b/tests/unit/server_types/conftest.py @@ -27,6 +27,7 @@ def server_type_response(): "storage_type": "local", "cpu_type": "shared", "architecture": "x86", + "included_traffic": 21990232555520, } } @@ -58,6 +59,7 @@ def two_server_types_response(): "storage_type": "local", "cpu_type": "shared", "architecture": "x86", + "included_traffic": 21990232555520, }, { "id": 2, @@ -93,6 +95,7 @@ def two_server_types_response(): "storage_type": "local", "cpu_type": "shared", "architecture": "x86", + "included_traffic": 21990232555520, }, ] } @@ -125,6 +128,7 @@ def one_server_types_response(): "storage_type": "local", "cpu_type": "shared", "architecture": "x86", + "included_traffic": 21990232555520, } ] } diff --git a/tests/unit/server_types/test_client.py b/tests/unit/server_types/test_client.py index 6ddaba17..c3fabbcb 100644 --- a/tests/unit/server_types/test_client.py +++ b/tests/unit/server_types/test_client.py @@ -24,6 +24,7 @@ def test_bound_server_type_init(self, server_type_response): assert bound_server_type.storage_type == "local" assert bound_server_type.cpu_type == "shared" assert bound_server_type.architecture == "x86" + assert bound_server_type.included_traffic == 21990232555520 class TestServerTypesClient(object): From da5caf2b42571e4a703f07400ad9d40edaabe872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Fri, 12 May 2023 11:55:20 +0200 Subject: [PATCH 018/406] ci: setup release-please (#186) Setup release-please to automate creating new tags according to semver rules. --- .github/workflows/release-please.yml | 21 +++++++++++++++++++++ hcloud/__version__.py | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release-please.yml diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 00000000..609550f5 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,21 @@ +on: + push: + branches: + - main +name: release-please + +jobs: + release-please: + # The secret HCLOUD_BOT_TOKEN is only available on the main repo, not in forks. + if: github.repository == 'hetznercloud/hcloud-python' + + runs-on: ubuntu-latest + steps: + - uses: google-github-actions/release-please-action@v3 + with: + token: ${{ secrets.HCLOUD_BOT_TOKEN }} + release-type: python + package-name: hcloud + + extra-files: | + hcloud/__version__.py diff --git a/hcloud/__version__.py b/hcloud/__version__.py index ccdbf7c1..0f2b0087 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1 +1 @@ -VERSION = "1.19.0" +VERSION = "1.19.0" # x-release-please-version From cad5163579bb021f6357e13ccefc37fb5b18cf3d Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Fri, 12 May 2023 11:59:10 +0200 Subject: [PATCH 019/406] chore(main): release 1.20.0 (#187) --- CHANGELOG.md | 8 ++++++++ hcloud/__version__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..1ea5f612 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## [1.20.0](https://github.com/hetznercloud/hcloud-python/compare/v1.19.0...v1.20.0) (2023-05-12) + + +### Features + +* **server_type:** add field for included traffic ([#185](https://github.com/hetznercloud/hcloud-python/issues/185)) ([8ae0bc6](https://github.com/hetznercloud/hcloud-python/commit/8ae0bc6e032440538f3aeb2222a9bee34adab04b)) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index 0f2b0087..494d0895 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1 +1 @@ -VERSION = "1.19.0" # x-release-please-version +VERSION = "1.20.0" # x-release-please-version From 0430bc59651d720ae4db4bf80e355af09c147cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Fri, 12 May 2023 12:50:33 +0200 Subject: [PATCH 020/406] build: include *.js *.svg static assets in sdist (#183) Extend the include for documentation assets to `*.js` and `*.svg` files, to fix building documentation from sdist archives. --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 2d23d6cc..2533badc 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,4 +7,4 @@ recursive-include tests * recursive-exclude * __pycache__ recursive-exclude * *.py[co] -recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif +recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif *.js *.svg From 47eb9f1c79e05a61084f0a639f9497beb22d6910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Wed, 14 Jun 2023 13:42:57 +0200 Subject: [PATCH 021/406] docs: improve branding, design & fix warnings (#191) * docs: fix warnings from sphinx build * docs: improve branding + design - Current header has a weird color contrast -> making bg gray and fixing the white text to a good color - Adding a favicon --- docs/_static/custom.css | 4 ++++ docs/_static/favicon.png | Bin 0 -> 688 bytes docs/api.clients.placement_groups.rst | 2 +- docs/conf.py | 8 ++++++-- docs/installation.rst | 2 +- hcloud/certificates/client.py | 4 ++-- hcloud/firewalls/client.py | 2 +- hcloud/floating_ips/client.py | 4 ++-- hcloud/images/client.py | 2 +- hcloud/server_types/client.py | 2 +- hcloud/volumes/client.py | 10 +++++----- 11 files changed, 24 insertions(+), 16 deletions(-) create mode 100644 docs/_static/custom.css create mode 100644 docs/_static/favicon.png diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 00000000..8a562b66 --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,4 @@ +.wy-side-nav-search > div.version { + /* Version in Nav is off-white by default, but we restyle the header to have an off-white bg */ + color: #404040; +} \ No newline at end of file diff --git a/docs/_static/favicon.png b/docs/_static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..f54ff679739ca6055c557f1f678554ce48251494 GIT binary patch literal 688 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10qBp=N#PuqV?iEh0D;%0v zIJB;E>s%8sxbGJKB&qyiNcMF}i|b-$5B$;|#}?mnj=5(a`TOOY|3L8X-@jkKp5-(= zNv-<#{m1{me}6xJea|l9VQ}`pU%y|qPk-4s=~e5rE3E2QSkzwDPx!QT|I@VUrx~>$ zH}3xT`_IQUJ06ARJPObIuwu)ED!8fU;befoW7dFRNT(_9N`K{b!a5;j+KHDobsFZd5ICjVMV;EJ?LWE=mPb3`Pcq z=DG%Ex<(cuhK5##=2k|A+6G2e1_m65V`We@8U}fi7AzZCsRRj99|U?Q4*9`u24{vpO%@Es!&o{kgAYbP?F5R zP%-E6CmxQ%Fb$1U{-@7)J`G}ER_4}A<`z~K_MR-lEUe(tU~)KxS$T7a!s#1VP8>ON jMCJ(l=?0GlUV03##05(}IhjrcTEXDy>gTe~DWM4fP-O^4 literal 0 HcmV?d00001 diff --git a/docs/api.clients.placement_groups.rst b/docs/api.clients.placement_groups.rst index fe3638ac..83457dfb 100644 --- a/docs/api.clients.placement_groups.rst +++ b/docs/api.clients.placement_groups.rst @@ -1,5 +1,5 @@ PlacementGroupsClient -================== +===================== .. autoclass:: hcloud.placement_groups.client.PlacementGroupsClient diff --git a/docs/conf.py b/docs/conf.py index 80205c98..549d7cd0 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -64,7 +64,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -88,11 +88,15 @@ # html_theme = "sphinx_rtd_theme" html_logo = "_static/logo-hetzner-online.svg" +html_favicon = "_static/favicon.png" # Theme options are theme-specific and customize the look and feel of a # theme further. For a list of options available for each theme, see the # documentation. # -html_theme_options = {"logo_only": True} +html_theme_options = {"logo_only": True, "style_nav_header_background": "#EFEFEF"} +html_css_files = [ + "custom.css", +] # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docs/installation.rst b/docs/installation.rst index 96096b0f..2908f0c5 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -24,7 +24,7 @@ you through the process. Via conda (Third-Party) ---------- +----------------------- Hetzner Cloud Python is also available as a ``conda``-package via `conda-forge`. This package is not maintained by Hetzner Cloud and might be outdated._: diff --git a/hcloud/certificates/client.py b/hcloud/certificates/client.py index adb7c215..c973795d 100644 --- a/hcloud/certificates/client.py +++ b/hcloud/certificates/client.py @@ -142,7 +142,7 @@ def get_list( return self._add_meta_to_result(certificates, response) def get_all(self, name=None, label_selector=None): - # type: (Optional[str]) -> List[BoundCertificate] + # type: (Optional[str], Optional[str]) -> List[BoundCertificate] """Get all certificates :param name: str (optional) @@ -166,7 +166,7 @@ def get_by_name(self, name): return super(CertificatesClient, self).get_by_name(name) def create(self, name, certificate, private_key, labels=None): - # type: (str, str, Optional[Dict[str, str]]) -> BoundCertificate + # type: (str, str, str, Optional[Dict[str, str]]) -> 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` diff --git a/hcloud/firewalls/client.py b/hcloud/firewalls/client.py index 7fa1b1ec..6a14a4e0 100644 --- a/hcloud/firewalls/client.py +++ b/hcloud/firewalls/client.py @@ -75,7 +75,7 @@ def get_actions_list(self, status=None, sort=None, page=None, per_page=None): return self._client.get_actions_list(self, status, sort, page, per_page) def get_actions(self, status=None, sort=None): - # type: (Optional[List[str]]) -> List[BoundAction] + # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] """Returns all action objects for a Firewall. :param status: List[str] (optional) diff --git a/hcloud/floating_ips/client.py b/hcloud/floating_ips/client.py index a14d3dff..37670176 100644 --- a/hcloud/floating_ips/client.py +++ b/hcloud/floating_ips/client.py @@ -43,7 +43,7 @@ def get_actions_list(self, status=None, sort=None, page=None, per_page=None): return self._client.get_actions_list(self, status, sort, page, per_page) def get_actions(self, status=None, sort=None): - # type: (Optional[List[str]]) -> List[BoundAction] + # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] """Returns all action objects for a Floating IP. :param status: List[str] (optional) @@ -91,7 +91,7 @@ def assign(self, server): # type: (Server) -> BoundAction """Assigns a Floating IP to a server. - :param server: :class:`BoundServer ` or :class:`Server ` + :param server: :class:`BoundServer ` or :class:`Server ` Server the Floating IP shall be assigned to :return: :class:`BoundAction ` """ diff --git a/hcloud/images/client.py b/hcloud/images/client.py index c5a65dfd..d05d9dc4 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -55,7 +55,7 @@ def get_actions(self, sort=None, status=None): return self._client.get_actions(self, status=status, sort=sort) def update(self, description=None, type=None, labels=None): - # type: (Optional[str], Optional[Dict[str, str]]) -> BoundImage + # type: (Optional[str], Optional[str], Optional[Dict[str, str]]) -> 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) diff --git a/hcloud/server_types/client.py b/hcloud/server_types/client.py index ab759ae2..c2bbe365 100644 --- a/hcloud/server_types/client.py +++ b/hcloud/server_types/client.py @@ -10,7 +10,7 @@ class ServerTypesClient(ClientEntityBase, GetEntityByNameMixin): results_list_attribute_name = "server_types" def get_by_id(self, id): - # type: (int) -> server_types.client.BoundServerType + # type: (int) -> BoundServerType """Returns a specific Server Type. :param id: int diff --git a/hcloud/volumes/client.py b/hcloud/volumes/client.py index e6327c38..f8ff86e3 100644 --- a/hcloud/volumes/client.py +++ b/hcloud/volumes/client.py @@ -24,7 +24,7 @@ def __init__(self, client, data, complete=True): super(BoundVolume, self).__init__(client, data, complete) def get_actions_list(self, status=None, sort=None, page=None, per_page=None): - # type: (Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]] + # type: (Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction, Meta]] """Returns all action objects for a volume. :param status: List[str] (optional) @@ -40,7 +40,7 @@ def get_actions_list(self, status=None, sort=None, page=None, per_page=None): return self._client.get_actions_list(self, status, sort, page, per_page) def get_actions(self, status=None, sort=None): - # type: (Optional[List[str]]) -> List[BoundAction] + # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] """Returns all action objects for a volume. :param status: List[str] (optional) @@ -72,7 +72,7 @@ def delete(self): return self._client.delete(self) def attach(self, server, automount=None): - # type: (Union[Server, BoundServer]) -> BoundAction + # type: (Union[Server, BoundServer], Optional[bool]) -> BoundAction """Attaches a volume to a server. Works only if the server is in the same location as the volume. :param server: :class:`BoundServer ` or :class:`Server ` @@ -247,7 +247,7 @@ def create( def get_actions_list( self, volume, status=None, sort=None, page=None, per_page=None ): - # type: (Volume, Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta] + # type: (Volume, Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta] """Returns all action objects for a volume. :param volume: :class:`BoundVolume ` or :class:`Volume ` @@ -283,7 +283,7 @@ def get_actions_list( return add_meta_to_result(actions, response, "actions") def get_actions(self, volume, status=None, sort=None): - # type: (Union[Volume, BoundVolume], Optional[List[str]]) -> List[BoundAction] + # type: (Union[Volume, BoundVolume], Optional[List[str]], Optional[List[str]]) -> List[BoundAction] """Returns all action objects for a volume. :param volume: :class:`BoundVolume ` or :class:`Volume ` From 3cba96d261499e5f812aca7936ae9ed1e75ccd52 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 16 Jun 2023 14:42:54 +0200 Subject: [PATCH 022/406] fix: adjust label validation for max length of 63 characters (#194) Related to https://github.com/hetznercloud/hcloud-go/pull/273 --- hcloud/helpers/labels.py | 4 ++-- tests/unit/helpers/test_labels.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/hcloud/helpers/labels.py b/hcloud/helpers/labels.py index eb7527dd..c9f45a97 100644 --- a/hcloud/helpers/labels.py +++ b/hcloud/helpers/labels.py @@ -4,10 +4,10 @@ class LabelValidator: KEY_REGEX = re.compile( - "^([a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]){0,253}[a-z0-9A-Z])?/)?[a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]|){0,62}[a-z0-9A-Z])?$" + "^([a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]){0,253}[a-z0-9A-Z])?/)?[a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]|){0,61}[a-z0-9A-Z])?$" ) VALUE_REGEX = re.compile( - "^(([a-z0-9A-Z](?:[\-_.]|[a-z0-9A-Z]){0,62})?[a-z0-9A-Z]$|$)" + "^(([a-z0-9A-Z](?:[\-_.]|[a-z0-9A-Z]){0,61})?[a-z0-9A-Z]$|$)" ) @staticmethod diff --git a/tests/unit/helpers/test_labels.py b/tests/unit/helpers/test_labels.py index 927564cf..013394c1 100644 --- a/tests/unit/helpers/test_labels.py +++ b/tests/unit/helpers/test_labels.py @@ -22,6 +22,12 @@ }, False, ), + ( + { + "valid_key": "63-characters-are-allowed-in-a-label__this-is-one-character-more", + }, + False, + ), # invalid keys ({"incorrect.de/": "correct.de"}, False), ({"incor rect.de/": "correct.de"}, False), @@ -75,6 +81,13 @@ def test_validate(labels, expected): False, "value", ), + ( + { + "valid_key": "63-characters-are-allowed-in-a-label__this-is-one-character-more", + }, + False, + "value", + ), # invalid keys ({"incorrect.de/": "correct.de"}, False, "key"), ({"incor rect.de/": "correct.de"}, False, "key"), From e061c59a4d58c1d48847e9aaa4c7ec76c5926aa0 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 16 Jun 2023 15:26:08 +0200 Subject: [PATCH 023/406] chore: setup pre-commit (#195) * chore: add basic pre-commit config * chore: fix shebangs and exec permission bit * style: format code using pre-commit and prettier - pre-commit hooks - prettier * ci: enforce pre-commit check * chore: add isort and black pre-commit hooks * style: format code using isort and black * chore: add pyupgrade pre-commit hook * refactor: upgrade code using pyupgrade * chore: add flake8 pre-commit hook * docs: check contribution with pre-commit --- .github/ISSUE_TEMPLATE.md | 2 +- .github/ISSUE_TEMPLATE/Bug_Report.md | 7 +- .github/ISSUE_TEMPLATE/Feature_Request.md | 3 +- .github/workflows/bot_stale.yml | 12 ++-- .github/workflows/code_style.yml | 14 ++-- .github/workflows/release.yml | 32 ++++----- .github/workflows/unit_test.yml | 10 +-- .gitlab-ci.yml | 14 ++-- .pre-commit-config.yaml | 50 ++++++++++++++ CHANGELOG.md | 3 +- CONTRIBUTING.rst | 6 +- LICENSE | 1 - Makefile | 4 +- README.rst | 4 +- docs/_static/custom.css | 6 +- docs/_static/js/open_links_in_new_tab.js | 6 +- docs/_static/logo-hetzner-online.svg | 2 +- docs/api.clients.actions.rst | 1 - docs/api.clients.certificates.rst | 1 - docs/api.clients.datacenters.rst | 1 - docs/api.clients.firewalls.rst | 1 - docs/api.clients.images.rst | 1 - docs/api.clients.isos.rst | 1 - docs/api.clients.locations.rst | 1 - docs/api.clients.primary_ips.rst | 1 - docs/api.clients.server_types.rst | 1 - docs/api.clients.ssh_keys.rst | 1 - docs/api.helpers.rst | 1 - docs/samples.rst | 2 +- examples/usage_procedurale.py | 3 +- hcloud/__init__.py | 2 +- hcloud/actions/client.py | 8 +-- hcloud/certificates/client.py | 25 +++---- hcloud/core/client.py | 6 +- hcloud/core/domain.py | 7 +- hcloud/datacenters/client.py | 13 ++-- hcloud/firewalls/client.py | 27 +++----- hcloud/floating_ips/client.py | 23 +++---- hcloud/hcloud.py | 24 +++---- hcloud/helpers/labels.py | 4 +- hcloud/images/client.py | 21 +++--- hcloud/isos/client.py | 9 +-- hcloud/load_balancer_types/client.py | 6 +- hcloud/load_balancers/client.py | 47 ++++++------- hcloud/locations/client.py | 11 ++- hcloud/networks/client.py | 29 +++----- hcloud/placement_groups/client.py | 11 ++- hcloud/primary_ips/__init__.py | 1 - hcloud/primary_ips/client.py | 20 ++---- hcloud/primary_ips/domain.py | 1 - hcloud/server_types/client.py | 10 ++- hcloud/servers/client.py | 68 ++++++++----------- hcloud/ssh_keys/client.py | 17 ++--- hcloud/volumes/client.py | 33 ++++----- requirements/test.txt | 3 - setup.cfg | 1 - setup.py | 2 +- tests/unit/actions/test_client.py | 5 +- tests/unit/actions/test_domain.py | 3 +- tests/unit/certificates/test_client.py | 9 +-- tests/unit/certificates/test_domain.py | 3 +- tests/unit/conftest.py | 2 + tests/unit/core/test_client.py | 3 +- tests/unit/core/test_domain.py | 6 +- tests/unit/datacenters/test_client.py | 9 +-- tests/unit/firewalls/test_client.py | 11 +-- tests/unit/firewalls/test_domain.py | 3 +- tests/unit/floating_ips/test_client.py | 13 ++-- tests/unit/floating_ips/test_domain.py | 3 +- tests/unit/images/test_client.py | 11 +-- tests/unit/images/test_domain.py | 3 +- tests/unit/isos/test_client.py | 11 +-- tests/unit/isos/test_domain.py | 3 +- tests/unit/load_balancer_types/test_client.py | 4 +- tests/unit/load_balancers/test_client.py | 17 +++-- tests/unit/load_balancers/test_domain.py | 3 +- tests/unit/locations/test_client.py | 5 +- tests/unit/networks/test_client.py | 9 +-- tests/unit/networks/test_domain.py | 3 +- tests/unit/placement_groups/test_client.py | 7 +- tests/unit/placement_groups/test_domain.py | 3 +- tests/unit/primary_ips/test_client.py | 11 +-- tests/unit/primary_ips/test_domain.py | 3 +- tests/unit/server_types/test_client.py | 8 +-- tests/unit/servers/test_client.py | 40 +++++------ tests/unit/servers/test_domain.py | 3 +- tests/unit/ssh_keys/test_client.py | 9 +-- tests/unit/ssh_keys/test_domain.py | 3 +- tests/unit/test_hcloud.py | 10 +-- tests/unit/volumes/test_client.py | 13 ++-- tests/unit/volumes/test_domain.py | 3 +- tox.ini | 14 ++-- 92 files changed, 426 insertions(+), 461 deletions(-) create mode 100644 .pre-commit-config.yaml mode change 100644 => 100755 setup.py diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index b376ed08..24e79d04 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -3,4 +3,4 @@ Thanks for filing an issue 😄 ! Before you submit, please read the following: Check the other issue templates if you are trying to submit a bug report, feature request, or question Search open/closed issues before submitting since someone might have asked the same thing before! ---> \ No newline at end of file +--> diff --git a/.github/ISSUE_TEMPLATE/Bug_Report.md b/.github/ISSUE_TEMPLATE/Bug_Report.md index d3524671..12a716a1 100644 --- a/.github/ISSUE_TEMPLATE/Bug_Report.md +++ b/.github/ISSUE_TEMPLATE/Bug_Report.md @@ -1,14 +1,15 @@ --- name: 🐛 Bug Report about: If something isn't working as expected 🤔. - --- + ## Bug Report **Current Behavior** A clear and concise description of the behavior. **Input Code** + - REPL or Repo link if applicable: ```python @@ -19,11 +20,13 @@ your = "code" + "here" A clear and concise description of what you expected to happen (or code). **Environment** + - Python Version: [e.g. v2.6, v2.7, v3.0] - Hcloud-Python Version: [e.g. v0.1, v1.0] **Possible Solution** + **Additional context/Screenshots** -Add any other context about the problem here. If applicable, add screenshots to help explain. \ No newline at end of file +Add any other context about the problem here. If applicable, add screenshots to help explain. diff --git a/.github/ISSUE_TEMPLATE/Feature_Request.md b/.github/ISSUE_TEMPLATE/Feature_Request.md index 5bf95629..69579688 100644 --- a/.github/ISSUE_TEMPLATE/Feature_Request.md +++ b/.github/ISSUE_TEMPLATE/Feature_Request.md @@ -1,7 +1,6 @@ --- name: 🚀 Feature Request about: I have a suggestion (and may want to implement it 🙂)! - --- ## Feature Request @@ -17,4 +16,4 @@ A clear and concise description of any alternative solutions or features you've **Teachability, Documentation, Adoption, Migration Strategy** If you can, explain how users will be able to use this and possibly write out a version the docs. -Maybe a screenshot or design? \ No newline at end of file +Maybe a screenshot or design? diff --git a/.github/workflows/bot_stale.yml b/.github/workflows/bot_stale.yml index 0a3b97fc..57b6084b 100644 --- a/.github/workflows/bot_stale.yml +++ b/.github/workflows/bot_stale.yml @@ -9,11 +9,11 @@ jobs: - uses: actions/stale@v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-issue-message: 'This issue has been marked as stale because it has not had recent activity. The bot will close the issue if no further action occurs.' - exempt-issue-label: 'pinned' - stale-issue-label: 'stale' - stale-pr-message: 'This PR has been marked as stale because it has not had recent activity. The bot will close the PR if no further action occurs.' - exempt-pr-label: 'pinned' - stale-pr-label: 'stale' + stale-issue-message: "This issue has been marked as stale because it has not had recent activity. The bot will close the issue if no further action occurs." + exempt-issue-label: "pinned" + stale-issue-label: "stale" + stale-pr-message: "This PR has been marked as stale because it has not had recent activity. The bot will close the PR if no further action occurs." + exempt-pr-label: "pinned" + stale-pr-label: "stale" days-before-stale: 90 days-before-close: 30 diff --git a/.github/workflows/code_style.yml b/.github/workflows/code_style.yml index b9562fba..60c616c5 100644 --- a/.github/workflows/code_style.yml +++ b/.github/workflows/code_style.yml @@ -14,11 +14,9 @@ jobs: architecture: x64 - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install -r requirements/test.txt - pip install tox tox-gh-actions - sudo apt install build-essential - - name: flake8 with tox - run: tox -e flake8 - - name: black with tox - run: tox -e black + python -m pip install --upgrade pip + pip install -r requirements/test.txt + pip install tox tox-gh-actions + sudo apt install build-essential + - name: pre-commit with tox + run: tox -e pre-commit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e9c4ad7..4a839b16 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,19 +8,19 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v3 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py sdist bdist_wheel - twine upload dist/* + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index a7ac659a..54dbb059 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] name: Python ${{ matrix.python-version }} steps: - uses: actions/checkout@v3 @@ -18,10 +18,10 @@ jobs: architecture: x64 - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install -r requirements/test.txt - pip install tox tox-gh-actions - sudo apt install build-essential + python -m pip install --upgrade pip + pip install -r requirements/test.txt + pip install tox tox-gh-actions + sudo apt install build-essential - name: Test with tox run: tox env: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 96e645a0..39f0eba2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,15 +1,14 @@ stages: -- test + - test .tests_template: &tests_template before_script: - - pip install tox - - apk add build-base + - pip install tox + - apk add build-base git stage: test - script: - tox + script: tox tags: - - hc-bladerunner + - hc-bladerunner python37: <<: *tests_template @@ -40,5 +39,4 @@ test-style: <<: *tests_template image: python:3.9-alpine script: - - tox -e flake8 - - tox -e black + - tox -e pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..6b2e5257 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,50 @@ +--- +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable + - id: check-symlinks + - id: destroyed-symlinks + + - id: check-json + - id: check-yaml + - id: check-toml + + - id: check-merge-conflict + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.7.1 + hooks: + - id: prettier + files: \.(md|ya?ml|js|css)$ + + - repo: https://github.com/asottile/pyupgrade + rev: v3.6.0 + hooks: + - id: pyupgrade + args: [--py37-plus] + + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + args: [--profile=black] + + - repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black + + - repo: https://github.com/pycqa/flake8 + rev: 6.0.0 + hooks: + - id: flake8 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ea5f612..7d4524d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,6 @@ ## [1.20.0](https://github.com/hetznercloud/hcloud-python/compare/v1.19.0...v1.20.0) (2023-05-12) - ### Features -* **server_type:** add field for included traffic ([#185](https://github.com/hetznercloud/hcloud-python/issues/185)) ([8ae0bc6](https://github.com/hetznercloud/hcloud-python/commit/8ae0bc6e032440538f3aeb2222a9bee34adab04b)) +- **server_type:** add field for included traffic ([#185](https://github.com/hetznercloud/hcloud-python/issues/185)) ([8ae0bc6](https://github.com/hetznercloud/hcloud-python/commit/8ae0bc6e032440538f3aeb2222a9bee34adab04b)) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a485ed14..324b1f66 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -74,14 +74,14 @@ Ready to contribute? Here's how to set up `hcloud-python` for local development. Now you can make your changes locally. -5. When you're done making changes, check that your changes pass flake8 and the +5. When you're done making changes, check that your changes pass pre-commit and the tests, including testing other Python versions with tox:: - $ flake8 hetznercloud tests + $ tox -e pre-commit $ python setup.py test or py.test $ tox - To get flake8 and tox, just pip install them into your virtualenv. + To get pre-commit and tox, just pip install them into your virtualenv. 6. Commit your changes and push your branch to GitHub:: diff --git a/LICENSE b/LICENSE index a3f4553e..5eadcc40 100644 --- a/LICENSE +++ b/LICENSE @@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/Makefile b/Makefile index bf621e39..cb8a27a2 100644 --- a/Makefile +++ b/Makefile @@ -50,8 +50,8 @@ clean-test: ## remove test and coverage artifacts rm -fr htmlcov/ rm -fr .pytest_cache -lint: ## check style with flake8 - flake8 hcloud tests +lint: ## check code with pre-commit + tox -e pre-commit test: ## run tests quickly with the default Python py.test diff --git a/README.rst b/README.rst index fe3a76cb..4720f28f 100644 --- a/README.rst +++ b/README.rst @@ -48,12 +48,12 @@ Run tests --------- * `tox .` * You can specify environment e.g `tox -e py36` -* You can test the code style with `tox -e flake8` +* You can test the code style with `tox -e pre-commit` Create Documentation -------------------- -Run `make docs`. This will also open a documentation in a tab in your default browser. +Run `make docs`. This will also open a documentation in a tab in your default browser. Style Guide diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 8a562b66..8291c26a 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -1,4 +1,4 @@ .wy-side-nav-search > div.version { - /* Version in Nav is off-white by default, but we restyle the header to have an off-white bg */ - color: #404040; -} \ No newline at end of file + /* Version in Nav is off-white by default, but we restyle the header to have an off-white bg */ + color: #404040; +} diff --git a/docs/_static/js/open_links_in_new_tab.js b/docs/_static/js/open_links_in_new_tab.js index 7981bcb1..6c17a212 100644 --- a/docs/_static/js/open_links_in_new_tab.js +++ b/docs/_static/js/open_links_in_new_tab.js @@ -1,3 +1,3 @@ -$(document).ready(function() { - $("a[href^='http']").attr('target','_blank'); -}); \ No newline at end of file +$(document).ready(function () { + $("a[href^='http']").attr("target", "_blank"); +}); diff --git a/docs/_static/logo-hetzner-online.svg b/docs/_static/logo-hetzner-online.svg index bc070ab8..3c99ccd6 100644 --- a/docs/_static/logo-hetzner-online.svg +++ b/docs/_static/logo-hetzner-online.svg @@ -4,4 +4,4 @@ - \ No newline at end of file + diff --git a/docs/api.clients.actions.rst b/docs/api.clients.actions.rst index 3c7f47cd..ab7d0999 100644 --- a/docs/api.clients.actions.rst +++ b/docs/api.clients.actions.rst @@ -10,4 +10,3 @@ ActionsClient .. autoclass:: hcloud.actions.domain.Action :members: - diff --git a/docs/api.clients.certificates.rst b/docs/api.clients.certificates.rst index 18c5e929..b5ec8500 100644 --- a/docs/api.clients.certificates.rst +++ b/docs/api.clients.certificates.rst @@ -10,4 +10,3 @@ CertificateClient .. autoclass:: hcloud.certificates.domain.Certificate :members: - diff --git a/docs/api.clients.datacenters.rst b/docs/api.clients.datacenters.rst index fc7f9c9f..9354313e 100644 --- a/docs/api.clients.datacenters.rst +++ b/docs/api.clients.datacenters.rst @@ -13,4 +13,3 @@ DatacentersClient .. autoclass:: hcloud.datacenters.domain.DatacenterServerTypes :members: - diff --git a/docs/api.clients.firewalls.rst b/docs/api.clients.firewalls.rst index 81ee6658..ad7358a6 100644 --- a/docs/api.clients.firewalls.rst +++ b/docs/api.clients.firewalls.rst @@ -19,4 +19,3 @@ FirewallsClient .. autoclass:: hcloud.firewalls.domain.CreateFirewallResponse :members: - diff --git a/docs/api.clients.images.rst b/docs/api.clients.images.rst index 78142df1..48833aba 100644 --- a/docs/api.clients.images.rst +++ b/docs/api.clients.images.rst @@ -13,4 +13,3 @@ ImagesClient .. autoclass:: hcloud.images.domain.CreateImageResponse :members: - diff --git a/docs/api.clients.isos.rst b/docs/api.clients.isos.rst index 0196fade..b3a71338 100644 --- a/docs/api.clients.isos.rst +++ b/docs/api.clients.isos.rst @@ -10,4 +10,3 @@ ISOsClient .. autoclass:: hcloud.isos.domain.Iso :members: - diff --git a/docs/api.clients.locations.rst b/docs/api.clients.locations.rst index 3273b0ef..90012f6b 100644 --- a/docs/api.clients.locations.rst +++ b/docs/api.clients.locations.rst @@ -10,4 +10,3 @@ LocationsClient .. autoclass:: hcloud.locations.domain.Location :members: - diff --git a/docs/api.clients.primary_ips.rst b/docs/api.clients.primary_ips.rst index 237e2237..3d326660 100644 --- a/docs/api.clients.primary_ips.rst +++ b/docs/api.clients.primary_ips.rst @@ -10,4 +10,3 @@ PrimaryIPsClient .. autoclass:: hcloud.primary_ips.domain.PrimaryIP :members: - diff --git a/docs/api.clients.server_types.rst b/docs/api.clients.server_types.rst index 503bc888..90cee18c 100644 --- a/docs/api.clients.server_types.rst +++ b/docs/api.clients.server_types.rst @@ -9,4 +9,3 @@ ServerTypesClient .. autoclass:: hcloud.server_types.domain.ServerType :members: - diff --git a/docs/api.clients.ssh_keys.rst b/docs/api.clients.ssh_keys.rst index fbbe3c5b..97d6e73a 100644 --- a/docs/api.clients.ssh_keys.rst +++ b/docs/api.clients.ssh_keys.rst @@ -10,4 +10,3 @@ SSHKeysClient .. autoclass:: hcloud.ssh_keys.domain.SSHKey :members: - diff --git a/docs/api.helpers.rst b/docs/api.helpers.rst index 93699443..6121a11a 100644 --- a/docs/api.helpers.rst +++ b/docs/api.helpers.rst @@ -4,4 +4,3 @@ Helpers .. autoclass:: hcloud.helpers.labels.LabelValidator :members: - diff --git a/docs/samples.rst b/docs/samples.rst index 603cfc49..8f2d1203 100644 --- a/docs/samples.rst +++ b/docs/samples.rst @@ -66,4 +66,4 @@ To use Hetzner Cloud Python in a project: server2.power_off() -More samples are in the repository: https://github.com/hetznercloud/hcloud-python/tree/master/examples. \ No newline at end of file +More samples are in the repository: https://github.com/hetznercloud/hcloud-python/tree/master/examples. diff --git a/examples/usage_procedurale.py b/examples/usage_procedurale.py index b49f7804..5f25e0c0 100644 --- a/examples/usage_procedurale.py +++ b/examples/usage_procedurale.py @@ -1,8 +1,7 @@ from hcloud import Client - from hcloud.images.domain import Image -from hcloud.servers.domain import Server from hcloud.server_types.domain import ServerType +from hcloud.servers.domain import Server from hcloud.volumes.domain import Volume client = Client(token="project-token") diff --git a/hcloud/__init__.py b/hcloud/__init__.py index 5ef06e2f..e6731233 100644 --- a/hcloud/__init__.py +++ b/hcloud/__init__.py @@ -1 +1 @@ -from .hcloud import Client, APIException # noqa +from .hcloud import APIException, Client # noqa diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index 02caa6e5..b5075287 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -1,7 +1,7 @@ import time -from hcloud.core.client import ClientEntityBase, BoundModelBase from hcloud.actions.domain import Action, ActionFailedException, ActionTimeoutException +from hcloud.core.client import BoundModelBase, ClientEntityBase class BoundAction(BoundModelBase): @@ -38,9 +38,7 @@ def get_by_id(self, id): :return: :class:`BoundAction ` """ - response = self._client.request( - url="/actions/{action_id}".format(action_id=id), method="GET" - ) + response = self._client.request(url=f"/actions/{id}", method="GET") return BoundAction(self, response["action"]) def get_list( @@ -89,4 +87,4 @@ def get_all(self, status=None, sort=None): 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 `] """ - return super(ActionsClient, self).get_all(status=status, sort=sort) + return super().get_all(status=status, sort=sort) diff --git a/hcloud/certificates/client.py b/hcloud/certificates/client.py index c973795d..5d673f9c 100644 --- a/hcloud/certificates/client.py +++ b/hcloud/certificates/client.py @@ -1,12 +1,11 @@ from hcloud.actions.client import BoundAction -from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin - from hcloud.certificates.domain import ( Certificate, CreateManagedCertificateResponse, - ManagedCertificateStatus, ManagedCertificateError, + ManagedCertificateStatus, ) +from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.core.domain import add_meta_to_result @@ -25,7 +24,7 @@ def __init__(self, client, data, complete=True): data["status"] = ManagedCertificateStatus( issuance=status["issuance"], renewal=status["renewal"], error=error ) - super(BoundCertificate, self).__init__(client, data, complete) + 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]] @@ -92,9 +91,7 @@ def get_by_id(self, id): :param id: int :return: :class:`BoundCertificate ` """ - response = self._client.request( - url="/certificates/{certificate_id}".format(certificate_id=id), method="GET" - ) + response = self._client.request(url=f"/certificates/{id}", method="GET") return BoundCertificate(self, response["certificate"]) def get_list( @@ -151,9 +148,7 @@ def get_all(self, name=None, label_selector=None): Can be used to filter certificates by labels. The response will only contain certificates matching the label selector. :return: List[:class:`BoundCertificate `] """ - return super(CertificatesClient, self).get_all( - name=name, label_selector=label_selector - ) + return super().get_all(name=name, label_selector=label_selector) def get_by_name(self, name): # type: (str) -> BoundCertificate @@ -163,7 +158,7 @@ def get_by_name(self, name): Used to get certificate by name. :return: :class:`BoundCertificate ` """ - return super(CertificatesClient, self).get_by_name(name) + return super().get_by_name(name) def create(self, name, certificate, private_key, labels=None): # type: (str, str, str, Optional[Dict[str, str]]) -> BoundCertificate @@ -232,7 +227,7 @@ def update(self, certificate, name=None, labels=None): if labels is not None: data["labels"] = labels response = self._client.request( - url="/certificates/{certificate_id}".format(certificate_id=certificate.id), + url=f"/certificates/{certificate.id}", method="PUT", json=data, ) @@ -241,7 +236,7 @@ def update(self, certificate, name=None, labels=None): def delete(self, certificate): # type: (Certificate) -> bool self._client.request( - url="/certificates/{certificate_id}".format(certificate_id=certificate.id), + url=f"/certificates/{certificate.id}", method="DELETE", ) """Deletes a certificate. @@ -303,9 +298,7 @@ def get_actions(self, certificate, status=None, sort=None): 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 `] """ - return super(CertificatesClient, self).get_actions( - certificate, status=status, sort=sort - ) + return super().get_actions(certificate, status=status, sort=sort) def retry_issuance(self, certificate): # type: (Certificate) -> BoundAction diff --git a/hcloud/core/client.py b/hcloud/core/client.py index 319f6368..d3e1f43f 100644 --- a/hcloud/core/client.py +++ b/hcloud/core/client.py @@ -1,7 +1,7 @@ from hcloud.core.domain import add_meta_to_result -class ClientEntityBase(object): +class ClientEntityBase: max_per_page = 50 results_list_attribute_name = None @@ -77,7 +77,7 @@ def get_actions(self, *args, **kwargs): return self._get_all(self.get_actions_list, "actions", *args, **kwargs) -class GetEntityByNameMixin(object): +class GetEntityByNameMixin: """ Use as a mixin for ClientEntityBase classes """ @@ -91,7 +91,7 @@ def get_by_name(self, name): return entity -class BoundModelBase(object): +class BoundModelBase: """Bound Model Base""" model = None diff --git a/hcloud/core/domain.py b/hcloud/core/domain.py index a58765fa..8d99f63a 100644 --- a/hcloud/core/domain.py +++ b/hcloud/core/domain.py @@ -1,7 +1,7 @@ from collections import namedtuple -class BaseDomain(object): +class BaseDomain: __slots__ = () @classmethod @@ -10,7 +10,7 @@ def from_dict(cls, data): return cls(**supported_data) -class DomainIdentityMixin(object): +class DomainIdentityMixin: __slots__ = () @property @@ -51,7 +51,6 @@ def __init__( class Meta(BaseDomain): - __slots__ = ("pagination",) def __init__(self, pagination=None): @@ -71,6 +70,6 @@ def parse_meta(cls, json_content): def add_meta_to_result(result, json_content, attr_name): # type: (List[BoundModelBase], json, string) -> PageResult - class_name = "PageResults{0}".format(attr_name.capitalize()) + 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)}) diff --git a/hcloud/datacenters/client.py b/hcloud/datacenters/client.py index 68c7a180..2f839e86 100644 --- a/hcloud/datacenters/client.py +++ b/hcloud/datacenters/client.py @@ -1,5 +1,4 @@ -from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin - +from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.datacenters.domain import Datacenter, DatacenterServerTypes from hcloud.locations.client import BoundLocation from hcloud.server_types.client import BoundServerType @@ -39,7 +38,7 @@ def __init__(self, client, data): available_for_migration=available_for_migration, ) - super(BoundDatacenter, self).__init__(client, data) + super().__init__(client, data) class DatacentersClient(ClientEntityBase, GetEntityByNameMixin): @@ -52,9 +51,7 @@ def get_by_id(self, id): :param id: int :return: :class:`BoundDatacenter ` """ - response = self._client.request( - url="/datacenters/{datacenter_id}".format(datacenter_id=id), method="GET" - ) + response = self._client.request(url=f"/datacenters/{id}", method="GET") return BoundDatacenter(self, response["datacenter"]) def get_list( @@ -101,7 +98,7 @@ def get_all(self, name=None): Can be used to filter datacenters by their name. :return: List[:class:`BoundDatacenter `] """ - return super(DatacentersClient, self).get_all(name=name) + return super().get_all(name=name) def get_by_name(self, name): # type: (str) -> BoundDatacenter @@ -111,4 +108,4 @@ def get_by_name(self, name): Used to get datacenter by name. :return: :class:`BoundDatacenter ` """ - return super(DatacentersClient, self).get_by_name(name) + return super().get_by_name(name) diff --git a/hcloud/firewalls/client.py b/hcloud/firewalls/client.py index 6a14a4e0..5e7cf741 100644 --- a/hcloud/firewalls/client.py +++ b/hcloud/firewalls/client.py @@ -1,13 +1,12 @@ from hcloud.actions.client import BoundAction from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.core.domain import add_meta_to_result - from hcloud.firewalls.domain import ( - Firewall, CreateFirewallResponse, - FirewallRule, + Firewall, FirewallResource, FirewallResourceLabelSelector, + FirewallRule, ) @@ -56,7 +55,7 @@ def __init__(self, client, data, complete=True): ) data["applied_to"] = ats - super(BoundFirewall, self).__init__(client, data, complete) + 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] @@ -168,7 +167,7 @@ def get_actions_list( if per_page is not None: params["per_page"] = per_page response = self._client.request( - url="/firewalls/{firewall_id}/actions".format(firewall_id=firewall.id), + url=f"/firewalls/{firewall.id}/actions", method="GET", params=params, ) @@ -195,9 +194,7 @@ def get_actions( :return: List[:class:`BoundAction `] """ - return super(FirewallsClient, self).get_actions( - firewall, status=status, sort=sort - ) + return super().get_actions(firewall, status=status, sort=sort) def get_by_id(self, id): # type: (int) -> BoundFirewall @@ -206,9 +203,7 @@ def get_by_id(self, id): :param id: int :return: :class:`BoundFirewall ` """ - response = self._client.request( - url="/firewalls/{firewall_id}".format(firewall_id=id), method="GET" - ) + response = self._client.request(url=f"/firewalls/{id}", method="GET") return BoundFirewall(self, response["firewall"]) def get_list( @@ -266,9 +261,7 @@ def get_all(self, label_selector=None, name=None, sort=None): Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) :return: List[:class:`BoundFirewall `] """ - return super(FirewallsClient, self).get_all( - label_selector=label_selector, name=name, sort=sort - ) + return super().get_all(label_selector=label_selector, name=name, sort=sort) def get_by_name(self, name): # type: (str) -> BoundFirewall @@ -278,7 +271,7 @@ def get_by_name(self, name): Used to get Firewall by name. :return: :class:`BoundFirewall ` """ - return super(FirewallsClient, self).get_by_name(name) + return super().get_by_name(name) def create( self, @@ -342,7 +335,7 @@ def update(self, firewall, labels=None, name=None): data["name"] = name response = self._client.request( - url="/firewalls/{firewall_id}".format(firewall_id=firewall.id), + url=f"/firewalls/{firewall.id}", method="PUT", json=data, ) @@ -356,7 +349,7 @@ def delete(self, firewall): :return: boolean """ self._client.request( - url="/firewalls/{firewall_id}".format(firewall_id=firewall.id), + url=f"/firewalls/{firewall.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 diff --git a/hcloud/floating_ips/client.py b/hcloud/floating_ips/client.py index 37670176..e930e357 100644 --- a/hcloud/floating_ips/client.py +++ b/hcloud/floating_ips/client.py @@ -1,8 +1,7 @@ from hcloud.actions.client import BoundAction from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.core.domain import add_meta_to_result - -from hcloud.floating_ips.domain import FloatingIP, CreateFloatingIPResponse +from hcloud.floating_ips.domain import CreateFloatingIPResponse, FloatingIP from hcloud.locations.client import BoundLocation @@ -24,7 +23,7 @@ def __init__(self, client, data, complete=True): client._client.locations, home_location ) - super(BoundFloatingIP, self).__init__(client, data, complete) + 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] @@ -182,9 +181,7 @@ def get_actions( :return: List[:class:`BoundAction `] """ - return super(FloatingIPsClient, self).get_actions( - floating_ip, status=status, sort=sort - ) + return super().get_actions(floating_ip, status=status, sort=sort) def get_by_id(self, id): # type: (int) -> BoundFloatingIP @@ -193,9 +190,7 @@ def get_by_id(self, id): :param id: int :return: :class:`BoundFloatingIP ` """ - response = self._client.request( - url="/floating_ips/{floating_ip_id}".format(floating_ip_id=id), method="GET" - ) + response = self._client.request(url=f"/floating_ips/{id}", method="GET") return BoundFloatingIP(self, response["floating_ip"]) def get_list( @@ -249,9 +244,7 @@ def get_all(self, label_selector=None, name=None): Can be used to filter networks by their name. :return: List[:class:`BoundFloatingIP `] """ - return super(FloatingIPsClient, self).get_all( - label_selector=label_selector, name=name - ) + return super().get_all(label_selector=label_selector, name=name) def get_by_name(self, name): # type: (str) -> BoundFloatingIP @@ -261,7 +254,7 @@ def get_by_name(self, name): Used to get Floating IP by name. :return: :class:`BoundFloatingIP ` """ - return super(FloatingIPsClient, self).get_by_name(name) + return super().get_by_name(name) def create( self, @@ -333,7 +326,7 @@ def update(self, floating_ip, description=None, labels=None, name=None): data["name"] = name response = self._client.request( - url="/floating_ips/{floating_ip_id}".format(floating_ip_id=floating_ip.id), + url=f"/floating_ips/{floating_ip.id}", method="PUT", json=data, ) @@ -347,7 +340,7 @@ def delete(self, floating_ip): :return: boolean """ self._client.request( - url="/floating_ips/{floating_ip_id}".format(floating_ip_id=floating_ip.id), + url=f"/floating_ips/{floating_ip.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 diff --git a/hcloud/hcloud.py b/hcloud/hcloud.py index ef6b57f8..1ce53cb8 100644 --- a/hcloud/hcloud.py +++ b/hcloud/hcloud.py @@ -1,22 +1,23 @@ import time + import requests from hcloud.actions.client import ActionsClient from hcloud.certificates.client import CertificatesClient +from hcloud.datacenters.client import DatacentersClient from hcloud.floating_ips.client import FloatingIPsClient -from hcloud.primary_ips.client import PrimaryIPsClient -from hcloud.networks.client import NetworksClient +from hcloud.images.client import ImagesClient from hcloud.isos.client import IsosClient -from hcloud.servers.client import ServersClient +from hcloud.load_balancer_types.client import LoadBalancerTypesClient +from hcloud.load_balancers.client import LoadBalancersClient +from hcloud.locations.client import LocationsClient +from hcloud.networks.client import NetworksClient +from hcloud.placement_groups.client import PlacementGroupsClient +from hcloud.primary_ips.client import PrimaryIPsClient from hcloud.server_types.client import ServerTypesClient +from hcloud.servers.client import ServersClient from hcloud.ssh_keys.client import SSHKeysClient from hcloud.volumes.client import VolumesClient -from hcloud.images.client import ImagesClient -from hcloud.locations.client import LocationsClient -from hcloud.datacenters.client import DatacentersClient -from hcloud.load_balancers.client import LoadBalancersClient -from hcloud.load_balancer_types.client import LoadBalancerTypesClient -from hcloud.placement_groups.client import PlacementGroupsClient from .__version__ import VERSION from .firewalls.client import FirewallsClient @@ -34,7 +35,7 @@ def __str__(self): return self.message -class Client(object): +class Client: """Base Client for accessing the Hetzner Cloud API""" _version = VERSION @@ -186,10 +187,9 @@ def _get_user_agent(self): ) def _get_headers(self): - headers = { "User-Agent": self._get_user_agent(), - "Authorization": "Bearer {token}".format(token=self.token), + "Authorization": f"Bearer {self.token}", } return headers diff --git a/hcloud/helpers/labels.py b/hcloud/helpers/labels.py index c9f45a97..515982f0 100644 --- a/hcloud/helpers/labels.py +++ b/hcloud/helpers/labels.py @@ -4,10 +4,10 @@ class LabelValidator: KEY_REGEX = re.compile( - "^([a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]){0,253}[a-z0-9A-Z])?/)?[a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]|){0,61}[a-z0-9A-Z])?$" + r"^([a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]){0,253}[a-z0-9A-Z])?/)?[a-z0-9A-Z]((?:[\-_.]|[a-z0-9A-Z]|){0,61}[a-z0-9A-Z])?$" ) VALUE_REGEX = re.compile( - "^(([a-z0-9A-Z](?:[\-_.]|[a-z0-9A-Z]){0,61})?[a-z0-9A-Z]$|$)" + r"^(([a-z0-9A-Z](?:[\-_.]|[a-z0-9A-Z]){0,61})?[a-z0-9A-Z]$|$)" ) @staticmethod diff --git a/hcloud/images/client.py b/hcloud/images/client.py index d05d9dc4..8992ac38 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -1,7 +1,6 @@ from hcloud.actions.client import BoundAction from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.core.domain import add_meta_to_result - from hcloud.images.domain import Image @@ -22,7 +21,7 @@ def __init__(self, client, data): client._client.servers, {"id": bound_to}, complete=False ) - super(BoundImage, self).__init__(client, data) + 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] @@ -123,7 +122,7 @@ def get_actions_list( if per_page is not None: params["per_page"] = per_page response = self._client.request( - url="/images/{image_id}/actions".format(image_id=image.id), + url=f"/images/{image.id}/actions", method="GET", params=params, ) @@ -149,7 +148,7 @@ def get_actions( 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 `] """ - return super(ImagesClient, self).get_actions(image, sort=sort, status=status) + return super().get_actions(image, sort=sort, status=status) def get_by_id(self, id): # type: (int) -> BoundImage @@ -158,9 +157,7 @@ def get_by_id(self, id): :param id: int :return: :class:`BoundImage `] """ - return super(ImagesClient, self).get_all( + return super().get_all( name=name, label_selector=label_selector, bound_to=bound_to, @@ -280,7 +277,7 @@ def get_by_name(self, name): Used to get image by name. :return: :class:`BoundImage ` """ - return super(ImagesClient, self).get_by_name(name) + return super().get_by_name(name) def get_by_name_and_architecture(self, name, architecture): # type: (str, str) -> BoundImage @@ -319,7 +316,7 @@ def update(self, image, description=None, type=None, labels=None): if labels is not None: data.update({"labels": labels}) response = self._client.request( - url="/images/{image_id}".format(image_id=image.id), method="PUT", json=data + url=f"/images/{image.id}", method="PUT", json=data ) return BoundImage(self, response["image"]) @@ -330,9 +327,7 @@ def delete(self, image): :param :class:`BoundImage ` or :class:`Image ` :return: bool """ - self._client.request( - url="/images/{image_id}".format(image_id=image.id), method="DELETE" - ) + self._client.request(url=f"/images/{image.id}", method="DELETE") # Return allays true, because the API does not return an action for it. When an error occurs a APIException will be raised return True diff --git a/hcloud/isos/client.py b/hcloud/isos/client.py index c28b632a..a7876dc7 100644 --- a/hcloud/isos/client.py +++ b/hcloud/isos/client.py @@ -1,5 +1,4 @@ from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin - from hcloud.isos.domain import Iso @@ -17,9 +16,7 @@ def get_by_id(self, id): :param id: int :return: :class:`BoundIso ` """ - response = self._client.request( - url="/isos/{iso_id}".format(iso_id=id), method="GET" - ) + response = self._client.request(url=f"/isos/{id}", method="GET") return BoundIso(self, response["iso"]) def get_list( @@ -80,7 +77,7 @@ def get_all( architecture and also want custom ISOs. :return: List[:class:`BoundIso `] """ - return super(IsosClient, self).get_all( + return super().get_all( name=name, architecture=architecture, include_wildcard_architecture=include_wildcard_architecture, @@ -94,4 +91,4 @@ def get_by_name(self, name): Used to get iso by name. :return: :class:`BoundIso ` """ - return super(IsosClient, self).get_by_name(name) + return super().get_by_name(name) diff --git a/hcloud/load_balancer_types/client.py b/hcloud/load_balancer_types/client.py index 341650a4..1b505a08 100644 --- a/hcloud/load_balancer_types/client.py +++ b/hcloud/load_balancer_types/client.py @@ -1,4 +1,4 @@ -from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin +from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.load_balancer_types.domain import LoadBalancerType @@ -61,7 +61,7 @@ def get_all(self, name=None): Can be used to filter Load Balancer type by their name. :return: List[:class:`BoundLoadBalancerType `] """ - return super(LoadBalancerTypesClient, self).get_all(name=name) + return super().get_all(name=name) def get_by_name(self, name): # type: (str) -> BoundLoadBalancerType @@ -71,4 +71,4 @@ def get_by_name(self, name): Used to get Load Balancer type by name. :return: :class:`BoundLoadBalancerType ` """ - return super(LoadBalancerTypesClient, self).get_by_name(name) + return super().get_by_name(name) diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index cb7ea3a4..7b905ddf 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -1,30 +1,27 @@ +from hcloud.actions.client import BoundAction from hcloud.certificates.client import BoundCertificate -from hcloud.servers.client import BoundServer - -from hcloud.load_balancer_types.client import BoundLoadBalancerType -from hcloud.locations.client import BoundLocation -from hcloud.networks.client import BoundNetwork - -from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin +from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.core.domain import add_meta_to_result - -from hcloud.actions.client import BoundAction +from hcloud.load_balancer_types.client import BoundLoadBalancerType from hcloud.load_balancers.domain import ( - LoadBalancer, + CreateLoadBalancerResponse, IPv4Address, IPv6Network, - PublicNetwork, - PrivateNet, - CreateLoadBalancerResponse, - LoadBalancerTarget, + LoadBalancer, + LoadBalancerAlgorithm, + LoadBalancerHealtCheckHttp, + LoadBalancerHealthCheck, LoadBalancerService, LoadBalancerServiceHttp, - LoadBalancerHealthCheck, - LoadBalancerHealtCheckHttp, - LoadBalancerAlgorithm, - LoadBalancerTargetLabelSelector, + LoadBalancerTarget, LoadBalancerTargetIP, + LoadBalancerTargetLabelSelector, + PrivateNet, + PublicNetwork, ) +from hcloud.locations.client import BoundLocation +from hcloud.networks.client import BoundNetwork +from hcloud.servers.client import BoundServer class BoundLoadBalancer(BoundModelBase): @@ -132,7 +129,7 @@ def __init__(self, client, data, complete=True): if location is not None: data["location"] = BoundLocation(client._client.locations, location) - super(BoundLoadBalancer, self).__init__(client, data, complete) + super().__init__(client, data, complete) def update(self, name=None, labels=None): # type: (Optional[str], Optional[Dict[str, str]]) -> BoundLoadBalancer @@ -322,7 +319,7 @@ def get_by_id(self, id): :return: :class:`BoundLoadBalancer ` """ response = self._client.request( - url="/load_balancers/{load_balancer_id}".format(load_balancer_id=id), + url=f"/load_balancers/{id}", method="GET", ) return BoundLoadBalancer(self, response["load_balancer"]) @@ -377,9 +374,7 @@ def get_all(self, name=None, label_selector=None): Can be used to filter Load Balancers by labels. The response will only contain Load Balancers matching the label selector. :return: List[:class:`BoundLoadBalancer `] """ - return super(LoadBalancersClient, self).get_all( - name=name, label_selector=label_selector - ) + return super().get_all(name=name, label_selector=label_selector) def get_by_name(self, name): # type: (str) -> BoundLoadBalancer @@ -389,7 +384,7 @@ def get_by_name(self, name): Used to get Load Balancer by name. :return: :class:`BoundLoadBalancer ` """ - return super(LoadBalancersClient, self).get_by_name(name) + return super().get_by_name(name) def create( self, @@ -566,9 +561,7 @@ def get_actions(self, load_balancer, status=None, sort=None): 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 `] """ - return super(LoadBalancersClient, self).get_actions( - load_balancer, status=status, sort=sort - ) + return super().get_actions(load_balancer, status=status, sort=sort) def add_service(self, load_balancer, service): # type: (Union[LoadBalancer, BoundLoadBalancer], LoadBalancerService) -> List[BoundAction] diff --git a/hcloud/locations/client.py b/hcloud/locations/client.py index 79cd53f6..03aebe2b 100644 --- a/hcloud/locations/client.py +++ b/hcloud/locations/client.py @@ -1,5 +1,4 @@ -from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin - +from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.locations.domain import Location @@ -17,9 +16,7 @@ def get_by_id(self, id): :param id: int :return: :class:`BoundLocation ` """ - response = self._client.request( - url="/locations/{location_id}".format(location_id=id), method="GET" - ) + 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): @@ -57,7 +54,7 @@ def get_all(self, name=None): Can be used to filter locations by their name. :return: List[:class:`BoundLocation `] """ - return super(LocationsClient, self).get_all(name=name) + return super().get_all(name=name) def get_by_name(self, name): # type: (str) -> BoundLocation @@ -67,4 +64,4 @@ def get_by_name(self, name): Used to get location by name. :return: :class:`BoundLocation ` """ - return super(LocationsClient, self).get_by_name(name) + return super().get_by_name(name) diff --git a/hcloud/networks/client.py b/hcloud/networks/client.py index 9cc369ec..b0be745e 100644 --- a/hcloud/networks/client.py +++ b/hcloud/networks/client.py @@ -1,7 +1,6 @@ -from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin -from hcloud.core.domain import add_meta_to_result - from hcloud.actions.client import BoundAction +from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from hcloud.core.domain import add_meta_to_result from hcloud.networks.domain import Network, NetworkRoute, NetworkSubnet @@ -29,7 +28,7 @@ def __init__(self, client, data, complete=True): ] data["servers"] = servers - super(BoundNetwork, self).__init__(client, data, complete) + super().__init__(client, data, complete) def update(self, name=None, labels=None): # type: (Optional[str], Optional[Dict[str, str]]) -> BoundNetwork @@ -150,9 +149,7 @@ def get_by_id(self, id): :param id: int :return: :class:`BoundNetwork """ - response = self._client.request( - url="/networks/{network_id}".format(network_id=id), method="GET" - ) + response = self._client.request(url=f"/networks/{id}", method="GET") return BoundNetwork(self, response["network"]) def get_list( @@ -202,9 +199,7 @@ def get_all(self, name=None, label_selector=None): Can be used to filter networks by labels. The response will only contain networks matching the label selector. :return: List[:class:`BoundNetwork `] """ - return super(NetworksClient, self).get_all( - name=name, label_selector=label_selector - ) + return super().get_all(name=name, label_selector=label_selector) def get_by_name(self, name): # type: (str) -> BoundNetwork @@ -214,7 +209,7 @@ def get_by_name(self, name): Used to get network by name. :return: :class:`BoundNetwork ` """ - return super(NetworksClient, self).get_by_name(name) + return super().get_by_name(name) def create( self, @@ -277,7 +272,7 @@ def update(self, network, name=None, labels=None): if labels is not None: data.update({"labels": labels}) response = self._client.request( - url="/networks/{network_id}".format(network_id=network.id), + url=f"/networks/{network.id}", method="PUT", json=data, ) @@ -290,9 +285,7 @@ def delete(self, network): :param network: :class:`BoundNetwork ` or :class:`Network ` :return: boolean """ - self._client.request( - url="/networks/{network_id}".format(network_id=network.id), method="DELETE" - ) + self._client.request(url=f"/networks/{network.id}", method="DELETE") return True def get_actions_list( @@ -323,7 +316,7 @@ def get_actions_list( params["per_page"] = per_page response = self._client.request( - url="/networks/{network_id}/actions".format(network_id=network.id), + url=f"/networks/{network.id}/actions", method="GET", params=params, ) @@ -344,9 +337,7 @@ def get_actions(self, network, status=None, sort=None): 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 `] """ - return super(NetworksClient, self).get_actions( - network, status=status, sort=sort - ) + return super().get_actions(network, status=status, sort=sort) def add_subnet(self, network, subnet): # type: (Union[Network, BoundNetwork], NetworkSubnet) -> List[BoundAction] diff --git a/hcloud/placement_groups/client.py b/hcloud/placement_groups/client.py index 81426d10..4965ed96 100644 --- a/hcloud/placement_groups/client.py +++ b/hcloud/placement_groups/client.py @@ -1,7 +1,6 @@ from hcloud.actions.client import BoundAction from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin - -from hcloud.placement_groups.domain import PlacementGroup, CreatePlacementGroupResponse +from hcloud.placement_groups.domain import CreatePlacementGroupResponse, PlacementGroup class BoundPlacementGroup(BoundModelBase): @@ -39,7 +38,7 @@ def get_by_id(self, id): :return: :class:`BoundPlacementGroup ` """ response = self._client.request( - url="/placement_groups/{placement_group_id}".format(placement_group_id=id), + url=f"/placement_groups/{id}", method="GET", ) return BoundPlacementGroup(self, response["placement_group"]) @@ -105,9 +104,7 @@ def get_all(self, label_selector=None, name=None, sort=None): Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) :return: List[:class:`BoundPlacementGroup `] """ - return super(PlacementGroupsClient, self).get_all( - label_selector=label_selector, name=name, sort=sort - ) + return super().get_all(label_selector=label_selector, name=name, sort=sort) def get_by_name(self, name): # type: (str) -> BoundPlacementGroup @@ -117,7 +114,7 @@ def get_by_name(self, name): Used to get Placement Group by name :return: class:`BoundPlacementGroup ` """ - return super(PlacementGroupsClient, self).get_by_name(name) + return super().get_by_name(name) def create( self, diff --git a/hcloud/primary_ips/__init__.py b/hcloud/primary_ips/__init__.py index 40a96afc..e69de29b 100644 --- a/hcloud/primary_ips/__init__.py +++ b/hcloud/primary_ips/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/hcloud/primary_ips/client.py b/hcloud/primary_ips/client.py index 7d8cb496..d984506c 100644 --- a/hcloud/primary_ips/client.py +++ b/hcloud/primary_ips/client.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- from hcloud.actions.client import BoundAction from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin - -from hcloud.primary_ips.domain import PrimaryIP, CreatePrimaryIPResponse +from hcloud.primary_ips.domain import CreatePrimaryIPResponse, PrimaryIP class BoundPrimaryIP(BoundModelBase): @@ -15,7 +13,7 @@ def __init__(self, client, data, complete=True): if datacenter: data["datacenter"] = BoundDatacenter(client._client.datacenters, datacenter) - super(BoundPrimaryIP, self).__init__(client, data, complete) + 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 @@ -94,9 +92,7 @@ def get_by_id(self, id): :param id: int :return: :class:`BoundPrimaryIP ` """ - response = self._client.request( - url="/primary_ips/{primary_ip_id}".format(primary_ip_id=id), method="GET" - ) + response = self._client.request(url=f"/primary_ips/{id}", method="GET") return BoundPrimaryIP(self, response["primary_ip"]) def get_list( @@ -153,9 +149,7 @@ def get_all(self, label_selector=None, name=None): Can be used to filter networks by their name. :return: List[:class:`BoundPrimaryIP `] """ - return super(PrimaryIPsClient, self).get_all( - label_selector=label_selector, name=name - ) + return super().get_all(label_selector=label_selector, name=name) def get_by_name(self, name): # type: (str) -> BoundPrimaryIP @@ -165,7 +159,7 @@ def get_by_name(self, name): Used to get Primary IP by name. :return: :class:`BoundPrimaryIP ` """ - return super(PrimaryIPsClient, self).get_by_name(name) + return super().get_by_name(name) def create( self, @@ -237,7 +231,7 @@ def update(self, primary_ip, auto_delete=None, labels=None, name=None): data["name"] = name response = self._client.request( - url="/primary_ips/{primary_ip_id}".format(primary_ip_id=primary_ip.id), + url=f"/primary_ips/{primary_ip.id}", method="PUT", json=data, ) @@ -251,7 +245,7 @@ def delete(self, primary_ip): :return: boolean """ self._client.request( - url="/primary_ips/{primary_ip_id}".format(primary_ip_id=primary_ip.id), + url=f"/primary_ips/{primary_ip.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 diff --git a/hcloud/primary_ips/domain.py b/hcloud/primary_ips/domain.py index 077c7106..e4741493 100644 --- a/hcloud/primary_ips/domain.py +++ b/hcloud/primary_ips/domain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from dateutil.parser import isoparse from hcloud.core.domain import BaseDomain diff --git a/hcloud/server_types/client.py b/hcloud/server_types/client.py index c2bbe365..4e646918 100644 --- a/hcloud/server_types/client.py +++ b/hcloud/server_types/client.py @@ -1,4 +1,4 @@ -from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin +from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.server_types.domain import ServerType @@ -16,9 +16,7 @@ def get_by_id(self, id): :param id: int :return: :class:`BoundServerType ` """ - response = self._client.request( - url="/server_types/{server_type_id}".format(server_type_id=id), method="GET" - ) + 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): @@ -58,7 +56,7 @@ def get_all(self, name=None): Can be used to filter server type by their name. :return: List[:class:`BoundServerType `] """ - return super(ServerTypesClient, self).get_all(name=name) + return super().get_all(name=name) def get_by_name(self, name): # type: (str) -> BoundServerType @@ -68,4 +66,4 @@ def get_by_name(self, name): Used to get Server type by name. :return: :class:`BoundServerType ` """ - return super(ServerTypesClient, self).get_by_name(name) + return super().get_by_name(name) diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index f52a0e53..b2f9269f 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -1,38 +1,36 @@ -from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin - from hcloud.actions.client import BoundAction +from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.core.domain import add_meta_to_result +from hcloud.datacenters.client import BoundDatacenter from hcloud.firewalls.client import BoundFirewall from hcloud.floating_ips.client import BoundFloatingIP +from hcloud.images.client import BoundImage +from hcloud.images.domain import CreateImageResponse from hcloud.isos.client import BoundIso +from hcloud.networks.client import BoundNetwork # noqa +from hcloud.networks.domain import Network # noqa +from hcloud.placement_groups.client import BoundPlacementGroup from hcloud.primary_ips.client import BoundPrimaryIP +from hcloud.server_types.client import BoundServerType from hcloud.servers.domain import ( - Server, CreateServerResponse, - ResetPasswordResponse, EnableRescueResponse, - RequestConsoleResponse, - PublicNetwork, IPv4Address, IPv6Network, PrivateNet, + PublicNetwork, PublicNetworkFirewall, + RequestConsoleResponse, + ResetPasswordResponse, + Server, ) from hcloud.volumes.client import BoundVolume -from hcloud.images.domain import CreateImageResponse -from hcloud.images.client import BoundImage -from hcloud.server_types.client import BoundServerType -from hcloud.datacenters.client import BoundDatacenter -from hcloud.networks.client import BoundNetwork # noqa -from hcloud.networks.domain import Network # noqa -from hcloud.placement_groups.client import BoundPlacementGroup class BoundServer(BoundModelBase): model = Server def __init__(self, client, data, complete=True): - datacenter = data.get("datacenter") if datacenter is not None: data["datacenter"] = BoundDatacenter(client._client.datacenters, datacenter) @@ -137,7 +135,7 @@ def __init__(self, client, data, complete=True): ) data["placement_group"] = placement_group - super(BoundServer, self).__init__(client, data, complete) + 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]] @@ -419,9 +417,7 @@ def get_by_id(self, id): :param id: int :return: :class:`BoundServer ` """ - response = self._client.request( - url="/servers/{server_id}".format(server_id=id), method="GET" - ) + response = self._client.request(url=f"/servers/{id}", method="GET") return BoundServer(self, response["server"]) def get_list( @@ -478,9 +474,7 @@ def get_all(self, name=None, label_selector=None, status=None): Can be used to filter servers by their status. The response will only contain servers matching the status. :return: List[:class:`BoundServer `] """ - return super(ServersClient, self).get_all( - name=name, label_selector=label_selector, status=status - ) + return super().get_all(name=name, label_selector=label_selector, status=status) def get_by_name(self, name): # type: (str) -> BoundServer @@ -490,7 +484,7 @@ def get_by_name(self, name): Used to get server by name. :return: :class:`BoundServer ` """ - return super(ServersClient, self).get_by_name(name) + return super().get_by_name(name) def create( self, @@ -621,7 +615,7 @@ def get_actions_list( params["per_page"] = per_page response = self._client.request( - url="/servers/{server_id}/actions".format(server_id=server.id), + url=f"/servers/{server.id}/actions", method="GET", params=params, ) @@ -642,7 +636,7 @@ def get_actions(self, server, status=None, sort=None): 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 `] """ - return super(ServersClient, self).get_actions(server, status=status, sort=sort) + return super().get_actions(server, status=status, sort=sort) def update(self, server, name=None, labels=None): # type:(Server, Optional[str], Optional[Dict[str, str]]) -> BoundServer @@ -661,7 +655,7 @@ def update(self, server, name=None, labels=None): if labels is not None: data.update({"labels": labels}) response = self._client.request( - url="/servers/{server_id}".format(server_id=server.id), + url=f"/servers/{server.id}", method="PUT", json=data, ) @@ -674,9 +668,7 @@ def delete(self, server): :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ - response = self._client.request( - url="/servers/{server_id}".format(server_id=server.id), method="DELETE" - ) + response = self._client.request(url=f"/servers/{server.id}", method="DELETE") return BoundAction(self._client.actions, response["action"]) def power_off(self, server): @@ -687,7 +679,7 @@ def power_off(self, server): :return: :class:`BoundAction ` """ response = self._client.request( - url="/servers/{server_id}/actions/poweroff".format(server_id=server.id), + url=f"/servers/{server.id}/actions/poweroff", method="POST", ) return BoundAction(self._client.actions, response["action"]) @@ -700,7 +692,7 @@ def power_on(self, server): :return: :class:`BoundAction ` """ response = self._client.request( - url="/servers/{server_id}/actions/poweron".format(server_id=server.id), + url=f"/servers/{server.id}/actions/poweron", method="POST", ) return BoundAction(self._client.actions, response["action"]) @@ -713,7 +705,7 @@ def reboot(self, server): :return: :class:`BoundAction ` """ response = self._client.request( - url="/servers/{server_id}/actions/reboot".format(server_id=server.id), + url=f"/servers/{server.id}/actions/reboot", method="POST", ) return BoundAction(self._client.actions, response["action"]) @@ -726,7 +718,7 @@ def reset(self, server): :return: :class:`BoundAction ` """ response = self._client.request( - url="/servers/{server_id}/actions/reset".format(server_id=server.id), + url=f"/servers/{server.id}/actions/reset", method="POST", ) return BoundAction(self._client.actions, response["action"]) @@ -739,7 +731,7 @@ def shutdown(self, server): :return: :class:`BoundAction ` """ response = self._client.request( - url="/servers/{server_id}/actions/shutdown".format(server_id=server.id), + url=f"/servers/{server.id}/actions/shutdown", method="POST", ) return BoundAction(self._client.actions, response["action"]) @@ -775,7 +767,7 @@ def change_type(self, server, server_type, upgrade_disk): """ data = {"server_type": server_type.id_or_name, "upgrade_disk": upgrade_disk} response = self._client.request( - url="/servers/{server_id}/actions/change_type".format(server_id=server.id), + url=f"/servers/{server.id}/actions/change_type", method="POST", json=data, ) @@ -849,7 +841,7 @@ def create_image(self, server, description=None, type=None, labels=None): data.update({"labels": labels}) response = self._client.request( - url="/servers/{server_id}/actions/create_image".format(server_id=server.id), + url=f"/servers/{server.id}/actions/create_image", method="POST", json=data, ) @@ -868,7 +860,7 @@ def rebuild(self, server, image): """ data = {"image": image.id_or_name} response = self._client.request( - url="/servers/{server_id}/actions/rebuild".format(server_id=server.id), + url=f"/servers/{server.id}/actions/rebuild", method="POST", json=data, ) @@ -914,7 +906,7 @@ def attach_iso(self, server, iso): """ data = {"iso": iso.id_or_name} response = self._client.request( - url="/servers/{server_id}/actions/attach_iso".format(server_id=server.id), + url=f"/servers/{server.id}/actions/attach_iso", method="POST", json=data, ) @@ -928,7 +920,7 @@ def detach_iso(self, server): :return: :class:`BoundAction ` """ response = self._client.request( - url="/servers/{server_id}/actions/detach_iso".format(server_id=server.id), + url=f"/servers/{server.id}/actions/detach_iso", method="POST", ) return BoundAction(self._client.actions, response["action"]) diff --git a/hcloud/ssh_keys/client.py b/hcloud/ssh_keys/client.py index d9c93e1e..2ecf642e 100644 --- a/hcloud/ssh_keys/client.py +++ b/hcloud/ssh_keys/client.py @@ -1,5 +1,4 @@ -from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin - +from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.ssh_keys.domain import SSHKey @@ -36,9 +35,7 @@ def get_by_id(self, id): :param id: int :return: :class:`BoundSSHKey ` """ - response = self._client.request( - url="/ssh_keys/{ssh_key_id}".format(ssh_key_id=id), method="GET" - ) + response = self._client.request(url=f"/ssh_keys/{id}", method="GET") return BoundSSHKey(self, response["ssh_key"]) def get_list( @@ -95,7 +92,7 @@ def get_all(self, name=None, fingerprint=None, label_selector=None): Can be used to filter SSH keys by labels. The response will only contain SSH keys matching the label selector. :return: List[:class:`BoundSSHKey `] """ - return super(SSHKeysClient, self).get_all( + return super().get_all( name=name, fingerprint=fingerprint, label_selector=label_selector ) @@ -107,7 +104,7 @@ def get_by_name(self, name): Used to get ssh key by name. :return: :class:`BoundSSHKey ` """ - return super(SSHKeysClient, self).get_by_name(name) + return super().get_by_name(name) def get_by_fingerprint(self, fingerprint): # type: (str) -> BoundSSHKey @@ -155,7 +152,7 @@ def update(self, ssh_key, name=None, labels=None): if labels is not None: data["labels"] = labels response = self._client.request( - url="/ssh_keys/{ssh_key_id}".format(ssh_key_id=ssh_key.id), + url=f"/ssh_keys/{ssh_key.id}", method="PUT", json=data, ) @@ -163,9 +160,7 @@ def update(self, ssh_key, name=None, labels=None): def delete(self, ssh_key): # type: (SSHKey) -> bool - self._client.request( - url="/ssh_keys/{ssh_key_id}".format(ssh_key_id=ssh_key.id), method="DELETE" - ) + self._client.request(url=f"/ssh_keys/{ssh_key.id}", method="DELETE") """Deletes an SSH key. It cannot be used anymore. :param ssh_key: :class:`BoundSSHKey ` or :class:`SSHKey ` diff --git a/hcloud/volumes/client.py b/hcloud/volumes/client.py index f8ff86e3..5fee1c88 100644 --- a/hcloud/volumes/client.py +++ b/hcloud/volumes/client.py @@ -1,9 +1,8 @@ -from hcloud.core.client import ClientEntityBase, BoundModelBase, GetEntityByNameMixin - from hcloud.actions.client import BoundAction +from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.core.domain import add_meta_to_result -from hcloud.volumes.domain import Volume, CreateVolumeResponse from hcloud.locations.client import BoundLocation +from hcloud.volumes.domain import CreateVolumeResponse, Volume class BoundVolume(BoundModelBase): @@ -21,7 +20,7 @@ def __init__(self, client, data, complete=True): data["server"] = BoundServer( client._client.servers, {"id": server}, complete=False ) - super(BoundVolume, self).__init__(client, data, complete) + 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]] @@ -120,9 +119,7 @@ def get_by_id(self, id): :param id: int :return: :class:`BoundVolume ` """ - response = self._client.request( - url="/volumes/{volume_id}".format(volume_id=id), method="GET" - ) + response = self._client.request(url=f"/volumes/{id}", method="GET") return BoundVolume(self, response["volume"]) def get_list( @@ -171,9 +168,7 @@ def get_all(self, label_selector=None, status=None): Can be used to filter volumes by their status. The response will only contain volumes matching the status. :return: List[:class:`BoundVolume `] """ - return super(VolumesClient, self).get_all( - label_selector=label_selector, status=status - ) + return super().get_all(label_selector=label_selector, status=status) def get_by_name(self, name): # type: (str) -> BoundVolume @@ -183,7 +178,7 @@ def get_by_name(self, name): Used to get volume by name. :return: :class:`BoundVolume ` """ - return super(VolumesClient, self).get_by_name(name) + return super().get_by_name(name) def create( self, @@ -272,7 +267,7 @@ def get_actions_list( params["per_page"] = per_page response = self._client.request( - url="/volumes/{volume_id}/actions".format(volume_id=volume.id), + url=f"/volumes/{volume.id}/actions", method="GET", params=params, ) @@ -293,7 +288,7 @@ def get_actions(self, volume, status=None, sort=None): 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 `] """ - return super(VolumesClient, self).get_actions(volume, status=status, sort=sort) + return super().get_actions(volume, status=status, sort=sort) def update(self, volume, name=None, labels=None): # type:(Union[Volume, BoundVolume], Optional[str], Optional[Dict[str, str]]) -> BoundVolume @@ -312,7 +307,7 @@ def update(self, volume, name=None, labels=None): if labels is not None: data.update({"labels": labels}) response = self._client.request( - url="/volumes/{volume_id}".format(volume_id=volume.id), + url=f"/volumes/{volume.id}", method="PUT", json=data, ) @@ -325,9 +320,7 @@ def delete(self, volume): :param volume: :class:`BoundVolume ` or :class:`Volume ` :return: boolean """ - self._client.request( - url="/volumes/{volume_id}".format(volume_id=volume.id), method="DELETE" - ) + self._client.request(url=f"/volumes/{volume.id}", method="DELETE") return True def resize(self, volume, size): @@ -340,7 +333,7 @@ def resize(self, volume, size): :return: :class:`BoundAction ` """ data = self._client.request( - url="/volumes/{volume_id}/actions/resize".format(volume_id=volume.id), + url=f"/volumes/{volume.id}/actions/resize", json={"size": size}, method="POST", ) @@ -360,7 +353,7 @@ def attach(self, volume, server, automount=None): data["automount"] = automount data = self._client.request( - url="/volumes/{volume_id}/actions/attach".format(volume_id=volume.id), + url=f"/volumes/{volume.id}/actions/attach", json=data, method="POST", ) @@ -374,7 +367,7 @@ def detach(self, volume): :return: :class:`BoundAction ` """ data = self._client.request( - url="/volumes/{volume_id}/actions/detach".format(volume_id=volume.id), + url=f"/volumes/{volume.id}/actions/detach", method="POST", ) return BoundAction(self._client.actions, data["action"]) diff --git a/requirements/test.txt b/requirements/test.txt index a0fe53ac..3f71ee45 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,7 +1,4 @@ -r base.txt -flake8==3.6.0 -isort==4.3.4 pytest tox==3.23.1 -black==21.7b0 diff --git a/setup.cfg b/setup.cfg index d3d2afb0..0b67081e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,4 +17,3 @@ test = pytest [tool:pytest] collect_ignore = ['setup.py'] - diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 96879fb5..48b5b023 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ """The setup script.""" -from setuptools import setup, find_packages +from setuptools import find_packages, setup with open("README.rst") as readme_file: readme = readme_file.read() diff --git a/tests/unit/actions/test_client.py b/tests/unit/actions/test_client.py index 8ee7fe60..ce8d65f8 100644 --- a/tests/unit/actions/test_client.py +++ b/tests/unit/actions/test_client.py @@ -1,11 +1,12 @@ from unittest import mock + import pytest from hcloud.actions.client import ActionsClient, BoundAction from hcloud.actions.domain import Action, ActionFailedException, ActionTimeoutException -class TestBoundAction(object): +class TestBoundAction: @pytest.fixture() def bound_running_action(self, mocked_requests): return BoundAction( @@ -47,7 +48,7 @@ def test_wait_until_finished_max_retries( assert mocked_requests.request.call_count == 1 -class TestActionsClient(object): +class TestActionsClient: @pytest.fixture() def actions_client(self): return ActionsClient(client=mock.MagicMock()) diff --git a/tests/unit/actions/test_domain.py b/tests/unit/actions/test_domain.py index 5278964b..b15d04e7 100644 --- a/tests/unit/actions/test_domain.py +++ b/tests/unit/actions/test_domain.py @@ -1,10 +1,11 @@ import datetime + from dateutil.tz import tzoffset from hcloud.actions.domain import Action -class TestAction(object): +class TestAction: def test_started_finished_is_datetime(self): action = Action( id=1, started="2016-01-30T23:50+00:00", finished="2016-03-30T23:50+00:00" diff --git a/tests/unit/certificates/test_client.py b/tests/unit/certificates/test_client.py index 3090ff24..e7581188 100644 --- a/tests/unit/certificates/test_client.py +++ b/tests/unit/certificates/test_client.py @@ -1,12 +1,13 @@ -import pytest from unittest import mock +import pytest + from hcloud.actions.client import BoundAction -from hcloud.certificates.client import CertificatesClient, BoundCertificate +from hcloud.certificates.client import BoundCertificate, CertificatesClient from hcloud.certificates.domain import Certificate, ManagedCertificateStatus -class TestBoundCertificate(object): +class TestBoundCertificate: @pytest.fixture() def bound_certificate(self, hetzner_client): return BoundCertificate(client=hetzner_client.certificates, data=dict(id=14)) @@ -101,7 +102,7 @@ def test_retry_issuance( assert action.command == "issue_certificate" -class TestCertificatesClient(object): +class TestCertificatesClient: @pytest.fixture() def certificates_client(self): return CertificatesClient(client=mock.MagicMock()) diff --git a/tests/unit/certificates/test_domain.py b/tests/unit/certificates/test_domain.py index e558aa32..30cdee0f 100644 --- a/tests/unit/certificates/test_domain.py +++ b/tests/unit/certificates/test_domain.py @@ -1,10 +1,11 @@ import datetime + from dateutil.tz import tzoffset from hcloud.certificates.domain import Certificate -class TestCertificate(object): +class TestCertificate: def test_created_is_datetime(self): certificate = Certificate( id=1, diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index f40734d4..d15d36b5 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,5 +1,7 @@ from unittest import mock + import pytest + from hcloud import Client diff --git a/tests/unit/core/test_client.py b/tests/unit/core/test_client.py index 7c5aa36c..e373884a 100644 --- a/tests/unit/core/test_client.py +++ b/tests/unit/core/test_client.py @@ -1,8 +1,9 @@ from unittest import mock + import pytest from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.core.domain import add_meta_to_result, BaseDomain +from hcloud.core.domain import BaseDomain, add_meta_to_result class TestBoundModelBase: diff --git a/tests/unit/core/test_domain.py b/tests/unit/core/test_domain.py index 988f54bb..9e71cee4 100644 --- a/tests/unit/core/test_domain.py +++ b/tests/unit/core/test_domain.py @@ -10,7 +10,7 @@ ) -class TestMeta(object): +class TestMeta: @pytest.mark.parametrize("json_content", [None, "", {}]) def test_parse_meta_empty_json(self, json_content): result = Meta.parse_meta(json_content) @@ -74,7 +74,7 @@ def __init__(self, id=None, name=None): self.name = name -class TestDomainIdentityMixin(object): +class TestDomainIdentityMixin: @pytest.mark.parametrize( "domain,expected_result", [ @@ -104,7 +104,7 @@ def __init__(self, id, name="name1", started=None): self.started = isoparse(started) if started else None -class TestBaseDomain(object): +class TestBaseDomain: @pytest.mark.parametrize( "data_dict,expected_result", [ diff --git a/tests/unit/datacenters/test_client.py b/tests/unit/datacenters/test_client.py index a34fe8ad..7e525527 100644 --- a/tests/unit/datacenters/test_client.py +++ b/tests/unit/datacenters/test_client.py @@ -1,12 +1,13 @@ -import pytest # noqa: F401 from unittest import mock # noqa: F401 -from hcloud.datacenters.client import DatacentersClient, BoundDatacenter +import pytest # noqa: F401 + +from hcloud.datacenters.client import BoundDatacenter, DatacentersClient from hcloud.datacenters.domain import DatacenterServerTypes from hcloud.locations.client import BoundLocation -class TestBoundDatacenter(object): +class TestBoundDatacenter: def test_bound_datacenter_init(self, datacenter_response): bound_datacenter = BoundDatacenter( client=mock.MagicMock(), data=datacenter_response["datacenter"] @@ -54,7 +55,7 @@ def test_bound_datacenter_init(self, datacenter_response): ) -class TestDatacentersClient(object): +class TestDatacentersClient: @pytest.fixture() def datacenters_client(self): return DatacentersClient(client=mock.MagicMock()) diff --git a/tests/unit/firewalls/test_client.py b/tests/unit/firewalls/test_client.py index 95311c21..4094f6d4 100644 --- a/tests/unit/firewalls/test_client.py +++ b/tests/unit/firewalls/test_client.py @@ -1,18 +1,19 @@ -import pytest from unittest import mock -from hcloud.firewalls.client import FirewallsClient, BoundFirewall +import pytest + from hcloud.actions.client import BoundAction +from hcloud.firewalls.client import BoundFirewall, FirewallsClient from hcloud.firewalls.domain import ( Firewall, - FirewallRule, FirewallResource, FirewallResourceLabelSelector, + FirewallRule, ) from hcloud.servers.domain import Server -class TestBoundFirewall(object): +class TestBoundFirewall: @pytest.fixture() def bound_firewall(self, hetzner_client): return BoundFirewall(client=hetzner_client.firewalls, data=dict(id=1)) @@ -188,7 +189,7 @@ def test_remove_from_resources( assert actions[0].progress == 100 -class TestFirewallsClient(object): +class TestFirewallsClient: @pytest.fixture() def firewalls_client(self): return FirewallsClient(client=mock.MagicMock()) diff --git a/tests/unit/firewalls/test_domain.py b/tests/unit/firewalls/test_domain.py index a4edc731..11170adc 100644 --- a/tests/unit/firewalls/test_domain.py +++ b/tests/unit/firewalls/test_domain.py @@ -1,10 +1,11 @@ import datetime + from dateutil.tz import tzoffset from hcloud.firewalls.domain import Firewall -class TestFirewall(object): +class TestFirewall: def test_created_is_datetime(self): firewall = Firewall(id=1, created="2016-01-30T23:50+00:00") assert firewall.created == datetime.datetime( diff --git a/tests/unit/floating_ips/test_client.py b/tests/unit/floating_ips/test_client.py index 97fcd28f..67bd2ff8 100644 --- a/tests/unit/floating_ips/test_client.py +++ b/tests/unit/floating_ips/test_client.py @@ -1,16 +1,17 @@ -import pytest from unittest import mock +import pytest + from hcloud.actions.client import BoundAction -from hcloud.servers.client import BoundServer -from hcloud.servers.domain import Server -from hcloud.floating_ips.client import FloatingIPsClient, BoundFloatingIP +from hcloud.floating_ips.client import BoundFloatingIP, FloatingIPsClient from hcloud.floating_ips.domain import FloatingIP from hcloud.locations.client import BoundLocation from hcloud.locations.domain import Location +from hcloud.servers.client import BoundServer +from hcloud.servers.domain import Server -class TestBoundFloatingIP(object): +class TestBoundFloatingIP: @pytest.fixture() def bound_floating_ip(self, hetzner_client): return BoundFloatingIP(client=hetzner_client.floating_ips, data=dict(id=14)) @@ -126,7 +127,7 @@ def test_change_dns_ptr(self, hetzner_client, bound_floating_ip, generic_action) assert action.progress == 0 -class TestFloatingIPsClient(object): +class TestFloatingIPsClient: @pytest.fixture() def floating_ips_client(self): return FloatingIPsClient(client=mock.MagicMock()) diff --git a/tests/unit/floating_ips/test_domain.py b/tests/unit/floating_ips/test_domain.py index 167540aa..293cafca 100644 --- a/tests/unit/floating_ips/test_domain.py +++ b/tests/unit/floating_ips/test_domain.py @@ -1,10 +1,11 @@ import datetime + from dateutil.tz import tzoffset from hcloud.floating_ips.domain import FloatingIP -class TestFloatingIP(object): +class TestFloatingIP: def test_created_is_datetime(self): floatingIP = FloatingIP(id=1, created="2016-01-30T23:50+00:00") assert floatingIP.created == datetime.datetime( diff --git a/tests/unit/images/test_client.py b/tests/unit/images/test_client.py index bba1416c..53ca5447 100644 --- a/tests/unit/images/test_client.py +++ b/tests/unit/images/test_client.py @@ -1,15 +1,16 @@ -import pytest -from unittest import mock import datetime +from unittest import mock + +import pytest from dateutil.tz import tzoffset -from hcloud.images.client import ImagesClient, BoundImage from hcloud.actions.client import BoundAction +from hcloud.images.client import BoundImage, ImagesClient from hcloud.images.domain import Image from hcloud.servers.client import BoundServer -class TestBoundImage(object): +class TestBoundImage: @pytest.fixture() def bound_image(self, hetzner_client): return BoundImage(client=hetzner_client.images, data=dict(id=14)) @@ -120,7 +121,7 @@ def test_change_protection(self, hetzner_client, bound_image, generic_action): assert action.progress == 0 -class TestImagesClient(object): +class TestImagesClient: @pytest.fixture() def images_client(self): return ImagesClient(client=mock.MagicMock()) diff --git a/tests/unit/images/test_domain.py b/tests/unit/images/test_domain.py index 733d45b2..939fa14a 100644 --- a/tests/unit/images/test_domain.py +++ b/tests/unit/images/test_domain.py @@ -1,10 +1,11 @@ import datetime + from dateutil.tz import tzoffset from hcloud.images.domain import Image -class TestImage(object): +class TestImage: def test_created_is_datetime(self): image = Image(id=1, created="2016-01-30T23:50+00:00") assert image.created == datetime.datetime( diff --git a/tests/unit/isos/test_client.py b/tests/unit/isos/test_client.py index 34f052e2..0550ca5b 100644 --- a/tests/unit/isos/test_client.py +++ b/tests/unit/isos/test_client.py @@ -1,12 +1,13 @@ -import pytest -from unittest import mock import datetime +from unittest import mock + +import pytest from dateutil.tz import tzoffset -from hcloud.isos.client import IsosClient, BoundIso +from hcloud.isos.client import BoundIso, IsosClient -class TestBoundIso(object): +class TestBoundIso: @pytest.fixture() def bound_iso(self, hetzner_client): return BoundIso(client=hetzner_client.isos, data=dict(id=14)) @@ -24,7 +25,7 @@ def test_bound_iso_init(self, iso_response): ) -class TestIsosClient(object): +class TestIsosClient: @pytest.fixture() def isos_client(self): return IsosClient(client=mock.MagicMock()) diff --git a/tests/unit/isos/test_domain.py b/tests/unit/isos/test_domain.py index 6396a984..da1c1dee 100644 --- a/tests/unit/isos/test_domain.py +++ b/tests/unit/isos/test_domain.py @@ -1,10 +1,11 @@ import datetime + from dateutil.tz import tzoffset from hcloud.isos.domain import Iso -class TestIso(object): +class TestIso: def test_deprecated_is_datetime(self): iso = Iso(id=1, deprecated="2016-01-30T23:50+00:00") assert iso.deprecated == datetime.datetime( diff --git a/tests/unit/load_balancer_types/test_client.py b/tests/unit/load_balancer_types/test_client.py index b9686ad0..7e6d1a19 100644 --- a/tests/unit/load_balancer_types/test_client.py +++ b/tests/unit/load_balancer_types/test_client.py @@ -1,11 +1,11 @@ -import pytest from unittest import mock +import pytest from hcloud.load_balancer_types.client import LoadBalancerTypesClient -class TestLoadBalancerTypesClient(object): +class TestLoadBalancerTypesClient: @pytest.fixture() def load_balancer_types_client(self): return LoadBalancerTypesClient(client=mock.MagicMock()) diff --git a/tests/unit/load_balancers/test_client.py b/tests/unit/load_balancers/test_client.py index b2c05a5b..631e1134 100644 --- a/tests/unit/load_balancers/test_client.py +++ b/tests/unit/load_balancers/test_client.py @@ -1,26 +1,25 @@ from unittest import mock + import pytest +from hcloud.actions.client import BoundAction from hcloud.load_balancer_types.domain import LoadBalancerType -from hcloud.locations.domain import Location -from hcloud.networks.domain import Network -from hcloud.servers.domain import Server - from hcloud.load_balancers.client import BoundLoadBalancer, LoadBalancersClient - from hcloud.load_balancers.domain import ( + LoadBalancer, LoadBalancerAlgorithm, LoadBalancerHealthCheck, LoadBalancerService, LoadBalancerTarget, - LoadBalancer, LoadBalancerTargetIP, LoadBalancerTargetLabelSelector, ) -from hcloud.actions.client import BoundAction +from hcloud.locations.domain import Location +from hcloud.networks.domain import Network +from hcloud.servers.domain import Server -class TestBoundLoadBalancer(object): +class TestBoundLoadBalancer: @pytest.fixture() def bound_load_balancer(self, hetzner_client): return BoundLoadBalancer(client=hetzner_client.load_balancers, data=dict(id=14)) @@ -350,7 +349,7 @@ def test_change_type(self, hetzner_client, bound_load_balancer, generic_action): assert action.progress == 0 -class TestLoadBalancerslient(object): +class TestLoadBalancerslient: @pytest.fixture() def load_balancers_client(self): return LoadBalancersClient(client=mock.MagicMock()) diff --git a/tests/unit/load_balancers/test_domain.py b/tests/unit/load_balancers/test_domain.py index ecd0ecdd..2f29cce3 100644 --- a/tests/unit/load_balancers/test_domain.py +++ b/tests/unit/load_balancers/test_domain.py @@ -1,10 +1,11 @@ import datetime + from dateutil.tz import tzoffset from hcloud.load_balancers.domain import LoadBalancer -class TestLoadBalancers(object): +class TestLoadBalancers: def test_created_is_datetime(self): lb = LoadBalancer(id=1, created="2016-01-30T23:50+00:00") assert lb.created == datetime.datetime( diff --git a/tests/unit/locations/test_client.py b/tests/unit/locations/test_client.py index 217d6ae7..0bfcb65a 100644 --- a/tests/unit/locations/test_client.py +++ b/tests/unit/locations/test_client.py @@ -1,10 +1,11 @@ -import pytest # noqa: F401 from unittest import mock # noqa: F401 +import pytest # noqa: F401 + from hcloud.locations.client import LocationsClient -class TestLocationsClient(object): +class TestLocationsClient: @pytest.fixture() def locations_client(self): return LocationsClient(client=mock.MagicMock()) diff --git a/tests/unit/networks/test_client.py b/tests/unit/networks/test_client.py index 81c12dcb..cc68390e 100644 --- a/tests/unit/networks/test_client.py +++ b/tests/unit/networks/test_client.py @@ -1,14 +1,15 @@ +from unittest import mock + import pytest from dateutil.parser import isoparse -from unittest import mock from hcloud.actions.client import BoundAction from hcloud.networks.client import BoundNetwork, NetworksClient -from hcloud.networks.domain import NetworkSubnet, NetworkRoute, Network +from hcloud.networks.domain import Network, NetworkRoute, NetworkSubnet from hcloud.servers.client import BoundServer -class TestBoundNetwork(object): +class TestBoundNetwork: @pytest.fixture() def bound_network(self, hetzner_client): return BoundNetwork(client=hetzner_client.networks, data=dict(id=14)) @@ -157,7 +158,7 @@ def test_change_ip(self, hetzner_client, bound_network, generic_action): assert action.progress == 0 -class TestNetworksClient(object): +class TestNetworksClient: @pytest.fixture() def networks_client(self): return NetworksClient(client=mock.MagicMock()) diff --git a/tests/unit/networks/test_domain.py b/tests/unit/networks/test_domain.py index 84c71d31..4740ff82 100644 --- a/tests/unit/networks/test_domain.py +++ b/tests/unit/networks/test_domain.py @@ -1,10 +1,11 @@ import datetime + from dateutil.tz import tzoffset from hcloud.networks.domain import Network -class TestNetwork(object): +class TestNetwork: def test_created_is_datetime(self): network = Network(id=1, created="2016-01-30T23:50+00:00") assert network.created == datetime.datetime( diff --git a/tests/unit/placement_groups/test_client.py b/tests/unit/placement_groups/test_client.py index aecea55d..0bbe7869 100644 --- a/tests/unit/placement_groups/test_client.py +++ b/tests/unit/placement_groups/test_client.py @@ -1,6 +1,7 @@ -import pytest from unittest import mock +import pytest + from hcloud.placement_groups.client import BoundPlacementGroup, PlacementGroupsClient @@ -12,7 +13,7 @@ def check_variables(placement_group, expected): assert placement_group.type == expected["type"] -class TestBoundPlacementGroup(object): +class TestBoundPlacementGroup: @pytest.fixture() def bound_placement_group(self, hetzner_client): return BoundPlacementGroup( @@ -58,7 +59,7 @@ def test_delete(self, hetzner_client, bound_placement_group): assert delete_success is True -class TestPlacementGroupsClient(object): +class TestPlacementGroupsClient: @pytest.fixture() def placement_groups_client(self): return PlacementGroupsClient(client=mock.MagicMock()) diff --git a/tests/unit/placement_groups/test_domain.py b/tests/unit/placement_groups/test_domain.py index f6d040b6..7f0eba47 100644 --- a/tests/unit/placement_groups/test_domain.py +++ b/tests/unit/placement_groups/test_domain.py @@ -1,10 +1,11 @@ import datetime + from dateutil.tz import tzoffset from hcloud.placement_groups.domain import PlacementGroup -class TestPlacementGroup(object): +class TestPlacementGroup: def test_created_is_datetime(self): placement_group = PlacementGroup(id=1, created="2016-01-30T23:50+00:00") assert placement_group.created == datetime.datetime( diff --git a/tests/unit/primary_ips/test_client.py b/tests/unit/primary_ips/test_client.py index 2624f923..78863951 100644 --- a/tests/unit/primary_ips/test_client.py +++ b/tests/unit/primary_ips/test_client.py @@ -1,13 +1,14 @@ -import pytest from unittest import mock -from hcloud.primary_ips.client import PrimaryIPsClient, BoundPrimaryIP -from hcloud.primary_ips.domain import PrimaryIP +import pytest + from hcloud.datacenters.client import BoundDatacenter from hcloud.datacenters.domain import Datacenter +from hcloud.primary_ips.client import BoundPrimaryIP, PrimaryIPsClient +from hcloud.primary_ips.domain import PrimaryIP -class TestBoundPrimaryIP(object): +class TestBoundPrimaryIP: @pytest.fixture() def bound_primary_ip(self, hetzner_client): return BoundPrimaryIP(client=hetzner_client.primary_ips, data=dict(id=14)) @@ -102,7 +103,7 @@ def test_change_dns_ptr(self, hetzner_client, bound_primary_ip, generic_action): assert action.progress == 0 -class TestPrimaryIPsClient(object): +class TestPrimaryIPsClient: @pytest.fixture() def primary_ips_client(self): return PrimaryIPsClient(client=mock.MagicMock()) diff --git a/tests/unit/primary_ips/test_domain.py b/tests/unit/primary_ips/test_domain.py index 65648904..bf2bd6d9 100644 --- a/tests/unit/primary_ips/test_domain.py +++ b/tests/unit/primary_ips/test_domain.py @@ -1,10 +1,11 @@ import datetime + from dateutil.tz import tzoffset from hcloud.primary_ips.domain import PrimaryIP -class TestPrimaryIP(object): +class TestPrimaryIP: def test_created_is_datetime(self): primaryIP = PrimaryIP(id=1, created="2016-01-30T23:50+00:00") assert primaryIP.created == datetime.datetime( diff --git a/tests/unit/server_types/test_client.py b/tests/unit/server_types/test_client.py index c3fabbcb..182dd332 100644 --- a/tests/unit/server_types/test_client.py +++ b/tests/unit/server_types/test_client.py @@ -1,11 +1,11 @@ -import pytest from unittest import mock +import pytest -from hcloud.server_types.client import ServerTypesClient, BoundServerType +from hcloud.server_types.client import BoundServerType, ServerTypesClient -class TestBoundIso(object): +class TestBoundIso: @pytest.fixture() def bound_server_type(self, hetzner_client): return BoundServerType(client=hetzner_client.server_types, data=dict(id=14)) @@ -27,7 +27,7 @@ def test_bound_server_type_init(self, server_type_response): assert bound_server_type.included_traffic == 21990232555520 -class TestServerTypesClient(object): +class TestServerTypesClient: @pytest.fixture() def server_types_client(self): return ServerTypesClient(client=mock.MagicMock()) diff --git a/tests/unit/servers/test_client.py b/tests/unit/servers/test_client.py index c8bf2493..5c5e88af 100644 --- a/tests/unit/servers/test_client.py +++ b/tests/unit/servers/test_client.py @@ -1,38 +1,38 @@ from unittest import mock + import pytest +from hcloud.actions.client import BoundAction +from hcloud.datacenters.client import BoundDatacenter +from hcloud.datacenters.domain import Datacenter from hcloud.firewalls.client import BoundFirewall from hcloud.firewalls.domain import Firewall from hcloud.floating_ips.client import BoundFloatingIP +from hcloud.images.client import BoundImage +from hcloud.images.domain import Image from hcloud.isos.client import BoundIso -from hcloud.servers.client import ServersClient, BoundServer - +from hcloud.isos.domain import Iso +from hcloud.locations.domain import Location +from hcloud.networks.client import BoundNetwork +from hcloud.networks.domain import Network +from hcloud.placement_groups.client import BoundPlacementGroup +from hcloud.placement_groups.domain import PlacementGroup +from hcloud.server_types.client import BoundServerType +from hcloud.server_types.domain import ServerType +from hcloud.servers.client import BoundServer, ServersClient from hcloud.servers.domain import ( - Server, - PublicNetwork, IPv4Address, IPv6Network, - PublicNetworkFirewall, PrivateNet, + PublicNetwork, + PublicNetworkFirewall, + Server, ) from hcloud.volumes.client import BoundVolume from hcloud.volumes.domain import Volume -from hcloud.images.domain import Image -from hcloud.images.client import BoundImage -from hcloud.isos.domain import Iso -from hcloud.datacenters.client import BoundDatacenter -from hcloud.datacenters.domain import Datacenter -from hcloud.locations.domain import Location -from hcloud.actions.client import BoundAction -from hcloud.server_types.client import BoundServerType -from hcloud.server_types.domain import ServerType -from hcloud.networks.domain import Network -from hcloud.networks.client import BoundNetwork -from hcloud.placement_groups.domain import PlacementGroup -from hcloud.placement_groups.client import BoundPlacementGroup -class TestBoundServer(object): +class TestBoundServer: @pytest.fixture() def bound_server(self, hetzner_client): return BoundServer(client=hetzner_client.servers, data=dict(id=14)) @@ -500,7 +500,7 @@ def test_remove_from_placement_group( assert action.command == "remove_from_placement_group" -class TestServersClient(object): +class TestServersClient: @pytest.fixture() def servers_client(self): return ServersClient(client=mock.MagicMock()) diff --git a/tests/unit/servers/test_domain.py b/tests/unit/servers/test_domain.py index 9bd1b394..10039104 100644 --- a/tests/unit/servers/test_domain.py +++ b/tests/unit/servers/test_domain.py @@ -1,10 +1,11 @@ import datetime + from dateutil.tz import tzoffset from hcloud.servers.domain import Server -class TestServer(object): +class TestServer: def test_created_is_datetime(self): server = Server(id=1, created="2016-01-30T23:50+00:00") assert server.created == datetime.datetime( diff --git a/tests/unit/ssh_keys/test_client.py b/tests/unit/ssh_keys/test_client.py index 01ab9e0e..a512122c 100644 --- a/tests/unit/ssh_keys/test_client.py +++ b/tests/unit/ssh_keys/test_client.py @@ -1,11 +1,12 @@ -import pytest from unittest import mock -from hcloud.ssh_keys.client import SSHKeysClient, BoundSSHKey +import pytest + +from hcloud.ssh_keys.client import BoundSSHKey, SSHKeysClient from hcloud.ssh_keys.domain import SSHKey -class TestBoundSSHKey(object): +class TestBoundSSHKey: @pytest.fixture() def bound_ssh_key(self, hetzner_client): return BoundSSHKey(client=hetzner_client.ssh_keys, data=dict(id=14)) @@ -41,7 +42,7 @@ def test_delete(self, hetzner_client, bound_ssh_key, generic_action): assert delete_success is True -class TestSSHKeysClient(object): +class TestSSHKeysClient: @pytest.fixture() def ssh_keys_client(self): return SSHKeysClient(client=mock.MagicMock()) diff --git a/tests/unit/ssh_keys/test_domain.py b/tests/unit/ssh_keys/test_domain.py index c19a37e6..017ffca8 100644 --- a/tests/unit/ssh_keys/test_domain.py +++ b/tests/unit/ssh_keys/test_domain.py @@ -1,10 +1,11 @@ import datetime + from dateutil.tz import tzoffset from hcloud.ssh_keys.domain import SSHKey -class TestSSHKey(object): +class TestSSHKey: def test_created_is_datetime(self): sshKey = SSHKey(id=1, created="2016-01-30T23:50+00:00") assert sshKey.created == datetime.datetime( diff --git a/tests/unit/test_hcloud.py b/tests/unit/test_hcloud.py index a4f2e61a..0fc0600f 100644 --- a/tests/unit/test_hcloud.py +++ b/tests/unit/test_hcloud.py @@ -1,13 +1,13 @@ -#!/usr/bin/env python import json from unittest.mock import MagicMock -import requests import pytest -from hcloud import Client, APIException +import requests + +from hcloud import APIException, Client -class TestHetznerClient(object): +class TestHetznerClient: @pytest.fixture() def client(self): Client._version = "0.0.0" @@ -118,7 +118,7 @@ def test_request_500(self, client, fail_response): assert error.details["content"] == "Internal Server Error" def test_request_broken_json_200(self, client, response): - content = "{'key': 'value'".encode("utf-8") + content = b"{'key': 'value'" response.reason = "OK" response._content = content client._requests_session.request.return_value = response diff --git a/tests/unit/volumes/test_client.py b/tests/unit/volumes/test_client.py index 7145194c..fda74f99 100644 --- a/tests/unit/volumes/test_client.py +++ b/tests/unit/volumes/test_client.py @@ -1,17 +1,18 @@ +from unittest import mock + import pytest from dateutil.parser import isoparse -from unittest import mock from hcloud.actions.client import BoundAction +from hcloud.locations.client import BoundLocation +from hcloud.locations.domain import Location from hcloud.servers.client import BoundServer from hcloud.servers.domain import Server -from hcloud.volumes.client import VolumesClient, BoundVolume +from hcloud.volumes.client import BoundVolume, VolumesClient from hcloud.volumes.domain import Volume -from hcloud.locations.client import BoundLocation -from hcloud.locations.domain import Location -class TestBoundVolume(object): +class TestBoundVolume: @pytest.fixture() def bound_volume(self, hetzner_client): return BoundVolume(client=hetzner_client.volumes, data=dict(id=14)) @@ -131,7 +132,7 @@ def test_resize(self, hetzner_client, bound_volume, generic_action): assert action.progress == 0 -class TestVolumesClient(object): +class TestVolumesClient: @pytest.fixture() def volumes_client(self): return VolumesClient(client=mock.MagicMock()) diff --git a/tests/unit/volumes/test_domain.py b/tests/unit/volumes/test_domain.py index 63886281..5587e3fb 100644 --- a/tests/unit/volumes/test_domain.py +++ b/tests/unit/volumes/test_domain.py @@ -1,10 +1,11 @@ import datetime + from dateutil.tz import tzoffset from hcloud.volumes.domain import Volume -class TestVolume(object): +class TestVolume: def test_created_is_datetime(self): volume = Volume(id=1, created="2016-01-30T23:50+00:00") assert volume.created == datetime.datetime( diff --git a/tox.ini b/tox.ini index a48f6f7e..cae4dcfd 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,9 @@ [tox] -envlist = py37, py38, py39, py310, py311, flake8 +envlist = py37, py38, py39, py310, py311 -[testenv:flake8] -basepython = python -deps = flake8==5.0.4 -commands = flake8 --ignore F821,E501,W605,W503 hcloud tests setup.py - -[testenv:black] -basepython = python -deps = black==22.6.0 -commands = black . --check --diff +[testenv:pre-commit] +deps = pre-commit +commands = pre-commit run --verbose --all-files --show-diff-on-failure [testenv] passenv = FAKE_API_ENDPOINT From 93f48ff27c0561f66e5fe871e42fc2953bab0993 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 16 Jun 2023 15:44:02 +0200 Subject: [PATCH 024/406] docs: use venv for the dev setup (#196) --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 4720f28f..478e04e4 100644 --- a/README.rst +++ b/README.rst @@ -39,7 +39,7 @@ Development Setup Dev Environment --------------------- -1) `mkvirtualenv hcloud-python` +1) `python3 -m venv venv && source venv/bin/activate` 2) `pip install -e .` or `pip install -e .[docs]` to be able to build docs From 58714d13d14c24db80280efdb17b0a7e5237a9a2 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 16 Jun 2023 16:08:54 +0200 Subject: [PATCH 025/406] chore: prettier breaks the release-please changelog format (#197) Overriding the release-please templates is not yet supported, but might be a good idea to be able to comply with prettier's markdown styling. --- .pre-commit-config.yaml | 1 + CHANGELOG.md | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6b2e5257..723a2a2c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,6 +26,7 @@ repos: hooks: - id: prettier files: \.(md|ya?ml|js|css)$ + exclude: ^CHANGELOG.md$ - repo: https://github.com/asottile/pyupgrade rev: v3.6.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d4524d2..f910a11a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [1.20.0](https://github.com/hetznercloud/hcloud-python/compare/v1.19.0...v1.20.0) (2023-05-12) + ### Features -- **server_type:** add field for included traffic ([#185](https://github.com/hetznercloud/hcloud-python/issues/185)) ([8ae0bc6](https://github.com/hetznercloud/hcloud-python/commit/8ae0bc6e032440538f3aeb2222a9bee34adab04b)) + * **server_type:** add field for included traffic ([#185](https://github.com/hetznercloud/hcloud-python/issues/185)) ([8ae0bc6](https://github.com/hetznercloud/hcloud-python/commit/8ae0bc6e032440538f3aeb2222a9bee34adab04b)) From 4a0fce7da6d47a7e9094c5efd1769d3d9395b540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Mon, 19 Jun 2023 12:06:27 +0200 Subject: [PATCH 026/406] feat: add deprecation field to ServerType (#192) --- docs/api.deprecation.rst | 5 +++++ docs/api.rst | 9 ++++++++ hcloud/deprecation/__init__.py | 0 hcloud/deprecation/domain.py | 31 ++++++++++++++++++++++++++ hcloud/server_types/domain.py | 11 ++++++++- tests/unit/server_types/conftest.py | 17 ++++++++++++++ tests/unit/server_types/test_client.py | 11 ++++++++- 7 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 docs/api.deprecation.rst create mode 100644 hcloud/deprecation/__init__.py create mode 100644 hcloud/deprecation/domain.py diff --git a/docs/api.deprecation.rst b/docs/api.deprecation.rst new file mode 100644 index 00000000..844a60c7 --- /dev/null +++ b/docs/api.deprecation.rst @@ -0,0 +1,5 @@ +Deprecation Info +================== + +.. autoclass:: hcloud.deprecation.domain.DeprecationInfo + :members: diff --git a/docs/api.rst b/docs/api.rst index e999a32f..831f9328 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -27,3 +27,12 @@ Exceptions .. autoclass:: hcloud.actions.domain.ActionTimeoutException :members: + +Other +------------- + +.. toctree:: + :maxdepth: 3 + + api.helpers + api.deprecation diff --git a/hcloud/deprecation/__init__.py b/hcloud/deprecation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hcloud/deprecation/domain.py b/hcloud/deprecation/domain.py new file mode 100644 index 00000000..da967742 --- /dev/null +++ b/hcloud/deprecation/domain.py @@ -0,0 +1,31 @@ +from dateutil.parser import isoparse + +from hcloud.core.domain import BaseDomain + + +class DeprecationInfo(BaseDomain): + """Describes if, when & how the resources was deprecated. If this field is set to ``None`` the resource is not + deprecated. If it has a value, it is considered deprecated. + + :param announced: datetime + Date of when the deprecation was announced. + :param unavailable_after: datetime + After the time in this field, the resource will not be available from the general listing endpoint of the + resource type, and it can not be used in new resources. For example, if this is an image, you can not create + new servers with this image after the mentioned date. + """ + + __slots__ = ( + "announced", + "unavailable_after", + ) + + def __init__( + self, + announced=None, + unavailable_after=None, + ): + self.announced = isoparse(announced) if announced else None + self.unavailable_after = ( + isoparse(unavailable_after) if unavailable_after else None + ) diff --git a/hcloud/server_types/domain.py b/hcloud/server_types/domain.py index 5293b8d8..d584312c 100644 --- a/hcloud/server_types/domain.py +++ b/hcloud/server_types/domain.py @@ -1,4 +1,5 @@ from hcloud.core.domain import BaseDomain, DomainIdentityMixin +from hcloud.deprecation.domain import DeprecationInfo class ServerType(BaseDomain, DomainIdentityMixin): @@ -25,7 +26,10 @@ class ServerType(BaseDomain, DomainIdentityMixin): :param architecture: string Architecture of cpu. Choices: `x86`, `arm` :param deprecated: bool - True if server type is deprecated + True if server type is deprecated. This field is deprecated. Use `deprecation` instead. + :param deprecation: :class:`DeprecationInfo `, None + Describes if, when & how the resources was deprecated. If this field is set to None the resource is not + deprecated. If it has a value, it is considered deprecated. :param included_traffic: int Free traffic per month in bytes """ @@ -42,6 +46,7 @@ class ServerType(BaseDomain, DomainIdentityMixin): "cpu_type", "architecture", "deprecated", + "deprecation", "included_traffic", ) @@ -58,6 +63,7 @@ def __init__( cpu_type=None, architecture=None, deprecated=None, + deprecation=None, included_traffic=None, ): self.id = id @@ -71,4 +77,7 @@ def __init__( self.cpu_type = cpu_type self.architecture = architecture self.deprecated = deprecated + self.deprecation = ( + DeprecationInfo.from_dict(deprecation) if deprecation is not None else None + ) self.included_traffic = included_traffic diff --git a/tests/unit/server_types/conftest.py b/tests/unit/server_types/conftest.py index 559e8d2e..5735f0e2 100644 --- a/tests/unit/server_types/conftest.py +++ b/tests/unit/server_types/conftest.py @@ -28,6 +28,11 @@ def server_type_response(): "cpu_type": "shared", "architecture": "x86", "included_traffic": 21990232555520, + "deprecated": True, + "deprecation": { + "announced": "2023-06-01T00:00:00+00:00", + "unavailable_after": "2023-09-01T00:00:00+00:00", + }, } } @@ -60,6 +65,11 @@ def two_server_types_response(): "cpu_type": "shared", "architecture": "x86", "included_traffic": 21990232555520, + "deprecated": True, + "deprecation": { + "announced": "2023-06-01T00:00:00+00:00", + "unavailable_after": "2023-09-01T00:00:00+00:00", + }, }, { "id": 2, @@ -96,6 +106,8 @@ def two_server_types_response(): "cpu_type": "shared", "architecture": "x86", "included_traffic": 21990232555520, + "deprecated": False, + "deprecation": None, }, ] } @@ -129,6 +141,11 @@ def one_server_types_response(): "cpu_type": "shared", "architecture": "x86", "included_traffic": 21990232555520, + "deprecated": True, + "deprecation": { + "announced": "2023-06-01T00:00:00+00:00", + "unavailable_after": "2023-09-01T00:00:00+00:00", + }, } ] } diff --git a/tests/unit/server_types/test_client.py b/tests/unit/server_types/test_client.py index 182dd332..442da4b5 100644 --- a/tests/unit/server_types/test_client.py +++ b/tests/unit/server_types/test_client.py @@ -1,3 +1,4 @@ +from datetime import datetime, timezone from unittest import mock import pytest @@ -5,7 +6,7 @@ from hcloud.server_types.client import BoundServerType, ServerTypesClient -class TestBoundIso: +class TestBoundServerType: @pytest.fixture() def bound_server_type(self, hetzner_client): return BoundServerType(client=hetzner_client.server_types, data=dict(id=14)) @@ -24,6 +25,14 @@ def test_bound_server_type_init(self, server_type_response): assert bound_server_type.storage_type == "local" assert bound_server_type.cpu_type == "shared" assert bound_server_type.architecture == "x86" + assert bound_server_type.deprecated is True + assert bound_server_type.deprecation is not None + assert bound_server_type.deprecation.announced == datetime( + 2023, 6, 1, tzinfo=timezone.utc + ) + assert bound_server_type.deprecation.unavailable_after == datetime( + 2023, 9, 1, tzinfo=timezone.utc + ) assert bound_server_type.included_traffic == 21990232555520 From 578b4f695192af228d61492e79eaff941ab5ea76 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Mon, 19 Jun 2023 12:23:44 +0200 Subject: [PATCH 027/406] chore(main): release 1.21.0 (#193) --- CHANGELOG.md | 18 ++++++++++++++++++ hcloud/__version__.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f910a11a..eb4b574b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [1.21.0](https://github.com/hetznercloud/hcloud-python/compare/v1.20.0...v1.21.0) (2023-06-19) + + +### Features + +* add deprecation field to ServerType ([#192](https://github.com/hetznercloud/hcloud-python/issues/192)) ([4a0fce7](https://github.com/hetznercloud/hcloud-python/commit/4a0fce7da6d47a7e9094c5efd1769d3d9395b540)) + + +### Bug Fixes + +* adjust label validation for max length of 63 characters ([#194](https://github.com/hetznercloud/hcloud-python/issues/194)) ([3cba96d](https://github.com/hetznercloud/hcloud-python/commit/3cba96d261499e5f812aca7936ae9ed1e75ccd52)) + + +### Documentation + +* improve branding, design & fix warnings ([#191](https://github.com/hetznercloud/hcloud-python/issues/191)) ([47eb9f1](https://github.com/hetznercloud/hcloud-python/commit/47eb9f1c79e05a61084f0a639f9497beb22d6910)) +* use venv for the dev setup ([#196](https://github.com/hetznercloud/hcloud-python/issues/196)) ([93f48ff](https://github.com/hetznercloud/hcloud-python/commit/93f48ff27c0561f66e5fe871e42fc2953bab0993)) + ## [1.20.0](https://github.com/hetznercloud/hcloud-python/compare/v1.19.0...v1.20.0) (2023-05-12) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index 494d0895..ecfc50ea 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1 +1 @@ -VERSION = "1.20.0" # x-release-please-version +VERSION = "1.21.0" # x-release-please-version From 5001af662452bb77c42842123a7ae0f48566759b Mon Sep 17 00:00:00 2001 From: Jonas L Date: Wed, 21 Jun 2023 10:26:57 +0200 Subject: [PATCH 028/406] refactor: consistent import statements (#200) --- hcloud/hcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hcloud/hcloud.py b/hcloud/hcloud.py index 1ce53cb8..7e9c778a 100644 --- a/hcloud/hcloud.py +++ b/hcloud/hcloud.py @@ -5,6 +5,7 @@ from hcloud.actions.client import ActionsClient from hcloud.certificates.client import CertificatesClient from hcloud.datacenters.client import DatacentersClient +from hcloud.firewalls.client import FirewallsClient from hcloud.floating_ips.client import FloatingIPsClient from hcloud.images.client import ImagesClient from hcloud.isos.client import IsosClient @@ -20,7 +21,6 @@ from hcloud.volumes.client import VolumesClient from .__version__ import VERSION -from .firewalls.client import FirewallsClient class APIException(Exception): From 62d89f94a8a86babd8ab238443054ca4cd9411ef Mon Sep 17 00:00:00 2001 From: Jonas L Date: Wed, 21 Jun 2023 10:27:22 +0200 Subject: [PATCH 029/406] feat: bump required python version to >=3.7 (#198) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 48b5b023..d6ddfec1 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", ], - python_requires=">3.5", + python_requires=">=3.7", description="Official Hetzner Cloud python library", install_requires=requirements, extras_require=extras_require, From 03089f933ad51a9660e44b7fec013621a8e055d1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Jun 2023 10:27:57 +0200 Subject: [PATCH 030/406] chore: configure Renovate (#179) * Add renovate.json * chore: upgrade pre-commit using renovate * chore: configure semantic commits for renovate --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: jo --- renovate.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..5d564925 --- /dev/null +++ b/renovate.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base", + ":enablePreCommit", + + ":semanticCommitTypeAll(deps)", + ":semanticCommitScopeDisabled" + ] +} From 846664576a126472289464c0345eb9108c5f46d4 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Wed, 21 Jun 2023 11:01:56 +0200 Subject: [PATCH 031/406] feat: setup exception hierarchy (#199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: setup exception hierarchy * fix: improve ActionTimeoutException message * refactor: move APIException in _exceptions module * chore: typo Co-authored-by: Julian Tölle * chore: wording Co-authored-by: Julian Tölle --------- Co-authored-by: Julian Tölle --- docs/api.rst | 6 ++++++ hcloud/__init__.py | 3 ++- hcloud/_exceptions.py | 14 ++++++++++++++ hcloud/actions/domain.py | 15 +++++++++------ hcloud/hcloud.py | 13 +------------ 5 files changed, 32 insertions(+), 19 deletions(-) create mode 100644 hcloud/_exceptions.py diff --git a/docs/api.rst b/docs/api.rst index 831f9328..e284673a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -19,9 +19,15 @@ API Clients Exceptions --------------- +.. autoclass:: hcloud.HCloudException + :members: + .. autoclass:: hcloud.APIException :members: +.. autoclass:: hcloud.actions.domain.ActionException + :members: + .. autoclass:: hcloud.actions.domain.ActionFailedException :members: diff --git a/hcloud/__init__.py b/hcloud/__init__.py index e6731233..592ff64d 100644 --- a/hcloud/__init__.py +++ b/hcloud/__init__.py @@ -1 +1,2 @@ -from .hcloud import APIException, Client # noqa +from ._exceptions import APIException, HCloudException # noqa +from .hcloud import Client # noqa diff --git a/hcloud/_exceptions.py b/hcloud/_exceptions.py new file mode 100644 index 00000000..36fee5de --- /dev/null +++ b/hcloud/_exceptions.py @@ -0,0 +1,14 @@ +class HCloudException(Exception): + """There was an error while using the hcloud library""" + + +class APIException(HCloudException): + """There was an error while performing an API Request""" + + def __init__(self, code, message, details): + self.code = code + self.message = message + self.details = details + + def __str__(self): + return self.message diff --git a/hcloud/actions/domain.py b/hcloud/actions/domain.py index d0837055..e004be7e 100644 --- a/hcloud/actions/domain.py +++ b/hcloud/actions/domain.py @@ -2,6 +2,8 @@ from hcloud.core.domain import BaseDomain +from .._exceptions import HCloudException + class Action(BaseDomain): """Action Domain @@ -56,15 +58,16 @@ def __init__( self.error = error -class ActionFailedException(Exception): - """The Action you was waiting for failed""" +class ActionException(HCloudException): + """A generic action exception""" def __init__(self, action): self.action = action -class ActionTimeoutException(Exception): - """The Action you was waiting for timeouted in hcloud-python.""" +class ActionFailedException(ActionException): + """The Action you were waiting for failed""" - def __init__(self, action): - self.action = action + +class ActionTimeoutException(ActionException): + """The Action you were waiting for timed out""" diff --git a/hcloud/hcloud.py b/hcloud/hcloud.py index 7e9c778a..c06d4fbd 100644 --- a/hcloud/hcloud.py +++ b/hcloud/hcloud.py @@ -21,18 +21,7 @@ from hcloud.volumes.client import VolumesClient from .__version__ import VERSION - - -class APIException(Exception): - """There was an error while performing an API Request""" - - def __init__(self, code, message, details): - self.code = code - self.message = message - self.details = details - - def __str__(self): - return self.message +from ._exceptions import APIException class Client: From c46c5a49fcc127a21c73e958aa074ff37a2b9664 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Jun 2023 11:04:43 +0200 Subject: [PATCH 032/406] deps: update pre-commit hook asottile/pyupgrade to v3.7.0 (#205) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 723a2a2c..eb75a7e7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: exclude: ^CHANGELOG.md$ - repo: https://github.com/asottile/pyupgrade - rev: v3.6.0 + rev: v3.7.0 hooks: - id: pyupgrade args: [--py37-plus] From aeee575a8ea7c4a1afe312a2cc2624ee564a1408 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Jun 2023 09:49:07 +0200 Subject: [PATCH 033/406] deps: update actions/setup-python action to v4 (#209) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/code_style.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/unit_test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/code_style.yml b/.github/workflows/code_style.yml index 60c616c5..03ed9ba2 100644 --- a/.github/workflows/code_style.yml +++ b/.github/workflows/code_style.yml @@ -8,7 +8,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Setup python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: 3.9 architecture: x64 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4a839b16..b4214a9c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: "3.x" - name: Install dependencies diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml index 54dbb059..bb3de0ee 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/unit_test.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Setup python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} architecture: x64 From cb13230e570acdbb0287c678b4cee52a0a08a170 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Jun 2023 10:17:50 +0200 Subject: [PATCH 034/406] deps: update actions/stale action to v8 (#210) * deps: update actions/stale action to v8 --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: jo --- .github/workflows/bot_stale.yml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/bot_stale.yml b/.github/workflows/bot_stale.yml index 57b6084b..322b4a72 100644 --- a/.github/workflows/bot_stale.yml +++ b/.github/workflows/bot_stale.yml @@ -1,19 +1,24 @@ name: "Close stale issues" + on: schedule: - cron: "30 12 * * *" + +permissions: + issues: write + pull-requests: write + jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v1 + - uses: actions/stale@v8 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: "This issue has been marked as stale because it has not had recent activity. The bot will close the issue if no further action occurs." - exempt-issue-label: "pinned" - stale-issue-label: "stale" + exempt-issue-labels: "pinned" + stale-issue-labels: "stale" stale-pr-message: "This PR has been marked as stale because it has not had recent activity. The bot will close the PR if no further action occurs." - exempt-pr-label: "pinned" - stale-pr-label: "stale" + exempt-pr-labels: "pinned" + stale-pr-labels: "stale" days-before-stale: 90 days-before-close: 30 From 7a19addd8b5200f8e61360657964233e7bfae13d Mon Sep 17 00:00:00 2001 From: Jonas L Date: Thu, 22 Jun 2023 10:32:49 +0200 Subject: [PATCH 035/406] feat: adhere to PEP 517 (#213) * feat: adhere to PEP 517 Use the modern PEP 517 build system. https://peps.python.org/pep-0517/ * ci: build package using build --- .github/workflows/release.yml | 16 ++++++++++------ pyproject.toml | 3 +++ 2 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 pyproject.toml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b4214a9c..820b3a73 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,18 +9,22 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.x" + - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish + pip install --upgrade pip + pip install --upgrade build twine + + - name: Build + run: python3 -m build + + - name: Publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py sdist bdist_wheel - twine upload dist/* + run: twine upload dist/* diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..9787c3bd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" From 5321182d084d03484431c8ad27da12875d255768 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Thu, 22 Jun 2023 12:34:05 +0200 Subject: [PATCH 036/406] feat(network): add field expose_routes_to_vswitch (#208) * fix(network): missing subnets vswitch_id in networks create * feat(network): add field expose_routes_to_vswitch --- hcloud/networks/client.py | 51 +++++++++++++++---- hcloud/networks/domain.py | 5 ++ tests/unit/networks/conftest.py | 31 ++++++++++++ tests/unit/networks/test_client.py | 80 +++++++++++++++++++++++++++++- 4 files changed, 156 insertions(+), 11 deletions(-) diff --git a/hcloud/networks/client.py b/hcloud/networks/client.py index b0be745e..b7e22af6 100644 --- a/hcloud/networks/client.py +++ b/hcloud/networks/client.py @@ -30,17 +30,29 @@ def __init__(self, client, data, complete=True): super().__init__(client, data, complete) - def update(self, name=None, labels=None): - # type: (Optional[str], Optional[Dict[str, str]]) -> BoundNetwork + 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 """Updates a network. You can update a network’s name and a networks’s labels. :param name: str (optional) New name to set + :param expose_routes_to_vswitch: Optional[bool] + Indicates if the routes from this network should be exposed to the vSwitch connection. + The exposing only takes effect if a vSwitch connection is active. :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundNetwork ` """ - return self._client.update(self, name, labels) + return self._client.update( + self, + name=name, + expose_routes_to_vswitch=expose_routes_to_vswitch, + labels=labels, + ) def delete(self): # type: () -> BoundAction @@ -217,6 +229,7 @@ def create( 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]] ): """Creates a network with range ip_range. @@ -229,25 +242,37 @@ def create( Array of subnets allocated :param routes: List[:class:`NetworkRoute `] Array of routes set in this network + :param expose_routes_to_vswitch: Optional[bool] + Indicates if the routes from this network should be exposed to the vSwitch connection. + The exposing only takes effect if a vSwitch connection is active. :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundNetwork ` """ data = {"name": name, "ip_range": ip_range} if subnets is not None: - data["subnets"] = [ - { + data_subnets = [] + for subnet in subnets: + data_subnet = { "type": subnet.type, "ip_range": subnet.ip_range, "network_zone": subnet.network_zone, } - for subnet in subnets - ] + if subnet.vswitch_id is not None: + data_subnet["vswitch_id"] = subnet.vswitch_id + + data_subnets.append(data_subnet) + data["subnets"] = data_subnets + if routes is not None: data["routes"] = [ {"destination": route.destination, "gateway": route.gateway} for route in routes ] + + if expose_routes_to_vswitch is not None: + data["expose_routes_to_vswitch"] = expose_routes_to_vswitch + if labels is not None: data["labels"] = labels @@ -255,13 +280,16 @@ def create( return BoundNetwork(self, response["network"]) - def update(self, network, name=None, labels=None): - # type:(Network, Optional[str], Optional[Dict[str, str]]) -> BoundNetwork + def update(self, network, name=None, expose_routes_to_vswitch=None, labels=None): + # type:(Network, Optional[str], Optional[bool], Optional[Dict[str, str]]) -> BoundNetwork """Updates a network. You can update a network’s name and a network’s labels. :param network: :class:`BoundNetwork ` or :class:`Network ` :param name: str (optional) New name to set + :param expose_routes_to_vswitch: Optional[bool] + Indicates if the routes from this network should be exposed to the vSwitch connection. + The exposing only takes effect if a vSwitch connection is active. :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundNetwork ` @@ -269,8 +297,13 @@ def update(self, network, name=None, labels=None): data = {} if name is not None: data.update({"name": name}) + + if expose_routes_to_vswitch is not None: + data["expose_routes_to_vswitch"] = expose_routes_to_vswitch + if labels is not None: data.update({"labels": labels}) + response = self._client.request( url=f"/networks/{network.id}", method="PUT", diff --git a/hcloud/networks/domain.py b/hcloud/networks/domain.py index 8fc39901..be203ecd 100644 --- a/hcloud/networks/domain.py +++ b/hcloud/networks/domain.py @@ -16,6 +16,8 @@ class Network(BaseDomain): Subnets allocated in this network :param routes: List[:class:`NetworkRoute `] Routes set in this network + :param expose_routes_to_vswitch: bool + Indicates if the routes from this network should be exposed to the vSwitch connection. :param servers: List[:class:`BoundServer `] Servers attached to this network :param protection: dict @@ -30,6 +32,7 @@ class Network(BaseDomain): "ip_range", "subnets", "routes", + "expose_routes_to_vswitch", "servers", "protection", "labels", @@ -44,6 +47,7 @@ def __init__( ip_range=None, subnets=None, routes=None, + expose_routes_to_vswitch=None, servers=None, protection=None, labels=None, @@ -54,6 +58,7 @@ def __init__( self.ip_range = ip_range self.subnets = subnets self.routes = routes + self.expose_routes_to_vswitch = expose_routes_to_vswitch self.servers = servers self.protection = protection self.labels = labels diff --git a/tests/unit/networks/conftest.py b/tests/unit/networks/conftest.py index 109e3b5c..7320d4a5 100644 --- a/tests/unit/networks/conftest.py +++ b/tests/unit/networks/conftest.py @@ -24,6 +24,7 @@ def network_response(): }, ], "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], + "expose_routes_to_vswitch": False, "servers": [42], "protection": {"delete": False}, "labels": {}, @@ -55,6 +56,7 @@ def two_networks_response(): }, ], "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], + "expose_routes_to_vswitch": False, "servers": [42], "protection": {"delete": False}, "labels": {}, @@ -73,6 +75,7 @@ def two_networks_response(): } ], "routes": [{"destination": "12.100.1.0/24", "gateway": "12.0.1.1"}], + "expose_routes_to_vswitch": False, "servers": [45], "protection": {"delete": False}, "labels": {}, @@ -105,6 +108,7 @@ def one_network_response(): }, ], "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], + "expose_routes_to_vswitch": False, "servers": [42], "protection": {"delete": False}, "labels": {}, @@ -129,6 +133,32 @@ def network_create_response(): } ], "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], + "expose_routes_to_vswitch": False, + "servers": [42], + "protection": {"delete": False}, + "labels": {}, + "created": "2016-01-30T23:50:00+00:00", + } + } + + +@pytest.fixture() +def network_create_response_with_expose_routes_to_vswitch(): + return { + "network": { + "id": 4711, + "name": "mynet", + "ip_range": "10.0.0.0/16", + "subnets": [ + { + "type": "cloud", + "ip_range": "10.0.1.0/24", + "network_zone": "eu-central", + "gateway": "10.0.0.1", + } + ], + "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], + "expose_routes_to_vswitch": True, "servers": [42], "protection": {"delete": False}, "labels": {}, @@ -153,6 +183,7 @@ def response_update_network(): } ], "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], + "expose_routes_to_vswitch": True, "servers": [42], "protection": {"delete": False}, "labels": {}, diff --git a/tests/unit/networks/test_client.py b/tests/unit/networks/test_client.py index cc68390e..5d70d99a 100644 --- a/tests/unit/networks/test_client.py +++ b/tests/unit/networks/test_client.py @@ -268,6 +268,25 @@ def test_create(self, networks_client, network_create_response): json={"name": "mynet", "ip_range": "10.0.0.0/8"}, ) + def test_create_with_expose_routes_to_vswitch( + self, networks_client, network_create_response_with_expose_routes_to_vswitch + ): + networks_client._client.request.return_value = ( + network_create_response_with_expose_routes_to_vswitch + ) + networks_client.create( + name="mynet", ip_range="10.0.0.0/8", expose_routes_to_vswitch=True + ) + networks_client._client.request.assert_called_with( + url="/networks", + method="POST", + json={ + "name": "mynet", + "ip_range": "10.0.0.0/8", + "expose_routes_to_vswitch": True, + }, + ) + def test_create_with_subnet( self, networks_client, network_subnet, network_create_response ): @@ -291,6 +310,32 @@ def test_create_with_subnet( }, ) + def test_create_with_subnet_vswitch( + self, networks_client, network_subnet, network_create_response + ): + networks_client._client.request.return_value = network_create_response + network_subnet.type = NetworkSubnet.TYPE_VSWITCH + network_subnet.vswitch_id = 1000 + networks_client.create( + name="mynet", ip_range="10.0.0.0/8", subnets=[network_subnet] + ) + networks_client._client.request.assert_called_with( + url="/networks", + method="POST", + json={ + "name": "mynet", + "ip_range": "10.0.0.0/8", + "subnets": [ + { + "type": NetworkSubnet.TYPE_VSWITCH, + "ip_range": "10.0.1.0/24", + "network_zone": "eu-central", + "vswitch_id": 1000, + } + ], + }, + ) + def test_create_with_route( self, networks_client, network_route, network_create_response ): @@ -308,6 +353,32 @@ def test_create_with_route( }, ) + def test_create_with_route_and_expose_routes_to_vswitch( + self, + networks_client, + network_route, + network_create_response_with_expose_routes_to_vswitch, + ): + networks_client._client.request.return_value = ( + network_create_response_with_expose_routes_to_vswitch + ) + networks_client.create( + name="mynet", + ip_range="10.0.0.0/8", + routes=[network_route], + expose_routes_to_vswitch=True, + ) + networks_client._client.request.assert_called_with( + url="/networks", + method="POST", + json={ + "name": "mynet", + "ip_range": "10.0.0.0/8", + "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], + "expose_routes_to_vswitch": True, + }, + ) + def test_create_with_route_and_subnet( self, networks_client, network_subnet, network_route, network_create_response ): @@ -358,13 +429,18 @@ def test_get_actions_list(self, networks_client, network, response_get_actions): ) def test_update(self, networks_client, network, response_update_network): networks_client._client.request.return_value = response_update_network - network = networks_client.update(network, name="new-name") + network = networks_client.update( + network, name="new-name", expose_routes_to_vswitch=True + ) networks_client._client.request.assert_called_with( - url="/networks/1", method="PUT", json={"name": "new-name"} + url="/networks/1", + method="PUT", + json={"name": "new-name", "expose_routes_to_vswitch": True}, ) assert network.id == 4711 assert network.name == "new-name" + assert network.expose_routes_to_vswitch is True @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] From a8d386e0fc8067ae3bf5d9a72911b1c41cd88736 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Thu, 22 Jun 2023 12:40:22 +0200 Subject: [PATCH 037/406] chore(main): release 1.22.0 (#201) --- CHANGELOG.md | 17 +++++++++++++++++ hcloud/__version__.py | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb4b574b..fdeb0c4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [1.22.0](https://github.com/hetznercloud/hcloud-python/compare/v1.21.0...v1.22.0) (2023-06-22) + + +### Features + +* adhere to PEP 517 ([#213](https://github.com/hetznercloud/hcloud-python/issues/213)) ([7a19add](https://github.com/hetznercloud/hcloud-python/commit/7a19addd8b5200f8e61360657964233e7bfae13d)) +* bump required python version to >=3.7 ([#198](https://github.com/hetznercloud/hcloud-python/issues/198)) ([62d89f9](https://github.com/hetznercloud/hcloud-python/commit/62d89f94a8a86babd8ab238443054ca4cd9411ef)) +* **network:** add field expose_routes_to_vswitch ([#208](https://github.com/hetznercloud/hcloud-python/issues/208)) ([5321182](https://github.com/hetznercloud/hcloud-python/commit/5321182d084d03484431c8ad27da12875d255768)) +* setup exception hierarchy ([#199](https://github.com/hetznercloud/hcloud-python/issues/199)) ([8466645](https://github.com/hetznercloud/hcloud-python/commit/846664576a126472289464c0345eb9108c5f46d4)) + + +### Dependencies + +* update actions/setup-python action to v4 ([#209](https://github.com/hetznercloud/hcloud-python/issues/209)) ([aeee575](https://github.com/hetznercloud/hcloud-python/commit/aeee575a8ea7c4a1afe312a2cc2624ee564a1408)) +* update actions/stale action to v8 ([#210](https://github.com/hetznercloud/hcloud-python/issues/210)) ([cb13230](https://github.com/hetznercloud/hcloud-python/commit/cb13230e570acdbb0287c678b4cee52a0a08a170)) +* update pre-commit hook asottile/pyupgrade to v3.7.0 ([#205](https://github.com/hetznercloud/hcloud-python/issues/205)) ([c46c5a4](https://github.com/hetznercloud/hcloud-python/commit/c46c5a49fcc127a21c73e958aa074ff37a2b9664)) + ## [1.21.0](https://github.com/hetznercloud/hcloud-python/compare/v1.20.0...v1.21.0) (2023-06-19) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index ecfc50ea..c408c196 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1 +1 @@ -VERSION = "1.21.0" # x-release-please-version +VERSION = "1.22.0" # x-release-please-version From 7c9aba7de77c4cfc6aa70030c1a84d3baa98698d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Fri, 23 Jun 2023 09:23:20 +0200 Subject: [PATCH 038/406] chore: remove redundant wheel dep from pyproject.toml (#216) Remove the redundant `wheel` dependency, as it is added by the backend automatically. Listing it explicitly in the documentation was a historical mistake and has been fixed since, see: https://github.com/pypa/setuptools/commit/f7d30a9529378cf69054b5176249e5457aaf640a --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9787c3bd..fed528d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [build-system] -requires = ["setuptools", "wheel"] +requires = ["setuptools"] build-backend = "setuptools.build_meta" From 74444ddea6ca833e7b5ecb5976681986ae13711c Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 23 Jun 2023 14:08:35 +0200 Subject: [PATCH 039/406] chore: rework dev setup (#214) * chore: rework dev setup * chore: remove changelog from package description --- .flake8 | 5 + .github/ISSUE_TEMPLATE.md | 6 -- .github/workflows/code_style.yml | 22 ----- .github/workflows/lint.yml | 23 +++++ .github/workflows/release-please.yml | 6 +- .github/workflows/release.yml | 6 +- .../workflows/{bot_stale.yml => stale.yml} | 2 +- .github/workflows/{unit_test.yml => test.yml} | 25 ++--- .gitlab-ci.yml | 53 ++++------- .pre-commit-config.yaml | 1 - Makefile | 94 ++++--------------- docs/conf.py | 2 +- pyproject.toml | 7 ++ requirements/base.txt | 1 - requirements/test.txt | 4 - setup.cfg | 19 ---- setup.py | 48 +++++----- tox.ini | 6 +- 18 files changed, 119 insertions(+), 211 deletions(-) create mode 100644 .flake8 delete mode 100644 .github/ISSUE_TEMPLATE.md delete mode 100644 .github/workflows/code_style.yml create mode 100644 .github/workflows/lint.yml rename .github/workflows/{bot_stale.yml => stale.yml} (96%) rename .github/workflows/{unit_test.yml => test.yml} (53%) delete mode 100644 requirements/base.txt delete mode 100644 requirements/test.txt delete mode 100644 setup.cfg mode change 100755 => 100644 setup.py diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..09ecbe1d --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +extend-ignore = + E501 +extend-exclude = + docs diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 24e79d04..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/.github/workflows/code_style.yml b/.github/workflows/code_style.yml deleted file mode 100644 index 03ed9ba2..00000000 --- a/.github/workflows/code_style.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Code Style - -on: [push, pull_request] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Setup python - uses: actions/setup-python@v4 - with: - python-version: 3.9 - architecture: x64 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements/test.txt - pip install tox tox-gh-actions - sudo apt install build-essential - - name: pre-commit with tox - run: tox -e pre-commit diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..8a56cc68 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,23 @@ +name: Lint + +on: + push: + branches: [main] + pull_request: + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup python + uses: actions/setup-python@v4 + with: + python-version: 3.x + + - name: Install dependencies + run: pip install pre-commit + + - name: Run pre-commit + run: pre-commit run --all-files --show-diff-on-failure diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 609550f5..b11e876c 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -1,8 +1,8 @@ +name: Release please + on: push: - branches: - - main -name: release-please + branches: [main] jobs: release-please: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 820b3a73..1cf9e6e0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,12 +13,10 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.x" + python-version: 3.x - name: Install dependencies - run: | - pip install --upgrade pip - pip install --upgrade build twine + run: pip install build twine - name: Build run: python3 -m build diff --git a/.github/workflows/bot_stale.yml b/.github/workflows/stale.yml similarity index 96% rename from .github/workflows/bot_stale.yml rename to .github/workflows/stale.yml index 322b4a72..466d9cb2 100644 --- a/.github/workflows/bot_stale.yml +++ b/.github/workflows/stale.yml @@ -1,4 +1,4 @@ -name: "Close stale issues" +name: Close stale issues on: schedule: diff --git a/.github/workflows/unit_test.yml b/.github/workflows/test.yml similarity index 53% rename from .github/workflows/unit_test.yml rename to .github/workflows/test.yml index bb3de0ee..19a1cd28 100644 --- a/.github/workflows/unit_test.yml +++ b/.github/workflows/test.yml @@ -1,28 +1,29 @@ -name: Unit Tests +name: Test -on: [push, pull_request] +on: + push: + branches: [main] + pull_request: jobs: - build: + test: runs-on: ubuntu-latest + strategy: matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + name: Python ${{ matrix.python-version }} steps: - uses: actions/checkout@v3 + - name: Setup python uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - architecture: x64 + - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements/test.txt - pip install tox tox-gh-actions - sudo apt install build-essential - - name: Test with tox + run: pip install tox tox-gh-actions + + - name: Run tox run: tox - env: - PLATFORM: ${{ matrix.platform }} diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 39f0eba2..206d0b8e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,42 +1,29 @@ +default: + tags: + - cloud-integrations + stages: - test -.tests_template: &tests_template - before_script: - - pip install tox - - apk add build-base git +pre-commit: stage: test - script: tox - tags: - - hc-bladerunner - -python37: - <<: *tests_template - image: python:3.7-alpine - script: tox -e py37 - -python38: - <<: *tests_template - image: python:3.8-alpine - script: tox -e py38 -python39: - <<: *tests_template - image: python:3.9-alpine - script: tox -e py39 + image: python:3.11-alpine + before_script: + - apk add build-base git + - pip install pre-commit + script: + - pre-commit run --all-files --show-diff-on-failure -python310: - <<: *tests_template - image: python:3.10-alpine - script: tox -e py310 +test: + stage: test -python311: - <<: *tests_template - image: python:3.11-alpine - script: tox -e py311 + parallel: + matrix: + - python_version: ["3.7", "3.8", "3.9", "3.10", "3.11"] -test-style: - <<: *tests_template - image: python:3.9-alpine + image: python:${python_version}-alpine + before_script: + - pip install tox script: - - tox -e pre-commit + - tox -e ${python_version} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eb75a7e7..d759c062 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,6 @@ repos: rev: 5.12.0 hooks: - id: isort - args: [--profile=black] - repo: https://github.com/psf/black rev: 23.3.0 diff --git a/Makefile b/Makefile index cb8a27a2..b994d903 100644 --- a/Makefile +++ b/Makefile @@ -1,85 +1,23 @@ -.PHONY: clean clean-test clean-pyc clean-build docs help -.DEFAULT_GOAL := help +SHELL := bash +.PHONY: test coverage docs clean -define BROWSER_PYSCRIPT -import os, webbrowser, sys +venv: + python3 -m venv venv + venv/bin/pip install -e .[docs,test] -try: - from urllib import pathname2url -except: - from urllib.request import pathname2url +test: venv + venv/bin/pytest -v -webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) -endef -export BROWSER_PYSCRIPT +coverage: venv + venv/bin/coverage run -m pytest -v + venv/bin/coverage report --show-missing + venv/bin/coverage html + xdg-open htmlcov/index.html -define PRINT_HELP_PYSCRIPT -import re, sys - -for line in sys.stdin: - match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) - if match: - target, help = match.groups() - print("%-20s %s" % (target, help)) -endef -export PRINT_HELP_PYSCRIPT - -BROWSER := python -c "$$BROWSER_PYSCRIPT" - -help: - @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) - -clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts - -clean-build: ## remove build artifacts - rm -fr build/ - rm -fr dist/ - rm -fr .eggs/ - find . -name '*.egg-info' -exec rm -fr {} + - find . -name '*.egg' -exec rm -f {} + - -clean-pyc: ## remove Python file artifacts - find . -name '*.pyc' -exec rm -f {} + - find . -name '*.pyo' -exec rm -f {} + - find . -name '*~' -exec rm -f {} + - find . -name '__pycache__' -exec rm -fr {} + - -clean-test: ## remove test and coverage artifacts - rm -fr .tox/ - rm -f .coverage - rm -fr htmlcov/ - rm -fr .pytest_cache - -lint: ## check code with pre-commit - tox -e pre-commit - -test: ## run tests quickly with the default Python - py.test - -test-all: ## run tests on every Python version with tox - tox - -coverage: ## check code coverage quickly with the default Python - coverage run --source hcloud -m pytest - coverage report -m - coverage html - $(BROWSER) htmlcov/index.html - -docs: ## generate Sphinx HTML documentation, including API docs +docs: $(MAKE) -C docs clean $(MAKE) -C docs html - $(BROWSER) docs/_build/html/index.html - -servedocs: docs ## compile the docs watching for changes - watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . - -release: dist ## package and upload a release - twine upload dist/* - -dist: clean ## builds source and wheel package - python setup.py sdist - python setup.py bdist_wheel - ls -l dist + xdg-open docs/_build/html/index.html -install: clean ## install the package to the active Python's site-packages - python setup.py install +clean: + git clean -xdf diff --git a/docs/conf.py b/docs/conf.py index 549d7cd0..b56f284f 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # hcloud documentation build configuration file, created by # sphinx-quickstart on Fri Jun 9 13:47:02 2017. diff --git a/pyproject.toml b/pyproject.toml index fed528d4..3c43be34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,10 @@ +[tool.isort] +profile = "black" +combine_as_imports = true + +[tool.coverage.run] +source = ["hcloud"] + [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" diff --git a/requirements/base.txt b/requirements/base.txt deleted file mode 100644 index 9c558e35..00000000 --- a/requirements/base.txt +++ /dev/null @@ -1 +0,0 @@ -. diff --git a/requirements/test.txt b/requirements/test.txt deleted file mode 100644 index 3f71ee45..00000000 --- a/requirements/test.txt +++ /dev/null @@ -1,4 +0,0 @@ --r base.txt - -pytest -tox==3.23.1 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 0b67081e..00000000 --- a/setup.cfg +++ /dev/null @@ -1,19 +0,0 @@ -[bdist_wheel] -universal = 1 - - -[pep8] -ignore = E501,E722,W503 - -[flake8] -ignore = E501,E722,W503 -exclude = - .git, - docs, - - -[aliases] -test = pytest - -[tool:pytest] -collect_ignore = ['setup.py'] diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index d6ddfec1..e0d959cc --- a/setup.py +++ b/setup.py @@ -1,26 +1,28 @@ -#!/usr/bin/env python - -"""The setup script.""" - from setuptools import find_packages, setup with open("README.rst") as readme_file: readme = readme_file.read() -with open("CHANGELOG.rst") as changelog_file: - changelog = changelog_file.read() - -requirements = ["python-dateutil>=2.7.5", "requests>=2.20"] - -extras_require = {"docs": ["Sphinx==1.8.1", "sphinx-rtd-theme==0.4.2"]} - version = {} with open("hcloud/__version__.py") as fp: exec(fp.read(), version) setup( + name="hcloud", + version=version["VERSION"], + keywords="hcloud hetzner cloud", + description="Official Hetzner Cloud python library", + long_description=readme, author="Hetzner Cloud GmbH", author_email="support-cloud@hetzner.com", + url="https://github.com/hetznercloud/hcloud-python", + project_urls={ + "Bug Tracker": "https://github.com/hetznercloud/hcloud-python/issues", + "Documentation": "https://hcloud-python.readthedocs.io/en/stable/", + "Changelog": "https://github.com/hetznercloud/hcloud-python/blob/main/CHANGELOG.md", + "Source Code": "https://github.com/hetznercloud/hcloud-python", + }, + license="MIT license", classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -34,17 +36,21 @@ "Programming Language :: Python :: 3.11", ], python_requires=">=3.7", - description="Official Hetzner Cloud python library", - install_requires=requirements, - extras_require=extras_require, - license="MIT license", - long_description=readme + "\n\n" + changelog, + install_requires=[ + "python-dateutil>=2.7.5", + "requests>=2.20", + ], + extras_require={ + "docs": [ + "sphinx==1.8.1", + "sphinx-rtd-theme==0.4.2", + ], + "test": [ + "coverage>=7.2.7,<7.3", + "pytest>=7.3.2,<7.4", + ], + }, include_package_data=True, - keywords="hcloud hetzner cloud", - name="hcloud", packages=find_packages(exclude=["examples", "tests*", "docs"]), - test_suite="tests", - url="https://github.com/hetznercloud/hcloud-python", - version=version["VERSION"], zip_safe=False, ) diff --git a/tox.ini b/tox.ini index cae4dcfd..e8b91b02 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,10 @@ [tox] envlist = py37, py38, py39, py310, py311 -[testenv:pre-commit] -deps = pre-commit -commands = pre-commit run --verbose --all-files --show-diff-on-failure - [testenv] passenv = FAKE_API_ENDPOINT deps = - -r{toxinidir}/requirements/test.txt + -e.[test] commands = pytest tests/unit {posargs} From 11e1f455611b17a22328b3422d0b800552ea91e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Jun 2023 14:30:15 +0200 Subject: [PATCH 040/406] deps: update dependency pytest to >=7.4,<7.5 (#217) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e0d959cc..b96ef0c9 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ ], "test": [ "coverage>=7.2.7,<7.3", - "pytest>=7.3.2,<7.4", + "pytest>=7.4,<7.5", ], }, include_package_data=True, From c3c3613bfc32d4f7d93cae93d2ebc9bea2954ed6 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 23 Jun 2023 15:00:56 +0200 Subject: [PATCH 041/406] chore: upgrade sphinx to v6 (#218) * chore: upgrade sphinx to v6 * chore: allow writing docs using markdown --- Makefile | 3 +- docs/Makefile | 8 +-- docs/conf.py | 162 +++++++++----------------------------------------- docs/make.bat | 21 ++++--- setup.py | 5 +- 5 files changed, 47 insertions(+), 152 deletions(-) mode change 100755 => 100644 docs/conf.py diff --git a/Makefile b/Makefile index b994d903..d52cef44 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,8 @@ coverage: venv venv/bin/coverage html xdg-open htmlcov/index.html -docs: +export SPHINXBUILD=../venv/bin/sphinx-build +docs: venv $(MAKE) -C docs clean $(MAKE) -C docs html xdg-open docs/_build/html/index.html diff --git a/docs/Makefile b/docs/Makefile index 26d1985e..d4bb2cbb 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,10 +1,10 @@ # Minimal makefile for Sphinx documentation # -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = python -msphinx -SPHINXPROJ = hcloud +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build diff --git a/docs/conf.py b/docs/conf.py old mode 100755 new mode 100644 index b56f284f..9fa1a497 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,162 +1,56 @@ -#!/usr/bin/env python3 -# -# hcloud documentation build configuration file, created by -# sphinx-quickstart on Fri Jun 9 13:47:02 2017. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# If extensions (or modules to document with autodoc) are in another -# directory, add these directories to sys.path here. If the directory is -# relative to the documentation root, use os.path.abspath to make it -# absolute, like shown here. -# import os import sys +from datetime import datetime sys.path.insert(0, os.path.abspath("..")) from hcloud.__version__ import VERSION # noqa -# -- General configuration --------------------------------------------- - -# If your documentation needs a minimal Sphinx _version, state it here. +# Configuration file for the Sphinx documentation builder. # -# needs_sphinx = '1.0' +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -# source_suffix = '.rst' - -# The master toctree document. -master_doc = "index" - -# General information about the project. project = "Hetzner Cloud Python" -copyright = "2019, Hetzner Cloud GmbH" author = "Hetzner Cloud GmbH" +copyright = f"{datetime.now().year}, {author}" -# The version info for the project you're documenting, acts as replacement -# for |version| and |release|, also used in various other places throughout -# the built documents. -# -# The short X.Y _version. version = VERSION -# The full _version, including alpha/beta/rc tags. release = VERSION -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = "en" +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path +extensions = ["myst_parser", "sphinx.ext.autodoc", "sphinx.ext.viewcode"] +templates_path = ["_templates"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False +source_suffix = { + ".rst": "restructuredtext", + ".md": "markdown", +} -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). +# A boolean that decides whether module names are prepended to all object names (for +# object types where a “module” of some kind is defined), e.g. for py:function +# directives. Default is True. add_module_names = False -# -- Options for HTML output ------------------------------------------- +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] + html_logo = "_static/logo-hetzner-online.svg" html_favicon = "_static/favicon.png" -# Theme options are theme-specific and customize the look and feel of a -# theme further. For a list of options available for each theme, see the -# documentation. -# -html_theme_options = {"logo_only": True, "style_nav_header_background": "#EFEFEF"} +# Theme options are theme-specific and customize the look and feel of a theme further. +# For a list of options available for each theme, see the documentation. +html_theme_options = { + "logo_only": True, + "style_nav_header_background": "#efefef", +} html_css_files = [ "custom.css", ] - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] - -# -- Options for HTMLHelp output --------------------------------------- - -# Output file base name for HTML help builder. -htmlhelp_basename = "hclouddoc" - -# -- Options for LaTeX output ------------------------------------------ - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass -# [howto, manual, or own class]). -latex_documents = [ - ( - master_doc, - "hcloud.tex", - "Hetzner Cloud Python Documentation", - "Hetzner Cloud GmbH", - "manual", - ) -] - -# -- Options for manual page output ------------------------------------ - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [(master_doc, "Hetzner Cloud Python Documentation", [author], 1)] - -# -- Options for Texinfo output ---------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ( - master_doc, - "Hetzner Cloud Python Documentation", - author, - "HCloud-python is a library for the Hetzner Cloud API.", - "Miscellaneous", - ) -] - -source_suffix = [".rst"] diff --git a/docs/make.bat b/docs/make.bat index 1650e6b8..954237b9 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -5,32 +5,31 @@ pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=python -msphinx + set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build -set SPHINXPROJ=hcloud - -if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. - echo.The Sphinx module was not found. Make sure you have Sphinx installed, - echo.then set the SPHINXBUILD environment variable to point to the full - echo.path of the 'sphinx-build' executable. Alternatively you may add the - echo.Sphinx directory to PATH. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ + echo.https://www.sphinx-doc.org/ exit /b 1 ) -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd diff --git a/setup.py b/setup.py index b96ef0c9..d0cdbf56 100644 --- a/setup.py +++ b/setup.py @@ -42,8 +42,9 @@ ], extras_require={ "docs": [ - "sphinx==1.8.1", - "sphinx-rtd-theme==0.4.2", + "sphinx>=6.2.1,<7.0", + "sphinx-rtd-theme>=1.2.2,<1.3", + "myst-parser>=2.0.0,<2.1", ], "test": [ "coverage>=7.2.7,<7.3", From 0067ee650269718b422b3bf325f260633b6b8414 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 23 Jun 2023 15:30:47 +0200 Subject: [PATCH 042/406] chore: add readthedocs config (#220) https://blog.readthedocs.com/migrate-configuration-v2/ The new config allow us to specify the package requirements of the docs, this should fix the build process at readthedocs. --- .readthedocs.yaml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..7d43aa60 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,26 @@ +--- +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Optionally build your docs in additional formats such as PDF and ePub +formats: [pdf, epub] + +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - method: pip + path: . + extra_requirements: [docs] From 981b5404f42e35def5ea7bcf4dcc051ee692158b Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 23 Jun 2023 16:15:51 +0200 Subject: [PATCH 043/406] chore: merge changelogs (#215) * chore: merge changelogs * chore: import changelog from gh releases * chore: remove old changelog * docs: include new changelog --- CHANGELOG.md | 173 +++++++++++++++++++++++++++++++++++++++++++++ CHANGELOG.rst | 169 ------------------------------------------- docs/changelog.md | 2 + docs/changelog.rst | 1 - docs/conf.py | 3 + docs/index.rst | 2 +- 6 files changed, 179 insertions(+), 171 deletions(-) delete mode 100644 CHANGELOG.rst create mode 100644 docs/changelog.md delete mode 100644 docs/changelog.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index fdeb0c4c..cd0c3f96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,3 +41,176 @@ ### Features * **server_type:** add field for included traffic ([#185](https://github.com/hetznercloud/hcloud-python/issues/185)) ([8ae0bc6](https://github.com/hetznercloud/hcloud-python/commit/8ae0bc6e032440538f3aeb2222a9bee34adab04b)) + + +## v1.19.0 (2023-04-12) + +- docs: link to PrivateNet broken by @apricote in [#177](https://github.com/hetznercloud/hcloud-python/issues/177) +- feat: add support for ARM APIs by @apricote in [#182](https://github.com/hetznercloud/hcloud-python/issues/182) + +## v1.18.2 (2022-12-27) + +- fix: remove unused future dependency by @apricote in [#173](https://github.com/hetznercloud/hcloud-python/issues/173) +- chore: update tests to use released python-3.11 by @apricote in [#175](https://github.com/hetznercloud/hcloud-python/issues/175) +- chore: prepare release 1.18.2 by @apricote in [#174](https://github.com/hetznercloud/hcloud-python/issues/174) + +## v1.18.1 (2022-10-25) + +- Update Github Actions by @LKaemmerling in [#165](https://github.com/hetznercloud/hcloud-python/issues/165) +- Add tests for Python 3.11 by @LKaemmerling in [#167](https://github.com/hetznercloud/hcloud-python/issues/167) + +## v1.18.0 (2022-08-17) + +- Remove use of external mock module by @s-t-e-v-e-n-k in [#162](https://github.com/hetznercloud/hcloud-python/issues/162) +- document installation path via conda-forge by @s-m-e in [#149](https://github.com/hetznercloud/hcloud-python/issues/149) +- Drop # -- coding: utf-8 -- from files by @jonasdlindner in [#154](https://github.com/hetznercloud/hcloud-python/issues/154) +- Simplify Requirement Constraints by @LKaemmerling in [#163](https://github.com/hetznercloud/hcloud-python/issues/163) +- Add validation helper for Label Values/Keys by @LKaemmerling in [#164](https://github.com/hetznercloud/hcloud-python/issues/164) + +## v1.17.0 (2022-06-29) + +- Add primary IP support by @LKaemmerling in [#160](https://github.com/hetznercloud/hcloud-python/issues/160) + +## v1.16.0 (2021-08-17) + +- Feature: Add support for Load Balancer DNS PTRs + +## v1.15.0 (2021-08-16) + +- Feature: Add support for Placement Groups + +## v1.14.1 (2021-08-10) + +- Bugfix: Fix crash on extra fields in public_net response +- Improvement: Format code with black + +## v1.14.0 (2021-08-03) + +- Feature: Add support for Firewall rule descriptions + +## v1.13.0 (2021-07-16) + +- Feature: Add support for Firewall Protocols ESP and GRE +- Feature: Add support for Image Type APP +- Feature: Add support for creating Firewalls with Firewalls +- Feature: Add support for Label Selectors in Firewalls +- Improvement: Improve handling of underlying TCP connections. Now for every client instance a single TCP connection is used instead of one per call. + +- Note: Support for Python 2.7 and Python 3.5 was removed + +## v1.12.0 (2021-04-06) + +- Feature: Add support for managed Certificates + +## v1.11.0 (2021-03-11) + +- Feature: Add support for Firewalls +- Feature: Add `primary_disk_size` to `Server` Domain + +## v1.10.0 (2020-11-03) + +- Feature: Add `include_deprecated` filter to `get_list` and `get_all` on `ImagesClient` +- Feature: Add vSwitch support to `add_subnet` on `NetworksClient` +- Feature: Add subnet type constants to `NetworkSubnet` domain (`NetworkSubnet.TYPE_CLOUD`, `NetworkSubnet.TYPE_VSWITCH`) + +## v1.9.1 (2020-08-11) + +- Bugfix: BoundLoadBalancer serialization failed when using IP targets + +## v1.9.0 (2020-08-10) + +- Feature: Add `included_traffic`, `outgoing_traffic` and `ingoing_traffic` properties to Load Balancer domain +- Feature: Add `change_type`-method to `LoadBalancersClient` +- Feature: Add support for `LoadBalancerTargetLabelSelector` +- Feature: Add support for `LoadBalancerTargetLabelSelector` + +## v1.8.2 (2020-07-20) + +- Fix: Loosen up the requirements. + +## v1.8.1 (2020-06-29) + +- Fix Load Balancer Client. +- Fix: Unify setting of request parameters within `get_list` methods. + +## 1.8.0 (2020-06-22) + +- Feature: Add Load Balancers **Attention: The Load Balancer support in v1.8.0 is kind of broken. Please use v1.8.1** +- Feature: Add Certificates + +## 1.7.1 (2020-06-15) + +- Feature: Add requests 2.23 support + +## 1.7.0 (2020-06-05) + +- Feature: Add support for the optional 'networks' parameter on server creation. +- Feature: Add python 3.9 support +- Feature: Add subnet type `cloud` + +## 1.6.3 (2020-01-09) + +- Feature: Add 'created' property to SSH Key domain +- Fix: Remove ISODatetime Descriptor because it leads to wrong dates + +## 1.6.2 (2019-10-15) + +- Fix: future dependency requirement was too strict + +## 1.6.1 (2019-10-01) + +- Fix: python-dateutil dependency requirement was too strict + +## 1.6.0 (2019-09-17) + +- Feature: Add missing `get_by_name` on `FloatingIPsClient` + +## 1.5.0 (2019-09-16) + +- Fix: ServersClient.create_image fails when specifying the `labels` +- Feature: Add support for `name` on Floating IPs + +## 1.4.1 (2019-08-19) + +- Fix: Documentation for `NetworkRoute` domain was missing + +- Fix: `requests` dependency requirement was to strict + +## 1.4.0 (2019-07-29) + +- Feature: Add `mac_address` to Server PrivateNet domain + +- Feature: Add python 3.8 support + +## 1.3.0 (2019-07-10) + +- Feature: Add status filter for servers, images and volumes +- Feature: Add 'created' property to Floating IP domain +- Feature: Add 'Networks' support + +## 1.2.1 (2019-03-13) + +- Fix: BoundVolume.server server property now casted to the 'BoundServer'. + +## 1.2.0 (2019-03-06) + +- Feature: Add `get_by_fingerprint`-method for ssh keys +- Fix: Create Floating IP with location raises an error because no action was given. + +## 1.1.0 (2019-02-27) + +- Feature: Add `STATUS`-constants for server and volume status + +## 1.0.1 (2019-02-22) + +Fix: Ignore unknown fields in API response instead of raising an error + +## 1.0.0 (2019-02-21) + +- First stable release. + + You can find the documentation under https://hcloud-python.readthedocs.io/en/latest/ + +## 0.1.0 (2018-12-20) + +- First release on GitHub. diff --git a/CHANGELOG.rst b/CHANGELOG.rst deleted file mode 100644 index 52aecdb9..00000000 --- a/CHANGELOG.rst +++ /dev/null @@ -1,169 +0,0 @@ -======= -History -======= - -v1.16.0 (2021-08-17) ---------------------- -* Feature: Add support for Load Balancer DNS PTRs - -v1.15.0 (2021-08-16) ---------------------- -* Feature: Add support for Placement Groups - -v1.14.1 (2021-08-10) ---------------------- -* Bugfix: Fix crash on extra fields in public_net response -* Improvement: Format code with black - -v1.14.0 (2021-08-03) ---------------------- -* Feature: Add support for Firewall rule descriptions - -v1.13.0 (2021-07-16) ---------------------- -* Feature: Add support for Firewall Protocols ESP and GRE -* Feature: Add support for Image Type APP -* Feature: Add support for creating Firewalls with Firewalls -* Feature: Add support for Label Selectors in Firewalls -* Improvement: Improve handling of underlying TCP connections. Now for every client instance a single TCP connection is used instead of one per call. - -* Note: Support for Python 2.7 and Python 3.5 was removed - -v1.12.0 (2021-04-06) ---------------------- -* Feature: Add support for managed Certificates - -v1.11.0 (2021-03-11) ---------------------- -* Feature: Add support for Firewalls -* Feature: Add `primary_disk_size` to `Server` Domain - -v1.10.0 (2020-11-03) ---------------------- - -* Feature: Add `include_deprecated` filter to `get_list` and `get_all` on `ImagesClient` -* Feature: Add vSwitch support to `add_subnet` on `NetworksClient` -* Feature: Add subnet type constants to `NetworkSubnet` domain (`NetworkSubnet.TYPE_CLOUD`, `NetworkSubnet.TYPE_VSWITCH`) - -v1.9.1 (2020-08-11) --------------------- - -* Bugfix: BoundLoadBalancer serialization failed when using IP targets - -v1.9.0 (2020-08-10) --------------------- - -* Feature: Add `included_traffic`, `outgoing_traffic` and `ingoing_traffic` properties to Load Balancer domain -* Feature: Add `change_type`-method to `LoadBalancersClient` -* Feature: Add support for `LoadBalancerTargetLabelSelector` -* Feature: Add support for `LoadBalancerTargetLabelSelector` - -v1.8.2 (2020-07-20) --------------------- - -* Fix: Loosen up the requirements. - - -v1.8.1 (2020-06-29) --------------------- - -* Fix Load Balancer Client. -* Fix: Unify setting of request parameters within `get_list` methods. - -1.8.0 (2020-06-22) --------------------- - -* Feature: Add Load Balancers **Attention: The Load Balancer support in v1.8.0 is kind of broken. Please use v1.8.1** -* Feature: Add Certificates - - -1.7.1 (2020-06-15) --------------------- - -* Feature: Add requests 2.23 support - -1.7.0 (2020-06-05) --------------------- - -* Feature: Add support for the optional 'networks' parameter on server creation. -* Feature: Add python 3.9 support -* Feature: Add subnet type `cloud` - -1.6.3 (2020-01-09) --------------------- - -* Feature: Add 'created' property to SSH Key domain -* Fix: Remove ISODatetime Descriptor because it leads to wrong dates - -1.6.2 (2019-10-15) -------------------- -* Fix: future dependency requirement was too strict - -1.6.1 (2019-10-01) -------------------- -* Fix: python-dateutil dependency requirement was too strict - -1.6.0 (2019-09-17) -------------------- - -* Feature: Add missing `get_by_name` on `FloatingIPsClient` - -1.5.0 (2019-09-16) -------------------- - -* Fix: ServersClient.create_image fails when specifying the `labels` -* Feature: Add support for `name` on Floating IPs - -1.4.1 (2019-08-19) ------------------- - -* Fix: Documentation for `NetworkRoute` domain was missing - -* Fix: `requests` dependency requirement was to strict - -1.4.0 (2019-07-29) ------------------- - -* Feature: Add `mac_address` to Server PrivateNet domain - -* Feature: Add python 3.8 support - -1.3.0 (2019-07-10) ------------------- - -* Feature: Add status filter for servers, images and volumes -* Feature: Add 'created' property to Floating IP domain -* Feature: Add 'Networks' support - -1.2.1 (2019-03-13) ------------------- - -* Fix: BoundVolume.server server property now casted to the 'BoundServer'. - -1.2.0 (2019-03-06) ------------------- - -* Feature: Add `get_by_fingerprint`-method for ssh keys -* Fix: Create Floating IP with location raises an error because no action was given. - -1.1.0 (2019-02-27) ------------------- - -* Feature: Add `STATUS`-constants for server and volume status - -1.0.1 (2019-02-22) ------------------- - - Fix: Ignore unknown fields in API response instead of raising an error - -1.0.0 (2019-02-21) ------------------- - -* First stable release. - - You can find the documentation under https://hcloud-python.readthedocs.io/en/latest/ - -0.1.0 (2018-12-20) ------------------- - -* First release on GitHub. diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 00000000..67c32d35 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,2 @@ +:::{include} ../CHANGELOG.md +::: diff --git a/docs/changelog.rst b/docs/changelog.rst deleted file mode 100644 index 565b0521..00000000 --- a/docs/changelog.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../CHANGELOG.rst diff --git a/docs/conf.py b/docs/conf.py index 9fa1a497..8836d22a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -37,6 +37,9 @@ # directives. Default is True. add_module_names = False +# Myst Parser +myst_enable_extensions = ["colon_fence"] + # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output diff --git a/docs/index.rst b/docs/index.rst index b5605a32..27c994de 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,7 +8,7 @@ api Hetzner Cloud API Documentation contributing - changelog + changelog.md Hetzner Cloud Python ==================== From 8ea4aa0ad12e85eeb14c81dfa2195e1a6ee79a76 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 23 Jun 2023 16:41:06 +0200 Subject: [PATCH 044/406] feat: remove python-dateutil dependency (#221) * refactor: replace pytz tzoffset with timezone utc * refactor: replace isoparse with datetime.fromisoformat * feat: remove python-dateutil dependency --- hcloud/actions/domain.py | 6 +++--- hcloud/certificates/domain.py | 12 ++++++++---- hcloud/deprecation/domain.py | 6 +++--- hcloud/firewalls/domain.py | 4 ++-- hcloud/floating_ips/domain.py | 4 ++-- hcloud/images/domain.py | 6 +++--- hcloud/isos/domain.py | 4 ++-- hcloud/load_balancers/domain.py | 4 ++-- hcloud/networks/domain.py | 4 ++-- hcloud/placement_groups/domain.py | 4 ++-- hcloud/primary_ips/domain.py | 4 ++-- hcloud/servers/domain.py | 4 ++-- hcloud/ssh_keys/domain.py | 4 ++-- hcloud/volumes/domain.py | 4 ++-- setup.py | 1 - tests/unit/actions/test_domain.py | 7 +++---- tests/unit/certificates/test_domain.py | 9 ++++----- tests/unit/core/test_domain.py | 7 ++++--- tests/unit/firewalls/test_domain.py | 5 ++--- tests/unit/floating_ips/test_domain.py | 5 ++--- tests/unit/images/test_client.py | 6 +++--- tests/unit/images/test_domain.py | 5 ++--- tests/unit/isos/test_client.py | 4 ++-- tests/unit/isos/test_domain.py | 5 ++--- tests/unit/load_balancers/test_domain.py | 7 ++----- tests/unit/networks/test_client.py | 6 ++++-- tests/unit/networks/test_domain.py | 5 ++--- tests/unit/placement_groups/test_domain.py | 5 ++--- tests/unit/primary_ips/test_domain.py | 5 ++--- tests/unit/servers/test_domain.py | 5 ++--- tests/unit/ssh_keys/test_domain.py | 5 ++--- tests/unit/volumes/test_client.py | 6 ++++-- tests/unit/volumes/test_domain.py | 5 ++--- 33 files changed, 83 insertions(+), 90 deletions(-) diff --git a/hcloud/actions/domain.py b/hcloud/actions/domain.py index e004be7e..c3ea4cc6 100644 --- a/hcloud/actions/domain.py +++ b/hcloud/actions/domain.py @@ -1,4 +1,4 @@ -from dateutil.parser import isoparse +from datetime import datetime from hcloud.core.domain import BaseDomain @@ -52,8 +52,8 @@ def __init__( self.status = status self.progress = progress - self.started = isoparse(started) if started else None - self.finished = isoparse(finished) if finished else None + self.started = datetime.fromisoformat(started) if started else None + self.finished = datetime.fromisoformat(finished) if finished else None self.resources = resources self.error = error diff --git a/hcloud/certificates/domain.py b/hcloud/certificates/domain.py index 46f13392..797d3068 100644 --- a/hcloud/certificates/domain.py +++ b/hcloud/certificates/domain.py @@ -1,4 +1,4 @@ -from dateutil.parser import isoparse +from datetime import datetime from hcloud.core.domain import BaseDomain, DomainIdentityMixin @@ -59,9 +59,13 @@ def __init__( self.certificate = certificate self.domain_names = domain_names self.fingerprint = fingerprint - self.not_valid_before = isoparse(not_valid_before) if not_valid_before else None - self.not_valid_after = isoparse(not_valid_after) if not_valid_after else None - self.created = isoparse(created) if created else None + self.not_valid_before = ( + datetime.fromisoformat(not_valid_before) if not_valid_before else None + ) + self.not_valid_after = ( + datetime.fromisoformat(not_valid_after) if not_valid_after else None + ) + self.created = datetime.fromisoformat(created) if created else None self.labels = labels self.status = status diff --git a/hcloud/deprecation/domain.py b/hcloud/deprecation/domain.py index da967742..e7fa3384 100644 --- a/hcloud/deprecation/domain.py +++ b/hcloud/deprecation/domain.py @@ -1,4 +1,4 @@ -from dateutil.parser import isoparse +from datetime import datetime from hcloud.core.domain import BaseDomain @@ -25,7 +25,7 @@ def __init__( announced=None, unavailable_after=None, ): - self.announced = isoparse(announced) if announced else None + self.announced = datetime.fromisoformat(announced) if announced else None self.unavailable_after = ( - isoparse(unavailable_after) if unavailable_after else None + datetime.fromisoformat(unavailable_after) if unavailable_after else None ) diff --git a/hcloud/firewalls/domain.py b/hcloud/firewalls/domain.py index 07cfadde..5390cce7 100644 --- a/hcloud/firewalls/domain.py +++ b/hcloud/firewalls/domain.py @@ -1,4 +1,4 @@ -from dateutil.parser import isoparse +from datetime import datetime from hcloud.core.domain import BaseDomain @@ -30,7 +30,7 @@ def __init__( self.rules = rules self.applied_to = applied_to self.labels = labels - self.created = isoparse(created) if created else None + self.created = datetime.fromisoformat(created) if created else None class FirewallRule: diff --git a/hcloud/floating_ips/domain.py b/hcloud/floating_ips/domain.py index 01fce179..ea1884e9 100644 --- a/hcloud/floating_ips/domain.py +++ b/hcloud/floating_ips/domain.py @@ -1,4 +1,4 @@ -from dateutil.parser import isoparse +from datetime import datetime from hcloud.core.domain import BaseDomain @@ -72,7 +72,7 @@ def __init__( self.blocked = blocked self.protection = protection self.labels = labels - self.created = isoparse(created) if created else None + self.created = datetime.fromisoformat(created) if created else None self.name = name diff --git a/hcloud/images/domain.py b/hcloud/images/domain.py index 6e387b6a..67ca9cfa 100644 --- a/hcloud/images/domain.py +++ b/hcloud/images/domain.py @@ -1,4 +1,4 @@ -from dateutil.parser import isoparse +from datetime import datetime from hcloud.core.domain import BaseDomain, DomainIdentityMixin @@ -85,11 +85,11 @@ def __init__( self.id = id self.name = name self.type = type - self.created = isoparse(created) if created else None + self.created = datetime.fromisoformat(created) if created else None self.description = description self.image_size = image_size self.disk_size = disk_size - self.deprecated = isoparse(deprecated) if deprecated else None + self.deprecated = datetime.fromisoformat(deprecated) if deprecated else None self.bound_to = bound_to self.os_flavor = os_flavor self.os_version = os_version diff --git a/hcloud/isos/domain.py b/hcloud/isos/domain.py index 906b42d5..52a97817 100644 --- a/hcloud/isos/domain.py +++ b/hcloud/isos/domain.py @@ -1,4 +1,4 @@ -from dateutil.parser import isoparse +from datetime import datetime from hcloud.core.domain import BaseDomain, DomainIdentityMixin @@ -36,4 +36,4 @@ def __init__( self.type = type self.architecture = architecture self.description = description - self.deprecated = isoparse(deprecated) if deprecated else None + self.deprecated = datetime.fromisoformat(deprecated) if deprecated else None diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index 07297344..c57913e9 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -1,4 +1,4 @@ -from dateutil.parser import isoparse +from datetime import datetime from hcloud.core.domain import BaseDomain @@ -76,7 +76,7 @@ def __init__( ): self.id = id self.name = name - self.created = isoparse(created) if created else None + self.created = datetime.fromisoformat(created) if created else None self.public_net = public_net self.private_net = private_net self.location = location diff --git a/hcloud/networks/domain.py b/hcloud/networks/domain.py index be203ecd..a013278e 100644 --- a/hcloud/networks/domain.py +++ b/hcloud/networks/domain.py @@ -1,4 +1,4 @@ -from dateutil.parser import isoparse +from datetime import datetime from hcloud.core.domain import BaseDomain @@ -54,7 +54,7 @@ def __init__( ): self.id = id self.name = name - self.created = isoparse(created) if created else None + self.created = datetime.fromisoformat(created) if created else None self.ip_range = ip_range self.subnets = subnets self.routes = routes diff --git a/hcloud/placement_groups/domain.py b/hcloud/placement_groups/domain.py index e3ee0c6b..ffc262a5 100644 --- a/hcloud/placement_groups/domain.py +++ b/hcloud/placement_groups/domain.py @@ -1,4 +1,4 @@ -from dateutil.parser import isoparse +from datetime import datetime from hcloud.core.domain import BaseDomain @@ -35,7 +35,7 @@ def __init__( self.labels = labels self.servers = servers self.type = type - self.created = isoparse(created) if created else None + self.created = datetime.fromisoformat(created) if created else None class CreatePlacementGroupResponse(BaseDomain): diff --git a/hcloud/primary_ips/domain.py b/hcloud/primary_ips/domain.py index e4741493..7ab39893 100644 --- a/hcloud/primary_ips/domain.py +++ b/hcloud/primary_ips/domain.py @@ -1,4 +1,4 @@ -from dateutil.parser import isoparse +from datetime import datetime from hcloud.core.domain import BaseDomain @@ -74,7 +74,7 @@ def __init__( self.blocked = blocked self.protection = protection self.labels = labels - self.created = isoparse(created) if created else None + self.created = datetime.fromisoformat(created) if created else None self.name = name self.assignee_id = assignee_id self.assignee_type = assignee_type diff --git a/hcloud/servers/domain.py b/hcloud/servers/domain.py index a2aabd3b..fc7e819a 100644 --- a/hcloud/servers/domain.py +++ b/hcloud/servers/domain.py @@ -1,4 +1,4 @@ -from dateutil.parser import isoparse +from datetime import datetime from hcloud.core.domain import BaseDomain @@ -113,7 +113,7 @@ def __init__( self.id = id self.name = name self.status = status - self.created = isoparse(created) if created else None + self.created = datetime.fromisoformat(created) if created else None self.public_net = public_net self.server_type = server_type self.datacenter = datacenter diff --git a/hcloud/ssh_keys/domain.py b/hcloud/ssh_keys/domain.py index 84d89f42..d57111c9 100644 --- a/hcloud/ssh_keys/domain.py +++ b/hcloud/ssh_keys/domain.py @@ -1,4 +1,4 @@ -from dateutil.parser import isoparse +from datetime import datetime from hcloud.core.domain import BaseDomain, DomainIdentityMixin @@ -36,4 +36,4 @@ def __init__( self.fingerprint = fingerprint self.public_key = public_key self.labels = labels - self.created = isoparse(created) if created else None + self.created = datetime.fromisoformat(created) if created else None diff --git a/hcloud/volumes/domain.py b/hcloud/volumes/domain.py index 71abeaee..b0a98a25 100644 --- a/hcloud/volumes/domain.py +++ b/hcloud/volumes/domain.py @@ -1,4 +1,4 @@ -from dateutil.parser import isoparse +from datetime import datetime from hcloud.core.domain import BaseDomain, DomainIdentityMixin @@ -66,7 +66,7 @@ def __init__( self.id = id self.name = name self.server = server - self.created = isoparse(created) if created else None + self.created = datetime.fromisoformat(created) if created else None self.location = location self.size = size self.linux_device = linux_device diff --git a/setup.py b/setup.py index d0cdbf56..bb1945da 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,6 @@ ], python_requires=">=3.7", install_requires=[ - "python-dateutil>=2.7.5", "requests>=2.20", ], extras_require={ diff --git a/tests/unit/actions/test_domain.py b/tests/unit/actions/test_domain.py index b15d04e7..02959da7 100644 --- a/tests/unit/actions/test_domain.py +++ b/tests/unit/actions/test_domain.py @@ -1,6 +1,5 @@ import datetime - -from dateutil.tz import tzoffset +from datetime import timezone from hcloud.actions.domain import Action @@ -11,8 +10,8 @@ def test_started_finished_is_datetime(self): id=1, started="2016-01-30T23:50+00:00", finished="2016-03-30T23:50+00:00" ) assert action.started == datetime.datetime( - 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) + 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) assert action.finished == datetime.datetime( - 2016, 3, 30, 23, 50, tzinfo=tzoffset(None, 0) + 2016, 3, 30, 23, 50, tzinfo=timezone.utc ) diff --git a/tests/unit/certificates/test_domain.py b/tests/unit/certificates/test_domain.py index 30cdee0f..f2ec28df 100644 --- a/tests/unit/certificates/test_domain.py +++ b/tests/unit/certificates/test_domain.py @@ -1,6 +1,5 @@ import datetime - -from dateutil.tz import tzoffset +from datetime import timezone from hcloud.certificates.domain import Certificate @@ -14,11 +13,11 @@ def test_created_is_datetime(self): not_valid_before="2016-01-30T23:50+00:00", ) assert certificate.created == datetime.datetime( - 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) + 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) assert certificate.not_valid_after == datetime.datetime( - 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) + 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) assert certificate.not_valid_before == datetime.datetime( - 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) + 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) diff --git a/tests/unit/core/test_domain.py b/tests/unit/core/test_domain.py index 9e71cee4..09aa6bab 100644 --- a/tests/unit/core/test_domain.py +++ b/tests/unit/core/test_domain.py @@ -1,5 +1,6 @@ +from datetime import datetime + import pytest -from dateutil.parser import isoparse from hcloud.core.domain import ( BaseDomain, @@ -101,7 +102,7 @@ class ActionDomain(BaseDomain, DomainIdentityMixin): def __init__(self, id, name="name1", started=None): self.id = id self.name = name - self.started = isoparse(started) if started else None + self.started = datetime.fromisoformat(started) if started else None class TestBaseDomain: @@ -125,7 +126,7 @@ class TestBaseDomain: { "id": 4, "name": "name-name3", - "started": isoparse("2016-01-30T23:50+00:00"), + "started": datetime.fromisoformat("2016-01-30T23:50+00:00"), }, ), ], diff --git a/tests/unit/firewalls/test_domain.py b/tests/unit/firewalls/test_domain.py index 11170adc..bcbfea3e 100644 --- a/tests/unit/firewalls/test_domain.py +++ b/tests/unit/firewalls/test_domain.py @@ -1,6 +1,5 @@ import datetime - -from dateutil.tz import tzoffset +from datetime import timezone from hcloud.firewalls.domain import Firewall @@ -9,5 +8,5 @@ class TestFirewall: def test_created_is_datetime(self): firewall = Firewall(id=1, created="2016-01-30T23:50+00:00") assert firewall.created == datetime.datetime( - 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) + 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) diff --git a/tests/unit/floating_ips/test_domain.py b/tests/unit/floating_ips/test_domain.py index 293cafca..3480ba52 100644 --- a/tests/unit/floating_ips/test_domain.py +++ b/tests/unit/floating_ips/test_domain.py @@ -1,6 +1,5 @@ import datetime - -from dateutil.tz import tzoffset +from datetime import timezone from hcloud.floating_ips.domain import FloatingIP @@ -9,5 +8,5 @@ class TestFloatingIP: def test_created_is_datetime(self): floatingIP = FloatingIP(id=1, created="2016-01-30T23:50+00:00") assert floatingIP.created == datetime.datetime( - 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) + 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) diff --git a/tests/unit/images/test_client.py b/tests/unit/images/test_client.py index 53ca5447..94ef4d0d 100644 --- a/tests/unit/images/test_client.py +++ b/tests/unit/images/test_client.py @@ -1,8 +1,8 @@ import datetime +from datetime import timezone from unittest import mock import pytest -from dateutil.tz import tzoffset from hcloud.actions.client import BoundAction from hcloud.images.client import BoundImage, ImagesClient @@ -26,14 +26,14 @@ def test_bound_image_init(self, image_response): assert bound_image.image_size == 2.3 assert bound_image.disk_size == 10 assert bound_image.created == datetime.datetime( - 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) + 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) assert bound_image.os_flavor == "ubuntu" assert bound_image.os_version == "16.04" assert bound_image.architecture == "x86" assert bound_image.rapid_deploy is False assert bound_image.deprecated == datetime.datetime( - 2018, 2, 28, 0, 0, tzinfo=tzoffset(None, 0) + 2018, 2, 28, 0, 0, tzinfo=timezone.utc ) assert isinstance(bound_image.created_from, BoundServer) diff --git a/tests/unit/images/test_domain.py b/tests/unit/images/test_domain.py index 939fa14a..4808ff94 100644 --- a/tests/unit/images/test_domain.py +++ b/tests/unit/images/test_domain.py @@ -1,6 +1,5 @@ import datetime - -from dateutil.tz import tzoffset +from datetime import timezone from hcloud.images.domain import Image @@ -9,5 +8,5 @@ class TestImage: def test_created_is_datetime(self): image = Image(id=1, created="2016-01-30T23:50+00:00") assert image.created == datetime.datetime( - 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) + 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) diff --git a/tests/unit/isos/test_client.py b/tests/unit/isos/test_client.py index 0550ca5b..13bc2426 100644 --- a/tests/unit/isos/test_client.py +++ b/tests/unit/isos/test_client.py @@ -1,8 +1,8 @@ import datetime +from datetime import timezone from unittest import mock import pytest -from dateutil.tz import tzoffset from hcloud.isos.client import BoundIso, IsosClient @@ -21,7 +21,7 @@ def test_bound_iso_init(self, iso_response): assert bound_iso.type == "public" assert bound_iso.architecture == "x86" assert bound_iso.deprecated == datetime.datetime( - 2018, 2, 28, 0, 0, tzinfo=tzoffset(None, 0) + 2018, 2, 28, 0, 0, tzinfo=timezone.utc ) diff --git a/tests/unit/isos/test_domain.py b/tests/unit/isos/test_domain.py index da1c1dee..aceead55 100644 --- a/tests/unit/isos/test_domain.py +++ b/tests/unit/isos/test_domain.py @@ -1,6 +1,5 @@ import datetime - -from dateutil.tz import tzoffset +from datetime import timezone from hcloud.isos.domain import Iso @@ -9,5 +8,5 @@ class TestIso: def test_deprecated_is_datetime(self): iso = Iso(id=1, deprecated="2016-01-30T23:50+00:00") assert iso.deprecated == datetime.datetime( - 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) + 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) diff --git a/tests/unit/load_balancers/test_domain.py b/tests/unit/load_balancers/test_domain.py index 2f29cce3..cff4127c 100644 --- a/tests/unit/load_balancers/test_domain.py +++ b/tests/unit/load_balancers/test_domain.py @@ -1,6 +1,5 @@ import datetime - -from dateutil.tz import tzoffset +from datetime import timezone from hcloud.load_balancers.domain import LoadBalancer @@ -8,6 +7,4 @@ class TestLoadBalancers: def test_created_is_datetime(self): lb = LoadBalancer(id=1, created="2016-01-30T23:50+00:00") - assert lb.created == datetime.datetime( - 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) - ) + assert lb.created == datetime.datetime(2016, 1, 30, 23, 50, tzinfo=timezone.utc) diff --git a/tests/unit/networks/test_client.py b/tests/unit/networks/test_client.py index 5d70d99a..649094f9 100644 --- a/tests/unit/networks/test_client.py +++ b/tests/unit/networks/test_client.py @@ -1,7 +1,7 @@ +from datetime import datetime from unittest import mock import pytest -from dateutil.parser import isoparse from hcloud.actions.client import BoundAction from hcloud.networks.client import BoundNetwork, NetworksClient @@ -20,7 +20,9 @@ def test_bound_network_init(self, network_response): ) assert bound_network.id == 1 - assert bound_network.created == isoparse("2016-01-30T23:50:11+00:00") + assert bound_network.created == datetime.fromisoformat( + "2016-01-30T23:50:11+00:00" + ) assert bound_network.name == "mynet" assert bound_network.ip_range == "10.0.0.0/16" assert bound_network.protection["delete"] is False diff --git a/tests/unit/networks/test_domain.py b/tests/unit/networks/test_domain.py index 4740ff82..a704dfda 100644 --- a/tests/unit/networks/test_domain.py +++ b/tests/unit/networks/test_domain.py @@ -1,6 +1,5 @@ import datetime - -from dateutil.tz import tzoffset +from datetime import timezone from hcloud.networks.domain import Network @@ -9,5 +8,5 @@ class TestNetwork: def test_created_is_datetime(self): network = Network(id=1, created="2016-01-30T23:50+00:00") assert network.created == datetime.datetime( - 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) + 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) diff --git a/tests/unit/placement_groups/test_domain.py b/tests/unit/placement_groups/test_domain.py index 7f0eba47..779304e7 100644 --- a/tests/unit/placement_groups/test_domain.py +++ b/tests/unit/placement_groups/test_domain.py @@ -1,6 +1,5 @@ import datetime - -from dateutil.tz import tzoffset +from datetime import timezone from hcloud.placement_groups.domain import PlacementGroup @@ -9,5 +8,5 @@ class TestPlacementGroup: def test_created_is_datetime(self): placement_group = PlacementGroup(id=1, created="2016-01-30T23:50+00:00") assert placement_group.created == datetime.datetime( - 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) + 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) diff --git a/tests/unit/primary_ips/test_domain.py b/tests/unit/primary_ips/test_domain.py index bf2bd6d9..091ea029 100644 --- a/tests/unit/primary_ips/test_domain.py +++ b/tests/unit/primary_ips/test_domain.py @@ -1,6 +1,5 @@ import datetime - -from dateutil.tz import tzoffset +from datetime import timezone from hcloud.primary_ips.domain import PrimaryIP @@ -9,5 +8,5 @@ class TestPrimaryIP: def test_created_is_datetime(self): primaryIP = PrimaryIP(id=1, created="2016-01-30T23:50+00:00") assert primaryIP.created == datetime.datetime( - 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) + 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) diff --git a/tests/unit/servers/test_domain.py b/tests/unit/servers/test_domain.py index 10039104..fc2bdeba 100644 --- a/tests/unit/servers/test_domain.py +++ b/tests/unit/servers/test_domain.py @@ -1,6 +1,5 @@ import datetime - -from dateutil.tz import tzoffset +from datetime import timezone from hcloud.servers.domain import Server @@ -9,5 +8,5 @@ class TestServer: def test_created_is_datetime(self): server = Server(id=1, created="2016-01-30T23:50+00:00") assert server.created == datetime.datetime( - 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) + 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) diff --git a/tests/unit/ssh_keys/test_domain.py b/tests/unit/ssh_keys/test_domain.py index 017ffca8..9bea981a 100644 --- a/tests/unit/ssh_keys/test_domain.py +++ b/tests/unit/ssh_keys/test_domain.py @@ -1,6 +1,5 @@ import datetime - -from dateutil.tz import tzoffset +from datetime import timezone from hcloud.ssh_keys.domain import SSHKey @@ -9,5 +8,5 @@ class TestSSHKey: def test_created_is_datetime(self): sshKey = SSHKey(id=1, created="2016-01-30T23:50+00:00") assert sshKey.created == datetime.datetime( - 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) + 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) diff --git a/tests/unit/volumes/test_client.py b/tests/unit/volumes/test_client.py index fda74f99..8065fe26 100644 --- a/tests/unit/volumes/test_client.py +++ b/tests/unit/volumes/test_client.py @@ -1,7 +1,7 @@ +from datetime import datetime from unittest import mock import pytest -from dateutil.parser import isoparse from hcloud.actions.client import BoundAction from hcloud.locations.client import BoundLocation @@ -23,7 +23,9 @@ def test_bound_volume_init(self, volume_response): ) assert bound_volume.id == 1 - assert bound_volume.created == isoparse("2016-01-30T23:50:11+00:00") + assert bound_volume.created == datetime.fromisoformat( + "2016-01-30T23:50:11+00:00" + ) assert bound_volume.name == "database-storage" assert isinstance(bound_volume.server, BoundServer) assert bound_volume.server.id == 12 diff --git a/tests/unit/volumes/test_domain.py b/tests/unit/volumes/test_domain.py index 5587e3fb..48946305 100644 --- a/tests/unit/volumes/test_domain.py +++ b/tests/unit/volumes/test_domain.py @@ -1,6 +1,5 @@ import datetime - -from dateutil.tz import tzoffset +from datetime import timezone from hcloud.volumes.domain import Volume @@ -9,5 +8,5 @@ class TestVolume: def test_created_is_datetime(self): volume = Volume(id=1, created="2016-01-30T23:50+00:00") assert volume.created == datetime.datetime( - 2016, 1, 30, 23, 50, tzinfo=tzoffset(None, 0) + 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) From c3dfcaba44d88fcf6913a6e68caee2afde06e551 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 26 Jun 2023 15:30:25 +0200 Subject: [PATCH 045/406] fix(isos): invalid name for include_wildcard_architecture argument (#222) --- hcloud/isos/client.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/hcloud/isos/client.py b/hcloud/isos/client.py index a7876dc7..1fbe10a5 100644 --- a/hcloud/isos/client.py +++ b/hcloud/isos/client.py @@ -1,3 +1,5 @@ +from warnings import warn + from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from hcloud.isos.domain import Iso @@ -24,6 +26,7 @@ def get_list( 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] ): @@ -35,6 +38,8 @@ def get_list( :param architecture: List[str] (optional) Can be used to filter ISOs by their architecture. Choices: x86 arm :param include_wildcard_architecture: bool (optional) + Deprecated, please use `include_architecture_wildcard` instead. + :param include_architecture_wildcard: bool (optional) Custom ISOs do not have an architecture set. You must also set this flag to True if you are filtering by architecture and also want custom ISOs. :param page: int (optional) @@ -43,13 +48,21 @@ def get_list( Specifies how many results are returned by page :return: (List[:class:`BoundIso `], :class:`Meta `) """ + + if include_wildcard_architecture is not None: + warn( + "The `include_wildcard_architecture` argument is deprecated, please use the `include_architecture_wildcard` argument instead.", + DeprecationWarning, + ) + include_architecture_wildcard = include_wildcard_architecture + params = {} if name is not None: params["name"] = name if architecture is not None: params["architecture"] = architecture - if include_wildcard_architecture is not None: - params["include_wildcard_architecture"] = include_wildcard_architecture + if include_architecture_wildcard is not None: + params["include_architecture_wildcard"] = include_architecture_wildcard if page is not None: params["page"] = page if per_page is not None: @@ -64,6 +77,7 @@ def get_all( 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] """Get all ISOs @@ -73,14 +87,24 @@ def get_all( :param architecture: List[str] (optional) Can be used to filter ISOs by their architecture. Choices: x86 arm :param include_wildcard_architecture: bool (optional) + Deprecated, please use `include_architecture_wildcard` instead. + :param include_architecture_wildcard: bool (optional) Custom ISOs do not have an architecture set. You must also set this flag to True if you are filtering by architecture and also want custom ISOs. :return: List[:class:`BoundIso `] """ + + if include_wildcard_architecture is not None: + warn( + "The `include_wildcard_architecture` argument is deprecated, please use the `include_architecture_wildcard` argument instead.", + DeprecationWarning, + ) + include_architecture_wildcard = include_wildcard_architecture + return super().get_all( name=name, architecture=architecture, - include_wildcard_architecture=include_wildcard_architecture, + include_architecture_wildcard=include_architecture_wildcard, ) def get_by_name(self, name): From ac5c36bc529e9ce39dc01df81e4990091e3e7f05 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Thu, 29 Jun 2023 13:52:13 +0200 Subject: [PATCH 046/406] chore(main): release 1.23.0 (#219) --- CHANGELOG.md | 17 +++++++++++++++++ hcloud/__version__.py | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd0c3f96..eac6b009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [1.23.0](https://github.com/hetznercloud/hcloud-python/compare/v1.22.0...v1.23.0) (2023-06-26) + + +### Features + +* remove python-dateutil dependency ([#221](https://github.com/hetznercloud/hcloud-python/issues/221)) ([8ea4aa0](https://github.com/hetznercloud/hcloud-python/commit/8ea4aa0ad12e85eeb14c81dfa2195e1a6ee79a76)) + + +### Bug Fixes + +* **isos:** invalid name for include_wildcard_architecture argument ([#222](https://github.com/hetznercloud/hcloud-python/issues/222)) ([c3dfcab](https://github.com/hetznercloud/hcloud-python/commit/c3dfcaba44d88fcf6913a6e68caee2afde06e551)) + + +### Dependencies + +* update dependency pytest to >=7.4,<7.5 ([#217](https://github.com/hetznercloud/hcloud-python/issues/217)) ([11e1f45](https://github.com/hetznercloud/hcloud-python/commit/11e1f455611b17a22328b3422d0b800552ea91e3)) + ## [1.22.0](https://github.com/hetznercloud/hcloud-python/compare/v1.21.0...v1.22.0) (2023-06-22) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index c408c196..7618a1cf 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1 +1 @@ -VERSION = "1.22.0" # x-release-please-version +VERSION = "1.23.0" # x-release-please-version From 2b6c46abe8278569e527b30e7cde3da282a1385e Mon Sep 17 00:00:00 2001 From: Jonas L Date: Thu, 29 Jun 2023 14:41:10 +0200 Subject: [PATCH 047/406] chore: update README links (#224) --- README.rst | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 478e04e4..9accd0d4 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +1,10 @@ Hetzner Cloud Python ==================== - -.. image:: https://github.com/hetznercloud/hcloud-python/workflows/Unit%20Tests/badge.svg - :target: https://github.com/hetznercloud/hcloud-cloud-controller-manager/actions -.. image:: https://github.com/hetznercloud/hcloud-python/workflows/Code%20Style/badge.svg - :target: https://github.com/hetznercloud/hcloud-cloud-controller-manager/actions +.. image:: https://github.com/hetznercloud/hcloud-python/actions/workflows/test.yml/badge.svg + :target: https://github.com/hetznercloud/hcloud-python/actions/workflows/test.yml +.. image:: https://github.com/hetznercloud/hcloud-python/actions/workflows/lint.yml/badge.svg + :target: https://github.com/hetznercloud/hcloud-python/actions/workflows/lint.yml .. image:: https://readthedocs.org/projects/hcloud-python/badge/?version=latest :target: https://hcloud-python.readthedocs.io .. image:: https://img.shields.io/pypi/pyversions/hcloud.svg @@ -32,7 +31,7 @@ Supported Python versions We support python versions until `end-of-life`_. -.. _end-of-life: https://devguide.python.org/#status-of-python-branches +.. _end-of-life: https://devguide.python.org/versions/#status-of-python-versions Development ----------- @@ -64,4 +63,4 @@ License ------------- The MIT License (MIT). Please see `License File`_ for more information. -.. _License File: https://github.com/hetznercloud/hcloud-python/blob/master/LICENSE +.. _License File: https://github.com/hetznercloud/hcloud-python/blob/main/LICENSE From c821dbe3e2bec59211154dd52bafe3b930945907 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Thu, 29 Jun 2023 14:55:19 +0200 Subject: [PATCH 048/406] refactor: use relative imports within the package (#225) --- hcloud/actions/client.py | 4 ++-- hcloud/actions/domain.py | 3 +-- hcloud/certificates/client.py | 8 +++---- hcloud/certificates/domain.py | 2 +- hcloud/core/client.py | 2 +- hcloud/datacenters/client.py | 8 +++---- hcloud/datacenters/domain.py | 2 +- hcloud/deprecation/domain.py | 2 +- hcloud/firewalls/client.py | 10 ++++---- hcloud/firewalls/domain.py | 2 +- hcloud/floating_ips/client.py | 12 +++++----- hcloud/floating_ips/domain.py | 2 +- hcloud/hcloud.py | 35 ++++++++++++++-------------- hcloud/images/client.py | 10 ++++---- hcloud/images/domain.py | 2 +- hcloud/isos/client.py | 4 ++-- hcloud/isos/domain.py | 2 +- hcloud/load_balancer_types/client.py | 4 ++-- hcloud/load_balancer_types/domain.py | 2 +- hcloud/load_balancers/client.py | 18 +++++++------- hcloud/load_balancers/domain.py | 2 +- hcloud/locations/client.py | 4 ++-- hcloud/locations/domain.py | 2 +- hcloud/networks/client.py | 10 ++++---- hcloud/networks/domain.py | 2 +- hcloud/placement_groups/client.py | 6 ++--- hcloud/placement_groups/domain.py | 2 +- hcloud/primary_ips/client.py | 8 +++---- hcloud/primary_ips/domain.py | 2 +- hcloud/server_types/client.py | 4 ++-- hcloud/server_types/domain.py | 4 ++-- hcloud/servers/client.py | 32 ++++++++++++------------- hcloud/servers/domain.py | 2 +- hcloud/ssh_keys/client.py | 4 ++-- hcloud/ssh_keys/domain.py | 2 +- hcloud/volumes/client.py | 12 +++++----- hcloud/volumes/domain.py | 2 +- 37 files changed, 116 insertions(+), 118 deletions(-) diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index b5075287..2186b656 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -1,7 +1,7 @@ import time -from hcloud.actions.domain import Action, ActionFailedException, ActionTimeoutException -from hcloud.core.client import BoundModelBase, ClientEntityBase +from ..core.client import BoundModelBase, ClientEntityBase +from .domain import Action, ActionFailedException, ActionTimeoutException class BoundAction(BoundModelBase): diff --git a/hcloud/actions/domain.py b/hcloud/actions/domain.py index c3ea4cc6..ef77fc7e 100644 --- a/hcloud/actions/domain.py +++ b/hcloud/actions/domain.py @@ -1,8 +1,7 @@ from datetime import datetime -from hcloud.core.domain import BaseDomain - from .._exceptions import HCloudException +from ..core.domain import BaseDomain class Action(BaseDomain): diff --git a/hcloud/certificates/client.py b/hcloud/certificates/client.py index 5d673f9c..1ab18bdc 100644 --- a/hcloud/certificates/client.py +++ b/hcloud/certificates/client.py @@ -1,12 +1,12 @@ -from hcloud.actions.client import BoundAction -from hcloud.certificates.domain import ( +from ..actions.client import BoundAction +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import add_meta_to_result +from .domain import ( Certificate, CreateManagedCertificateResponse, ManagedCertificateError, ManagedCertificateStatus, ) -from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.core.domain import add_meta_to_result class BoundCertificate(BoundModelBase): diff --git a/hcloud/certificates/domain.py b/hcloud/certificates/domain.py index 797d3068..0ee92e6f 100644 --- a/hcloud/certificates/domain.py +++ b/hcloud/certificates/domain.py @@ -1,6 +1,6 @@ from datetime import datetime -from hcloud.core.domain import BaseDomain, DomainIdentityMixin +from ..core.domain import BaseDomain, DomainIdentityMixin class Certificate(BaseDomain, DomainIdentityMixin): diff --git a/hcloud/core/client.py b/hcloud/core/client.py index d3e1f43f..10a2115f 100644 --- a/hcloud/core/client.py +++ b/hcloud/core/client.py @@ -1,4 +1,4 @@ -from hcloud.core.domain import add_meta_to_result +from .domain import add_meta_to_result class ClientEntityBase: diff --git a/hcloud/datacenters/client.py b/hcloud/datacenters/client.py index 2f839e86..0ef212b4 100644 --- a/hcloud/datacenters/client.py +++ b/hcloud/datacenters/client.py @@ -1,7 +1,7 @@ -from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.datacenters.domain import Datacenter, DatacenterServerTypes -from hcloud.locations.client import BoundLocation -from hcloud.server_types.client import BoundServerType +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..locations.client import BoundLocation +from ..server_types.client import BoundServerType +from .domain import Datacenter, DatacenterServerTypes class BoundDatacenter(BoundModelBase): diff --git a/hcloud/datacenters/domain.py b/hcloud/datacenters/domain.py index 52f9ccde..984cc854 100644 --- a/hcloud/datacenters/domain.py +++ b/hcloud/datacenters/domain.py @@ -1,4 +1,4 @@ -from hcloud.core.domain import BaseDomain, DomainIdentityMixin +from ..core.domain import BaseDomain, DomainIdentityMixin class Datacenter(BaseDomain, DomainIdentityMixin): diff --git a/hcloud/deprecation/domain.py b/hcloud/deprecation/domain.py index e7fa3384..1679feb6 100644 --- a/hcloud/deprecation/domain.py +++ b/hcloud/deprecation/domain.py @@ -1,6 +1,6 @@ from datetime import datetime -from hcloud.core.domain import BaseDomain +from ..core.domain import BaseDomain class DeprecationInfo(BaseDomain): diff --git a/hcloud/firewalls/client.py b/hcloud/firewalls/client.py index 5e7cf741..5ff716d3 100644 --- a/hcloud/firewalls/client.py +++ b/hcloud/firewalls/client.py @@ -1,7 +1,7 @@ -from hcloud.actions.client import BoundAction -from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.core.domain import add_meta_to_result -from hcloud.firewalls.domain import ( +from ..actions.client import BoundAction +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import add_meta_to_result +from .domain import ( CreateFirewallResponse, Firewall, FirewallResource, @@ -31,7 +31,7 @@ def __init__(self, client, data, complete=True): applied_to = data.get("applied_to", []) if applied_to: - from hcloud.servers.client import BoundServer + from ..servers.client import BoundServer ats = [] for a in applied_to: diff --git a/hcloud/firewalls/domain.py b/hcloud/firewalls/domain.py index 5390cce7..7a439236 100644 --- a/hcloud/firewalls/domain.py +++ b/hcloud/firewalls/domain.py @@ -1,6 +1,6 @@ from datetime import datetime -from hcloud.core.domain import BaseDomain +from ..core.domain import BaseDomain class Firewall(BaseDomain): diff --git a/hcloud/floating_ips/client.py b/hcloud/floating_ips/client.py index e930e357..d5d7a16e 100644 --- a/hcloud/floating_ips/client.py +++ b/hcloud/floating_ips/client.py @@ -1,15 +1,15 @@ -from hcloud.actions.client import BoundAction -from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.core.domain import add_meta_to_result -from hcloud.floating_ips.domain import CreateFloatingIPResponse, FloatingIP -from hcloud.locations.client import BoundLocation +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 .domain import CreateFloatingIPResponse, FloatingIP class BoundFloatingIP(BoundModelBase): model = FloatingIP def __init__(self, client, data, complete=True): - from hcloud.servers.client import BoundServer + from ..servers.client import BoundServer server = data.get("server") if server is not None: diff --git a/hcloud/floating_ips/domain.py b/hcloud/floating_ips/domain.py index ea1884e9..a268a439 100644 --- a/hcloud/floating_ips/domain.py +++ b/hcloud/floating_ips/domain.py @@ -1,6 +1,6 @@ from datetime import datetime -from hcloud.core.domain import BaseDomain +from ..core.domain import BaseDomain class FloatingIP(BaseDomain): diff --git a/hcloud/hcloud.py b/hcloud/hcloud.py index c06d4fbd..e8dfaad8 100644 --- a/hcloud/hcloud.py +++ b/hcloud/hcloud.py @@ -2,26 +2,25 @@ import requests -from hcloud.actions.client import ActionsClient -from hcloud.certificates.client import CertificatesClient -from hcloud.datacenters.client import DatacentersClient -from hcloud.firewalls.client import FirewallsClient -from hcloud.floating_ips.client import FloatingIPsClient -from hcloud.images.client import ImagesClient -from hcloud.isos.client import IsosClient -from hcloud.load_balancer_types.client import LoadBalancerTypesClient -from hcloud.load_balancers.client import LoadBalancersClient -from hcloud.locations.client import LocationsClient -from hcloud.networks.client import NetworksClient -from hcloud.placement_groups.client import PlacementGroupsClient -from hcloud.primary_ips.client import PrimaryIPsClient -from hcloud.server_types.client import ServerTypesClient -from hcloud.servers.client import ServersClient -from hcloud.ssh_keys.client import SSHKeysClient -from hcloud.volumes.client import VolumesClient - 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 class Client: diff --git a/hcloud/images/client.py b/hcloud/images/client.py index 8992ac38..30c10af6 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -1,14 +1,14 @@ -from hcloud.actions.client import BoundAction -from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.core.domain import add_meta_to_result -from hcloud.images.domain import Image +from ..actions.client import BoundAction +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import add_meta_to_result +from .domain import Image class BoundImage(BoundModelBase): model = Image def __init__(self, client, data): - from hcloud.servers.client import BoundServer + from ..servers.client import BoundServer created_from = data.get("created_from") if created_from is not None: diff --git a/hcloud/images/domain.py b/hcloud/images/domain.py index 67ca9cfa..2950d9d1 100644 --- a/hcloud/images/domain.py +++ b/hcloud/images/domain.py @@ -1,6 +1,6 @@ from datetime import datetime -from hcloud.core.domain import BaseDomain, DomainIdentityMixin +from ..core.domain import BaseDomain, DomainIdentityMixin class Image(BaseDomain, DomainIdentityMixin): diff --git a/hcloud/isos/client.py b/hcloud/isos/client.py index 1fbe10a5..4ca95282 100644 --- a/hcloud/isos/client.py +++ b/hcloud/isos/client.py @@ -1,7 +1,7 @@ from warnings import warn -from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.isos.domain import Iso +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from .domain import Iso class BoundIso(BoundModelBase): diff --git a/hcloud/isos/domain.py b/hcloud/isos/domain.py index 52a97817..63ca5f99 100644 --- a/hcloud/isos/domain.py +++ b/hcloud/isos/domain.py @@ -1,6 +1,6 @@ from datetime import datetime -from hcloud.core.domain import BaseDomain, DomainIdentityMixin +from ..core.domain import BaseDomain, DomainIdentityMixin class Iso(BaseDomain, DomainIdentityMixin): diff --git a/hcloud/load_balancer_types/client.py b/hcloud/load_balancer_types/client.py index 1b505a08..405e2b50 100644 --- a/hcloud/load_balancer_types/client.py +++ b/hcloud/load_balancer_types/client.py @@ -1,5 +1,5 @@ -from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.load_balancer_types.domain import LoadBalancerType +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from .domain import LoadBalancerType class BoundLoadBalancerType(BoundModelBase): diff --git a/hcloud/load_balancer_types/domain.py b/hcloud/load_balancer_types/domain.py index ecfffc0b..880d0885 100644 --- a/hcloud/load_balancer_types/domain.py +++ b/hcloud/load_balancer_types/domain.py @@ -1,4 +1,4 @@ -from hcloud.core.domain import BaseDomain, DomainIdentityMixin +from ..core.domain import BaseDomain, DomainIdentityMixin class LoadBalancerType(BaseDomain, DomainIdentityMixin): diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index 7b905ddf..25cc1343 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -1,9 +1,12 @@ -from hcloud.actions.client import BoundAction -from hcloud.certificates.client import BoundCertificate -from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.core.domain import add_meta_to_result -from hcloud.load_balancer_types.client import BoundLoadBalancerType -from hcloud.load_balancers.domain import ( +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 .domain import ( CreateLoadBalancerResponse, IPv4Address, IPv6Network, @@ -19,9 +22,6 @@ PrivateNet, PublicNetwork, ) -from hcloud.locations.client import BoundLocation -from hcloud.networks.client import BoundNetwork -from hcloud.servers.client import BoundServer class BoundLoadBalancer(BoundModelBase): diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index c57913e9..27e2a356 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -1,6 +1,6 @@ from datetime import datetime -from hcloud.core.domain import BaseDomain +from ..core.domain import BaseDomain class LoadBalancer(BaseDomain): diff --git a/hcloud/locations/client.py b/hcloud/locations/client.py index 03aebe2b..6171ed67 100644 --- a/hcloud/locations/client.py +++ b/hcloud/locations/client.py @@ -1,5 +1,5 @@ -from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.locations.domain import Location +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from .domain import Location class BoundLocation(BoundModelBase): diff --git a/hcloud/locations/domain.py b/hcloud/locations/domain.py index 366631dd..6ca16d6f 100644 --- a/hcloud/locations/domain.py +++ b/hcloud/locations/domain.py @@ -1,4 +1,4 @@ -from hcloud.core.domain import BaseDomain, DomainIdentityMixin +from ..core.domain import BaseDomain, DomainIdentityMixin class Location(BaseDomain, DomainIdentityMixin): diff --git a/hcloud/networks/client.py b/hcloud/networks/client.py index b7e22af6..17152052 100644 --- a/hcloud/networks/client.py +++ b/hcloud/networks/client.py @@ -1,7 +1,7 @@ -from hcloud.actions.client import BoundAction -from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.core.domain import add_meta_to_result -from hcloud.networks.domain import Network, NetworkRoute, NetworkSubnet +from ..actions.client import BoundAction +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import add_meta_to_result +from .domain import Network, NetworkRoute, NetworkSubnet class BoundNetwork(BoundModelBase): @@ -18,7 +18,7 @@ def __init__(self, client, data, complete=True): routes = [NetworkRoute.from_dict(route) for route in routes] data["routes"] = routes - from hcloud.servers.client import BoundServer + from ..servers.client import BoundServer servers = data.get("servers", []) if servers is not None: diff --git a/hcloud/networks/domain.py b/hcloud/networks/domain.py index a013278e..01012805 100644 --- a/hcloud/networks/domain.py +++ b/hcloud/networks/domain.py @@ -1,6 +1,6 @@ from datetime import datetime -from hcloud.core.domain import BaseDomain +from ..core.domain import BaseDomain class Network(BaseDomain): diff --git a/hcloud/placement_groups/client.py b/hcloud/placement_groups/client.py index 4965ed96..c5cb7c0b 100644 --- a/hcloud/placement_groups/client.py +++ b/hcloud/placement_groups/client.py @@ -1,6 +1,6 @@ -from hcloud.actions.client import BoundAction -from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.placement_groups.domain import CreatePlacementGroupResponse, PlacementGroup +from ..actions.client import BoundAction +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from .domain import CreatePlacementGroupResponse, PlacementGroup class BoundPlacementGroup(BoundModelBase): diff --git a/hcloud/placement_groups/domain.py b/hcloud/placement_groups/domain.py index ffc262a5..d013032e 100644 --- a/hcloud/placement_groups/domain.py +++ b/hcloud/placement_groups/domain.py @@ -1,6 +1,6 @@ from datetime import datetime -from hcloud.core.domain import BaseDomain +from ..core.domain import BaseDomain class PlacementGroup(BaseDomain): diff --git a/hcloud/primary_ips/client.py b/hcloud/primary_ips/client.py index d984506c..e8c8d672 100644 --- a/hcloud/primary_ips/client.py +++ b/hcloud/primary_ips/client.py @@ -1,13 +1,13 @@ -from hcloud.actions.client import BoundAction -from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.primary_ips.domain import CreatePrimaryIPResponse, PrimaryIP +from ..actions.client import BoundAction +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from .domain import CreatePrimaryIPResponse, PrimaryIP class BoundPrimaryIP(BoundModelBase): model = PrimaryIP def __init__(self, client, data, complete=True): - from hcloud.datacenters.client import BoundDatacenter + from ..datacenters.client import BoundDatacenter datacenter = data.get("datacenter", {}) if datacenter: diff --git a/hcloud/primary_ips/domain.py b/hcloud/primary_ips/domain.py index 7ab39893..cdf1f239 100644 --- a/hcloud/primary_ips/domain.py +++ b/hcloud/primary_ips/domain.py @@ -1,6 +1,6 @@ from datetime import datetime -from hcloud.core.domain import BaseDomain +from ..core.domain import BaseDomain class PrimaryIP(BaseDomain): diff --git a/hcloud/server_types/client.py b/hcloud/server_types/client.py index 4e646918..d572e7b4 100644 --- a/hcloud/server_types/client.py +++ b/hcloud/server_types/client.py @@ -1,5 +1,5 @@ -from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.server_types.domain import ServerType +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from .domain import ServerType class BoundServerType(BoundModelBase): diff --git a/hcloud/server_types/domain.py b/hcloud/server_types/domain.py index d584312c..bd2e26c3 100644 --- a/hcloud/server_types/domain.py +++ b/hcloud/server_types/domain.py @@ -1,5 +1,5 @@ -from hcloud.core.domain import BaseDomain, DomainIdentityMixin -from hcloud.deprecation.domain import DeprecationInfo +from ..core.domain import BaseDomain, DomainIdentityMixin +from ..deprecation.domain import DeprecationInfo class ServerType(BaseDomain, DomainIdentityMixin): diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index b2f9269f..b88c69b0 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -1,18 +1,19 @@ -from hcloud.actions.client import BoundAction -from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.core.domain import add_meta_to_result -from hcloud.datacenters.client import BoundDatacenter -from hcloud.firewalls.client import BoundFirewall -from hcloud.floating_ips.client import BoundFloatingIP -from hcloud.images.client import BoundImage -from hcloud.images.domain import CreateImageResponse -from hcloud.isos.client import BoundIso -from hcloud.networks.client import BoundNetwork # noqa -from hcloud.networks.domain import Network # noqa -from hcloud.placement_groups.client import BoundPlacementGroup -from hcloud.primary_ips.client import BoundPrimaryIP -from hcloud.server_types.client import BoundServerType -from hcloud.servers.domain import ( +from ..actions.client import BoundAction +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import add_meta_to_result +from ..datacenters.client import BoundDatacenter +from ..firewalls.client import BoundFirewall +from ..floating_ips.client import BoundFloatingIP +from ..images.client import BoundImage +from ..images.domain import CreateImageResponse +from ..isos.client import BoundIso +from ..networks.client import BoundNetwork # noqa +from ..networks.domain import Network # noqa +from ..placement_groups.client import BoundPlacementGroup +from ..primary_ips.client import BoundPrimaryIP +from ..server_types.client import BoundServerType +from ..volumes.client import BoundVolume +from .domain import ( CreateServerResponse, EnableRescueResponse, IPv4Address, @@ -24,7 +25,6 @@ ResetPasswordResponse, Server, ) -from hcloud.volumes.client import BoundVolume class BoundServer(BoundModelBase): diff --git a/hcloud/servers/domain.py b/hcloud/servers/domain.py index fc7e819a..15b81cda 100644 --- a/hcloud/servers/domain.py +++ b/hcloud/servers/domain.py @@ -1,6 +1,6 @@ from datetime import datetime -from hcloud.core.domain import BaseDomain +from ..core.domain import BaseDomain class Server(BaseDomain): diff --git a/hcloud/ssh_keys/client.py b/hcloud/ssh_keys/client.py index 2ecf642e..e9a94023 100644 --- a/hcloud/ssh_keys/client.py +++ b/hcloud/ssh_keys/client.py @@ -1,5 +1,5 @@ -from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.ssh_keys.domain import SSHKey +from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from .domain import SSHKey class BoundSSHKey(BoundModelBase): diff --git a/hcloud/ssh_keys/domain.py b/hcloud/ssh_keys/domain.py index d57111c9..530b7f8e 100644 --- a/hcloud/ssh_keys/domain.py +++ b/hcloud/ssh_keys/domain.py @@ -1,6 +1,6 @@ from datetime import datetime -from hcloud.core.domain import BaseDomain, DomainIdentityMixin +from ..core.domain import BaseDomain, DomainIdentityMixin class SSHKey(BaseDomain, DomainIdentityMixin): diff --git a/hcloud/volumes/client.py b/hcloud/volumes/client.py index 5fee1c88..f7f6bef5 100644 --- a/hcloud/volumes/client.py +++ b/hcloud/volumes/client.py @@ -1,8 +1,8 @@ -from hcloud.actions.client import BoundAction -from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.core.domain import add_meta_to_result -from hcloud.locations.client import BoundLocation -from hcloud.volumes.domain import CreateVolumeResponse, Volume +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 .domain import CreateVolumeResponse, Volume class BoundVolume(BoundModelBase): @@ -13,7 +13,7 @@ def __init__(self, client, data, complete=True): if location is not None: data["location"] = BoundLocation(client._client.locations, location) - from hcloud.servers.client import BoundServer + from ..servers.client import BoundServer server = data.get("server") if server is not None: diff --git a/hcloud/volumes/domain.py b/hcloud/volumes/domain.py index b0a98a25..953b4beb 100644 --- a/hcloud/volumes/domain.py +++ b/hcloud/volumes/domain.py @@ -1,6 +1,6 @@ from datetime import datetime -from hcloud.core.domain import BaseDomain, DomainIdentityMixin +from ..core.domain import BaseDomain, DomainIdentityMixin class Volume(BaseDomain, DomainIdentityMixin): From 6a5c3f42c092610c4a82cb79c0052499563549dc Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 30 Jun 2023 10:32:51 +0200 Subject: [PATCH 049/406] fix: handle Z timezone in ISO8601 datetime format (#228) * fix: handle Z timezone in ISO8601 datetime format Fixes #226 --- hcloud/_compat.py | 11 +++++++++ hcloud/actions/domain.py | 7 +++--- hcloud/certificates/domain.py | 13 ++++------- hcloud/deprecation/domain.py | 7 +++--- hcloud/firewalls/domain.py | 5 ++--- hcloud/floating_ips/domain.py | 5 ++--- hcloud/images/domain.py | 7 +++--- hcloud/isos/domain.py | 5 ++--- hcloud/load_balancers/domain.py | 5 ++--- hcloud/networks/domain.py | 5 ++--- hcloud/placement_groups/domain.py | 5 ++--- hcloud/primary_ips/domain.py | 5 ++--- hcloud/servers/domain.py | 5 ++--- hcloud/ssh_keys/domain.py | 5 ++--- hcloud/volumes/domain.py | 5 ++--- tests/unit/core/test_domain.py | 7 +++--- tests/unit/networks/test_client.py | 6 ++--- tests/unit/test_compat.py | 36 ++++++++++++++++++++++++++++++ tests/unit/volumes/test_client.py | 6 ++--- 19 files changed, 87 insertions(+), 63 deletions(-) create mode 100644 hcloud/_compat.py create mode 100644 tests/unit/test_compat.py diff --git a/hcloud/_compat.py b/hcloud/_compat.py new file mode 100644 index 00000000..6eb552d1 --- /dev/null +++ b/hcloud/_compat.py @@ -0,0 +1,11 @@ +from datetime import datetime + + +def isoparse(value: str) -> datetime: + # Python <3.11 doesn't fully support parsing ISO8601 datetime strings. This + # workaround replaces the ending `Z` or `z` with `+00:00` and allows + # `datetime.fromisoformat` to parse the datetime string. + if value[-1] in "Zz": + value = value[:-1] + "+00:00" + + return datetime.fromisoformat(value) diff --git a/hcloud/actions/domain.py b/hcloud/actions/domain.py index ef77fc7e..865a4051 100644 --- a/hcloud/actions/domain.py +++ b/hcloud/actions/domain.py @@ -1,5 +1,4 @@ -from datetime import datetime - +from .._compat import isoparse from .._exceptions import HCloudException from ..core.domain import BaseDomain @@ -51,8 +50,8 @@ def __init__( self.status = status self.progress = progress - self.started = datetime.fromisoformat(started) if started else None - self.finished = datetime.fromisoformat(finished) if finished else None + self.started = isoparse(started) if started else None + self.finished = isoparse(finished) if finished else None self.resources = resources self.error = error diff --git a/hcloud/certificates/domain.py b/hcloud/certificates/domain.py index 0ee92e6f..afe44ed5 100644 --- a/hcloud/certificates/domain.py +++ b/hcloud/certificates/domain.py @@ -1,5 +1,4 @@ -from datetime import datetime - +from .._compat import isoparse from ..core.domain import BaseDomain, DomainIdentityMixin @@ -59,13 +58,9 @@ def __init__( self.certificate = certificate self.domain_names = domain_names self.fingerprint = fingerprint - self.not_valid_before = ( - datetime.fromisoformat(not_valid_before) if not_valid_before else None - ) - self.not_valid_after = ( - datetime.fromisoformat(not_valid_after) if not_valid_after else None - ) - self.created = datetime.fromisoformat(created) if created else None + self.not_valid_before = isoparse(not_valid_before) if not_valid_before else None + self.not_valid_after = isoparse(not_valid_after) if not_valid_after else None + self.created = isoparse(created) if created else None self.labels = labels self.status = status diff --git a/hcloud/deprecation/domain.py b/hcloud/deprecation/domain.py index 1679feb6..705016d2 100644 --- a/hcloud/deprecation/domain.py +++ b/hcloud/deprecation/domain.py @@ -1,5 +1,4 @@ -from datetime import datetime - +from .._compat import isoparse from ..core.domain import BaseDomain @@ -25,7 +24,7 @@ def __init__( announced=None, unavailable_after=None, ): - self.announced = datetime.fromisoformat(announced) if announced else None + self.announced = isoparse(announced) if announced else None self.unavailable_after = ( - datetime.fromisoformat(unavailable_after) if unavailable_after else None + isoparse(unavailable_after) if unavailable_after else None ) diff --git a/hcloud/firewalls/domain.py b/hcloud/firewalls/domain.py index 7a439236..ea431b5a 100644 --- a/hcloud/firewalls/domain.py +++ b/hcloud/firewalls/domain.py @@ -1,5 +1,4 @@ -from datetime import datetime - +from .._compat import isoparse from ..core.domain import BaseDomain @@ -30,7 +29,7 @@ def __init__( self.rules = rules self.applied_to = applied_to self.labels = labels - self.created = datetime.fromisoformat(created) if created else None + self.created = isoparse(created) if created else None class FirewallRule: diff --git a/hcloud/floating_ips/domain.py b/hcloud/floating_ips/domain.py index a268a439..133cc11a 100644 --- a/hcloud/floating_ips/domain.py +++ b/hcloud/floating_ips/domain.py @@ -1,5 +1,4 @@ -from datetime import datetime - +from .._compat import isoparse from ..core.domain import BaseDomain @@ -72,7 +71,7 @@ def __init__( self.blocked = blocked self.protection = protection self.labels = labels - self.created = datetime.fromisoformat(created) if created else None + self.created = isoparse(created) if created else None self.name = name diff --git a/hcloud/images/domain.py b/hcloud/images/domain.py index 2950d9d1..8b1c63ab 100644 --- a/hcloud/images/domain.py +++ b/hcloud/images/domain.py @@ -1,5 +1,4 @@ -from datetime import datetime - +from .._compat import isoparse from ..core.domain import BaseDomain, DomainIdentityMixin @@ -85,11 +84,11 @@ def __init__( self.id = id self.name = name self.type = type - self.created = datetime.fromisoformat(created) if created else None + self.created = isoparse(created) if created else None self.description = description self.image_size = image_size self.disk_size = disk_size - self.deprecated = datetime.fromisoformat(deprecated) if deprecated else None + self.deprecated = isoparse(deprecated) if deprecated else None self.bound_to = bound_to self.os_flavor = os_flavor self.os_version = os_version diff --git a/hcloud/isos/domain.py b/hcloud/isos/domain.py index 63ca5f99..6d4572ee 100644 --- a/hcloud/isos/domain.py +++ b/hcloud/isos/domain.py @@ -1,5 +1,4 @@ -from datetime import datetime - +from .._compat import isoparse from ..core.domain import BaseDomain, DomainIdentityMixin @@ -36,4 +35,4 @@ def __init__( self.type = type self.architecture = architecture self.description = description - self.deprecated = datetime.fromisoformat(deprecated) if deprecated else None + self.deprecated = isoparse(deprecated) if deprecated else None diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index 27e2a356..9c77432d 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -1,5 +1,4 @@ -from datetime import datetime - +from .._compat import isoparse from ..core.domain import BaseDomain @@ -76,7 +75,7 @@ def __init__( ): self.id = id self.name = name - self.created = datetime.fromisoformat(created) if created else None + self.created = isoparse(created) if created else None self.public_net = public_net self.private_net = private_net self.location = location diff --git a/hcloud/networks/domain.py b/hcloud/networks/domain.py index 01012805..709a3d71 100644 --- a/hcloud/networks/domain.py +++ b/hcloud/networks/domain.py @@ -1,5 +1,4 @@ -from datetime import datetime - +from .._compat import isoparse from ..core.domain import BaseDomain @@ -54,7 +53,7 @@ def __init__( ): self.id = id self.name = name - self.created = datetime.fromisoformat(created) if created else None + self.created = isoparse(created) if created else None self.ip_range = ip_range self.subnets = subnets self.routes = routes diff --git a/hcloud/placement_groups/domain.py b/hcloud/placement_groups/domain.py index d013032e..41d06804 100644 --- a/hcloud/placement_groups/domain.py +++ b/hcloud/placement_groups/domain.py @@ -1,5 +1,4 @@ -from datetime import datetime - +from .._compat import isoparse from ..core.domain import BaseDomain @@ -35,7 +34,7 @@ def __init__( self.labels = labels self.servers = servers self.type = type - self.created = datetime.fromisoformat(created) if created else None + self.created = isoparse(created) if created else None class CreatePlacementGroupResponse(BaseDomain): diff --git a/hcloud/primary_ips/domain.py b/hcloud/primary_ips/domain.py index cdf1f239..66e73c01 100644 --- a/hcloud/primary_ips/domain.py +++ b/hcloud/primary_ips/domain.py @@ -1,5 +1,4 @@ -from datetime import datetime - +from .._compat import isoparse from ..core.domain import BaseDomain @@ -74,7 +73,7 @@ def __init__( self.blocked = blocked self.protection = protection self.labels = labels - self.created = datetime.fromisoformat(created) if created else None + self.created = isoparse(created) if created else None self.name = name self.assignee_id = assignee_id self.assignee_type = assignee_type diff --git a/hcloud/servers/domain.py b/hcloud/servers/domain.py index 15b81cda..286ba861 100644 --- a/hcloud/servers/domain.py +++ b/hcloud/servers/domain.py @@ -1,5 +1,4 @@ -from datetime import datetime - +from .._compat import isoparse from ..core.domain import BaseDomain @@ -113,7 +112,7 @@ def __init__( self.id = id self.name = name self.status = status - self.created = datetime.fromisoformat(created) if created else None + self.created = isoparse(created) if created else None self.public_net = public_net self.server_type = server_type self.datacenter = datacenter diff --git a/hcloud/ssh_keys/domain.py b/hcloud/ssh_keys/domain.py index 530b7f8e..25096f42 100644 --- a/hcloud/ssh_keys/domain.py +++ b/hcloud/ssh_keys/domain.py @@ -1,5 +1,4 @@ -from datetime import datetime - +from .._compat import isoparse from ..core.domain import BaseDomain, DomainIdentityMixin @@ -36,4 +35,4 @@ def __init__( self.fingerprint = fingerprint self.public_key = public_key self.labels = labels - self.created = datetime.fromisoformat(created) if created else None + self.created = isoparse(created) if created else None diff --git a/hcloud/volumes/domain.py b/hcloud/volumes/domain.py index 953b4beb..0366cd57 100644 --- a/hcloud/volumes/domain.py +++ b/hcloud/volumes/domain.py @@ -1,5 +1,4 @@ -from datetime import datetime - +from .._compat import isoparse from ..core.domain import BaseDomain, DomainIdentityMixin @@ -66,7 +65,7 @@ def __init__( self.id = id self.name = name self.server = server - self.created = datetime.fromisoformat(created) if created else None + self.created = isoparse(created) if created else None self.location = location self.size = size self.linux_device = linux_device diff --git a/tests/unit/core/test_domain.py b/tests/unit/core/test_domain.py index 09aa6bab..b1116df7 100644 --- a/tests/unit/core/test_domain.py +++ b/tests/unit/core/test_domain.py @@ -1,7 +1,6 @@ -from datetime import datetime - import pytest +from hcloud._compat import isoparse from hcloud.core.domain import ( BaseDomain, DomainIdentityMixin, @@ -102,7 +101,7 @@ class ActionDomain(BaseDomain, DomainIdentityMixin): def __init__(self, id, name="name1", started=None): self.id = id self.name = name - self.started = datetime.fromisoformat(started) if started else None + self.started = isoparse(started) if started else None class TestBaseDomain: @@ -126,7 +125,7 @@ class TestBaseDomain: { "id": 4, "name": "name-name3", - "started": datetime.fromisoformat("2016-01-30T23:50+00:00"), + "started": isoparse("2016-01-30T23:50+00:00"), }, ), ], diff --git a/tests/unit/networks/test_client.py b/tests/unit/networks/test_client.py index 649094f9..9442919b 100644 --- a/tests/unit/networks/test_client.py +++ b/tests/unit/networks/test_client.py @@ -1,8 +1,8 @@ -from datetime import datetime from unittest import mock import pytest +from hcloud._compat import isoparse from hcloud.actions.client import BoundAction from hcloud.networks.client import BoundNetwork, NetworksClient from hcloud.networks.domain import Network, NetworkRoute, NetworkSubnet @@ -20,9 +20,7 @@ def test_bound_network_init(self, network_response): ) assert bound_network.id == 1 - assert bound_network.created == datetime.fromisoformat( - "2016-01-30T23:50:11+00:00" - ) + assert bound_network.created == isoparse("2016-01-30T23:50:11+00:00") assert bound_network.name == "mynet" assert bound_network.ip_range == "10.0.0.0/16" assert bound_network.protection["delete"] is False diff --git a/tests/unit/test_compat.py b/tests/unit/test_compat.py new file mode 100644 index 00000000..095d0f3b --- /dev/null +++ b/tests/unit/test_compat.py @@ -0,0 +1,36 @@ +from datetime import datetime, timedelta, timezone + +import pytest + +from hcloud._compat import isoparse + + +@pytest.mark.parametrize( + ["value", "expected"], + [ + ( + "2023-06-29T15:37:22", + datetime(2023, 6, 29, 15, 37, 22), + ), + ( + "2023-06-29T15:37:22+00:00", + datetime(2023, 6, 29, 15, 37, 22, tzinfo=timezone.utc), + ), + ( + "2023-06-29T15:37:22+02:12", + datetime( + 2023, 6, 29, 15, 37, 22, tzinfo=timezone(timedelta(hours=2, minutes=12)) + ), + ), + ( + "2023-06-29T15:37:22Z", + datetime(2023, 6, 29, 15, 37, 22, tzinfo=timezone.utc), + ), + ( + "2023-06-29T15:37:22z", + datetime(2023, 6, 29, 15, 37, 22, tzinfo=timezone.utc), + ), + ], +) +def test_isoparse(value: str, expected: datetime): + assert isoparse(value) == expected diff --git a/tests/unit/volumes/test_client.py b/tests/unit/volumes/test_client.py index 8065fe26..16fea070 100644 --- a/tests/unit/volumes/test_client.py +++ b/tests/unit/volumes/test_client.py @@ -1,8 +1,8 @@ -from datetime import datetime from unittest import mock import pytest +from hcloud._compat import isoparse from hcloud.actions.client import BoundAction from hcloud.locations.client import BoundLocation from hcloud.locations.domain import Location @@ -23,9 +23,7 @@ def test_bound_volume_init(self, volume_response): ) assert bound_volume.id == 1 - assert bound_volume.created == datetime.fromisoformat( - "2016-01-30T23:50:11+00:00" - ) + assert bound_volume.created == isoparse("2016-01-30T23:50:11+00:00") assert bound_volume.name == "database-storage" assert isinstance(bound_volume.server, BoundServer) assert bound_volume.server.id == 12 From 9c0a96a0c8562c1bab6290f547326874a63433a7 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 30 Jun 2023 10:40:05 +0200 Subject: [PATCH 050/406] refactor: improve hcloud.Client (#227) * refactor: simplify _get_user_agent * refactor: rename json_content variable * refactor: add typings to hcloud.Client * test: modernize test_request_ok --- hcloud/hcloud.py | 112 +++++++++++++++++--------------------- tests/unit/test_hcloud.py | 17 +++--- 2 files changed, 60 insertions(+), 69 deletions(-) diff --git a/hcloud/hcloud.py b/hcloud/hcloud.py index e8dfaad8..e62624f1 100644 --- a/hcloud/hcloud.py +++ b/hcloud/hcloud.py @@ -1,4 +1,5 @@ import time +from typing import Optional, Union import requests @@ -32,24 +33,19 @@ class Client: def __init__( self, - token, - api_endpoint="https://api.hetzner.cloud/v1", - application_name=None, - application_version=None, - poll_interval=1, + token: str, + api_endpoint: str = "https://api.hetzner.cloud/v1", + application_name: Optional[str] = None, + application_version: Optional[str] = None, + poll_interval: int = 1, ): """Create an new Client instance - :param token: str - Hetzner Cloud API token - :param api_endpoint: str - Hetzner Cloud API endpoint (default is https://api.hetzner.cloud/v1) - :param application_name: str - Your application name (default is None) - :param application_version: str - Your application _version (default is None) - :param poll_interval: int - Interval for polling information from Hetzner Cloud API in seconds (default is 1) + :param token: Hetzner Cloud API token + :param api_endpoint: Hetzner Cloud API endpoint + :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 """ self.token = token self._api_endpoint = api_endpoint @@ -148,85 +144,79 @@ def __init__( :type: :class:`PlacementGroupsClient ` """ - def _get_user_agent(self): + def _get_user_agent(self) -> str: """Get the user agent of the hcloud-python instance with the user application name (if specified) - :return: str - The user agent of this hcloud-python instance - """ - if self._application_name is not None and self._application_version is None: - return "{application_name} {prefix}/{version}".format( - application_name=self._application_name, - prefix=self.__user_agent_prefix, - version=self._version, - ) - elif ( - self._application_name is not None and self._application_version is not None - ): - return "{application_name}/{application_version} {prefix}/{version}".format( - application_name=self._application_name, - application_version=self._application_version, - prefix=self.__user_agent_prefix, - version=self._version, - ) - else: - return "{prefix}/{version}".format( - prefix=self.__user_agent_prefix, version=self._version - ) - - def _get_headers(self): + :return: The user agent of this hcloud-python instance + """ + 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 headers - def _raise_exception_from_response(self, response): + def _raise_exception_from_response(self, response: requests.Response): raise APIException( code=response.status_code, message=response.reason, details={"content": response.content}, ) - def _raise_exception_from_json_content(self, json_content): + def _raise_exception_from_content(self, content: dict): raise APIException( - code=json_content["error"]["code"], - message=json_content["error"]["message"], - details=json_content["error"]["details"], + code=content["error"]["code"], + message=content["error"]["message"], + details=content["error"]["details"], ) - def request(self, method, url, tries=1, **kwargs): + def request( + self, + method: str, + url: str, + tries: int = 1, + **kwargs, + ) -> Union[bytes, dict]: """Perform a request to the Hetzner Cloud API, wrapper around requests.request - :param method: str - HTTP Method to perform the Request - :param url: str - URL of the Endpoint - :param tries: int - Tries of the request (used internally, should not be set by the user) + :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) :return: Response - :rtype: requests.Response """ response = self._requests_session.request( - method, self._api_endpoint + url, headers=self._get_headers(), **kwargs + method=method, + url=self._api_endpoint + url, + headers=self._get_headers(), + **kwargs, ) - json_content = response.content + content = response.content try: - if len(json_content) > 0: - json_content = response.json() + if len(content) > 0: + content = response.json() except (TypeError, ValueError): self._raise_exception_from_response(response) if not response.ok: - if json_content: - if json_content["error"]["code"] == "rate_limit_exceeded" and tries < 5: + if content: + 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_json_content(json_content) + self._raise_exception_from_content(content) else: self._raise_exception_from_response(response) - return json_content + return content diff --git a/tests/unit/test_hcloud.py b/tests/unit/test_hcloud.py index 0fc0600f..5f5bf811 100644 --- a/tests/unit/test_hcloud.py +++ b/tests/unit/test_hcloud.py @@ -81,15 +81,16 @@ def test_request_ok(self, client, response): response = client.request( "POST", "/servers", params={"argument": "value"}, timeout=2 ) - client._requests_session.request.assert_called_once() - assert client._requests_session.request.call_args[0] == ( - "POST", - "https://api.hetzner.cloud/v1/servers", + client._requests_session.request.assert_called_once_with( + method="POST", + url="https://api.hetzner.cloud/v1/servers", + headers={ + "User-Agent": "hcloud-python/0.0.0", + "Authorization": "Bearer project_token", + }, + params={"argument": "value"}, + timeout=2, ) - assert client._requests_session.request.call_args[1]["params"] == { - "argument": "value" - } - assert client._requests_session.request.call_args[1]["timeout"] == 2 assert response == {"result": "data"} def test_request_fails(self, client, fail_response): From aa6d1e1a86b4d173f276095f540182a9e2bbb99e Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Fri, 30 Jun 2023 10:40:40 +0200 Subject: [PATCH 051/406] chore(main): release 1.23.1 (#229) --- CHANGELOG.md | 7 +++++++ hcloud/__version__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eac6b009..78ef42da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.23.1](https://github.com/hetznercloud/hcloud-python/compare/v1.23.0...v1.23.1) (2023-06-30) + + +### Bug Fixes + +* handle Z timezone in ISO8601 datetime format ([#228](https://github.com/hetznercloud/hcloud-python/issues/228)) ([6a5c3f4](https://github.com/hetznercloud/hcloud-python/commit/6a5c3f42c092610c4a82cb79c0052499563549dc)), closes [#226](https://github.com/hetznercloud/hcloud-python/issues/226) + ## [1.23.0](https://github.com/hetznercloud/hcloud-python/compare/v1.22.0...v1.23.0) (2023-06-26) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index 7618a1cf..53b7342c 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1 +1 @@ -VERSION = "1.23.0" # x-release-please-version +VERSION = "1.23.1" # x-release-please-version From 4be240cd9bb1617c7b2a6b28a2b96d32eb3cb983 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 30 Jun 2023 16:06:33 +0200 Subject: [PATCH 052/406] chore: force linux line ending (#230) * chore: force linux line ending * chore: fix file with wrong line ending --- .pre-commit-config.yaml | 1 + hcloud/load_balancer_types/domain.py | 110 ++-- hcloud/load_balancers/domain.py | 732 +++++++++++++-------------- 3 files changed, 422 insertions(+), 421 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d759c062..3052e196 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,6 +19,7 @@ repos: - id: check-merge-conflict - id: end-of-file-fixer - id: mixed-line-ending + args: [--fix=lf] - id: trailing-whitespace - repo: https://github.com/pre-commit/mirrors-prettier diff --git a/hcloud/load_balancer_types/domain.py b/hcloud/load_balancer_types/domain.py index 880d0885..7bdb1c05 100644 --- a/hcloud/load_balancer_types/domain.py +++ b/hcloud/load_balancer_types/domain.py @@ -1,55 +1,55 @@ -from ..core.domain import BaseDomain, DomainIdentityMixin - - -class LoadBalancerType(BaseDomain, DomainIdentityMixin): - """LoadBalancerType Domain - - :param id: int - ID of the Load Balancer type - :param name: str - Name of the Load Balancer type - :param description: str - Description of the Load Balancer type - :param max_connections: int - Max amount of connections the Load Balancer can handle - :param max_services: int - Max amount of services the Load Balancer can handle - :param max_targets: int - Max amount of targets the Load Balancer can handle - :param max_assigned_certificates: int - Max amount of certificates the Load Balancer can serve - :param prices: Dict - Prices in different locations - - """ - - __slots__ = ( - "id", - "name", - "description", - "max_connections", - "max_services", - "max_targets", - "max_assigned_certificates", - "prices", - ) - - def __init__( - self, - id=None, - name=None, - description=None, - max_connections=None, - max_services=None, - max_targets=None, - max_assigned_certificates=None, - prices=None, - ): - self.id = id - self.name = name - self.description = description - self.max_connections = max_connections - self.max_services = max_services - self.max_targets = max_targets - self.max_assigned_certificates = max_assigned_certificates - self.prices = prices +from ..core.domain import BaseDomain, DomainIdentityMixin + + +class LoadBalancerType(BaseDomain, DomainIdentityMixin): + """LoadBalancerType Domain + + :param id: int + ID of the Load Balancer type + :param name: str + Name of the Load Balancer type + :param description: str + Description of the Load Balancer type + :param max_connections: int + Max amount of connections the Load Balancer can handle + :param max_services: int + Max amount of services the Load Balancer can handle + :param max_targets: int + Max amount of targets the Load Balancer can handle + :param max_assigned_certificates: int + Max amount of certificates the Load Balancer can serve + :param prices: Dict + Prices in different locations + + """ + + __slots__ = ( + "id", + "name", + "description", + "max_connections", + "max_services", + "max_targets", + "max_assigned_certificates", + "prices", + ) + + def __init__( + self, + id=None, + name=None, + description=None, + max_connections=None, + max_services=None, + max_targets=None, + max_assigned_certificates=None, + prices=None, + ): + self.id = id + self.name = name + self.description = description + self.max_connections = max_connections + self.max_services = max_services + self.max_targets = max_targets + self.max_assigned_certificates = max_assigned_certificates + self.prices = prices diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index 9c77432d..963fb274 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -1,366 +1,366 @@ -from .._compat import isoparse -from ..core.domain import BaseDomain - - -class LoadBalancer(BaseDomain): - """LoadBalancer Domain - - :param id: int - ID of the Load Balancer - :param name: str - Name of the Load Balancer (must be unique per project) - :param created: datetime - Point in time when the Load Balancer was created - :param protection: dict - Protection configuration for the Load Balancer - :param labels: dict - User-defined labels (key-value pairs) - :param location: Location - Location of the Load Balancer - :param public_net: :class:`PublicNetwork ` - Public network information. - :param private_net: List[:class:`PrivateNet ` - :param ipv6: :class:`IPv6Network ` - :param enabled: boolean - """ - - __slots__ = ("ipv4", "ipv6", "enabled") - - def __init__( - self, - ipv4, # type: IPv4Address - ipv6, # type: IPv6Network - enabled, # type: bool - ): - self.ipv4 = ipv4 - self.ipv6 = ipv6 - self.enabled = enabled - - -class IPv4Address(BaseDomain): - """IPv4 Address Domain - - :param ip: str - The IPv4 Address - """ - - __slots__ = ("ip", "dns_ptr") - - def __init__( - self, - ip, # type: str - dns_ptr, # type: str - ): - self.ip = ip - self.dns_ptr = dns_ptr - - -class IPv6Network(BaseDomain): - """IPv6 Network Domain - - :param ip: str - The IPv6 Network as CIDR Notation - """ - - __slots__ = ("ip", "dns_ptr") - - def __init__( - self, - ip, # type: str - dns_ptr, # type: str - ): - self.ip = ip - self.dns_ptr = dns_ptr - - -class PrivateNet(BaseDomain): - """PrivateNet Domain - - :param network: :class:`BoundNetwork ` - The Network the LoadBalancer is attached to - :param ip: str - The main IP Address of the LoadBalancer in the Network - """ - - __slots__ = ("network", "ip") - - def __init__( - self, - network, # type: BoundNetwork - ip, # type: str - ): - self.network = network - self.ip = ip - - -class CreateLoadBalancerResponse(BaseDomain): - """Create Load Balancer Response Domain - - :param load_balancer: :class:`BoundLoadBalancer ` - The created Load Balancer - :param action: :class:`BoundAction ` - Shows the progress of the Load Balancer creation - """ - - __slots__ = ("load_balancer", "action") - - def __init__( - self, - load_balancer, # type: BoundLoadBalancer - action, # type: BoundAction - ): - self.load_balancer = load_balancer - self.action = action +from .._compat import isoparse +from ..core.domain import BaseDomain + + +class LoadBalancer(BaseDomain): + """LoadBalancer Domain + + :param id: int + ID of the Load Balancer + :param name: str + Name of the Load Balancer (must be unique per project) + :param created: datetime + Point in time when the Load Balancer was created + :param protection: dict + Protection configuration for the Load Balancer + :param labels: dict + User-defined labels (key-value pairs) + :param location: Location + Location of the Load Balancer + :param public_net: :class:`PublicNetwork ` + Public network information. + :param private_net: List[:class:`PrivateNet ` + :param ipv6: :class:`IPv6Network ` + :param enabled: boolean + """ + + __slots__ = ("ipv4", "ipv6", "enabled") + + def __init__( + self, + ipv4, # type: IPv4Address + ipv6, # type: IPv6Network + enabled, # type: bool + ): + self.ipv4 = ipv4 + self.ipv6 = ipv6 + self.enabled = enabled + + +class IPv4Address(BaseDomain): + """IPv4 Address Domain + + :param ip: str + The IPv4 Address + """ + + __slots__ = ("ip", "dns_ptr") + + def __init__( + self, + ip, # type: str + dns_ptr, # type: str + ): + self.ip = ip + self.dns_ptr = dns_ptr + + +class IPv6Network(BaseDomain): + """IPv6 Network Domain + + :param ip: str + The IPv6 Network as CIDR Notation + """ + + __slots__ = ("ip", "dns_ptr") + + def __init__( + self, + ip, # type: str + dns_ptr, # type: str + ): + self.ip = ip + self.dns_ptr = dns_ptr + + +class PrivateNet(BaseDomain): + """PrivateNet Domain + + :param network: :class:`BoundNetwork ` + The Network the LoadBalancer is attached to + :param ip: str + The main IP Address of the LoadBalancer in the Network + """ + + __slots__ = ("network", "ip") + + def __init__( + self, + network, # type: BoundNetwork + ip, # type: str + ): + self.network = network + self.ip = ip + + +class CreateLoadBalancerResponse(BaseDomain): + """Create Load Balancer Response Domain + + :param load_balancer: :class:`BoundLoadBalancer ` + The created Load Balancer + :param action: :class:`BoundAction ` + Shows the progress of the Load Balancer creation + """ + + __slots__ = ("load_balancer", "action") + + def __init__( + self, + load_balancer, # type: BoundLoadBalancer + action, # type: BoundAction + ): + self.load_balancer = load_balancer + self.action = action From 27f21bc41e17a800a8a3bed1df7935e7fb31de42 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Jul 2023 09:13:10 +0200 Subject: [PATCH 053/406] deps: update pre-commit hook asottile/pyupgrade to v3.8.0 (#232) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3052e196..2a1f0061 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: exclude: ^CHANGELOG.md$ - repo: https://github.com/asottile/pyupgrade - rev: v3.7.0 + rev: v3.8.0 hooks: - id: pyupgrade args: [--py37-plus] From 945bfde2ff0f64896e5c4d017e69236913e9d9dd Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 3 Jul 2023 12:17:57 +0200 Subject: [PATCH 054/406] feat: revert remove python-dateutil dependency (#231) This reverts commit 8ea4aa0ad12e85eeb14c81dfa2195e1a6ee79a76. Fixes #226 --- hcloud/_compat.py | 11 --------- hcloud/actions/domain.py | 3 ++- hcloud/certificates/domain.py | 3 ++- hcloud/deprecation/domain.py | 3 ++- hcloud/firewalls/domain.py | 3 ++- hcloud/floating_ips/domain.py | 3 ++- hcloud/images/domain.py | 3 ++- hcloud/isos/domain.py | 3 ++- hcloud/load_balancers/domain.py | 3 ++- hcloud/networks/domain.py | 3 ++- hcloud/placement_groups/domain.py | 3 ++- hcloud/primary_ips/domain.py | 3 ++- hcloud/servers/domain.py | 3 ++- hcloud/ssh_keys/domain.py | 3 ++- hcloud/volumes/domain.py | 3 ++- setup.py | 1 + tests/unit/core/test_domain.py | 2 +- tests/unit/networks/test_client.py | 2 +- tests/unit/test_compat.py | 36 ------------------------------ tests/unit/volumes/test_client.py | 2 +- 20 files changed, 32 insertions(+), 64 deletions(-) delete mode 100644 hcloud/_compat.py delete mode 100644 tests/unit/test_compat.py diff --git a/hcloud/_compat.py b/hcloud/_compat.py deleted file mode 100644 index 6eb552d1..00000000 --- a/hcloud/_compat.py +++ /dev/null @@ -1,11 +0,0 @@ -from datetime import datetime - - -def isoparse(value: str) -> datetime: - # Python <3.11 doesn't fully support parsing ISO8601 datetime strings. This - # workaround replaces the ending `Z` or `z` with `+00:00` and allows - # `datetime.fromisoformat` to parse the datetime string. - if value[-1] in "Zz": - value = value[:-1] + "+00:00" - - return datetime.fromisoformat(value) diff --git a/hcloud/actions/domain.py b/hcloud/actions/domain.py index 865a4051..0d91eaca 100644 --- a/hcloud/actions/domain.py +++ b/hcloud/actions/domain.py @@ -1,4 +1,5 @@ -from .._compat import isoparse +from dateutil.parser import isoparse + from .._exceptions import HCloudException from ..core.domain import BaseDomain diff --git a/hcloud/certificates/domain.py b/hcloud/certificates/domain.py index afe44ed5..f05173b5 100644 --- a/hcloud/certificates/domain.py +++ b/hcloud/certificates/domain.py @@ -1,4 +1,5 @@ -from .._compat import isoparse +from dateutil.parser import isoparse + from ..core.domain import BaseDomain, DomainIdentityMixin diff --git a/hcloud/deprecation/domain.py b/hcloud/deprecation/domain.py index 705016d2..8da63b95 100644 --- a/hcloud/deprecation/domain.py +++ b/hcloud/deprecation/domain.py @@ -1,4 +1,5 @@ -from .._compat import isoparse +from dateutil.parser import isoparse + from ..core.domain import BaseDomain diff --git a/hcloud/firewalls/domain.py b/hcloud/firewalls/domain.py index ea431b5a..861b40e8 100644 --- a/hcloud/firewalls/domain.py +++ b/hcloud/firewalls/domain.py @@ -1,4 +1,5 @@ -from .._compat import isoparse +from dateutil.parser import isoparse + from ..core.domain import BaseDomain diff --git a/hcloud/floating_ips/domain.py b/hcloud/floating_ips/domain.py index 133cc11a..e59a8dc2 100644 --- a/hcloud/floating_ips/domain.py +++ b/hcloud/floating_ips/domain.py @@ -1,4 +1,5 @@ -from .._compat import isoparse +from dateutil.parser import isoparse + from ..core.domain import BaseDomain diff --git a/hcloud/images/domain.py b/hcloud/images/domain.py index 8b1c63ab..4ca06001 100644 --- a/hcloud/images/domain.py +++ b/hcloud/images/domain.py @@ -1,4 +1,5 @@ -from .._compat import isoparse +from dateutil.parser import isoparse + from ..core.domain import BaseDomain, DomainIdentityMixin diff --git a/hcloud/isos/domain.py b/hcloud/isos/domain.py index 6d4572ee..52ae6952 100644 --- a/hcloud/isos/domain.py +++ b/hcloud/isos/domain.py @@ -1,4 +1,5 @@ -from .._compat import isoparse +from dateutil.parser import isoparse + from ..core.domain import BaseDomain, DomainIdentityMixin diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index 963fb274..e9eda775 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -1,4 +1,5 @@ -from .._compat import isoparse +from dateutil.parser import isoparse + from ..core.domain import BaseDomain diff --git a/hcloud/networks/domain.py b/hcloud/networks/domain.py index 709a3d71..8a05cb1b 100644 --- a/hcloud/networks/domain.py +++ b/hcloud/networks/domain.py @@ -1,4 +1,5 @@ -from .._compat import isoparse +from dateutil.parser import isoparse + from ..core.domain import BaseDomain diff --git a/hcloud/placement_groups/domain.py b/hcloud/placement_groups/domain.py index 41d06804..2484414e 100644 --- a/hcloud/placement_groups/domain.py +++ b/hcloud/placement_groups/domain.py @@ -1,4 +1,5 @@ -from .._compat import isoparse +from dateutil.parser import isoparse + from ..core.domain import BaseDomain diff --git a/hcloud/primary_ips/domain.py b/hcloud/primary_ips/domain.py index 66e73c01..6a0b83ad 100644 --- a/hcloud/primary_ips/domain.py +++ b/hcloud/primary_ips/domain.py @@ -1,4 +1,5 @@ -from .._compat import isoparse +from dateutil.parser import isoparse + from ..core.domain import BaseDomain diff --git a/hcloud/servers/domain.py b/hcloud/servers/domain.py index 286ba861..b28f62e6 100644 --- a/hcloud/servers/domain.py +++ b/hcloud/servers/domain.py @@ -1,4 +1,5 @@ -from .._compat import isoparse +from dateutil.parser import isoparse + from ..core.domain import BaseDomain diff --git a/hcloud/ssh_keys/domain.py b/hcloud/ssh_keys/domain.py index 25096f42..5f162f74 100644 --- a/hcloud/ssh_keys/domain.py +++ b/hcloud/ssh_keys/domain.py @@ -1,4 +1,5 @@ -from .._compat import isoparse +from dateutil.parser import isoparse + from ..core.domain import BaseDomain, DomainIdentityMixin diff --git a/hcloud/volumes/domain.py b/hcloud/volumes/domain.py index 0366cd57..6b4ae903 100644 --- a/hcloud/volumes/domain.py +++ b/hcloud/volumes/domain.py @@ -1,4 +1,5 @@ -from .._compat import isoparse +from dateutil.parser import isoparse + from ..core.domain import BaseDomain, DomainIdentityMixin diff --git a/setup.py b/setup.py index bb1945da..d0cdbf56 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,7 @@ ], python_requires=">=3.7", install_requires=[ + "python-dateutil>=2.7.5", "requests>=2.20", ], extras_require={ diff --git a/tests/unit/core/test_domain.py b/tests/unit/core/test_domain.py index b1116df7..9e71cee4 100644 --- a/tests/unit/core/test_domain.py +++ b/tests/unit/core/test_domain.py @@ -1,6 +1,6 @@ import pytest +from dateutil.parser import isoparse -from hcloud._compat import isoparse from hcloud.core.domain import ( BaseDomain, DomainIdentityMixin, diff --git a/tests/unit/networks/test_client.py b/tests/unit/networks/test_client.py index 9442919b..5d70d99a 100644 --- a/tests/unit/networks/test_client.py +++ b/tests/unit/networks/test_client.py @@ -1,8 +1,8 @@ from unittest import mock import pytest +from dateutil.parser import isoparse -from hcloud._compat import isoparse from hcloud.actions.client import BoundAction from hcloud.networks.client import BoundNetwork, NetworksClient from hcloud.networks.domain import Network, NetworkRoute, NetworkSubnet diff --git a/tests/unit/test_compat.py b/tests/unit/test_compat.py deleted file mode 100644 index 095d0f3b..00000000 --- a/tests/unit/test_compat.py +++ /dev/null @@ -1,36 +0,0 @@ -from datetime import datetime, timedelta, timezone - -import pytest - -from hcloud._compat import isoparse - - -@pytest.mark.parametrize( - ["value", "expected"], - [ - ( - "2023-06-29T15:37:22", - datetime(2023, 6, 29, 15, 37, 22), - ), - ( - "2023-06-29T15:37:22+00:00", - datetime(2023, 6, 29, 15, 37, 22, tzinfo=timezone.utc), - ), - ( - "2023-06-29T15:37:22+02:12", - datetime( - 2023, 6, 29, 15, 37, 22, tzinfo=timezone(timedelta(hours=2, minutes=12)) - ), - ), - ( - "2023-06-29T15:37:22Z", - datetime(2023, 6, 29, 15, 37, 22, tzinfo=timezone.utc), - ), - ( - "2023-06-29T15:37:22z", - datetime(2023, 6, 29, 15, 37, 22, tzinfo=timezone.utc), - ), - ], -) -def test_isoparse(value: str, expected: datetime): - assert isoparse(value) == expected diff --git a/tests/unit/volumes/test_client.py b/tests/unit/volumes/test_client.py index 16fea070..fda74f99 100644 --- a/tests/unit/volumes/test_client.py +++ b/tests/unit/volumes/test_client.py @@ -1,8 +1,8 @@ from unittest import mock import pytest +from dateutil.parser import isoparse -from hcloud._compat import isoparse from hcloud.actions.client import BoundAction from hcloud.locations.client import BoundLocation from hcloud.locations.domain import Location From e20aad78cfcf5273ea6b40ccead72f9b00f9cc1e Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Mon, 3 Jul 2023 16:53:48 +0200 Subject: [PATCH 055/406] chore(main): release 1.24.0 (#233) --- CHANGELOG.md | 12 ++++++++++++ hcloud/__version__.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78ef42da..22ac1ef5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [1.24.0](https://github.com/hetznercloud/hcloud-python/compare/v1.23.1...v1.24.0) (2023-07-03) + + +### Features + +* revert remove python-dateutil dependency ([#231](https://github.com/hetznercloud/hcloud-python/issues/231)) ([945bfde](https://github.com/hetznercloud/hcloud-python/commit/945bfde2ff0f64896e5c4d017e69236913e9d9dd)), closes [#226](https://github.com/hetznercloud/hcloud-python/issues/226) + + +### Dependencies + +* update pre-commit hook asottile/pyupgrade to v3.8.0 ([#232](https://github.com/hetznercloud/hcloud-python/issues/232)) ([27f21bc](https://github.com/hetznercloud/hcloud-python/commit/27f21bc41e17a800a8a3bed1df7935e7fb31de42)) + ## [1.23.1](https://github.com/hetznercloud/hcloud-python/compare/v1.23.0...v1.23.1) (2023-06-30) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index 53b7342c..0c992210 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1 +1 @@ -VERSION = "1.23.1" # x-release-please-version +VERSION = "1.24.0" # x-release-please-version From 047d4e173a53e91252d57d01b2e95def1c4949d9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 6 Jul 2023 12:31:05 +0200 Subject: [PATCH 056/406] deps: update pre-commit hook pre-commit/mirrors-prettier to v3 (#235) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2a1f0061..b75be64d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.7.1 + rev: v3.0.0 hooks: - id: prettier files: \.(md|ya?ml|js|css)$ From 593f9adaf321b9ceb92600ce21ca9db44669d5b4 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 10 Jul 2023 09:47:24 +0200 Subject: [PATCH 057/406] ci: use shared gitlab CI template (#237) --- .gitlab-ci.yml | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 206d0b8e..8f2fa236 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,20 +1,12 @@ -default: - tags: - - cloud-integrations +include: + - project: cloud/integrations/ci + file: + - default.yml + - pre-commit.yml stages: - test -pre-commit: - stage: test - - image: python:3.11-alpine - before_script: - - apk add build-base git - - pip install pre-commit - script: - - pre-commit run --all-files --show-diff-on-failure - test: stage: test From 0053ded5a1d0c2407134706830dd8ff3d4d1e8ce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Jul 2023 09:52:53 +0200 Subject: [PATCH 058/406] deps: update pre-commit hook asottile/pyupgrade to v3.9.0 (#238) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b75be64d..dc0b2bf4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: exclude: ^CHANGELOG.md$ - repo: https://github.com/asottile/pyupgrade - rev: v3.8.0 + rev: v3.9.0 hooks: - id: pyupgrade args: [--py37-plus] From 443bf262cb524dd674d2007db8100fec94dab80d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Jul 2023 11:28:41 +0200 Subject: [PATCH 059/406] deps: update pre-commit hook psf/black to v23.7.0 (#239) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dc0b2bf4..3caa94bd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: - id: isort - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.7.0 hooks: - id: black From cf64e549a2b28aea91062dea67db8733b4ecdd6f Mon Sep 17 00:00:00 2001 From: Jonas L Date: Tue, 11 Jul 2023 14:23:30 +0200 Subject: [PATCH 060/406] feat: add details to raise exceptions (#240) --- hcloud/_exceptions.py | 4 +-- hcloud/actions/domain.py | 10 +++++-- tests/unit/actions/test_domain.py | 49 ++++++++++++++++++++++++++++++- 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/hcloud/_exceptions.py b/hcloud/_exceptions.py index 36fee5de..d0801e95 100644 --- a/hcloud/_exceptions.py +++ b/hcloud/_exceptions.py @@ -6,9 +6,7 @@ class APIException(HCloudException): """There was an error while performing an API Request""" def __init__(self, code, message, details): + super().__init__(message) self.code = code self.message = message self.details = details - - def __str__(self): - return self.message diff --git a/hcloud/actions/domain.py b/hcloud/actions/domain.py index 0d91eaca..1cc25d47 100644 --- a/hcloud/actions/domain.py +++ b/hcloud/actions/domain.py @@ -61,12 +61,18 @@ class ActionException(HCloudException): """A generic action exception""" def __init__(self, action): + message = self.__doc__ + if action.error is not None and "message" in action.error: + message += f": {action.error['message']}" + + super().__init__(message) + self.message = message self.action = action class ActionFailedException(ActionException): - """The Action you were waiting for failed""" + """The pending action failed""" class ActionTimeoutException(ActionException): - """The Action you were waiting for timed out""" + """The pending action timed out""" diff --git a/tests/unit/actions/test_domain.py b/tests/unit/actions/test_domain.py index 02959da7..54b730c7 100644 --- a/tests/unit/actions/test_domain.py +++ b/tests/unit/actions/test_domain.py @@ -1,7 +1,14 @@ import datetime from datetime import timezone -from hcloud.actions.domain import Action +import pytest + +from hcloud.actions.domain import ( + Action, + ActionException, + ActionFailedException, + ActionTimeoutException, +) class TestAction: @@ -15,3 +22,43 @@ def test_started_finished_is_datetime(self): assert action.finished == datetime.datetime( 2016, 3, 30, 23, 50, tzinfo=timezone.utc ) + + +def test_action_exceptions(): + with pytest.raises( + ActionException, + match=r"The pending action failed: Server does not exist anymore", + ): + raise ActionFailedException( + action=Action( + **{ + "id": 1084730887, + "command": "change_server_type", + "status": "error", + "progress": 100, + "resources": [{"id": 34574042, "type": "server"}], + "error": { + "code": "server_does_not_exist_anymore", + "message": "Server does not exist anymore", + }, + "started": "2023-07-06T14:52:42+00:00", + "finished": "2023-07-06T14:53:08+00:00", + } + ) + ) + + with pytest.raises(ActionException, match=r"The pending action timed out"): + raise ActionTimeoutException( + action=Action( + **{ + "id": 1084659545, + "command": "create_server", + "status": "running", + "progress": 50, + "started": "2023-07-06T13:58:38+00:00", + "finished": None, + "resources": [{"id": 34572291, "type": "server"}], + "error": None, + } + ) + ) From 413472d7af1602b872a9b56324b9bffd0067eee6 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 14 Jul 2023 14:41:46 +0200 Subject: [PATCH 061/406] feat: move hcloud.hcloud module to hcloud._client (#243) * feat: move hcloud.hcloud module to hcloud._client * feat: add deprecation for hcloud.hcloud module --- hcloud/__init__.py | 2 +- hcloud/_client.py | 222 +++++++++++++++++++++++++++++++++++++ hcloud/hcloud.py | 227 ++------------------------------------ tests/unit/conftest.py | 2 +- tests/unit/test_client.py | 182 ++++++++++++++++++++++++++++++ tests/unit/test_hcloud.py | 182 +----------------------------- 6 files changed, 416 insertions(+), 401 deletions(-) create mode 100644 hcloud/_client.py create mode 100644 tests/unit/test_client.py diff --git a/hcloud/__init__.py b/hcloud/__init__.py index 592ff64d..5beda91d 100644 --- a/hcloud/__init__.py +++ b/hcloud/__init__.py @@ -1,2 +1,2 @@ +from ._client import Client # noqa from ._exceptions import APIException, HCloudException # noqa -from .hcloud import Client # noqa diff --git a/hcloud/_client.py b/hcloud/_client.py new file mode 100644 index 00000000..e62624f1 --- /dev/null +++ b/hcloud/_client.py @@ -0,0 +1,222 @@ +import time +from typing import Optional, Union + +import requests + +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 + + +class Client: + """Base Client for accessing the Hetzner Cloud API""" + + _version = VERSION + _retry_wait_time = 0.5 + __user_agent_prefix = "hcloud-python" + + def __init__( + self, + token: str, + api_endpoint: str = "https://api.hetzner.cloud/v1", + application_name: Optional[str] = None, + application_version: Optional[str] = None, + poll_interval: int = 1, + ): + """Create an new Client instance + + :param token: Hetzner Cloud API token + :param api_endpoint: Hetzner Cloud API endpoint + :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 + """ + self.token = token + self._api_endpoint = api_endpoint + self._application_name = application_name + self._application_version = application_version + self._requests_session = requests.Session() + self.poll_interval = poll_interval + + self.datacenters = DatacentersClient(self) + """DatacentersClient Instance + + :type: :class:`DatacentersClient ` + """ + self.locations = LocationsClient(self) + """LocationsClient Instance + + :type: :class:`LocationsClient ` + """ + self.servers = ServersClient(self) + """ServersClient Instance + + :type: :class:`ServersClient ` + """ + self.server_types = ServerTypesClient(self) + """ServerTypesClient Instance + + :type: :class:`ServerTypesClient ` + """ + self.volumes = VolumesClient(self) + """VolumesClient Instance + + :type: :class:`VolumesClient ` + """ + self.actions = ActionsClient(self) + """ActionsClient Instance + + :type: :class:`ActionsClient ` + """ + self.images = ImagesClient(self) + """ImagesClient Instance + + :type: :class:`ImagesClient ` + """ + self.isos = IsosClient(self) + """ImagesClient Instance + + :type: :class:`IsosClient ` + """ + self.ssh_keys = SSHKeysClient(self) + """SSHKeysClient Instance + + :type: :class:`SSHKeysClient ` + """ + self.floating_ips = FloatingIPsClient(self) + """FloatingIPsClient Instance + + :type: :class:`FloatingIPsClient ` + """ + self.primary_ips = PrimaryIPsClient(self) + """PrimaryIPsClient Instance + + :type: :class:`PrimaryIPsClient ` + """ + self.networks = NetworksClient(self) + """NetworksClient Instance + + :type: :class:`NetworksClient ` + """ + self.certificates = CertificatesClient(self) + """CertificatesClient Instance + + :type: :class:`CertificatesClient ` + """ + + self.load_balancers = LoadBalancersClient(self) + """LoadBalancersClient Instance + + :type: :class:`LoadBalancersClient ` + """ + + self.load_balancer_types = LoadBalancerTypesClient(self) + """LoadBalancerTypesClient Instance + + :type: :class:`LoadBalancerTypesClient ` + """ + + self.firewalls = FirewallsClient(self) + """FirewallsClient Instance + + :type: :class:`FirewallsClient ` + """ + + self.placement_groups = PlacementGroupsClient(self) + """PlacementGroupsClient Instance + + :type: :class:`PlacementGroupsClient ` + """ + + def _get_user_agent(self) -> str: + """Get the user agent of the hcloud-python instance with the user application name (if specified) + + :return: The user agent of this hcloud-python instance + """ + 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 headers + + def _raise_exception_from_response(self, response: requests.Response): + raise APIException( + code=response.status_code, + message=response.reason, + details={"content": response.content}, + ) + + def _raise_exception_from_content(self, content: dict): + raise APIException( + code=content["error"]["code"], + message=content["error"]["message"], + details=content["error"]["details"], + ) + + def request( + self, + method: str, + url: str, + tries: int = 1, + **kwargs, + ) -> Union[bytes, 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) + :return: Response + """ + response = self._requests_session.request( + method=method, + url=self._api_endpoint + url, + headers=self._get_headers(), + **kwargs, + ) + + content = response.content + try: + if len(content) > 0: + content = response.json() + except (TypeError, ValueError): + self._raise_exception_from_response(response) + + if not response.ok: + if content: + 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) + else: + self._raise_exception_from_response(response) + + return content diff --git a/hcloud/hcloud.py b/hcloud/hcloud.py index e62624f1..af4e5c2a 100644 --- a/hcloud/hcloud.py +++ b/hcloud/hcloud.py @@ -1,222 +1,9 @@ -import time -from typing import Optional, Union +import warnings -import requests +warnings.warn( + "The 'hcloud.hcloud' module is deprecated, please import from the 'hcloud' module instead (e.g. 'from hcloud import Client').", + DeprecationWarning, + stacklevel=2, +) -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 - - -class Client: - """Base Client for accessing the Hetzner Cloud API""" - - _version = VERSION - _retry_wait_time = 0.5 - __user_agent_prefix = "hcloud-python" - - def __init__( - self, - token: str, - api_endpoint: str = "https://api.hetzner.cloud/v1", - application_name: Optional[str] = None, - application_version: Optional[str] = None, - poll_interval: int = 1, - ): - """Create an new Client instance - - :param token: Hetzner Cloud API token - :param api_endpoint: Hetzner Cloud API endpoint - :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 - """ - self.token = token - self._api_endpoint = api_endpoint - self._application_name = application_name - self._application_version = application_version - self._requests_session = requests.Session() - self.poll_interval = poll_interval - - self.datacenters = DatacentersClient(self) - """DatacentersClient Instance - - :type: :class:`DatacentersClient ` - """ - self.locations = LocationsClient(self) - """LocationsClient Instance - - :type: :class:`LocationsClient ` - """ - self.servers = ServersClient(self) - """ServersClient Instance - - :type: :class:`ServersClient ` - """ - self.server_types = ServerTypesClient(self) - """ServerTypesClient Instance - - :type: :class:`ServerTypesClient ` - """ - self.volumes = VolumesClient(self) - """VolumesClient Instance - - :type: :class:`VolumesClient ` - """ - self.actions = ActionsClient(self) - """ActionsClient Instance - - :type: :class:`ActionsClient ` - """ - self.images = ImagesClient(self) - """ImagesClient Instance - - :type: :class:`ImagesClient ` - """ - self.isos = IsosClient(self) - """ImagesClient Instance - - :type: :class:`IsosClient ` - """ - self.ssh_keys = SSHKeysClient(self) - """SSHKeysClient Instance - - :type: :class:`SSHKeysClient ` - """ - self.floating_ips = FloatingIPsClient(self) - """FloatingIPsClient Instance - - :type: :class:`FloatingIPsClient ` - """ - self.primary_ips = PrimaryIPsClient(self) - """PrimaryIPsClient Instance - - :type: :class:`PrimaryIPsClient ` - """ - self.networks = NetworksClient(self) - """NetworksClient Instance - - :type: :class:`NetworksClient ` - """ - self.certificates = CertificatesClient(self) - """CertificatesClient Instance - - :type: :class:`CertificatesClient ` - """ - - self.load_balancers = LoadBalancersClient(self) - """LoadBalancersClient Instance - - :type: :class:`LoadBalancersClient ` - """ - - self.load_balancer_types = LoadBalancerTypesClient(self) - """LoadBalancerTypesClient Instance - - :type: :class:`LoadBalancerTypesClient ` - """ - - self.firewalls = FirewallsClient(self) - """FirewallsClient Instance - - :type: :class:`FirewallsClient ` - """ - - self.placement_groups = PlacementGroupsClient(self) - """PlacementGroupsClient Instance - - :type: :class:`PlacementGroupsClient ` - """ - - def _get_user_agent(self) -> str: - """Get the user agent of the hcloud-python instance with the user application name (if specified) - - :return: The user agent of this hcloud-python instance - """ - 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 headers - - def _raise_exception_from_response(self, response: requests.Response): - raise APIException( - code=response.status_code, - message=response.reason, - details={"content": response.content}, - ) - - def _raise_exception_from_content(self, content: dict): - raise APIException( - code=content["error"]["code"], - message=content["error"]["message"], - details=content["error"]["details"], - ) - - def request( - self, - method: str, - url: str, - tries: int = 1, - **kwargs, - ) -> Union[bytes, 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) - :return: Response - """ - response = self._requests_session.request( - method=method, - url=self._api_endpoint + url, - headers=self._get_headers(), - **kwargs, - ) - - content = response.content - try: - if len(content) > 0: - content = response.json() - except (TypeError, ValueError): - self._raise_exception_from_response(response) - - if not response.ok: - if content: - 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) - else: - self._raise_exception_from_response(response) - - return content +from ._client import * # noqa diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index d15d36b5..0ab74906 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -7,7 +7,7 @@ @pytest.fixture(autouse=True, scope="function") def mocked_requests(): - patcher = mock.patch("hcloud.hcloud.requests") + patcher = mock.patch("hcloud._client.requests") mocked_requests = patcher.start() yield mocked_requests patcher.stop() diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py new file mode 100644 index 00000000..5f5bf811 --- /dev/null +++ b/tests/unit/test_client.py @@ -0,0 +1,182 @@ +import json +from unittest.mock import MagicMock + +import pytest +import requests + +from hcloud import APIException, Client + + +class TestHetznerClient: + @pytest.fixture() + def client(self): + Client._version = "0.0.0" + client = Client(token="project_token") + + client._requests_session = MagicMock() + return client + + @pytest.fixture() + def response(self): + response = requests.Response() + response.status_code = 200 + response._content = json.dumps({"result": "data"}).encode("utf-8") + return response + + @pytest.fixture() + def fail_response(self, response): + response.status_code = 422 + error = { + "code": "invalid_input", + "message": "invalid input in field 'broken_field': is too long", + "details": { + "fields": [{"name": "broken_field", "messages": ["is too long"]}] + }, + } + response._content = json.dumps({"error": error}).encode("utf-8") + return response + + @pytest.fixture() + def rate_limit_response(self, response): + response.status_code = 422 + error = { + "code": "rate_limit_exceeded", + "message": "limit of 10 requests per hour reached", + "details": {}, + } + response._content = json.dumps({"error": error}).encode("utf-8") + return response + + def test__get_user_agent(self, client): + user_agent = client._get_user_agent() + assert user_agent == "hcloud-python/0.0.0" + + def test__get_user_agent_with_application_name(self, client): + client = Client(token="project_token", application_name="my-app") + user_agent = client._get_user_agent() + assert user_agent == "my-app hcloud-python/0.0.0" + + def test__get_user_agent_with_application_name_and_version(self, client): + client = Client( + token="project_token", + application_name="my-app", + application_version="1.0.0", + ) + user_agent = client._get_user_agent() + assert user_agent == "my-app/1.0.0 hcloud-python/0.0.0" + + def test__get_headers(self, client): + headers = client._get_headers() + assert headers == { + "User-Agent": "hcloud-python/0.0.0", + "Authorization": "Bearer project_token", + } + + def test_request_library_mocked(self, client): + response = client.request("POST", "url", params={"1": 2}) + assert response.__class__.__name__ == "MagicMock" + + def test_request_ok(self, client, response): + client._requests_session.request.return_value = response + response = client.request( + "POST", "/servers", params={"argument": "value"}, timeout=2 + ) + client._requests_session.request.assert_called_once_with( + method="POST", + url="https://api.hetzner.cloud/v1/servers", + headers={ + "User-Agent": "hcloud-python/0.0.0", + "Authorization": "Bearer project_token", + }, + params={"argument": "value"}, + timeout=2, + ) + assert response == {"result": "data"} + + def test_request_fails(self, client, fail_response): + client._requests_session.request.return_value = fail_response + with pytest.raises(APIException) as exception_info: + client.request( + "POST", "http://url.com", params={"argument": "value"}, timeout=2 + ) + error = exception_info.value + assert error.code == "invalid_input" + assert error.message == "invalid input in field 'broken_field': is too long" + assert error.details["fields"][0]["name"] == "broken_field" + + def test_request_500(self, client, fail_response): + fail_response.status_code = 500 + fail_response.reason = "Internal Server Error" + fail_response._content = "Internal Server Error" + client._requests_session.request.return_value = fail_response + with pytest.raises(APIException) as exception_info: + client.request( + "POST", "http://url.com", params={"argument": "value"}, timeout=2 + ) + error = exception_info.value + assert error.code == 500 + assert error.message == "Internal Server Error" + assert error.details["content"] == "Internal Server Error" + + def test_request_broken_json_200(self, client, response): + content = b"{'key': 'value'" + response.reason = "OK" + response._content = content + client._requests_session.request.return_value = response + with pytest.raises(APIException) as exception_info: + client.request( + "POST", "http://url.com", params={"argument": "value"}, timeout=2 + ) + error = exception_info.value + assert error.code == 200 + assert error.message == "OK" + assert error.details["content"] == content + + def test_request_empty_content_200(self, client, response): + content = "" + response.reason = "OK" + response._content = content + client._requests_session.request.return_value = response + response = client.request( + "POST", "http://url.com", params={"argument": "value"}, timeout=2 + ) + assert response == "" + + def test_request_500_empty_content(self, client, fail_response): + fail_response.status_code = 500 + fail_response.reason = "Internal Server Error" + fail_response._content = "" + client._requests_session.request.return_value = fail_response + with pytest.raises(APIException) as exception_info: + client.request( + "POST", "http://url.com", params={"argument": "value"}, timeout=2 + ) + error = exception_info.value + assert error.code == 500 + assert error.message == "Internal Server Error" + assert error.details["content"] == "" + assert str(error) == "Internal Server Error" + + def test_request_limit(self, client, rate_limit_response): + client._retry_wait_time = 0 + client._requests_session.request.return_value = rate_limit_response + with pytest.raises(APIException) as exception_info: + client.request( + "POST", "http://url.com", params={"argument": "value"}, timeout=2 + ) + error = exception_info.value + assert client._requests_session.request.call_count == 5 + assert error.code == "rate_limit_exceeded" + assert error.message == "limit of 10 requests per hour reached" + + def test_request_limit_then_success(self, client, rate_limit_response): + client._retry_wait_time = 0 + response = requests.Response() + response.status_code = 200 + response._content = json.dumps({"result": "data"}).encode("utf-8") + client._requests_session.request.side_effect = [rate_limit_response, response] + + client.request( + "POST", "http://url.com", params={"argument": "value"}, timeout=2 + ) + assert client._requests_session.request.call_count == 2 diff --git a/tests/unit/test_hcloud.py b/tests/unit/test_hcloud.py index 5f5bf811..87ab50aa 100644 --- a/tests/unit/test_hcloud.py +++ b/tests/unit/test_hcloud.py @@ -1,182 +1,6 @@ -import json -from unittest.mock import MagicMock - import pytest -import requests - -from hcloud import APIException, Client - - -class TestHetznerClient: - @pytest.fixture() - def client(self): - Client._version = "0.0.0" - client = Client(token="project_token") - - client._requests_session = MagicMock() - return client - - @pytest.fixture() - def response(self): - response = requests.Response() - response.status_code = 200 - response._content = json.dumps({"result": "data"}).encode("utf-8") - return response - - @pytest.fixture() - def fail_response(self, response): - response.status_code = 422 - error = { - "code": "invalid_input", - "message": "invalid input in field 'broken_field': is too long", - "details": { - "fields": [{"name": "broken_field", "messages": ["is too long"]}] - }, - } - response._content = json.dumps({"error": error}).encode("utf-8") - return response - - @pytest.fixture() - def rate_limit_response(self, response): - response.status_code = 422 - error = { - "code": "rate_limit_exceeded", - "message": "limit of 10 requests per hour reached", - "details": {}, - } - response._content = json.dumps({"error": error}).encode("utf-8") - return response - - def test__get_user_agent(self, client): - user_agent = client._get_user_agent() - assert user_agent == "hcloud-python/0.0.0" - - def test__get_user_agent_with_application_name(self, client): - client = Client(token="project_token", application_name="my-app") - user_agent = client._get_user_agent() - assert user_agent == "my-app hcloud-python/0.0.0" - - def test__get_user_agent_with_application_name_and_version(self, client): - client = Client( - token="project_token", - application_name="my-app", - application_version="1.0.0", - ) - user_agent = client._get_user_agent() - assert user_agent == "my-app/1.0.0 hcloud-python/0.0.0" - - def test__get_headers(self, client): - headers = client._get_headers() - assert headers == { - "User-Agent": "hcloud-python/0.0.0", - "Authorization": "Bearer project_token", - } - - def test_request_library_mocked(self, client): - response = client.request("POST", "url", params={"1": 2}) - assert response.__class__.__name__ == "MagicMock" - - def test_request_ok(self, client, response): - client._requests_session.request.return_value = response - response = client.request( - "POST", "/servers", params={"argument": "value"}, timeout=2 - ) - client._requests_session.request.assert_called_once_with( - method="POST", - url="https://api.hetzner.cloud/v1/servers", - headers={ - "User-Agent": "hcloud-python/0.0.0", - "Authorization": "Bearer project_token", - }, - params={"argument": "value"}, - timeout=2, - ) - assert response == {"result": "data"} - - def test_request_fails(self, client, fail_response): - client._requests_session.request.return_value = fail_response - with pytest.raises(APIException) as exception_info: - client.request( - "POST", "http://url.com", params={"argument": "value"}, timeout=2 - ) - error = exception_info.value - assert error.code == "invalid_input" - assert error.message == "invalid input in field 'broken_field': is too long" - assert error.details["fields"][0]["name"] == "broken_field" - - def test_request_500(self, client, fail_response): - fail_response.status_code = 500 - fail_response.reason = "Internal Server Error" - fail_response._content = "Internal Server Error" - client._requests_session.request.return_value = fail_response - with pytest.raises(APIException) as exception_info: - client.request( - "POST", "http://url.com", params={"argument": "value"}, timeout=2 - ) - error = exception_info.value - assert error.code == 500 - assert error.message == "Internal Server Error" - assert error.details["content"] == "Internal Server Error" - - def test_request_broken_json_200(self, client, response): - content = b"{'key': 'value'" - response.reason = "OK" - response._content = content - client._requests_session.request.return_value = response - with pytest.raises(APIException) as exception_info: - client.request( - "POST", "http://url.com", params={"argument": "value"}, timeout=2 - ) - error = exception_info.value - assert error.code == 200 - assert error.message == "OK" - assert error.details["content"] == content - - def test_request_empty_content_200(self, client, response): - content = "" - response.reason = "OK" - response._content = content - client._requests_session.request.return_value = response - response = client.request( - "POST", "http://url.com", params={"argument": "value"}, timeout=2 - ) - assert response == "" - - def test_request_500_empty_content(self, client, fail_response): - fail_response.status_code = 500 - fail_response.reason = "Internal Server Error" - fail_response._content = "" - client._requests_session.request.return_value = fail_response - with pytest.raises(APIException) as exception_info: - client.request( - "POST", "http://url.com", params={"argument": "value"}, timeout=2 - ) - error = exception_info.value - assert error.code == 500 - assert error.message == "Internal Server Error" - assert error.details["content"] == "" - assert str(error) == "Internal Server Error" - - def test_request_limit(self, client, rate_limit_response): - client._retry_wait_time = 0 - client._requests_session.request.return_value = rate_limit_response - with pytest.raises(APIException) as exception_info: - client.request( - "POST", "http://url.com", params={"argument": "value"}, timeout=2 - ) - error = exception_info.value - assert client._requests_session.request.call_count == 5 - assert error.code == "rate_limit_exceeded" - assert error.message == "limit of 10 requests per hour reached" - def test_request_limit_then_success(self, client, rate_limit_response): - client._retry_wait_time = 0 - response = requests.Response() - response.status_code = 200 - response._content = json.dumps({"result": "data"}).encode("utf-8") - client._requests_session.request.side_effect = [rate_limit_response, response] - client.request( - "POST", "http://url.com", params={"argument": "value"}, timeout=2 - ) - assert client._requests_session.request.call_count == 2 +def test_deprecated_hcloud_hcloud_module(): + with pytest.deprecated_call(): + from hcloud.hcloud import Client # noqa From a84592e4cbd6ab951c90a430a203896f84d9ee17 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Fri, 14 Jul 2023 15:23:55 +0200 Subject: [PATCH 062/406] chore(main): release 1.25.0 (#236) --- CHANGELOG.md | 15 +++++++++++++++ hcloud/__version__.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22ac1ef5..6e6fecc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [1.25.0](https://github.com/hetznercloud/hcloud-python/compare/v1.24.0...v1.25.0) (2023-07-14) + + +### Features + +* add details to raise exceptions ([#240](https://github.com/hetznercloud/hcloud-python/issues/240)) ([cf64e54](https://github.com/hetznercloud/hcloud-python/commit/cf64e549a2b28aea91062dea67db8733b4ecdd6f)) +* move hcloud.hcloud module to hcloud._client ([#243](https://github.com/hetznercloud/hcloud-python/issues/243)) ([413472d](https://github.com/hetznercloud/hcloud-python/commit/413472d7af1602b872a9b56324b9bffd0067eee6)) + + +### Dependencies + +* update pre-commit hook asottile/pyupgrade to v3.9.0 ([#238](https://github.com/hetznercloud/hcloud-python/issues/238)) ([0053ded](https://github.com/hetznercloud/hcloud-python/commit/0053ded5a1d0c2407134706830dd8ff3d4d1e8ce)) +* update pre-commit hook pre-commit/mirrors-prettier to v3 ([#235](https://github.com/hetznercloud/hcloud-python/issues/235)) ([047d4e1](https://github.com/hetznercloud/hcloud-python/commit/047d4e173a53e91252d57d01b2e95def1c4949d9)) +* update pre-commit hook psf/black to v23.7.0 ([#239](https://github.com/hetznercloud/hcloud-python/issues/239)) ([443bf26](https://github.com/hetznercloud/hcloud-python/commit/443bf262cb524dd674d2007db8100fec94dab80d)) + ## [1.24.0](https://github.com/hetznercloud/hcloud-python/compare/v1.23.1...v1.24.0) (2023-07-03) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index 0c992210..61abd10c 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1 +1 @@ -VERSION = "1.24.0" # x-release-please-version +VERSION = "1.25.0" # x-release-please-version From 2ce71e9ded5e9bb87ce96519ce59db942f4f9670 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 17 Jul 2023 17:56:37 +0200 Subject: [PATCH 063/406] feat: drop support for python 3.7 (#242) --- .github/workflows/test.yml | 2 +- .gitlab-ci.yml | 2 +- .pre-commit-config.yaml | 2 +- setup.py | 3 +-- tox.ini | 3 +-- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 19a1cd28..92d72cde 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] name: Python ${{ matrix.python-version }} steps: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8f2fa236..8fcc3750 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,7 +12,7 @@ test: parallel: matrix: - - python_version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + - python_version: ["3.8", "3.9", "3.10", "3.11"] image: python:${python_version}-alpine before_script: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3caa94bd..d769cd92 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,7 +33,7 @@ repos: rev: v3.9.0 hooks: - id: pyupgrade - args: [--py37-plus] + args: [--py38-plus] - repo: https://github.com/pycqa/isort rev: 5.12.0 diff --git a/setup.py b/setup.py index d0cdbf56..2dd05316 100644 --- a/setup.py +++ b/setup.py @@ -29,13 +29,12 @@ "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", ], - python_requires=">=3.7", + python_requires=">=3.8", install_requires=[ "python-dateutil>=2.7.5", "requests>=2.20", diff --git a/tox.ini b/tox.ini index e8b91b02..3470ad15 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37, py38, py39, py310, py311 +envlist = py38, py39, py310, py311 [testenv] passenv = FAKE_API_ENDPOINT @@ -10,7 +10,6 @@ commands = [gh-actions] python = - 3.7: py37 3.8: py38 3.9: py39 3.10: py310 From 4c227659bfb61551e44c41315b135039576960d3 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Wed, 19 Jul 2023 12:05:55 +0200 Subject: [PATCH 064/406] feat: add __repr__ method to domains (#246) --- hcloud/core/domain.py | 4 ++++ tests/unit/core/test_domain.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/hcloud/core/domain.py b/hcloud/core/domain.py index 8d99f63a..c4d3308f 100644 --- a/hcloud/core/domain.py +++ b/hcloud/core/domain.py @@ -9,6 +9,10 @@ def from_dict(cls, data): 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__] + return f"{self.__class__.__qualname__}({', '.join(kwargs)})" + class DomainIdentityMixin: __slots__ = () diff --git a/tests/unit/core/test_domain.py b/tests/unit/core/test_domain.py index 9e71cee4..57cb7906 100644 --- a/tests/unit/core/test_domain.py +++ b/tests/unit/core/test_domain.py @@ -104,6 +104,15 @@ def __init__(self, id, name="name1", started=None): self.started = isoparse(started) if started else None +class SomeOtherDomain(BaseDomain): + __slots__ = ("id", "name", "child") + + def __init__(self, id=None, name=None, child=None): + self.id = id + self.name = name + self.child = child + + class TestBaseDomain: @pytest.mark.parametrize( "data_dict,expected_result", @@ -134,3 +143,23 @@ def test_from_dict_ok(self, data_dict, expected_result): model = ActionDomain.from_dict(data_dict) for k, v in expected_result.items(): assert getattr(model, k) == v + + @pytest.mark.parametrize( + "data,expected", + [ + ( + SomeOtherDomain(id=1, name="name1"), + "SomeOtherDomain(id=1, name='name1', child=None)", + ), + ( + SomeOtherDomain( + id=2, + name="name2", + child=SomeOtherDomain(id=3, name="name3"), + ), + "SomeOtherDomain(id=2, name='name2', child=SomeOtherDomain(id=3, name='name3', child=None))", + ), + ], + ) + def test_repr_ok(self, data, expected): + assert data.__repr__() == expected From 91b7285fc8e639dc0aee6382207f459dfb94b4ba Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Wed, 19 Jul 2023 12:31:48 +0200 Subject: [PATCH 065/406] chore(main): release 1.26.0 (#245) --- CHANGELOG.md | 8 ++++++++ hcloud/__version__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e6fecc2..5006d384 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.26.0](https://github.com/hetznercloud/hcloud-python/compare/v1.25.0...v1.26.0) (2023-07-19) + + +### Features + +* add __repr__ method to domains ([#246](https://github.com/hetznercloud/hcloud-python/issues/246)) ([4c22765](https://github.com/hetznercloud/hcloud-python/commit/4c227659bfb61551e44c41315b135039576960d3)) +* drop support for python 3.7 ([#242](https://github.com/hetznercloud/hcloud-python/issues/242)) ([2ce71e9](https://github.com/hetznercloud/hcloud-python/commit/2ce71e9ded5e9bb87ce96519ce59db942f4f9670)) + ## [1.25.0](https://github.com/hetznercloud/hcloud-python/compare/v1.24.0...v1.25.0) (2023-07-14) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index 61abd10c..974595a6 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1 +1 @@ -VERSION = "1.25.0" # x-release-please-version +VERSION = "1.26.0" # x-release-please-version From c920c3417d1a1a7d944561051291af6a6d1e0cf4 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Thu, 20 Jul 2023 14:19:32 +0200 Subject: [PATCH 066/406] refactor: add from __future__ import annotations imports (#249) * chore: make isort import future annotations * chore: add from __future__ import annotations imports --- docs/conf.py | 2 ++ examples/create_server.py | 2 ++ examples/list_servers.py | 2 ++ examples/usage_oop.py | 2 ++ examples/usage_procedurale.py | 2 ++ hcloud/__init__.py | 2 ++ hcloud/__version__.py | 2 ++ hcloud/_client.py | 9 +++++---- hcloud/_exceptions.py | 3 +++ hcloud/actions/client.py | 2 ++ hcloud/actions/domain.py | 2 ++ hcloud/certificates/client.py | 2 ++ hcloud/certificates/domain.py | 2 ++ hcloud/core/client.py | 4 +++- hcloud/core/domain.py | 2 ++ hcloud/datacenters/client.py | 2 ++ hcloud/datacenters/domain.py | 2 ++ hcloud/deprecation/domain.py | 2 ++ hcloud/firewalls/client.py | 2 ++ hcloud/firewalls/domain.py | 2 ++ hcloud/floating_ips/client.py | 2 ++ hcloud/floating_ips/domain.py | 2 ++ hcloud/hcloud.py | 2 ++ hcloud/helpers/labels.py | 7 ++++--- hcloud/images/client.py | 2 ++ hcloud/images/domain.py | 2 ++ hcloud/isos/client.py | 2 ++ hcloud/isos/domain.py | 2 ++ hcloud/load_balancer_types/client.py | 2 ++ hcloud/load_balancer_types/domain.py | 2 ++ hcloud/load_balancers/client.py | 2 ++ hcloud/load_balancers/domain.py | 2 ++ hcloud/locations/client.py | 2 ++ hcloud/locations/domain.py | 2 ++ hcloud/networks/client.py | 2 ++ hcloud/networks/domain.py | 2 ++ hcloud/placement_groups/client.py | 2 ++ hcloud/placement_groups/domain.py | 2 ++ hcloud/primary_ips/client.py | 2 ++ hcloud/primary_ips/domain.py | 2 ++ hcloud/server_types/client.py | 2 ++ hcloud/server_types/domain.py | 2 ++ hcloud/servers/client.py | 2 ++ hcloud/servers/domain.py | 2 ++ hcloud/ssh_keys/client.py | 2 ++ hcloud/ssh_keys/domain.py | 2 ++ hcloud/volumes/client.py | 2 ++ hcloud/volumes/domain.py | 2 ++ pyproject.toml | 1 + setup.py | 2 ++ tests/unit/actions/conftest.py | 2 ++ tests/unit/actions/test_client.py | 2 ++ tests/unit/actions/test_domain.py | 2 ++ tests/unit/certificates/conftest.py | 2 ++ tests/unit/certificates/test_client.py | 2 ++ tests/unit/certificates/test_domain.py | 2 ++ tests/unit/conftest.py | 2 ++ tests/unit/core/test_client.py | 2 ++ tests/unit/core/test_domain.py | 2 ++ tests/unit/datacenters/conftest.py | 2 ++ tests/unit/datacenters/test_client.py | 2 ++ tests/unit/firewalls/conftest.py | 2 ++ tests/unit/firewalls/test_client.py | 2 ++ tests/unit/firewalls/test_domain.py | 2 ++ tests/unit/floating_ips/conftest.py | 2 ++ tests/unit/floating_ips/test_client.py | 2 ++ tests/unit/floating_ips/test_domain.py | 2 ++ tests/unit/helpers/test_labels.py | 2 ++ tests/unit/images/conftest.py | 2 ++ tests/unit/images/test_client.py | 2 ++ tests/unit/images/test_domain.py | 2 ++ tests/unit/isos/conftest.py | 2 ++ tests/unit/isos/test_client.py | 2 ++ tests/unit/isos/test_domain.py | 2 ++ tests/unit/load_balancer_types/conftest.py | 2 ++ tests/unit/load_balancer_types/test_client.py | 2 ++ tests/unit/load_balancers/conftest.py | 2 ++ tests/unit/load_balancers/test_client.py | 2 ++ tests/unit/load_balancers/test_domain.py | 2 ++ tests/unit/locations/conftest.py | 2 ++ tests/unit/locations/test_client.py | 2 ++ tests/unit/networks/conftest.py | 2 ++ tests/unit/networks/test_client.py | 2 ++ tests/unit/networks/test_domain.py | 2 ++ tests/unit/placement_groups/conftest.py | 2 ++ tests/unit/placement_groups/test_client.py | 2 ++ tests/unit/placement_groups/test_domain.py | 2 ++ tests/unit/primary_ips/conftest.py | 2 ++ tests/unit/primary_ips/test_client.py | 2 ++ tests/unit/primary_ips/test_domain.py | 2 ++ tests/unit/server_types/conftest.py | 2 ++ tests/unit/server_types/test_client.py | 2 ++ tests/unit/servers/conftest.py | 2 ++ tests/unit/servers/test_client.py | 2 ++ tests/unit/servers/test_domain.py | 2 ++ tests/unit/ssh_keys/conftest.py | 2 ++ tests/unit/ssh_keys/test_client.py | 2 ++ tests/unit/ssh_keys/test_domain.py | 2 ++ tests/unit/test_client.py | 2 ++ tests/unit/test_hcloud.py | 2 ++ tests/unit/volumes/conftest.py | 2 ++ tests/unit/volumes/test_client.py | 2 ++ tests/unit/volumes/test_domain.py | 2 ++ 103 files changed, 212 insertions(+), 8 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 8836d22a..4da293ea 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import sys from datetime import datetime diff --git a/examples/create_server.py b/examples/create_server.py index 2aaae82f..44451d69 100644 --- a/examples/create_server.py +++ b/examples/create_server.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from hcloud import Client from hcloud.images.domain import Image from hcloud.server_types.domain import ServerType diff --git a/examples/list_servers.py b/examples/list_servers.py index 9e34e5c0..005e9b6c 100644 --- a/examples/list_servers.py +++ b/examples/list_servers.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from hcloud import Client client = Client( diff --git a/examples/usage_oop.py b/examples/usage_oop.py index f2460528..0d453dec 100644 --- a/examples/usage_oop.py +++ b/examples/usage_oop.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from hcloud import Client from hcloud.images.domain import Image from hcloud.server_types.domain import ServerType diff --git a/examples/usage_procedurale.py b/examples/usage_procedurale.py index 5f25e0c0..94e3c30f 100644 --- a/examples/usage_procedurale.py +++ b/examples/usage_procedurale.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from hcloud import Client from hcloud.images.domain import Image from hcloud.server_types.domain import ServerType diff --git a/hcloud/__init__.py b/hcloud/__init__.py index 5beda91d..544a2196 100644 --- a/hcloud/__init__.py +++ b/hcloud/__init__.py @@ -1,2 +1,4 @@ +from __future__ import annotations + from ._client import Client # noqa from ._exceptions import APIException, HCloudException # noqa diff --git a/hcloud/__version__.py b/hcloud/__version__.py index 974595a6..d8bd11a0 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1 +1,3 @@ +from __future__ import annotations + VERSION = "1.26.0" # x-release-please-version diff --git a/hcloud/_client.py b/hcloud/_client.py index e62624f1..0f37fb1c 100644 --- a/hcloud/_client.py +++ b/hcloud/_client.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import time -from typing import Optional, Union import requests @@ -35,8 +36,8 @@ def __init__( 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, ): """Create an new Client instance @@ -186,7 +187,7 @@ def request( url: str, tries: int = 1, **kwargs, - ) -> Union[bytes, dict]: + ) -> bytes | dict: """Perform a request to the Hetzner Cloud API, wrapper around requests.request :param method: HTTP Method to perform the Request diff --git a/hcloud/_exceptions.py b/hcloud/_exceptions.py index d0801e95..029f823a 100644 --- a/hcloud/_exceptions.py +++ b/hcloud/_exceptions.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + class HCloudException(Exception): """There was an error while using the hcloud library""" diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index 2186b656..314a8059 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import time from ..core.client import BoundModelBase, ClientEntityBase diff --git a/hcloud/actions/domain.py b/hcloud/actions/domain.py index 1cc25d47..140fcad9 100644 --- a/hcloud/actions/domain.py +++ b/hcloud/actions/domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dateutil.parser import isoparse from .._exceptions import HCloudException diff --git a/hcloud/certificates/client.py b/hcloud/certificates/client.py index 1ab18bdc..548feb7e 100644 --- a/hcloud/certificates/client.py +++ b/hcloud/certificates/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ..actions.client import BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from ..core.domain import add_meta_to_result diff --git a/hcloud/certificates/domain.py b/hcloud/certificates/domain.py index f05173b5..04f83ffd 100644 --- a/hcloud/certificates/domain.py +++ b/hcloud/certificates/domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dateutil.parser import isoparse from ..core.domain import BaseDomain, DomainIdentityMixin diff --git a/hcloud/core/client.py b/hcloud/core/client.py index 10a2115f..3224ac6b 100644 --- a/hcloud/core/client.py +++ b/hcloud/core/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from .domain import add_meta_to_result @@ -34,7 +36,7 @@ def _get_all( list_function, # type: function results_list_attribute_name, # type: str *args, - **kwargs + **kwargs, ): # type (...) -> List[BoundModelBase] diff --git a/hcloud/core/domain.py b/hcloud/core/domain.py index c4d3308f..302ca9ec 100644 --- a/hcloud/core/domain.py +++ b/hcloud/core/domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections import namedtuple diff --git a/hcloud/datacenters/client.py b/hcloud/datacenters/client.py index 0ef212b4..9166648a 100644 --- a/hcloud/datacenters/client.py +++ b/hcloud/datacenters/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from ..locations.client import BoundLocation from ..server_types.client import BoundServerType diff --git a/hcloud/datacenters/domain.py b/hcloud/datacenters/domain.py index 984cc854..71df150a 100644 --- a/hcloud/datacenters/domain.py +++ b/hcloud/datacenters/domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ..core.domain import BaseDomain, DomainIdentityMixin diff --git a/hcloud/deprecation/domain.py b/hcloud/deprecation/domain.py index 8da63b95..1137a080 100644 --- a/hcloud/deprecation/domain.py +++ b/hcloud/deprecation/domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dateutil.parser import isoparse from ..core.domain import BaseDomain diff --git a/hcloud/firewalls/client.py b/hcloud/firewalls/client.py index 5ff716d3..a3c76d13 100644 --- a/hcloud/firewalls/client.py +++ b/hcloud/firewalls/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ..actions.client import BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from ..core.domain import add_meta_to_result diff --git a/hcloud/firewalls/domain.py b/hcloud/firewalls/domain.py index 861b40e8..3a4f0e33 100644 --- a/hcloud/firewalls/domain.py +++ b/hcloud/firewalls/domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dateutil.parser import isoparse from ..core.domain import BaseDomain diff --git a/hcloud/floating_ips/client.py b/hcloud/floating_ips/client.py index d5d7a16e..16a1dd52 100644 --- a/hcloud/floating_ips/client.py +++ b/hcloud/floating_ips/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ..actions.client import BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from ..core.domain import add_meta_to_result diff --git a/hcloud/floating_ips/domain.py b/hcloud/floating_ips/domain.py index e59a8dc2..84690b51 100644 --- a/hcloud/floating_ips/domain.py +++ b/hcloud/floating_ips/domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dateutil.parser import isoparse from ..core.domain import BaseDomain diff --git a/hcloud/hcloud.py b/hcloud/hcloud.py index af4e5c2a..df67a5be 100644 --- a/hcloud/hcloud.py +++ b/hcloud/hcloud.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import warnings warnings.warn( diff --git a/hcloud/helpers/labels.py b/hcloud/helpers/labels.py index 515982f0..c5c08c17 100644 --- a/hcloud/helpers/labels.py +++ b/hcloud/helpers/labels.py @@ -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 @@ def validate(labels: Dict[str, str]) -> bool: 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, if everything is fine. diff --git a/hcloud/images/client.py b/hcloud/images/client.py index 30c10af6..7e099154 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ..actions.client import BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from ..core.domain import add_meta_to_result diff --git a/hcloud/images/domain.py b/hcloud/images/domain.py index 4ca06001..b445f0bb 100644 --- a/hcloud/images/domain.py +++ b/hcloud/images/domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dateutil.parser import isoparse from ..core.domain import BaseDomain, DomainIdentityMixin diff --git a/hcloud/isos/client.py b/hcloud/isos/client.py index 4ca95282..9d0fcddb 100644 --- a/hcloud/isos/client.py +++ b/hcloud/isos/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from warnings import warn from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin diff --git a/hcloud/isos/domain.py b/hcloud/isos/domain.py index 52ae6952..647c4ab5 100644 --- a/hcloud/isos/domain.py +++ b/hcloud/isos/domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dateutil.parser import isoparse from ..core.domain import BaseDomain, DomainIdentityMixin diff --git a/hcloud/load_balancer_types/client.py b/hcloud/load_balancer_types/client.py index 405e2b50..12e5edd3 100644 --- a/hcloud/load_balancer_types/client.py +++ b/hcloud/load_balancer_types/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from .domain import LoadBalancerType diff --git a/hcloud/load_balancer_types/domain.py b/hcloud/load_balancer_types/domain.py index 7bdb1c05..799b10f2 100644 --- a/hcloud/load_balancer_types/domain.py +++ b/hcloud/load_balancer_types/domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ..core.domain import BaseDomain, DomainIdentityMixin diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index 25cc1343..4c913154 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ..actions.client import BoundAction from ..certificates.client import BoundCertificate from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index e9eda775..3a7b9815 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dateutil.parser import isoparse from ..core.domain import BaseDomain diff --git a/hcloud/locations/client.py b/hcloud/locations/client.py index 6171ed67..97b5fdfd 100644 --- a/hcloud/locations/client.py +++ b/hcloud/locations/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from .domain import Location diff --git a/hcloud/locations/domain.py b/hcloud/locations/domain.py index 6ca16d6f..47fcecf3 100644 --- a/hcloud/locations/domain.py +++ b/hcloud/locations/domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ..core.domain import BaseDomain, DomainIdentityMixin diff --git a/hcloud/networks/client.py b/hcloud/networks/client.py index 17152052..46d25f5e 100644 --- a/hcloud/networks/client.py +++ b/hcloud/networks/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ..actions.client import BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from ..core.domain import add_meta_to_result diff --git a/hcloud/networks/domain.py b/hcloud/networks/domain.py index 8a05cb1b..9658a227 100644 --- a/hcloud/networks/domain.py +++ b/hcloud/networks/domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dateutil.parser import isoparse from ..core.domain import BaseDomain diff --git a/hcloud/placement_groups/client.py b/hcloud/placement_groups/client.py index c5cb7c0b..ca78bc82 100644 --- a/hcloud/placement_groups/client.py +++ b/hcloud/placement_groups/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ..actions.client import BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from .domain import CreatePlacementGroupResponse, PlacementGroup diff --git a/hcloud/placement_groups/domain.py b/hcloud/placement_groups/domain.py index 2484414e..af95ab2d 100644 --- a/hcloud/placement_groups/domain.py +++ b/hcloud/placement_groups/domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dateutil.parser import isoparse from ..core.domain import BaseDomain diff --git a/hcloud/primary_ips/client.py b/hcloud/primary_ips/client.py index e8c8d672..d33f0f20 100644 --- a/hcloud/primary_ips/client.py +++ b/hcloud/primary_ips/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ..actions.client import BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from .domain import CreatePrimaryIPResponse, PrimaryIP diff --git a/hcloud/primary_ips/domain.py b/hcloud/primary_ips/domain.py index 6a0b83ad..b80f4723 100644 --- a/hcloud/primary_ips/domain.py +++ b/hcloud/primary_ips/domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dateutil.parser import isoparse from ..core.domain import BaseDomain diff --git a/hcloud/server_types/client.py b/hcloud/server_types/client.py index d572e7b4..70e29e69 100644 --- a/hcloud/server_types/client.py +++ b/hcloud/server_types/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from .domain import ServerType diff --git a/hcloud/server_types/domain.py b/hcloud/server_types/domain.py index bd2e26c3..a578d6b8 100644 --- a/hcloud/server_types/domain.py +++ b/hcloud/server_types/domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ..core.domain import BaseDomain, DomainIdentityMixin from ..deprecation.domain import DeprecationInfo diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index b88c69b0..99fe53b6 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ..actions.client import BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from ..core.domain import add_meta_to_result diff --git a/hcloud/servers/domain.py b/hcloud/servers/domain.py index b28f62e6..9aaf53cf 100644 --- a/hcloud/servers/domain.py +++ b/hcloud/servers/domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dateutil.parser import isoparse from ..core.domain import BaseDomain diff --git a/hcloud/ssh_keys/client.py b/hcloud/ssh_keys/client.py index e9a94023..66c13fa5 100644 --- a/hcloud/ssh_keys/client.py +++ b/hcloud/ssh_keys/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from .domain import SSHKey diff --git a/hcloud/ssh_keys/domain.py b/hcloud/ssh_keys/domain.py index 5f162f74..3dc8c645 100644 --- a/hcloud/ssh_keys/domain.py +++ b/hcloud/ssh_keys/domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dateutil.parser import isoparse from ..core.domain import BaseDomain, DomainIdentityMixin diff --git a/hcloud/volumes/client.py b/hcloud/volumes/client.py index f7f6bef5..bd9dff55 100644 --- a/hcloud/volumes/client.py +++ b/hcloud/volumes/client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ..actions.client import BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from ..core.domain import add_meta_to_result diff --git a/hcloud/volumes/domain.py b/hcloud/volumes/domain.py index 6b4ae903..6e469694 100644 --- a/hcloud/volumes/domain.py +++ b/hcloud/volumes/domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dateutil.parser import isoparse from ..core.domain import BaseDomain, DomainIdentityMixin diff --git a/pyproject.toml b/pyproject.toml index 3c43be34..a2a4e7a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [tool.isort] profile = "black" combine_as_imports = true +add_imports = ["from __future__ import annotations"] [tool.coverage.run] source = ["hcloud"] diff --git a/setup.py b/setup.py index 2dd05316..4d6e655c 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from setuptools import find_packages, setup with open("README.rst") as readme_file: diff --git a/tests/unit/actions/conftest.py b/tests/unit/actions/conftest.py index 2fbd3c5a..dbc3cc9f 100644 --- a/tests/unit/actions/conftest.py +++ b/tests/unit/actions/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/tests/unit/actions/test_client.py b/tests/unit/actions/test_client.py index ce8d65f8..29f192a7 100644 --- a/tests/unit/actions/test_client.py +++ b/tests/unit/actions/test_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import mock import pytest diff --git a/tests/unit/actions/test_domain.py b/tests/unit/actions/test_domain.py index 54b730c7..34f335f9 100644 --- a/tests/unit/actions/test_domain.py +++ b/tests/unit/actions/test_domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime from datetime import timezone diff --git a/tests/unit/certificates/conftest.py b/tests/unit/certificates/conftest.py index 5fdc5b5f..a13873da 100644 --- a/tests/unit/certificates/conftest.py +++ b/tests/unit/certificates/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/tests/unit/certificates/test_client.py b/tests/unit/certificates/test_client.py index e7581188..0d5c44ae 100644 --- a/tests/unit/certificates/test_client.py +++ b/tests/unit/certificates/test_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import mock import pytest diff --git a/tests/unit/certificates/test_domain.py b/tests/unit/certificates/test_domain.py index f2ec28df..90340cb8 100644 --- a/tests/unit/certificates/test_domain.py +++ b/tests/unit/certificates/test_domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime from datetime import timezone diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 0ab74906..0dd4df46 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import mock import pytest diff --git a/tests/unit/core/test_client.py b/tests/unit/core/test_client.py index e373884a..80bab5e3 100644 --- a/tests/unit/core/test_client.py +++ b/tests/unit/core/test_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import mock import pytest diff --git a/tests/unit/core/test_domain.py b/tests/unit/core/test_domain.py index 57cb7906..e9e06a35 100644 --- a/tests/unit/core/test_domain.py +++ b/tests/unit/core/test_domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from dateutil.parser import isoparse diff --git a/tests/unit/datacenters/conftest.py b/tests/unit/datacenters/conftest.py index f775869e..68d92b6b 100644 --- a/tests/unit/datacenters/conftest.py +++ b/tests/unit/datacenters/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/tests/unit/datacenters/test_client.py b/tests/unit/datacenters/test_client.py index 7e525527..e4df2047 100644 --- a/tests/unit/datacenters/test_client.py +++ b/tests/unit/datacenters/test_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import mock # noqa: F401 import pytest # noqa: F401 diff --git a/tests/unit/firewalls/conftest.py b/tests/unit/firewalls/conftest.py index 922acd42..26f8b853 100644 --- a/tests/unit/firewalls/conftest.py +++ b/tests/unit/firewalls/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/tests/unit/firewalls/test_client.py b/tests/unit/firewalls/test_client.py index 4094f6d4..ba644a96 100644 --- a/tests/unit/firewalls/test_client.py +++ b/tests/unit/firewalls/test_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import mock import pytest diff --git a/tests/unit/firewalls/test_domain.py b/tests/unit/firewalls/test_domain.py index bcbfea3e..d8d291fd 100644 --- a/tests/unit/firewalls/test_domain.py +++ b/tests/unit/firewalls/test_domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime from datetime import timezone diff --git a/tests/unit/floating_ips/conftest.py b/tests/unit/floating_ips/conftest.py index 755889ef..8165c3b6 100644 --- a/tests/unit/floating_ips/conftest.py +++ b/tests/unit/floating_ips/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/tests/unit/floating_ips/test_client.py b/tests/unit/floating_ips/test_client.py index 67bd2ff8..349f67f4 100644 --- a/tests/unit/floating_ips/test_client.py +++ b/tests/unit/floating_ips/test_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import mock import pytest diff --git a/tests/unit/floating_ips/test_domain.py b/tests/unit/floating_ips/test_domain.py index 3480ba52..fae67452 100644 --- a/tests/unit/floating_ips/test_domain.py +++ b/tests/unit/floating_ips/test_domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime from datetime import timezone diff --git a/tests/unit/helpers/test_labels.py b/tests/unit/helpers/test_labels.py index 013394c1..f5494cb0 100644 --- a/tests/unit/helpers/test_labels.py +++ b/tests/unit/helpers/test_labels.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from hcloud.helpers.labels import LabelValidator diff --git a/tests/unit/images/conftest.py b/tests/unit/images/conftest.py index 7ba4dbc1..0917fbae 100644 --- a/tests/unit/images/conftest.py +++ b/tests/unit/images/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/tests/unit/images/test_client.py b/tests/unit/images/test_client.py index 94ef4d0d..20e91395 100644 --- a/tests/unit/images/test_client.py +++ b/tests/unit/images/test_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime from datetime import timezone from unittest import mock diff --git a/tests/unit/images/test_domain.py b/tests/unit/images/test_domain.py index 4808ff94..437c1e72 100644 --- a/tests/unit/images/test_domain.py +++ b/tests/unit/images/test_domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime from datetime import timezone diff --git a/tests/unit/isos/conftest.py b/tests/unit/isos/conftest.py index 76f20738..0c045f5e 100644 --- a/tests/unit/isos/conftest.py +++ b/tests/unit/isos/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/tests/unit/isos/test_client.py b/tests/unit/isos/test_client.py index 13bc2426..6a0953b9 100644 --- a/tests/unit/isos/test_client.py +++ b/tests/unit/isos/test_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime from datetime import timezone from unittest import mock diff --git a/tests/unit/isos/test_domain.py b/tests/unit/isos/test_domain.py index aceead55..3f9f387a 100644 --- a/tests/unit/isos/test_domain.py +++ b/tests/unit/isos/test_domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime from datetime import timezone diff --git a/tests/unit/load_balancer_types/conftest.py b/tests/unit/load_balancer_types/conftest.py index 7e159c2e..0736d787 100644 --- a/tests/unit/load_balancer_types/conftest.py +++ b/tests/unit/load_balancer_types/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/tests/unit/load_balancer_types/test_client.py b/tests/unit/load_balancer_types/test_client.py index 7e6d1a19..45572288 100644 --- a/tests/unit/load_balancer_types/test_client.py +++ b/tests/unit/load_balancer_types/test_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import mock import pytest diff --git a/tests/unit/load_balancers/conftest.py b/tests/unit/load_balancers/conftest.py index 597bb919..dff89b10 100644 --- a/tests/unit/load_balancers/conftest.py +++ b/tests/unit/load_balancers/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/tests/unit/load_balancers/test_client.py b/tests/unit/load_balancers/test_client.py index 631e1134..bc1d49d1 100644 --- a/tests/unit/load_balancers/test_client.py +++ b/tests/unit/load_balancers/test_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import mock import pytest diff --git a/tests/unit/load_balancers/test_domain.py b/tests/unit/load_balancers/test_domain.py index cff4127c..7334c16a 100644 --- a/tests/unit/load_balancers/test_domain.py +++ b/tests/unit/load_balancers/test_domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime from datetime import timezone diff --git a/tests/unit/locations/conftest.py b/tests/unit/locations/conftest.py index 1ed03ea3..c7643ea4 100644 --- a/tests/unit/locations/conftest.py +++ b/tests/unit/locations/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/tests/unit/locations/test_client.py b/tests/unit/locations/test_client.py index 0bfcb65a..fd3072b7 100644 --- a/tests/unit/locations/test_client.py +++ b/tests/unit/locations/test_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import mock # noqa: F401 import pytest # noqa: F401 diff --git a/tests/unit/networks/conftest.py b/tests/unit/networks/conftest.py index 7320d4a5..69287d21 100644 --- a/tests/unit/networks/conftest.py +++ b/tests/unit/networks/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/tests/unit/networks/test_client.py b/tests/unit/networks/test_client.py index 5d70d99a..79f4825e 100644 --- a/tests/unit/networks/test_client.py +++ b/tests/unit/networks/test_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import mock import pytest diff --git a/tests/unit/networks/test_domain.py b/tests/unit/networks/test_domain.py index a704dfda..9fa2ac5f 100644 --- a/tests/unit/networks/test_domain.py +++ b/tests/unit/networks/test_domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime from datetime import timezone diff --git a/tests/unit/placement_groups/conftest.py b/tests/unit/placement_groups/conftest.py index bf82cdc3..37f532a4 100644 --- a/tests/unit/placement_groups/conftest.py +++ b/tests/unit/placement_groups/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/tests/unit/placement_groups/test_client.py b/tests/unit/placement_groups/test_client.py index 0bbe7869..afc5550d 100644 --- a/tests/unit/placement_groups/test_client.py +++ b/tests/unit/placement_groups/test_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import mock import pytest diff --git a/tests/unit/placement_groups/test_domain.py b/tests/unit/placement_groups/test_domain.py index 779304e7..357f5c75 100644 --- a/tests/unit/placement_groups/test_domain.py +++ b/tests/unit/placement_groups/test_domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime from datetime import timezone diff --git a/tests/unit/primary_ips/conftest.py b/tests/unit/primary_ips/conftest.py index 8885535f..4a7b23c6 100644 --- a/tests/unit/primary_ips/conftest.py +++ b/tests/unit/primary_ips/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/tests/unit/primary_ips/test_client.py b/tests/unit/primary_ips/test_client.py index 78863951..8cf0cde3 100644 --- a/tests/unit/primary_ips/test_client.py +++ b/tests/unit/primary_ips/test_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import mock import pytest diff --git a/tests/unit/primary_ips/test_domain.py b/tests/unit/primary_ips/test_domain.py index 091ea029..1856c65d 100644 --- a/tests/unit/primary_ips/test_domain.py +++ b/tests/unit/primary_ips/test_domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime from datetime import timezone diff --git a/tests/unit/server_types/conftest.py b/tests/unit/server_types/conftest.py index 5735f0e2..7f4b1344 100644 --- a/tests/unit/server_types/conftest.py +++ b/tests/unit/server_types/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/tests/unit/server_types/test_client.py b/tests/unit/server_types/test_client.py index 442da4b5..9797d277 100644 --- a/tests/unit/server_types/test_client.py +++ b/tests/unit/server_types/test_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from datetime import datetime, timezone from unittest import mock diff --git a/tests/unit/servers/conftest.py b/tests/unit/servers/conftest.py index 43d047f5..d1c304e5 100644 --- a/tests/unit/servers/conftest.py +++ b/tests/unit/servers/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/tests/unit/servers/test_client.py b/tests/unit/servers/test_client.py index 5c5e88af..5b491617 100644 --- a/tests/unit/servers/test_client.py +++ b/tests/unit/servers/test_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import mock import pytest diff --git a/tests/unit/servers/test_domain.py b/tests/unit/servers/test_domain.py index fc2bdeba..3dc83f98 100644 --- a/tests/unit/servers/test_domain.py +++ b/tests/unit/servers/test_domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime from datetime import timezone diff --git a/tests/unit/ssh_keys/conftest.py b/tests/unit/ssh_keys/conftest.py index 1b45c5f3..b465b241 100644 --- a/tests/unit/ssh_keys/conftest.py +++ b/tests/unit/ssh_keys/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/tests/unit/ssh_keys/test_client.py b/tests/unit/ssh_keys/test_client.py index a512122c..448a58c9 100644 --- a/tests/unit/ssh_keys/test_client.py +++ b/tests/unit/ssh_keys/test_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import mock import pytest diff --git a/tests/unit/ssh_keys/test_domain.py b/tests/unit/ssh_keys/test_domain.py index 9bea981a..db799edf 100644 --- a/tests/unit/ssh_keys/test_domain.py +++ b/tests/unit/ssh_keys/test_domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime from datetime import timezone diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 5f5bf811..36ac9288 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json from unittest.mock import MagicMock diff --git a/tests/unit/test_hcloud.py b/tests/unit/test_hcloud.py index 87ab50aa..c6931d7d 100644 --- a/tests/unit/test_hcloud.py +++ b/tests/unit/test_hcloud.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/tests/unit/volumes/conftest.py b/tests/unit/volumes/conftest.py index e4a52542..936ac6fa 100644 --- a/tests/unit/volumes/conftest.py +++ b/tests/unit/volumes/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/tests/unit/volumes/test_client.py b/tests/unit/volumes/test_client.py index fda74f99..9dc07fb4 100644 --- a/tests/unit/volumes/test_client.py +++ b/tests/unit/volumes/test_client.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import mock import pytest diff --git a/tests/unit/volumes/test_domain.py b/tests/unit/volumes/test_domain.py index 48946305..750adb4a 100644 --- a/tests/unit/volumes/test_domain.py +++ b/tests/unit/volumes/test_domain.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime from datetime import timezone From e63741fab50524f4e4098af5c77f806915ae93c8 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Thu, 20 Jul 2023 14:21:15 +0200 Subject: [PATCH 067/406] docs: update documentation (#247) * chore: add docs-dev makefile target * docs: update documentation * fix link * remove duplicate dev guide content --- CONTRIBUTING.rst | 51 ++++------------------ Makefile | 9 ++++ README.md | 92 +++++++++++++++++++++++++++++++++++++++ README.rst | 66 ---------------------------- docs/api.rst | 2 +- docs/index.md | 15 +++++++ docs/index.rst | 63 --------------------------- docs/installation.rst | 12 ++--- docs/samples.rst | 69 ----------------------------- examples/create_server.py | 9 ++-- setup.py | 3 +- 11 files changed, 136 insertions(+), 255 deletions(-) create mode 100644 README.md delete mode 100644 README.rst create mode 100644 docs/index.md delete mode 100644 docs/index.rst delete mode 100644 docs/samples.rst diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 324b1f66..ba0c50d8 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -55,41 +55,27 @@ If you are proposing a feature: Get Started! ------------- -Ready to contribute? Here's how to set up `hcloud-python` for local development. +Ready to contribute? Here's how to set up ``hcloud-python`` for local development. -1. Fork the `hcloud-python` repo on GitHub. +1. Fork the ``hcloud-python`` repo on GitHub. 2. Clone your fork locally:: $ git clone git@github.com:your_name_here/hcloud-python.git -3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: - - $ mkvirtualenv hcloud-python - $ cd hcloud-python/ - $ python setup.py develop - +3. Read the ``Development`` section in the ``README.md``, to setup your development environment. 4. Create a branch for local development:: $ git checkout -b name-of-your-bugfix-or-feature Now you can make your changes locally. -5. When you're done making changes, check that your changes pass pre-commit and the - tests, including testing other Python versions with tox:: - - $ tox -e pre-commit - $ python setup.py test or py.test - $ tox - - To get pre-commit and tox, just pip install them into your virtualenv. - -6. Commit your changes and push your branch to GitHub:: +5. Commit your changes and push your branch to GitHub:: $ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature -7. Submit a pull request through the GitHub website. +6. Submit a pull request through the GitHub website. Pull Request Guidelines ------------------------ @@ -99,27 +85,6 @@ Before you submit a pull request, check that it meets these guidelines: 1. The pull request should include tests. 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the - feature to the list in README.rst. -3. The pull request should work for Python 2.7, 3.5 and 3.6, and for PyPy. Check - https://travis-ci.org/hetznercloud/hcloud-python/pull_requests - and make sure that the tests pass for all supported Python versions. - -Tips ------ - -To run a subset of tests:: - -$ py.test tests.test_hetznercloud - - -How to release ---------------- - -A reminder for the maintainers on how to release a new version. -Make sure all your changes are committed (including an entry in CHANGELOG.rst). -Then run:: - -1. Change the version under /hcloud/version.py -2. Push the change to the `master` branch and tag an new release through the `Github UI `_. - -Travis will then deploy to PyPI if tests pass. + feature to the list in README.md. +3. The pull request should work for all the versions of Python the library supports, and + for PyPy. diff --git a/Makefile b/Makefile index d52cef44..119b651a 100644 --- a/Makefile +++ b/Makefile @@ -20,5 +20,14 @@ docs: venv $(MAKE) -C docs html xdg-open docs/_build/html/index.html +docs-dev: venv docs + venv/bin/watchmedo shell-command \ + --patterns="*.py;*.rst;*.md" \ + --ignore-pattern=".git/*" \ + --recursive \ + --drop \ + --command="$(MAKE) -C docs html" \ + . + clean: git clean -xdf diff --git a/README.md b/README.md new file mode 100644 index 00000000..bccec255 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# Hetzner Cloud Python + +[![](https://github.com/hetznercloud/hcloud-python/actions/workflows/test.yml/badge.svg)](https://github.com/hetznercloud/hcloud-python/actions/workflows/test.yml) +[![](https://github.com/hetznercloud/hcloud-python/actions/workflows/lint.yml/badge.svg)](https://github.com/hetznercloud/hcloud-python/actions/workflows/lint.yml) +[![](https://readthedocs.org/projects/hcloud-python/badge/?version=latest)](https://hcloud-python.readthedocs.io) +[![](https://img.shields.io/pypi/pyversions/hcloud.svg)](https://pypi.org/project/hcloud/) + +Official Hetzner Cloud python library. + +The library's documentation is available at [hcloud-python.readthedocs.io](https://hcloud-python.readthedocs.io), the public API documentation is available at [docs.hetzner.cloud](https://docs.hetzner.cloud). + +## Usage + +Install the `hcloud` library: + +```sh +pip install hcloud +``` + +For more installation details, please see the [installation docs](https://hcloud-python.readthedocs.io/en/stable/installation.html). + +Here is an example that creates a server and list them: + +```python +from hcloud import Client +from hcloud.images.domain import Image +from hcloud.server_types.domain import ServerType + +client = Client(token="{YOUR_API_TOKEN}") # Please paste your API token here + +# Create a server named my-server +response = client.servers.create( + name="my-server", + server_type=ServerType(name="cx11"), + image=Image(name="ubuntu-22.04"), +) +server = response.server +print(f"{server.id=} {server.name=} {server.status=}") +print(f"root password: {response.root_password}") + +# List your servers +servers = client.servers.get_all() +for server in servers: + print(f"{server.id=} {server.name=} {server.status=}") +``` + +For more details, please see the [API reference](https://hcloud-python.readthedocs.io/en/stable/api.html). + +You can find some more examples under the [`examples/`](https://github.com/hetznercloud/hcloud-python/tree/main/examples) directory. + +## Supported Python versions + +We support python versions until [`end-of-life`](https://devguide.python.org/versions/#status-of-python-versions). + +## Development + +First, create a virtual environment and activate it: + +```sh +make venv +source venv/bin/activate +``` + +You may setup [`pre-commit`](https://pre-commit.com/) to run before you commit changes, this removes the need to run it manually afterwards: + +```sh +pre-commit install +``` + +You can then run different tasks defined in the `Makefile`, below are the most important ones: + +Build the documentation and open it in your browser: + +```sh +make docs +``` + +Run tests using the current `python3` version: + +```sh +make test +``` + +You may also run the tests for multiple `python3` versions using `tox`: + +```sh +tox . +``` + +## License + +The MIT License (MIT). Please see [`License File`](https://github.com/hetznercloud/hcloud-python/blob/main/LICENSE) for more information. diff --git a/README.rst b/README.rst deleted file mode 100644 index 9accd0d4..00000000 --- a/README.rst +++ /dev/null @@ -1,66 +0,0 @@ -Hetzner Cloud Python -==================== - -.. image:: https://github.com/hetznercloud/hcloud-python/actions/workflows/test.yml/badge.svg - :target: https://github.com/hetznercloud/hcloud-python/actions/workflows/test.yml -.. image:: https://github.com/hetznercloud/hcloud-python/actions/workflows/lint.yml/badge.svg - :target: https://github.com/hetznercloud/hcloud-python/actions/workflows/lint.yml -.. image:: https://readthedocs.org/projects/hcloud-python/badge/?version=latest - :target: https://hcloud-python.readthedocs.io -.. image:: https://img.shields.io/pypi/pyversions/hcloud.svg - :target: https://pypi.org/project/hcloud/ - -Official Hetzner Cloud python library - -The library's documentation is available at `ReadTheDocs`_, the public API documentation is available at https://docs.hetzner.cloud. - -.. _ReadTheDocs: https://hcloud-python.readthedocs.io - -Usage example -------------- - -After the documentation has been created, click on `Usage` section - -Or open `docs/usage.rst` - -You can find some more examples under `examples/`. - - -Supported Python versions -------------------------- - -We support python versions until `end-of-life`_. - -.. _end-of-life: https://devguide.python.org/versions/#status-of-python-versions - -Development ------------ - -Setup Dev Environment ---------------------- -1) `python3 -m venv venv && source venv/bin/activate` - -2) `pip install -e .` or `pip install -e .[docs]` to be able to build docs - - -Run tests ---------- -* `tox .` -* You can specify environment e.g `tox -e py36` -* You can test the code style with `tox -e pre-commit` - -Create Documentation --------------------- - -Run `make docs`. This will also open a documentation in a tab in your default browser. - - -Style Guide -------------- -* **Type Hints**: If the type hint line is too long use inline hinting. Maximum inline type hint line should be 150 chars. - -License -------------- -The MIT License (MIT). Please see `License File`_ for more information. - -.. _License File: https://github.com/hetznercloud/hcloud-python/blob/main/LICENSE diff --git a/docs/api.rst b/docs/api.rst index e284673a..1bb9b638 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,4 +1,4 @@ -hcloud-python API +API References ================== Main Interface diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..915e125c --- /dev/null +++ b/docs/index.md @@ -0,0 +1,15 @@ +:::{toctree} +:maxdepth: 4 +:hidden: + +self +installation.rst +api.rst +Hetzner Cloud API Documentation +contributing.rst +changelog.md + +::: + +:::{include} ../README.md +::: diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 27c994de..00000000 --- a/docs/index.rst +++ /dev/null @@ -1,63 +0,0 @@ -.. toctree:: - :maxdepth: 4 - :hidden: - - self - installation - samples - api - Hetzner Cloud API Documentation - contributing - changelog.md - -Hetzner Cloud Python -==================== - - -.. image:: https://travis-ci.com/hetznercloud/hcloud-python.svg?branch=master - :target: https://travis-ci.com/hetznercloud/hcloud-python -.. image:: https://readthedocs.org/projects/hcloud-python/badge/?version=latest - :target: https://hcloud-python.readthedocs.io - -This is the official `Hetzner Cloud`_ python library. - -.. _Hetzner Cloud: https://www.hetzner.com/cloud - -Examples -------------- - -Create Server -------------- -.. code-block:: python - :linenos: - - from hcloud import Client - from hcloud.server_types.domain import ServerType - from hcloud.images.domain import Image - - client = Client(token="{YOUR_API_TOKEN}") # Please paste your API token here between the quotes - response = client.servers.create(name="my-server", server_type=ServerType(name="cx11"), image=Image(name="ubuntu-20.04")) - server = response.server - print(server) - print("Root Password: " + response.root_password) - -List Servers ------------- -.. code-block:: python - :linenos: - - from hcloud import Client - - client = Client(token="{YOUR_API_TOKEN}") # Please paste your API token here between the quotes - servers = client.servers.get_all() - print(servers) - -You can find more examples in the `Example Folder`_ in the Github Repository. - -.. _Example Folder: https://github.com/hetznercloud/hcloud-python/tree/master/examples - -License -------- -The MIT License (MIT). Please see `License File`_ for more information. - -.. _License File: https://github.com/hetznercloud/hcloud-python/blob/master/LICENSE diff --git a/docs/installation.rst b/docs/installation.rst index 2908f0c5..0b6f0dba 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -38,7 +38,7 @@ Hetzner Cloud Python is also available as a ``conda``-package via `conda-forge`. From sources ------------ -The sources for Hetzner Cloud Python can be downloaded from the `Github repo`_. +The sources for Hetzner Cloud Python can be downloaded from the Github repo. You can either clone the public repository: @@ -46,18 +46,14 @@ You can either clone the public repository: $ git clone git://github.com/hetznercloud/hcloud-python -Or download the `tarball`_: +Or download the tarball: .. code-block:: console - $ curl -OL https://github.com/hetznercloud/hcloud-python/tarball/master + $ curl -OL https://github.com/hetznercloud/hcloud-python/tarball/main Once you have a copy of the source, you can install it with: .. code-block:: console - $ python setup.py install - - -.. _Github repo: https://github.com/hetznercloud/hcloud-python -.. _tarball: https://github.com/hetznercloud/hcloud-python/tarball/master + $ pip install . diff --git a/docs/samples.rst b/docs/samples.rst deleted file mode 100644 index 8f2d1203..00000000 --- a/docs/samples.rst +++ /dev/null @@ -1,69 +0,0 @@ -======== -Samples -======== - -To use Hetzner Cloud Python in a project: - -.. code-block:: python - - from hcloud import Client - from hcloud.images.domain import Image - from hcloud.server_types.domain import ServerType - - # Create a client - client = Client(token="project-token") - - # Create 2 servers - # Create 2 servers - response1 = client.servers.create( - "Server1", - server_type=ServerType(name="cx11"), - image=Image(id=4711) - ) - - response2 = client.servers.create( - "Server2", - server_type=ServerType(name="cx11"), - image=Image(id=4711) - ) - # Get all servers - server1 = response1.server - server2 = response2.server - - servers = client.servers.get_all() - - assert servers[0].id == server1.id - assert servers[1].id == server2.id - # Create 2 volumes - - response1 = client.volumes.create( - size=15, - name="Volume1", - location=server1.location - ) - response2 = client.volumes.create( - size=10, - name="Volume2", - location=server2.location - ) - - volume1 = response1.volume - volume2 = response2.volume - - # Attach volume to server - - volume1.attach(server1) - volume2.attach(server2) - - # Detach second volume - - volume2.detach() - - # Poweroff 2nd server - server2.power_off() - - # Poweroff 2nd server - server2.power_off() - - -More samples are in the repository: https://github.com/hetznercloud/hcloud-python/tree/master/examples. diff --git a/examples/create_server.py b/examples/create_server.py index 44451d69..46a724df 100644 --- a/examples/create_server.py +++ b/examples/create_server.py @@ -4,11 +4,12 @@ from hcloud.images.domain import Image from hcloud.server_types.domain import ServerType -client = Client( - token="{YOUR_API_TOKEN}" -) # Please paste your API token here between the quotes +# Please paste your API token here between the quotes +client = Client(token="{YOUR_API_TOKEN}") response = client.servers.create( - name="my-server", server_type=ServerType("cx11"), image=Image(name="ubuntu-20.04") + name="my-server", + server_type=ServerType("cx11"), + image=Image(name="ubuntu-20.04"), ) server = response.server print(server) diff --git a/setup.py b/setup.py index 4d6e655c..a596aae9 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages, setup -with open("README.rst") as readme_file: +with open("README.md") as readme_file: readme = readme_file.read() version = {} @@ -46,6 +46,7 @@ "sphinx>=6.2.1,<7.0", "sphinx-rtd-theme>=1.2.2,<1.3", "myst-parser>=2.0.0,<2.1", + "watchdog>=3.0.0,<3.1", ], "test": [ "coverage>=7.2.7,<7.3", From c89b5bd21469a60af8b7ed99247225610ab69901 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 21 Jul 2023 17:30:27 +0200 Subject: [PATCH 068/406] refactor: use typed namedtuples for the `get_list` page result (#251) This is a nice refactor that remove some magic from the `get_list` methods. The `get_list` function returns a named tuple that always has the following structure `(result, meta)`. Since this is a tuple, we can unpack it without using the named attributes, and remove the `results_list_attribute_name` attributes used to access the data inside the tuple. - Use predefined `NamedTuples` classes instead of dynamic (magic) `namedtuples` for the `get_list` `PageResult`. This will greatly improve the library typing definition. - Remove unused functions and attributes after the refactor. This cleans up and simplify the code. * refactor: simplify client _get_all implementation * refactor: rewrite Meta.parse_meta method * refactor: simplify GetEntityByNameMixin * refactor: don't use dynamic page results namedtuple * refactor: simplify ImagesClient.get_by_name_and_architecture * refactor: remove unused results_list_attribute_name attr * add comment about page result tuple unpacking --- hcloud/actions/client.py | 11 +++++-- hcloud/certificates/client.py | 17 ++++++---- hcloud/core/client.py | 45 ++++++-------------------- hcloud/core/domain.py | 22 ++++--------- hcloud/datacenters/client.py | 12 +++++-- hcloud/firewalls/client.py | 17 ++++++---- hcloud/floating_ips/client.py | 17 ++++++---- hcloud/images/client.py | 20 +++++++----- hcloud/isos/client.py | 11 +++++-- hcloud/load_balancer_types/client.py | 14 ++++++-- hcloud/load_balancers/client.py | 19 +++++++---- hcloud/locations/client.py | 12 +++++-- hcloud/networks/client.py | 19 +++++++---- hcloud/placement_groups/client.py | 12 +++++-- hcloud/primary_ips/client.py | 12 +++++-- hcloud/server_types/client.py | 12 +++++-- hcloud/servers/client.py | 17 ++++++---- hcloud/ssh_keys/client.py | 14 +++++--- hcloud/volumes/client.py | 17 ++++++---- tests/unit/core/test_client.py | 48 ++++++++-------------------- tests/unit/core/test_domain.py | 29 +---------------- 21 files changed, 204 insertions(+), 193 deletions(-) diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index 314a8059..ef262384 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -1,8 +1,10 @@ from __future__ import annotations import time +from typing import NamedTuple from ..core.client import BoundModelBase, ClientEntityBase +from ..core.domain import Meta from .domain import Action, ActionFailedException, ActionTimeoutException @@ -29,9 +31,12 @@ def wait_until_finished(self, max_retries=100): raise ActionFailedException(action=self) -class ActionsClient(ClientEntityBase): - results_list_attribute_name = "actions" +class ActionsPageResult(NamedTuple): + actions: list[BoundAction] + meta: Meta | None + +class ActionsClient(ClientEntityBase): def get_by_id(self, id): # type: (int) -> BoundAction """Get a specific action by its ID. @@ -77,7 +82,7 @@ def get_list( actions = [ BoundAction(self, action_data) for action_data in response["actions"] ] - return self._add_meta_to_result(actions, response) + return ActionsPageResult(actions, Meta.parse_meta(response)) def get_all(self, status=None, sort=None): # type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction] diff --git a/hcloud/certificates/client.py b/hcloud/certificates/client.py index 548feb7e..2f60bd1e 100644 --- a/hcloud/certificates/client.py +++ b/hcloud/certificates/client.py @@ -1,8 +1,10 @@ from __future__ import annotations -from ..actions.client import BoundAction +from typing import NamedTuple + +from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import add_meta_to_result +from ..core.domain import Meta from .domain import ( Certificate, CreateManagedCertificateResponse, @@ -83,9 +85,12 @@ def retry_issuance(self): return self._client.retry_issuance(self) -class CertificatesClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "certificates" +class CertificatesPageResult(NamedTuple): + certificates: list[BoundCertificate] + meta: Meta | None + +class CertificatesClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundCertificate """Get a specific certificate by its ID. @@ -138,7 +143,7 @@ def get_list( for certificate_data in response["certificates"] ] - return self._add_meta_to_result(certificates, response) + return CertificatesPageResult(certificates, Meta.parse_meta(response)) def get_all(self, name=None, label_selector=None): # type: (Optional[str], Optional[str]) -> List[BoundCertificate] @@ -287,7 +292,7 @@ def get_actions_list( BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] - return add_meta_to_result(actions, response, "actions") + return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions(self, certificate, status=None, sort=None): # type: (Certificate, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] diff --git a/hcloud/core/client.py b/hcloud/core/client.py index 3224ac6b..b0bde8c1 100644 --- a/hcloud/core/client.py +++ b/hcloud/core/client.py @@ -1,7 +1,5 @@ from __future__ import annotations -from .domain import add_meta_to_result - class ClientEntityBase: max_per_page = 50 @@ -14,44 +12,25 @@ def __init__(self, client): """ self._client = client - def _is_list_attribute_implemented(self): - if self.results_list_attribute_name is None: - raise NotImplementedError( - "in order to get results list, 'results_list_attribute_name' attribute of {} has to be specified".format( - self.__class__.__name__ - ) - ) - - def _add_meta_to_result( - self, - results, # type: List[BoundModelBase] - response, # type: json - ): - # type: (...) -> PageResult - self._is_list_attribute_implemented() - return add_meta_to_result(results, response, self.results_list_attribute_name) - def _get_all( self, list_function, # type: function - results_list_attribute_name, # type: str *args, **kwargs, ): # type (...) -> List[BoundModelBase] - - page = 1 - results = [] + page = 1 while page: - page_result = list_function( + # The *PageResult tuples MUST have the following structure + # `(result: List[Bound*], meta: Meta)` + result, meta = list_function( page=page, per_page=self.max_per_page, *args, **kwargs ) - result = getattr(page_result, results_list_attribute_name) if result: results.extend(result) - meta = page_result.meta + if ( meta and meta.pagination @@ -66,17 +45,14 @@ def _get_all( def get_all(self, *args, **kwargs): # type: (...) -> List[BoundModelBase] - self._is_list_attribute_implemented() - return self._get_all( - self.get_list, self.results_list_attribute_name, *args, **kwargs - ) + return self._get_all(self.get_list, *args, **kwargs) def get_actions(self, *args, **kwargs): # type: (...) -> List[BoundModelBase] if not hasattr(self, "get_actions_list"): raise ValueError("this endpoint does not support get_actions method") - return self._get_all(self.get_actions_list, "actions", *args, **kwargs) + return self._get_all(self.get_actions_list, *args, **kwargs) class GetEntityByNameMixin: @@ -86,11 +62,8 @@ class GetEntityByNameMixin: def get_by_name(self, name): # type: (str) -> BoundModelBase - self._is_list_attribute_implemented() - response = self.get_list(name=name) - entities = getattr(response, self.results_list_attribute_name) - entity = entities[0] if entities else None - return entity + entities, _ = self.get_list(name=name) + return entities[0] if entities else None class BoundModelBase: diff --git a/hcloud/core/domain.py b/hcloud/core/domain.py index 302ca9ec..8b0a2574 100644 --- a/hcloud/core/domain.py +++ b/hcloud/core/domain.py @@ -1,7 +1,5 @@ from __future__ import annotations -from collections import namedtuple - class BaseDomain: __slots__ = () @@ -63,19 +61,13 @@ def __init__(self, pagination=None): self.pagination = pagination @classmethod - def parse_meta(cls, json_content): + def parse_meta(cls, response: dict) -> Meta | None: meta = None - if json_content and "meta" in json_content: + if response and "meta" in response: meta = cls() - pagination_json = json_content["meta"].get("pagination") - if pagination_json: - pagination = Pagination(**pagination_json) - meta.pagination = pagination - return meta + try: + meta.pagination = Pagination(**response["meta"]["pagination"]) + except KeyError: + pass - -def add_meta_to_result(result, json_content, attr_name): - # type: (List[BoundModelBase], json, string) -> PageResult - class_name = f"PageResults{attr_name.capitalize()}" - PageResults = namedtuple(class_name, [attr_name, "meta"]) - return PageResults(**{attr_name: result, "meta": Meta.parse_meta(json_content)}) + return meta diff --git a/hcloud/datacenters/client.py b/hcloud/datacenters/client.py index 9166648a..76e3c707 100644 --- a/hcloud/datacenters/client.py +++ b/hcloud/datacenters/client.py @@ -1,6 +1,9 @@ from __future__ import annotations +from typing import NamedTuple + from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import Meta from ..locations.client import BoundLocation from ..server_types.client import BoundServerType from .domain import Datacenter, DatacenterServerTypes @@ -43,9 +46,12 @@ def __init__(self, client, data): super().__init__(client, data) -class DatacentersClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "datacenters" +class DatacentersPageResult(NamedTuple): + datacenters: list[BoundDatacenter] + meta: Meta | None + +class DatacentersClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundDatacenter """Get a specific datacenter by its ID. @@ -90,7 +96,7 @@ def get_list( for datacenter_data in response["datacenters"] ] - return self._add_meta_to_result(datacenters, response) + return DatacentersPageResult(datacenters, Meta.parse_meta(response)) def get_all(self, name=None): # type: (Optional[str]) -> List[BoundDatacenter] diff --git a/hcloud/firewalls/client.py b/hcloud/firewalls/client.py index a3c76d13..118fc850 100644 --- a/hcloud/firewalls/client.py +++ b/hcloud/firewalls/client.py @@ -1,8 +1,10 @@ from __future__ import annotations -from ..actions.client import BoundAction +from typing import NamedTuple + +from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import add_meta_to_result +from ..core.domain import Meta from .domain import ( CreateFirewallResponse, Firewall, @@ -134,9 +136,12 @@ def remove_from_resources(self, resources): return self._client.remove_from_resources(self, resources) -class FirewallsClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "firewalls" +class FirewallsPageResult(NamedTuple): + firewalls: list[BoundFirewall] + meta: Meta | None + +class FirewallsClient(ClientEntityBase, GetEntityByNameMixin): def get_actions_list( self, firewall, # type: Firewall @@ -177,7 +182,7 @@ def get_actions_list( BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] - return add_meta_to_result(actions, response, "actions") + return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions( self, @@ -249,7 +254,7 @@ def get_list( for firewall_data in response["firewalls"] ] - return self._add_meta_to_result(firewalls, response) + return FirewallsPageResult(firewalls, Meta.parse_meta(response)) def get_all(self, label_selector=None, name=None, sort=None): # type: (Optional[str], Optional[str], Optional[List[str]]) -> List[BoundFirewall] diff --git a/hcloud/floating_ips/client.py b/hcloud/floating_ips/client.py index 16a1dd52..f6d56113 100644 --- a/hcloud/floating_ips/client.py +++ b/hcloud/floating_ips/client.py @@ -1,8 +1,10 @@ from __future__ import annotations -from ..actions.client import BoundAction +from typing import NamedTuple + +from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import add_meta_to_result +from ..core.domain import Meta from ..locations.client import BoundLocation from .domain import CreateFloatingIPResponse, FloatingIP @@ -119,9 +121,12 @@ def change_dns_ptr(self, ip, dns_ptr): return self._client.change_dns_ptr(self, ip, dns_ptr) -class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "floating_ips" +class FloatingIPsPageResult(NamedTuple): + floating_ips: list[BoundFloatingIP] + meta: Meta | None + +class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin): def get_actions_list( self, floating_ip, # type: FloatingIP @@ -164,7 +169,7 @@ def get_actions_list( BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] - return add_meta_to_result(actions, response, "actions") + return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions( self, @@ -234,7 +239,7 @@ def get_list( for floating_ip_data in response["floating_ips"] ] - return self._add_meta_to_result(floating_ips, response) + return FloatingIPsPageResult(floating_ips, Meta.parse_meta(response)) def get_all(self, label_selector=None, name=None): # type: (Optional[str], Optional[str]) -> List[BoundFloatingIP] diff --git a/hcloud/images/client.py b/hcloud/images/client.py index 7e099154..55729a0f 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -1,8 +1,10 @@ from __future__ import annotations -from ..actions.client import BoundAction +from typing import NamedTuple + +from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import add_meta_to_result +from ..core.domain import Meta from .domain import Image @@ -89,9 +91,12 @@ def change_protection(self, delete=None): return self._client.change_protection(self, delete) -class ImagesClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "images" +class ImagesPageResult(NamedTuple): + images: list[BoundImage] + meta: Meta | None + +class ImagesClient(ClientEntityBase, GetEntityByNameMixin): def get_actions_list( self, image, # type: Image @@ -132,7 +137,7 @@ def get_actions_list( BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] - return add_meta_to_result(actions, response, "actions") + return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions( self, @@ -224,7 +229,7 @@ def get_list( response = self._client.request(url="/images", method="GET", params=params) images = [BoundImage(self, image_data) for image_data in response["images"]] - return self._add_meta_to_result(images, response) + return ImagesPageResult(images, Meta.parse_meta(response)) def get_all( self, @@ -291,8 +296,7 @@ def get_by_name_and_architecture(self, name, architecture): Used to identify the image. :return: :class:`BoundImage ` """ - response = self.get_list(name=name, architecture=[architecture]) - entities = getattr(response, self.results_list_attribute_name) + entities, _ = self.get_list(name=name, architecture=[architecture]) entity = entities[0] if entities else None return entity diff --git a/hcloud/isos/client.py b/hcloud/isos/client.py index 9d0fcddb..fba3930e 100644 --- a/hcloud/isos/client.py +++ b/hcloud/isos/client.py @@ -1,8 +1,10 @@ from __future__ import annotations +from typing import NamedTuple from warnings import warn from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import Meta from .domain import Iso @@ -10,9 +12,12 @@ class BoundIso(BoundModelBase): model = Iso -class IsosClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "isos" +class IsosPageResult(NamedTuple): + isos: list[BoundIso] + meta: Meta | None + +class IsosClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundIso """Get a specific ISO by its id @@ -72,7 +77,7 @@ def get_list( response = self._client.request(url="/isos", method="GET", params=params) isos = [BoundIso(self, iso_data) for iso_data in response["isos"]] - return self._add_meta_to_result(isos, response) + return IsosPageResult(isos, Meta.parse_meta(response)) def get_all( self, diff --git a/hcloud/load_balancer_types/client.py b/hcloud/load_balancer_types/client.py index 12e5edd3..7dff45a3 100644 --- a/hcloud/load_balancer_types/client.py +++ b/hcloud/load_balancer_types/client.py @@ -1,6 +1,9 @@ from __future__ import annotations +from typing import NamedTuple + from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import Meta from .domain import LoadBalancerType @@ -8,9 +11,12 @@ class BoundLoadBalancerType(BoundModelBase): model = LoadBalancerType -class LoadBalancerTypesClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "load_balancer_types" +class LoadBalancerTypesPageResult(NamedTuple): + load_balancer_types: list[BoundLoadBalancerType] + meta: Meta | None + +class LoadBalancerTypesClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> load_balancer_types.client.BoundLoadBalancerType """Returns a specific Load Balancer Type. @@ -53,7 +59,9 @@ def get_list(self, name=None, page=None, per_page=None): BoundLoadBalancerType(self, load_balancer_type_data) for load_balancer_type_data in response["load_balancer_types"] ] - return self._add_meta_to_result(load_balancer_types, response) + return LoadBalancerTypesPageResult( + load_balancer_types, Meta.parse_meta(response) + ) def get_all(self, name=None): # type: (Optional[str]) -> List[BoundLoadBalancerType] diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index 4c913154..85e73d82 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -1,9 +1,11 @@ from __future__ import annotations -from ..actions.client import BoundAction +from typing import NamedTuple + +from ..actions.client import ActionsPageResult, BoundAction from ..certificates.client import BoundCertificate from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import add_meta_to_result +from ..core.domain import Meta from ..load_balancer_types.client import BoundLoadBalancerType from ..locations.client import BoundLocation from ..networks.client import BoundNetwork @@ -310,9 +312,12 @@ def change_type(self, load_balancer_type): return self._client.change_type(self, load_balancer_type) -class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "load_balancers" +class LoadBalancersPageResult(NamedTuple): + load_balancers: list[BoundLoadBalancer] + meta: Meta | None + +class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundLoadBalancer """Get a specific Load Balancer @@ -360,11 +365,11 @@ def get_list( url="/load_balancers", method="GET", params=params ) - ass_load_balancers = [ + load_balancers = [ BoundLoadBalancer(self, load_balancer_data) for load_balancer_data in response["load_balancers"] ] - return self._add_meta_to_result(ass_load_balancers, response) + return LoadBalancersPageResult(load_balancers, Meta.parse_meta(response)) def get_all(self, name=None, label_selector=None): # type: (Optional[str], Optional[str]) -> List[BoundLoadBalancer] @@ -550,7 +555,7 @@ def get_actions_list( BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] - return add_meta_to_result(actions, response, "actions") + return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions(self, load_balancer, status=None, sort=None): # type: (LoadBalancer, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] diff --git a/hcloud/locations/client.py b/hcloud/locations/client.py index 97b5fdfd..c21c6a75 100644 --- a/hcloud/locations/client.py +++ b/hcloud/locations/client.py @@ -1,6 +1,9 @@ from __future__ import annotations +from typing import NamedTuple + from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import Meta from .domain import Location @@ -8,9 +11,12 @@ class BoundLocation(BoundModelBase): model = Location -class LocationsClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "locations" +class LocationsPageResult(NamedTuple): + locations: list[BoundLocation] + meta: Meta | None + +class LocationsClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> locations.client.BoundLocation """Get a specific location by its ID. @@ -46,7 +52,7 @@ def get_list(self, name=None, page=None, per_page=None): BoundLocation(self, location_data) for location_data in response["locations"] ] - return self._add_meta_to_result(locations, response) + return LocationsPageResult(locations, Meta.parse_meta(response)) def get_all(self, name=None): # type: (Optional[str]) -> List[BoundLocation] diff --git a/hcloud/networks/client.py b/hcloud/networks/client.py index 46d25f5e..6a93c0cd 100644 --- a/hcloud/networks/client.py +++ b/hcloud/networks/client.py @@ -1,8 +1,10 @@ from __future__ import annotations -from ..actions.client import BoundAction +from typing import NamedTuple + +from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import add_meta_to_result +from ..core.domain import Meta from .domain import Network, NetworkRoute, NetworkSubnet @@ -153,9 +155,12 @@ def change_protection(self, delete=None): return self._client.change_protection(self, delete=delete) -class NetworksClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "networks" +class NetworksPageResult(NamedTuple): + networks: list[BoundNetwork] + meta: Meta | None + +class NetworksClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundNetwork """Get a specific network @@ -198,10 +203,10 @@ def get_list( response = self._client.request(url="/networks", method="GET", params=params) - ass_networks = [ + networks = [ BoundNetwork(self, network_data) for network_data in response["networks"] ] - return self._add_meta_to_result(ass_networks, response) + return NetworksPageResult(networks, Meta.parse_meta(response)) def get_all(self, name=None, label_selector=None): # type: (Optional[str], Optional[str]) -> List[BoundNetwork] @@ -359,7 +364,7 @@ def get_actions_list( BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] - return add_meta_to_result(actions, response, "actions") + return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions(self, network, status=None, sort=None): # type: (Network, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] diff --git a/hcloud/placement_groups/client.py b/hcloud/placement_groups/client.py index ca78bc82..5a97c7c5 100644 --- a/hcloud/placement_groups/client.py +++ b/hcloud/placement_groups/client.py @@ -1,7 +1,10 @@ from __future__ import annotations +from typing import NamedTuple + from ..actions.client import BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import Meta from .domain import CreatePlacementGroupResponse, PlacementGroup @@ -29,9 +32,12 @@ def delete(self): return self._client.delete(self) -class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "placement_groups" +class PlacementGroupsPageResult(NamedTuple): + placement_groups: list[BoundPlacementGroup] + meta: Meta | None + +class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundPlacementGroup """Returns a specific Placement Group object @@ -92,7 +98,7 @@ def get_list( for placement_group_data in response["placement_groups"] ] - return self._add_meta_to_result(placement_groups, response) + return PlacementGroupsPageResult(placement_groups, Meta.parse_meta(response)) def get_all(self, label_selector=None, name=None, sort=None): # type: (Optional[str], Optional[str], Optional[List[str]]) -> List[BoundPlacementGroup] diff --git a/hcloud/primary_ips/client.py b/hcloud/primary_ips/client.py index d33f0f20..717406a5 100644 --- a/hcloud/primary_ips/client.py +++ b/hcloud/primary_ips/client.py @@ -1,7 +1,10 @@ from __future__ import annotations +from typing import NamedTuple + from ..actions.client import BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import Meta from .domain import CreatePrimaryIPResponse, PrimaryIP @@ -84,9 +87,12 @@ def change_dns_ptr(self, ip, dns_ptr): return self._client.change_dns_ptr(self, ip, dns_ptr) -class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "primary_ips" +class PrimaryIPsPageResult(NamedTuple): + primary_ips: list[BoundPrimaryIP] + meta: Meta | None + +class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundPrimaryIP """Returns a specific Primary IP object. @@ -139,7 +145,7 @@ def get_list( for primary_ip_data in response["primary_ips"] ] - return self._add_meta_to_result(primary_ips, response) + return PrimaryIPsPageResult(primary_ips, Meta.parse_meta(response)) def get_all(self, label_selector=None, name=None): # type: (Optional[str], Optional[str]) -> List[BoundPrimaryIP] diff --git a/hcloud/server_types/client.py b/hcloud/server_types/client.py index 70e29e69..ed90dbb6 100644 --- a/hcloud/server_types/client.py +++ b/hcloud/server_types/client.py @@ -1,6 +1,9 @@ from __future__ import annotations +from typing import NamedTuple + from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import Meta from .domain import ServerType @@ -8,9 +11,12 @@ class BoundServerType(BoundModelBase): model = ServerType -class ServerTypesClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "server_types" +class ServerTypesPageResult(NamedTuple): + server_types: list[BoundServerType] + meta: Meta | None + +class ServerTypesClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundServerType """Returns a specific Server Type. @@ -48,7 +54,7 @@ def get_list(self, name=None, page=None, per_page=None): BoundServerType(self, server_type_data) for server_type_data in response["server_types"] ] - return self._add_meta_to_result(server_types, response) + return ServerTypesPageResult(server_types, Meta.parse_meta(response)) def get_all(self, name=None): # type: (Optional[str]) -> List[BoundServerType] diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index 99fe53b6..b9f7455e 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -1,8 +1,10 @@ from __future__ import annotations -from ..actions.client import BoundAction +from typing import NamedTuple + +from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import add_meta_to_result +from ..core.domain import Meta from ..datacenters.client import BoundDatacenter from ..firewalls.client import BoundFirewall from ..floating_ips.client import BoundFloatingIP @@ -409,9 +411,12 @@ def remove_from_placement_group(self): return self._client.remove_from_placement_group(self) -class ServersClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "servers" +class ServersPageResult(NamedTuple): + servers: list[BoundServer] + meta: Meta | None + +class ServersClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundServer """Get a specific server @@ -462,7 +467,7 @@ def get_list( ass_servers = [ BoundServer(self, server_data) for server_data in response["servers"] ] - return self._add_meta_to_result(ass_servers, response) + return ServersPageResult(ass_servers, Meta.parse_meta(response)) def get_all(self, name=None, label_selector=None, status=None): # type: (Optional[str], Optional[str], Optional[List[str]]) -> List[BoundServer] @@ -625,7 +630,7 @@ def get_actions_list( BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] - return add_meta_to_result(actions, response, "actions") + return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions(self, server, status=None, sort=None): # type: (Server, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] diff --git a/hcloud/ssh_keys/client.py b/hcloud/ssh_keys/client.py index 66c13fa5..0c89d6b6 100644 --- a/hcloud/ssh_keys/client.py +++ b/hcloud/ssh_keys/client.py @@ -1,6 +1,9 @@ from __future__ import annotations +from typing import NamedTuple + from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin +from ..core.domain import Meta from .domain import SSHKey @@ -27,9 +30,12 @@ def delete(self): return self._client.delete(self) -class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "ssh_keys" +class SSHKeysPageResult(NamedTuple): + ssh_keys: list[BoundSSHKey] + meta: Meta | None + +class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> BoundSSHKey """Get a specific SSH Key by its ID @@ -77,10 +83,10 @@ def get_list( response = self._client.request(url="/ssh_keys", method="GET", params=params) - ass_ssh_keys = [ + ssh_keys = [ BoundSSHKey(self, server_data) for server_data in response["ssh_keys"] ] - return self._add_meta_to_result(ass_ssh_keys, response) + return SSHKeysPageResult(ssh_keys, Meta.parse_meta(response)) def get_all(self, name=None, fingerprint=None, label_selector=None): # type: (Optional[str], Optional[str], Optional[str]) -> List[BoundSSHKey] diff --git a/hcloud/volumes/client.py b/hcloud/volumes/client.py index bd9dff55..687f145d 100644 --- a/hcloud/volumes/client.py +++ b/hcloud/volumes/client.py @@ -1,8 +1,10 @@ from __future__ import annotations -from ..actions.client import BoundAction +from typing import NamedTuple + +from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import add_meta_to_result +from ..core.domain import Meta from ..locations.client import BoundLocation from .domain import CreateVolumeResponse, Volume @@ -111,9 +113,12 @@ def change_protection(self, delete=None): return self._client.change_protection(self, delete) -class VolumesClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "volumes" +class VolumesPageResult(NamedTuple): + volumes: list[BoundVolume] + meta: Meta | None + +class VolumesClient(ClientEntityBase, GetEntityByNameMixin): def get_by_id(self, id): # type: (int) -> volumes.client.BoundVolume """Get a specific volume by its id @@ -158,7 +163,7 @@ def get_list( volumes = [ BoundVolume(self, volume_data) for volume_data in response["volumes"] ] - return self._add_meta_to_result(volumes, response) + return VolumesPageResult(volumes, Meta.parse_meta(response)) def get_all(self, label_selector=None, status=None): # type: (Optional[str], Optional[List[str]]) -> List[BoundVolume] @@ -277,7 +282,7 @@ def get_actions_list( BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] - return add_meta_to_result(actions, response, "actions") + return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions(self, volume, status=None, sort=None): # type: (Union[Volume, BoundVolume], Optional[List[str]], Optional[List[str]]) -> List[BoundAction] diff --git a/tests/unit/core/test_client.py b/tests/unit/core/test_client.py index 80bab5e3..6ea2cd63 100644 --- a/tests/unit/core/test_client.py +++ b/tests/unit/core/test_client.py @@ -1,11 +1,13 @@ from __future__ import annotations +from typing import Any, NamedTuple from unittest import mock import pytest +from hcloud.actions.client import ActionsPageResult from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.core.domain import BaseDomain, add_meta_to_result +from hcloud.core.domain import BaseDomain, Meta class TestBoundModelBase: @@ -84,15 +86,17 @@ class TestClientEntityBase: @pytest.fixture() def client_class_constructor(self): def constructor(json_content_function): - class CandiesClient(ClientEntityBase): - results_list_attribute_name = "candies" + class CandiesPageResult(NamedTuple): + candies: list[Any] + meta: Meta + class CandiesClient(ClientEntityBase): def get_list(self, status, page=None, per_page=None): json_content = json_content_function(page) results = [ (r, page, status, per_page) for r in json_content["candies"] ] - return self._add_meta_to_result(results, json_content) + return CandiesPageResult(results, Meta.parse_meta(json_content)) return CandiesClient(mock.MagicMock()) @@ -107,7 +111,7 @@ def get_actions_list(self, status, page=None, per_page=None): results = [ (r, page, status, per_page) for r in json_content["actions"] ] - return add_meta_to_result(results, json_content, "actions") + return ActionsPageResult(results, Meta.parse_meta(json_content)) return CandiesClient(mock.MagicMock()) @@ -205,44 +209,20 @@ def json_content_function(p): (23, 3, "sweet", 50), ] - def test_raise_exception_if_list_attribute_is_not_implemented( - self, client_class_with_actions_constructor - ): - def json_content_function(p): - return { - "actions": [10 + p, 20 + p], - "meta": { - "pagination": { - "page": p, - "per_page": 11, - "next_page": p + 1 if p < 3 else None, - } - }, - } - - candies_client = client_class_with_actions_constructor(json_content_function) - - with pytest.raises(NotImplementedError) as exception_info: - candies_client.get_all() - - error = exception_info.value - assert ( - str(error) - == "in order to get results list, 'results_list_attribute_name' attribute of CandiesClient has to be specified" - ) - class TestGetEntityByNameMixin: @pytest.fixture() def client_class_constructor(self): def constructor(json_content_function): - class CandiesClient(ClientEntityBase, GetEntityByNameMixin): - results_list_attribute_name = "candies" + class CandiesPageResult(NamedTuple): + candies: list[Any] + meta: Meta + class CandiesClient(ClientEntityBase, GetEntityByNameMixin): def get_list(self, name, page=None, per_page=None): json_content = json_content_function(page) results = json_content["candies"] - return self._add_meta_to_result(results, json_content) + return CandiesPageResult(results, Meta.parse_meta(json_content)) return CandiesClient(mock.MagicMock()) diff --git a/tests/unit/core/test_domain.py b/tests/unit/core/test_domain.py index e9e06a35..97cb6f74 100644 --- a/tests/unit/core/test_domain.py +++ b/tests/unit/core/test_domain.py @@ -3,13 +3,7 @@ import pytest from dateutil.parser import isoparse -from hcloud.core.domain import ( - BaseDomain, - DomainIdentityMixin, - Meta, - Pagination, - add_meta_to_result, -) +from hcloud.core.domain import BaseDomain, DomainIdentityMixin, Meta, Pagination class TestMeta: @@ -46,27 +40,6 @@ def test_parse_meta_json_ok(self): assert result.pagination.last_page == 10 assert result.pagination.total_entries == 100 - def test_add_meta_to_result(self): - json_content = { - "meta": { - "pagination": { - "page": 2, - "per_page": 10, - "previous_page": 1, - "next_page": 3, - "last_page": 10, - "total_entries": 100, - } - } - } - result = add_meta_to_result([1, 2, 3], json_content, "id_list") - assert result.id_list == [1, 2, 3] - assert result.meta.pagination.page == 2 - assert result.meta.pagination.per_page == 10 - assert result.meta.pagination.next_page == 3 - assert result.meta.pagination.last_page == 10 - assert result.meta.pagination.total_entries == 100 - class SomeDomain(BaseDomain, DomainIdentityMixin): __slots__ = ("id", "name") From 66f9163cf44663dfdcac7cc4add3850e4a0d8b45 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 21 Jul 2023 17:52:49 +0200 Subject: [PATCH 069/406] refactor: add Clients type hints (#253) --- hcloud/actions/client.py | 9 ++++++++- hcloud/certificates/client.py | 9 ++++++++- hcloud/datacenters/client.py | 9 ++++++++- hcloud/firewalls/client.py | 9 ++++++++- hcloud/floating_ips/client.py | 9 ++++++++- hcloud/images/client.py | 9 ++++++++- hcloud/isos/client.py | 9 ++++++++- hcloud/load_balancer_types/client.py | 9 ++++++++- hcloud/load_balancers/client.py | 9 ++++++++- hcloud/locations/client.py | 9 ++++++++- hcloud/networks/client.py | 9 ++++++++- hcloud/placement_groups/client.py | 9 ++++++++- hcloud/primary_ips/client.py | 9 ++++++++- hcloud/server_types/client.py | 9 ++++++++- hcloud/servers/client.py | 9 ++++++++- hcloud/ssh_keys/client.py | 9 ++++++++- hcloud/volumes/client.py | 9 ++++++++- 17 files changed, 136 insertions(+), 17 deletions(-) diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index ef262384..695fb2c7 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -1,14 +1,19 @@ from __future__ import annotations import time -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from ..core.client import BoundModelBase, ClientEntityBase from ..core.domain import 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): @@ -37,6 +42,8 @@ class ActionsPageResult(NamedTuple): class ActionsClient(ClientEntityBase): + _client: Client + def get_by_id(self, id): # type: (int) -> BoundAction """Get a specific action by its ID. diff --git a/hcloud/certificates/client.py b/hcloud/certificates/client.py index 2f60bd1e..b9a93f86 100644 --- a/hcloud/certificates/client.py +++ b/hcloud/certificates/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin @@ -12,8 +12,13 @@ ManagedCertificateStatus, ) +if TYPE_CHECKING: + from .._client import Client + class BoundCertificate(BoundModelBase): + _client: CertificatesClient + model = Certificate def __init__(self, client, data, complete=True): @@ -91,6 +96,8 @@ class CertificatesPageResult(NamedTuple): class CertificatesClient(ClientEntityBase, GetEntityByNameMixin): + _client: Client + def get_by_id(self, id): # type: (int) -> BoundCertificate """Get a specific certificate by its ID. diff --git a/hcloud/datacenters/client.py b/hcloud/datacenters/client.py index 76e3c707..0e598d5d 100644 --- a/hcloud/datacenters/client.py +++ b/hcloud/datacenters/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from ..core.domain import Meta @@ -8,8 +8,13 @@ from ..server_types.client 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): @@ -52,6 +57,8 @@ class DatacentersPageResult(NamedTuple): class DatacentersClient(ClientEntityBase, GetEntityByNameMixin): + _client: Client + def get_by_id(self, id): # type: (int) -> BoundDatacenter """Get a specific datacenter by its ID. diff --git a/hcloud/firewalls/client.py b/hcloud/firewalls/client.py index 118fc850..ab4cd4c8 100644 --- a/hcloud/firewalls/client.py +++ b/hcloud/firewalls/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin @@ -13,8 +13,13 @@ FirewallRule, ) +if TYPE_CHECKING: + from .._client import Client + class BoundFirewall(BoundModelBase): + _client: FirewallsClient + model = Firewall def __init__(self, client, data, complete=True): @@ -142,6 +147,8 @@ class FirewallsPageResult(NamedTuple): class FirewallsClient(ClientEntityBase, GetEntityByNameMixin): + _client: Client + def get_actions_list( self, firewall, # type: Firewall diff --git a/hcloud/floating_ips/client.py b/hcloud/floating_ips/client.py index f6d56113..3c8b49c9 100644 --- a/hcloud/floating_ips/client.py +++ b/hcloud/floating_ips/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin @@ -8,8 +8,13 @@ from ..locations.client import BoundLocation from .domain import CreateFloatingIPResponse, FloatingIP +if TYPE_CHECKING: + from .._client import Client + class BoundFloatingIP(BoundModelBase): + _client: FloatingIPsClient + model = FloatingIP def __init__(self, client, data, complete=True): @@ -127,6 +132,8 @@ class FloatingIPsPageResult(NamedTuple): class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin): + _client: Client + def get_actions_list( self, floating_ip, # type: FloatingIP diff --git a/hcloud/images/client.py b/hcloud/images/client.py index 55729a0f..aed78284 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -1,14 +1,19 @@ from __future__ import annotations -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from ..core.domain import Meta from .domain import Image +if TYPE_CHECKING: + from .._client import Client + class BoundImage(BoundModelBase): + _client: ImagesClient + model = Image def __init__(self, client, data): @@ -97,6 +102,8 @@ class ImagesPageResult(NamedTuple): class ImagesClient(ClientEntityBase, GetEntityByNameMixin): + _client: Client + def get_actions_list( self, image, # type: Image diff --git a/hcloud/isos/client.py b/hcloud/isos/client.py index fba3930e..38408fdb 100644 --- a/hcloud/isos/client.py +++ b/hcloud/isos/client.py @@ -1,14 +1,19 @@ from __future__ import annotations -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from warnings import warn from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from ..core.domain import Meta from .domain import Iso +if TYPE_CHECKING: + from .._client import Client + class BoundIso(BoundModelBase): + _client: IsosClient + model = Iso @@ -18,6 +23,8 @@ class IsosPageResult(NamedTuple): class IsosClient(ClientEntityBase, GetEntityByNameMixin): + _client: Client + def get_by_id(self, id): # type: (int) -> BoundIso """Get a specific ISO by its id diff --git a/hcloud/load_balancer_types/client.py b/hcloud/load_balancer_types/client.py index 7dff45a3..50d8681a 100644 --- a/hcloud/load_balancer_types/client.py +++ b/hcloud/load_balancer_types/client.py @@ -1,13 +1,18 @@ from __future__ import annotations -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from ..core.domain import Meta from .domain import LoadBalancerType +if TYPE_CHECKING: + from .._client import Client + class BoundLoadBalancerType(BoundModelBase): + _client: LoadBalancerTypesClient + model = LoadBalancerType @@ -17,6 +22,8 @@ class LoadBalancerTypesPageResult(NamedTuple): class LoadBalancerTypesClient(ClientEntityBase, GetEntityByNameMixin): + _client: Client + def get_by_id(self, id): # type: (int) -> load_balancer_types.client.BoundLoadBalancerType """Returns a specific Load Balancer Type. diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index 85e73d82..53774609 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from ..actions.client import ActionsPageResult, BoundAction from ..certificates.client import BoundCertificate @@ -27,8 +27,13 @@ PublicNetwork, ) +if TYPE_CHECKING: + from .._client import Client + class BoundLoadBalancer(BoundModelBase): + _client: LoadBalancersClient + model = LoadBalancer def __init__(self, client, data, complete=True): @@ -318,6 +323,8 @@ class LoadBalancersPageResult(NamedTuple): class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin): + _client: Client + def get_by_id(self, id): # type: (int) -> BoundLoadBalancer """Get a specific Load Balancer diff --git a/hcloud/locations/client.py b/hcloud/locations/client.py index c21c6a75..9c9e217a 100644 --- a/hcloud/locations/client.py +++ b/hcloud/locations/client.py @@ -1,13 +1,18 @@ from __future__ import annotations -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from ..core.domain import Meta from .domain import Location +if TYPE_CHECKING: + from .._client import Client + class BoundLocation(BoundModelBase): + _client: LocationsClient + model = Location @@ -17,6 +22,8 @@ class LocationsPageResult(NamedTuple): class LocationsClient(ClientEntityBase, GetEntityByNameMixin): + _client: Client + def get_by_id(self, id): # type: (int) -> locations.client.BoundLocation """Get a specific location by its ID. diff --git a/hcloud/networks/client.py b/hcloud/networks/client.py index 6a93c0cd..3fa69c0d 100644 --- a/hcloud/networks/client.py +++ b/hcloud/networks/client.py @@ -1,14 +1,19 @@ from __future__ import annotations -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from ..core.domain import 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): @@ -161,6 +166,8 @@ class NetworksPageResult(NamedTuple): class NetworksClient(ClientEntityBase, GetEntityByNameMixin): + _client: Client + def get_by_id(self, id): # type: (int) -> BoundNetwork """Get a specific network diff --git a/hcloud/placement_groups/client.py b/hcloud/placement_groups/client.py index 5a97c7c5..b3bc56cb 100644 --- a/hcloud/placement_groups/client.py +++ b/hcloud/placement_groups/client.py @@ -1,14 +1,19 @@ from __future__ import annotations -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from ..actions.client import BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from ..core.domain import 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): @@ -38,6 +43,8 @@ class PlacementGroupsPageResult(NamedTuple): class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin): + _client: Client + def get_by_id(self, id): # type: (int) -> BoundPlacementGroup """Returns a specific Placement Group object diff --git a/hcloud/primary_ips/client.py b/hcloud/primary_ips/client.py index 717406a5..a7aea615 100644 --- a/hcloud/primary_ips/client.py +++ b/hcloud/primary_ips/client.py @@ -1,14 +1,19 @@ from __future__ import annotations -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from ..actions.client import BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from ..core.domain import Meta from .domain import CreatePrimaryIPResponse, PrimaryIP +if TYPE_CHECKING: + from .._client import Client + class BoundPrimaryIP(BoundModelBase): + _client: PrimaryIPsClient + model = PrimaryIP def __init__(self, client, data, complete=True): @@ -93,6 +98,8 @@ class PrimaryIPsPageResult(NamedTuple): class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin): + _client: Client + def get_by_id(self, id): # type: (int) -> BoundPrimaryIP """Returns a specific Primary IP object. diff --git a/hcloud/server_types/client.py b/hcloud/server_types/client.py index ed90dbb6..b7fdae97 100644 --- a/hcloud/server_types/client.py +++ b/hcloud/server_types/client.py @@ -1,13 +1,18 @@ from __future__ import annotations -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from ..core.domain import Meta from .domain import ServerType +if TYPE_CHECKING: + from .._client import Client + class BoundServerType(BoundModelBase): + _client: ServerTypesClient + model = ServerType @@ -17,6 +22,8 @@ class ServerTypesPageResult(NamedTuple): class ServerTypesClient(ClientEntityBase, GetEntityByNameMixin): + _client: Client + def get_by_id(self, id): # type: (int) -> BoundServerType """Returns a specific Server Type. diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index b9f7455e..a8fb3496 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin @@ -30,8 +30,13 @@ Server, ) +if TYPE_CHECKING: + from .._client import Client + class BoundServer(BoundModelBase): + _client: ServersClient + model = Server def __init__(self, client, data, complete=True): @@ -417,6 +422,8 @@ class ServersPageResult(NamedTuple): class ServersClient(ClientEntityBase, GetEntityByNameMixin): + _client: Client + def get_by_id(self, id): # type: (int) -> BoundServer """Get a specific server diff --git a/hcloud/ssh_keys/client.py b/hcloud/ssh_keys/client.py index 0c89d6b6..8913fe24 100644 --- a/hcloud/ssh_keys/client.py +++ b/hcloud/ssh_keys/client.py @@ -1,13 +1,18 @@ from __future__ import annotations -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin from ..core.domain import 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): @@ -36,6 +41,8 @@ class SSHKeysPageResult(NamedTuple): class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin): + _client: Client + def get_by_id(self, id): # type: (int) -> BoundSSHKey """Get a specific SSH Key by its ID diff --git a/hcloud/volumes/client.py b/hcloud/volumes/client.py index 687f145d..6a0cf27f 100644 --- a/hcloud/volumes/client.py +++ b/hcloud/volumes/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from ..actions.client import ActionsPageResult, BoundAction from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin @@ -8,8 +8,13 @@ from ..locations.client import BoundLocation from .domain import CreateVolumeResponse, Volume +if TYPE_CHECKING: + from .._client import Client + class BoundVolume(BoundModelBase): + _client: VolumesClient + model = Volume def __init__(self, client, data, complete=True): @@ -119,6 +124,8 @@ class VolumesPageResult(NamedTuple): class VolumesClient(ClientEntityBase, GetEntityByNameMixin): + _client: Client + def get_by_id(self, id): # type: (int) -> volumes.client.BoundVolume """Get a specific volume by its id From c8253f3def785dd632b8640a79fa42e1727b92fb Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 24 Jul 2023 09:52:54 +0200 Subject: [PATCH 070/406] test: reduce poll_interval during tests (#257) This speeds up the few action tests that run wait_until_finished. Reduce time from ~5.8s to ~1.7s --- tests/unit/actions/test_client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/unit/actions/test_client.py b/tests/unit/actions/test_client.py index 29f192a7..6fc6aef4 100644 --- a/tests/unit/actions/test_client.py +++ b/tests/unit/actions/test_client.py @@ -11,8 +11,12 @@ class TestBoundAction: @pytest.fixture() def bound_running_action(self, mocked_requests): + action_client = ActionsClient(client=mocked_requests) + # Speed up tests that run `wait_until_finished` + action_client._client.poll_interval = 0.1 + return BoundAction( - client=ActionsClient(client=mocked_requests), + client=action_client, data=dict(id=14, status=Action.STATUS_RUNNING), ) From 23b36079d997d28d73cb9edc9a51a8c3b4481d7e Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 24 Jul 2023 09:55:11 +0200 Subject: [PATCH 071/406] fix: invalid attribute in placement group (#258) The actions client attribute name is `actions` --- hcloud/placement_groups/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hcloud/placement_groups/client.py b/hcloud/placement_groups/client.py index b3bc56cb..f5017501 100644 --- a/hcloud/placement_groups/client.py +++ b/hcloud/placement_groups/client.py @@ -158,7 +158,7 @@ def create( 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"]), From 854c12bbde3a5f0dcc77cabe72ecab2fd72fbac0 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 24 Jul 2023 09:55:45 +0200 Subject: [PATCH 072/406] feat: reexport references in parent ressources modules (#256) * feat: reexport references in ressources module * refactor: use new module hierarchy for imports * style: run isort on the new imports * refactor: use new module hierarchy for imports in tests and docs * style: run isort on the new imports * fix: add missing reexport * refactor: use new module hierarchy imports in client --- README.md | 4 +-- examples/create_server.py | 4 +-- examples/usage_oop.py | 4 +-- examples/usage_procedurale.py | 8 ++--- hcloud/_client.py | 34 +++++++++--------- hcloud/actions/__init__.py | 9 +++++ hcloud/actions/client.py | 3 +- hcloud/actions/domain.py | 2 +- hcloud/certificates/__init__.py | 13 +++++++ hcloud/certificates/client.py | 5 ++- hcloud/certificates/domain.py | 2 +- hcloud/core/__init__.py | 4 +++ hcloud/datacenters/__init__.py | 8 +++++ hcloud/datacenters/client.py | 7 ++-- hcloud/datacenters/domain.py | 2 +- hcloud/deprecation/__init__.py | 3 ++ hcloud/deprecation/domain.py | 2 +- hcloud/firewalls/__init__.py | 10 ++++++ hcloud/firewalls/client.py | 7 ++-- hcloud/firewalls/domain.py | 2 +- hcloud/floating_ips/__init__.py | 8 +++++ hcloud/floating_ips/client.py | 9 +++-- hcloud/floating_ips/domain.py | 2 +- hcloud/helpers/__init__.py | 3 ++ hcloud/images/__init__.py | 4 +++ hcloud/images/client.py | 7 ++-- hcloud/images/domain.py | 2 +- hcloud/isos/__init__.py | 4 +++ hcloud/isos/client.py | 3 +- hcloud/isos/domain.py | 2 +- hcloud/load_balancer_types/__init__.py | 8 +++++ hcloud/load_balancer_types/client.py | 3 +- hcloud/load_balancer_types/domain.py | 2 +- hcloud/load_balancers/__init__.py | 23 ++++++++++++ hcloud/load_balancers/client.py | 15 ++++---- hcloud/load_balancers/domain.py | 2 +- hcloud/locations/__init__.py | 4 +++ hcloud/locations/client.py | 3 +- hcloud/locations/domain.py | 2 +- hcloud/networks/__init__.py | 9 +++++ hcloud/networks/client.py | 7 ++-- hcloud/networks/domain.py | 2 +- hcloud/placement_groups/__init__.py | 8 +++++ hcloud/placement_groups/client.py | 5 ++- hcloud/placement_groups/domain.py | 2 +- hcloud/primary_ips/__init__.py | 4 +++ hcloud/primary_ips/client.py | 7 ++-- hcloud/primary_ips/domain.py | 2 +- hcloud/server_types/__init__.py | 8 +++++ hcloud/server_types/client.py | 3 +- hcloud/server_types/domain.py | 4 +-- hcloud/servers/__init__.py | 16 +++++++++ hcloud/servers/client.py | 28 +++++++-------- hcloud/servers/domain.py | 2 +- hcloud/ssh_keys/__init__.py | 4 +++ hcloud/ssh_keys/client.py | 3 +- hcloud/ssh_keys/domain.py | 2 +- hcloud/volumes/__init__.py | 4 +++ hcloud/volumes/client.py | 9 +++-- hcloud/volumes/domain.py | 2 +- tests/unit/actions/test_client.py | 9 +++-- tests/unit/actions/test_domain.py | 2 +- tests/unit/certificates/test_client.py | 10 ++++-- tests/unit/certificates/test_domain.py | 2 +- tests/unit/core/test_client.py | 11 ++++-- tests/unit/core/test_domain.py | 2 +- tests/unit/datacenters/test_client.py | 5 ++- tests/unit/firewalls/test_client.py | 9 ++--- tests/unit/firewalls/test_domain.py | 2 +- tests/unit/floating_ips/test_client.py | 11 +++--- tests/unit/floating_ips/test_domain.py | 2 +- tests/unit/images/test_client.py | 7 ++-- tests/unit/images/test_domain.py | 2 +- tests/unit/isos/test_client.py | 2 +- tests/unit/isos/test_domain.py | 2 +- tests/unit/load_balancer_types/test_client.py | 2 +- tests/unit/load_balancers/test_client.py | 15 ++++---- tests/unit/load_balancers/test_domain.py | 2 +- tests/unit/locations/test_client.py | 2 +- tests/unit/networks/test_client.py | 13 ++++--- tests/unit/networks/test_domain.py | 2 +- tests/unit/placement_groups/test_client.py | 2 +- tests/unit/placement_groups/test_domain.py | 2 +- tests/unit/primary_ips/test_client.py | 6 ++-- tests/unit/primary_ips/test_domain.py | 2 +- tests/unit/server_types/test_client.py | 2 +- tests/unit/servers/test_client.py | 35 ++++++++----------- tests/unit/servers/test_domain.py | 2 +- tests/unit/ssh_keys/test_client.py | 3 +- tests/unit/ssh_keys/test_domain.py | 2 +- tests/unit/volumes/test_client.py | 11 +++--- tests/unit/volumes/test_domain.py | 2 +- 92 files changed, 346 insertions(+), 207 deletions(-) diff --git a/README.md b/README.md index bccec255..88cba5f7 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ Here is an example that creates a server and list them: ```python from hcloud import Client -from hcloud.images.domain import Image -from hcloud.server_types.domain import ServerType +from hcloud.images import Image +from hcloud.server_types import ServerType client = Client(token="{YOUR_API_TOKEN}") # Please paste your API token here diff --git a/examples/create_server.py b/examples/create_server.py index 46a724df..348939a2 100644 --- a/examples/create_server.py +++ b/examples/create_server.py @@ -1,8 +1,8 @@ from __future__ import annotations from hcloud import Client -from hcloud.images.domain import Image -from hcloud.server_types.domain import ServerType +from hcloud.images import Image +from hcloud.server_types import ServerType # Please paste your API token here between the quotes client = Client(token="{YOUR_API_TOKEN}") diff --git a/examples/usage_oop.py b/examples/usage_oop.py index 0d453dec..b6fcddcd 100644 --- a/examples/usage_oop.py +++ b/examples/usage_oop.py @@ -1,8 +1,8 @@ from __future__ import annotations from hcloud import Client -from hcloud.images.domain import Image -from hcloud.server_types.domain import ServerType +from hcloud.images import Image +from hcloud.server_types import ServerType # Create a client client = Client(token="project-token") diff --git a/examples/usage_procedurale.py b/examples/usage_procedurale.py index 94e3c30f..6c4aa962 100644 --- a/examples/usage_procedurale.py +++ b/examples/usage_procedurale.py @@ -1,10 +1,10 @@ from __future__ import annotations from hcloud import Client -from hcloud.images.domain import Image -from hcloud.server_types.domain import ServerType -from hcloud.servers.domain import Server -from hcloud.volumes.domain import Volume +from hcloud.images import Image +from hcloud.server_types import ServerType +from hcloud.servers import Server +from hcloud.volumes import Volume client = Client(token="project-token") diff --git a/hcloud/_client.py b/hcloud/_client.py index 0f37fb1c..c3aa3b33 100644 --- a/hcloud/_client.py +++ b/hcloud/_client.py @@ -6,23 +6,23 @@ 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: diff --git a/hcloud/actions/__init__.py b/hcloud/actions/__init__.py index e69de29b..66bb7b4d 100644 --- a/hcloud/actions/__init__.py +++ b/hcloud/actions/__init__.py @@ -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, +) diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index 695fb2c7..6a632761 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -3,8 +3,7 @@ import time from typing import TYPE_CHECKING, NamedTuple -from ..core.client import BoundModelBase, ClientEntityBase -from ..core.domain import Meta +from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import Action, ActionFailedException, ActionTimeoutException if TYPE_CHECKING: diff --git a/hcloud/actions/domain.py b/hcloud/actions/domain.py index 140fcad9..7663bf1b 100644 --- a/hcloud/actions/domain.py +++ b/hcloud/actions/domain.py @@ -3,7 +3,7 @@ from dateutil.parser import isoparse from .._exceptions import HCloudException -from ..core.domain import BaseDomain +from ..core import BaseDomain class Action(BaseDomain): diff --git a/hcloud/certificates/__init__.py b/hcloud/certificates/__init__.py index e69de29b..4e63df57 100644 --- a/hcloud/certificates/__init__.py +++ b/hcloud/certificates/__init__.py @@ -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, +) diff --git a/hcloud/certificates/client.py b/hcloud/certificates/client.py index b9a93f86..88201f78 100644 --- a/hcloud/certificates/client.py +++ b/hcloud/certificates/client.py @@ -2,9 +2,8 @@ from typing import TYPE_CHECKING, NamedTuple -from ..actions.client import ActionsPageResult, BoundAction -from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import Meta +from ..actions import ActionsPageResult, BoundAction +from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta from .domain import ( Certificate, CreateManagedCertificateResponse, diff --git a/hcloud/certificates/domain.py b/hcloud/certificates/domain.py index 04f83ffd..c07db1a9 100644 --- a/hcloud/certificates/domain.py +++ b/hcloud/certificates/domain.py @@ -2,7 +2,7 @@ from dateutil.parser import isoparse -from ..core.domain import BaseDomain, DomainIdentityMixin +from ..core import BaseDomain, DomainIdentityMixin class Certificate(BaseDomain, DomainIdentityMixin): diff --git a/hcloud/core/__init__.py b/hcloud/core/__init__.py index e69de29b..2e279f75 100644 --- a/hcloud/core/__init__.py +++ b/hcloud/core/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from .client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin # noqa: F401 +from .domain import BaseDomain, DomainIdentityMixin, Meta, Pagination # noqa: F401 diff --git a/hcloud/datacenters/__init__.py b/hcloud/datacenters/__init__.py index e69de29b..559694c0 100644 --- a/hcloud/datacenters/__init__.py +++ b/hcloud/datacenters/__init__.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .client import ( # noqa: F401 + BoundDatacenter, + DatacentersClient, + DatacentersPageResult, +) +from .domain import Datacenter, DatacenterServerTypes # noqa: F401 diff --git a/hcloud/datacenters/client.py b/hcloud/datacenters/client.py index 0e598d5d..2292148a 100644 --- a/hcloud/datacenters/client.py +++ b/hcloud/datacenters/client.py @@ -2,10 +2,9 @@ from typing import TYPE_CHECKING, NamedTuple -from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import Meta -from ..locations.client import BoundLocation -from ..server_types.client import BoundServerType +from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..locations import BoundLocation +from ..server_types import BoundServerType from .domain import Datacenter, DatacenterServerTypes if TYPE_CHECKING: diff --git a/hcloud/datacenters/domain.py b/hcloud/datacenters/domain.py index 71df150a..e934760f 100644 --- a/hcloud/datacenters/domain.py +++ b/hcloud/datacenters/domain.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ..core.domain import BaseDomain, DomainIdentityMixin +from ..core import BaseDomain, DomainIdentityMixin class Datacenter(BaseDomain, DomainIdentityMixin): diff --git a/hcloud/deprecation/__init__.py b/hcloud/deprecation/__init__.py index e69de29b..315576b1 100644 --- a/hcloud/deprecation/__init__.py +++ b/hcloud/deprecation/__init__.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +from .domain import DeprecationInfo # noqa: F401 diff --git a/hcloud/deprecation/domain.py b/hcloud/deprecation/domain.py index 1137a080..40d446ec 100644 --- a/hcloud/deprecation/domain.py +++ b/hcloud/deprecation/domain.py @@ -2,7 +2,7 @@ from dateutil.parser import isoparse -from ..core.domain import BaseDomain +from ..core import BaseDomain class DeprecationInfo(BaseDomain): diff --git a/hcloud/firewalls/__init__.py b/hcloud/firewalls/__init__.py index e69de29b..42bde369 100644 --- a/hcloud/firewalls/__init__.py +++ b/hcloud/firewalls/__init__.py @@ -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, +) diff --git a/hcloud/firewalls/client.py b/hcloud/firewalls/client.py index ab4cd4c8..61c6b539 100644 --- a/hcloud/firewalls/client.py +++ b/hcloud/firewalls/client.py @@ -2,9 +2,8 @@ from typing import TYPE_CHECKING, NamedTuple -from ..actions.client import ActionsPageResult, BoundAction -from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import Meta +from ..actions import ActionsPageResult, BoundAction +from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta from .domain import ( CreateFirewallResponse, Firewall, @@ -40,7 +39,7 @@ def __init__(self, client, data, complete=True): applied_to = data.get("applied_to", []) if applied_to: - from ..servers.client import BoundServer + from ..servers import BoundServer ats = [] for a in applied_to: diff --git a/hcloud/firewalls/domain.py b/hcloud/firewalls/domain.py index 3a4f0e33..9bddfed9 100644 --- a/hcloud/firewalls/domain.py +++ b/hcloud/firewalls/domain.py @@ -2,7 +2,7 @@ from dateutil.parser import isoparse -from ..core.domain import BaseDomain +from ..core import BaseDomain class Firewall(BaseDomain): diff --git a/hcloud/floating_ips/__init__.py b/hcloud/floating_ips/__init__.py index e69de29b..4e55bf5f 100644 --- a/hcloud/floating_ips/__init__.py +++ b/hcloud/floating_ips/__init__.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .client import ( # noqa: F401 + BoundFloatingIP, + FloatingIPsClient, + FloatingIPsPageResult, +) +from .domain import CreateFloatingIPResponse, FloatingIP # noqa: F401 diff --git a/hcloud/floating_ips/client.py b/hcloud/floating_ips/client.py index 3c8b49c9..a4a1bf8e 100644 --- a/hcloud/floating_ips/client.py +++ b/hcloud/floating_ips/client.py @@ -2,10 +2,9 @@ from typing import TYPE_CHECKING, NamedTuple -from ..actions.client import ActionsPageResult, BoundAction -from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import Meta -from ..locations.client import BoundLocation +from ..actions import ActionsPageResult, BoundAction +from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..locations import BoundLocation from .domain import CreateFloatingIPResponse, FloatingIP if TYPE_CHECKING: @@ -18,7 +17,7 @@ class BoundFloatingIP(BoundModelBase): model = FloatingIP def __init__(self, client, data, complete=True): - from ..servers.client import BoundServer + from ..servers import BoundServer server = data.get("server") if server is not None: diff --git a/hcloud/floating_ips/domain.py b/hcloud/floating_ips/domain.py index 84690b51..39e1fe1a 100644 --- a/hcloud/floating_ips/domain.py +++ b/hcloud/floating_ips/domain.py @@ -2,7 +2,7 @@ from dateutil.parser import isoparse -from ..core.domain import BaseDomain +from ..core import BaseDomain class FloatingIP(BaseDomain): diff --git a/hcloud/helpers/__init__.py b/hcloud/helpers/__init__.py index e69de29b..b6a4cd73 100644 --- a/hcloud/helpers/__init__.py +++ b/hcloud/helpers/__init__.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +from .labels import LabelValidator # noqa: F401 diff --git a/hcloud/images/__init__.py b/hcloud/images/__init__.py index e69de29b..78cb6868 100644 --- a/hcloud/images/__init__.py +++ b/hcloud/images/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from .client import BoundImage, ImagesClient, ImagesPageResult # noqa: F401 +from .domain import CreateImageResponse, Image # noqa: F401 diff --git a/hcloud/images/client.py b/hcloud/images/client.py index aed78284..f1e2a898 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -2,9 +2,8 @@ from typing import TYPE_CHECKING, NamedTuple -from ..actions.client import ActionsPageResult, BoundAction -from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import Meta +from ..actions import ActionsPageResult, BoundAction +from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta from .domain import Image if TYPE_CHECKING: @@ -17,7 +16,7 @@ class BoundImage(BoundModelBase): model = Image def __init__(self, client, data): - from ..servers.client import BoundServer + from ..servers import BoundServer created_from = data.get("created_from") if created_from is not None: diff --git a/hcloud/images/domain.py b/hcloud/images/domain.py index b445f0bb..2d47077d 100644 --- a/hcloud/images/domain.py +++ b/hcloud/images/domain.py @@ -2,7 +2,7 @@ from dateutil.parser import isoparse -from ..core.domain import BaseDomain, DomainIdentityMixin +from ..core import BaseDomain, DomainIdentityMixin class Image(BaseDomain, DomainIdentityMixin): diff --git a/hcloud/isos/__init__.py b/hcloud/isos/__init__.py index e69de29b..0d5e38fa 100644 --- a/hcloud/isos/__init__.py +++ b/hcloud/isos/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from .client import BoundIso, IsosClient, IsosPageResult # noqa: F401 +from .domain import Iso # noqa: F401 diff --git a/hcloud/isos/client.py b/hcloud/isos/client.py index 38408fdb..8c453d0c 100644 --- a/hcloud/isos/client.py +++ b/hcloud/isos/client.py @@ -3,8 +3,7 @@ from typing import TYPE_CHECKING, NamedTuple from warnings import warn -from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import Meta +from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta from .domain import Iso if TYPE_CHECKING: diff --git a/hcloud/isos/domain.py b/hcloud/isos/domain.py index 647c4ab5..daba7c38 100644 --- a/hcloud/isos/domain.py +++ b/hcloud/isos/domain.py @@ -2,7 +2,7 @@ from dateutil.parser import isoparse -from ..core.domain import BaseDomain, DomainIdentityMixin +from ..core import BaseDomain, DomainIdentityMixin class Iso(BaseDomain, DomainIdentityMixin): diff --git a/hcloud/load_balancer_types/__init__.py b/hcloud/load_balancer_types/__init__.py index e69de29b..fa1dc33c 100644 --- a/hcloud/load_balancer_types/__init__.py +++ b/hcloud/load_balancer_types/__init__.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .client import ( # noqa: F401 + BoundLoadBalancerType, + LoadBalancerTypesClient, + LoadBalancerTypesPageResult, +) +from .domain import LoadBalancerType # noqa: F401 diff --git a/hcloud/load_balancer_types/client.py b/hcloud/load_balancer_types/client.py index 50d8681a..4c584f20 100644 --- a/hcloud/load_balancer_types/client.py +++ b/hcloud/load_balancer_types/client.py @@ -2,8 +2,7 @@ from typing import TYPE_CHECKING, NamedTuple -from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import Meta +from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta from .domain import LoadBalancerType if TYPE_CHECKING: diff --git a/hcloud/load_balancer_types/domain.py b/hcloud/load_balancer_types/domain.py index 799b10f2..8fa56b2c 100644 --- a/hcloud/load_balancer_types/domain.py +++ b/hcloud/load_balancer_types/domain.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ..core.domain import BaseDomain, DomainIdentityMixin +from ..core import BaseDomain, DomainIdentityMixin class LoadBalancerType(BaseDomain, DomainIdentityMixin): diff --git a/hcloud/load_balancers/__init__.py b/hcloud/load_balancers/__init__.py index e69de29b..76106a55 100644 --- a/hcloud/load_balancers/__init__.py +++ b/hcloud/load_balancers/__init__.py @@ -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, +) diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index 53774609..f03df7e9 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -2,14 +2,13 @@ from typing import TYPE_CHECKING, NamedTuple -from ..actions.client import ActionsPageResult, BoundAction -from ..certificates.client import BoundCertificate -from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import Meta -from ..load_balancer_types.client import BoundLoadBalancerType -from ..locations.client import BoundLocation -from ..networks.client import BoundNetwork -from ..servers.client import BoundServer +from ..actions import ActionsPageResult, BoundAction +from ..certificates import BoundCertificate +from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..load_balancer_types import BoundLoadBalancerType +from ..locations import BoundLocation +from ..networks import BoundNetwork +from ..servers import BoundServer from .domain import ( CreateLoadBalancerResponse, IPv4Address, diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index 3a7b9815..d5f44644 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -2,7 +2,7 @@ from dateutil.parser import isoparse -from ..core.domain import BaseDomain +from ..core import BaseDomain class LoadBalancer(BaseDomain): diff --git a/hcloud/locations/__init__.py b/hcloud/locations/__init__.py index e69de29b..1c23a517 100644 --- a/hcloud/locations/__init__.py +++ b/hcloud/locations/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from .client import BoundLocation, LocationsClient, LocationsPageResult # noqa: F401 +from .domain import Location # noqa: F401 diff --git a/hcloud/locations/client.py b/hcloud/locations/client.py index 9c9e217a..29d49a95 100644 --- a/hcloud/locations/client.py +++ b/hcloud/locations/client.py @@ -2,8 +2,7 @@ from typing import TYPE_CHECKING, NamedTuple -from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import Meta +from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta from .domain import Location if TYPE_CHECKING: diff --git a/hcloud/locations/domain.py b/hcloud/locations/domain.py index 47fcecf3..23358dbb 100644 --- a/hcloud/locations/domain.py +++ b/hcloud/locations/domain.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ..core.domain import BaseDomain, DomainIdentityMixin +from ..core import BaseDomain, DomainIdentityMixin class Location(BaseDomain, DomainIdentityMixin): diff --git a/hcloud/networks/__init__.py b/hcloud/networks/__init__.py index e69de29b..5bf4a88f 100644 --- a/hcloud/networks/__init__.py +++ b/hcloud/networks/__init__.py @@ -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, +) diff --git a/hcloud/networks/client.py b/hcloud/networks/client.py index 3fa69c0d..7abae6e5 100644 --- a/hcloud/networks/client.py +++ b/hcloud/networks/client.py @@ -2,9 +2,8 @@ from typing import TYPE_CHECKING, NamedTuple -from ..actions.client import ActionsPageResult, BoundAction -from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import Meta +from ..actions import ActionsPageResult, BoundAction +from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta from .domain import Network, NetworkRoute, NetworkSubnet if TYPE_CHECKING: @@ -27,7 +26,7 @@ def __init__(self, client, data, complete=True): 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: diff --git a/hcloud/networks/domain.py b/hcloud/networks/domain.py index 9658a227..25874288 100644 --- a/hcloud/networks/domain.py +++ b/hcloud/networks/domain.py @@ -2,7 +2,7 @@ from dateutil.parser import isoparse -from ..core.domain import BaseDomain +from ..core import BaseDomain class Network(BaseDomain): diff --git a/hcloud/placement_groups/__init__.py b/hcloud/placement_groups/__init__.py index e69de29b..9c25dd7f 100644 --- a/hcloud/placement_groups/__init__.py +++ b/hcloud/placement_groups/__init__.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .client import ( # noqa: F401 + BoundPlacementGroup, + PlacementGroupsClient, + PlacementGroupsPageResult, +) +from .domain import CreatePlacementGroupResponse, PlacementGroup # noqa: F401 diff --git a/hcloud/placement_groups/client.py b/hcloud/placement_groups/client.py index f5017501..31245549 100644 --- a/hcloud/placement_groups/client.py +++ b/hcloud/placement_groups/client.py @@ -2,9 +2,8 @@ from typing import TYPE_CHECKING, NamedTuple -from ..actions.client import BoundAction -from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import Meta +from ..actions import BoundAction +from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta from .domain import CreatePlacementGroupResponse, PlacementGroup if TYPE_CHECKING: diff --git a/hcloud/placement_groups/domain.py b/hcloud/placement_groups/domain.py index af95ab2d..4a016c68 100644 --- a/hcloud/placement_groups/domain.py +++ b/hcloud/placement_groups/domain.py @@ -2,7 +2,7 @@ from dateutil.parser import isoparse -from ..core.domain import BaseDomain +from ..core import BaseDomain class PlacementGroup(BaseDomain): diff --git a/hcloud/primary_ips/__init__.py b/hcloud/primary_ips/__init__.py index e69de29b..d079a23b 100644 --- a/hcloud/primary_ips/__init__.py +++ b/hcloud/primary_ips/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from .client import BoundPrimaryIP, PrimaryIPsClient, PrimaryIPsPageResult # noqa: F401 +from .domain import CreatePrimaryIPResponse, PrimaryIP # noqa: F401 diff --git a/hcloud/primary_ips/client.py b/hcloud/primary_ips/client.py index a7aea615..03f9b9d9 100644 --- a/hcloud/primary_ips/client.py +++ b/hcloud/primary_ips/client.py @@ -2,9 +2,8 @@ from typing import TYPE_CHECKING, NamedTuple -from ..actions.client import BoundAction -from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import Meta +from ..actions import BoundAction +from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta from .domain import CreatePrimaryIPResponse, PrimaryIP if TYPE_CHECKING: @@ -17,7 +16,7 @@ class BoundPrimaryIP(BoundModelBase): model = PrimaryIP def __init__(self, client, data, complete=True): - from ..datacenters.client import BoundDatacenter + from ..datacenters import BoundDatacenter datacenter = data.get("datacenter", {}) if datacenter: diff --git a/hcloud/primary_ips/domain.py b/hcloud/primary_ips/domain.py index b80f4723..93e008fd 100644 --- a/hcloud/primary_ips/domain.py +++ b/hcloud/primary_ips/domain.py @@ -2,7 +2,7 @@ from dateutil.parser import isoparse -from ..core.domain import BaseDomain +from ..core import BaseDomain class PrimaryIP(BaseDomain): diff --git a/hcloud/server_types/__init__.py b/hcloud/server_types/__init__.py index e69de29b..1e978d09 100644 --- a/hcloud/server_types/__init__.py +++ b/hcloud/server_types/__init__.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .client import ( # noqa: F401 + BoundServerType, + ServerTypesClient, + ServerTypesPageResult, +) +from .domain import ServerType # noqa: F401 diff --git a/hcloud/server_types/client.py b/hcloud/server_types/client.py index b7fdae97..5c551a84 100644 --- a/hcloud/server_types/client.py +++ b/hcloud/server_types/client.py @@ -2,8 +2,7 @@ from typing import TYPE_CHECKING, NamedTuple -from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import Meta +from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta from .domain import ServerType if TYPE_CHECKING: diff --git a/hcloud/server_types/domain.py b/hcloud/server_types/domain.py index a578d6b8..2f8ff5fc 100644 --- a/hcloud/server_types/domain.py +++ b/hcloud/server_types/domain.py @@ -1,7 +1,7 @@ from __future__ import annotations -from ..core.domain import BaseDomain, DomainIdentityMixin -from ..deprecation.domain import DeprecationInfo +from ..core import BaseDomain, DomainIdentityMixin +from ..deprecation import DeprecationInfo class ServerType(BaseDomain, DomainIdentityMixin): diff --git a/hcloud/servers/__init__.py b/hcloud/servers/__init__.py index e69de29b..a7a61d4e 100644 --- a/hcloud/servers/__init__.py +++ b/hcloud/servers/__init__.py @@ -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, +) diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index a8fb3496..2dfd79bc 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -2,21 +2,19 @@ from typing import TYPE_CHECKING, NamedTuple -from ..actions.client import ActionsPageResult, BoundAction -from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import Meta -from ..datacenters.client import BoundDatacenter -from ..firewalls.client import BoundFirewall -from ..floating_ips.client import BoundFloatingIP -from ..images.client import BoundImage -from ..images.domain import CreateImageResponse -from ..isos.client import BoundIso -from ..networks.client import BoundNetwork # noqa -from ..networks.domain import Network # noqa -from ..placement_groups.client import BoundPlacementGroup -from ..primary_ips.client import BoundPrimaryIP -from ..server_types.client import BoundServerType -from ..volumes.client import BoundVolume +from ..actions import ActionsPageResult, BoundAction +from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..datacenters import BoundDatacenter +from ..firewalls import BoundFirewall +from ..floating_ips import BoundFloatingIP +from ..images import BoundImage, CreateImageResponse +from ..isos import BoundIso +from ..networks import BoundNetwork # noqa +from ..networks import Network # noqa +from ..placement_groups import BoundPlacementGroup +from ..primary_ips import BoundPrimaryIP +from ..server_types import BoundServerType +from ..volumes import BoundVolume from .domain import ( CreateServerResponse, EnableRescueResponse, diff --git a/hcloud/servers/domain.py b/hcloud/servers/domain.py index 9aaf53cf..ab10d956 100644 --- a/hcloud/servers/domain.py +++ b/hcloud/servers/domain.py @@ -2,7 +2,7 @@ from dateutil.parser import isoparse -from ..core.domain import BaseDomain +from ..core import BaseDomain class Server(BaseDomain): diff --git a/hcloud/ssh_keys/__init__.py b/hcloud/ssh_keys/__init__.py index e69de29b..25155906 100644 --- a/hcloud/ssh_keys/__init__.py +++ b/hcloud/ssh_keys/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from .client import BoundSSHKey, SSHKeysClient, SSHKeysPageResult # noqa: F401 +from .domain import SSHKey # noqa: F401 diff --git a/hcloud/ssh_keys/client.py b/hcloud/ssh_keys/client.py index 8913fe24..85d3bb54 100644 --- a/hcloud/ssh_keys/client.py +++ b/hcloud/ssh_keys/client.py @@ -2,8 +2,7 @@ from typing import TYPE_CHECKING, NamedTuple -from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import Meta +from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta from .domain import SSHKey if TYPE_CHECKING: diff --git a/hcloud/ssh_keys/domain.py b/hcloud/ssh_keys/domain.py index 3dc8c645..60ead2b7 100644 --- a/hcloud/ssh_keys/domain.py +++ b/hcloud/ssh_keys/domain.py @@ -2,7 +2,7 @@ from dateutil.parser import isoparse -from ..core.domain import BaseDomain, DomainIdentityMixin +from ..core import BaseDomain, DomainIdentityMixin class SSHKey(BaseDomain, DomainIdentityMixin): diff --git a/hcloud/volumes/__init__.py b/hcloud/volumes/__init__.py index e69de29b..dc8ccbb6 100644 --- a/hcloud/volumes/__init__.py +++ b/hcloud/volumes/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from .client import BoundVolume, VolumesClient, VolumesPageResult # noqa: F401 +from .domain import CreateVolumeResponse, Volume # noqa: F401 diff --git a/hcloud/volumes/client.py b/hcloud/volumes/client.py index 6a0cf27f..908f1c32 100644 --- a/hcloud/volumes/client.py +++ b/hcloud/volumes/client.py @@ -2,10 +2,9 @@ from typing import TYPE_CHECKING, NamedTuple -from ..actions.client import ActionsPageResult, BoundAction -from ..core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from ..core.domain import Meta -from ..locations.client import BoundLocation +from ..actions import ActionsPageResult, BoundAction +from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..locations import BoundLocation from .domain import CreateVolumeResponse, Volume if TYPE_CHECKING: @@ -22,7 +21,7 @@ def __init__(self, client, data, complete=True): 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: diff --git a/hcloud/volumes/domain.py b/hcloud/volumes/domain.py index 6e469694..ea3dc2de 100644 --- a/hcloud/volumes/domain.py +++ b/hcloud/volumes/domain.py @@ -2,7 +2,7 @@ from dateutil.parser import isoparse -from ..core.domain import BaseDomain, DomainIdentityMixin +from ..core import BaseDomain, DomainIdentityMixin class Volume(BaseDomain, DomainIdentityMixin): diff --git a/tests/unit/actions/test_client.py b/tests/unit/actions/test_client.py index 6fc6aef4..16a79618 100644 --- a/tests/unit/actions/test_client.py +++ b/tests/unit/actions/test_client.py @@ -4,8 +4,13 @@ import pytest -from hcloud.actions.client import ActionsClient, BoundAction -from hcloud.actions.domain import Action, ActionFailedException, ActionTimeoutException +from hcloud.actions import ( + Action, + ActionFailedException, + ActionsClient, + ActionTimeoutException, + BoundAction, +) class TestBoundAction: diff --git a/tests/unit/actions/test_domain.py b/tests/unit/actions/test_domain.py index 34f335f9..5133a240 100644 --- a/tests/unit/actions/test_domain.py +++ b/tests/unit/actions/test_domain.py @@ -5,7 +5,7 @@ import pytest -from hcloud.actions.domain import ( +from hcloud.actions import ( Action, ActionException, ActionFailedException, diff --git a/tests/unit/certificates/test_client.py b/tests/unit/certificates/test_client.py index 0d5c44ae..a039f3b5 100644 --- a/tests/unit/certificates/test_client.py +++ b/tests/unit/certificates/test_client.py @@ -4,9 +4,13 @@ import pytest -from hcloud.actions.client import BoundAction -from hcloud.certificates.client import BoundCertificate, CertificatesClient -from hcloud.certificates.domain import Certificate, ManagedCertificateStatus +from hcloud.actions import BoundAction +from hcloud.certificates import ( + BoundCertificate, + Certificate, + CertificatesClient, + ManagedCertificateStatus, +) class TestBoundCertificate: diff --git a/tests/unit/certificates/test_domain.py b/tests/unit/certificates/test_domain.py index 90340cb8..df916964 100644 --- a/tests/unit/certificates/test_domain.py +++ b/tests/unit/certificates/test_domain.py @@ -3,7 +3,7 @@ import datetime from datetime import timezone -from hcloud.certificates.domain import Certificate +from hcloud.certificates import Certificate class TestCertificate: diff --git a/tests/unit/core/test_client.py b/tests/unit/core/test_client.py index 6ea2cd63..1d4b2da0 100644 --- a/tests/unit/core/test_client.py +++ b/tests/unit/core/test_client.py @@ -5,9 +5,14 @@ import pytest -from hcloud.actions.client import ActionsPageResult -from hcloud.core.client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin -from hcloud.core.domain import BaseDomain, Meta +from hcloud.actions import ActionsPageResult +from hcloud.core import ( + BaseDomain, + BoundModelBase, + ClientEntityBase, + GetEntityByNameMixin, + Meta, +) class TestBoundModelBase: diff --git a/tests/unit/core/test_domain.py b/tests/unit/core/test_domain.py index 97cb6f74..456b3bb6 100644 --- a/tests/unit/core/test_domain.py +++ b/tests/unit/core/test_domain.py @@ -3,7 +3,7 @@ import pytest from dateutil.parser import isoparse -from hcloud.core.domain import BaseDomain, DomainIdentityMixin, Meta, Pagination +from hcloud.core import BaseDomain, DomainIdentityMixin, Meta, Pagination class TestMeta: diff --git a/tests/unit/datacenters/test_client.py b/tests/unit/datacenters/test_client.py index e4df2047..369566b4 100644 --- a/tests/unit/datacenters/test_client.py +++ b/tests/unit/datacenters/test_client.py @@ -4,9 +4,8 @@ import pytest # noqa: F401 -from hcloud.datacenters.client import BoundDatacenter, DatacentersClient -from hcloud.datacenters.domain import DatacenterServerTypes -from hcloud.locations.client import BoundLocation +from hcloud.datacenters import BoundDatacenter, DatacentersClient, DatacenterServerTypes +from hcloud.locations import BoundLocation class TestBoundDatacenter: diff --git a/tests/unit/firewalls/test_client.py b/tests/unit/firewalls/test_client.py index ba644a96..fdf1d116 100644 --- a/tests/unit/firewalls/test_client.py +++ b/tests/unit/firewalls/test_client.py @@ -4,15 +4,16 @@ import pytest -from hcloud.actions.client import BoundAction -from hcloud.firewalls.client import BoundFirewall, FirewallsClient -from hcloud.firewalls.domain import ( +from hcloud.actions import BoundAction +from hcloud.firewalls import ( + BoundFirewall, Firewall, FirewallResource, FirewallResourceLabelSelector, FirewallRule, + FirewallsClient, ) -from hcloud.servers.domain import Server +from hcloud.servers import Server class TestBoundFirewall: diff --git a/tests/unit/firewalls/test_domain.py b/tests/unit/firewalls/test_domain.py index d8d291fd..6c641222 100644 --- a/tests/unit/firewalls/test_domain.py +++ b/tests/unit/firewalls/test_domain.py @@ -3,7 +3,7 @@ import datetime from datetime import timezone -from hcloud.firewalls.domain import Firewall +from hcloud.firewalls import Firewall class TestFirewall: diff --git a/tests/unit/floating_ips/test_client.py b/tests/unit/floating_ips/test_client.py index 349f67f4..36eaf7ca 100644 --- a/tests/unit/floating_ips/test_client.py +++ b/tests/unit/floating_ips/test_client.py @@ -4,13 +4,10 @@ import pytest -from hcloud.actions.client import BoundAction -from hcloud.floating_ips.client import BoundFloatingIP, FloatingIPsClient -from hcloud.floating_ips.domain import FloatingIP -from hcloud.locations.client import BoundLocation -from hcloud.locations.domain import Location -from hcloud.servers.client import BoundServer -from hcloud.servers.domain import Server +from hcloud.actions import BoundAction +from hcloud.floating_ips import BoundFloatingIP, FloatingIP, FloatingIPsClient +from hcloud.locations import BoundLocation, Location +from hcloud.servers import BoundServer, Server class TestBoundFloatingIP: diff --git a/tests/unit/floating_ips/test_domain.py b/tests/unit/floating_ips/test_domain.py index fae67452..69e50161 100644 --- a/tests/unit/floating_ips/test_domain.py +++ b/tests/unit/floating_ips/test_domain.py @@ -3,7 +3,7 @@ import datetime from datetime import timezone -from hcloud.floating_ips.domain import FloatingIP +from hcloud.floating_ips import FloatingIP class TestFloatingIP: diff --git a/tests/unit/images/test_client.py b/tests/unit/images/test_client.py index 20e91395..876fbfe4 100644 --- a/tests/unit/images/test_client.py +++ b/tests/unit/images/test_client.py @@ -6,10 +6,9 @@ import pytest -from hcloud.actions.client import BoundAction -from hcloud.images.client import BoundImage, ImagesClient -from hcloud.images.domain import Image -from hcloud.servers.client import BoundServer +from hcloud.actions import BoundAction +from hcloud.images import BoundImage, Image, ImagesClient +from hcloud.servers import BoundServer class TestBoundImage: diff --git a/tests/unit/images/test_domain.py b/tests/unit/images/test_domain.py index 437c1e72..5c768877 100644 --- a/tests/unit/images/test_domain.py +++ b/tests/unit/images/test_domain.py @@ -3,7 +3,7 @@ import datetime from datetime import timezone -from hcloud.images.domain import Image +from hcloud.images import Image class TestImage: diff --git a/tests/unit/isos/test_client.py b/tests/unit/isos/test_client.py index 6a0953b9..7472184c 100644 --- a/tests/unit/isos/test_client.py +++ b/tests/unit/isos/test_client.py @@ -6,7 +6,7 @@ import pytest -from hcloud.isos.client import BoundIso, IsosClient +from hcloud.isos import BoundIso, IsosClient class TestBoundIso: diff --git a/tests/unit/isos/test_domain.py b/tests/unit/isos/test_domain.py index 3f9f387a..7ba847bb 100644 --- a/tests/unit/isos/test_domain.py +++ b/tests/unit/isos/test_domain.py @@ -3,7 +3,7 @@ import datetime from datetime import timezone -from hcloud.isos.domain import Iso +from hcloud.isos import Iso class TestIso: diff --git a/tests/unit/load_balancer_types/test_client.py b/tests/unit/load_balancer_types/test_client.py index 45572288..23d2d14f 100644 --- a/tests/unit/load_balancer_types/test_client.py +++ b/tests/unit/load_balancer_types/test_client.py @@ -4,7 +4,7 @@ import pytest -from hcloud.load_balancer_types.client import LoadBalancerTypesClient +from hcloud.load_balancer_types import LoadBalancerTypesClient class TestLoadBalancerTypesClient: diff --git a/tests/unit/load_balancers/test_client.py b/tests/unit/load_balancers/test_client.py index bc1d49d1..9c9d9071 100644 --- a/tests/unit/load_balancers/test_client.py +++ b/tests/unit/load_balancers/test_client.py @@ -4,21 +4,22 @@ import pytest -from hcloud.actions.client import BoundAction -from hcloud.load_balancer_types.domain import LoadBalancerType -from hcloud.load_balancers.client import BoundLoadBalancer, LoadBalancersClient -from hcloud.load_balancers.domain import ( +from hcloud.actions import BoundAction +from hcloud.load_balancer_types import LoadBalancerType +from hcloud.load_balancers import ( + BoundLoadBalancer, LoadBalancer, LoadBalancerAlgorithm, LoadBalancerHealthCheck, + LoadBalancersClient, LoadBalancerService, LoadBalancerTarget, LoadBalancerTargetIP, LoadBalancerTargetLabelSelector, ) -from hcloud.locations.domain import Location -from hcloud.networks.domain import Network -from hcloud.servers.domain import Server +from hcloud.locations import Location +from hcloud.networks import Network +from hcloud.servers import Server class TestBoundLoadBalancer: diff --git a/tests/unit/load_balancers/test_domain.py b/tests/unit/load_balancers/test_domain.py index 7334c16a..24bed108 100644 --- a/tests/unit/load_balancers/test_domain.py +++ b/tests/unit/load_balancers/test_domain.py @@ -3,7 +3,7 @@ import datetime from datetime import timezone -from hcloud.load_balancers.domain import LoadBalancer +from hcloud.load_balancers import LoadBalancer class TestLoadBalancers: diff --git a/tests/unit/locations/test_client.py b/tests/unit/locations/test_client.py index fd3072b7..9356707a 100644 --- a/tests/unit/locations/test_client.py +++ b/tests/unit/locations/test_client.py @@ -4,7 +4,7 @@ import pytest # noqa: F401 -from hcloud.locations.client import LocationsClient +from hcloud.locations import LocationsClient class TestLocationsClient: diff --git a/tests/unit/networks/test_client.py b/tests/unit/networks/test_client.py index 79f4825e..68855fd1 100644 --- a/tests/unit/networks/test_client.py +++ b/tests/unit/networks/test_client.py @@ -5,10 +5,15 @@ import pytest from dateutil.parser import isoparse -from hcloud.actions.client import BoundAction -from hcloud.networks.client import BoundNetwork, NetworksClient -from hcloud.networks.domain import Network, NetworkRoute, NetworkSubnet -from hcloud.servers.client import BoundServer +from hcloud.actions import BoundAction +from hcloud.networks import ( + BoundNetwork, + Network, + NetworkRoute, + NetworksClient, + NetworkSubnet, +) +from hcloud.servers import BoundServer class TestBoundNetwork: diff --git a/tests/unit/networks/test_domain.py b/tests/unit/networks/test_domain.py index 9fa2ac5f..1e5f10e3 100644 --- a/tests/unit/networks/test_domain.py +++ b/tests/unit/networks/test_domain.py @@ -3,7 +3,7 @@ import datetime from datetime import timezone -from hcloud.networks.domain import Network +from hcloud.networks import Network class TestNetwork: diff --git a/tests/unit/placement_groups/test_client.py b/tests/unit/placement_groups/test_client.py index afc5550d..6ba5db5c 100644 --- a/tests/unit/placement_groups/test_client.py +++ b/tests/unit/placement_groups/test_client.py @@ -4,7 +4,7 @@ import pytest -from hcloud.placement_groups.client import BoundPlacementGroup, PlacementGroupsClient +from hcloud.placement_groups import BoundPlacementGroup, PlacementGroupsClient def check_variables(placement_group, expected): diff --git a/tests/unit/placement_groups/test_domain.py b/tests/unit/placement_groups/test_domain.py index 357f5c75..91fd0c3d 100644 --- a/tests/unit/placement_groups/test_domain.py +++ b/tests/unit/placement_groups/test_domain.py @@ -3,7 +3,7 @@ import datetime from datetime import timezone -from hcloud.placement_groups.domain import PlacementGroup +from hcloud.placement_groups import PlacementGroup class TestPlacementGroup: diff --git a/tests/unit/primary_ips/test_client.py b/tests/unit/primary_ips/test_client.py index 8cf0cde3..85d4b9c4 100644 --- a/tests/unit/primary_ips/test_client.py +++ b/tests/unit/primary_ips/test_client.py @@ -4,10 +4,8 @@ import pytest -from hcloud.datacenters.client import BoundDatacenter -from hcloud.datacenters.domain import Datacenter -from hcloud.primary_ips.client import BoundPrimaryIP, PrimaryIPsClient -from hcloud.primary_ips.domain import PrimaryIP +from hcloud.datacenters import BoundDatacenter, Datacenter +from hcloud.primary_ips import BoundPrimaryIP, PrimaryIP, PrimaryIPsClient class TestBoundPrimaryIP: diff --git a/tests/unit/primary_ips/test_domain.py b/tests/unit/primary_ips/test_domain.py index 1856c65d..945522fd 100644 --- a/tests/unit/primary_ips/test_domain.py +++ b/tests/unit/primary_ips/test_domain.py @@ -3,7 +3,7 @@ import datetime from datetime import timezone -from hcloud.primary_ips.domain import PrimaryIP +from hcloud.primary_ips import PrimaryIP class TestPrimaryIP: diff --git a/tests/unit/server_types/test_client.py b/tests/unit/server_types/test_client.py index 9797d277..54ab3a9b 100644 --- a/tests/unit/server_types/test_client.py +++ b/tests/unit/server_types/test_client.py @@ -5,7 +5,7 @@ import pytest -from hcloud.server_types.client import BoundServerType, ServerTypesClient +from hcloud.server_types import BoundServerType, ServerTypesClient class TestBoundServerType: diff --git a/tests/unit/servers/test_client.py b/tests/unit/servers/test_client.py index 5b491617..6402d7db 100644 --- a/tests/unit/servers/test_client.py +++ b/tests/unit/servers/test_client.py @@ -4,34 +4,27 @@ import pytest -from hcloud.actions.client import BoundAction -from hcloud.datacenters.client import BoundDatacenter -from hcloud.datacenters.domain import Datacenter -from hcloud.firewalls.client import BoundFirewall -from hcloud.firewalls.domain import Firewall -from hcloud.floating_ips.client import BoundFloatingIP -from hcloud.images.client import BoundImage -from hcloud.images.domain import Image -from hcloud.isos.client import BoundIso -from hcloud.isos.domain import Iso -from hcloud.locations.domain import Location -from hcloud.networks.client import BoundNetwork -from hcloud.networks.domain import Network -from hcloud.placement_groups.client import BoundPlacementGroup -from hcloud.placement_groups.domain import PlacementGroup -from hcloud.server_types.client import BoundServerType -from hcloud.server_types.domain import ServerType -from hcloud.servers.client import BoundServer, ServersClient -from hcloud.servers.domain import ( +from hcloud.actions import BoundAction +from hcloud.datacenters import BoundDatacenter, Datacenter +from hcloud.firewalls import BoundFirewall, Firewall +from hcloud.floating_ips import BoundFloatingIP +from hcloud.images import BoundImage, Image +from hcloud.isos import BoundIso, Iso +from hcloud.locations import Location +from hcloud.networks import BoundNetwork, Network +from hcloud.placement_groups import BoundPlacementGroup, PlacementGroup +from hcloud.server_types import BoundServerType, ServerType +from hcloud.servers import ( + BoundServer, IPv4Address, IPv6Network, PrivateNet, PublicNetwork, PublicNetworkFirewall, Server, + ServersClient, ) -from hcloud.volumes.client import BoundVolume -from hcloud.volumes.domain import Volume +from hcloud.volumes import BoundVolume, Volume class TestBoundServer: diff --git a/tests/unit/servers/test_domain.py b/tests/unit/servers/test_domain.py index 3dc83f98..a4d10b63 100644 --- a/tests/unit/servers/test_domain.py +++ b/tests/unit/servers/test_domain.py @@ -3,7 +3,7 @@ import datetime from datetime import timezone -from hcloud.servers.domain import Server +from hcloud.servers import Server class TestServer: diff --git a/tests/unit/ssh_keys/test_client.py b/tests/unit/ssh_keys/test_client.py index 448a58c9..8a64a89f 100644 --- a/tests/unit/ssh_keys/test_client.py +++ b/tests/unit/ssh_keys/test_client.py @@ -4,8 +4,7 @@ import pytest -from hcloud.ssh_keys.client import BoundSSHKey, SSHKeysClient -from hcloud.ssh_keys.domain import SSHKey +from hcloud.ssh_keys import BoundSSHKey, SSHKey, SSHKeysClient class TestBoundSSHKey: diff --git a/tests/unit/ssh_keys/test_domain.py b/tests/unit/ssh_keys/test_domain.py index db799edf..edcf7526 100644 --- a/tests/unit/ssh_keys/test_domain.py +++ b/tests/unit/ssh_keys/test_domain.py @@ -3,7 +3,7 @@ import datetime from datetime import timezone -from hcloud.ssh_keys.domain import SSHKey +from hcloud.ssh_keys import SSHKey class TestSSHKey: diff --git a/tests/unit/volumes/test_client.py b/tests/unit/volumes/test_client.py index 9dc07fb4..0ed3fb1d 100644 --- a/tests/unit/volumes/test_client.py +++ b/tests/unit/volumes/test_client.py @@ -5,13 +5,10 @@ import pytest from dateutil.parser import isoparse -from hcloud.actions.client import BoundAction -from hcloud.locations.client import BoundLocation -from hcloud.locations.domain import Location -from hcloud.servers.client import BoundServer -from hcloud.servers.domain import Server -from hcloud.volumes.client import BoundVolume, VolumesClient -from hcloud.volumes.domain import Volume +from hcloud.actions import BoundAction +from hcloud.locations import BoundLocation, Location +from hcloud.servers import BoundServer, Server +from hcloud.volumes import BoundVolume, Volume, VolumesClient class TestBoundVolume: diff --git a/tests/unit/volumes/test_domain.py b/tests/unit/volumes/test_domain.py index 750adb4a..3844f2de 100644 --- a/tests/unit/volumes/test_domain.py +++ b/tests/unit/volumes/test_domain.py @@ -3,7 +3,7 @@ import datetime from datetime import timezone -from hcloud.volumes.domain import Volume +from hcloud.volumes import Volume class TestVolume: From efa5780d0de3080bffe43994c064a0f1bcf6da43 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 09:40:06 +0200 Subject: [PATCH 073/406] deps: update pre-commit hook asottile/pyupgrade to v3.10.1 (#261) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d769cd92..097b5e0e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: exclude: ^CHANGELOG.md$ - repo: https://github.com/asottile/pyupgrade - rev: v3.9.0 + rev: v3.10.1 hooks: - id: pyupgrade args: [--py38-plus] From fd013842f7f94e98520ed403a8cd91b68a4c4e5c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 09:40:23 +0200 Subject: [PATCH 074/406] deps: update pre-commit hook pycqa/flake8 to v6.1.0 (#260) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 097b5e0e..dd91eb42 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,6 +46,6 @@ repos: - id: black - repo: https://github.com/pycqa/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 From aaf81aeac069cf4228637be781a1c3ebbaad7242 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 31 Jul 2023 13:28:01 +0200 Subject: [PATCH 075/406] refactor: convert type comments to type hints (#255) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The converts the type comments into type hints, and adds a few extra typing fixes. Also improved readability by breaking down long typed argument lists on multiple lines. Removing the types from the docstrings will be done in a future PR or gradually while improving the docstrings. * refactor: remove unused results_list_attribute_name * test: add mypy linter * ci: add lint job * chore: convert type comments to annotations * style: break down large arg list on multiline * refactor: fix type hints * chore: fix server type hint Co-authored-by: Julian Tölle * chore: fix type hints * docs: add note about new lint task --------- Co-authored-by: Julian Tölle --- .github/workflows/lint.yml | 16 ++ .gitlab-ci.yml | 9 + Makefile | 3 + README.md | 6 + hcloud/_client.py | 6 +- hcloud/_exceptions.py | 4 +- hcloud/actions/client.py | 27 +- hcloud/actions/domain.py | 24 +- hcloud/certificates/client.py | 119 ++++++--- hcloud/certificates/domain.py | 41 +-- hcloud/core/client.py | 42 +-- hcloud/core/domain.py | 23 +- hcloud/datacenters/client.py | 24 +- hcloud/datacenters/domain.py | 20 +- hcloud/deprecation/domain.py | 4 +- hcloud/firewalls/client.py | 151 ++++++----- hcloud/firewalls/domain.py | 37 ++- hcloud/floating_ips/client.py | 153 ++++++----- hcloud/floating_ips/domain.py | 10 +- hcloud/helpers/labels.py | 2 +- hcloud/images/client.py | 138 +++++----- hcloud/images/domain.py | 45 ++-- hcloud/isos/client.py | 36 ++- hcloud/load_balancer_types/client.py | 21 +- hcloud/load_balancers/client.py | 278 +++++++++++-------- hcloud/load_balancers/domain.py | 43 ++- hcloud/locations/client.py | 21 +- hcloud/networks/client.py | 186 ++++++++----- hcloud/networks/domain.py | 17 +- hcloud/placement_groups/client.py | 67 ++--- hcloud/placement_groups/domain.py | 18 +- hcloud/primary_ips/client.py | 117 ++++---- hcloud/primary_ips/domain.py | 10 +- hcloud/server_types/client.py | 21 +- hcloud/servers/client.py | 382 ++++++++++++++++----------- hcloud/servers/domain.py | 76 +++--- hcloud/ssh_keys/client.py | 67 +++-- hcloud/volumes/client.py | 150 ++++++----- hcloud/volumes/domain.py | 36 ++- setup.py | 3 + 40 files changed, 1470 insertions(+), 983 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8a56cc68..17b04718 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,3 +21,19 @@ jobs: - name: Run pre-commit run: pre-commit run --all-files --show-diff-on-failure + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup python + uses: actions/setup-python@v4 + with: + python-version: 3.x + + - name: Install dependencies + run: make venv + + - name: Run lint + run: make lint diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8fcc3750..f83f1544 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,6 +7,15 @@ include: stages: - test +lint: + stage: test + + image: python:3.11-alpine + before_script: + - make venv + script: + - make lint + test: stage: test diff --git a/Makefile b/Makefile index 119b651a..a1cb1a3b 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,9 @@ venv: python3 -m venv venv venv/bin/pip install -e .[docs,test] +lint: venv + venv/bin/mypy hcloud + test: venv venv/bin/pytest -v diff --git a/README.md b/README.md index 88cba5f7..95a217fd 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,12 @@ Build the documentation and open it in your browser: make docs ``` +Lint the code: + +```sh +make lint +``` + Run tests using the current `python3` version: ```sh diff --git a/hcloud/_client.py b/hcloud/_client.py index c3aa3b33..a0969fef 100644 --- a/hcloud/_client.py +++ b/hcloud/_client.py @@ -187,7 +187,7 @@ def request( url: str, tries: int = 1, **kwargs, - ) -> bytes | dict: + ) -> dict: """Perform a request to the Hetzner Cloud API, wrapper around requests.request :param method: HTTP Method to perform the Request @@ -211,6 +211,7 @@ def request( 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 @@ -220,4 +221,5 @@ def request( 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] diff --git a/hcloud/_exceptions.py b/hcloud/_exceptions.py index 029f823a..51e3745a 100644 --- a/hcloud/_exceptions.py +++ b/hcloud/_exceptions.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + class HCloudException(Exception): """There was an error while using the hcloud library""" @@ -8,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 diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index 6a632761..7414726f 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -1,7 +1,7 @@ from __future__ import annotations import time -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import Action, ActionFailedException, ActionTimeoutException @@ -15,7 +15,7 @@ class BoundAction(BoundModelBase): 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 @@ -43,8 +43,7 @@ class ActionsPageResult(NamedTuple): class ActionsClient(ClientEntityBase): _client: Client - def get_by_id(self, id): - # type: (int) -> BoundAction + def get_by_id(self, id: int) -> BoundAction: """Get a specific action by its ID. :param id: int @@ -56,12 +55,11 @@ def get_by_id(self, id): 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) @@ -74,7 +72,7 @@ def get_list( Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if status is not None: params["status"] = status if sort is not None: @@ -90,8 +88,11 @@ def get_list( ] 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) diff --git a/hcloud/actions/domain.py b/hcloud/actions/domain.py index 7663bf1b..6950aea4 100644 --- a/hcloud/actions/domain.py +++ b/hcloud/actions/domain.py @@ -1,10 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from dateutil.parser import isoparse from .._exceptions import HCloudException from ..core import BaseDomain +if TYPE_CHECKING: + from .client import BoundAction + class Action(BaseDomain): """Action Domain @@ -39,14 +44,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 @@ -62,7 +67,8 @@ def __init__( 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']}" diff --git a/hcloud/certificates/client.py b/hcloud/certificates/client.py index 88201f78..966d9c55 100644 --- a/hcloud/certificates/client.py +++ b/hcloud/certificates/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta @@ -20,7 +20,7 @@ class BoundCertificate(BoundModelBase): 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") @@ -34,8 +34,13 @@ def __init__(self, client, data, complete=True): ) 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) @@ -50,8 +55,11 @@ def get_actions_list(self, status=None, sort=None, page=None, per_page=None): """ 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) @@ -62,8 +70,11 @@ def get_actions(self, status=None, sort=None): """ 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) @@ -74,15 +85,13 @@ def update(self, name=None, labels=None): """ 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 """ @@ -97,8 +106,7 @@ class CertificatesPageResult(NamedTuple): class CertificatesClient(ClientEntityBase, GetEntityByNameMixin): _client: Client - def get_by_id(self, id): - # type: (int) -> BoundCertificate + def get_by_id(self, id: int) -> BoundCertificate: """Get a specific certificate by its ID. :param id: int @@ -109,12 +117,11 @@ def get_by_id(self, id): 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) @@ -127,7 +134,7 @@ def get_list( Specifies how many results are returned by page :return: (List[:class:`BoundCertificate `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if name is not None: params["name"] = name @@ -151,8 +158,11 @@ def get_list( 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) @@ -163,8 +173,7 @@ def get_all(self, name=None, label_selector=None): """ return super().get_all(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 @@ -173,8 +182,13 @@ def get_by_name(self, name): """ return super().get_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` @@ -187,7 +201,7 @@ def create(self, name, certificate, private_key, labels=None): User-defined labels (key-value pairs) :return: :class:`BoundCertificate ` """ - data = { + data: dict[str, Any] = { "name": name, "certificate": certificate, "private_key": private_key, @@ -198,8 +212,12 @@ def create(self, name, certificate, private_key, labels=None): 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` @@ -210,7 +228,7 @@ def create_managed(self, name, domain_names, labels=None): User-defined labels (key-value pairs) :return: :class:`BoundCertificate ` """ - data = { + data: dict[str, Any] = { "name": name, "type": Certificate.TYPE_MANAGED, "domain_names": domain_names, @@ -223,8 +241,12 @@ def create_managed(self, name, domain_names, labels=None): 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 ` or :class:`Certificate ` @@ -234,7 +256,7 @@ def update(self, certificate, name=None, labels=None): User-defined labels (key-value pairs) :return: :class:`BoundCertificate ` """ - data = {} + data: dict[str, Any] = {} if name is not None: data["name"] = name if labels is not None: @@ -246,8 +268,7 @@ def update(self, certificate, name=None, labels=None): ) return BoundCertificate(self, response["certificate"]) - def delete(self, certificate): - # type: (Certificate) -> bool + def delete(self, certificate: Certificate | BoundCertificate) -> bool: self._client.request( url=f"/certificates/{certificate.id}", method="DELETE", @@ -261,9 +282,13 @@ def delete(self, certificate): 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 ` or :class:`Certificate ` @@ -277,7 +302,7 @@ def get_actions_list( Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if status is not None: params["status"] = status if sort is not None: @@ -300,8 +325,12 @@ def get_actions_list( ] 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 ` or :class:`Certificate ` @@ -313,8 +342,10 @@ def get_actions(self, certificate, status=None, sort=None): """ return super().get_actions(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 ` or :class:`Certificate ` diff --git a/hcloud/certificates/domain.py b/hcloud/certificates/domain.py index c07db1a9..20be3e33 100644 --- a/hcloud/certificates/domain.py +++ b/hcloud/certificates/domain.py @@ -1,9 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from dateutil.parser import isoparse from ..core import BaseDomain, DomainIdentityMixin +if TYPE_CHECKING: + from ..actions import BoundAction + from .client import BoundCertificate + class Certificate(BaseDomain, DomainIdentityMixin): """Certificate Domain @@ -43,17 +49,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 @@ -79,7 +85,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 @@ -94,7 +105,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 @@ -112,8 +123,8 @@ class CreateManagedCertificateResponse(BaseDomain): def __init__( self, - certificate, # type: BoundCertificate - action, # type: BoundAction + certificate: BoundCertificate, + action: BoundAction, ): self.certificate = certificate self.action = action diff --git a/hcloud/core/client.py b/hcloud/core/client.py index b0bde8c1..13d11e0f 100644 --- a/hcloud/core/client.py +++ b/hcloud/core/client.py @@ -1,11 +1,18 @@ from __future__ import annotations +from typing import TYPE_CHECKING, Any, Callable + +if TYPE_CHECKING: + from .._client import Client + from ..actions import BoundAction + class ClientEntityBase: - max_per_page = 50 - results_list_attribute_name = None + _client: Client + + max_per_page: int = 50 - def __init__(self, client): + def __init__(self, client: Client): """ :param client: Client :return self @@ -14,11 +21,10 @@ def __init__(self, client): def _get_all( self, - list_function, # type: function + list_function: Callable, *args, **kwargs, - ): - # type (...) -> List[BoundModelBase] + ) -> list: results = [] page = 1 @@ -39,16 +45,15 @@ def _get_all( ): page = meta.pagination.next_page else: - page = None + page = 0 return results - def get_all(self, *args, **kwargs): - # type: (...) -> List[BoundModelBase] + def get_all(self, *args, **kwargs) -> list: + assert hasattr(self, "get_list") return self._get_all(self.get_list, *args, **kwargs) - def get_actions(self, *args, **kwargs): - # type: (...) -> List[BoundModelBase] + def get_actions(self, *args, **kwargs) -> list[BoundAction]: if not hasattr(self, "get_actions_list"): raise ValueError("this endpoint does not support get_actions method") @@ -60,8 +65,8 @@ class GetEntityByNameMixin: Use as a mixin for ClientEntityBase classes """ - def get_by_name(self, name): - # type: (str) -> BoundModelBase + def get_by_name(self, name: str): + assert hasattr(self, "get_list") entities, _ = self.get_list(name=name) return entities[0] if entities else None @@ -69,9 +74,14 @@ def get_by_name(self, name): 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 @@ -84,7 +94,7 @@ def __init__(self, client, data={}, complete=True): self.complete = complete self.data_model = self.model.from_dict(data) - def __getattr__(self, name): + def __getattr__(self, name: str): """Allow magical access to the properties of the model :param name: str :return: diff --git a/hcloud/core/domain.py b/hcloud/core/domain.py index 8b0a2574..56229703 100644 --- a/hcloud/core/domain.py +++ b/hcloud/core/domain.py @@ -5,20 +5,23 @@ class BaseDomain: __slots__ = () @classmethod - def from_dict(cls, data): + def from_dict(cls, data: dict): 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,7 +60,7 @@ def __init__( class Meta(BaseDomain): __slots__ = ("pagination",) - def __init__(self, pagination=None): + def __init__(self, pagination: Pagination | None = None): self.pagination = pagination @classmethod diff --git a/hcloud/datacenters/client.py b/hcloud/datacenters/client.py index 2292148a..2f874c5b 100644 --- a/hcloud/datacenters/client.py +++ b/hcloud/datacenters/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta from ..locations import BoundLocation @@ -16,7 +16,7 @@ class BoundDatacenter(BoundModelBase): 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) @@ -58,8 +58,7 @@ class DatacentersPageResult(NamedTuple): class DatacentersClient(ClientEntityBase, GetEntityByNameMixin): _client: Client - def get_by_id(self, id): - # type: (int) -> BoundDatacenter + def get_by_id(self, id: int) -> BoundDatacenter: """Get a specific datacenter by its ID. :param id: int @@ -70,11 +69,10 @@ def get_by_id(self, id): 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) @@ -85,7 +83,7 @@ def get_list( Specifies how many results are returned by page :return: (List[:class:`BoundDatacenter `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if name is not None: params["name"] = name @@ -104,8 +102,7 @@ def get_list( 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) @@ -114,8 +111,7 @@ def get_all(self, name=None): """ return super().get_all(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 diff --git a/hcloud/datacenters/domain.py b/hcloud/datacenters/domain.py index e934760f..1c59bfa9 100644 --- a/hcloud/datacenters/domain.py +++ b/hcloud/datacenters/domain.py @@ -1,7 +1,13 @@ 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): """Datacenter Domain @@ -16,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 @@ -38,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 diff --git a/hcloud/deprecation/domain.py b/hcloud/deprecation/domain.py index 40d446ec..cd1b5365 100644 --- a/hcloud/deprecation/domain.py +++ b/hcloud/deprecation/domain.py @@ -24,8 +24,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 = ( diff --git a/hcloud/firewalls/client.py b/hcloud/firewalls/client.py index 61c6b539..764cc0cd 100644 --- a/hcloud/firewalls/client.py +++ b/hcloud/firewalls/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta @@ -21,7 +21,7 @@ class BoundFirewall(BoundModelBase): 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 = [ @@ -65,8 +65,13 @@ def __init__(self, client, data, complete=True): 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) @@ -81,8 +86,11 @@ def get_actions_list(self, status=None, sort=None, page=None, per_page=None): """ 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) @@ -94,8 +102,11 @@ def get_actions(self, status=None, sort=None): """ 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) @@ -106,16 +117,14 @@ def update(self, name=None, labels=None): """ 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 `] :return: List[:class:`BoundAction `] @@ -123,16 +132,20 @@ def set_rules(self, rules): 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 `] :return: List[:class:`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 `] :return: List[:class:`BoundAction `] @@ -150,13 +163,12 @@ class FirewallsClient(ClientEntityBase, GetEntityByNameMixin): 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 ` or :class:`Firewall ` @@ -170,7 +182,7 @@ def get_actions_list( Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if status is not None: params["status"] = status if sort is not None: @@ -192,11 +204,10 @@ def get_actions_list( 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 ` or :class:`Firewall ` @@ -209,8 +220,7 @@ def get_actions( """ return super().get_actions(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 @@ -221,13 +231,12 @@ def get_by_id(self, id): 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) @@ -242,7 +251,7 @@ def get_list( Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) :return: (List[:class:`BoundFirewall `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if label_selector is not None: params["label_selector"] = label_selector @@ -262,8 +271,12 @@ def get_list( 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) @@ -276,8 +289,7 @@ def get_all(self, label_selector=None, name=None, sort=None): """ return super().get_all(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 @@ -288,12 +300,11 @@ def get_by_name(self, 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 @@ -305,7 +316,7 @@ def create( :return: :class:`CreateFirewallResponse ` """ - data = {"name": name} + data: dict[str, Any] = {"name": name} if labels is not None: data["labels"] = labels @@ -330,8 +341,12 @@ def create( ) 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 ` or :class:`Firewall ` @@ -341,7 +356,7 @@ def update(self, firewall, labels=None, name=None): New name to set :return: :class:`BoundFirewall ` """ - data = {} + data: dict[str, Any] = {} if labels is not None: data["labels"] = labels if name is not None: @@ -354,8 +369,7 @@ def update(self, firewall, labels=None, name=None): ) 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 ` or :class:`Firewall ` @@ -368,15 +382,18 @@ def delete(self, firewall): # 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 ` or :class:`Firewall ` :param rules: List[:class:`FirewallRule `] :return: List[:class:`BoundAction `] """ - data = {"rules": []} + data: dict[str, Any] = {"rules": []} for rule in rules: data["rules"].append(rule.to_payload()) response = self._client.request( @@ -388,15 +405,18 @@ def set_rules(self, firewall, rules): ) return [BoundAction(self._client.actions, _) for _ 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 ` or :class:`Firewall ` :param resources: List[:class:`FirewallResource `] :return: List[:class:`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( @@ -408,15 +428,18 @@ def apply_to_resources(self, firewall, resources): ) return [BoundAction(self._client.actions, _) for _ 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 ` or :class:`Firewall ` :param resources: List[:class:`FirewallResource `] :return: List[:class:`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( diff --git a/hcloud/firewalls/domain.py b/hcloud/firewalls/domain.py index 9bddfed9..192e6491 100644 --- a/hcloud/firewalls/domain.py +++ b/hcloud/firewalls/domain.py @@ -1,9 +1,16 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from dateutil.parser import isoparse from ..core import BaseDomain +if TYPE_CHECKING: + from ..actions import BoundAction + from ..servers import BoundServer, Server + from .client import BoundFirewall + class Firewall(BaseDomain): """Firewall Domain @@ -25,7 +32,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=None, + name=None, + labels=None, + rules=None, + applied_to=None, + created=None, ): self.id = id self.name = name @@ -80,12 +93,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 @@ -129,9 +142,9 @@ 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 @@ -172,8 +185,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 diff --git a/hcloud/floating_ips/client.py b/hcloud/floating_ips/client.py index a4a1bf8e..e994fc84 100644 --- a/hcloud/floating_ips/client.py +++ b/hcloud/floating_ips/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta @@ -9,6 +9,8 @@ if TYPE_CHECKING: from .._client import Client + from ..locations import Location + from ..servers import BoundServer, Server class BoundFloatingIP(BoundModelBase): @@ -16,7 +18,7 @@ class BoundFloatingIP(BoundModelBase): model = FloatingIP - def __init__(self, client, data, complete=True): + def __init__(self, client: FloatingIPsClient, data: dict, complete: bool = True): from ..servers import BoundServer server = data.get("server") @@ -33,8 +35,13 @@ def __init__(self, client, data, complete=True): 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) @@ -49,8 +56,11 @@ def get_actions_list(self, status=None, sort=None, page=None, per_page=None): """ 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) @@ -62,8 +72,12 @@ def get_actions(self, status=None, sort=None): """ 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) @@ -76,16 +90,14 @@ def update(self, description=None, labels=None, name=None): """ 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 @@ -94,8 +106,7 @@ def change_protection(self, delete=None): """ 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 ` or :class:`Server ` @@ -104,16 +115,14 @@ def assign(self, server): """ 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 ` """ 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 @@ -135,13 +144,12 @@ class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin): 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 ` or :class:`FloatingIP ` @@ -155,7 +163,7 @@ def get_actions_list( Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if status is not None: params["status"] = status if sort is not None: @@ -179,11 +187,10 @@ def get_actions_list( 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 ` or :class:`FloatingIP ` @@ -196,8 +203,7 @@ def get_actions( """ return super().get_actions(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 @@ -208,12 +214,11 @@ def get_by_id(self, id): 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) @@ -226,7 +231,7 @@ def get_list( Can be used to filter networks by their name. :return: (List[:class:`BoundFloatingIP `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if label_selector is not None: params["label_selector"] = label_selector @@ -247,8 +252,11 @@ def get_list( 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) @@ -259,8 +267,7 @@ def get_all(self, label_selector=None, name=None): """ return super().get_all(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 @@ -271,14 +278,13 @@ def get_by_name(self, 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 @@ -294,7 +300,7 @@ def create( :return: :class:`CreateFloatingIPResponse ` """ - data = {"type": type} + data: dict[str, Any] = {"type": type} if description is not None: data["description"] = description if labels is not None: @@ -317,8 +323,13 @@ def create( ) 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 ` or :class:`FloatingIP ` @@ -330,7 +341,7 @@ def update(self, floating_ip, description=None, labels=None, name=None): New name to set :return: :class:`BoundFloatingIP ` """ - data = {} + data: dict[str, Any] = {} if description is not None: data["description"] = description if labels is not None: @@ -345,8 +356,7 @@ def update(self, floating_ip, description=None, labels=None, name=None): ) 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 ` or :class:`FloatingIP ` @@ -359,8 +369,11 @@ def delete(self, floating_ip): # 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 ` or :class:`FloatingIP ` @@ -368,7 +381,7 @@ def change_protection(self, floating_ip, delete=None): If true, prevents the Floating IP from being deleted :return: :class:`BoundAction ` """ - data = {} + data: dict[str, Any] = {} if delete is not None: data.update({"delete": delete}) @@ -381,8 +394,11 @@ def change_protection(self, floating_ip, delete=None): ) 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 ` or :class:`FloatingIP ` @@ -399,8 +415,7 @@ def assign(self, floating_ip, server): ) 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 ` or :class:`FloatingIP ` @@ -414,8 +429,12 @@ def unassign(self, floating_ip): ) 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 ` or :class:`FloatingIP ` diff --git a/hcloud/floating_ips/domain.py b/hcloud/floating_ips/domain.py index 39e1fe1a..22cce81f 100644 --- a/hcloud/floating_ips/domain.py +++ b/hcloud/floating_ips/domain.py @@ -1,9 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from dateutil.parser import isoparse from ..core import BaseDomain +if TYPE_CHECKING: + from ..actions import BoundAction + from .client import BoundFloatingIP + class FloatingIP(BaseDomain): """Floating IP Domain @@ -91,8 +97,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 diff --git a/hcloud/helpers/labels.py b/hcloud/helpers/labels.py index c5c08c17..d5af8bcc 100644 --- a/hcloud/helpers/labels.py +++ b/hcloud/helpers/labels.py @@ -26,7 +26,7 @@ def validate(labels: dict[str, str]) -> bool: return True @staticmethod - def validate_verbose(labels: dict[str, str]) -> tuple(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, if everything is fine. diff --git a/hcloud/images/client.py b/hcloud/images/client.py index f1e2a898..4bf724d0 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta @@ -15,7 +15,7 @@ class BoundImage(BoundModelBase): model = Image - def __init__(self, client, data): + def __init__(self, client: ImagesClient, data: dict): from ..servers import BoundServer created_from = data.get("created_from") @@ -31,8 +31,13 @@ def __init__(self, client, data): 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) @@ -49,8 +54,11 @@ def get_actions_list(self, sort=None, page=None, per_page=None, status=None): 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) @@ -61,8 +69,12 @@ def get_actions(self, sort=None, status=None): """ 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) @@ -76,16 +88,14 @@ def update(self, description=None, type=None, labels=None): """ 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 @@ -105,13 +115,12 @@ class ImagesClient(ClientEntityBase, GetEntityByNameMixin): 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 ` or :class:`Image ` @@ -125,7 +134,7 @@ def get_actions_list( Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if sort is not None: params["sort"] = sort if status is not None: @@ -147,11 +156,10 @@ def get_actions_list( 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 ` or :class:`Image ` @@ -163,8 +171,7 @@ def get_actions( """ return super().get_actions(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 @@ -175,18 +182,17 @@ def get_by_id(self, id): 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) @@ -211,7 +217,7 @@ def get_list( Specifies how many results are returned by page :return: (List[:class:`BoundImage `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if name is not None: params["name"] = name if label_selector is not None: @@ -239,16 +245,15 @@ def get_list( 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) @@ -280,8 +285,7 @@ def get_all( 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. @@ -292,8 +296,11 @@ def get_by_name(self, name): """ return super().get_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 @@ -306,8 +313,13 @@ def get_by_name_and_architecture(self, name, architecture): entity = entities[0] if entities else None return entity - 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 ` or :class:`Image ` @@ -320,7 +332,7 @@ def update(self, image, description=None, type=None, labels=None): User-defined labels (key-value pairs) :return: :class:`BoundImage ` """ - data = {} + data: dict[str, Any] = {} if description is not None: data.update({"description": description}) if type is not None: @@ -332,8 +344,7 @@ def update(self, image, description=None, type=None, labels=None): ) 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 ` or :class:`Image ` @@ -343,8 +354,11 @@ def delete(self, image): # 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 ` or :class:`Image ` @@ -352,7 +366,7 @@ def change_protection(self, image, delete=None): If true, prevents the snapshot from being deleted :return: :class:`BoundAction ` """ - data = {} + data: dict[str, Any] = {} if delete is not None: data.update({"delete": delete}) diff --git a/hcloud/images/domain.py b/hcloud/images/domain.py index 2d47077d..0f4add4f 100644 --- a/hcloud/images/domain.py +++ b/hcloud/images/domain.py @@ -1,9 +1,16 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from dateutil.parser import isoparse 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): """Image Domain @@ -66,23 +73,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 @@ -116,8 +123,8 @@ class CreateImageResponse(BaseDomain): def __init__( self, - action, # type: BoundAction - image, # type: BoundImage + action: BoundAction, + image: BoundImage, ): self.action = action self.image = image diff --git a/hcloud/isos/client.py b/hcloud/isos/client.py index 8c453d0c..c171673b 100644 --- a/hcloud/isos/client.py +++ b/hcloud/isos/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from warnings import warn from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta @@ -24,8 +24,7 @@ class IsosPageResult(NamedTuple): class IsosClient(ClientEntityBase, GetEntityByNameMixin): _client: Client - def get_by_id(self, id): - # type: (int) -> BoundIso + def get_by_id(self, id: int) -> BoundIso: """Get a specific ISO by its id :param id: int @@ -36,14 +35,13 @@ def get_by_id(self, id): 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) @@ -69,7 +67,7 @@ def get_list( ) include_architecture_wildcard = include_wildcard_architecture - params = {} + params: dict[str, Any] = {} if name is not None: params["name"] = name if architecture is not None: @@ -87,12 +85,11 @@ def get_list( 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) @@ -120,8 +117,7 @@ def get_all( 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 diff --git a/hcloud/load_balancer_types/client.py b/hcloud/load_balancer_types/client.py index 4c584f20..093b03fe 100644 --- a/hcloud/load_balancer_types/client.py +++ b/hcloud/load_balancer_types/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta from .domain import LoadBalancerType @@ -23,8 +23,7 @@ class LoadBalancerTypesPageResult(NamedTuple): class LoadBalancerTypesClient(ClientEntityBase, GetEntityByNameMixin): _client: Client - def get_by_id(self, id): - # type: (int) -> load_balancer_types.client.BoundLoadBalancerType + def get_by_id(self, id: int) -> BoundLoadBalancerType: """Returns a specific Load Balancer Type. :param id: int @@ -38,8 +37,12 @@ def get_by_id(self, id): ) 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) @@ -50,7 +53,7 @@ def get_list(self, name=None, page=None, per_page=None): Specifies how many results are returned by page :return: (List[:class:`BoundLoadBalancerType `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if name is not None: params["name"] = name if page is not None: @@ -69,8 +72,7 @@ def get_list(self, name=None, page=None, per_page=None): 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) @@ -79,8 +81,7 @@ def get_all(self, name=None): """ return super().get_all(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 diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index f03df7e9..9188f47e 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction from ..certificates import BoundCertificate @@ -28,6 +28,9 @@ if TYPE_CHECKING: from .._client import Client + from ..load_balancer_types import LoadBalancerType + from ..locations import Location + from ..networks import Network class BoundLoadBalancer(BoundModelBase): @@ -35,7 +38,7 @@ class BoundLoadBalancer(BoundModelBase): 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"]) @@ -139,8 +142,11 @@ def __init__(self, client, data, complete=True): 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) @@ -151,16 +157,20 @@ def update(self, name=None, labels=None): """ 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) @@ -175,8 +185,11 @@ def get_actions_list(self, status=None, sort=None, page=None, per_page=None): """ 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) @@ -187,8 +200,7 @@ def get_actions(self, status=None, sort=None): """ 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 ` @@ -197,8 +209,7 @@ def add_service(self, service): """ 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 ` @@ -207,8 +218,7 @@ def update_service(self, service): """ 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 ` @@ -217,8 +227,7 @@ def delete_service(self, service): """ 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 ` @@ -227,8 +236,7 @@ def add_target(self, target): """ 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 ` @@ -237,8 +245,7 @@ def remove_target(self, target): """ 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 ` @@ -247,8 +254,7 @@ def change_algorithm(self, algorithm): """ 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 @@ -259,8 +265,7 @@ def change_dns_ptr(self, ip, dns_ptr): """ 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 @@ -269,8 +274,11 @@ def change_protection(self, delete): """ 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 ` or :class:`Network ` @@ -280,8 +288,7 @@ def attach_to_network(self, network, ip=None): """ 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 ` or :class:`Network ` @@ -289,24 +296,24 @@ def detach_from_network(self, network): """ 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 ` """ 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 ` """ 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 ` or :class:`LoadBalancerType ` @@ -324,8 +331,7 @@ class LoadBalancersPageResult(NamedTuple): class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin): _client: Client - def get_by_id(self, id): - # type: (int) -> BoundLoadBalancer + def get_by_id(self, id: int) -> BoundLoadBalancer: """Get a specific Load Balancer :param id: int @@ -339,12 +345,11 @@ def get_by_id(self, id): 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) @@ -357,7 +362,7 @@ def get_list( Specifies how many results are returned by page :return: (List[:class:`BoundLoadBalancer `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if name is not None: params["name"] = name if label_selector is not None: @@ -377,8 +382,11 @@ def get_list( ] 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) @@ -389,8 +397,7 @@ def get_all(self, name=None, label_selector=None): """ return super().get_all(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 @@ -401,18 +408,17 @@ def get_by_name(self, 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 @@ -437,7 +443,10 @@ def create( Adds the Load Balancer to a Network :return: :class:`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: @@ -483,8 +492,12 @@ def create( 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 LoadBalancer’s name and a LoadBalancer’s labels. :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` @@ -494,7 +507,7 @@ def update(self, load_balancer, name=None, labels=None): User-defined labels (key-value pairs) :return: :class:`BoundLoadBalancer ` """ - data = {} + data: dict[str, Any] = {} if name is not None: data.update({"name": name}) if labels is not None: @@ -508,8 +521,7 @@ def update(self, load_balancer, name=None, labels=None): ) 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 ` or :class:`LoadBalancer ` @@ -524,9 +536,13 @@ def delete(self, load_balancer): 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 ` or :class:`LoadBalancer ` @@ -540,7 +556,7 @@ def get_actions_list( Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if status is not None: params["status"] = status if sort is not None: @@ -563,8 +579,12 @@ def get_actions_list( ] 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 ` or :class:`LoadBalancer ` @@ -576,8 +596,11 @@ def get_actions(self, load_balancer, status=None, sort=None): """ return super().get_actions(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 ` or :class:`LoadBalancer ` @@ -596,8 +619,8 @@ def add_service(self, load_balancer, service): ) return BoundAction(self._client.actions, response["action"]) - def get_service_parameters(self, service): - data = {} + def get_service_parameters(self, service: LoadBalancerService) -> dict[str, Any]: + data: dict[str, Any] = {} if service.protocol is not None: data["protocol"] = service.protocol if service.listen_port is not None: @@ -661,8 +684,11 @@ def get_service_parameters(self, service): 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 ` or :class:`LoadBalancer ` @@ -680,8 +706,11 @@ def update_service(self, load_balancer, service): ) 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 ` or :class:`LoadBalancer ` @@ -689,7 +718,7 @@ def delete_service(self, load_balancer, service): The LoadBalancerService you want to delete from the Load Balancer :return: :class:`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( @@ -700,8 +729,11 @@ def delete_service(self, load_balancer, service): ) 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 ` or :class:`LoadBalancer ` @@ -709,7 +741,10 @@ def add_target(self, load_balancer, target): The LoadBalancerTarget you want to add to the Load Balancer :return: :class:`BoundAction ` """ - data = {"type": target.type, "use_private_ip": target.use_private_ip} + data: dict[str, Any] = { + "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": @@ -726,8 +761,11 @@ def add_target(self, load_balancer, target): ) 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 ` or :class:`LoadBalancer ` @@ -735,7 +773,7 @@ def remove_target(self, load_balancer, target): The LoadBalancerTarget you want to remove from the Load Balancer :return: :class:`BoundAction ` """ - data = {"type": target.type} + data: dict[str, Any] = {"type": target.type} if target.type == "server": data["server"] = {"id": target.server.id} elif target.type == "label_selector": @@ -752,8 +790,11 @@ def remove_target(self, load_balancer, target): ) 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:` ` or :class:`LoadBalancer ` @@ -761,7 +802,7 @@ def change_algorithm(self, load_balancer, algorithm): The LoadBalancerSubnet you want to add to the Load Balancer :return: :class:`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( @@ -772,8 +813,12 @@ def change_algorithm(self, load_balancer, algorithm): ) 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 @@ -792,8 +837,11 @@ def change_dns_ptr(self, load_balancer, ip, 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:` ` or :class:`LoadBalancer ` @@ -801,7 +849,7 @@ def change_protection(self, load_balancer, delete=None): If True, prevents the Load Balancer from being deleted :return: :class:`BoundAction ` """ - data = {} + data: dict[str, Any] = {} if delete is not None: data.update({"delete": delete}) @@ -816,9 +864,9 @@ def change_protection(self, load_balancer, delete=None): 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, ): """Attach a Load Balancer to a Network. @@ -828,7 +876,7 @@ def attach_to_network( IP to request to be assigned to this Load Balancer :return: :class:`BoundAction ` """ - data = {"network": network.id} + data: dict[str, Any] = {"network": network.id} if ip is not None: data.update({"ip": ip}) @@ -841,15 +889,18 @@ def attach_to_network( ) 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:` ` or :class:`LoadBalancer ` :param network: :class:`BoundNetwork ` or :class:`Network ` :return: :class:`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 @@ -859,8 +910,10 @@ def detach_from_network(self, load_balancer, network): ) 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:` ` or :class:`LoadBalancer ` @@ -876,8 +929,10 @@ def enable_public_interface(self, load_balancer): ) 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:` ` or :class:`LoadBalancer ` @@ -893,8 +948,11 @@ def disable_public_interface(self, load_balancer): ) 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 ` or :class:`LoadBalancer ` @@ -902,7 +960,7 @@ def change_type(self, load_balancer, load_balancer_type): Load Balancer type the Load Balancer should migrate to :return: :class:`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 diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index d5f44644..aeff1d23 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -1,9 +1,16 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from dateutil.parser import isoparse from ..core import BaseDomain +if TYPE_CHECKING: + from ..actions import BoundAction + from ..networks import BoundNetwork + from .client import BoundLoadBalancer + class LoadBalancer(BaseDomain): """LoadBalancer Domain @@ -207,7 +214,12 @@ class LoadBalancerHealtCheckHttp(BaseDomain): """ def __init__( - self, domain=None, path=None, response=None, status_codes=None, tls=None + self, + domain=None, + path=None, + response=None, + status_codes=None, + tls=None, ): self.domain = domain self.path = path @@ -232,7 +244,12 @@ class LoadBalancerTarget(BaseDomain): """ def __init__( - self, type=None, server=None, label_selector=None, ip=None, use_private_ip=None + self, + type=None, + server=None, + label_selector=None, + ip=None, + use_private_ip=None, ): self.type = type self.server = server @@ -284,9 +301,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 @@ -304,8 +321,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 @@ -322,8 +339,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 @@ -342,8 +359,8 @@ class PrivateNet(BaseDomain): def __init__( self, - network, # type: BoundNetwork - ip, # type: str + network: BoundNetwork, + ip: str, ): self.network = network self.ip = ip @@ -362,8 +379,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 diff --git a/hcloud/locations/client.py b/hcloud/locations/client.py index 29d49a95..5ad0d1f1 100644 --- a/hcloud/locations/client.py +++ b/hcloud/locations/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta from .domain import Location @@ -23,8 +23,7 @@ class LocationsPageResult(NamedTuple): class LocationsClient(ClientEntityBase, GetEntityByNameMixin): _client: Client - def get_by_id(self, id): - # type: (int) -> locations.client.BoundLocation + def get_by_id(self, id: int) -> BoundLocation: """Get a specific location by its ID. :param id: int @@ -33,8 +32,12 @@ def get_by_id(self, id): 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) @@ -45,7 +48,7 @@ def get_list(self, name=None, page=None, per_page=None): Specifies how many results are returned by page :return: (List[:class:`BoundLocation `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if name is not None: params["name"] = name if page is not None: @@ -60,8 +63,7 @@ def get_list(self, name=None, page=None, per_page=None): ] 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) @@ -70,8 +72,7 @@ def get_all(self, name=None): """ return super().get_all(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 diff --git a/hcloud/networks/client.py b/hcloud/networks/client.py index 7abae6e5..afa98990 100644 --- a/hcloud/networks/client.py +++ b/hcloud/networks/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta @@ -15,7 +15,7 @@ class BoundNetwork(BoundModelBase): 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] @@ -40,10 +40,10 @@ def __init__(self, client, data, complete=True): 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 network’s name and a networks’s labels. :param name: str (optional) @@ -62,16 +62,20 @@ def update( 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) @@ -86,8 +90,11 @@ def get_actions_list(self, status=None, sort=None, page=None, per_page=None): """ 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) @@ -98,8 +105,7 @@ def get_actions(self, status=None, sort=None): """ 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 ` @@ -108,8 +114,7 @@ def add_subnet(self, subnet): """ 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 ` @@ -118,8 +123,7 @@ def delete_subnet(self, subnet): """ 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 ` @@ -128,8 +132,7 @@ def add_route(self, route): """ 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 ` @@ -138,8 +141,7 @@ def delete_route(self, route): """ 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 @@ -148,8 +150,7 @@ def change_ip_range(self, ip_range): """ 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 @@ -167,8 +168,7 @@ class NetworksPageResult(NamedTuple): class NetworksClient(ClientEntityBase, GetEntityByNameMixin): _client: Client - def get_by_id(self, id): - # type: (int) -> BoundNetwork + def get_by_id(self, id: int) -> BoundNetwork: """Get a specific network :param id: int @@ -179,12 +179,11 @@ def get_by_id(self, id): 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) @@ -197,7 +196,7 @@ def get_list( Specifies how many results are returned by page :return: (List[:class:`BoundNetwork `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if name is not None: params["name"] = name if label_selector is not None: @@ -214,8 +213,11 @@ def get_list( ] 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) @@ -226,8 +228,7 @@ def get_all(self, name=None, label_selector=None): """ return super().get_all(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 @@ -238,12 +239,12 @@ def get_by_name(self, 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, ): """Creates a network with range ip_range. @@ -262,7 +263,7 @@ def create( User-defined labels (key-value pairs) :return: :class:`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: @@ -293,8 +294,13 @@ def create( 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 network’s name and a network’s labels. :param network: :class:`BoundNetwork ` or :class:`Network ` @@ -307,7 +313,7 @@ def update(self, network, name=None, expose_routes_to_vswitch=None, labels=None) User-defined labels (key-value pairs) :return: :class:`BoundNetwork ` """ - data = {} + data: dict[str, Any] = {} if name is not None: data.update({"name": name}) @@ -324,8 +330,7 @@ def update(self, network, name=None, expose_routes_to_vswitch=None, labels=None) ) 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 ` or :class:`Network ` @@ -335,9 +340,13 @@ def delete(self, network): 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 ` or :class:`Network ` @@ -351,7 +360,7 @@ def get_actions_list( Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if status is not None: params["status"] = status if sort is not None: @@ -372,8 +381,12 @@ def get_actions_list( ] 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 ` or :class:`Network ` @@ -385,8 +398,11 @@ def get_actions(self, network, status=None, sort=None): """ return super().get_actions(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 ` or :class:`Network ` @@ -394,7 +410,10 @@ def add_subnet(self, network, subnet): The NetworkSubnet you want to add to the Network :return: :class:`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: @@ -409,8 +428,11 @@ def add_subnet(self, network, subnet): ) 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 ` or :class:`Network ` @@ -418,7 +440,7 @@ def delete_subnet(self, network, subnet): The NetworkSubnet you want to remove from the Network :return: :class:`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( @@ -429,8 +451,11 @@ def delete_subnet(self, network, subnet): ) 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 ` or :class:`Network ` @@ -438,7 +463,10 @@ def add_route(self, network, route): The NetworkRoute you want to add to the Network :return: :class:`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( @@ -449,8 +477,11 @@ def add_route(self, network, route): ) 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 ` or :class:`Network ` @@ -458,7 +489,10 @@ def delete_route(self, network, route): The NetworkRoute you want to remove from the Network :return: :class:`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( @@ -469,8 +503,11 @@ def delete_route(self, network, route): ) 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 ` or :class:`Network ` @@ -478,7 +515,7 @@ def change_ip_range(self, network, ip_range): The new prefix for the whole network. :return: :class:`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( @@ -489,8 +526,11 @@ def change_ip_range(self, network, ip_range): ) 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 ` or :class:`Network ` @@ -498,7 +538,7 @@ def change_protection(self, network, delete=None): If True, prevents the network from being deleted :return: :class:`BoundAction ` """ - data = {} + data: dict[str, Any] = {} if delete is not None: data.update({"delete": delete}) diff --git a/hcloud/networks/domain.py b/hcloud/networks/domain.py index 25874288..b6ed9c9e 100644 --- a/hcloud/networks/domain.py +++ b/hcloud/networks/domain.py @@ -1,9 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from dateutil.parser import isoparse from ..core import BaseDomain +if TYPE_CHECKING: + from ..actions import BoundAction + from .client import BoundNetwork + class Network(BaseDomain): """Network Domain @@ -90,7 +96,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, + type=None, + network_zone=None, + gateway=None, + vswitch_id=None, ): self.type = type self.ip_range = ip_range @@ -128,8 +139,8 @@ class CreateNetworkResponse(BaseDomain): def __init__( self, - network, # type: BoundNetwork - action, # type: BoundAction + network: BoundNetwork, + action: BoundAction, ): self.network = network self.action = action diff --git a/hcloud/placement_groups/client.py b/hcloud/placement_groups/client.py index 31245549..80f3d39d 100644 --- a/hcloud/placement_groups/client.py +++ b/hcloud/placement_groups/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import BoundAction from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta @@ -15,8 +15,11 @@ class BoundPlacementGroup(BoundModelBase): 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) @@ -27,8 +30,7 @@ def update(self, labels=None, name=None): """ return self._client.update(self, labels, name) - def delete(self): - # type: () -> bool + def delete(self) -> bool: """Deletes a Placement Group :return: boolean @@ -44,8 +46,7 @@ class PlacementGroupsPageResult(NamedTuple): class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin): _client: Client - def get_by_id(self, id): - # type: (int) -> BoundPlacementGroup + def get_by_id(self, id: int) -> BoundPlacementGroup: """Returns a specific Placement Group object :param id: int @@ -59,14 +60,13 @@ def get_by_id(self, id): 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) @@ -82,7 +82,7 @@ def get_list( :return: (List[:class:`BoundPlacementGroup `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if label_selector is not None: params["label_selector"] = label_selector @@ -106,8 +106,12 @@ def get_list( 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) @@ -120,8 +124,7 @@ def get_all(self, label_selector=None, name=None, sort=None): """ return super().get_all(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 @@ -132,11 +135,10 @@ def get_by_name(self, 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 @@ -148,7 +150,7 @@ def create( :return: :class:`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( @@ -165,8 +167,12 @@ def create( ) 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 ` or :class:`PlacementGroup ` @@ -177,7 +183,7 @@ def update(self, placement_group, labels=None, name=None): :return: :class:`BoundPlacementGroup ` """ - data = {} + data: dict[str, Any] = {} if labels is not None: data["labels"] = labels if name is not None: @@ -192,8 +198,7 @@ def update(self, placement_group, labels=None, name=None): ) 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 ` or :class:`PlacementGroup ` diff --git a/hcloud/placement_groups/domain.py b/hcloud/placement_groups/domain.py index 4a016c68..14a15e12 100644 --- a/hcloud/placement_groups/domain.py +++ b/hcloud/placement_groups/domain.py @@ -1,9 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from dateutil.parser import isoparse from ..core import BaseDomain +if TYPE_CHECKING: + from ..actions import BoundAction + from .client import BoundPlacementGroup + class PlacementGroup(BaseDomain): """Placement Group Domain @@ -30,7 +36,13 @@ class PlacementGroup(BaseDomain): TYPE_SPREAD = "spread" def __init__( - self, id=None, name=None, labels=None, servers=None, type=None, created=None + self, + id=None, + name=None, + labels=None, + servers=None, + type=None, + created=None, ): self.id = id self.name = name @@ -53,8 +65,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 diff --git a/hcloud/primary_ips/client.py b/hcloud/primary_ips/client.py index 03f9b9d9..8b676114 100644 --- a/hcloud/primary_ips/client.py +++ b/hcloud/primary_ips/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import BoundAction from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta @@ -8,6 +8,7 @@ if TYPE_CHECKING: from .._client import Client + from ..datacenters import BoundDatacenter, Datacenter class BoundPrimaryIP(BoundModelBase): @@ -15,7 +16,7 @@ class BoundPrimaryIP(BoundModelBase): model = PrimaryIP - def __init__(self, client, data, complete=True): + def __init__(self, client: PrimaryIPsClient, data: dict, complete: bool = True): from ..datacenters import BoundDatacenter datacenter = data.get("datacenter", {}) @@ -24,8 +25,12 @@ def __init__(self, client, data, complete=True): 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) @@ -40,16 +45,14 @@ def update(self, auto_delete=None, labels=None, name=None): 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 @@ -58,8 +61,7 @@ def change_protection(self, delete=None): """ 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` @@ -70,16 +72,14 @@ def assign(self, assignee_id, assignee_type): """ 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 ` """ 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 @@ -99,8 +99,7 @@ class PrimaryIPsPageResult(NamedTuple): class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin): _client: Client - def get_by_id(self, id): - # type: (int) -> BoundPrimaryIP + def get_by_id(self, id: int) -> BoundPrimaryIP: """Returns a specific Primary IP object. :param id: int @@ -111,13 +110,12 @@ def get_by_id(self, id): 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) @@ -132,7 +130,7 @@ def get_list( Can be used to filter resources by their ip. The response will only contain the resources matching the specified ip. :return: (List[:class:`BoundPrimaryIP `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if label_selector is not None: params["label_selector"] = label_selector @@ -153,8 +151,11 @@ def get_list( 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) @@ -165,8 +166,7 @@ def get_all(self, label_selector=None, name=None): """ return super().get_all(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 @@ -177,15 +177,14 @@ def get_by_name(self, 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, + datacenter: Datacenter | BoundDatacenter, + 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 @@ -200,7 +199,7 @@ def create( :return: :class:`CreatePrimaryIPResponse ` """ - data = { + data: dict[str, Any] = { "type": type, "assignee_type": assignee_type, "auto_delete": auto_delete, @@ -223,8 +222,13 @@ def create( ) 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 ` or :class:`PrimaryIP ` @@ -236,7 +240,7 @@ def update(self, primary_ip, auto_delete=None, labels=None, name=None): New name to set :return: :class:`BoundPrimaryIP ` """ - data = {} + data: dict[str, Any] = {} if auto_delete is not None: data["auto_delete"] = auto_delete if labels is not None: @@ -251,8 +255,7 @@ def update(self, primary_ip, auto_delete=None, labels=None, name=None): ) 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 ` or :class:`PrimaryIP ` @@ -265,8 +268,11 @@ def delete(self, primary_ip): # 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 ` or :class:`PrimaryIP ` @@ -274,7 +280,7 @@ def change_protection(self, primary_ip, delete=None): If true, prevents the Primary IP from being deleted :return: :class:`BoundAction ` """ - data = {} + data: dict[str, Any] = {} if delete is not None: data.update({"delete": delete}) @@ -287,8 +293,12 @@ def change_protection(self, primary_ip, delete=None): ) 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 ` or :class:`PrimaryIP ` @@ -307,8 +317,7 @@ def assign(self, primary_ip, assignee_id, assignee_type="server"): ) 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 ` or :class:`PrimaryIP ` @@ -322,8 +331,12 @@ def unassign(self, primary_ip): ) 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 ` or :class:`PrimaryIP ` diff --git a/hcloud/primary_ips/domain.py b/hcloud/primary_ips/domain.py index 93e008fd..8a1f22b3 100644 --- a/hcloud/primary_ips/domain.py +++ b/hcloud/primary_ips/domain.py @@ -1,9 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from dateutil.parser import isoparse from ..core import BaseDomain +if TYPE_CHECKING: + from ..actions import BoundAction + from .client import BoundPrimaryIP + class PrimaryIP(BaseDomain): """Primary IP Domain @@ -96,8 +102,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 diff --git a/hcloud/server_types/client.py b/hcloud/server_types/client.py index 5c551a84..20b2c9fe 100644 --- a/hcloud/server_types/client.py +++ b/hcloud/server_types/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta from .domain import ServerType @@ -23,8 +23,7 @@ class ServerTypesPageResult(NamedTuple): class ServerTypesClient(ClientEntityBase, GetEntityByNameMixin): _client: Client - def get_by_id(self, id): - # type: (int) -> BoundServerType + def get_by_id(self, id: int) -> BoundServerType: """Returns a specific Server Type. :param id: int @@ -33,8 +32,12 @@ def get_by_id(self, id): 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) @@ -45,7 +48,7 @@ def get_list(self, name=None, page=None, per_page=None): Specifies how many results are returned by page :return: (List[:class:`BoundServerType `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if name is not None: params["name"] = name if page is not None: @@ -62,8 +65,7 @@ def get_list(self, name=None, page=None, per_page=None): ] 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) @@ -72,8 +74,7 @@ def get_all(self, name=None): """ return super().get_all(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 diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index 2dfd79bc..f3ba4ed2 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta @@ -30,6 +30,16 @@ if TYPE_CHECKING: from .._client import Client + from ..datacenters import Datacenter + from ..firewalls import Firewall + from ..images import Image + from ..isos import Iso + from ..locations import BoundLocation, Location + from ..placement_groups import PlacementGroup + from ..server_types import ServerType + from ..ssh_keys import BoundSSHKey, SSHKey + from ..volumes import Volume + from .domain import ServerCreatePublicNetwork class BoundServer(BoundModelBase): @@ -37,7 +47,7 @@ class BoundServer(BoundModelBase): model = Server - def __init__(self, client, data, complete=True): + def __init__(self, client: ServersClient, data: dict, complete: bool = True): datacenter = data.get("datacenter") if datacenter is not None: data["datacenter"] = BoundDatacenter(client._client.datacenters, datacenter) @@ -144,8 +154,13 @@ def __init__(self, client, data, complete=True): 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 server. :param status: List[str] (optional) @@ -160,8 +175,11 @@ def get_actions_list(self, status=None, sort=None, page=None, per_page=None): """ 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 server. :param status: List[str] (optional) @@ -172,8 +190,11 @@ def get_actions(self, status=None, sort=None): """ return self._client.get_actions(self, status, sort) - def update(self, name=None, labels=None): - # type: (Optional[str], Optional[Dict[str, str]]) -> BoundServer + def update( + self, + name: str | None = None, + labels: dict[str, str] | None = None, + ) -> BoundServer: """Updates a server. You can update a server’s name and a server’s labels. :param name: str (optional) @@ -184,64 +205,60 @@ def update(self, name=None, labels=None): """ return self._client.update(self, name, labels) - def delete(self): - # type: () -> BoundAction + def delete(self) -> BoundAction: """Deletes a server. This immediately removes the server from your account, and it is no longer accessible. :return: :class:`BoundAction ` """ return self._client.delete(self) - def power_off(self): - # type: () -> BoundAction + def power_off(self) -> BoundAction: """Cuts power to the server. This forcefully stops it without giving the server operating system time to gracefully stop :return: :class:`BoundAction ` """ return self._client.power_off(self) - def power_on(self): - # type: () -> BoundAction + def power_on(self) -> BoundAction: """Starts a server by turning its power on. :return: :class:`BoundAction ` """ return self._client.power_on(self) - def reboot(self): - # type: () -> BoundAction + def reboot(self) -> BoundAction: """Reboots a server gracefully by sending an ACPI request. :return: :class:`BoundAction ` """ return self._client.reboot(self) - def reset(self): - # type: () -> BoundAction + def reset(self) -> BoundAction: """Cuts power to a server and starts it again. :return: :class:`BoundAction ` """ return self._client.reset(self) - def shutdown(self): - # type: () -> BoundAction + def shutdown(self) -> BoundAction: """Shuts down a server gracefully by sending an ACPI shutdown request. :return: :class:`BoundAction ` """ return self._client.shutdown(self) - def reset_password(self): - # type: () -> ResetPasswordResponse + def reset_password(self) -> ResetPasswordResponse: """Resets the root password. Only works for Linux systems that are running the qemu guest agent. :return: :class:`ResetPasswordResponse ` """ return self._client.reset_password(self) - def enable_rescue(self, type=None, ssh_keys=None): - # type: (str, Optional[List[str]]) -> EnableRescueResponse + def enable_rescue( + self, + type: str | None = None, + ssh_keys: list[str] | None = None, + ) -> EnableRescueResponse: """Enable the Hetzner Rescue System for this server. :param type: str @@ -253,16 +270,19 @@ def enable_rescue(self, type=None, ssh_keys=None): """ return self._client.enable_rescue(self, type=type, ssh_keys=ssh_keys) - def disable_rescue(self): - # type: () -> BoundAction + def disable_rescue(self) -> BoundAction: """Disables the Hetzner Rescue System for a server. :return: :class:`BoundAction ` """ return self._client.disable_rescue(self) - def create_image(self, description=None, type=None, labels=None): - # type: (str, str, Optional[Dict[str, str]]) -> CreateImageResponse + def create_image( + self, + description: str | None = None, + type: str | None = None, + labels: dict[str, str] | None = None, + ) -> CreateImageResponse: """Creates an image (snapshot) from a server by copying the contents of its disks. :param description: str (optional) @@ -276,8 +296,7 @@ def create_image(self, description=None, type=None, labels=None): """ return self._client.create_image(self, description, type, labels) - def rebuild(self, image): - # type: (Image) -> BoundAction + def rebuild(self, image: Image | BoundImage) -> BoundAction: """Rebuilds a server overwriting its disk with the content of an image, thereby destroying all data on the target server. :param image: :class:`BoundImage ` or :class:`Image ` @@ -285,8 +304,11 @@ def rebuild(self, image): """ return self._client.rebuild(self, image) - def change_type(self, server_type, upgrade_disk): - # type: (BoundServerType, bool) -> BoundAction + def change_type( + self, + server_type: ServerType | BoundServerType, + upgrade_disk: bool, + ) -> BoundAction: """Changes the type (Cores, RAM and disk sizes) of a server. :param server_type: :class:`BoundServerType ` or :class:`ServerType ` @@ -297,24 +319,21 @@ def change_type(self, server_type, upgrade_disk): """ return self._client.change_type(self, server_type, upgrade_disk) - def enable_backup(self): - # type: () -> BoundAction + def enable_backup(self) -> BoundAction: """Enables and configures the automatic daily backup option for the server. Enabling automatic backups will increase the price of the server by 20%. :return: :class:`BoundAction ` """ return self._client.enable_backup(self) - def disable_backup(self): - # type: () -> BoundAction + def disable_backup(self) -> BoundAction: """Disables the automatic backup option and deletes all existing Backups for a Server. :return: :class:`BoundAction ` """ return self._client.disable_backup(self) - def attach_iso(self, iso): - # type: (Iso) -> BoundAction + def attach_iso(self, iso: Iso | BoundIso) -> BoundAction: """Attaches an ISO to a server. :param iso: :class:`BoundIso ` or :class:`Server ` @@ -322,16 +341,14 @@ def attach_iso(self, iso): """ return self._client.attach_iso(self, iso) - def detach_iso(self): - # type: () -> BoundAction + def detach_iso(self) -> BoundAction: """Detaches an ISO from a server. :return: :class:`BoundAction ` """ return self._client.detach_iso(self) - def change_dns_ptr(self, ip, dns_ptr): - # type: (str, Optional[str]) -> BoundAction + def change_dns_ptr(self, ip: str, dns_ptr: str | None) -> BoundAction: """Changes the hostname that will appear when getting the hostname belonging to the primary IPs (ipv4 and ipv6) of this server. :param ip: str @@ -342,8 +359,11 @@ def change_dns_ptr(self, ip, dns_ptr): """ return self._client.change_dns_ptr(self, ip, dns_ptr) - def change_protection(self, delete=None, rebuild=None): - # type: (Optional[bool], Optional[bool]) -> BoundAction + def change_protection( + self, + delete: bool | None = None, + rebuild: bool | None = None, + ) -> BoundAction: """Changes the protection configuration of the server. :param server: :class:`BoundServer ` or :class:`Server ` @@ -355,16 +375,19 @@ def change_protection(self, delete=None, rebuild=None): """ return self._client.change_protection(self, delete, rebuild) - def request_console(self): - # type: () -> RequestConsoleResponse + def request_console(self) -> RequestConsoleResponse: """Requests credentials for remote access via vnc over websocket to keyboard, monitor, and mouse for a server. :return: :class:`RequestConsoleResponse ` """ return self._client.request_console(self) - def attach_to_network(self, network, ip=None, alias_ips=None): - # type: (Union[Network,BoundNetwork],Optional[str], Optional[List[str]]) -> BoundAction + def attach_to_network( + self, + network: Network | BoundNetwork, + ip: str | None = None, + alias_ips: list[str] | None = None, + ) -> BoundAction: """Attaches a server to a network :param network: :class:`BoundNetwork ` or :class:`Network ` @@ -376,8 +399,7 @@ def attach_to_network(self, network, ip=None, alias_ips=None): """ return self._client.attach_to_network(self, network, ip, alias_ips) - def detach_from_network(self, network): - # type: ( Union[Network,BoundNetwork]) -> BoundAction + def detach_from_network(self, network: Network | BoundNetwork) -> BoundAction: """Detaches a server from a network. :param network: :class:`BoundNetwork ` or :class:`Network ` @@ -385,8 +407,11 @@ def detach_from_network(self, network): """ return self._client.detach_from_network(self, network) - def change_alias_ips(self, network, alias_ips): - # type: (Union[Network,BoundNetwork], List[str]) -> BoundAction + def change_alias_ips( + self, + network: Network | BoundNetwork, + alias_ips: list[str], + ) -> BoundAction: """Changes the alias IPs of an already attached network. :param network: :class:`BoundNetwork ` or :class:`Network ` @@ -396,8 +421,10 @@ def change_alias_ips(self, network, alias_ips): """ return self._client.change_alias_ips(self, network, alias_ips) - def add_to_placement_group(self, placement_group): - # type: (Union[PlacementGroup,BoundPlacementGroup]) -> BoundAction + def add_to_placement_group( + self, + placement_group: PlacementGroup | BoundPlacementGroup, + ) -> BoundAction: """Adds a server to a placement group. :param placement_group: :class:`BoundPlacementGroup ` or :class:`Network ` @@ -405,8 +432,7 @@ def add_to_placement_group(self, placement_group): """ return self._client.add_to_placement_group(self, placement_group) - def remove_from_placement_group(self): - # type: () -> BoundAction + def remove_from_placement_group(self) -> BoundAction: """Removes a server from a placement group. :return: :class:`BoundAction ` @@ -422,8 +448,7 @@ class ServersPageResult(NamedTuple): class ServersClient(ClientEntityBase, GetEntityByNameMixin): _client: Client - def get_by_id(self, id): - # type: (int) -> BoundServer + def get_by_id(self, id: int) -> BoundServer: """Get a specific server :param id: int @@ -434,13 +459,12 @@ def get_by_id(self, id): 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] - status=None, # type: Optional[List[str]] - ): - # type: (...) -> PageResults[List[BoundServer], Meta] + name: str | None = None, + label_selector: str | None = None, + page: int | None = None, + per_page: int | None = None, + status: list[str] | None = None, + ) -> ServersPageResult: """Get a list of servers from this account :param name: str (optional) @@ -455,7 +479,7 @@ def get_list( Specifies how many results are returned by page :return: (List[:class:`BoundServer `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if name is not None: params["name"] = name if label_selector is not None: @@ -474,8 +498,12 @@ def get_list( ] return ServersPageResult(ass_servers, Meta.parse_meta(response)) - def get_all(self, name=None, label_selector=None, status=None): - # type: (Optional[str], Optional[str], Optional[List[str]]) -> List[BoundServer] + def get_all( + self, + name: str | None = None, + label_selector: str | None = None, + status: list[str] | None = None, + ) -> list[BoundServer]: """Get all servers from this account :param name: str (optional) @@ -488,8 +516,7 @@ def get_all(self, name=None, label_selector=None, status=None): """ return super().get_all(name=name, label_selector=label_selector, status=status) - def get_by_name(self, name): - # type: (str) -> BoundServer + def get_by_name(self, name: str) -> BoundServer | None: """Get server by name :param name: str @@ -500,23 +527,22 @@ def get_by_name(self, name): def create( self, - name, # type: str - server_type, # type: ServerType - image, # type: Image - ssh_keys=None, # type: Optional[List[SSHKey]] - volumes=None, # type: Optional[List[Volume]] - firewalls=None, # type: Optional[List[Firewall]] - networks=None, # type: Optional[List[Network]] - user_data=None, # type: Optional[str] - labels=None, # type: Optional[Dict[str, str]] - location=None, # type: Optional[Location] - datacenter=None, # type: Optional[Datacenter] - start_after_create=True, # type: Optional[bool] - automount=None, # type: Optional[bool] - placement_group=None, # type: Optional[PlacementGroup] - public_net=None, # type: Optional[ServerCreatePublicNetwork] - ): - # type: (...) -> CreateServerResponse + name: str, + server_type: ServerType | BoundServerType, + image: Image, + ssh_keys: list[SSHKey | BoundSSHKey] | None = None, + volumes: list[Volume | BoundVolume] | None = None, + firewalls: list[Firewall | BoundFirewall] | None = None, + networks: list[Network | BoundNetwork] | None = None, + user_data: str | None = None, + labels: dict[str, str] | None = None, + location: Location | BoundLocation | None = None, + datacenter: Datacenter | BoundDatacenter | None = None, + start_after_create: bool | None = True, + automount: bool | None = None, + placement_group: PlacementGroup | BoundPlacementGroup | None = None, + public_net: ServerCreatePublicNetwork | None = None, + ) -> CreateServerResponse: """Creates a new server. Returns preliminary information about the server as well as an action that covers progress of creation. :param name: str @@ -547,7 +573,7 @@ def create( Options to configure the public network of a server on creation :return: :class:`CreateServerResponse ` """ - data = { + data: dict[str, Any] = { "name": name, "server_type": server_type.id_or_name, "start_after_create": start_after_create, @@ -600,9 +626,13 @@ def create( return result def get_actions_list( - self, server, status=None, sort=None, page=None, per_page=None - ): - # type: (Server, Optional[List[str]], Optional[List[str]], Optional[int], Optional[int]) -> PageResults[List[BoundAction], Meta] + self, + server: Server | BoundServer, + 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 server. :param server: :class:`BoundServer ` or :class:`Server ` @@ -616,7 +646,7 @@ def get_actions_list( Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if status is not None: params["status"] = status if sort is not None: @@ -637,8 +667,12 @@ def get_actions_list( ] return ActionsPageResult(actions, Meta.parse_meta(response)) - def get_actions(self, server, status=None, sort=None): - # type: (Server, Optional[List[str]], Optional[List[str]]) -> List[BoundAction] + def get_actions( + self, + server: Server | BoundServer, + status: list[str] | None = None, + sort: list[str] | None = None, + ) -> list[BoundAction]: """Returns all action objects for a server. :param server: :class:`BoundServer ` or :class:`Server ` @@ -650,8 +684,12 @@ def get_actions(self, server, status=None, sort=None): """ return super().get_actions(server, status=status, sort=sort) - def update(self, server, name=None, labels=None): - # type:(Server, Optional[str], Optional[Dict[str, str]]) -> BoundServer + def update( + self, + server: Server | BoundServer, + name: str | None = None, + labels: dict[str, str] | None = None, + ) -> BoundServer: """Updates a server. You can update a server’s name and a server’s labels. :param server: :class:`BoundServer ` or :class:`Server ` @@ -661,7 +699,7 @@ def update(self, server, name=None, labels=None): User-defined labels (key-value pairs) :return: :class:`BoundServer ` """ - data = {} + data: dict[str, Any] = {} if name is not None: data.update({"name": name}) if labels is not None: @@ -673,8 +711,7 @@ def update(self, server, name=None, labels=None): ) return BoundServer(self, response["server"]) - def delete(self, server): - # type: (Server) -> BoundAction + def delete(self, server: Server | BoundServer) -> BoundAction: """Deletes a server. This immediately removes the server from your account, and it is no longer accessible. :param server: :class:`BoundServer ` or :class:`Server ` @@ -683,8 +720,7 @@ def delete(self, server): response = self._client.request(url=f"/servers/{server.id}", method="DELETE") return BoundAction(self._client.actions, response["action"]) - def power_off(self, server): - # type: (Server) -> Action + def power_off(self, server: Server | BoundServer) -> BoundAction: """Cuts power to the server. This forcefully stops it without giving the server operating system time to gracefully stop :param server: :class:`BoundServer ` or :class:`Server ` @@ -696,8 +732,7 @@ def power_off(self, server): ) return BoundAction(self._client.actions, response["action"]) - def power_on(self, server): - # type: (servers.domain.Server) -> actions.domain.Action + def power_on(self, server: Server | BoundServer) -> BoundAction: """Starts a server by turning its power on. :param server: :class:`BoundServer ` or :class:`Server ` @@ -709,8 +744,7 @@ def power_on(self, server): ) return BoundAction(self._client.actions, response["action"]) - def reboot(self, server): - # type: (servers.domain.Server) -> actions.domain.Action + def reboot(self, server: Server | BoundServer) -> BoundAction: """Reboots a server gracefully by sending an ACPI request. :param server: :class:`BoundServer ` or :class:`Server ` @@ -722,8 +756,7 @@ def reboot(self, server): ) return BoundAction(self._client.actions, response["action"]) - def reset(self, server): - # type: (servers.domain.Server) -> actions.domainAction + def reset(self, server: Server | BoundServer) -> BoundAction: """Cuts power to a server and starts it again. :param server: :class:`BoundServer ` or :class:`Server ` @@ -735,8 +768,7 @@ def reset(self, server): ) return BoundAction(self._client.actions, response["action"]) - def shutdown(self, server): - # type: (servers.domain.Server) -> actions.domainAction + def shutdown(self, server: Server | BoundServer) -> BoundAction: """Shuts down a server gracefully by sending an ACPI shutdown request. :param server: :class:`BoundServer ` or :class:`Server ` @@ -748,8 +780,7 @@ def shutdown(self, server): ) return BoundAction(self._client.actions, response["action"]) - def reset_password(self, server): - # type: (servers.domain.Server) -> ResetPasswordResponse + def reset_password(self, server: Server | BoundServer) -> ResetPasswordResponse: """Resets the root password. Only works for Linux systems that are running the qemu guest agent. :param server: :class:`BoundServer ` or :class:`Server ` @@ -766,8 +797,12 @@ def reset_password(self, server): root_password=response["root_password"], ) - def change_type(self, server, server_type, upgrade_disk): - # type: (servers.domain.Server, BoundServerType, bool) -> actions.domainAction + def change_type( + self, + server: Server | BoundServer, + server_type: ServerType | BoundServerType, + upgrade_disk: bool, + ) -> BoundAction: """Changes the type (Cores, RAM and disk sizes) of a server. :param server: :class:`BoundServer ` or :class:`Server ` @@ -777,7 +812,10 @@ def change_type(self, server, server_type, upgrade_disk): If false, do not upgrade the disk. This allows downgrading the server type later. :return: :class:`BoundAction ` """ - data = {"server_type": server_type.id_or_name, "upgrade_disk": upgrade_disk} + data: dict[str, Any] = { + "server_type": server_type.id_or_name, + "upgrade_disk": upgrade_disk, + } response = self._client.request( url=f"/servers/{server.id}/actions/change_type", method="POST", @@ -785,8 +823,12 @@ def change_type(self, server, server_type, upgrade_disk): ) return BoundAction(self._client.actions, response["action"]) - def enable_rescue(self, server, type=None, ssh_keys=None): - # type: (servers.domain.Server, str, Optional[List[str]]) -> EnableRescueResponse + def enable_rescue( + self, + server: Server | BoundServer, + type: str | None = None, + ssh_keys: list[str] | None = None, + ) -> EnableRescueResponse: """Enable the Hetzner Rescue System for this server. :param server: :class:`BoundServer ` or :class:`Server ` @@ -797,7 +839,7 @@ def enable_rescue(self, server, type=None, ssh_keys=None): Array of SSH key IDs which should be injected into the rescue system. Only available for types: linux64 and linux32. :return: :class:`EnableRescueResponse ` """ - data = {"type": type} + data: dict[str, Any] = {"type": type} if ssh_keys is not None: data.update({"ssh_keys": ssh_keys}) @@ -813,8 +855,7 @@ def enable_rescue(self, server, type=None, ssh_keys=None): root_password=response["root_password"], ) - def disable_rescue(self, server): - # type: (servers.domain.Server) -> actions.domainAction + def disable_rescue(self, server: Server | BoundServer) -> BoundAction: """Disables the Hetzner Rescue System for a server. :param server: :class:`BoundServer ` or :class:`Server ` @@ -828,8 +869,13 @@ def disable_rescue(self, server): ) return BoundAction(self._client.actions, response["action"]) - def create_image(self, server, description=None, type=None, labels=None): - # type: (servers.domain.Server, str, str, Optional[Dict[str, str]]) -> CreateImageResponse + def create_image( + self, + server: Server | BoundServer, + description: str | None = None, + type: str | None = None, + labels: dict[str, str] | None = None, + ) -> CreateImageResponse: """Creates an image (snapshot) from a server by copying the contents of its disks. :param server: :class:`BoundServer ` or :class:`Server ` @@ -842,7 +888,7 @@ def create_image(self, server, description=None, type=None, labels=None): User-defined labels (key-value pairs) :return: :class:`CreateImageResponse ` """ - data = {} + data: dict[str, Any] = {} if description is not None: data.update({"description": description}) @@ -862,15 +908,18 @@ def create_image(self, server, description=None, type=None, labels=None): image=BoundImage(self._client.images, response["image"]), ) - def rebuild(self, server, image): - # type: (servers.domain.Server, Image) -> actions.domainAction + def rebuild( + self, + server: Server | BoundServer, + image: Image | BoundImage, + ) -> BoundAction: """Rebuilds a server overwriting its disk with the content of an image, thereby destroying all data on the target server. :param server: :class:`BoundServer ` or :class:`Server ` :param image: :class:`BoundImage ` or :class:`Image ` :return: :class:`BoundAction ` """ - data = {"image": image.id_or_name} + data: dict[str, Any] = {"image": image.id_or_name} response = self._client.request( url=f"/servers/{server.id}/actions/rebuild", method="POST", @@ -878,8 +927,7 @@ def rebuild(self, server, image): ) return BoundAction(self._client.actions, response["action"]) - def enable_backup(self, server): - # type: (servers.domain.Server) -> actions.domainAction + def enable_backup(self, server: Server | BoundServer) -> BoundAction: """Enables and configures the automatic daily backup option for the server. Enabling automatic backups will increase the price of the server by 20%. :param server: :class:`BoundServer ` or :class:`Server ` @@ -893,8 +941,7 @@ def enable_backup(self, server): ) return BoundAction(self._client.actions, response["action"]) - def disable_backup(self, server): - # type: (servers.domain.Server) -> actions.domainAction + def disable_backup(self, server: Server | BoundServer) -> BoundAction: """Disables the automatic backup option and deletes all existing Backups for a Server. :param server: :class:`BoundServer ` or :class:`Server ` @@ -908,15 +955,18 @@ def disable_backup(self, server): ) return BoundAction(self._client.actions, response["action"]) - def attach_iso(self, server, iso): - # type: (servers.domain.Server, Iso) -> actions.domainAction + def attach_iso( + self, + server: Server | BoundServer, + iso: Iso | BoundIso, + ) -> BoundAction: """Attaches an ISO to a server. :param server: :class:`BoundServer ` or :class:`Server ` :param iso: :class:`BoundIso ` or :class:`Server ` :return: :class:`BoundAction ` """ - data = {"iso": iso.id_or_name} + data: dict[str, Any] = {"iso": iso.id_or_name} response = self._client.request( url=f"/servers/{server.id}/actions/attach_iso", method="POST", @@ -924,8 +974,7 @@ def attach_iso(self, server, iso): ) return BoundAction(self._client.actions, response["action"]) - def detach_iso(self, server): - # type: (servers.domain.Server) -> actions.domainAction + def detach_iso(self, server: Server | BoundServer) -> BoundAction: """Detaches an ISO from a server. :param server: :class:`BoundServer ` or :class:`Server ` @@ -937,8 +986,12 @@ def detach_iso(self, server): ) return BoundAction(self._client.actions, response["action"]) - def change_dns_ptr(self, server, ip, dns_ptr): - # type: (servers.domain.Server, str, str) -> actions.domainAction + def change_dns_ptr( + self, + server: Server | BoundServer, + ip: str, + dns_ptr: str | None, + ) -> BoundAction: """Changes the hostname that will appear when getting the hostname belonging to the primary IPs (ipv4 and ipv6) of this server. :param server: :class:`BoundServer ` or :class:`Server ` @@ -948,7 +1001,7 @@ def change_dns_ptr(self, server, ip, dns_ptr): Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` :return: :class:`BoundAction ` """ - data = {"ip": ip, "dns_ptr": dns_ptr} + data: dict[str, Any] = {"ip": ip, "dns_ptr": dns_ptr} response = self._client.request( url="/servers/{server_id}/actions/change_dns_ptr".format( server_id=server.id @@ -958,8 +1011,12 @@ def change_dns_ptr(self, server, ip, dns_ptr): ) return BoundAction(self._client.actions, response["action"]) - def change_protection(self, server, delete=None, rebuild=None): - # type: (servers.domain.Server, Optional[bool], Optional[bool]) -> actions.domainAction + def change_protection( + self, + server: Server | BoundServer, + delete: bool | None = None, + rebuild: bool | None = None, + ) -> BoundAction: """Changes the protection configuration of the server. :param server: :class:`BoundServer ` or :class:`Server ` @@ -969,7 +1026,7 @@ def change_protection(self, server, delete=None, rebuild=None): If true, prevents the server from being rebuilt (currently delete and rebuild attribute needs to have the same value) :return: :class:`BoundAction ` """ - data = {} + data: dict[str, Any] = {} if delete is not None: data.update({"delete": delete}) if rebuild is not None: @@ -984,8 +1041,7 @@ def change_protection(self, server, delete=None, rebuild=None): ) return BoundAction(self._client.actions, response["action"]) - def request_console(self, server): - # type: (servers.domain.Server) -> RequestConsoleResponse + def request_console(self, server: Server | BoundServer) -> RequestConsoleResponse: """Requests credentials for remote access via vnc over websocket to keyboard, monitor, and mouse for a server. :param server: :class:`BoundServer ` or :class:`Server ` @@ -1003,8 +1059,13 @@ def request_console(self, server): password=response["password"], ) - def attach_to_network(self, server, network, ip=None, alias_ips=None): - # type: (Union[Server,BoundServer], Union[Network,BoundNetwork],Optional[str], Optional[List[str]]) -> BoundAction + def attach_to_network( + self, + server: Server | BoundServer, + network: Network | BoundNetwork, + ip: str | None = None, + alias_ips: list[str] | None = None, + ) -> BoundAction: """Attaches a server to a network :param server: :class:`BoundServer ` or :class:`Server ` @@ -1015,7 +1076,7 @@ def attach_to_network(self, server, network, ip=None, alias_ips=None): New alias IPs to set for this server. :return: :class:`BoundAction ` """ - data = {"network": network.id} + data: dict[str, Any] = {"network": network.id} if ip is not None: data.update({"ip": ip}) if alias_ips is not None: @@ -1029,15 +1090,18 @@ def attach_to_network(self, server, network, ip=None, alias_ips=None): ) return BoundAction(self._client.actions, response["action"]) - def detach_from_network(self, server, network): - # type: (Union[Server,BoundServer], Union[Network,BoundNetwork]) -> BoundAction + def detach_from_network( + self, + server: Server | BoundServer, + network: Network | BoundNetwork, + ) -> BoundAction: """Detaches a server from a network. :param server: :class:`BoundServer ` or :class:`Server ` :param network: :class:`BoundNetwork ` or :class:`Network ` :return: :class:`BoundAction ` """ - data = {"network": network.id} + data: dict[str, Any] = {"network": network.id} response = self._client.request( url="/servers/{server_id}/actions/detach_from_network".format( server_id=server.id @@ -1047,8 +1111,12 @@ def detach_from_network(self, server, network): ) return BoundAction(self._client.actions, response["action"]) - def change_alias_ips(self, server, network, alias_ips): - # type: (Union[Server,BoundServer], Union[Network,BoundNetwork], List[str]) -> BoundAction + def change_alias_ips( + self, + server: Server | BoundServer, + network: Network | BoundNetwork, + alias_ips: list[str], + ) -> BoundAction: """Changes the alias IPs of an already attached network. :param server: :class:`BoundServer ` or :class:`Server ` @@ -1057,7 +1125,7 @@ def change_alias_ips(self, server, network, alias_ips): New alias IPs to set for this server. :return: :class:`BoundAction ` """ - data = {"network": network.id, "alias_ips": alias_ips} + data: dict[str, Any] = {"network": network.id, "alias_ips": alias_ips} response = self._client.request( url="/servers/{server_id}/actions/change_alias_ips".format( server_id=server.id @@ -1067,15 +1135,18 @@ def change_alias_ips(self, server, network, alias_ips): ) return BoundAction(self._client.actions, response["action"]) - def add_to_placement_group(self, server, placement_group): - # type: (Union[Server,BoundServer], Union[PlacementGroup,BoundPlacementGroup]) -> BoundAction + def add_to_placement_group( + self, + server: Server | BoundServer, + placement_group: PlacementGroup | BoundPlacementGroup, + ) -> BoundAction: """Adds a server to a placement group. :param server: :class:`BoundServer ` or :class:`Server ` :param placement_group: :class:`BoundPlacementGroup ` or :class:`Network ` :return: :class:`BoundAction ` """ - data = {"placement_group": str(placement_group.id)} + data: dict[str, Any] = {"placement_group": str(placement_group.id)} response = self._client.request( url="/servers/{server_id}/actions/add_to_placement_group".format( server_id=server.id @@ -1085,8 +1156,7 @@ def add_to_placement_group(self, server, placement_group): ) return BoundAction(self._client.actions, response["action"]) - def remove_from_placement_group(self, server): - # type: (Union[Server,BoundServer]) -> BoundAction + def remove_from_placement_group(self, server: Server | BoundServer) -> BoundAction: """Removes a server from a placement group. :param server: :class:`BoundServer ` or :class:`Server ` diff --git a/hcloud/servers/domain.py b/hcloud/servers/domain.py index ab10d956..f4c3ac93 100644 --- a/hcloud/servers/domain.py +++ b/hcloud/servers/domain.py @@ -1,9 +1,19 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from dateutil.parser import isoparse from ..core import BaseDomain +if TYPE_CHECKING: + from ..actions import BoundAction + from ..firewalls import BoundFirewall + from ..floating_ips import BoundFloatingIP + from ..networks import BoundNetwork + from ..primary_ips import BoundPrimaryIP, PrimaryIP + from .client import BoundServer + class Server(BaseDomain): """Server Domain @@ -152,10 +162,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 @@ -176,8 +186,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 @@ -196,8 +206,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 @@ -218,9 +228,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 @@ -249,12 +259,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 @@ -280,8 +290,8 @@ class PublicNetworkFirewall(BaseDomain): def __init__( self, - firewall, # type: BoundFirewall - status, # type: str + firewall: BoundFirewall, + status: str, ): self.firewall = firewall self.status = status @@ -302,9 +312,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 @@ -330,9 +340,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 @@ -359,10 +369,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 @@ -383,10 +393,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 diff --git a/hcloud/ssh_keys/client.py b/hcloud/ssh_keys/client.py index 85d3bb54..8ccc6f60 100644 --- a/hcloud/ssh_keys/client.py +++ b/hcloud/ssh_keys/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta from .domain import SSHKey @@ -14,8 +14,11 @@ class BoundSSHKey(BoundModelBase): 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) @@ -26,8 +29,7 @@ def update(self, name=None, labels=None): """ 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 """ @@ -42,8 +44,7 @@ class SSHKeysPageResult(NamedTuple): class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin): _client: Client - def get_by_id(self, id): - # type: (int) -> BoundSSHKey + def get_by_id(self, id: int) -> BoundSSHKey: """Get a specific SSH Key by its ID :param id: int @@ -54,13 +55,12 @@ def get_by_id(self, id): 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) @@ -75,7 +75,7 @@ def get_list( Specifies how many results are returned by page :return: (List[:class:`BoundSSHKey `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if name is not None: params["name"] = name if fingerprint is not None: @@ -94,8 +94,12 @@ def get_list( ] 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) @@ -110,8 +114,7 @@ def get_all(self, name=None, fingerprint=None, label_selector=None): 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 @@ -120,8 +123,7 @@ def get_by_name(self, name): """ return super().get_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 @@ -132,8 +134,12 @@ def get_by_fingerprint(self, fingerprint): sshkeys = response.ssh_keys return sshkeys[0] if sshkeys else None - 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 @@ -143,14 +149,18 @@ def create(self, name, public_key, labels=None): User-defined labels (key-value pairs) :return: :class:`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 ` or :class:`SSHKey ` @@ -160,7 +170,7 @@ def update(self, ssh_key, name=None, labels=None): User-defined labels (key-value pairs) :return: :class:`BoundSSHKey ` """ - data = {} + data: dict[str, Any] = {} if name is not None: data["name"] = name if labels is not None: @@ -172,8 +182,7 @@ def update(self, ssh_key, name=None, labels=None): ) return BoundSSHKey(self, response["ssh_key"]) - def delete(self, ssh_key): - # type: (SSHKey) -> bool + def delete(self, ssh_key: SSHKey | BoundSSHKey) -> bool: self._client.request(url=f"/ssh_keys/{ssh_key.id}", method="DELETE") """Deletes an SSH key. It cannot be used anymore. diff --git a/hcloud/volumes/client.py b/hcloud/volumes/client.py index 908f1c32..2ebda12b 100644 --- a/hcloud/volumes/client.py +++ b/hcloud/volumes/client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta @@ -9,6 +9,8 @@ if TYPE_CHECKING: from .._client import Client + from ..locations import Location + from ..servers import BoundServer, Server class BoundVolume(BoundModelBase): @@ -16,7 +18,7 @@ class BoundVolume(BoundModelBase): 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) @@ -30,8 +32,13 @@ def __init__(self, client, data, complete=True): ) 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) @@ -46,8 +53,11 @@ def get_actions_list(self, status=None, sort=None, page=None, per_page=None): """ 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) @@ -58,8 +68,11 @@ def get_actions(self, status=None, sort=None): """ 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) @@ -70,16 +83,18 @@ def update(self, name=None, labels=None): """ 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 ` or :class:`Server ` @@ -88,16 +103,14 @@ def attach(self, server, automount=None): """ return self._client.attach(self, server, automount) - def detach(self): - # type: () -> BoundAction + def detach(self) -> BoundAction: """Detaches a volume from the server it’s attached to. You may attach it to a server again at a later time. :return: :class:`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 @@ -106,8 +119,7 @@ def resize(self, size): """ 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 @@ -125,8 +137,7 @@ class VolumesPageResult(NamedTuple): class VolumesClient(ClientEntityBase, GetEntityByNameMixin): _client: Client - def get_by_id(self, id): - # type: (int) -> volumes.client.BoundVolume + def get_by_id(self, id: int) -> BoundVolume: """Get a specific volume by its id :param id: int @@ -136,9 +147,13 @@ def get_by_id(self, id): 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) @@ -153,7 +168,7 @@ def get_list( Specifies how many results are returned by page :return: (List[:class:`BoundVolume `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if name is not None: params["name"] = name if label_selector is not None: @@ -171,8 +186,11 @@ def get_list( ] 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: @@ -183,8 +201,7 @@ def get_all(self, label_selector=None, status=None): """ return super().get_all(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 @@ -195,15 +212,14 @@ def get_by_name(self, 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 @@ -227,7 +243,7 @@ def create( 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: @@ -253,9 +269,13 @@ def create( 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 ` or :class:`Volume ` @@ -269,7 +289,7 @@ def get_actions_list( Specifies how many results are returned by page :return: (List[:class:`BoundAction `], :class:`Meta `) """ - params = {} + params: dict[str, Any] = {} if status is not None: params["status"] = status if sort is not None: @@ -290,8 +310,12 @@ def get_actions_list( ] 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 ` or :class:`Volume ` @@ -303,8 +327,12 @@ def get_actions(self, volume, status=None, sort=None): """ return super().get_actions(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 ` or :class:`Volume ` @@ -314,7 +342,7 @@ def update(self, volume, name=None, labels=None): User-defined labels (key-value pairs) :return: :class:`BoundAction ` """ - data = {} + data: dict[str, Any] = {} if name is not None: data.update({"name": name}) if labels is not None: @@ -326,8 +354,7 @@ def update(self, volume, name=None, labels=None): ) 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 ` or :class:`Volume ` @@ -336,8 +363,7 @@ def delete(self, volume): 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 ` or :class:`Volume ` @@ -352,8 +378,12 @@ def resize(self, volume, size): ) 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 ` or :class:`Volume ` @@ -361,7 +391,7 @@ def attach(self, volume, server, automount=None): :param automount: boolean :return: :class:`BoundAction ` """ - data = {"server": server.id} + data: dict[str, Any] = {"server": server.id} if automount is not None: data["automount"] = automount @@ -372,8 +402,7 @@ def attach(self, volume, server, automount=None): ) 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 it’s attached to. You may attach it to a server again at a later time. :param volume: :class:`BoundVolume ` or :class:`Volume ` @@ -385,8 +414,11 @@ def detach(self, volume): ) 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 ` or :class:`Volume ` @@ -394,7 +426,7 @@ def change_protection(self, volume, delete=None): If True, prevents the volume from being deleted :return: :class:`BoundAction ` """ - data = {} + data: dict[str, Any] = {} if delete is not None: data.update({"delete": delete}) diff --git a/hcloud/volumes/domain.py b/hcloud/volumes/domain.py index ea3dc2de..fb793ef6 100644 --- a/hcloud/volumes/domain.py +++ b/hcloud/volumes/domain.py @@ -1,9 +1,17 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from dateutil.parser import isoparse 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): """Volume Domain @@ -53,17 +61,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 @@ -93,9 +101,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 diff --git a/setup.py b/setup.py index a596aae9..fa3e0ee2 100644 --- a/setup.py +++ b/setup.py @@ -51,6 +51,9 @@ "test": [ "coverage>=7.2.7,<7.3", "pytest>=7.4,<7.5", + "mypy>=1.4.1,<1.5", + "types-python-dateutil", + "types-requests", ], }, include_package_data=True, From 68bb0f2bf33df5e12607b94a306f5ec328685c44 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Tue, 1 Aug 2023 11:30:38 +0200 Subject: [PATCH 076/406] chore: fix package included files (#263) --- MANIFEST.in | 6 +++--- setup.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 2533badc..6e1ce0d8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,10 @@ +include CHANGELOG.md include CONTRIBUTING.rst -include CHANGELOG.rst include LICENSE -include README.rst +include README.md recursive-include tests * recursive-exclude * __pycache__ recursive-exclude * *.py[co] -recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif *.js *.svg +recursive-include docs conf.py Makefile make.bat *.rst *.md *.jpg *.png *.gif *.js *.svg diff --git a/setup.py b/setup.py index fa3e0ee2..9840a1dd 100644 --- a/setup.py +++ b/setup.py @@ -2,11 +2,11 @@ from setuptools import find_packages, setup -with open("README.md") as readme_file: +with open("README.md", encoding="utf-8") as readme_file: readme = readme_file.read() version = {} -with open("hcloud/__version__.py") as fp: +with open("hcloud/__version__.py", encoding="utf-8") as fp: exec(fp.read(), version) setup( @@ -57,6 +57,6 @@ ], }, include_package_data=True, - packages=find_packages(exclude=["examples", "tests*", "docs"]), + packages=find_packages(), zip_safe=False, ) From bb34df9390030e70f39bb82c92f4040eef18eb3b Mon Sep 17 00:00:00 2001 From: Jonas L Date: Tue, 1 Aug 2023 14:30:38 +0200 Subject: [PATCH 077/406] fix: ineffective doc strings (#266) --- hcloud/certificates/client.py | 8 ++++---- hcloud/ssh_keys/client.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/hcloud/certificates/client.py b/hcloud/certificates/client.py index 966d9c55..08a2b9ce 100644 --- a/hcloud/certificates/client.py +++ b/hcloud/certificates/client.py @@ -269,15 +269,15 @@ def update( return BoundCertificate(self, response["certificate"]) def delete(self, certificate: Certificate | BoundCertificate) -> bool: - self._client.request( - url=f"/certificates/{certificate.id}", - method="DELETE", - ) """Deletes a certificate. :param certificate: :class:`BoundCertificate ` or :class:`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 diff --git a/hcloud/ssh_keys/client.py b/hcloud/ssh_keys/client.py index 8ccc6f60..797619f1 100644 --- a/hcloud/ssh_keys/client.py +++ b/hcloud/ssh_keys/client.py @@ -183,11 +183,11 @@ def update( return BoundSSHKey(self, response["ssh_key"]) def delete(self, ssh_key: SSHKey | BoundSSHKey) -> bool: - self._client.request(url=f"/ssh_keys/{ssh_key.id}", method="DELETE") """Deletes an SSH key. It cannot be used anymore. :param ssh_key: :class:`BoundSSHKey ` or :class:`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 From ee79851dbf00e50d7f6b398fd4323f3e14831831 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Tue, 1 Aug 2023 14:31:08 +0200 Subject: [PATCH 078/406] docs: update hetzner logo (#264) --- Makefile | 2 +- docs/_static/custom.css | 8 ++++++++ docs/_static/logo-hetzner-online.svg | 7 ------- docs/_static/logo-hetzner.svg | 28 ++++++++++++++++++++++++++++ docs/conf.py | 4 ++-- 5 files changed, 39 insertions(+), 10 deletions(-) delete mode 100644 docs/_static/logo-hetzner-online.svg create mode 100644 docs/_static/logo-hetzner.svg diff --git a/Makefile b/Makefile index a1cb1a3b..338c547f 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ docs: venv docs-dev: venv docs venv/bin/watchmedo shell-command \ - --patterns="*.py;*.rst;*.md" \ + --patterns="*.py;*.rst;*.md;*.css" \ --ignore-pattern=".git/*" \ --recursive \ --drop \ diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 8291c26a..339b9cce 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -2,3 +2,11 @@ /* Version in Nav is off-white by default, but we restyle the header to have an off-white bg */ color: #404040; } + +.wy-side-nav-search input[type="text"] { + border-color: #404040; +} + +.logo { + margin: 1rem !important; +} diff --git a/docs/_static/logo-hetzner-online.svg b/docs/_static/logo-hetzner-online.svg deleted file mode 100644 index 3c99ccd6..00000000 --- a/docs/_static/logo-hetzner-online.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/docs/_static/logo-hetzner.svg b/docs/_static/logo-hetzner.svg new file mode 100644 index 00000000..95c2e8cc --- /dev/null +++ b/docs/_static/logo-hetzner.svg @@ -0,0 +1,28 @@ + + + + + Element 1 + + + + + + diff --git a/docs/conf.py b/docs/conf.py index 4da293ea..b3be9a7f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,13 +48,13 @@ html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] -html_logo = "_static/logo-hetzner-online.svg" +html_logo = "_static/logo-hetzner.svg" html_favicon = "_static/favicon.png" # Theme options are theme-specific and customize the look and feel of a theme further. # For a list of options available for each theme, see the documentation. html_theme_options = { "logo_only": True, - "style_nav_header_background": "#efefef", + "style_nav_header_background": "#fff", } html_css_files = [ "custom.css", From bc8316f4e135c2726cedc77a6b19790d6b9c1012 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Tue, 1 Aug 2023 14:31:27 +0200 Subject: [PATCH 079/406] refactor: use f-string (#267) --- hcloud/certificates/client.py | 8 +--- hcloud/firewalls/client.py | 12 ++---- hcloud/floating_ips/client.py | 20 +++------ hcloud/images/client.py | 4 +- hcloud/load_balancer_types/client.py | 4 +- hcloud/load_balancers/client.py | 64 +++++++--------------------- hcloud/networks/client.py | 24 +++-------- hcloud/placement_groups/client.py | 8 +--- hcloud/primary_ips/client.py | 16 ++----- hcloud/servers/client.py | 52 ++++++---------------- hcloud/volumes/client.py | 4 +- 11 files changed, 54 insertions(+), 162 deletions(-) diff --git a/hcloud/certificates/client.py b/hcloud/certificates/client.py index 08a2b9ce..4478e15b 100644 --- a/hcloud/certificates/client.py +++ b/hcloud/certificates/client.py @@ -313,9 +313,7 @@ def get_actions_list( 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, ) @@ -352,9 +350,7 @@ def retry_issuance( :return: :class:`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"]) diff --git a/hcloud/firewalls/client.py b/hcloud/firewalls/client.py index 764cc0cd..749d6d1d 100644 --- a/hcloud/firewalls/client.py +++ b/hcloud/firewalls/client.py @@ -397,9 +397,7 @@ def set_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, ) @@ -420,9 +418,7 @@ def apply_to_resources( 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, ) @@ -443,9 +439,7 @@ def remove_from_resources( 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, ) diff --git a/hcloud/floating_ips/client.py b/hcloud/floating_ips/client.py index e994fc84..0c2aa967 100644 --- a/hcloud/floating_ips/client.py +++ b/hcloud/floating_ips/client.py @@ -173,9 +173,7 @@ def get_actions_list( 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, ) @@ -386,9 +384,7 @@ def change_protection( 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, ) @@ -407,9 +403,7 @@ def assign( :return: :class:`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}, ) @@ -422,9 +416,7 @@ def unassign(self, floating_ip: FloatingIP | BoundFloatingIP) -> BoundAction: :return: :class:`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"]) @@ -445,9 +437,7 @@ def change_dns_ptr( :return: :class:`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}, ) diff --git a/hcloud/images/client.py b/hcloud/images/client.py index 4bf724d0..40986729 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -371,9 +371,7 @@ def change_protection( 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, ) diff --git a/hcloud/load_balancer_types/client.py b/hcloud/load_balancer_types/client.py index 093b03fe..6387e628 100644 --- a/hcloud/load_balancer_types/client.py +++ b/hcloud/load_balancer_types/client.py @@ -30,9 +30,7 @@ def get_by_id(self, id: int) -> BoundLoadBalancerType: :return: :class:`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"]) diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index 9188f47e..648832e0 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -513,9 +513,7 @@ def update( 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, ) @@ -528,9 +526,7 @@ def delete(self, load_balancer: LoadBalancer | BoundLoadBalancer) -> bool: :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 @@ -567,9 +563,7 @@ def get_actions_list( 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, ) @@ -611,9 +605,7 @@ def add_service( data = self.get_service_parameters(service) 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, ) @@ -698,9 +690,7 @@ def update_service( """ data = self.get_service_parameters(service) 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, ) @@ -721,9 +711,7 @@ def delete_service( 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, ) @@ -753,9 +741,7 @@ def add_target( data["ip"] = {"ip": target.ip.ip} 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, ) @@ -782,9 +768,7 @@ def remove_target( data["ip"] = {"ip": target.ip.ip} 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, ) @@ -805,9 +789,7 @@ def change_algorithm( 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, ) @@ -829,9 +811,7 @@ def change_dns_ptr( """ 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}, ) @@ -854,9 +834,7 @@ def change_protection( 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, ) @@ -881,9 +859,7 @@ def attach_to_network( 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, ) @@ -902,9 +878,7 @@ def detach_from_network( """ 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, ) @@ -922,9 +896,7 @@ def enable_public_interface( """ 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"]) @@ -941,9 +913,7 @@ def disable_public_interface( """ 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"]) @@ -962,9 +932,7 @@ def change_type( """ 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, ) diff --git a/hcloud/networks/client.py b/hcloud/networks/client.py index afa98990..c3b8ece9 100644 --- a/hcloud/networks/client.py +++ b/hcloud/networks/client.py @@ -420,9 +420,7 @@ def add_subnet( 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, ) @@ -443,9 +441,7 @@ def delete_subnet( 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, ) @@ -469,9 +465,7 @@ def add_route( } 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, ) @@ -495,9 +489,7 @@ def delete_route( } 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, ) @@ -518,9 +510,7 @@ def change_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, ) @@ -543,9 +533,7 @@ def change_protection( 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, ) diff --git a/hcloud/placement_groups/client.py b/hcloud/placement_groups/client.py index 80f3d39d..517fba55 100644 --- a/hcloud/placement_groups/client.py +++ b/hcloud/placement_groups/client.py @@ -190,9 +190,7 @@ def update( 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, ) @@ -205,9 +203,7 @@ def delete(self, placement_group: PlacementGroup | BoundPlacementGroup) -> bool: :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 diff --git a/hcloud/primary_ips/client.py b/hcloud/primary_ips/client.py index 8b676114..cdb26568 100644 --- a/hcloud/primary_ips/client.py +++ b/hcloud/primary_ips/client.py @@ -285,9 +285,7 @@ def change_protection( 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, ) @@ -309,9 +307,7 @@ def assign( :return: :class:`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}, ) @@ -324,9 +320,7 @@ def unassign(self, primary_ip: PrimaryIP | BoundPrimaryIP) -> BoundAction: :return: :class:`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"]) @@ -347,9 +341,7 @@ def change_dns_ptr( :return: :class:`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}, ) diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index f3ba4ed2..2f9ea69b 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -787,9 +787,7 @@ def reset_password(self, server: Server | BoundServer) -> ResetPasswordResponse: :return: :class:`ResetPasswordResponse ` """ response = self._client.request( - url="/servers/{server_id}/actions/reset_password".format( - server_id=server.id - ), + url=f"/servers/{server.id}/actions/reset_password", method="POST", ) return ResetPasswordResponse( @@ -844,9 +842,7 @@ def enable_rescue( data.update({"ssh_keys": ssh_keys}) response = self._client.request( - url="/servers/{server_id}/actions/enable_rescue".format( - server_id=server.id - ), + url=f"/servers/{server.id}/actions/enable_rescue", method="POST", json=data, ) @@ -862,9 +858,7 @@ def disable_rescue(self, server: Server | BoundServer) -> BoundAction: :return: :class:`BoundAction ` """ response = self._client.request( - url="/servers/{server_id}/actions/disable_rescue".format( - server_id=server.id - ), + url=f"/servers/{server.id}/actions/disable_rescue", method="POST", ) return BoundAction(self._client.actions, response["action"]) @@ -934,9 +928,7 @@ def enable_backup(self, server: Server | BoundServer) -> BoundAction: :return: :class:`BoundAction ` """ response = self._client.request( - url="/servers/{server_id}/actions/enable_backup".format( - server_id=server.id - ), + url=f"/servers/{server.id}/actions/enable_backup", method="POST", ) return BoundAction(self._client.actions, response["action"]) @@ -948,9 +940,7 @@ def disable_backup(self, server: Server | BoundServer) -> BoundAction: :return: :class:`BoundAction ` """ response = self._client.request( - url="/servers/{server_id}/actions/disable_backup".format( - server_id=server.id - ), + url=f"/servers/{server.id}/actions/disable_backup", method="POST", ) return BoundAction(self._client.actions, response["action"]) @@ -1003,9 +993,7 @@ def change_dns_ptr( """ data: dict[str, Any] = {"ip": ip, "dns_ptr": dns_ptr} response = self._client.request( - url="/servers/{server_id}/actions/change_dns_ptr".format( - server_id=server.id - ), + url=f"/servers/{server.id}/actions/change_dns_ptr", method="POST", json=data, ) @@ -1033,9 +1021,7 @@ def change_protection( data.update({"rebuild": rebuild}) response = self._client.request( - url="/servers/{server_id}/actions/change_protection".format( - server_id=server.id - ), + url=f"/servers/{server.id}/actions/change_protection", method="POST", json=data, ) @@ -1048,9 +1034,7 @@ def request_console(self, server: Server | BoundServer) -> RequestConsoleRespons :return: :class:`RequestConsoleResponse ` """ response = self._client.request( - url="/servers/{server_id}/actions/request_console".format( - server_id=server.id - ), + url=f"/servers/{server.id}/actions/request_console", method="POST", ) return RequestConsoleResponse( @@ -1082,9 +1066,7 @@ def attach_to_network( if alias_ips is not None: data.update({"alias_ips": alias_ips}) response = self._client.request( - url="/servers/{server_id}/actions/attach_to_network".format( - server_id=server.id - ), + url=f"/servers/{server.id}/actions/attach_to_network", method="POST", json=data, ) @@ -1103,9 +1085,7 @@ def detach_from_network( """ data: dict[str, Any] = {"network": network.id} response = self._client.request( - url="/servers/{server_id}/actions/detach_from_network".format( - server_id=server.id - ), + url=f"/servers/{server.id}/actions/detach_from_network", method="POST", json=data, ) @@ -1127,9 +1107,7 @@ def change_alias_ips( """ data: dict[str, Any] = {"network": network.id, "alias_ips": alias_ips} response = self._client.request( - url="/servers/{server_id}/actions/change_alias_ips".format( - server_id=server.id - ), + url=f"/servers/{server.id}/actions/change_alias_ips", method="POST", json=data, ) @@ -1148,9 +1126,7 @@ def add_to_placement_group( """ data: dict[str, Any] = {"placement_group": str(placement_group.id)} response = self._client.request( - url="/servers/{server_id}/actions/add_to_placement_group".format( - server_id=server.id - ), + url=f"/servers/{server.id}/actions/add_to_placement_group", method="POST", json=data, ) @@ -1163,9 +1139,7 @@ def remove_from_placement_group(self, server: Server | BoundServer) -> BoundActi :return: :class:`BoundAction ` """ response = self._client.request( - url="/servers/{server_id}/actions/remove_from_placement_group".format( - server_id=server.id - ), + url=f"/servers/{server.id}/actions/remove_from_placement_group", method="POST", ) return BoundAction(self._client.actions, response["action"]) diff --git a/hcloud/volumes/client.py b/hcloud/volumes/client.py index 2ebda12b..a00a78bb 100644 --- a/hcloud/volumes/client.py +++ b/hcloud/volumes/client.py @@ -431,9 +431,7 @@ def change_protection( 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, ) From da8baa551628fb759c790871362fef1e3666c56b Mon Sep 17 00:00:00 2001 From: Jonas L Date: Tue, 1 Aug 2023 14:34:42 +0200 Subject: [PATCH 080/406] feat: the package is now typed (#265) See https://peps.python.org/pep-0561/ --- MANIFEST.in | 2 ++ hcloud/py.typed | 1 + 2 files changed, 3 insertions(+) create mode 100644 hcloud/py.typed diff --git a/MANIFEST.in b/MANIFEST.in index 6e1ce0d8..f3658c7c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,6 +3,8 @@ include CONTRIBUTING.rst include LICENSE include README.md +include hcloud/py.typed + recursive-include tests * recursive-exclude * __pycache__ recursive-exclude * *.py[co] diff --git a/hcloud/py.typed b/hcloud/py.typed new file mode 100644 index 00000000..1242d432 --- /dev/null +++ b/hcloud/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. From 2239b0bc9beae457215c6514b0b823cc84a4a463 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Aug 2023 14:14:18 +0200 Subject: [PATCH 081/406] deps: update pre-commit hook pre-commit/mirrors-prettier to v3.0.1 (#269) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dd91eb42..e21721ae 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.0 + rev: v3.0.1 hooks: - id: prettier files: \.(md|ya?ml|js|css)$ From ec7c4d2d9aac6b192fb997f66c6b1787c77b4204 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 4 Aug 2023 08:42:22 +0200 Subject: [PATCH 082/406] refactor: do not use _ as functional variable name (#268) --- hcloud/firewalls/client.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/hcloud/firewalls/client.py b/hcloud/firewalls/client.py index 749d6d1d..bf3cb526 100644 --- a/hcloud/firewalls/client.py +++ b/hcloud/firewalls/client.py @@ -333,7 +333,8 @@ def create( 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( @@ -401,7 +402,10 @@ def 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, @@ -422,7 +426,10 @@ def 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, @@ -443,4 +450,7 @@ def 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"] + ] From 48561a650b490addd7587d5deeaac4ffc5114413 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 4 Aug 2023 17:39:37 +0200 Subject: [PATCH 083/406] refactor: improve type hints (#273) * refactor: add type hints * refactor: create to_payload for load_balancer domains * chore: enable mypy disallow_untyped_defs --- hcloud/_client.py | 7 +- hcloud/core/client.py | 13 +- hcloud/core/domain.py | 2 +- hcloud/firewalls/domain.py | 36 ++--- hcloud/floating_ips/domain.py | 26 ++-- hcloud/isos/domain.py | 12 +- hcloud/load_balancer_types/domain.py | 16 +- hcloud/load_balancers/client.py | 117 ++------------ hcloud/load_balancers/domain.py | 188 +++++++++++++++++------ hcloud/locations/domain.py | 16 +- hcloud/networks/client.py | 4 +- hcloud/networks/domain.py | 33 ++-- hcloud/placement_groups/domain.py | 12 +- hcloud/primary_ips/domain.py | 27 ++-- hcloud/server_types/domain.py | 26 ++-- hcloud/servers/client.py | 8 +- hcloud/servers/domain.py | 48 +++--- hcloud/ssh_keys/domain.py | 12 +- pyproject.toml | 3 + tests/unit/load_balancers/test_client.py | 8 +- 20 files changed, 312 insertions(+), 302 deletions(-) diff --git a/hcloud/_client.py b/hcloud/_client.py index a0969fef..b64a1b71 100644 --- a/hcloud/_client.py +++ b/hcloud/_client.py @@ -1,6 +1,7 @@ from __future__ import annotations import time +from typing import NoReturn import requests @@ -167,21 +168,21 @@ def _get_headers(self) -> dict: } return headers - def _raise_exception_from_response(self, response: requests.Response): + def _raise_exception_from_response(self, response: requests.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, diff --git a/hcloud/core/client.py b/hcloud/core/client.py index 13d11e0f..22b0f0ba 100644 --- a/hcloud/core/client.py +++ b/hcloud/core/client.py @@ -19,7 +19,7 @@ def __init__(self, client: Client): """ self._client = client - def _get_all( + def _get_all( # type: ignore[no-untyped-def] self, list_function: Callable, *args, @@ -49,11 +49,11 @@ def _get_all( return results - def get_all(self, *args, **kwargs) -> list: + def get_all(self, *args, **kwargs) -> list: # type: ignore[no-untyped-def] assert hasattr(self, "get_list") return self._get_all(self.get_list, *args, **kwargs) - def get_actions(self, *args, **kwargs) -> list[BoundAction]: + def get_actions(self, *args, **kwargs) -> list[BoundAction]: # type: ignore[no-untyped-def] if not hasattr(self, "get_actions_list"): raise ValueError("this endpoint does not support get_actions method") @@ -65,7 +65,7 @@ class GetEntityByNameMixin: Use as a mixin for ClientEntityBase classes """ - def get_by_name(self, name: str): + def get_by_name(self, name: str): # type: ignore[no-untyped-def] assert hasattr(self, "get_list") entities, _ = self.get_list(name=name) return entities[0] if entities else None @@ -94,7 +94,7 @@ def __init__( self.complete = complete self.data_model = self.model.from_dict(data) - def __getattr__(self, name: str): + def __getattr__(self, name: str): # type: ignore[no-untyped-def] """Allow magical access to the properties of the model :param name: str :return: @@ -105,8 +105,9 @@ def __getattr__(self, name: str): 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 diff --git a/hcloud/core/domain.py b/hcloud/core/domain.py index 56229703..21aed341 100644 --- a/hcloud/core/domain.py +++ b/hcloud/core/domain.py @@ -5,7 +5,7 @@ class BaseDomain: __slots__ = () @classmethod - def from_dict(cls, data: dict): + 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) diff --git a/hcloud/firewalls/domain.py b/hcloud/firewalls/domain.py index 192e6491..f9c7368d 100644 --- a/hcloud/firewalls/domain.py +++ b/hcloud/firewalls/domain.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from dateutil.parser import isoparse @@ -33,12 +33,12 @@ class Firewall(BaseDomain): def __init__( self, - id=None, - name=None, - labels=None, - rules=None, - applied_to=None, - created=None, + 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 @@ -107,18 +107,18 @@ def __init__( 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 @@ -150,15 +150,13 @@ def __init__( 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 @@ -168,7 +166,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 diff --git a/hcloud/floating_ips/domain.py b/hcloud/floating_ips/domain.py index 22cce81f..78d8e5af 100644 --- a/hcloud/floating_ips/domain.py +++ b/hcloud/floating_ips/domain.py @@ -8,6 +8,8 @@ if TYPE_CHECKING: from ..actions import BoundAction + from ..locations import BoundLocation + from ..servers import BoundServer from .client import BoundFloatingIP @@ -57,18 +59,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 diff --git a/hcloud/isos/domain.py b/hcloud/isos/domain.py index daba7c38..1578be17 100644 --- a/hcloud/isos/domain.py +++ b/hcloud/isos/domain.py @@ -26,12 +26,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 diff --git a/hcloud/load_balancer_types/domain.py b/hcloud/load_balancer_types/domain.py index 8fa56b2c..35719dba 100644 --- a/hcloud/load_balancer_types/domain.py +++ b/hcloud/load_balancer_types/domain.py @@ -38,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 diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index 648832e0..b0c68770 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -456,30 +456,9 @@ def create( 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: @@ -602,7 +581,7 @@ def add_service( The LoadBalancerService you want to add to the Load Balancer :return: :class:`BoundAction ` """ - data = self.get_service_parameters(service) + data: dict[str, Any] = service.to_payload() response = self._client.request( url=f"/load_balancers/{load_balancer.id}/actions/add_service", @@ -611,71 +590,6 @@ def add_service( ) return BoundAction(self._client.actions, response["action"]) - def get_service_parameters(self, service: LoadBalancerService) -> dict[str, Any]: - data: dict[str, Any] = {} - 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: LoadBalancer | BoundLoadBalancer, @@ -688,7 +602,7 @@ def update_service( The LoadBalancerService with updated values within for the Load Balancer :return: :class:`BoundAction ` """ - data = self.get_service_parameters(service) + data: dict[str, Any] = service.to_payload() response = self._client.request( url=f"/load_balancers/{load_balancer.id}/actions/update_service", method="POST", @@ -729,16 +643,7 @@ def add_target( The LoadBalancerTarget you want to add to the Load Balancer :return: :class:`BoundAction ` """ - data: dict[str, Any] = { - "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=f"/load_balancers/{load_balancer.id}/actions/add_target", @@ -759,13 +664,9 @@ def remove_target( The LoadBalancerTarget you want to remove from the Load Balancer :return: :class:`BoundAction ` """ - data: dict[str, Any] = {"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=f"/load_balancers/{load_balancer.id}/actions/remove_target", @@ -845,7 +746,7 @@ def attach_to_network( load_balancer: LoadBalancer | BoundLoadBalancer, network: Network | BoundNetwork, ip: str | None = None, - ): + ) -> BoundAction: """Attach a Load Balancer to a Network. :param load_balancer: :class:` ` or :class:`LoadBalancer ` diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index aeff1d23..c22aa27b 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from dateutil.parser import isoparse @@ -8,7 +8,11 @@ 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 @@ -67,21 +71,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 @@ -119,12 +123,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 @@ -133,6 +137,74 @@ def __init__( 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 @@ -151,11 +223,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 @@ -183,12 +255,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 @@ -215,11 +287,11 @@ class LoadBalancerHealtCheckHttp(BaseDomain): def __init__( self, - domain=None, - path=None, - response=None, - status_codes=None, - tls=None, + 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 @@ -245,11 +317,11 @@ class LoadBalancerTarget(BaseDomain): def __init__( self, - type=None, - server=None, - label_selector=None, - ip=None, - use_private_ip=None, + 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 @@ -257,6 +329,30 @@ def __init__( 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 @@ -264,7 +360,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 @@ -274,7 +370,7 @@ class LoadBalancerTargetIP(BaseDomain): :param ip: str Target IP """ - def __init__(self, ip=None): + def __init__(self, ip: str | None = None): self.ip = ip @@ -285,7 +381,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 diff --git a/hcloud/locations/domain.py b/hcloud/locations/domain.py index 23358dbb..7d4af223 100644 --- a/hcloud/locations/domain.py +++ b/hcloud/locations/domain.py @@ -37,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 diff --git a/hcloud/networks/client.py b/hcloud/networks/client.py index c3b8ece9..4b049e20 100644 --- a/hcloud/networks/client.py +++ b/hcloud/networks/client.py @@ -245,7 +245,7 @@ def create( 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 @@ -267,7 +267,7 @@ def create( 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, diff --git a/hcloud/networks/domain.py b/hcloud/networks/domain.py index b6ed9c9e..725cedad 100644 --- a/hcloud/networks/domain.py +++ b/hcloud/networks/domain.py @@ -8,6 +8,7 @@ if TYPE_CHECKING: from ..actions import BoundAction + from ..servers import BoundServer from .client import BoundNetwork @@ -49,16 +50,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 @@ -97,11 +98,11 @@ class NetworkSubnet(BaseDomain): def __init__( self, - ip_range, - type=None, - network_zone=None, - gateway=None, - vswitch_id=None, + 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 @@ -121,7 +122,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 diff --git a/hcloud/placement_groups/domain.py b/hcloud/placement_groups/domain.py index 14a15e12..b2dc27ca 100644 --- a/hcloud/placement_groups/domain.py +++ b/hcloud/placement_groups/domain.py @@ -37,12 +37,12 @@ class PlacementGroup(BaseDomain): def __init__( self, - id=None, - name=None, - labels=None, - servers=None, - type=None, - created=None, + 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 diff --git a/hcloud/primary_ips/domain.py b/hcloud/primary_ips/domain.py index 8a1f22b3..a499799f 100644 --- a/hcloud/primary_ips/domain.py +++ b/hcloud/primary_ips/domain.py @@ -8,6 +8,7 @@ if TYPE_CHECKING: from ..actions import BoundAction + from ..datacenters import BoundDatacenter from .client import BoundPrimaryIP @@ -60,19 +61,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 diff --git a/hcloud/server_types/domain.py b/hcloud/server_types/domain.py index 2f8ff5fc..ab2553b1 100644 --- a/hcloud/server_types/domain.py +++ b/hcloud/server_types/domain.py @@ -54,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 diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index 2f9ea69b..0108309e 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -602,15 +602,15 @@ def create( data["placement_group"] = placement_group.id if public_net is not None: - pn = { + data_public_net: dict[str, Any] = { "enable_ipv4": public_net.enable_ipv4, "enable_ipv6": public_net.enable_ipv6, } if public_net.ipv4 is not None: - pn.update({"ipv4": public_net.ipv4.id}) + data_public_net["ipv4"] = public_net.ipv4.id if public_net.ipv6 is not None: - pn.update({"ipv6": public_net.ipv6.id}) - data["public_net"] = pn + data_public_net["ipv6"] = public_net.ipv6.id + data["public_net"] = data_public_net response = self._client.request(url="/servers", method="POST", json=data) diff --git a/hcloud/servers/domain.py b/hcloud/servers/domain.py index f4c3ac93..60cc7450 100644 --- a/hcloud/servers/domain.py +++ b/hcloud/servers/domain.py @@ -8,10 +8,16 @@ 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 @@ -100,27 +106,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 diff --git a/hcloud/ssh_keys/domain.py b/hcloud/ssh_keys/domain.py index 60ead2b7..09c98cae 100644 --- a/hcloud/ssh_keys/domain.py +++ b/hcloud/ssh_keys/domain.py @@ -26,12 +26,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 diff --git a/pyproject.toml b/pyproject.toml index a2a4e7a3..beb8fd80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,9 @@ profile = "black" combine_as_imports = true add_imports = ["from __future__ import annotations"] +[tool.mypy] +disallow_untyped_defs = true + [tool.coverage.run] source = ["hcloud"] diff --git a/tests/unit/load_balancers/test_client.py b/tests/unit/load_balancers/test_client.py index 9c9d9071..c8702046 100644 --- a/tests/unit/load_balancers/test_client.py +++ b/tests/unit/load_balancers/test_client.py @@ -133,7 +133,7 @@ def test_delete_service( LoadBalancerTarget( type="server", server=Server(id=1), use_private_ip=True ), - {"server": {"id": 1}}, + {"server": {"id": 1}, "use_private_ip": True}, ), ( LoadBalancerTarget(type="ip", ip=LoadBalancerTargetIP(ip="127.0.0.1")), @@ -153,9 +153,9 @@ def test_add_target( ): hetzner_client.request.return_value = response_add_target action = bound_load_balancer.add_target(target) - params.update({"type": target.type, "use_private_ip": target.use_private_ip}) + params.update({"type": target.type}) hetzner_client.request.assert_called_with( - json=params, url="/load_balancers/14/actions/add_target", method="POST" + url="/load_balancers/14/actions/add_target", method="POST", json=params ) assert action.id == 13 @@ -196,7 +196,7 @@ def test_remove_target( action = bound_load_balancer.remove_target(target) params.update({"type": target.type}) hetzner_client.request.assert_called_with( - json=params, url="/load_balancers/14/actions/remove_target", method="POST" + url="/load_balancers/14/actions/remove_target", method="POST", json=params ) assert action.id == 13 From e8ec3a319a57ba5ad789a353b9e11c774ad97c24 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 7 Aug 2023 13:01:48 +0200 Subject: [PATCH 084/406] c: fix gitlab lint job (#275) --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f83f1544..bfb355c5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,6 +12,7 @@ lint: image: python:3.11-alpine before_script: + - apk add make - make venv script: - make lint From 4375dc6ec351207380a011ec35e1397bf2bd17e9 Mon Sep 17 00:00:00 2001 From: 83hd4d <119742728+83hd4d@users.noreply.github.com> Date: Mon, 7 Aug 2023 14:39:11 +0330 Subject: [PATCH 085/406] fix: allow omitting `datacenter` when creating a primary ip (#171) * omitting "datacenter" if "assignee_id" is passed * fix: allow omitting the datacenter value when creating a primary IP --------- Co-authored-by: jo --- hcloud/primary_ips/client.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/hcloud/primary_ips/client.py b/hcloud/primary_ips/client.py index cdb26568..05c4e60a 100644 --- a/hcloud/primary_ips/client.py +++ b/hcloud/primary_ips/client.py @@ -178,7 +178,8 @@ def get_by_name(self, name: str) -> BoundPrimaryIP | None: def create( self, type: str, - datacenter: Datacenter | BoundDatacenter, + # TODO: Make the datacenter argument optional + datacenter: Datacenter | BoundDatacenter | None, name: str, assignee_type: str | None = "server", assignee_id: int | None = None, @@ -203,10 +204,11 @@ def create( "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 From 07a663fd8628d305a7461a90a94c61a97c12421b Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 7 Aug 2023 16:13:38 +0200 Subject: [PATCH 086/406] feat: add global request timeout option (#271) * chore: improve code * feat: add global requests timeout (default is 15s) * fix: default is no timeout * docs: add param to request method --- hcloud/_client.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/hcloud/_client.py b/hcloud/_client.py index b64a1b71..db28fcd6 100644 --- a/hcloud/_client.py +++ b/hcloud/_client.py @@ -40,6 +40,7 @@ def __init__( 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 @@ -48,12 +49,14 @@ def __init__( :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) @@ -194,12 +197,16 @@ def request( # type: ignore[no-untyped-def] :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, ) @@ -217,8 +224,8 @@ def request( # type: ignore[no-untyped-def] 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) From e8a2e1b0ecaf8df64fb9a067f8219ff234d77a02 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Tue, 8 Aug 2023 13:36:43 +0200 Subject: [PATCH 087/406] c: fix gitlab pipeline (#278) --- .gitlab-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bfb355c5..1f3158fe 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,7 +12,7 @@ lint: image: python:3.11-alpine before_script: - - apk add make + - apk add make bash - make venv script: - make lint @@ -26,6 +26,7 @@ test: image: python:${python_version}-alpine before_script: + - apk add make - pip install tox script: - tox -e ${python_version} From 7da2397364237e630a80c53c4254c8d9ab773993 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Tue, 8 Aug 2023 13:37:38 +0200 Subject: [PATCH 088/406] refactor: simplify client class inheritance complexity (#272) - Instead of extensively overriding a parent method, use a private helper method. - Prevent having get_actions methods for clients that do not support it. - Make sure we document all the clients methods by not forgetting to override a parent method. - Do not implement inheritance with different method signature. * refactor: rename ClientEntityBase _get_all to _iter_pages * refactor: move GetEntityByNameMixin.get_by_name to ClientEntityBase._get_first_by Remove GetEntityByNameMixin and use _get_first_by method. * refactor: remove ClientEntityBase get_* methods * fix: always pass args before kwargs in _iter_pages * refactor: reuse _get_first_by for all get_by_* methods --- hcloud/actions/client.py | 2 +- hcloud/certificates/client.py | 15 ++++-- hcloud/core/__init__.py | 2 +- hcloud/core/client.py | 32 ++----------- hcloud/datacenters/client.py | 8 ++-- hcloud/firewalls/client.py | 20 ++++++-- hcloud/floating_ips/client.py | 15 ++++-- hcloud/images/client.py | 20 ++++---- hcloud/isos/client.py | 9 ++-- hcloud/load_balancer_types/client.py | 8 ++-- hcloud/load_balancers/client.py | 15 ++++-- hcloud/locations/client.py | 8 ++-- hcloud/networks/client.py | 15 ++++-- hcloud/placement_groups/client.py | 13 ++++-- hcloud/primary_ips/client.py | 8 ++-- hcloud/server_types/client.py | 8 ++-- hcloud/servers/client.py | 20 ++++++-- hcloud/ssh_keys/client.py | 17 +++---- hcloud/volumes/client.py | 19 ++++++-- tests/unit/core/test_client.py | 68 +++++++--------------------- 20 files changed, 162 insertions(+), 160 deletions(-) diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index 7414726f..3105b3c5 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -101,4 +101,4 @@ def get_all( 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 `] """ - return super().get_all(status=status, sort=sort) + return self._iter_pages(self.get_list, status=status, sort=sort) diff --git a/hcloud/certificates/client.py b/hcloud/certificates/client.py index 4478e15b..1067dfa8 100644 --- a/hcloud/certificates/client.py +++ b/hcloud/certificates/client.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction -from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import ( Certificate, CreateManagedCertificateResponse, @@ -103,7 +103,7 @@ class CertificatesPageResult(NamedTuple): meta: Meta | None -class CertificatesClient(ClientEntityBase, GetEntityByNameMixin): +class CertificatesClient(ClientEntityBase): _client: Client def get_by_id(self, id: int) -> BoundCertificate: @@ -171,7 +171,7 @@ def get_all( Can be used to filter certificates by labels. The response will only contain certificates matching the label selector. :return: List[:class:`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: str) -> BoundCertificate | None: """Get certificate by name @@ -180,7 +180,7 @@ def get_by_name(self, name: str) -> BoundCertificate | None: Used to get certificate by name. :return: :class:`BoundCertificate ` """ - return super().get_by_name(name) + return self._get_first_by(name=name) def create( self, @@ -338,7 +338,12 @@ def get_actions( 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 `] """ - 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, diff --git a/hcloud/core/__init__.py b/hcloud/core/__init__.py index 2e279f75..4e17dac9 100644 --- a/hcloud/core/__init__.py +++ b/hcloud/core/__init__.py @@ -1,4 +1,4 @@ from __future__ import annotations -from .client import BoundModelBase, ClientEntityBase, GetEntityByNameMixin # noqa: F401 +from .client import BoundModelBase, ClientEntityBase # noqa: F401 from .domain import BaseDomain, DomainIdentityMixin, Meta, Pagination # noqa: F401 diff --git a/hcloud/core/client.py b/hcloud/core/client.py index 22b0f0ba..16c37e5d 100644 --- a/hcloud/core/client.py +++ b/hcloud/core/client.py @@ -4,7 +4,6 @@ if TYPE_CHECKING: from .._client import Client - from ..actions import BoundAction class ClientEntityBase: @@ -19,7 +18,7 @@ def __init__(self, client: Client): """ self._client = client - def _get_all( # type: ignore[no-untyped-def] + def _iter_pages( # type: ignore[no-untyped-def] self, list_function: Callable, *args, @@ -32,42 +31,21 @@ def _get_all( # type: ignore[no-untyped-def] # The *PageResult tuples MUST have the following structure # `(result: List[Bound*], meta: Meta)` result, meta = list_function( - page=page, per_page=self.max_per_page, *args, **kwargs + *args, page=page, per_page=self.max_per_page, **kwargs ) if result: results.extend(result) - 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 = 0 return results - def get_all(self, *args, **kwargs) -> list: # type: ignore[no-untyped-def] + def _get_first_by(self, **kwargs): # type: ignore[no-untyped-def] assert hasattr(self, "get_list") - return self._get_all(self.get_list, *args, **kwargs) - - def get_actions(self, *args, **kwargs) -> list[BoundAction]: # type: ignore[no-untyped-def] - 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, *args, **kwargs) - - -class GetEntityByNameMixin: - """ - Use as a mixin for ClientEntityBase classes - """ - - def get_by_name(self, name: str): # type: ignore[no-untyped-def] - assert hasattr(self, "get_list") - entities, _ = self.get_list(name=name) + entities, _ = self.get_list(**kwargs) return entities[0] if entities else None diff --git a/hcloud/datacenters/client.py b/hcloud/datacenters/client.py index 2f874c5b..ab5aa5e0 100644 --- a/hcloud/datacenters/client.py +++ b/hcloud/datacenters/client.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple -from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..core import BoundModelBase, ClientEntityBase, Meta from ..locations import BoundLocation from ..server_types import BoundServerType from .domain import Datacenter, DatacenterServerTypes @@ -55,7 +55,7 @@ class DatacentersPageResult(NamedTuple): meta: Meta | None -class DatacentersClient(ClientEntityBase, GetEntityByNameMixin): +class DatacentersClient(ClientEntityBase): _client: Client def get_by_id(self, id: int) -> BoundDatacenter: @@ -109,7 +109,7 @@ def get_all(self, name: str | None = None) -> list[BoundDatacenter]: Can be used to filter datacenters by their name. :return: List[:class:`BoundDatacenter `] """ - return super().get_all(name=name) + return self._iter_pages(self.get_list, name=name) def get_by_name(self, name: str) -> BoundDatacenter | None: """Get datacenter by name @@ -118,4 +118,4 @@ def get_by_name(self, name: str) -> BoundDatacenter | None: Used to get datacenter by name. :return: :class:`BoundDatacenter ` """ - return super().get_by_name(name) + return self._get_first_by(name=name) diff --git a/hcloud/firewalls/client.py b/hcloud/firewalls/client.py index bf3cb526..86960808 100644 --- a/hcloud/firewalls/client.py +++ b/hcloud/firewalls/client.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction -from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import ( CreateFirewallResponse, Firewall, @@ -158,7 +158,7 @@ class FirewallsPageResult(NamedTuple): meta: Meta | None -class FirewallsClient(ClientEntityBase, GetEntityByNameMixin): +class FirewallsClient(ClientEntityBase): _client: Client def get_actions_list( @@ -218,7 +218,12 @@ def get_actions( :return: List[:class:`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: int) -> BoundFirewall: """Returns a specific Firewall object. @@ -287,7 +292,12 @@ def get_all( Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) :return: List[:class:`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: str) -> BoundFirewall | None: """Get Firewall by name @@ -296,7 +306,7 @@ def get_by_name(self, name: str) -> BoundFirewall | None: Used to get Firewall by name. :return: :class:`BoundFirewall ` """ - return super().get_by_name(name) + return self._get_first_by(name=name) def create( self, diff --git a/hcloud/floating_ips/client.py b/hcloud/floating_ips/client.py index 0c2aa967..58d7fa37 100644 --- a/hcloud/floating_ips/client.py +++ b/hcloud/floating_ips/client.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction -from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..core import BoundModelBase, ClientEntityBase, Meta from ..locations import BoundLocation from .domain import CreateFloatingIPResponse, FloatingIP @@ -139,7 +139,7 @@ class FloatingIPsPageResult(NamedTuple): meta: Meta | None -class FloatingIPsClient(ClientEntityBase, GetEntityByNameMixin): +class FloatingIPsClient(ClientEntityBase): _client: Client def get_actions_list( @@ -199,7 +199,12 @@ def get_actions( :return: List[:class:`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: int) -> BoundFloatingIP: """Returns a specific Floating IP object. @@ -263,7 +268,7 @@ def get_all( Can be used to filter networks by their name. :return: List[:class:`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: str) -> BoundFloatingIP | None: """Get Floating IP by name @@ -272,7 +277,7 @@ def get_by_name(self, name: str) -> BoundFloatingIP | None: Used to get Floating IP by name. :return: :class:`BoundFloatingIP ` """ - return super().get_by_name(name) + return self._get_first_by(name=name) def create( self, diff --git a/hcloud/images/client.py b/hcloud/images/client.py index 40986729..a7dbef76 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction -from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import Image if TYPE_CHECKING: @@ -110,7 +110,7 @@ class ImagesPageResult(NamedTuple): meta: Meta | None -class ImagesClient(ClientEntityBase, GetEntityByNameMixin): +class ImagesClient(ClientEntityBase): _client: Client def get_actions_list( @@ -169,7 +169,12 @@ def get_actions( 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 `] """ - 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: int) -> BoundImage: """Get a specific Image @@ -274,7 +279,8 @@ def get_all( Include deprecated images in the response. Default: False :return: List[:class:`BoundImage `] """ - return super().get_all( + return self._iter_pages( + self.get_list, name=name, label_selector=label_selector, bound_to=bound_to, @@ -294,7 +300,7 @@ def get_by_name(self, name: str) -> BoundImage | None: Used to get image by name. :return: :class:`BoundImage ` """ - return super().get_by_name(name) + return self._get_first_by(name=name) def get_by_name_and_architecture( self, @@ -309,9 +315,7 @@ def get_by_name_and_architecture( Used to identify the image. :return: :class:`BoundImage ` """ - entities, _ = self.get_list(name=name, architecture=[architecture]) - entity = entities[0] if entities else None - return entity + return self._get_first_by(name=name, architecture=[architecture]) def update( self, diff --git a/hcloud/isos/client.py b/hcloud/isos/client.py index c171673b..1ab5fc9f 100644 --- a/hcloud/isos/client.py +++ b/hcloud/isos/client.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple from warnings import warn -from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import Iso if TYPE_CHECKING: @@ -21,7 +21,7 @@ class IsosPageResult(NamedTuple): meta: Meta | None -class IsosClient(ClientEntityBase, GetEntityByNameMixin): +class IsosClient(ClientEntityBase): _client: Client def get_by_id(self, id: int) -> BoundIso: @@ -111,7 +111,8 @@ def get_all( ) 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, @@ -124,4 +125,4 @@ def get_by_name(self, name: str) -> BoundIso | None: Used to get iso by name. :return: :class:`BoundIso ` """ - return super().get_by_name(name) + return self._get_first_by(name=name) diff --git a/hcloud/load_balancer_types/client.py b/hcloud/load_balancer_types/client.py index 6387e628..fa91c01d 100644 --- a/hcloud/load_balancer_types/client.py +++ b/hcloud/load_balancer_types/client.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple -from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import LoadBalancerType if TYPE_CHECKING: @@ -20,7 +20,7 @@ class LoadBalancerTypesPageResult(NamedTuple): meta: Meta | None -class LoadBalancerTypesClient(ClientEntityBase, GetEntityByNameMixin): +class LoadBalancerTypesClient(ClientEntityBase): _client: Client def get_by_id(self, id: int) -> BoundLoadBalancerType: @@ -77,7 +77,7 @@ def get_all(self, name: str | None = None) -> list[BoundLoadBalancerType]: Can be used to filter Load Balancer type by their name. :return: List[:class:`BoundLoadBalancerType `] """ - return super().get_all(name=name) + return self._iter_pages(self.get_list, name=name) def get_by_name(self, name: str) -> BoundLoadBalancerType | None: """Get Load Balancer type by name @@ -86,4 +86,4 @@ def get_by_name(self, name: str) -> BoundLoadBalancerType | None: Used to get Load Balancer type by name. :return: :class:`BoundLoadBalancerType ` """ - return super().get_by_name(name) + return self._get_first_by(name=name) diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index b0c68770..e228cc21 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -4,7 +4,7 @@ from ..actions import ActionsPageResult, BoundAction from ..certificates import BoundCertificate -from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..core import BoundModelBase, ClientEntityBase, Meta from ..load_balancer_types import BoundLoadBalancerType from ..locations import BoundLocation from ..networks import BoundNetwork @@ -328,7 +328,7 @@ class LoadBalancersPageResult(NamedTuple): meta: Meta | None -class LoadBalancersClient(ClientEntityBase, GetEntityByNameMixin): +class LoadBalancersClient(ClientEntityBase): _client: Client def get_by_id(self, id: int) -> BoundLoadBalancer: @@ -395,7 +395,7 @@ def get_all( Can be used to filter Load Balancers by labels. The response will only contain Load Balancers matching the label selector. :return: List[:class:`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: str) -> BoundLoadBalancer | None: """Get Load Balancer by name @@ -404,7 +404,7 @@ def get_by_name(self, name: str) -> BoundLoadBalancer | None: Used to get Load Balancer by name. :return: :class:`BoundLoadBalancer ` """ - return super().get_by_name(name) + return self._get_first_by(name=name) def create( self, @@ -567,7 +567,12 @@ def get_actions( 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 `] """ - 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, diff --git a/hcloud/locations/client.py b/hcloud/locations/client.py index 5ad0d1f1..2e2b6b47 100644 --- a/hcloud/locations/client.py +++ b/hcloud/locations/client.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple -from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import Location if TYPE_CHECKING: @@ -20,7 +20,7 @@ class LocationsPageResult(NamedTuple): meta: Meta | None -class LocationsClient(ClientEntityBase, GetEntityByNameMixin): +class LocationsClient(ClientEntityBase): _client: Client def get_by_id(self, id: int) -> BoundLocation: @@ -70,7 +70,7 @@ def get_all(self, name: str | None = None) -> list[BoundLocation]: Can be used to filter locations by their name. :return: List[:class:`BoundLocation `] """ - return super().get_all(name=name) + return self._iter_pages(self.get_list, name=name) def get_by_name(self, name: str) -> BoundLocation | None: """Get location by name @@ -79,4 +79,4 @@ def get_by_name(self, name: str) -> BoundLocation | None: Used to get location by name. :return: :class:`BoundLocation ` """ - return super().get_by_name(name) + return self._get_first_by(name=name) diff --git a/hcloud/networks/client.py b/hcloud/networks/client.py index 4b049e20..416a0402 100644 --- a/hcloud/networks/client.py +++ b/hcloud/networks/client.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction -from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import Network, NetworkRoute, NetworkSubnet if TYPE_CHECKING: @@ -165,7 +165,7 @@ class NetworksPageResult(NamedTuple): meta: Meta | None -class NetworksClient(ClientEntityBase, GetEntityByNameMixin): +class NetworksClient(ClientEntityBase): _client: Client def get_by_id(self, id: int) -> BoundNetwork: @@ -226,7 +226,7 @@ def get_all( Can be used to filter networks by labels. The response will only contain networks matching the label selector. :return: List[:class:`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: str) -> BoundNetwork | None: """Get network by name @@ -235,7 +235,7 @@ def get_by_name(self, name: str) -> BoundNetwork | None: Used to get network by name. :return: :class:`BoundNetwork ` """ - return super().get_by_name(name) + return self._get_first_by(name=name) def create( self, @@ -396,7 +396,12 @@ def get_actions( 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 `] """ - 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, diff --git a/hcloud/placement_groups/client.py b/hcloud/placement_groups/client.py index 517fba55..b551904f 100644 --- a/hcloud/placement_groups/client.py +++ b/hcloud/placement_groups/client.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import BoundAction -from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import CreatePlacementGroupResponse, PlacementGroup if TYPE_CHECKING: @@ -43,7 +43,7 @@ class PlacementGroupsPageResult(NamedTuple): meta: Meta | None -class PlacementGroupsClient(ClientEntityBase, GetEntityByNameMixin): +class PlacementGroupsClient(ClientEntityBase): _client: Client def get_by_id(self, id: int) -> BoundPlacementGroup: @@ -122,7 +122,12 @@ def get_all( Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) :return: List[:class:`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: str) -> BoundPlacementGroup | None: """Get Placement Group by name @@ -131,7 +136,7 @@ def get_by_name(self, name: str) -> BoundPlacementGroup | None: Used to get Placement Group by name :return: class:`BoundPlacementGroup ` """ - return super().get_by_name(name) + return self._get_first_by(name=name) def create( self, diff --git a/hcloud/primary_ips/client.py b/hcloud/primary_ips/client.py index 05c4e60a..a54de6dc 100644 --- a/hcloud/primary_ips/client.py +++ b/hcloud/primary_ips/client.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import BoundAction -from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import CreatePrimaryIPResponse, PrimaryIP if TYPE_CHECKING: @@ -96,7 +96,7 @@ class PrimaryIPsPageResult(NamedTuple): meta: Meta | None -class PrimaryIPsClient(ClientEntityBase, GetEntityByNameMixin): +class PrimaryIPsClient(ClientEntityBase): _client: Client def get_by_id(self, id: int) -> BoundPrimaryIP: @@ -164,7 +164,7 @@ def get_all( Can be used to filter networks by their name. :return: List[:class:`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: str) -> BoundPrimaryIP | None: """Get Primary IP by name @@ -173,7 +173,7 @@ def get_by_name(self, name: str) -> BoundPrimaryIP | None: Used to get Primary IP by name. :return: :class:`BoundPrimaryIP ` """ - return super().get_by_name(name) + return self._get_first_by(name=name) def create( self, diff --git a/hcloud/server_types/client.py b/hcloud/server_types/client.py index 20b2c9fe..12cf34a0 100644 --- a/hcloud/server_types/client.py +++ b/hcloud/server_types/client.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple -from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import ServerType if TYPE_CHECKING: @@ -20,7 +20,7 @@ class ServerTypesPageResult(NamedTuple): meta: Meta | None -class ServerTypesClient(ClientEntityBase, GetEntityByNameMixin): +class ServerTypesClient(ClientEntityBase): _client: Client def get_by_id(self, id: int) -> BoundServerType: @@ -72,7 +72,7 @@ def get_all(self, name: str | None = None) -> list[BoundServerType]: Can be used to filter server type by their name. :return: List[:class:`BoundServerType `] """ - return super().get_all(name=name) + return self._iter_pages(self.get_list, name=name) def get_by_name(self, name: str) -> BoundServerType | None: """Get Server type by name @@ -81,4 +81,4 @@ def get_by_name(self, name: str) -> BoundServerType | None: Used to get Server type by name. :return: :class:`BoundServerType ` """ - return super().get_by_name(name) + return self._get_first_by(name=name) diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index 0108309e..c1282881 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction -from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..core import BoundModelBase, ClientEntityBase, Meta from ..datacenters import BoundDatacenter from ..firewalls import BoundFirewall from ..floating_ips import BoundFloatingIP @@ -445,7 +445,7 @@ class ServersPageResult(NamedTuple): meta: Meta | None -class ServersClient(ClientEntityBase, GetEntityByNameMixin): +class ServersClient(ClientEntityBase): _client: Client def get_by_id(self, id: int) -> BoundServer: @@ -514,7 +514,12 @@ def get_all( Can be used to filter servers by their status. The response will only contain servers matching the status. :return: List[:class:`BoundServer `] """ - return super().get_all(name=name, label_selector=label_selector, status=status) + return self._iter_pages( + self.get_list, + name=name, + label_selector=label_selector, + status=status, + ) def get_by_name(self, name: str) -> BoundServer | None: """Get server by name @@ -523,7 +528,7 @@ def get_by_name(self, name: str) -> BoundServer | None: Used to get server by name. :return: :class:`BoundServer ` """ - return super().get_by_name(name) + return self._get_first_by(name=name) def create( self, @@ -682,7 +687,12 @@ def get_actions( 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 `] """ - return super().get_actions(server, status=status, sort=sort) + return self._iter_pages( + self.get_actions_list, + server, + status=status, + sort=sort, + ) def update( self, diff --git a/hcloud/ssh_keys/client.py b/hcloud/ssh_keys/client.py index 797619f1..8a86da8a 100644 --- a/hcloud/ssh_keys/client.py +++ b/hcloud/ssh_keys/client.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple -from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import SSHKey if TYPE_CHECKING: @@ -41,7 +41,7 @@ class SSHKeysPageResult(NamedTuple): meta: Meta | None -class SSHKeysClient(ClientEntityBase, GetEntityByNameMixin): +class SSHKeysClient(ClientEntityBase): _client: Client def get_by_id(self, id: int) -> BoundSSHKey: @@ -110,8 +110,11 @@ def get_all( Can be used to filter SSH keys by labels. The response will only contain SSH keys matching the label selector. :return: List[:class:`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: str) -> BoundSSHKey | None: @@ -121,7 +124,7 @@ def get_by_name(self, name: str) -> BoundSSHKey | None: Used to get ssh key by name. :return: :class:`BoundSSHKey ` """ - return super().get_by_name(name) + return self._get_first_by(name=name) def get_by_fingerprint(self, fingerprint: str) -> BoundSSHKey | None: """Get ssh key by fingerprint @@ -130,9 +133,7 @@ def get_by_fingerprint(self, fingerprint: str) -> BoundSSHKey | None: Used to get ssh key by fingerprint. :return: :class:`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, diff --git a/hcloud/volumes/client.py b/hcloud/volumes/client.py index a00a78bb..60b1d9b0 100644 --- a/hcloud/volumes/client.py +++ b/hcloud/volumes/client.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction -from ..core import BoundModelBase, ClientEntityBase, GetEntityByNameMixin, Meta +from ..core import BoundModelBase, ClientEntityBase, Meta from ..locations import BoundLocation from .domain import CreateVolumeResponse, Volume @@ -134,7 +134,7 @@ class VolumesPageResult(NamedTuple): meta: Meta | None -class VolumesClient(ClientEntityBase, GetEntityByNameMixin): +class VolumesClient(ClientEntityBase): _client: Client def get_by_id(self, id: int) -> BoundVolume: @@ -199,7 +199,11 @@ def get_all( Can be used to filter volumes by their status. The response will only contain volumes matching the status. :return: List[:class:`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: str) -> BoundVolume | None: """Get volume by name @@ -208,7 +212,7 @@ def get_by_name(self, name: str) -> BoundVolume | None: Used to get volume by name. :return: :class:`BoundVolume ` """ - return super().get_by_name(name) + return self._get_first_by(name=name) def create( self, @@ -325,7 +329,12 @@ def get_actions( 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 `] """ - 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, diff --git a/tests/unit/core/test_client.py b/tests/unit/core/test_client.py index 1d4b2da0..b8e424f7 100644 --- a/tests/unit/core/test_client.py +++ b/tests/unit/core/test_client.py @@ -6,13 +6,7 @@ import pytest from hcloud.actions import ActionsPageResult -from hcloud.core import ( - BaseDomain, - BoundModelBase, - ClientEntityBase, - GetEntityByNameMixin, - Meta, -) +from hcloud.core import BaseDomain, BoundModelBase, ClientEntityBase, Meta class TestBoundModelBase: @@ -96,7 +90,7 @@ class CandiesPageResult(NamedTuple): meta: Meta class CandiesClient(ClientEntityBase): - def get_list(self, status, page=None, per_page=None): + def get_list(self, status=None, page=None, per_page=None): json_content = json_content_function(page) results = [ (r, page, status, per_page) for r in json_content["candies"] @@ -122,7 +116,7 @@ def get_actions_list(self, status, page=None, per_page=None): return constructor - def test_get_all_no_meta(self, client_class_constructor): + def test_iter_pages_no_meta(self, client_class_constructor): json_content = {"candies": [1, 2]} def json_content_function(p): @@ -130,11 +124,11 @@ def json_content_function(p): candies_client = client_class_constructor(json_content_function) - result = candies_client.get_all(status="sweet") + result = candies_client._iter_pages(candies_client.get_list, status="sweet") assert result == [(1, 1, "sweet", 50), (2, 1, "sweet", 50)] - def test_get_all_no_next_page(self, client_class_constructor): + def test_iter_pages_no_next_page(self, client_class_constructor): json_content = { "candies": [1, 2], "meta": {"pagination": {"page": 1, "per_page": 11, "next_page": None}}, @@ -145,11 +139,11 @@ def json_content_function(p): candies_client = client_class_constructor(json_content_function) - result = candies_client.get_all(status="sweet") + result = candies_client._iter_pages(candies_client.get_list, status="sweet") assert result == [(1, 1, "sweet", 50), (2, 1, "sweet", 50)] - def test_get_all_ok(self, client_class_constructor): + def test_iter_pages_ok(self, client_class_constructor): def json_content_function(p): return { "candies": [10 + p, 20 + p], @@ -164,7 +158,7 @@ def json_content_function(p): candies_client = client_class_constructor(json_content_function) - result = candies_client.get_all(status="sweet") + result = candies_client._iter_pages(candies_client.get_list, status="sweet") assert result == [ (11, 1, "sweet", 50), @@ -175,19 +169,6 @@ def json_content_function(p): (23, 3, "sweet", 50), ] - def test_get_actions_no_method(self, client_class_constructor): - json_content = {"candies": [1, 2]} - - def json_content_function(p): - return json_content - - candies_client = client_class_constructor(json_content_function) - - with pytest.raises(ValueError) as exception_info: - candies_client.get_actions() - error = exception_info.value - assert str(error) == "this endpoint does not support get_actions method" - def test_get_actions_ok(self, client_class_with_actions_constructor): def json_content_function(p): return { @@ -203,7 +184,9 @@ def json_content_function(p): candies_client = client_class_with_actions_constructor(json_content_function) - result = candies_client.get_actions(status="sweet") + result = candies_client._iter_pages( + candies_client.get_actions_list, status="sweet" + ) assert result == [ (11, 1, "sweet", 50), @@ -214,26 +197,7 @@ def json_content_function(p): (23, 3, "sweet", 50), ] - -class TestGetEntityByNameMixin: - @pytest.fixture() - def client_class_constructor(self): - def constructor(json_content_function): - class CandiesPageResult(NamedTuple): - candies: list[Any] - meta: Meta - - class CandiesClient(ClientEntityBase, GetEntityByNameMixin): - def get_list(self, name, page=None, per_page=None): - json_content = json_content_function(page) - results = json_content["candies"] - return CandiesPageResult(results, Meta.parse_meta(json_content)) - - return CandiesClient(mock.MagicMock()) - - return constructor - - def test_get_by_name_result_exists(self, client_class_constructor): + def test_get_first_by_result_exists(self, client_class_constructor): json_content = {"candies": [1]} def json_content_function(p): @@ -241,11 +205,11 @@ def json_content_function(p): candies_client = client_class_constructor(json_content_function) - result = candies_client.get_by_name(name="sweet") + result = candies_client._get_first_by(status="sweet") - assert result == 1 + assert result == (1, None, "sweet", None) - def test_get_by_name_result_does_not_exist(self, client_class_constructor): + def test_get_first_by_result_does_not_exist(self, client_class_constructor): json_content = {"candies": []} def json_content_function(p): @@ -253,6 +217,6 @@ def json_content_function(p): candies_client = client_class_constructor(json_content_function) - result = candies_client.get_by_name(name="sweet") + result = candies_client._get_first_by(status="sweet") assert result is None From 344c955df99f03c567113a66cc3b3b6e43c7a775 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Tue, 8 Aug 2023 14:20:49 +0200 Subject: [PATCH 089/406] chore(main): release 1.27.0 (#250) --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ hcloud/__version__.py | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5006d384..3dc41091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## [1.27.0](https://github.com/hetznercloud/hcloud-python/compare/v1.26.0...v1.27.0) (2023-08-08) + + +### Features + +* add global request timeout option ([#271](https://github.com/hetznercloud/hcloud-python/issues/271)) ([07a663f](https://github.com/hetznercloud/hcloud-python/commit/07a663fd8628d305a7461a90a94c61a97c12421b)) +* reexport references in parent ressources modules ([#256](https://github.com/hetznercloud/hcloud-python/issues/256)) ([854c12b](https://github.com/hetznercloud/hcloud-python/commit/854c12bbde3a5f0dcc77cabe72ecab2fd72fbac0)) +* the package is now typed ([#265](https://github.com/hetznercloud/hcloud-python/issues/265)) ([da8baa5](https://github.com/hetznercloud/hcloud-python/commit/da8baa551628fb759c790871362fef1e3666c56b)) + + +### Bug Fixes + +* allow omitting `datacenter` when creating a primary ip ([#171](https://github.com/hetznercloud/hcloud-python/issues/171)) ([4375dc6](https://github.com/hetznercloud/hcloud-python/commit/4375dc6ec351207380a011ec35e1397bf2bd17e9)) +* ineffective doc strings ([#266](https://github.com/hetznercloud/hcloud-python/issues/266)) ([bb34df9](https://github.com/hetznercloud/hcloud-python/commit/bb34df9390030e70f39bb82c92f4040eef18eb3b)) +* invalid attribute in placement group ([#258](https://github.com/hetznercloud/hcloud-python/issues/258)) ([23b3607](https://github.com/hetznercloud/hcloud-python/commit/23b36079d997d28d73cb9edc9a51a8c3b4481d7e)) + + +### Dependencies + +* update pre-commit hook asottile/pyupgrade to v3.10.1 ([#261](https://github.com/hetznercloud/hcloud-python/issues/261)) ([efa5780](https://github.com/hetznercloud/hcloud-python/commit/efa5780d0de3080bffe43994c064a0f1bcf6da43)) +* update pre-commit hook pre-commit/mirrors-prettier to v3.0.1 ([#269](https://github.com/hetznercloud/hcloud-python/issues/269)) ([2239b0b](https://github.com/hetznercloud/hcloud-python/commit/2239b0bc9beae457215c6514b0b823cc84a4a463)) +* update pre-commit hook pycqa/flake8 to v6.1.0 ([#260](https://github.com/hetznercloud/hcloud-python/issues/260)) ([fd01384](https://github.com/hetznercloud/hcloud-python/commit/fd013842f7f94e98520ed403a8cd91b68a4c4e5c)) + + +### Documentation + +* update documentation ([#247](https://github.com/hetznercloud/hcloud-python/issues/247)) ([e63741f](https://github.com/hetznercloud/hcloud-python/commit/e63741fab50524f4e4098af5c77f806915ae93c8)) +* update hetzner logo ([#264](https://github.com/hetznercloud/hcloud-python/issues/264)) ([ee79851](https://github.com/hetznercloud/hcloud-python/commit/ee79851dbf00e50d7f6b398fd4323f3e14831831)) + ## [1.26.0](https://github.com/hetznercloud/hcloud-python/compare/v1.25.0...v1.26.0) (2023-07-19) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index d8bd11a0..ebc2c1bc 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1,3 +1,3 @@ from __future__ import annotations -VERSION = "1.26.0" # x-release-please-version +VERSION = "1.27.0" # x-release-please-version From 6d79d1d18d3731c3db70184c841428e9c4b2a32c Mon Sep 17 00:00:00 2001 From: Jonas L Date: Tue, 8 Aug 2023 14:47:37 +0200 Subject: [PATCH 090/406] fix: missing long_description content_type in setup.py (#279) * ci: check release process in pull requests * fix: missing long_description content_type in setup.py * ci: smaller release workflow name --- .github/workflows/release.yml | 11 +++++++++-- setup.py | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1cf9e6e0..c9da9793 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,11 +1,14 @@ -name: Release Python Package +name: Release on: + push: + branches: [main] + pull_request: release: types: [created] jobs: - deploy: + build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -21,7 +24,11 @@ jobs: - name: Build run: python3 -m build + - name: Check + run: twine check dist/* + - name: Publish + if: github.event_name == 'release' env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} diff --git a/setup.py b/setup.py index 9840a1dd..e4c665d5 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ keywords="hcloud hetzner cloud", description="Official Hetzner Cloud python library", long_description=readme, + long_description_content_type="text/markdown", author="Hetzner Cloud GmbH", author_email="support-cloud@hetzner.com", url="https://github.com/hetznercloud/hcloud-python", From c5447abed7bf59a2e47c076182745ee0d3268870 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Tue, 8 Aug 2023 14:52:29 +0200 Subject: [PATCH 091/406] chore(main): release 1.27.1 (#280) --- CHANGELOG.md | 7 +++++++ hcloud/__version__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dc41091..c843a0e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.27.1](https://github.com/hetznercloud/hcloud-python/compare/v1.27.0...v1.27.1) (2023-08-08) + + +### Bug Fixes + +* missing long_description content_type in setup.py ([#279](https://github.com/hetznercloud/hcloud-python/issues/279)) ([6d79d1d](https://github.com/hetznercloud/hcloud-python/commit/6d79d1d18d3731c3db70184c841428e9c4b2a32c)) + ## [1.27.0](https://github.com/hetznercloud/hcloud-python/compare/v1.26.0...v1.27.0) (2023-08-08) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index ebc2c1bc..29fb2a1a 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1,3 +1,3 @@ from __future__ import annotations -VERSION = "1.27.0" # x-release-please-version +VERSION = "1.27.1" # x-release-please-version From 52313c19216ea557cd71e5825842fcad207ed118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Wed, 9 Aug 2023 09:44:12 +0200 Subject: [PATCH 092/406] build: exclude top-level directories from being installed (#282) Fix setup.py to exclude installing the tests, examples and docs directories as a top-level Python package, i.e. into: /usr/lib/python*/site-packages/tests Related to 68bb0f2 --------- * build: exclude `tests` from being installed Fix `setup.py` to exclude installing the `tests` directory as a top-level Python package, i.e. into: /usr/lib/python*/site-packages/tests * build: also exclude examples and docs Related to 68bb0f2bf33df5e12607b94a306f5ec328685c44 --------- Co-authored-by: jo --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e4c665d5..b1cba79f 100644 --- a/setup.py +++ b/setup.py @@ -58,6 +58,6 @@ ], }, include_package_data=True, - packages=find_packages(), + packages=find_packages(exclude=["examples", "tests*", "docs"]), zip_safe=False, ) From 0c0518e38e8c6ebe280ee85259480fb5671c2d84 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Wed, 9 Aug 2023 10:17:31 +0200 Subject: [PATCH 093/406] docs: fix python references (#281) --- hcloud/certificates/client.py | 2 +- hcloud/floating_ips/client.py | 9 ++++----- hcloud/images/client.py | 2 +- hcloud/networks/client.py | 2 +- hcloud/ssh_keys/client.py | 2 +- hcloud/volumes/client.py | 4 ++-- 6 files changed, 10 insertions(+), 11 deletions(-) diff --git a/hcloud/certificates/client.py b/hcloud/certificates/client.py index 1067dfa8..ff245678 100644 --- a/hcloud/certificates/client.py +++ b/hcloud/certificates/client.py @@ -81,7 +81,7 @@ def update( New name to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) - :return: :class:`BoundCertificate + :return: :class:`BoundCertificate ` """ return self._client.update(self, name, labels) diff --git a/hcloud/floating_ips/client.py b/hcloud/floating_ips/client.py index 58d7fa37..b85cd9d9 100644 --- a/hcloud/floating_ips/client.py +++ b/hcloud/floating_ips/client.py @@ -46,13 +46,13 @@ def get_actions_list( :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` - :param sort: List[str] (optional) + :param sort: List[str] (optional) 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` - :param page: int (optional) + :param page: int (optional) Specifies the page to fetch - :param per_page: int (optional) + :param per_page: int (optional) Specifies how many results are returned by page - :return: (List[:class:`BoundAction `], :class:`Meta `) + :return: (List[:class:`BoundAction `], :class:`Meta `) """ return self._client.get_actions_list(self, status, sort, page, per_page) @@ -67,7 +67,6 @@ def get_actions( Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) 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 `] """ return self._client.get_actions(self, status, sort) diff --git a/hcloud/images/client.py b/hcloud/images/client.py index a7dbef76..132efdac 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -180,7 +180,7 @@ def get_by_id(self, id: int) -> BoundImage: """Get a specific Image :param id: int - :return: :class:`BoundImage BoundNetwork: """Get a specific network :param id: int - :return: :class:`BoundNetwork + :return: :class:`BoundNetwork ` """ response = self._client.request(url=f"/networks/{id}", method="GET") return BoundNetwork(self, response["network"]) diff --git a/hcloud/ssh_keys/client.py b/hcloud/ssh_keys/client.py index 8a86da8a..1c16f03d 100644 --- a/hcloud/ssh_keys/client.py +++ b/hcloud/ssh_keys/client.py @@ -25,7 +25,7 @@ def update( New Description to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) - :return: :class:`BoundSSHKey + :return: :class:`BoundSSHKey ` """ return self._client.update(self, name, labels) diff --git a/hcloud/volumes/client.py b/hcloud/volumes/client.py index 60b1d9b0..3dd628a1 100644 --- a/hcloud/volumes/client.py +++ b/hcloud/volumes/client.py @@ -62,7 +62,7 @@ def get_actions( :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` - :param sort:List[str] (optional) + :param sort: List[str] (optional) 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 `] """ @@ -325,7 +325,7 @@ def get_actions( :param volume: :class:`BoundVolume ` or :class:`Volume ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` - :param sort:List[str] (optional) + :param sort: List[str] (optional) 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 `] """ From 2eb1e6da24ecf69a904d27c9cb80bd183e92c50a Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Wed, 9 Aug 2023 18:37:49 +0200 Subject: [PATCH 094/406] chore(main): release 1.27.2 (#283) --- CHANGELOG.md | 7 +++++++ hcloud/__version__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c843a0e8..99b8ffda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.27.2](https://github.com/hetznercloud/hcloud-python/compare/v1.27.1...v1.27.2) (2023-08-09) + + +### Documentation + +* fix python references ([#281](https://github.com/hetznercloud/hcloud-python/issues/281)) ([0c0518e](https://github.com/hetznercloud/hcloud-python/commit/0c0518e38e8c6ebe280ee85259480fb5671c2d84)) + ## [1.27.1](https://github.com/hetznercloud/hcloud-python/compare/v1.27.0...v1.27.1) (2023-08-08) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index 29fb2a1a..71617d65 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1,3 +1,3 @@ from __future__ import annotations -VERSION = "1.27.1" # x-release-please-version +VERSION = "1.27.2" # x-release-please-version From 9dd5c8110bf679c13e8e6ba08e760019b4dae706 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 10 Aug 2023 16:43:30 +0200 Subject: [PATCH 095/406] deps: update dependency mypy to >=1.5,<1.6 (#284) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b1cba79f..ea83cfcb 100644 --- a/setup.py +++ b/setup.py @@ -52,7 +52,7 @@ "test": [ "coverage>=7.2.7,<7.3", "pytest>=7.4,<7.5", - "mypy>=1.4.1,<1.5", + "mypy>=1.5,<1.6", "types-python-dateutil", "types-requests", ], From a4df4fa1cc7a17e1afdea1c33f4428a8a594a011 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 09:02:51 +0200 Subject: [PATCH 096/406] deps: update dependency coverage to >=7.3,<7.4 (#286) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ea83cfcb..2714b0a0 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ "watchdog>=3.0.0,<3.1", ], "test": [ - "coverage>=7.2.7,<7.3", + "coverage>=7.3,<7.4", "pytest>=7.4,<7.5", "mypy>=1.5,<1.6", "types-python-dateutil", From 4bb9a9730eadea9fd0569d5d11b7585dbb5da157 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 14 Aug 2023 14:27:34 +0200 Subject: [PATCH 097/406] feat: implement resource actions clients (#252) This implements the new per resources actions endpoints. Related to https://docs.hetzner.cloud/changelog#2023-06-29-resource-action-endpoints ```py # Existing API client.actions.get_by_id() # /actions/{id} client..get_actions_all() # //{resource_id}/actions client..get_actions_list() # //{resource_id}/actions # New API client..actions.get_all() # //actions client..actions.get_list() # //actions client..actions.get_by_id() # //actions/{id} # Not planned client..get_action_by_id() # //{resource_id}/actions/{id} # Deprecated client.actions.get_all() # /actions client.actions.get_list() # /actions ``` One exception is the primary IPs client, it doesn't include calls to `//{resource_id}/actions` or `//{resource_id}/actions/{id}`: https://docs.hetzner.cloud/#primary-ip-actions * test: improve existing actions tests * feat: create ResourceActionsClient * feat: deprecated /actions endpoint * test: add tests for ResourceActionsClient * feat: spread ResourceActionsClient to all clients * test: add tests for all ResourceActionsClient * docs: improve reference docs * docs: add link to deprecation changelog --- docs/api.clients.actions.rst | 3 + hcloud/actions/__init__.py | 7 +- hcloud/actions/client.py | 79 +++++++++++++++++++--- hcloud/certificates/client.py | 12 +++- hcloud/firewalls/client.py | 12 +++- hcloud/floating_ips/client.py | 12 +++- hcloud/images/client.py | 12 +++- hcloud/load_balancers/client.py | 12 +++- hcloud/networks/client.py | 12 +++- hcloud/primary_ips/client.py | 12 +++- hcloud/servers/client.py | 12 +++- hcloud/volumes/client.py | 12 +++- tests/unit/actions/test_client.py | 86 ++++++++++++++++++++++-- tests/unit/certificates/test_client.py | 52 ++++++++++++++ tests/unit/firewalls/test_client.py | 52 ++++++++++++++ tests/unit/floating_ips/test_client.py | 51 ++++++++++++++ tests/unit/images/test_client.py | 52 ++++++++++++++ tests/unit/load_balancers/test_client.py | 52 ++++++++++++++ tests/unit/networks/test_client.py | 51 ++++++++++++++ tests/unit/primary_ips/test_client.py | 51 ++++++++++++++ tests/unit/servers/test_client.py | 58 ++++++++++++++++ tests/unit/volumes/test_client.py | 51 ++++++++++++++ 22 files changed, 727 insertions(+), 26 deletions(-) diff --git a/docs/api.clients.actions.rst b/docs/api.clients.actions.rst index ab7d0999..aecd2fe0 100644 --- a/docs/api.clients.actions.rst +++ b/docs/api.clients.actions.rst @@ -1,9 +1,12 @@ ActionsClient ================== +.. autoclass:: hcloud.actions.client.ResourceActionsClient + :members: .. autoclass:: hcloud.actions.client.ActionsClient :members: + :inherited-members: .. autoclass:: hcloud.actions.client.BoundAction :members: diff --git a/hcloud/actions/__init__.py b/hcloud/actions/__init__.py index 66bb7b4d..ca93c89f 100644 --- a/hcloud/actions/__init__.py +++ b/hcloud/actions/__init__.py @@ -1,6 +1,11 @@ from __future__ import annotations -from .client import ActionsClient, ActionsPageResult, BoundAction # noqa: F401 +from .client import ( # noqa: F401 + ActionsClient, + ActionsPageResult, + BoundAction, + ResourceActionsClient, +) from .domain import ( # noqa: F401 Action, ActionException, diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index 3105b3c5..63e1d884 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -1,6 +1,7 @@ from __future__ import annotations import time +import warnings from typing import TYPE_CHECKING, Any, NamedTuple from ..core import BoundModelBase, ClientEntityBase, Meta @@ -40,8 +41,12 @@ class ActionsPageResult(NamedTuple): meta: Meta | None -class ActionsClient(ClientEntityBase): - _client: Client +class ResourceActionsClient(ClientEntityBase): + _resource: str + + def __init__(self, client: Client, resource: str | None): + super().__init__(client) + self._resource = resource or "" def get_by_id(self, id: int) -> BoundAction: """Get a specific action by its ID. @@ -49,9 +54,11 @@ def get_by_id(self, id: int) -> BoundAction: :param id: int :return: :class:`BoundAction ` """ - - response = self._client.request(url=f"/actions/{id}", method="GET") - return BoundAction(self, response["action"]) + response = self._client.request( + url=f"{self._resource}/actions/{id}", + method="GET", + ) + return BoundAction(self._client.actions, response["action"]) def get_list( self, @@ -60,7 +67,7 @@ def get_list( page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: - """Get a list of actions from this account + """Get a list of actions. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` @@ -82,9 +89,14 @@ def get_list( if per_page is not None: params["per_page"] = per_page - response = self._client.request(url="/actions", method="GET", params=params) + response = self._client.request( + url=f"{self._resource}/actions", + method="GET", + params=params, + ) actions = [ - BoundAction(self, action_data) for action_data in response["actions"] + BoundAction(self._client.actions, action_data) + for action_data in response["actions"] ] return ActionsPageResult(actions, Meta.parse_meta(response)) @@ -93,7 +105,7 @@ def get_all( status: list[str] | None = None, sort: list[str] | None = None, ) -> list[BoundAction]: - """Get all actions of the account + """Get all actions. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` @@ -102,3 +114,52 @@ def get_all( :return: List[:class:`BoundAction `] """ return self._iter_pages(self.get_list, status=status, sort=sort) + + +class ActionsClient(ResourceActionsClient): + def __init__(self, client: Client): + super().__init__(client, None) + + def get_list( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + page: int | None = None, + per_page: int | None = None, + ) -> ActionsPageResult: + """ + .. deprecated:: 1.28 + Use :func:`client..actions.get_list` instead, + e.g. using :attr:`hcloud.certificates.client.CertificatesClient.actions`. + + `Starting 1 October 2023, it will no longer be available. `_ + """ + warnings.warn( + "The 'client.actions.get_list' method is deprecated, please use the " + "'client..actions.get_list' method instead (e.g. " + "'client.certificates.actions.get_list').", + DeprecationWarning, + stacklevel=2, + ) + return super().get_list(status=status, sort=sort, page=page, per_page=per_page) + + def get_all( + self, + status: list[str] | None = None, + sort: list[str] | None = None, + ) -> list[BoundAction]: + """ + .. deprecated:: 1.28 + Use :func:`client..actions.get_all` instead, + e.g. using :attr:`hcloud.certificates.client.CertificatesClient.actions`. + + `Starting 1 October 2023, it will no longer be available. `_ + """ + warnings.warn( + "The 'client.actions.get_all' method is deprecated, please use the " + "'client..actions.get_all' method instead (e.g. " + "'client.certificates.actions.get_all').", + DeprecationWarning, + stacklevel=2, + ) + return super().get_all(status=status, sort=sort) diff --git a/hcloud/certificates/client.py b/hcloud/certificates/client.py index ff245678..e368a952 100644 --- a/hcloud/certificates/client.py +++ b/hcloud/certificates/client.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple -from ..actions import ActionsPageResult, BoundAction +from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import ( Certificate, @@ -106,6 +106,16 @@ class CertificatesPageResult(NamedTuple): class CertificatesClient(ClientEntityBase): _client: Client + actions: ResourceActionsClient + """Certificates scoped actions client + + :type: :class:`ResourceActionsClient ` + """ + + def __init__(self, client: Client): + super().__init__(client) + self.actions = ResourceActionsClient(client, "/certificates") + def get_by_id(self, id: int) -> BoundCertificate: """Get a specific certificate by its ID. diff --git a/hcloud/firewalls/client.py b/hcloud/firewalls/client.py index 86960808..b9b341b6 100644 --- a/hcloud/firewalls/client.py +++ b/hcloud/firewalls/client.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple -from ..actions import ActionsPageResult, BoundAction +from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import ( CreateFirewallResponse, @@ -161,6 +161,16 @@ class FirewallsPageResult(NamedTuple): class FirewallsClient(ClientEntityBase): _client: Client + actions: ResourceActionsClient + """Firewalls scoped actions client + + :type: :class:`ResourceActionsClient ` + """ + + def __init__(self, client: Client): + super().__init__(client) + self.actions = ResourceActionsClient(client, "/firewalls") + def get_actions_list( self, firewall: Firewall | BoundFirewall, diff --git a/hcloud/floating_ips/client.py b/hcloud/floating_ips/client.py index b85cd9d9..49d7c8e5 100644 --- a/hcloud/floating_ips/client.py +++ b/hcloud/floating_ips/client.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple -from ..actions import ActionsPageResult, BoundAction +from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient from ..core import BoundModelBase, ClientEntityBase, Meta from ..locations import BoundLocation from .domain import CreateFloatingIPResponse, FloatingIP @@ -141,6 +141,16 @@ class FloatingIPsPageResult(NamedTuple): class FloatingIPsClient(ClientEntityBase): _client: Client + actions: ResourceActionsClient + """Floating IPs scoped actions client + + :type: :class:`ResourceActionsClient ` + """ + + def __init__(self, client: Client): + super().__init__(client) + self.actions = ResourceActionsClient(client, "/floating_ips") + def get_actions_list( self, floating_ip: FloatingIP | BoundFloatingIP, diff --git a/hcloud/images/client.py b/hcloud/images/client.py index 132efdac..dee910f0 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple -from ..actions import ActionsPageResult, BoundAction +from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import Image @@ -113,6 +113,16 @@ class ImagesPageResult(NamedTuple): class ImagesClient(ClientEntityBase): _client: Client + actions: ResourceActionsClient + """Images scoped actions client + + :type: :class:`ResourceActionsClient ` + """ + + def __init__(self, client: Client): + super().__init__(client) + self.actions = ResourceActionsClient(client, "/images") + def get_actions_list( self, image: Image | BoundImage, diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index e228cc21..b999ddc0 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple -from ..actions import ActionsPageResult, BoundAction +from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient from ..certificates import BoundCertificate from ..core import BoundModelBase, ClientEntityBase, Meta from ..load_balancer_types import BoundLoadBalancerType @@ -331,6 +331,16 @@ class LoadBalancersPageResult(NamedTuple): class LoadBalancersClient(ClientEntityBase): _client: Client + actions: ResourceActionsClient + """Load Balancers scoped actions client + + :type: :class:`ResourceActionsClient ` + """ + + def __init__(self, client: Client): + super().__init__(client) + self.actions = ResourceActionsClient(client, "/load_balancers") + def get_by_id(self, id: int) -> BoundLoadBalancer: """Get a specific Load Balancer diff --git a/hcloud/networks/client.py b/hcloud/networks/client.py index c6b90a65..36eaaa40 100644 --- a/hcloud/networks/client.py +++ b/hcloud/networks/client.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple -from ..actions import ActionsPageResult, BoundAction +from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import Network, NetworkRoute, NetworkSubnet @@ -168,6 +168,16 @@ class NetworksPageResult(NamedTuple): class NetworksClient(ClientEntityBase): _client: Client + actions: ResourceActionsClient + """Networks scoped actions client + + :type: :class:`ResourceActionsClient ` + """ + + def __init__(self, client: Client): + super().__init__(client) + self.actions = ResourceActionsClient(client, "/networks") + def get_by_id(self, id: int) -> BoundNetwork: """Get a specific network diff --git a/hcloud/primary_ips/client.py b/hcloud/primary_ips/client.py index a54de6dc..acd2f69e 100644 --- a/hcloud/primary_ips/client.py +++ b/hcloud/primary_ips/client.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple -from ..actions import BoundAction +from ..actions import BoundAction, ResourceActionsClient from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import CreatePrimaryIPResponse, PrimaryIP @@ -99,6 +99,16 @@ class PrimaryIPsPageResult(NamedTuple): class PrimaryIPsClient(ClientEntityBase): _client: Client + actions: ResourceActionsClient + """Primary IPs scoped actions client + + :type: :class:`ResourceActionsClient ` + """ + + def __init__(self, client: Client): + super().__init__(client) + self.actions = ResourceActionsClient(client, "/primary_ips") + def get_by_id(self, id: int) -> BoundPrimaryIP: """Returns a specific Primary IP object. diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index c1282881..d19180c5 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple -from ..actions import ActionsPageResult, BoundAction +from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient from ..core import BoundModelBase, ClientEntityBase, Meta from ..datacenters import BoundDatacenter from ..firewalls import BoundFirewall @@ -448,6 +448,16 @@ class ServersPageResult(NamedTuple): class ServersClient(ClientEntityBase): _client: Client + actions: ResourceActionsClient + """Servers scoped actions client + + :type: :class:`ResourceActionsClient ` + """ + + def __init__(self, client: Client): + super().__init__(client) + self.actions = ResourceActionsClient(client, "/servers") + def get_by_id(self, id: int) -> BoundServer: """Get a specific server diff --git a/hcloud/volumes/client.py b/hcloud/volumes/client.py index 3dd628a1..9017ff49 100644 --- a/hcloud/volumes/client.py +++ b/hcloud/volumes/client.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, NamedTuple -from ..actions import ActionsPageResult, BoundAction +from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient from ..core import BoundModelBase, ClientEntityBase, Meta from ..locations import BoundLocation from .domain import CreateVolumeResponse, Volume @@ -137,6 +137,16 @@ class VolumesPageResult(NamedTuple): class VolumesClient(ClientEntityBase): _client: Client + actions: ResourceActionsClient + """Volumes scoped actions client + + :type: :class:`ResourceActionsClient ` + """ + + def __init__(self, client: Client): + super().__init__(client) + self.actions = ResourceActionsClient(client, "/volumes") + def get_by_id(self, id: int) -> BoundVolume: """Get a specific volume by its id diff --git a/tests/unit/actions/test_client.py b/tests/unit/actions/test_client.py index 16a79618..519a8b88 100644 --- a/tests/unit/actions/test_client.py +++ b/tests/unit/actions/test_client.py @@ -10,6 +10,7 @@ ActionsClient, ActionTimeoutException, BoundAction, + ResourceActionsClient, ) @@ -30,6 +31,8 @@ def test_wait_until_finished( ): mocked_requests.request.side_effect = [running_action, successfully_action] bound_running_action.wait_until_finished() + mocked_requests.request.assert_called_with(url="/actions/2", method="GET") + assert bound_running_action.status == "success" assert mocked_requests.request.call_count == 2 @@ -59,6 +62,73 @@ def test_wait_until_finished_max_retries( assert mocked_requests.request.call_count == 1 +class TestResourceActionsClient: + @pytest.fixture() + def actions_client(self): + return ResourceActionsClient(client=mock.MagicMock(), resource="/resource") + + def test_get_by_id(self, actions_client, generic_action): + actions_client._client.request.return_value = generic_action + action = actions_client.get_by_id(1) + actions_client._client.request.assert_called_with( + url="/resource/actions/1", method="GET" + ) + assert action._client == actions_client._client.actions + assert action.id == 1 + assert action.command == "stop_server" + + @pytest.mark.parametrize( + "params", + [{}, {"status": ["active"], "sort": ["status"], "page": 2, "per_page": 10}], + ) + def test_get_list(self, actions_client, generic_action_list, params): + actions_client._client.request.return_value = generic_action_list + result = actions_client.get_list(**params) + actions_client._client.request.assert_called_with( + url="/resource/actions", method="GET", params=params + ) + + assert result.meta is None + + actions = result.actions + assert len(actions) == 2 + + action1 = actions[0] + action2 = actions[1] + + assert action1._client == actions_client._client.actions + assert action1.id == 1 + assert action1.command == "start_server" + + assert action2._client == actions_client._client.actions + assert action2.id == 2 + assert action2.command == "stop_server" + + @pytest.mark.parametrize("params", [{}, {"status": ["active"], "sort": ["status"]}]) + def test_get_all(self, actions_client, generic_action_list, params): + actions_client._client.request.return_value = generic_action_list + actions = actions_client.get_all(**params) + + params.update({"page": 1, "per_page": 50}) + + actions_client._client.request.assert_called_with( + url="/resource/actions", method="GET", params=params + ) + + assert len(actions) == 2 + + action1 = actions[0] + action2 = actions[1] + + assert action1._client == actions_client._client.actions + assert action1.id == 1 + assert action1.command == "start_server" + + assert action2._client == actions_client._client.actions + assert action2.id == 2 + assert action2.command == "stop_server" + + class TestActionsClient: @pytest.fixture() def actions_client(self): @@ -70,7 +140,7 @@ def test_get_by_id(self, actions_client, generic_action): actions_client._client.request.assert_called_with( url="/actions/1", method="GET" ) - assert action._client is actions_client + assert action._client == actions_client._client.actions assert action.id == 1 assert action.command == "stop_server" @@ -80,7 +150,8 @@ def test_get_by_id(self, actions_client, generic_action): ) def test_get_list(self, actions_client, generic_action_list, params): actions_client._client.request.return_value = generic_action_list - result = actions_client.get_list(**params) + with pytest.deprecated_call(): + result = actions_client.get_list(**params) actions_client._client.request.assert_called_with( url="/actions", method="GET", params=params ) @@ -93,18 +164,19 @@ def test_get_list(self, actions_client, generic_action_list, params): action1 = actions[0] action2 = actions[1] - assert action1._client is actions_client + assert action1._client == actions_client._client.actions assert action1.id == 1 assert action1.command == "start_server" - assert action2._client is actions_client + assert action2._client == actions_client._client.actions assert action2.id == 2 assert action2.command == "stop_server" @pytest.mark.parametrize("params", [{}, {"status": ["active"], "sort": ["status"]}]) def test_get_all(self, actions_client, generic_action_list, params): actions_client._client.request.return_value = generic_action_list - actions = actions_client.get_all(**params) + with pytest.deprecated_call(): + actions = actions_client.get_all(**params) params.update({"page": 1, "per_page": 50}) @@ -117,10 +189,10 @@ def test_get_all(self, actions_client, generic_action_list, params): action1 = actions[0] action2 = actions[1] - assert action1._client is actions_client + assert action1._client == actions_client._client.actions assert action1.id == 1 assert action1.command == "start_server" - assert action2._client is actions_client + assert action2._client == actions_client._client.actions assert action2.id == 2 assert action2.command == "stop_server" diff --git a/tests/unit/certificates/test_client.py b/tests/unit/certificates/test_client.py index a039f3b5..0857d0c9 100644 --- a/tests/unit/certificates/test_client.py +++ b/tests/unit/certificates/test_client.py @@ -33,6 +33,7 @@ def test_get_actions_list( assert len(actions) == 1 assert isinstance(actions[0], BoundAction) + assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" @@ -48,6 +49,7 @@ def test_get_actions(self, hetzner_client, bound_certificate, response_get_actio assert len(actions) == 1 assert isinstance(actions[0], BoundAction) + assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" @@ -286,3 +288,53 @@ def test_retry_issuance( assert action.id == 14 assert action.command == "issue_certificate" + + def test_actions_get_by_id(self, certificates_client, response_get_actions): + certificates_client._client.request.return_value = { + "action": response_get_actions["actions"][0] + } + action = certificates_client.actions.get_by_id(13) + + certificates_client._client.request.assert_called_with( + url="/certificates/actions/13", method="GET" + ) + + assert isinstance(action, BoundAction) + assert action._client == certificates_client._client.actions + assert action.id == 13 + assert action.command == "change_protection" + + def test_actions_get_list(self, certificates_client, response_get_actions): + certificates_client._client.request.return_value = response_get_actions + result = certificates_client.actions.get_list() + + certificates_client._client.request.assert_called_with( + url="/certificates/actions", + method="GET", + params={}, + ) + + actions = result.actions + assert result.meta is None + + assert len(actions) == 1 + assert isinstance(actions[0], BoundAction) + assert actions[0]._client == certificates_client._client.actions + assert actions[0].id == 13 + assert actions[0].command == "change_protection" + + def test_actions_get_all(self, certificates_client, response_get_actions): + certificates_client._client.request.return_value = response_get_actions + actions = certificates_client.actions.get_all() + + certificates_client._client.request.assert_called_with( + url="/certificates/actions", + method="GET", + params={"page": 1, "per_page": 50}, + ) + + assert len(actions) == 1 + assert isinstance(actions[0], BoundAction) + assert actions[0]._client == certificates_client._client.actions + assert actions[0].id == 13 + assert actions[0].command == "change_protection" diff --git a/tests/unit/firewalls/test_client.py b/tests/unit/firewalls/test_client.py index fdf1d116..4e8cea94 100644 --- a/tests/unit/firewalls/test_client.py +++ b/tests/unit/firewalls/test_client.py @@ -88,6 +88,7 @@ def test_get_actions_list( assert len(actions) == 1 assert isinstance(actions[0], BoundAction) + assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "set_firewall_rules" @@ -106,6 +107,7 @@ def test_get_actions( assert len(actions) == 1 assert isinstance(actions[0], BoundAction) + assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "set_firewall_rules" @@ -460,3 +462,53 @@ def test_remove_from_resources( assert actions[0].id == 13 assert actions[0].progress == 100 + + def test_actions_get_by_id(self, firewalls_client, response_get_actions): + firewalls_client._client.request.return_value = { + "action": response_get_actions["actions"][0] + } + action = firewalls_client.actions.get_by_id(13) + + firewalls_client._client.request.assert_called_with( + url="/firewalls/actions/13", method="GET" + ) + + assert isinstance(action, BoundAction) + assert action._client == firewalls_client._client.actions + assert action.id == 13 + assert action.command == "set_firewall_rules" + + def test_actions_get_list(self, firewalls_client, response_get_actions): + firewalls_client._client.request.return_value = response_get_actions + result = firewalls_client.actions.get_list() + + firewalls_client._client.request.assert_called_with( + url="/firewalls/actions", + method="GET", + params={}, + ) + + actions = result.actions + assert result.meta is None + + assert len(actions) == 1 + assert isinstance(actions[0], BoundAction) + assert actions[0]._client == firewalls_client._client.actions + assert actions[0].id == 13 + assert actions[0].command == "set_firewall_rules" + + def test_actions_get_all(self, firewalls_client, response_get_actions): + firewalls_client._client.request.return_value = response_get_actions + actions = firewalls_client.actions.get_all() + + firewalls_client._client.request.assert_called_with( + url="/firewalls/actions", + method="GET", + params={"page": 1, "per_page": 50}, + ) + + assert len(actions) == 1 + assert isinstance(actions[0], BoundAction) + assert actions[0]._client == firewalls_client._client.actions + assert actions[0].id == 13 + assert actions[0].command == "set_firewall_rules" diff --git a/tests/unit/floating_ips/test_client.py b/tests/unit/floating_ips/test_client.py index 36eaf7ca..a56f0829 100644 --- a/tests/unit/floating_ips/test_client.py +++ b/tests/unit/floating_ips/test_client.py @@ -52,6 +52,7 @@ def test_get_actions(self, hetzner_client, bound_floating_ip, response_get_actio assert len(actions) == 1 assert isinstance(actions[0], BoundAction) + assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "assign_floating_ip" @@ -391,3 +392,53 @@ def test_change_dns_ptr(self, floating_ips_client, floating_ip, generic_action): ) assert action.id == 1 assert action.progress == 0 + + def test_actions_get_by_id(self, floating_ips_client, response_get_actions): + floating_ips_client._client.request.return_value = { + "action": response_get_actions["actions"][0] + } + action = floating_ips_client.actions.get_by_id(13) + + floating_ips_client._client.request.assert_called_with( + url="/floating_ips/actions/13", method="GET" + ) + + assert isinstance(action, BoundAction) + assert action._client == floating_ips_client._client.actions + assert action.id == 13 + assert action.command == "assign_floating_ip" + + def test_actions_get_list(self, floating_ips_client, response_get_actions): + floating_ips_client._client.request.return_value = response_get_actions + result = floating_ips_client.actions.get_list() + + floating_ips_client._client.request.assert_called_with( + url="/floating_ips/actions", + method="GET", + params={}, + ) + + actions = result.actions + assert result.meta is None + + assert len(actions) == 1 + assert isinstance(actions[0], BoundAction) + assert actions[0]._client == floating_ips_client._client.actions + assert actions[0].id == 13 + assert actions[0].command == "assign_floating_ip" + + def test_actions_get_all(self, floating_ips_client, response_get_actions): + floating_ips_client._client.request.return_value = response_get_actions + actions = floating_ips_client.actions.get_all() + + floating_ips_client._client.request.assert_called_with( + url="/floating_ips/actions", + method="GET", + params={"page": 1, "per_page": 50}, + ) + + assert len(actions) == 1 + assert isinstance(actions[0], BoundAction) + assert actions[0]._client == floating_ips_client._client.actions + assert actions[0].id == 13 + assert actions[0].command == "assign_floating_ip" diff --git a/tests/unit/images/test_client.py b/tests/unit/images/test_client.py index 876fbfe4..8e1dc490 100644 --- a/tests/unit/images/test_client.py +++ b/tests/unit/images/test_client.py @@ -63,6 +63,7 @@ def test_get_actions_list( assert len(actions) == 1 assert isinstance(actions[0], BoundAction) + assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" @@ -81,6 +82,7 @@ def test_get_actions( assert len(actions) == 1 assert isinstance(actions[0], BoundAction) + assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" @@ -308,3 +310,53 @@ def test_delete(self, images_client, image, generic_action): ) assert delete_success is True + + def test_actions_get_by_id(self, images_client, response_get_actions): + images_client._client.request.return_value = { + "action": response_get_actions["actions"][0] + } + action = images_client.actions.get_by_id(13) + + images_client._client.request.assert_called_with( + url="/images/actions/13", method="GET" + ) + + assert isinstance(action, BoundAction) + assert action._client == images_client._client.actions + assert action.id == 13 + assert action.command == "change_protection" + + def test_actions_get_list(self, images_client, response_get_actions): + images_client._client.request.return_value = response_get_actions + result = images_client.actions.get_list() + + images_client._client.request.assert_called_with( + url="/images/actions", + method="GET", + params={}, + ) + + actions = result.actions + assert result.meta is None + + assert len(actions) == 1 + assert isinstance(actions[0], BoundAction) + assert actions[0]._client == images_client._client.actions + assert actions[0].id == 13 + assert actions[0].command == "change_protection" + + def test_actions_get_all(self, images_client, response_get_actions): + images_client._client.request.return_value = response_get_actions + actions = images_client.actions.get_all() + + images_client._client.request.assert_called_with( + url="/images/actions", + method="GET", + params={"page": 1, "per_page": 50}, + ) + + assert len(actions) == 1 + assert isinstance(actions[0], BoundAction) + assert actions[0]._client == images_client._client.actions + assert actions[0].id == 13 + assert actions[0].command == "change_protection" diff --git a/tests/unit/load_balancers/test_client.py b/tests/unit/load_balancers/test_client.py index c8702046..191334c3 100644 --- a/tests/unit/load_balancers/test_client.py +++ b/tests/unit/load_balancers/test_client.py @@ -50,6 +50,7 @@ def test_get_actions_list( assert len(actions) == 1 assert isinstance(actions[0], BoundAction) + assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" @@ -68,6 +69,7 @@ def test_get_actions( assert len(actions) == 1 assert isinstance(actions[0], BoundAction) + assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" @@ -516,3 +518,53 @@ def test_change_type_with_load_balancer_type_id( assert action.id == 1 assert action.progress == 0 + + def test_actions_get_by_id(self, load_balancers_client, response_get_actions): + load_balancers_client._client.request.return_value = { + "action": response_get_actions["actions"][0] + } + action = load_balancers_client.actions.get_by_id(13) + + load_balancers_client._client.request.assert_called_with( + url="/load_balancers/actions/13", method="GET" + ) + + assert isinstance(action, BoundAction) + assert action._client == load_balancers_client._client.actions + assert action.id == 13 + assert action.command == "change_protection" + + def test_actions_get_list(self, load_balancers_client, response_get_actions): + load_balancers_client._client.request.return_value = response_get_actions + result = load_balancers_client.actions.get_list() + + load_balancers_client._client.request.assert_called_with( + url="/load_balancers/actions", + method="GET", + params={}, + ) + + actions = result.actions + assert result.meta is None + + assert len(actions) == 1 + assert isinstance(actions[0], BoundAction) + assert actions[0]._client == load_balancers_client._client.actions + assert actions[0].id == 13 + assert actions[0].command == "change_protection" + + def test_actions_get_all(self, load_balancers_client, response_get_actions): + load_balancers_client._client.request.return_value = response_get_actions + actions = load_balancers_client.actions.get_all() + + load_balancers_client._client.request.assert_called_with( + url="/load_balancers/actions", + method="GET", + params={"page": 1, "per_page": 50}, + ) + + assert len(actions) == 1 + assert isinstance(actions[0], BoundAction) + assert actions[0]._client == load_balancers_client._client.actions + assert actions[0].id == 13 + assert actions[0].command == "change_protection" diff --git a/tests/unit/networks/test_client.py b/tests/unit/networks/test_client.py index 68855fd1..ff4dd233 100644 --- a/tests/unit/networks/test_client.py +++ b/tests/unit/networks/test_client.py @@ -60,6 +60,7 @@ def test_get_actions(self, hetzner_client, bound_network, response_get_actions): assert len(actions) == 1 assert isinstance(actions[0], BoundAction) + assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "add_subnet" @@ -585,3 +586,53 @@ def test_change_ip_range(self, networks_client, network, generic_action): assert action.id == 1 assert action.progress == 0 + + def test_actions_get_by_id(self, networks_client, response_get_actions): + networks_client._client.request.return_value = { + "action": response_get_actions["actions"][0] + } + action = networks_client.actions.get_by_id(13) + + networks_client._client.request.assert_called_with( + url="/networks/actions/13", method="GET" + ) + + assert isinstance(action, BoundAction) + assert action._client == networks_client._client.actions + assert action.id == 13 + assert action.command == "add_subnet" + + def test_actions_get_list(self, networks_client, response_get_actions): + networks_client._client.request.return_value = response_get_actions + result = networks_client.actions.get_list() + + networks_client._client.request.assert_called_with( + url="/networks/actions", + method="GET", + params={}, + ) + + actions = result.actions + assert result.meta is None + + assert len(actions) == 1 + assert isinstance(actions[0], BoundAction) + assert actions[0]._client == networks_client._client.actions + assert actions[0].id == 13 + assert actions[0].command == "add_subnet" + + def test_actions_get_all(self, networks_client, response_get_actions): + networks_client._client.request.return_value = response_get_actions + actions = networks_client.actions.get_all() + + networks_client._client.request.assert_called_with( + url="/networks/actions", + method="GET", + params={"page": 1, "per_page": 50}, + ) + + assert len(actions) == 1 + assert isinstance(actions[0], BoundAction) + assert actions[0]._client == networks_client._client.actions + assert actions[0].id == 13 + assert actions[0].command == "add_subnet" diff --git a/tests/unit/primary_ips/test_client.py b/tests/unit/primary_ips/test_client.py index 85d4b9c4..413ed2bb 100644 --- a/tests/unit/primary_ips/test_client.py +++ b/tests/unit/primary_ips/test_client.py @@ -4,6 +4,7 @@ import pytest +from hcloud.actions import BoundAction from hcloud.datacenters import BoundDatacenter, Datacenter from hcloud.primary_ips import BoundPrimaryIP, PrimaryIP, PrimaryIPsClient @@ -295,3 +296,53 @@ def test_change_dns_ptr(self, primary_ips_client, primary_ip, generic_action): ) assert action.id == 1 assert action.progress == 0 + + def test_actions_get_by_id(self, primary_ips_client, response_get_actions): + primary_ips_client._client.request.return_value = { + "action": response_get_actions["actions"][0] + } + action = primary_ips_client.actions.get_by_id(13) + + primary_ips_client._client.request.assert_called_with( + url="/primary_ips/actions/13", method="GET" + ) + + assert isinstance(action, BoundAction) + assert action._client == primary_ips_client._client.actions + assert action.id == 13 + assert action.command == "assign_primary_ip" + + def test_actions_get_list(self, primary_ips_client, response_get_actions): + primary_ips_client._client.request.return_value = response_get_actions + result = primary_ips_client.actions.get_list() + + primary_ips_client._client.request.assert_called_with( + url="/primary_ips/actions", + method="GET", + params={}, + ) + + actions = result.actions + assert result.meta is None + + assert len(actions) == 1 + assert isinstance(actions[0], BoundAction) + assert actions[0]._client == primary_ips_client._client.actions + assert actions[0].id == 13 + assert actions[0].command == "assign_primary_ip" + + def test_actions_get_all(self, primary_ips_client, response_get_actions): + primary_ips_client._client.request.return_value = response_get_actions + actions = primary_ips_client.actions.get_all() + + primary_ips_client._client.request.assert_called_with( + url="/primary_ips/actions", + method="GET", + params={"page": 1, "per_page": 50}, + ) + + assert len(actions) == 1 + assert isinstance(actions[0], BoundAction) + assert actions[0]._client == primary_ips_client._client.actions + assert actions[0].id == 13 + assert actions[0].command == "assign_primary_ip" diff --git a/tests/unit/servers/test_client.py b/tests/unit/servers/test_client.py index 6402d7db..2490ecfa 100644 --- a/tests/unit/servers/test_client.py +++ b/tests/unit/servers/test_client.py @@ -147,6 +147,7 @@ def test_get_actions_list( assert len(actions) == 1 assert isinstance(actions[0], BoundAction) + assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "start_server" @@ -167,6 +168,7 @@ def test_get_actions( assert len(actions) == 1 assert isinstance(actions[0], BoundAction) + assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "start_server" @@ -604,11 +606,17 @@ def test_create_with_datacenter( ) bound_server = response.server + bound_action = response.action assert bound_server._client is servers_client assert bound_server.id == 1 assert bound_server.name == "my-server" + assert isinstance(bound_action, BoundAction) + assert bound_action._client == servers_client._client.actions + assert bound_action.id == 1 + assert bound_action.command == "create_server" + def test_create_with_location(self, servers_client, response_create_simple_server): servers_client._client.request.return_value = response_create_simple_server response = servers_client.create( @@ -1218,3 +1226,53 @@ def test_change_alias_ips( assert action.id == 1 assert action.progress == 0 assert action.command == "change_alias_ips" + + def test_actions_get_by_id(self, servers_client, response_get_actions): + servers_client._client.request.return_value = { + "action": response_get_actions["actions"][0] + } + action = servers_client.actions.get_by_id(13) + + servers_client._client.request.assert_called_with( + url="/servers/actions/13", method="GET" + ) + + assert isinstance(action, BoundAction) + assert action._client == servers_client._client.actions + assert action.id == 13 + assert action.command == "start_server" + + def test_actions_get_list(self, servers_client, response_get_actions): + servers_client._client.request.return_value = response_get_actions + result = servers_client.actions.get_list() + + servers_client._client.request.assert_called_with( + url="/servers/actions", + method="GET", + params={}, + ) + + actions = result.actions + assert result.meta is None + + assert len(actions) == 1 + assert isinstance(actions[0], BoundAction) + assert actions[0]._client == servers_client._client.actions + assert actions[0].id == 13 + assert actions[0].command == "start_server" + + def test_actions_get_all(self, servers_client, response_get_actions): + servers_client._client.request.return_value = response_get_actions + actions = servers_client.actions.get_all() + + servers_client._client.request.assert_called_with( + url="/servers/actions", + method="GET", + params={"page": 1, "per_page": 50}, + ) + + assert len(actions) == 1 + assert isinstance(actions[0], BoundAction) + assert actions[0]._client == servers_client._client.actions + assert actions[0].id == 13 + assert actions[0].command == "start_server" diff --git a/tests/unit/volumes/test_client.py b/tests/unit/volumes/test_client.py index 0ed3fb1d..bb8bed00 100644 --- a/tests/unit/volumes/test_client.py +++ b/tests/unit/volumes/test_client.py @@ -52,6 +52,7 @@ def test_get_actions(self, hetzner_client, bound_volume, response_get_actions): assert len(actions) == 1 assert isinstance(actions[0], BoundAction) + assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "attach_volume" @@ -384,3 +385,53 @@ def test_resize(self, volumes_client, volume, generic_action): ) assert action.id == 1 assert action.progress == 0 + + def test_actions_get_by_id(self, volumes_client, response_get_actions): + volumes_client._client.request.return_value = { + "action": response_get_actions["actions"][0] + } + action = volumes_client.actions.get_by_id(13) + + volumes_client._client.request.assert_called_with( + url="/volumes/actions/13", method="GET" + ) + + assert isinstance(action, BoundAction) + assert action._client == volumes_client._client.actions + assert action.id == 13 + assert action.command == "attach_volume" + + def test_actions_get_list(self, volumes_client, response_get_actions): + volumes_client._client.request.return_value = response_get_actions + result = volumes_client.actions.get_list() + + volumes_client._client.request.assert_called_with( + url="/volumes/actions", + method="GET", + params={}, + ) + + actions = result.actions + assert result.meta is None + + assert len(actions) == 1 + assert isinstance(actions[0], BoundAction) + assert actions[0]._client == volumes_client._client.actions + assert actions[0].id == 13 + assert actions[0].command == "attach_volume" + + def test_actions_get_all(self, volumes_client, response_get_actions): + volumes_client._client.request.return_value = response_get_actions + actions = volumes_client.actions.get_all() + + volumes_client._client.request.assert_called_with( + url="/volumes/actions", + method="GET", + params={"page": 1, "per_page": 50}, + ) + + assert len(actions) == 1 + assert isinstance(actions[0], BoundAction) + assert actions[0]._client == volumes_client._client.actions + assert actions[0].id == 13 + assert actions[0].command == "attach_volume" From 6bf03cb9ab1203f172e1634d28a99a7cb3210ad0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Aug 2023 13:04:43 +0200 Subject: [PATCH 098/406] deps: update pre-commit hook pre-commit/mirrors-prettier to v3.0.2 (#287) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e21721ae..4869ed61 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.1 + rev: v3.0.2 hooks: - id: prettier files: \.(md|ya?ml|js|css)$ From e61300eda7f0ba15e0a91cce3e4b8f7542ed42c8 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Thu, 17 Aug 2023 14:17:53 +0200 Subject: [PATCH 099/406] docs: fail on warning (#289) * docs: fail on warning Make sure to catch warnings during the docs build process. * docs: fix docstrings warnings --- .readthedocs.yaml | 1 + hcloud/actions/client.py | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 7d43aa60..dcda10d7 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -14,6 +14,7 @@ build: # Build documentation in the "docs/" directory with Sphinx sphinx: configuration: docs/conf.py + fail_on_warning: true # Optionally build your docs in additional formats such as PDF and ePub formats: [pdf, epub] diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index 63e1d884..2433bcdc 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -129,10 +129,10 @@ def get_list( ) -> ActionsPageResult: """ .. deprecated:: 1.28 - Use :func:`client..actions.get_list` instead, - e.g. using :attr:`hcloud.certificates.client.CertificatesClient.actions`. + Use :func:`client..actions.get_list` instead, + e.g. using :attr:`hcloud.certificates.client.CertificatesClient.actions`. - `Starting 1 October 2023, it will no longer be available. `_ + `Starting 1 October 2023, it will no longer be available. `_ """ warnings.warn( "The 'client.actions.get_list' method is deprecated, please use the " @@ -150,10 +150,10 @@ def get_all( ) -> list[BoundAction]: """ .. deprecated:: 1.28 - Use :func:`client..actions.get_all` instead, - e.g. using :attr:`hcloud.certificates.client.CertificatesClient.actions`. + Use :func:`client..actions.get_all` instead, + e.g. using :attr:`hcloud.certificates.client.CertificatesClient.actions`. - `Starting 1 October 2023, it will no longer be available. `_ + `Starting 1 October 2023, it will no longer be available. `_ """ warnings.warn( "The 'client.actions.get_all' method is deprecated, please use the " From 5780418f00a42e20cccacec6e030e464105807ba Mon Sep 17 00:00:00 2001 From: Jonas L Date: Thu, 17 Aug 2023 14:39:58 +0200 Subject: [PATCH 100/406] feat: add load balancer target health status field (#288) Related to ansible-collections/hetzner.hcloud#221 Docs: https://docs.hetzner.cloud/#load-balancers-get-a-load-balancer --- docs/api.clients.load_balancers.rst | 3 +++ hcloud/load_balancers/__init__.py | 1 + hcloud/load_balancers/client.py | 12 ++++++++++++ hcloud/load_balancers/domain.py | 20 ++++++++++++++++++++ 4 files changed, 36 insertions(+) diff --git a/docs/api.clients.load_balancers.rst b/docs/api.clients.load_balancers.rst index f407f20f..771eadfc 100644 --- a/docs/api.clients.load_balancers.rst +++ b/docs/api.clients.load_balancers.rst @@ -26,6 +26,9 @@ LoadBalancerClient .. autoclass:: hcloud.load_balancers.domain.LoadBalancerTarget :members: +.. autoclass:: hcloud.load_balancers.domain.LoadBalancerTargetHealthStatus + :members: + .. autoclass:: hcloud.load_balancers.domain.LoadBalancerTargetLabelSelector :members: diff --git a/hcloud/load_balancers/__init__.py b/hcloud/load_balancers/__init__.py index 76106a55..4ac79ce5 100644 --- a/hcloud/load_balancers/__init__.py +++ b/hcloud/load_balancers/__init__.py @@ -16,6 +16,7 @@ LoadBalancerService, LoadBalancerServiceHttp, LoadBalancerTarget, + LoadBalancerTargetHealthStatus, LoadBalancerTargetIP, LoadBalancerTargetLabelSelector, PrivateNet, diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index b999ddc0..72325f1d 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -20,6 +20,7 @@ LoadBalancerService, LoadBalancerServiceHttp, LoadBalancerTarget, + LoadBalancerTargetHealthStatus, LoadBalancerTargetIP, LoadBalancerTargetLabelSelector, PrivateNet, @@ -83,6 +84,17 @@ def __init__(self, client: LoadBalancersClient, data: dict, complete: bool = Tru tmp_target.use_private_ip = target["use_private_ip"] elif target["type"] == "ip": tmp_target.ip = LoadBalancerTargetIP(ip=target["ip"]["ip"]) + + target_health_status = target.get("health_status") + if target_health_status is not None: + tmp_target.health_status = [ + LoadBalancerTargetHealthStatus( + listen_port=target_health_status_item["listen_port"], + status=target_health_status_item["status"], + ) + for target_health_status_item in target_health_status + ] + tmp_targets.append(tmp_target) data["targets"] = tmp_targets diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index c22aa27b..c5810d67 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -313,6 +313,8 @@ class LoadBalancerTarget(BaseDomain): Target IP :param use_private_ip: bool use the private IP instead of primary public IP + :param health_status: list + List of health statuses of the services on this target. Only present for target types "server" and "ip". """ def __init__( @@ -322,12 +324,14 @@ def __init__( label_selector: LoadBalancerTargetLabelSelector | None = None, ip: LoadBalancerTargetIP | None = None, use_private_ip: bool | None = None, + health_status: list[LoadBalancerTargetHealthStatus] | None = None, ): self.type = type self.server = server self.label_selector = label_selector self.ip = ip self.use_private_ip = use_private_ip + self.health_status = health_status def to_payload(self) -> dict[str, Any]: payload: dict[str, Any] = { @@ -354,6 +358,22 @@ def to_payload(self) -> dict[str, Any]: return payload +class LoadBalancerTargetHealthStatus(BaseDomain): + """LoadBalancerTargetHealthStatus Domain + + :param listen_port: Load Balancer Target listen port + :param status: Load Balancer Target status. Choices: healthy, unhealthy, unknown + """ + + def __init__( + self, + listen_port: int | None = None, + status: str | None = None, + ): + self.listen_port = listen_port + self.status = status + + class LoadBalancerTargetLabelSelector(BaseDomain): """LoadBalancerTargetLabelSelector Domain From 7cad30fcea322517c70de43d41e31da8be4c7925 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Thu, 17 Aug 2023 14:45:35 +0200 Subject: [PATCH 101/406] chore(main): release 1.28.0 (#285) Co-authored-by: Jonas L --- CHANGELOG.md | 20 ++++++++++++++++++++ hcloud/__version__.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99b8ffda..61524a40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [1.28.0](https://github.com/hetznercloud/hcloud-python/compare/v1.27.2...v1.28.0) (2023-08-17) + + +### Features + +* add load balancer target health status field ([#288](https://github.com/hetznercloud/hcloud-python/issues/288)) ([5780418](https://github.com/hetznercloud/hcloud-python/commit/5780418f00a42e20cccacec6e030e464105807ba)) +* implement resource actions clients ([#252](https://github.com/hetznercloud/hcloud-python/issues/252)) ([4bb9a97](https://github.com/hetznercloud/hcloud-python/commit/4bb9a9730eadea9fd0569d5d11b7585dbb5da157)) + + +### Dependencies + +* update dependency coverage to >=7.3,<7.4 ([#286](https://github.com/hetznercloud/hcloud-python/issues/286)) ([a4df4fa](https://github.com/hetznercloud/hcloud-python/commit/a4df4fa1cc7a17e1afdea1c33f4428a8a594a011)) +* update dependency mypy to >=1.5,<1.6 ([#284](https://github.com/hetznercloud/hcloud-python/issues/284)) ([9dd5c81](https://github.com/hetznercloud/hcloud-python/commit/9dd5c8110bf679c13e8e6ba08e760019b4dae706)) +* update pre-commit hook pre-commit/mirrors-prettier to v3.0.2 ([#287](https://github.com/hetznercloud/hcloud-python/issues/287)) ([6bf03cb](https://github.com/hetznercloud/hcloud-python/commit/6bf03cb9ab1203f172e1634d28a99a7cb3210ad0)) + + +### Documentation + +* fail on warning ([#289](https://github.com/hetznercloud/hcloud-python/issues/289)) ([e61300e](https://github.com/hetznercloud/hcloud-python/commit/e61300eda7f0ba15e0a91cce3e4b8f7542ed42c8)) + ## [1.27.2](https://github.com/hetznercloud/hcloud-python/compare/v1.27.1...v1.27.2) (2023-08-09) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index 71617d65..112641f0 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1,3 +1,3 @@ from __future__ import annotations -VERSION = "1.27.2" # x-release-please-version +VERSION = "1.28.0" # x-release-please-version From f635c94c23b8ae49283b9b7fcb4fe7b948b203b9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 08:53:06 +0200 Subject: [PATCH 102/406] deps: update dependency sphinx to v7 (#211) * deps: update dependency sphinx to v7 * deps: update dependency sphinx-rtd-theme to >=1.3,<1.4 --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: jo --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 2714b0a0..a812ade1 100644 --- a/setup.py +++ b/setup.py @@ -44,8 +44,8 @@ ], extras_require={ "docs": [ - "sphinx>=6.2.1,<7.0", - "sphinx-rtd-theme>=1.2.2,<1.3", + "sphinx>=7.1.2,<7.2", + "sphinx-rtd-theme>=1.3.0,<1.4", "myst-parser>=2.0.0,<2.1", "watchdog>=3.0.0,<3.1", ], From 10234ea7bf51a427b18f2b5605d9ffa7ac5f5ee8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 08:55:53 +0200 Subject: [PATCH 103/406] deps: update dependency sphinx to >=7.2.2,<7.3 (#291) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a812ade1..c52a9cfb 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ ], extras_require={ "docs": [ - "sphinx>=7.1.2,<7.2", + "sphinx>=7.2.2,<7.3", "sphinx-rtd-theme>=1.3.0,<1.4", "myst-parser>=2.0.0,<2.1", "watchdog>=3.0.0,<3.1", From 982e35b47e36e24b140fcad84817a4b28dc09c2d Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 25 Aug 2023 16:14:21 +0200 Subject: [PATCH 104/406] ci: explicitly declare pre-commit job (#293) --- .gitlab-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1f3158fe..f5ef29aa 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,6 +7,9 @@ include: stages: - test +pre-commit: + extends: [.pre-commit] + lint: stage: test From c02b4468f0e499791bbee8fe48fe7a737985df1f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 12:05:09 +0200 Subject: [PATCH 105/406] deps: update actions/checkout action to v4 (#295) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/lint.yml | 4 ++-- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 17b04718..a824f818 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,7 +9,7 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup python uses: actions/setup-python@v4 @@ -25,7 +25,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup python uses: actions/setup-python@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9da9793..6e552128 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 92d72cde..049925ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: name: Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup python uses: actions/setup-python@v4 From 381e336ff1259fa26cb6abae3b7341cb16229a4b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 12:12:37 +0200 Subject: [PATCH 106/406] deps: update pre-commit hook pre-commit/mirrors-prettier to v3.0.3 (#294) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4869ed61..297f25fb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.2 + rev: v3.0.3 hooks: - id: prettier files: \.(md|ya?ml|js|css)$ From 4374a7be9f244a72f1fc0c2dd76357cf63f19bfd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 08:46:19 +0200 Subject: [PATCH 107/406] deps: update pre-commit hook psf/black to v23.9.1 (#296) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 297f25fb..c7caa5db 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: - id: isort - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 23.9.1 hooks: - id: black From f238c2e99fe922eaf69e622f74ae8beff193dee9 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Tue, 12 Sep 2023 08:29:37 +0200 Subject: [PATCH 108/406] test: add pylint linter (#248) * chore: add pylint linter * refactor: fix linting errors * docs: fix incomplete docstring --- Makefile | 1 + hcloud/_client.py | 2 +- hcloud/actions/client.py | 1 + hcloud/core/client.py | 1 + hcloud/core/domain.py | 14 +++++++++++--- hcloud/firewalls/client.py | 25 ++++++++++++++----------- hcloud/firewalls/domain.py | 6 ++++++ hcloud/floating_ips/client.py | 1 + hcloud/hcloud.py | 1 + hcloud/helpers/labels.py | 22 ++++++++++++++-------- hcloud/images/client.py | 1 + hcloud/images/domain.py | 1 + hcloud/load_balancers/client.py | 1 + hcloud/load_balancers/domain.py | 8 ++++++++ hcloud/networks/client.py | 1 + hcloud/primary_ips/client.py | 1 + hcloud/servers/client.py | 2 ++ hcloud/servers/domain.py | 1 + hcloud/volumes/client.py | 3 ++- pyproject.toml | 26 ++++++++++++++++++++++++++ setup.py | 1 + 21 files changed, 96 insertions(+), 24 deletions(-) diff --git a/Makefile b/Makefile index 338c547f..b63a6ff2 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ venv: venv/bin/pip install -e .[docs,test] lint: venv + venv/bin/pylint hcloud venv/bin/mypy hcloud test: venv diff --git a/hcloud/_client.py b/hcloud/_client.py index db28fcd6..a12f204c 100644 --- a/hcloud/_client.py +++ b/hcloud/_client.py @@ -42,7 +42,7 @@ def __init__( poll_interval: int = 1, timeout: float | tuple[float, float] | None = None, ): - """Create an new Client instance + """Create a new Client instance :param token: Hetzner Cloud API token :param api_endpoint: Hetzner Cloud API endpoint diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index 2433bcdc..6ac87cbc 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -27,6 +27,7 @@ def wait_until_finished(self, max_retries: int = 100) -> None: while self.status == Action.STATUS_RUNNING: if max_retries > 0: self.reload() + # pylint: disable=protected-access time.sleep(self._client._client.poll_interval) max_retries = max_retries - 1 else: diff --git a/hcloud/core/client.py b/hcloud/core/client.py index 16c37e5d..1d4edfd1 100644 --- a/hcloud/core/client.py +++ b/hcloud/core/client.py @@ -45,6 +45,7 @@ def _iter_pages( # type: ignore[no-untyped-def] def _get_first_by(self, **kwargs): # type: ignore[no-untyped-def] assert hasattr(self, "get_list") + # pylint: disable=no-member entities, _ = self.get_list(**kwargs) return entities[0] if entities else None diff --git a/hcloud/core/domain.py b/hcloud/core/domain.py index 21aed341..692f7488 100644 --- a/hcloud/core/domain.py +++ b/hcloud/core/domain.py @@ -6,6 +6,9 @@ class BaseDomain: @classmethod def from_dict(cls, data: dict): # type: ignore[no-untyped-def] + """ + Build the domain object from the data dict. + """ supported_data = {k: v for k, v in data.items() if k in cls.__slots__} return cls(**supported_data) @@ -22,12 +25,14 @@ class DomainIdentityMixin: @property def id_or_name(self) -> int | str: + """ + Return the first defined value, and fails if none is defined. + """ if self.id is not None: return self.id - elif self.name is not None: + if self.name is not None: return self.name - else: - raise ValueError("id or name must be set") + raise ValueError("id or name must be set") class Pagination(BaseDomain): @@ -65,6 +70,9 @@ def __init__(self, pagination: Pagination | None = None): @classmethod def parse_meta(cls, response: dict) -> Meta | None: + """ + If present, extract the meta details from the response and return a meta object. + """ meta = None if response and "meta" in response: meta = cls() diff --git a/hcloud/firewalls/client.py b/hcloud/firewalls/client.py index b9b341b6..b77c0663 100644 --- a/hcloud/firewalls/client.py +++ b/hcloud/firewalls/client.py @@ -39,29 +39,32 @@ def __init__(self, client: FirewallsClient, data: dict, complete: bool = True): applied_to = data.get("applied_to", []) if applied_to: + # pylint: disable=import-outside-toplevel from ..servers import BoundServer - ats = [] - for a in applied_to: - if a["type"] == FirewallResource.TYPE_SERVER: - ats.append( + data_applied_to = [] + for firewall_resource in applied_to: + if firewall_resource["type"] == FirewallResource.TYPE_SERVER: + data_applied_to.append( FirewallResource( - type=a["type"], + type=firewall_resource["type"], server=BoundServer( - client._client.servers, a["server"], complete=False + client._client.servers, + firewall_resource["server"], + complete=False, ), ) ) - elif a["type"] == FirewallResource.TYPE_LABEL_SELECTOR: - ats.append( + elif firewall_resource["type"] == FirewallResource.TYPE_LABEL_SELECTOR: + data_applied_to.append( FirewallResource( - type=a["type"], + type=firewall_resource["type"], label_selector=FirewallResourceLabelSelector( - selector=a["label_selector"]["selector"] + selector=firewall_resource["label_selector"]["selector"] ), ) ) - data["applied_to"] = ats + data["applied_to"] = data_applied_to super().__init__(client, data, complete) diff --git a/hcloud/firewalls/domain.py b/hcloud/firewalls/domain.py index f9c7368d..44bd7c9b 100644 --- a/hcloud/firewalls/domain.py +++ b/hcloud/firewalls/domain.py @@ -108,6 +108,9 @@ def __init__( self.description = description def to_payload(self) -> dict[str, Any]: + """ + Generates the request payload from this domain object. + """ payload: dict[str, Any] = { "direction": self.direction, "protocol": self.protocol, @@ -151,6 +154,9 @@ def __init__( self.label_selector = label_selector def to_payload(self) -> dict[str, Any]: + """ + Generates the request payload from this domain object. + """ payload: dict[str, Any] = {"type": self.type} if self.server is not None: payload["server"] = {"id": self.server.id} diff --git a/hcloud/floating_ips/client.py b/hcloud/floating_ips/client.py index 49d7c8e5..31e54773 100644 --- a/hcloud/floating_ips/client.py +++ b/hcloud/floating_ips/client.py @@ -19,6 +19,7 @@ class BoundFloatingIP(BoundModelBase): model = FloatingIP def __init__(self, client: FloatingIPsClient, data: dict, complete: bool = True): + # pylint: disable=import-outside-toplevel from ..servers import BoundServer server = data.get("server") diff --git a/hcloud/hcloud.py b/hcloud/hcloud.py index df67a5be..9de1cfe5 100644 --- a/hcloud/hcloud.py +++ b/hcloud/hcloud.py @@ -8,4 +8,5 @@ stacklevel=2, ) +# pylint: disable=wildcard-import,wrong-import-position,unused-wildcard-import from ._client import * # noqa diff --git a/hcloud/helpers/labels.py b/hcloud/helpers/labels.py index d5af8bcc..36041578 100644 --- a/hcloud/helpers/labels.py +++ b/hcloud/helpers/labels.py @@ -18,10 +18,10 @@ def validate(labels: dict[str, str]) -> bool: :return: bool """ - for k, v in labels.items(): - if LabelValidator.KEY_REGEX.match(k) is None: + for key, value in labels.items(): + if LabelValidator.KEY_REGEX.match(key) is None: return False - if LabelValidator.VALUE_REGEX.match(v) is None: + if LabelValidator.VALUE_REGEX.match(value) is None: return False return True @@ -32,9 +32,15 @@ def validate_verbose(labels: dict[str, str]) -> tuple[bool, str]: :return: bool, str """ - for k, v in labels.items(): - if LabelValidator.KEY_REGEX.match(k) is None: - return False, f"label key {k} is not correctly formatted" - if LabelValidator.VALUE_REGEX.match(v) is None: - return False, f"label value {v} (key: {k}) is not correctly formatted" + for key, value in labels.items(): + if LabelValidator.KEY_REGEX.match(key) is None: + return ( + False, + f"label key {key} is not correctly formatted", + ) + if LabelValidator.VALUE_REGEX.match(value) is None: + return ( + False, + f"label value {value} (key: {key}) is not correctly formatted", + ) return True, "" diff --git a/hcloud/images/client.py b/hcloud/images/client.py index dee910f0..4d8cd109 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -16,6 +16,7 @@ class BoundImage(BoundModelBase): model = Image def __init__(self, client: ImagesClient, data: dict): + # pylint: disable=import-outside-toplevel from ..servers import BoundServer created_from = data.get("created_from") diff --git a/hcloud/images/domain.py b/hcloud/images/domain.py index 0f4add4f..37d6d9f7 100644 --- a/hcloud/images/domain.py +++ b/hcloud/images/domain.py @@ -71,6 +71,7 @@ class Image(BaseDomain, DomainIdentityMixin): "deprecated", ) + # pylint: disable=too-many-locals def __init__( self, id: int | None = None, diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index 72325f1d..301240b8 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -39,6 +39,7 @@ class BoundLoadBalancer(BoundModelBase): model = LoadBalancer + # pylint: disable=too-many-branches,too-many-locals def __init__(self, client: LoadBalancersClient, data: dict, complete: bool = True): algorithm = data.get("algorithm") if algorithm: diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index c5810d67..212bbdc0 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -69,6 +69,7 @@ class LoadBalancer(BaseDomain): "included_traffic", ) + # pylint: disable=too-many-locals def __init__( self, id: int, @@ -137,7 +138,11 @@ def __init__( self.health_check = health_check self.http = http + # pylint: disable=too-many-branches def to_payload(self) -> dict[str, Any]: + """ + Generates the request payload from this domain object. + """ payload: dict[str, Any] = {} if self.protocol is not None: @@ -334,6 +339,9 @@ def __init__( self.health_status = health_status def to_payload(self) -> dict[str, Any]: + """ + Generates the request payload from this domain object. + """ payload: dict[str, Any] = { "type": self.type, } diff --git a/hcloud/networks/client.py b/hcloud/networks/client.py index 36eaaa40..f5894e7e 100644 --- a/hcloud/networks/client.py +++ b/hcloud/networks/client.py @@ -26,6 +26,7 @@ def __init__(self, client: NetworksClient, data: dict, complete: bool = True): routes = [NetworkRoute.from_dict(route) for route in routes] data["routes"] = routes + # pylint: disable=import-outside-toplevel from ..servers import BoundServer servers = data.get("servers", []) diff --git a/hcloud/primary_ips/client.py b/hcloud/primary_ips/client.py index acd2f69e..a55dfc7f 100644 --- a/hcloud/primary_ips/client.py +++ b/hcloud/primary_ips/client.py @@ -17,6 +17,7 @@ class BoundPrimaryIP(BoundModelBase): model = PrimaryIP def __init__(self, client: PrimaryIPsClient, data: dict, complete: bool = True): + # pylint: disable=import-outside-toplevel from ..datacenters import BoundDatacenter datacenter = data.get("datacenter", {}) diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index d19180c5..433c537c 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -47,6 +47,7 @@ class BoundServer(BoundModelBase): model = Server + # pylint: disable=too-many-locals def __init__(self, client: ServersClient, data: dict, complete: bool = True): datacenter = data.get("datacenter") if datacenter is not None: @@ -540,6 +541,7 @@ def get_by_name(self, name: str) -> BoundServer | None: """ return self._get_first_by(name=name) + # pylint: disable=too-many-branches,too-many-locals def create( self, name: str, diff --git a/hcloud/servers/domain.py b/hcloud/servers/domain.py index 60cc7450..2d55fd30 100644 --- a/hcloud/servers/domain.py +++ b/hcloud/servers/domain.py @@ -104,6 +104,7 @@ class Server(BaseDomain): "placement_group", ) + # pylint: disable=too-many-locals def __init__( self, id: int, diff --git a/hcloud/volumes/client.py b/hcloud/volumes/client.py index 9017ff49..cb9e8839 100644 --- a/hcloud/volumes/client.py +++ b/hcloud/volumes/client.py @@ -23,6 +23,7 @@ def __init__(self, client: VolumesClient, data: dict, complete: bool = True): if location is not None: data["location"] = BoundLocation(client._client.locations, location) + # pylint: disable=import-outside-toplevel from ..servers import BoundServer server = data.get("server") @@ -254,7 +255,7 @@ def create( if size <= 0: raise ValueError("size must be greater than 0") - if not (bool(location) ^ bool(server)): + if not bool(location) ^ bool(server): raise ValueError("only one of server or location must be provided") data: dict[str, Any] = {"name": name, "size": size} diff --git a/pyproject.toml b/pyproject.toml index beb8fd80..2b768342 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,3 +12,29 @@ source = ["hcloud"] [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" + +[tool.pylint.main] +py-version = "3.8" +recursive = true +jobs = 0 + +[tool.pylint.reports] +output-format = "colorized" + +[tool.pylint.basic] +good-names = ["i", "j", "k", "ex", "_", "ip", "id"] + +[tool.pylint."messages control"] +disable = [ + "fixme", + "line-too-long", + "missing-class-docstring", + "missing-module-docstring", + "redefined-builtin", + # Consider disabling line-by-line + "too-few-public-methods", + "too-many-public-methods", + "too-many-arguments", + "too-many-instance-attributes", + "too-many-lines", +] diff --git a/setup.py b/setup.py index c52a9cfb..b4a20b6e 100644 --- a/setup.py +++ b/setup.py @@ -51,6 +51,7 @@ ], "test": [ "coverage>=7.3,<7.4", + "pylint>=2.17.4,<2.18", "pytest>=7.4,<7.5", "mypy>=1.5,<1.6", "types-python-dateutil", From 4bbd0ccb0f606e2f90f8242951d3f4d9b86d7aea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 07:16:37 +0200 Subject: [PATCH 109/406] deps: update pre-commit hook asottile/pyupgrade to v3.11.0 (#298) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c7caa5db..9ef47565 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: exclude: ^CHANGELOG.md$ - repo: https://github.com/asottile/pyupgrade - rev: v3.10.1 + rev: v3.11.0 hooks: - id: pyupgrade args: [--py38-plus] From 2f9fcd7bb80efb8da6eafab0ee70a8dda93eb6f1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 20 Sep 2023 18:11:47 +0200 Subject: [PATCH 110/406] deps: update pre-commit hook asottile/pyupgrade to v3.11.1 (#299) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9ef47565..25db3b8c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: exclude: ^CHANGELOG.md$ - repo: https://github.com/asottile/pyupgrade - rev: v3.11.0 + rev: v3.11.1 hooks: - id: pyupgrade args: [--py38-plus] From 951dbf3e3b3816ffaeb44a583251a5a3a4b90b70 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 12:03:06 +0200 Subject: [PATCH 111/406] deps: update pre-commit hook asottile/pyupgrade to v3.13.0 (#301) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 25db3b8c..9926274d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: exclude: ^CHANGELOG.md$ - repo: https://github.com/asottile/pyupgrade - rev: v3.11.1 + rev: v3.13.0 hooks: - id: pyupgrade args: [--py38-plus] From 55d2b2043ec1e3a040eb9e360ca0dc0c299ad60f Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 25 Sep 2023 12:09:27 +0200 Subject: [PATCH 112/406] feat(firewalls): add `applied_to_resources` to `FirewallResource` (#297) Add the read-only `applied_to_resources` field to `FirewallResource` https://docs.hetzner.cloud/#firewalls-get-a-firewall --- hcloud/firewalls/__init__.py | 1 + hcloud/firewalls/client.py | 22 ++++++++++++++++++++++ hcloud/firewalls/domain.py | 24 +++++++++++++++++++++++- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/hcloud/firewalls/__init__.py b/hcloud/firewalls/__init__.py index 42bde369..5205d766 100644 --- a/hcloud/firewalls/__init__.py +++ b/hcloud/firewalls/__init__.py @@ -5,6 +5,7 @@ CreateFirewallResponse, Firewall, FirewallResource, + FirewallResourceAppliedToResources, FirewallResourceLabelSelector, FirewallRule, ) diff --git a/hcloud/firewalls/client.py b/hcloud/firewalls/client.py index b77c0663..48505eaf 100644 --- a/hcloud/firewalls/client.py +++ b/hcloud/firewalls/client.py @@ -8,6 +8,7 @@ CreateFirewallResponse, Firewall, FirewallResource, + FirewallResourceAppliedToResources, FirewallResourceLabelSelector, FirewallRule, ) @@ -44,6 +45,24 @@ def __init__(self, client: FirewallsClient, data: dict, complete: bool = True): data_applied_to = [] for firewall_resource in applied_to: + applied_to_resources = None + if firewall_resource.get("applied_to_resources"): + applied_to_resources = [ + FirewallResourceAppliedToResources( + type=resource["type"], + server=( + BoundServer( + client._client.servers, + resource.get("server"), + complete=False, + ) + if resource.get("server") is not None + else None + ), + ) + for resource in firewall_resource.get("applied_to_resources") + ] + if firewall_resource["type"] == FirewallResource.TYPE_SERVER: data_applied_to.append( FirewallResource( @@ -53,6 +72,7 @@ def __init__(self, client: FirewallsClient, data: dict, complete: bool = True): firewall_resource["server"], complete=False, ), + applied_to_resources=applied_to_resources, ) ) elif firewall_resource["type"] == FirewallResource.TYPE_LABEL_SELECTOR: @@ -62,8 +82,10 @@ def __init__(self, client: FirewallsClient, data: dict, complete: bool = True): label_selector=FirewallResourceLabelSelector( selector=firewall_resource["label_selector"]["selector"] ), + applied_to_resources=applied_to_resources, ) ) + data["applied_to"] = data_applied_to super().__init__(client, data, complete) diff --git a/hcloud/firewalls/domain.py b/hcloud/firewalls/domain.py index 44bd7c9b..d294fb8d 100644 --- a/hcloud/firewalls/domain.py +++ b/hcloud/firewalls/domain.py @@ -134,9 +134,11 @@ class FirewallResource: Server the Firewall is applied to :param label_selector: Optional[FirewallResourceLabelSelector] Label Selector for Servers the Firewall should be applied to + :param applied_to_resources: (read-only) List of effective resources the firewall is + applied to. """ - __slots__ = ("type", "server", "label_selector") + __slots__ = ("type", "server", "label_selector", "applied_to_resources") TYPE_SERVER = "server" """Firewall Used By Type Server""" @@ -148,10 +150,12 @@ def __init__( type: str, server: Server | BoundServer | None = None, label_selector: FirewallResourceLabelSelector | None = None, + applied_to_resources: list[FirewallResourceAppliedToResources] | None = None, ): self.type = type self.server = server self.label_selector = label_selector + self.applied_to_resources = applied_to_resources def to_payload(self) -> dict[str, Any]: """ @@ -166,6 +170,24 @@ def to_payload(self) -> dict[str, Any]: return payload +class FirewallResourceAppliedToResources(BaseDomain): + """Firewall Resource applied to Domain + + :param type: Type of resource referenced + :param server: Server the Firewall is applied to + """ + + __slots__ = ("type", "server") + + def __init__( + self, + type: str, + server: BoundServer | None = None, + ): + self.type = type + self.server = server + + class FirewallResourceLabelSelector(BaseDomain): """FirewallResourceLabelSelector Domain From 6d46d06c42e2e86e88b32a74d7fbd588911cc8ad Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 25 Sep 2023 12:23:22 +0200 Subject: [PATCH 113/406] feat: add domain attribute type hints to bound models (#300) --- hcloud/actions/client.py | 2 +- hcloud/certificates/client.py | 2 +- hcloud/datacenters/client.py | 2 +- hcloud/firewalls/client.py | 2 +- hcloud/floating_ips/client.py | 2 +- hcloud/images/client.py | 2 +- hcloud/isos/client.py | 2 +- hcloud/load_balancer_types/client.py | 2 +- hcloud/load_balancers/client.py | 2 +- hcloud/locations/client.py | 2 +- hcloud/networks/client.py | 2 +- hcloud/placement_groups/client.py | 2 +- hcloud/primary_ips/client.py | 2 +- hcloud/server_types/client.py | 2 +- hcloud/servers/client.py | 2 +- hcloud/ssh_keys/client.py | 2 +- hcloud/volumes/client.py | 2 +- tests/unit/core/test_client.py | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index 6ac87cbc..a188f624 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -11,7 +11,7 @@ from .._client import Client -class BoundAction(BoundModelBase): +class BoundAction(BoundModelBase, Action): _client: ActionsClient model = Action diff --git a/hcloud/certificates/client.py b/hcloud/certificates/client.py index e368a952..a5fe1d77 100644 --- a/hcloud/certificates/client.py +++ b/hcloud/certificates/client.py @@ -15,7 +15,7 @@ from .._client import Client -class BoundCertificate(BoundModelBase): +class BoundCertificate(BoundModelBase, Certificate): _client: CertificatesClient model = Certificate diff --git a/hcloud/datacenters/client.py b/hcloud/datacenters/client.py index ab5aa5e0..1be1e126 100644 --- a/hcloud/datacenters/client.py +++ b/hcloud/datacenters/client.py @@ -11,7 +11,7 @@ from .._client import Client -class BoundDatacenter(BoundModelBase): +class BoundDatacenter(BoundModelBase, Datacenter): _client: DatacentersClient model = Datacenter diff --git a/hcloud/firewalls/client.py b/hcloud/firewalls/client.py index 48505eaf..fbcd1008 100644 --- a/hcloud/firewalls/client.py +++ b/hcloud/firewalls/client.py @@ -17,7 +17,7 @@ from .._client import Client -class BoundFirewall(BoundModelBase): +class BoundFirewall(BoundModelBase, Firewall): _client: FirewallsClient model = Firewall diff --git a/hcloud/floating_ips/client.py b/hcloud/floating_ips/client.py index 31e54773..00600e48 100644 --- a/hcloud/floating_ips/client.py +++ b/hcloud/floating_ips/client.py @@ -13,7 +13,7 @@ from ..servers import BoundServer, Server -class BoundFloatingIP(BoundModelBase): +class BoundFloatingIP(BoundModelBase, FloatingIP): _client: FloatingIPsClient model = FloatingIP diff --git a/hcloud/images/client.py b/hcloud/images/client.py index 4d8cd109..65b7546a 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -10,7 +10,7 @@ from .._client import Client -class BoundImage(BoundModelBase): +class BoundImage(BoundModelBase, Image): _client: ImagesClient model = Image diff --git a/hcloud/isos/client.py b/hcloud/isos/client.py index 1ab5fc9f..cc46af7f 100644 --- a/hcloud/isos/client.py +++ b/hcloud/isos/client.py @@ -10,7 +10,7 @@ from .._client import Client -class BoundIso(BoundModelBase): +class BoundIso(BoundModelBase, Iso): _client: IsosClient model = Iso diff --git a/hcloud/load_balancer_types/client.py b/hcloud/load_balancer_types/client.py index fa91c01d..9a83dc70 100644 --- a/hcloud/load_balancer_types/client.py +++ b/hcloud/load_balancer_types/client.py @@ -9,7 +9,7 @@ from .._client import Client -class BoundLoadBalancerType(BoundModelBase): +class BoundLoadBalancerType(BoundModelBase, LoadBalancerType): _client: LoadBalancerTypesClient model = LoadBalancerType diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index 301240b8..56b93c8b 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -34,7 +34,7 @@ from ..networks import Network -class BoundLoadBalancer(BoundModelBase): +class BoundLoadBalancer(BoundModelBase, LoadBalancer): _client: LoadBalancersClient model = LoadBalancer diff --git a/hcloud/locations/client.py b/hcloud/locations/client.py index 2e2b6b47..047ad9d4 100644 --- a/hcloud/locations/client.py +++ b/hcloud/locations/client.py @@ -9,7 +9,7 @@ from .._client import Client -class BoundLocation(BoundModelBase): +class BoundLocation(BoundModelBase, Location): _client: LocationsClient model = Location diff --git a/hcloud/networks/client.py b/hcloud/networks/client.py index f5894e7e..d819d580 100644 --- a/hcloud/networks/client.py +++ b/hcloud/networks/client.py @@ -10,7 +10,7 @@ from .._client import Client -class BoundNetwork(BoundModelBase): +class BoundNetwork(BoundModelBase, Network): _client: NetworksClient model = Network diff --git a/hcloud/placement_groups/client.py b/hcloud/placement_groups/client.py index b551904f..fcfd86ae 100644 --- a/hcloud/placement_groups/client.py +++ b/hcloud/placement_groups/client.py @@ -10,7 +10,7 @@ from .._client import Client -class BoundPlacementGroup(BoundModelBase): +class BoundPlacementGroup(BoundModelBase, PlacementGroup): _client: PlacementGroupsClient model = PlacementGroup diff --git a/hcloud/primary_ips/client.py b/hcloud/primary_ips/client.py index a55dfc7f..ece8d88f 100644 --- a/hcloud/primary_ips/client.py +++ b/hcloud/primary_ips/client.py @@ -11,7 +11,7 @@ from ..datacenters import BoundDatacenter, Datacenter -class BoundPrimaryIP(BoundModelBase): +class BoundPrimaryIP(BoundModelBase, PrimaryIP): _client: PrimaryIPsClient model = PrimaryIP diff --git a/hcloud/server_types/client.py b/hcloud/server_types/client.py index 12cf34a0..31f56a20 100644 --- a/hcloud/server_types/client.py +++ b/hcloud/server_types/client.py @@ -9,7 +9,7 @@ from .._client import Client -class BoundServerType(BoundModelBase): +class BoundServerType(BoundModelBase, ServerType): _client: ServerTypesClient model = ServerType diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index 433c537c..5cbd48a2 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -42,7 +42,7 @@ from .domain import ServerCreatePublicNetwork -class BoundServer(BoundModelBase): +class BoundServer(BoundModelBase, Server): _client: ServersClient model = Server diff --git a/hcloud/ssh_keys/client.py b/hcloud/ssh_keys/client.py index 1c16f03d..69c1683d 100644 --- a/hcloud/ssh_keys/client.py +++ b/hcloud/ssh_keys/client.py @@ -9,7 +9,7 @@ from .._client import Client -class BoundSSHKey(BoundModelBase): +class BoundSSHKey(BoundModelBase, SSHKey): _client: SSHKeysClient model = SSHKey diff --git a/hcloud/volumes/client.py b/hcloud/volumes/client.py index cb9e8839..a4709748 100644 --- a/hcloud/volumes/client.py +++ b/hcloud/volumes/client.py @@ -13,7 +13,7 @@ from ..servers import BoundServer, Server -class BoundVolume(BoundModelBase): +class BoundVolume(BoundModelBase, Volume): _client: VolumesClient model = Volume diff --git a/tests/unit/core/test_client.py b/tests/unit/core/test_client.py index b8e424f7..584af8cc 100644 --- a/tests/unit/core/test_client.py +++ b/tests/unit/core/test_client.py @@ -20,7 +20,7 @@ def __init__(self, id, name="", description=""): self.name = name self.description = description - class BoundModel(BoundModelBase): + class BoundModel(BoundModelBase, Model): model = Model return BoundModel From f18c9a60e045743b26892eeb1fe9e5737a63c11f Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 25 Sep 2023 12:39:12 +0200 Subject: [PATCH 114/406] docs: load token from env in examples scripts (#302) --- examples/create_server.py | 11 +++++++++-- examples/list_servers.py | 11 ++++++++--- examples/usage_oop.py | 9 ++++++++- examples/usage_procedurale.py | 9 ++++++++- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/examples/create_server.py b/examples/create_server.py index 348939a2..d85c1d9d 100644 --- a/examples/create_server.py +++ b/examples/create_server.py @@ -1,11 +1,18 @@ from __future__ import annotations +from os import environ + from hcloud import Client from hcloud.images import Image from hcloud.server_types import ServerType -# Please paste your API token here between the quotes -client = Client(token="{YOUR_API_TOKEN}") +assert ( + "HCLOUD_TOKEN" in environ +), "Please export your API token in the HCLOUD_TOKEN environment variable" +token = environ["HCLOUD_TOKEN"] + +client = Client(token=token) + response = client.servers.create( name="my-server", server_type=ServerType("cx11"), diff --git a/examples/list_servers.py b/examples/list_servers.py index 005e9b6c..82524301 100644 --- a/examples/list_servers.py +++ b/examples/list_servers.py @@ -1,9 +1,14 @@ from __future__ import annotations +from os import environ + from hcloud import Client -client = Client( - token="{YOUR_API_TOKEN}" -) # Please paste your API token here between the quotes +assert ( + "HCLOUD_TOKEN" in environ +), "Please export your API token in the HCLOUD_TOKEN environment variable" +token = environ["HCLOUD_TOKEN"] + +client = Client(token=token) servers = client.servers.get_all() print(servers) diff --git a/examples/usage_oop.py b/examples/usage_oop.py index b6fcddcd..0a867cf6 100644 --- a/examples/usage_oop.py +++ b/examples/usage_oop.py @@ -1,11 +1,18 @@ from __future__ import annotations +from os import environ + from hcloud import Client from hcloud.images import Image from hcloud.server_types import ServerType +assert ( + "HCLOUD_TOKEN" in environ +), "Please export your API token in the HCLOUD_TOKEN environment variable" +token = environ["HCLOUD_TOKEN"] + # Create a client -client = Client(token="project-token") +client = Client(token=token) # Create 2 servers # Create 2 servers diff --git a/examples/usage_procedurale.py b/examples/usage_procedurale.py index 6c4aa962..8e901e60 100644 --- a/examples/usage_procedurale.py +++ b/examples/usage_procedurale.py @@ -1,12 +1,19 @@ from __future__ import annotations +from os import environ + from hcloud import Client from hcloud.images import Image from hcloud.server_types import ServerType from hcloud.servers import Server from hcloud.volumes import Volume -client = Client(token="project-token") +assert ( + "HCLOUD_TOKEN" in environ +), "Please export your API token in the HCLOUD_TOKEN environment variable" +token = environ["HCLOUD_TOKEN"] + +client = Client(token=token) # Create 2 servers response1 = client.servers.create( From 0ee759856cb1352f6cc538b7ef86a91cd20380f2 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 25 Sep 2023 12:57:31 +0200 Subject: [PATCH 115/406] fix: missing BaseDomain base class inheritance (#303) --- hcloud/datacenters/domain.py | 2 +- hcloud/firewalls/domain.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hcloud/datacenters/domain.py b/hcloud/datacenters/domain.py index 1c59bfa9..05d5f793 100644 --- a/hcloud/datacenters/domain.py +++ b/hcloud/datacenters/domain.py @@ -36,7 +36,7 @@ def __init__( self.server_types = server_types -class DatacenterServerTypes: +class DatacenterServerTypes(BaseDomain): """DatacenterServerTypes Domain :param available: List[:class:`BoundServerTypes `] diff --git a/hcloud/firewalls/domain.py b/hcloud/firewalls/domain.py index d294fb8d..22e8c517 100644 --- a/hcloud/firewalls/domain.py +++ b/hcloud/firewalls/domain.py @@ -48,7 +48,7 @@ def __init__( self.created = isoparse(created) if created else None -class FirewallRule: +class FirewallRule(BaseDomain): """Firewall Rule Domain :param direction: str @@ -125,7 +125,7 @@ def to_payload(self) -> dict[str, Any]: return payload -class FirewallResource: +class FirewallResource(BaseDomain): """Firewall Used By Domain :param type: str From 5a730756134958074fea9027033a4493c27a9aa8 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Mon, 25 Sep 2023 13:01:36 +0200 Subject: [PATCH 116/406] chore(main): release 1.29.0 (#292) --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ hcloud/__version__.py | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61524a40..7f31cec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## [1.29.0](https://github.com/hetznercloud/hcloud-python/compare/v1.28.0...v1.29.0) (2023-09-25) + + +### Features + +* add domain attribute type hints to bound models ([#300](https://github.com/hetznercloud/hcloud-python/issues/300)) ([6d46d06](https://github.com/hetznercloud/hcloud-python/commit/6d46d06c42e2e86e88b32a74d7fbd588911cc8ad)) +* **firewalls:** add `applied_to_resources` to `FirewallResource` ([#297](https://github.com/hetznercloud/hcloud-python/issues/297)) ([55d2b20](https://github.com/hetznercloud/hcloud-python/commit/55d2b2043ec1e3a040eb9e360ca0dc0c299ad60f)) + + +### Bug Fixes + +* missing BaseDomain base class inheritance ([#303](https://github.com/hetznercloud/hcloud-python/issues/303)) ([0ee7598](https://github.com/hetznercloud/hcloud-python/commit/0ee759856cb1352f6cc538b7ef86a91cd20380f2)) + + +### Dependencies + +* update actions/checkout action to v4 ([#295](https://github.com/hetznercloud/hcloud-python/issues/295)) ([c02b446](https://github.com/hetznercloud/hcloud-python/commit/c02b4468f0e499791bbee8fe48fe7a737985df1f)) +* update dependency sphinx to >=7.2.2,<7.3 ([#291](https://github.com/hetznercloud/hcloud-python/issues/291)) ([10234ea](https://github.com/hetznercloud/hcloud-python/commit/10234ea7bf51a427b18f2b5605d9ffa7ac5f5ee8)) +* update dependency sphinx to v7 ([#211](https://github.com/hetznercloud/hcloud-python/issues/211)) ([f635c94](https://github.com/hetznercloud/hcloud-python/commit/f635c94c23b8ae49283b9b7fcb4fe7b948b203b9)) +* update pre-commit hook asottile/pyupgrade to v3.11.0 ([#298](https://github.com/hetznercloud/hcloud-python/issues/298)) ([4bbd0cc](https://github.com/hetznercloud/hcloud-python/commit/4bbd0ccb0f606e2f90f8242951d3f4d9b86d7aea)) +* update pre-commit hook asottile/pyupgrade to v3.11.1 ([#299](https://github.com/hetznercloud/hcloud-python/issues/299)) ([2f9fcd7](https://github.com/hetznercloud/hcloud-python/commit/2f9fcd7bb80efb8da6eafab0ee70a8dda93eb6f1)) +* update pre-commit hook asottile/pyupgrade to v3.13.0 ([#301](https://github.com/hetznercloud/hcloud-python/issues/301)) ([951dbf3](https://github.com/hetznercloud/hcloud-python/commit/951dbf3e3b3816ffaeb44a583251a5a3a4b90b70)) +* update pre-commit hook pre-commit/mirrors-prettier to v3.0.3 ([#294](https://github.com/hetznercloud/hcloud-python/issues/294)) ([381e336](https://github.com/hetznercloud/hcloud-python/commit/381e336ff1259fa26cb6abae3b7341cb16229a4b)) +* update pre-commit hook psf/black to v23.9.1 ([#296](https://github.com/hetznercloud/hcloud-python/issues/296)) ([4374a7b](https://github.com/hetznercloud/hcloud-python/commit/4374a7be9f244a72f1fc0c2dd76357cf63f19bfd)) + + +### Documentation + +* load token from env in examples scripts ([#302](https://github.com/hetznercloud/hcloud-python/issues/302)) ([f18c9a6](https://github.com/hetznercloud/hcloud-python/commit/f18c9a60e045743b26892eeb1fe9e5737a63c11f)) + ## [1.28.0](https://github.com/hetznercloud/hcloud-python/compare/v1.27.2...v1.28.0) (2023-08-17) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index 112641f0..b65a355b 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1,3 +1,3 @@ from __future__ import annotations -VERSION = "1.28.0" # x-release-please-version +VERSION = "1.29.0" # x-release-please-version From c1de7efc851b3b10e2a50e66268fc8fb0ff648a8 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Tue, 26 Sep 2023 13:20:29 +0200 Subject: [PATCH 117/406] fix: prevent api calls when printing bound models (#305) On large objects such as servers, we might generate more than 10 api calls just to print a single server object. This is because the `__repr__` method will recursively generate the `__repr__` for each bound model property and will trigger a `reload` on incomplete models to gather the information from the API. --- hcloud/core/client.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/hcloud/core/client.py b/hcloud/core/client.py index 1d4edfd1..d213daf0 100644 --- a/hcloud/core/client.py +++ b/hcloud/core/client.py @@ -90,3 +90,9 @@ def reload(self) -> None: bound_model = self._client.get_by_id(self.data_model.id) self.data_model = bound_model.data_model self.complete = True + + def __repr__(self) -> str: + # Override and reset hcloud.core.domain.BaseDomain.__repr__ method for bound + # models, as they will generate a lot of API call trying to print all the fields + # of the model. + return object.__repr__(self) From 9f36c42b91d7283b007d9819e4ecf90096138d61 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Tue, 26 Sep 2023 13:22:08 +0200 Subject: [PATCH 118/406] chore(main): release 1.29.1 (#306) :robot: I have created a release *beep* *boop* --- ## [1.29.1](https://github.com/hetznercloud/hcloud-python/compare/v1.29.0...v1.29.1) (2023-09-26) ### Bug Fixes * prevent api calls when printing bound models ([#305](https://github.com/hetznercloud/hcloud-python/issues/305)) ([c1de7ef](https://github.com/hetznercloud/hcloud-python/commit/c1de7efc851b3b10e2a50e66268fc8fb0ff648a8)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 7 +++++++ hcloud/__version__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f31cec6..ee5c65e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.29.1](https://github.com/hetznercloud/hcloud-python/compare/v1.29.0...v1.29.1) (2023-09-26) + + +### Bug Fixes + +* prevent api calls when printing bound models ([#305](https://github.com/hetznercloud/hcloud-python/issues/305)) ([c1de7ef](https://github.com/hetznercloud/hcloud-python/commit/c1de7efc851b3b10e2a50e66268fc8fb0ff648a8)) + ## [1.29.0](https://github.com/hetznercloud/hcloud-python/compare/v1.28.0...v1.29.0) (2023-09-25) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index b65a355b..21bc9b86 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1,3 +1,3 @@ from __future__ import annotations -VERSION = "1.29.0" # x-release-please-version +VERSION = "1.29.1" # x-release-please-version From 07a4513e284b9ee964bca003d0a9dfd948d39b02 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 10:08:19 +0200 Subject: [PATCH 119/406] deps: update pre-commit hook asottile/pyupgrade to v3.14.0 (#308) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [asottile/pyupgrade](https://togithub.com/asottile/pyupgrade) | repository | minor | `v3.13.0` -> `v3.14.0` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://togithub.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
asottile/pyupgrade (asottile/pyupgrade) ### [`v3.14.0`](https://togithub.com/asottile/pyupgrade/compare/v3.13.0...v3.14.0) [Compare Source](https://togithub.com/asottile/pyupgrade/compare/v3.13.0...v3.14.0)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9926274d..e7242f87 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: exclude: ^CHANGELOG.md$ - repo: https://github.com/asottile/pyupgrade - rev: v3.13.0 + rev: v3.14.0 hooks: - id: pyupgrade args: [--py38-plus] From 277841dd84ba3b2bbc99a06a3f97e114d1c83dcb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 10:22:08 +0200 Subject: [PATCH 120/406] deps: update dependency pylint to v3 (#307) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [pylint](https://togithub.com/pylint-dev/pylint) ([changelog](https://pylint.readthedocs.io/en/latest/whatsnew/2/)) | `>=2.17.4,<2.18` -> `>=3,<3.1` | [![age](https://developer.mend.io/api/mc/badges/age/pypi/pylint/3.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/pylint/3.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/pylint/2.17.7/3.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/pylint/2.17.7/3.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
pylint-dev/pylint (pylint) ### [`v3.0.0`](https://togithub.com/pylint-dev/pylint/releases/tag/v3.0.0) Pylint now support python 3.12 officially. This long anticipated major version also provides some important usability and performance improvements, along with enacting necessary breaking changes and long-announced deprecations. The documentation of each message with an example is very close too. The required astroid version is now 3.0.0. See the [astroid changelog](https://pylint.readthedocs.io/projects/astroid/en/latest/changelog.html#what-s-new-in-astroid-3-0-0) for additional fixes, features, and performance improvements applicable to pylint. Our code is now fully typed. The invalid-name message no longer checks for a minimum length of 3 characters by default. Dependencies like wrapt or setuptools were removed. A new json2 reporter has been added. It features an enriched output that is easier to parse and provides more info, here's a sample output. ```json { "messages": [ { "type": "convention", "symbol": "line-too-long", "message": "Line too long (1/2)", "messageId": "C0301", "confidence": "HIGH", "module": "0123", "obj": "", "line": 1, "column": 0, "endLine": 1, "endColumn": 4, "path": "0123", "absolutePath": "0123" } ], "statistics": { "messageTypeCount": { "fatal": 0, "error": 0, "warning": 0, "refactor": 0, "convention": 1, "info": 0 }, "modulesLinted": 1, "score": 5.0 } } ``` ## Breaking Changes - Enabling or disabling individual messages will now take effect even if an `--enable=all` or `disable=all` follows in the same configuration file (or on the command line). This means for the following example, `fixme` messages will now be emitted: ```bash pylint my_module --enable=fixme --disable=all ``` To regain the prior behavior, remove the superfluous earlier option. Closes [#​3696](https://togithub.com/pylint-dev/pylint/issues/3696) - Remove support for launching pylint with Python 3.7. Code that supports Python 3.7 can still be linted with the `--py-version=3.7` setting. Refs [#​6306](https://togithub.com/pylint-dev/pylint/issues/6306) - Disables placed in a `try` block now apply to the `except` block. Previously, they only happened to do so in the presence of an `else` clause. Refs [#​7767](https://togithub.com/pylint-dev/pylint/issues/7767) - `pyreverse` now uses a new default color palette that is more colorblind friendly. The color scheme is taken from `Paul Tol's Notes `\_. If you prefer other colors, you can use the `--color-palette` option to specify custom colors. Closes [#​8251](https://togithub.com/pylint-dev/pylint/issues/8251) - Everything related to the `__implements__` construct was removed. It was based on PEP245 that was proposed in 2001 and rejected in 2006. The capability from pyreverse to take `__implements__` into account when generating diagrams was also removed. Refs [#​8404](https://togithub.com/pylint-dev/pylint/issues/8404) - `pyreverse`: Support for the `.vcg` output format (Visualization of Compiler Graphs) has been dropped. Closes [#​8416](https://togithub.com/pylint-dev/pylint/issues/8416) - The warning when the now useless old pylint cache directory (pylint.d) was found was removed. The cache dir is documented in `the FAQ `\_. Refs [#​8462](https://togithub.com/pylint-dev/pylint/issues/8462) - Following a deprecation period, `pylint.config.PYLINTRC` was removed. Use the `pylint.config.find_default_config_files` generator instead. Closes [#​8862](https://togithub.com/pylint-dev/pylint/issues/8862) ## Changes requiring user actions - The `invalid-name` message no longer checks for a minimum length of 3 characters by default. (This was an unadvertised commingling of concerns between casing and name length, and users regularly reported this to be surprising.) If checking for a minimum length is still desired, it can be regained in two ways: - If you are content with a `disallowed-name` message (instead of `invalid-name`), then simply add the option `bad-names-rgxs="^..?$"`, which will fail 1-2 character-long names. (Ensure you enable `disallowed-name`.) - If you would prefer an `invalid-name` message to be emitted, or would prefer finer-grained control over the circumstances in which messages are emitted (classes vs. methods, etc.), then avail yourself of the regex options described `here `*. (In particular, take note of the commented out options in the "example configuration" given at the bottom of the section.) The prior regexes can be found in the `pull request `* that removed the length requirements. Closes [#​2018](https://togithub.com/pylint-dev/pylint/issues/2018) - The compare to empty string checker (`pylint.extensions.emptystring`) and the compare to zero checker (`pylint.extensions.compare-to-zero`) have been removed and their checks are now part of the implicit booleaness checker: - `compare-to-zero` was renamed `use-implicit-booleaness-not-comparison-to-zero` and `compare-to-empty-string` was renamed `use-implicit-booleaness-not-comparison-to-string` and they now need to be enabled explicitly. - The `pylint.extensions.emptystring` and `pylint.extensions.compare-to-zero` extensions no longer exist and need to be removed from the `load-plugins` option. - Messages related to implicit booleaness were made more explicit and actionable. This permits to make their likeness explicit and will provide better performance as they share most of their conditions to be raised. Closes [#​6871](https://togithub.com/pylint-dev/pylint/issues/6871) - epylint was removed. It now lives at: https://github.com/emacsorphanage/pylint. Refs [#​7737](https://togithub.com/pylint-dev/pylint/issues/7737) - The `overgeneral-exceptions` option now only takes fully qualified names into account (`builtins.Exception` not `Exception`). If you overrode this option, you need to use the fully qualified name now. There's still a warning, but it will be removed in 3.1.0. Refs [#​8411](https://togithub.com/pylint-dev/pylint/issues/8411) - Following a deprecation period, it's no longer possible to use `MASTER` or `master` as configuration section in `setup.cfg` or `tox.ini`. It's bad practice to not start a section title with the tool name. Please use `pylint.main` instead. Refs [#​8465](https://togithub.com/pylint-dev/pylint/issues/8465) - Package stats are now printed when running Pyreverse and a `--verbose` flag was added to get the original output with parsed modules. You might need to activate the verbose option if you want to keep the old output. Closes [#​8973](https://togithub.com/pylint-dev/pylint/issues/8973) ## New Features - A new `json2` reporter has been added. It features a more enriched output that is easier to parse and provides more info. Compared to `json` the only changes are that messages are now under the `"messages"` key and that `"message-id"` now follows the camelCase convention and is renamed to `"messageId"`. The new reporter also reports the "score" of the modules you linted as defined by the `evaluation` option and provides statistics about the modules you linted. We encourage users to use the new reporter as the `json` reporter will no longer be maintained. Closes [#​4741](https://togithub.com/pylint-dev/pylint/issues/4741) - In Pyreverse package dependency diagrams, show when a module imports another only for type-checking. Closes [#​8112](https://togithub.com/pylint-dev/pylint/issues/8112) - Add new option (`--show-stdlib`, `-L`) to `pyreverse`. This is similar to the behavior of `--show-builtin` in that standard library modules are now not included by default, and this option will include them. Closes [#​8181](https://togithub.com/pylint-dev/pylint/issues/8181) - Add Pyreverse option to exclude standalone nodes from diagrams with `--no-standalone`. Closes [#​8476](https://togithub.com/pylint-dev/pylint/issues/8476) ## New Checks - Added `DataclassChecker` module and `invalid-field-call` checker to check for invalid dataclasses.field() usage. Refs [#​5159](https://togithub.com/pylint-dev/pylint/issues/5159) - Add `return-in-finally` to emit a message if a return statement was found in a finally clause. Closes [#​8260](https://togithub.com/pylint-dev/pylint/issues/8260) - Add a new checker `kwarg-superseded-by-positional-arg` to warn when a function is called with a keyword argument which shares a name with a positional-only parameter and the function contains a keyword variadic parameter dictionary. It may be surprising behaviour when the keyword argument is added to the keyword variadic parameter dictionary. Closes [#​8558](https://togithub.com/pylint-dev/pylint/issues/8558) ## Extensions - Add new `prefer-typing-namedtuple` message to the `CodeStyleChecker` to suggest rewriting calls to `collections.namedtuple` as classes inheriting from `typing.NamedTuple` on Python 3.6+. Requires `load-plugins=pylint.extensions.code_style` and `enable=prefer-typing-namedtuple` to be raised. Closes [#​8660](https://togithub.com/pylint-dev/pylint/issues/8660) ## False Positives Fixed - Extend concept of "function ambiguity" in `safe_infer()` from differing number of function arguments to differing set of argument names. Solves false positives in `tensorflow`. Closes [#​3613](https://togithub.com/pylint-dev/pylint/issues/3613) - Fix `unused-argument` false positive when `__new__` does not use all the arguments of `__init__`. Closes [#​3670](https://togithub.com/pylint-dev/pylint/issues/3670) - Fix a false positive for `invalid-name` when a type-annotated class variable in an `enum.Enum` class has no assigned value. Refs [#​7402](https://togithub.com/pylint-dev/pylint/issues/7402) - Fix `unused-import` false positive for usage of `six.with_metaclass`. Closes [#​7506](https://togithub.com/pylint-dev/pylint/issues/7506) - Fix false negatives and false positives for `too-many-try-statements`, `too-complex`, and `too-many-branches` by correctly counting statements under a `try`. Refs [#​7767](https://togithub.com/pylint-dev/pylint/issues/7767) - When checking for unbalanced dict unpacking in for-loops, Pylint will now test whether the length of each value to be unpacked matches the number of unpacking targets. Previously, Pylint would test the number of values for the loop iteration, which would produce a false unbalanced-dict-unpacking warning. Closes [#​8156](https://togithub.com/pylint-dev/pylint/issues/8156) - Fix false positive for `used-before-assignment` when usage and assignment are guarded by the same test in different statements. Closes [#​8167](https://togithub.com/pylint-dev/pylint/issues/8167) - Adds `asyncSetUp` to the default `defining-attr-methods` list to silence `attribute-defined-outside-init` warning when using `unittest.IsolatedAsyncioTestCase`. Refs [#​8403](https://togithub.com/pylint-dev/pylint/issues/8403) - `logging-not-lazy` is not longer emitted for explicitly concatenated string arguments. Closes [#​8410](https://togithub.com/pylint-dev/pylint/issues/8410) - Fix false positive for isinstance-second-argument-not-valid-type when union types contains None. Closes [#​8424](https://togithub.com/pylint-dev/pylint/issues/8424) - `invalid-name` now allows for integers in `typealias` names: - now valid: `Good2Name`, `GoodName2`. - still invalid: `_1BadName`. Closes [#​8485](https://togithub.com/pylint-dev/pylint/issues/8485) - No longer consider `Union` as type annotation as type alias for naming checks. Closes [#​8487](https://togithub.com/pylint-dev/pylint/issues/8487) - `unnecessary-lambda` no longer warns on lambdas which use its parameters in their body (other than the final arguments), e.g. `lambda foo: (bar if foo else baz)(foo)`. Closes [#​8496](https://togithub.com/pylint-dev/pylint/issues/8496) - Fixed `unused-import` so that it observes the `dummy-variables-rgx` option. Closes [#​8500](https://togithub.com/pylint-dev/pylint/issues/8500) - `Union` typed variables without assignment are no longer treated as `TypeAlias`. Closes [#​8540](https://togithub.com/pylint-dev/pylint/issues/8540) - Allow parenthesized implicitly concatenated strings when `check-str-concat-over-line-jumps` is enabled. Closes [#​8552](https://togithub.com/pylint-dev/pylint/issues/8552). - Fix false positive for `positional-only-arguments-expected` when a function contains both a positional-only parameter that has a default value, and `**kwargs`. Closes [#​8555](https://togithub.com/pylint-dev/pylint/issues/8555) - Fix false positive for `keyword-arg-before-vararg` when a positional-only parameter with a default value precedes `*args`. Closes [#​8570](https://togithub.com/pylint-dev/pylint/issues/8570) - Fix false positive for `arguments-differ` when overriding `__init_subclass__`. Closes [#​8919](https://togithub.com/pylint-dev/pylint/issues/8919) - Fix a false positive for `no-value-for-parameter` when a staticmethod is called in a class body. Closes [#​9036](https://togithub.com/pylint-dev/pylint/issues/9036) ## False Negatives Fixed - Emit `used-before-assignment` when calling module-level functions before definition. Closes [#​1144](https://togithub.com/pylint-dev/pylint/issues/1144) - Apply `infer_kwarg_from_call()` to more checks These mostly solve false negatives for various checks, save for one false positive for `use-maxsplit-arg`. Closes [#​7761](https://togithub.com/pylint-dev/pylint/issues/7761) - `TypeAlias` variables defined in functions are now checked for `invalid-name` errors. Closes [#​8536](https://togithub.com/pylint-dev/pylint/issues/8536) - Fix false negative for `no-value-for-parameter` when a function, whose signature contains both a positional-only parameter `name` and also `*kwargs`, is called with a keyword-argument for `name`. Closes [#​8559](https://togithub.com/pylint-dev/pylint/issues/8559) - Fix a false negative for `too-many-arguments` by considering positional-only and keyword-only parameters. Closes [#​8667](https://togithub.com/pylint-dev/pylint/issues/8667) - Emit `assignment-from-no-return` for calls to builtin methods like `dict.update()`. Calls to `list.sort()` now raise `assignment-from-no-return` rather than `assignment-from-none` for consistency. Closes [#​8714](https://togithub.com/pylint-dev/pylint/issues/8714) Closes [#​8810](https://togithub.com/pylint-dev/pylint/issues/8810) - `consider-using-augmented-assign` is now applied to dicts and lists as well. Closes [#​8959](https://togithub.com/pylint-dev/pylint/issues/8959) ## Other Bug Fixes - Support `duplicate-code` message when parallelizing with `--jobs`. Closes [#​374](https://togithub.com/pylint-dev/pylint/issues/374) - Support `cyclic-import` message when parallelizing with `--jobs`. Closes [#​4171](https://togithub.com/pylint-dev/pylint/issues/4171) - `--jobs` can now be used with `--load-plugins`. This had regressed in astroid 2.5.0. Closes [#​4874](https://togithub.com/pylint-dev/pylint/issues/4874) - docparams extension considers type comments as type documentation. Closes [#​6287](https://togithub.com/pylint-dev/pylint/issues/6287) - When parsing comma-separated lists of regular expressions in the config, ignore commas that are inside braces since those indicate quantifiers, not delineation between expressions. Closes [#​7229](https://togithub.com/pylint-dev/pylint/issues/7229) - The `ignored-modules` option will now be correctly taken into account for `no-name-in-module`. Closes [#​7578](https://togithub.com/pylint-dev/pylint/issues/7578) - `sys.argv` is now always correctly considered as impossible to infer (instead of using the actual values given to pylint). Closes [#​7710](https://togithub.com/pylint-dev/pylint/issues/7710) - Avoid duplicative warnings for unqualified exception names in the `overgeneral-exceptions` setting when running with `--jobs`. Closes [#​7774](https://togithub.com/pylint-dev/pylint/issues/7774) - Don't show class fields more than once in Pyreverse diagrams. Closes [#​8189](https://togithub.com/pylint-dev/pylint/issues/8189) - Fix `used-before-assignment` false negative when TYPE_CHECKING imports are used in multiple scopes. Closes [#​8198](https://togithub.com/pylint-dev/pylint/issues/8198) - `--clear-cache-post-run` now also clears LRU caches for pylint utilities holding references to AST nodes. Closes [#​8361](https://togithub.com/pylint-dev/pylint/issues/8361) - Fix a crash when `TYPE_CHECKING` is used without importing it. Closes [#​8434](https://togithub.com/pylint-dev/pylint/issues/8434) - Fix a `used-before-assignment` false positive when imports are made under the `TYPE_CHECKING` else if branch. Closes [#​8437](https://togithub.com/pylint-dev/pylint/issues/8437) - Fix a regression of `preferred-modules` where a partial match was used instead of the required full match. Closes [#​8453](https://togithub.com/pylint-dev/pylint/issues/8453) - Fix a crash in pyreverse when "/" characters are used in the output filename e.g pyreverse -o png -p name/ path/to/project. Closes [#​8504](https://togithub.com/pylint-dev/pylint/issues/8504) - Don't show arrows more than once in Pyreverse diagrams. Closes [#​8522](https://togithub.com/pylint-dev/pylint/issues/8522) - Improve output of `consider-using-generator` message for `min()` calls with `default` keyword. Closes [#​8563](https://togithub.com/pylint-dev/pylint/issues/8563) - Fixed a crash when generating a configuration file: `tomlkit.exceptions.TOMLKitError: Can't add a table to a dotted key` caused by tomlkit `v0.11.8`. Closes [#​8632](https://togithub.com/pylint-dev/pylint/issues/8632) - Fix a line break error in Pyreverse dot output. Closes [#​8671](https://togithub.com/pylint-dev/pylint/issues/8671) - Fix a false positive for `method-hidden` when using `cached_property` decorator. Closes [#​8753](https://togithub.com/pylint-dev/pylint/issues/8753) - Dunder methods defined in lambda do not trigger `unnecessary-dunder-call` anymore, if they cannot be replaced by the non-dunder call. Closes [#​8769](https://togithub.com/pylint-dev/pylint/issues/8769) - Don't show duplicate type annotations in Pyreverse diagrams. Closes [#​8888](https://togithub.com/pylint-dev/pylint/issues/8888) - Fixing inconsistent hashing issue in `BaseChecker` causing some reports not being exported. Closes [#​9001](https://togithub.com/pylint-dev/pylint/issues/9001) - Don't add `Optional` to `|` annotations with `None` in Pyreverse diagrams. Closes [#​9014](https://togithub.com/pylint-dev/pylint/issues/9014) - Pyreverse doesn't show multiple class association arrows anymore, but only the strongest one. Refs [#​9045](https://togithub.com/pylint-dev/pylint/issues/9045) - Prevented data loss in the linter stats for messages relating to the linter itself (e.g. `unknown-option-value`), fixing problems with score, fail-on, etc. Closes [#​9059](https://togithub.com/pylint-dev/pylint/issues/9059) - Fix crash in refactoring checker when unary operand used with variable in for loop. Closes [#​9074](https://togithub.com/pylint-dev/pylint/issues/9074) ## Other Changes - Pylint now exposes its type annotations. Closes [#​5488](https://togithub.com/pylint-dev/pylint/issues/5488) and [#​2079](https://togithub.com/pylint-dev/pylint/issues/2079) - Search for `pyproject.toml` recursively in parent directories up to a project or file system root. Refs [#​7163](https://togithub.com/pylint-dev/pylint/issues/7163), Closes [#​3289](https://togithub.com/pylint-dev/pylint/issues/3289) - All code related to the optparse config parsing has been removed. Refs [#​8405](https://togithub.com/pylint-dev/pylint/issues/8405) - Pylint now supports python 3.12. Refs [#​8718](https://togithub.com/pylint-dev/pylint/issues/8718) - Add a CITATION.cff file to the root of the repository containing the necessary metadata to cite Pylint. Closes [#​8760](https://togithub.com/pylint-dev/pylint/issues/8760) - Renamed the "unneeded-not" error into "unnecessary_negation" to be clearer. Closes [#​8789](https://togithub.com/pylint-dev/pylint/issues/8789) ## Internal Changes - `get_message_definition` was removed from the base checker API. You can access message definitions through the `MessageStore`. Refs [#​8401](https://togithub.com/pylint-dev/pylint/issues/8401) - Everything related to the `__implements__` construct was removed. It was based on PEP245 that was proposed in 2001 and rejected in 2006. All the classes inheriting `Interface` in `pylint.interfaces` were removed. `Checker` should only inherit `BaseChecker` or any of the other checker types from `pylint.checkers`. `Reporter` should only inherit `BaseReporter`. Refs [#​8404](https://togithub.com/pylint-dev/pylint/issues/8404) - `modname` and `msg_store` are now required to be given in `FileState`. `collect_block_lines` has also been removed. `Pylinter.current_name` cannot be null anymore. Refs [#​8407](https://togithub.com/pylint-dev/pylint/issues/8407) - `Reporter.set_output` was removed in favor of `reporter.out = stream`. Refs [#​8408](https://togithub.com/pylint-dev/pylint/issues/8408) - A number of old utility functions and classes have been removed: `MapReduceMixin`: To make a checker reduce map data simply implement `get_map_data` and `reduce_map_data`. `is_inside_lambda`: Use `utils.get_node_first_ancestor_of_type(x, nodes.Lambda)` `check_messages`: Use `utils.only_required_for_messages` `is_class_subscriptable_pep585_with_postponed_evaluation_enabled`: Use `is_postponed_evaluation_enabled(node)` and `is_node_in_type_annotation_context(node)` `get_python_path`: assumption that there's always an **init**.py is not true since python 3.3 and is causing problems, particularly with PEP 420. Use `discover_package_path` and pass source root(s). `fix_import_path`: Use `augmented_sys_path` and pass additional `sys.path` entries as an argument obtained from `discover_package_path`. `get_global_option`: Use `checker.linter.config` to get all global options. Related private objects have been removed as well. Refs [#​8409](https://togithub.com/pylint-dev/pylint/issues/8409) - `colorize_ansi` now only accepts a `MessageStyle` object. Refs [#​8412](https://togithub.com/pylint-dev/pylint/issues/8412) - Following a deprecation period, `Pylinter.check` now only works with sequences of strings, not strings. Refs [#​8463](https://togithub.com/pylint-dev/pylint/issues/8463) - Following a deprecation period, `ColorizedTextReporter` only accepts `ColorMappingDict`. Refs [#​8464](https://togithub.com/pylint-dev/pylint/issues/8464) - Following a deprecation period, `MessageTest`'s `end_line` and `end_col_offset` must be accurate in functional tests (for python 3.8 or above on cpython, and for python 3.9 or superior on pypy). Refs [#​8466](https://togithub.com/pylint-dev/pylint/issues/8466) - Following a deprecation period, the `do_exit` argument of the `Run` class (and of the `_Run` class in testutils) were removed. Refs [#​8472](https://togithub.com/pylint-dev/pylint/issues/8472) - Following a deprecation period, the `py_version` argument of the `MessageDefinition.may_be_emitted` function is now required. The most likely solution is to use 'linter.config.py_version' if you need to keep using this function, or to use 'MessageDefinition.is_message_enabled' instead. Refs [#​8473](https://togithub.com/pylint-dev/pylint/issues/8473) - Following a deprecation period, the `OutputLine` class now requires the right number of argument all the time. The functional output can be regenerated automatically to achieve that easily. Refs [#​8474](https://togithub.com/pylint-dev/pylint/issues/8474) - Following a deprecation period, `is_typing_guard`, `is_node_in_typing_guarded_import_block` and `is_node_in_guarded_import_block` from `pylint.utils` were removed: use a combination of `is_sys_guard` and `in_type_checking_block` instead. Refs [#​8475](https://togithub.com/pylint-dev/pylint/issues/8475) - Following a deprecation period, the `location` argument of the `Message` class must now be a `MessageLocationTuple`. Refs [#​8477](https://togithub.com/pylint-dev/pylint/issues/8477) - Following a deprecation period, the `check_single_file` function of the `Pylinter` is replaced by `Pylinter.check_single_file_item`. Refs [#​8478](https://togithub.com/pylint-dev/pylint/issues/8478) ## Performance Improvements - `pylint` runs (at least) ~5% faster after improvements to `astroid` that make better use of the inference cache. Refs [pylint-dev/astroid#529](https://togithub.com/pylint-dev/astroid/issues/529) - - Optimize `is_trailing_comma()`. - Cache `class_is_abstract()`. Refs [#​1954](https://togithub.com/pylint-dev/pylint/issues/1954) - Exit immediately if all messages are disabled. Closes [#​8715](https://togithub.com/pylint-dev/pylint/issues/8715)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: jo --- hcloud/servers/client.py | 6 ++++-- pyproject.toml | 4 +--- setup.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index 5cbd48a2..ea728512 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -9,8 +9,6 @@ from ..floating_ips import BoundFloatingIP from ..images import BoundImage, CreateImageResponse from ..isos import BoundIso -from ..networks import BoundNetwork # noqa -from ..networks import Network # noqa from ..placement_groups import BoundPlacementGroup from ..primary_ips import BoundPrimaryIP from ..server_types import BoundServerType @@ -35,6 +33,7 @@ from ..images import Image from ..isos import Iso from ..locations import BoundLocation, Location + from ..networks import BoundNetwork, Network from ..placement_groups import PlacementGroup from ..server_types import ServerType from ..ssh_keys import BoundSSHKey, SSHKey @@ -131,6 +130,9 @@ def __init__(self, client: ServersClient, data: dict, complete: bool = True): private_nets = data.get("private_net") if private_nets: + # pylint: disable=import-outside-toplevel + from ..networks import BoundNetwork + private_nets = [ PrivateNet( network=BoundNetwork( diff --git a/pyproject.toml b/pyproject.toml index 2b768342..60a7dd06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,9 +21,6 @@ jobs = 0 [tool.pylint.reports] output-format = "colorized" -[tool.pylint.basic] -good-names = ["i", "j", "k", "ex", "_", "ip", "id"] - [tool.pylint."messages control"] disable = [ "fixme", @@ -31,6 +28,7 @@ disable = [ "missing-class-docstring", "missing-module-docstring", "redefined-builtin", + "duplicate-code", # Consider disabling line-by-line "too-few-public-methods", "too-many-public-methods", diff --git a/setup.py b/setup.py index b4a20b6e..ca2fa23d 100644 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ ], "test": [ "coverage>=7.3,<7.4", - "pylint>=2.17.4,<2.18", + "pylint>=3,<3.1", "pytest>=7.4,<7.5", "mypy>=1.5,<1.6", "types-python-dateutil", From 7e8cd1d92e56d210fe3fb180e403122ef0e7bd7f Mon Sep 17 00:00:00 2001 From: Jonas L Date: Wed, 4 Oct 2023 10:26:45 +0200 Subject: [PATCH 121/406] feat: support python 3.12 (#311) Add support for python 3.12 --- .github/workflows/test.yml | 2 +- .gitlab-ci.yml | 2 +- setup.py | 1 + tox.ini | 3 ++- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 049925ca..19718451 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] name: Python ${{ matrix.python-version }} steps: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f5ef29aa..d7955624 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,7 +25,7 @@ test: parallel: matrix: - - python_version: ["3.8", "3.9", "3.10", "3.11"] + - python_version: ["3.8", "3.9", "3.10", "3.11", "3.12"] image: python:${python_version}-alpine before_script: diff --git a/setup.py b/setup.py index ca2fa23d..c56c669e 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ], python_requires=">=3.8", install_requires=[ diff --git a/tox.ini b/tox.ini index 3470ad15..cf17b6de 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py38, py39, py310, py311 +envlist = py38, py39, py310, py311, py312 [testenv] passenv = FAKE_API_ENDPOINT @@ -14,3 +14,4 @@ python = 3.9: py39 3.10: py310 3.11: py311 + 3.12: py312 From 3a1ee675f2c980a4d9e63188e8ffceb64f4797fc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 10:28:43 +0200 Subject: [PATCH 122/406] deps: update python docker tag to v3.12 (#309) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | python | image | minor | `3.11-alpine` -> `3.12-alpine` | --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d7955624..f3cfb8c8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,7 +13,7 @@ pre-commit: lint: stage: test - image: python:3.11-alpine + image: python:3.12-alpine before_script: - apk add make bash - make venv From f7f66b15970ba5de8bf86e574129d8857a7cb372 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 9 Oct 2023 11:25:42 +0200 Subject: [PATCH 123/406] ci: use reusable stale bot workflow (#314) --- .github/workflows/stale.yml | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 466d9cb2..d9410ae9 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -4,21 +4,10 @@ on: schedule: - cron: "30 12 * * *" -permissions: - issues: write - pull-requests: write - jobs: stale: - runs-on: ubuntu-latest - steps: - - uses: actions/stale@v8 - with: - stale-issue-message: "This issue has been marked as stale because it has not had recent activity. The bot will close the issue if no further action occurs." - exempt-issue-labels: "pinned" - stale-issue-labels: "stale" - stale-pr-message: "This PR has been marked as stale because it has not had recent activity. The bot will close the PR if no further action occurs." - exempt-pr-labels: "pinned" - stale-pr-labels: "stale" - days-before-stale: 90 - days-before-close: 30 + permissions: + issues: write + pull-requests: write + + uses: hetznercloud/.github/.github/workflows/stale.yml@main From e51eaa990336251c2afc8c83d4c5e6f5e5bb857b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 12:24:03 +0200 Subject: [PATCH 124/406] deps: update pre-commit hook pre-commit/pre-commit-hooks to v4.5.0 (#313) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [pre-commit/pre-commit-hooks](https://togithub.com/pre-commit/pre-commit-hooks) | repository | minor | `v4.4.0` -> `v4.5.0` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://togithub.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
pre-commit/pre-commit-hooks (pre-commit/pre-commit-hooks) ### [`v4.5.0`](https://togithub.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.0) [Compare Source](https://togithub.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.0)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e7242f87..e157268a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-added-large-files - id: check-case-conflict From c5446394acfa25d23761da4c6b5b75fb6d376b23 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 12:24:20 +0200 Subject: [PATCH 125/406] deps: update pre-commit hook asottile/pyupgrade to v3.15.0 (#312) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [asottile/pyupgrade](https://togithub.com/asottile/pyupgrade) | repository | minor | `v3.14.0` -> `v3.15.0` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://togithub.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
asottile/pyupgrade (asottile/pyupgrade) ### [`v3.15.0`](https://togithub.com/asottile/pyupgrade/compare/v3.14.0...v3.15.0) [Compare Source](https://togithub.com/asottile/pyupgrade/compare/v3.14.0...v3.15.0)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e157268a..eda07943 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: exclude: ^CHANGELOG.md$ - repo: https://github.com/asottile/pyupgrade - rev: v3.14.0 + rev: v3.15.0 hooks: - id: pyupgrade args: [--py38-plus] From 1eb94716e3bf68bb9c90669dd83f9832c3ec200f Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 9 Oct 2023 13:42:12 +0200 Subject: [PATCH 126/406] chore: update pre-commit hooks (#315) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eda07943..0bc47033 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: hooks: - id: isort - - repo: https://github.com/psf/black + - repo: https://github.com/psf/black-pre-commit-mirror rev: 23.9.1 hooks: - id: black From 218625471001e2e01fa87c5b8ddea031e786d172 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 9 Oct 2023 16:05:39 +0200 Subject: [PATCH 127/406] chore: update renovate bot config (#316) --- renovate.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/renovate.json b/renovate.json index 5d564925..5805c99a 100644 --- a/renovate.json +++ b/renovate.json @@ -1,10 +1,4 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:base", - ":enablePreCommit", - - ":semanticCommitTypeAll(deps)", - ":semanticCommitScopeDisabled" - ] + "extends": ["github>hetznercloud/.github//renovate/default"] } From d248bbd4e55f3bcf6a107cfa4f38768df0bf3de5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Oct 2023 09:49:25 +0200 Subject: [PATCH 128/406] deps: update dependency mypy to >=1.6,<1.7 (#317) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [mypy](https://www.mypy-lang.org/) ([source](https://togithub.com/python/mypy), [changelog](https://mypy-lang.blogspot.com/)) | `>=1.5,<1.6` -> `>=1.6,<1.7` | [![age](https://developer.mend.io/api/mc/badges/age/pypi/mypy/1.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/mypy/1.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/mypy/1.5.1/1.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/mypy/1.5.1/1.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
python/mypy (mypy) ### [`v1.6.0`](https://togithub.com/python/mypy/compare/v1.5.1...v1.6.0) [Compare Source](https://togithub.com/python/mypy/compare/v1.5.1...v1.6.0)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c56c669e..d396ecb8 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ "coverage>=7.3,<7.4", "pylint>=3,<3.1", "pytest>=7.4,<7.5", - "mypy>=1.5,<1.6", + "mypy>=1.6,<1.7", "types-python-dateutil", "types-requests", ], From 036b52fe51bcbb6b610c0c99ca224d3c4bbfc68d Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 13 Oct 2023 14:22:57 +0200 Subject: [PATCH 129/406] feat: add deprecation field to Iso (#318) See these changelog entries for the announcement: - https://docs.hetzner.cloud/changelog#2023-10-12-deprecation-info-for-isos - https://docs.hetzner.cloud/changelog#2023-10-12-field-deprecated-on-isos-is-now-deprecated --- hcloud/isos/domain.py | 20 +++++++++++++++++-- tests/unit/isos/test_domain.py | 36 ++++++++++++++++++++++++++++------ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/hcloud/isos/domain.py b/hcloud/isos/domain.py index 1578be17..17676ecf 100644 --- a/hcloud/isos/domain.py +++ b/hcloud/isos/domain.py @@ -3,6 +3,7 @@ from dateutil.parser import isoparse from ..core import BaseDomain, DomainIdentityMixin +from ..deprecation import DeprecationInfo class Iso(BaseDomain, DomainIdentityMixin): @@ -19,10 +20,21 @@ class Iso(BaseDomain, DomainIdentityMixin): :param architecture: str, None CPU Architecture that the ISO is compatible with. None means that the compatibility is unknown. Choices: `x86`, `arm` :param deprecated: datetime, None - ISO 8601 timestamp of deprecation, None if ISO is still available. After the deprecation time it will no longer be possible to attach the ISO to servers. + ISO 8601 timestamp of deprecation, None if ISO is still available. After the deprecation time it will no longer be possible to attach the ISO to servers. This field is deprecated. Use `deprecation` instead. + :param deprecation: :class:`DeprecationInfo `, None + Describes if, when & how the resources was deprecated. If this field is set to None the resource is not + deprecated. If it has a value, it is considered deprecated. """ - __slots__ = ("id", "name", "type", "architecture", "description", "deprecated") + __slots__ = ( + "id", + "name", + "type", + "architecture", + "description", + "deprecated", + "deprecation", + ) def __init__( self, @@ -32,6 +44,7 @@ def __init__( architecture: str | None = None, description: str | None = None, deprecated: str | None = None, + deprecation: dict | None = None, ): self.id = id self.name = name @@ -39,3 +52,6 @@ def __init__( self.architecture = architecture self.description = description self.deprecated = isoparse(deprecated) if deprecated else None + self.deprecation = ( + DeprecationInfo.from_dict(deprecation) if deprecation is not None else None + ) diff --git a/tests/unit/isos/test_domain.py b/tests/unit/isos/test_domain.py index 7ba847bb..0e32e50a 100644 --- a/tests/unit/isos/test_domain.py +++ b/tests/unit/isos/test_domain.py @@ -1,14 +1,38 @@ from __future__ import annotations -import datetime -from datetime import timezone +from datetime import datetime, timezone + +import pytest from hcloud.isos import Iso class TestIso: - def test_deprecated_is_datetime(self): - iso = Iso(id=1, deprecated="2016-01-30T23:50+00:00") - assert iso.deprecated == datetime.datetime( - 2016, 1, 30, 23, 50, tzinfo=timezone.utc + @pytest.fixture() + def deprecated_iso(self): + return Iso( + **{ + "id": 10433, + "name": "vyos-1.4-rolling-202111150317-amd64.iso", + "description": "VyOS 1.4 (amd64)", + "type": "public", + "deprecation": { + "announced": "2023-10-05T08:27:01Z", + "unavailable_after": "2023-11-05T08:27:01Z", + }, + "architecture": "x86", + "deprecated": "2023-11-05T08:27:01Z", + } + ) + + def test_deprecation(self, deprecated_iso: Iso): + assert deprecated_iso.deprecated == datetime( + 2023, 11, 5, 8, 27, 1, tzinfo=timezone.utc + ) + assert deprecated_iso.deprecation is not None + assert deprecated_iso.deprecation.announced == datetime( + 2023, 10, 5, 8, 27, 1, tzinfo=timezone.utc + ) + assert deprecated_iso.deprecation.unavailable_after == datetime( + 2023, 11, 5, 8, 27, 1, tzinfo=timezone.utc ) From f1627d474988a4a37047473b12a197929b139940 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Fri, 13 Oct 2023 14:27:52 +0200 Subject: [PATCH 130/406] chore(main): release 1.30.0 (#310) :robot: I have created a release *beep* *boop* --- ## [1.30.0](https://github.com/hetznercloud/hcloud-python/compare/v1.29.1...v1.30.0) (2023-10-13) ### Features * add deprecation field to Iso ([#318](https://github.com/hetznercloud/hcloud-python/issues/318)) ([036b52f](https://github.com/hetznercloud/hcloud-python/commit/036b52fe51bcbb6b610c0c99ca224d3c4bbfc68d)) * support python 3.12 ([#311](https://github.com/hetznercloud/hcloud-python/issues/311)) ([7e8cd1d](https://github.com/hetznercloud/hcloud-python/commit/7e8cd1d92e56d210fe3fb180e403122ef0e7bd7f)) ### Dependencies * update dependency mypy to >=1.6,<1.7 ([#317](https://github.com/hetznercloud/hcloud-python/issues/317)) ([d248bbd](https://github.com/hetznercloud/hcloud-python/commit/d248bbd4e55f3bcf6a107cfa4f38768df0bf3de5)) * update dependency pylint to v3 ([#307](https://github.com/hetznercloud/hcloud-python/issues/307)) ([277841d](https://github.com/hetznercloud/hcloud-python/commit/277841dd84ba3b2bbc99a06a3f97e114d1c83dcb)) * update pre-commit hook asottile/pyupgrade to v3.14.0 ([#308](https://github.com/hetznercloud/hcloud-python/issues/308)) ([07a4513](https://github.com/hetznercloud/hcloud-python/commit/07a4513e284b9ee964bca003d0a9dfd948d39b02)) * update pre-commit hook asottile/pyupgrade to v3.15.0 ([#312](https://github.com/hetznercloud/hcloud-python/issues/312)) ([c544639](https://github.com/hetznercloud/hcloud-python/commit/c5446394acfa25d23761da4c6b5b75fb6d376b23)) * update pre-commit hook pre-commit/pre-commit-hooks to v4.5.0 ([#313](https://github.com/hetznercloud/hcloud-python/issues/313)) ([e51eaa9](https://github.com/hetznercloud/hcloud-python/commit/e51eaa990336251c2afc8c83d4c5e6f5e5bb857b)) * update python docker tag to v3.12 ([#309](https://github.com/hetznercloud/hcloud-python/issues/309)) ([3a1ee67](https://github.com/hetznercloud/hcloud-python/commit/3a1ee675f2c980a4d9e63188e8ffceb64f4797fc)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 18 ++++++++++++++++++ hcloud/__version__.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee5c65e6..fd1c79f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [1.30.0](https://github.com/hetznercloud/hcloud-python/compare/v1.29.1...v1.30.0) (2023-10-13) + + +### Features + +* add deprecation field to Iso ([#318](https://github.com/hetznercloud/hcloud-python/issues/318)) ([036b52f](https://github.com/hetznercloud/hcloud-python/commit/036b52fe51bcbb6b610c0c99ca224d3c4bbfc68d)) +* support python 3.12 ([#311](https://github.com/hetznercloud/hcloud-python/issues/311)) ([7e8cd1d](https://github.com/hetznercloud/hcloud-python/commit/7e8cd1d92e56d210fe3fb180e403122ef0e7bd7f)) + + +### Dependencies + +* update dependency mypy to >=1.6,<1.7 ([#317](https://github.com/hetznercloud/hcloud-python/issues/317)) ([d248bbd](https://github.com/hetznercloud/hcloud-python/commit/d248bbd4e55f3bcf6a107cfa4f38768df0bf3de5)) +* update dependency pylint to v3 ([#307](https://github.com/hetznercloud/hcloud-python/issues/307)) ([277841d](https://github.com/hetznercloud/hcloud-python/commit/277841dd84ba3b2bbc99a06a3f97e114d1c83dcb)) +* update pre-commit hook asottile/pyupgrade to v3.14.0 ([#308](https://github.com/hetznercloud/hcloud-python/issues/308)) ([07a4513](https://github.com/hetznercloud/hcloud-python/commit/07a4513e284b9ee964bca003d0a9dfd948d39b02)) +* update pre-commit hook asottile/pyupgrade to v3.15.0 ([#312](https://github.com/hetznercloud/hcloud-python/issues/312)) ([c544639](https://github.com/hetznercloud/hcloud-python/commit/c5446394acfa25d23761da4c6b5b75fb6d376b23)) +* update pre-commit hook pre-commit/pre-commit-hooks to v4.5.0 ([#313](https://github.com/hetznercloud/hcloud-python/issues/313)) ([e51eaa9](https://github.com/hetznercloud/hcloud-python/commit/e51eaa990336251c2afc8c83d4c5e6f5e5bb857b)) +* update python docker tag to v3.12 ([#309](https://github.com/hetznercloud/hcloud-python/issues/309)) ([3a1ee67](https://github.com/hetznercloud/hcloud-python/commit/3a1ee675f2c980a4d9e63188e8ffceb64f4797fc)) + ## [1.29.1](https://github.com/hetznercloud/hcloud-python/compare/v1.29.0...v1.29.1) (2023-09-26) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index 21bc9b86..1a01ca7a 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1,3 +1,3 @@ from __future__ import annotations -VERSION = "1.29.1" # x-release-please-version +VERSION = "1.30.0" # x-release-please-version From 184bbe65a736a42d13774b6c29fa7dd8a13ec645 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Oct 2023 15:22:27 +0200 Subject: [PATCH 131/406] deps: update pre-commit hook psf/black-pre-commit-mirror to v23.10.0 (#319) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [psf/black-pre-commit-mirror](https://togithub.com/psf/black-pre-commit-mirror) | repository | minor | `23.9.1` -> `23.10.0` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://togithub.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
psf/black-pre-commit-mirror (psf/black-pre-commit-mirror) ### [`v23.10.0`](https://togithub.com/psf/black-pre-commit-mirror/compare/23.9.1...23.10.0) [Compare Source](https://togithub.com/psf/black-pre-commit-mirror/compare/23.9.1...23.10.0)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0bc47033..5f19d730 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: - id: isort - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.9.1 + rev: 23.10.0 hooks: - id: black From beae328dd6b9afb8c0db9fa9b44340270db7dd09 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 23 Oct 2023 10:41:18 +0200 Subject: [PATCH 132/406] feat: prepare for iso deprecated field removal (#320) Continuation of #318 --- hcloud/isos/domain.py | 20 ++++++++++++++++---- tests/unit/isos/conftest.py | 14 +++++++++++++- tests/unit/isos/test_client.py | 9 ++++++++- tests/unit/isos/test_domain.py | 7 ++++--- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/hcloud/isos/domain.py b/hcloud/isos/domain.py index 17676ecf..b2f4d30c 100644 --- a/hcloud/isos/domain.py +++ b/hcloud/isos/domain.py @@ -1,6 +1,7 @@ from __future__ import annotations -from dateutil.parser import isoparse +from datetime import datetime +from warnings import warn from ..core import BaseDomain, DomainIdentityMixin from ..deprecation import DeprecationInfo @@ -32,7 +33,6 @@ class Iso(BaseDomain, DomainIdentityMixin): "type", "architecture", "description", - "deprecated", "deprecation", ) @@ -43,7 +43,7 @@ def __init__( type: str | None = None, architecture: str | None = None, description: str | None = None, - deprecated: str | None = None, + deprecated: str | None = None, # pylint: disable=unused-argument deprecation: dict | None = None, ): self.id = id @@ -51,7 +51,19 @@ def __init__( self.type = type self.architecture = architecture self.description = description - self.deprecated = isoparse(deprecated) if deprecated else None self.deprecation = ( DeprecationInfo.from_dict(deprecation) if deprecation is not None else None ) + + @property + def deprecated(self) -> datetime | None: + """ + ISO 8601 timestamp of deprecation, None if ISO is still available. + """ + warn( + "The `deprecated` field is deprecated, please use the `deprecation` field instead.", + DeprecationWarning, + ) + if self.deprecation is None: + return None + return self.deprecation.unavailable_after diff --git a/tests/unit/isos/conftest.py b/tests/unit/isos/conftest.py index 0c045f5e..3052f4ac 100644 --- a/tests/unit/isos/conftest.py +++ b/tests/unit/isos/conftest.py @@ -13,6 +13,10 @@ def iso_response(): "type": "public", "architecture": "x86", "deprecated": "2018-02-28T00:00:00+00:00", + "deprecation": { + "announced": "2018-01-28T00:00:00+00:00", + "unavailable_after": "2018-02-28T00:00:00+00:00", + }, } } @@ -28,6 +32,10 @@ def two_isos_response(): "type": "public", "architecture": "x86", "deprecated": "2018-02-28T00:00:00+00:00", + "deprecation": { + "announced": "2018-01-28T00:00:00+00:00", + "unavailable_after": "2018-02-28T00:00:00+00:00", + }, }, { "id": 4712, @@ -35,7 +43,7 @@ def two_isos_response(): "description": "FreeBSD 11.0 x64", "type": "public", "architecture": "x86", - "deprecated": "2018-02-28T00:00:00+00:00", + "deprecated": None, }, ] } @@ -52,6 +60,10 @@ def one_isos_response(): "type": "public", "architecture": "x86", "deprecated": "2018-02-28T00:00:00+00:00", + "deprecation": { + "announced": "2018-01-28T00:00:00+00:00", + "unavailable_after": "2018-02-28T00:00:00+00:00", + }, } ] } diff --git a/tests/unit/isos/test_client.py b/tests/unit/isos/test_client.py index 7472184c..fc64ab47 100644 --- a/tests/unit/isos/test_client.py +++ b/tests/unit/isos/test_client.py @@ -22,7 +22,14 @@ def test_bound_iso_init(self, iso_response): assert bound_iso.description == "FreeBSD 11.0 x64" assert bound_iso.type == "public" assert bound_iso.architecture == "x86" - assert bound_iso.deprecated == datetime.datetime( + with pytest.deprecated_call(): + assert bound_iso.deprecated == datetime.datetime( + 2018, 2, 28, 0, 0, tzinfo=timezone.utc + ) + assert bound_iso.deprecation.announced == datetime.datetime( + 2018, 1, 28, 0, 0, tzinfo=timezone.utc + ) + assert bound_iso.deprecation.unavailable_after == datetime.datetime( 2018, 2, 28, 0, 0, tzinfo=timezone.utc ) diff --git a/tests/unit/isos/test_domain.py b/tests/unit/isos/test_domain.py index 0e32e50a..5017b529 100644 --- a/tests/unit/isos/test_domain.py +++ b/tests/unit/isos/test_domain.py @@ -26,9 +26,10 @@ def deprecated_iso(self): ) def test_deprecation(self, deprecated_iso: Iso): - assert deprecated_iso.deprecated == datetime( - 2023, 11, 5, 8, 27, 1, tzinfo=timezone.utc - ) + with pytest.deprecated_call(): + assert deprecated_iso.deprecated == datetime( + 2023, 11, 5, 8, 27, 1, tzinfo=timezone.utc + ) assert deprecated_iso.deprecation is not None assert deprecated_iso.deprecation.announced == datetime( 2023, 10, 5, 8, 27, 1, tzinfo=timezone.utc From e82e54c03dab85e9c385bc7d1e537bb22f53f799 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Mon, 23 Oct 2023 10:49:27 +0200 Subject: [PATCH 133/406] chore(main): release 1.31.0 (#321) :robot: I have created a release *beep* *boop* --- ## [1.31.0](https://github.com/hetznercloud/hcloud-python/compare/v1.30.0...v1.31.0) (2023-10-23) ### Features * prepare for iso deprecated field removal ([#320](https://github.com/hetznercloud/hcloud-python/issues/320)) ([beae328](https://github.com/hetznercloud/hcloud-python/commit/beae328dd6b9afb8c0db9fa9b44340270db7dd09)) ### Dependencies * update pre-commit hook psf/black-pre-commit-mirror to v23.10.0 ([#319](https://github.com/hetznercloud/hcloud-python/issues/319)) ([184bbe6](https://github.com/hetznercloud/hcloud-python/commit/184bbe65a736a42d13774b6c29fa7dd8a13ec645)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 12 ++++++++++++ hcloud/__version__.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd1c79f6..49956def 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [1.31.0](https://github.com/hetznercloud/hcloud-python/compare/v1.30.0...v1.31.0) (2023-10-23) + + +### Features + +* prepare for iso deprecated field removal ([#320](https://github.com/hetznercloud/hcloud-python/issues/320)) ([beae328](https://github.com/hetznercloud/hcloud-python/commit/beae328dd6b9afb8c0db9fa9b44340270db7dd09)) + + +### Dependencies + +* update pre-commit hook psf/black-pre-commit-mirror to v23.10.0 ([#319](https://github.com/hetznercloud/hcloud-python/issues/319)) ([184bbe6](https://github.com/hetznercloud/hcloud-python/commit/184bbe65a736a42d13774b6c29fa7dd8a13ec645)) + ## [1.30.0](https://github.com/hetznercloud/hcloud-python/compare/v1.29.1...v1.30.0) (2023-10-13) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index 1a01ca7a..d7f97868 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1,3 +1,3 @@ from __future__ import annotations -VERSION = "1.30.0" # x-release-please-version +VERSION = "1.31.0" # x-release-please-version From 999afe37e02a113639930aff6879f50918ac0e89 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 10:29:33 +0200 Subject: [PATCH 134/406] deps: update pre-commit hook psf/black-pre-commit-mirror to v23.10.1 (#322) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [psf/black-pre-commit-mirror](https://togithub.com/psf/black-pre-commit-mirror) | repository | patch | `23.10.0` -> `23.10.1` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://togithub.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
psf/black-pre-commit-mirror (psf/black-pre-commit-mirror) ### [`v23.10.1`](https://togithub.com/psf/black-pre-commit-mirror/compare/23.10.0...23.10.1) [Compare Source](https://togithub.com/psf/black-pre-commit-mirror/compare/23.10.0...23.10.1)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5f19d730..d35404a0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: - id: isort - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.10.0 + rev: 23.10.1 hooks: - id: black From 7b2a24ecf69c0bead7f9113053fda37a0cc31d1b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:48:50 +0100 Subject: [PATCH 135/406] deps: update pre-commit hook psf/black-pre-commit-mirror to v23.11.0 (#324) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [psf/black-pre-commit-mirror](https://togithub.com/psf/black-pre-commit-mirror) | repository | minor | `23.10.1` -> `23.11.0` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://togithub.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
psf/black-pre-commit-mirror (psf/black-pre-commit-mirror) ### [`v23.11.0`](https://togithub.com/psf/black-pre-commit-mirror/compare/23.10.1...23.11.0) [Compare Source](https://togithub.com/psf/black-pre-commit-mirror/compare/23.10.1...23.11.0)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d35404a0..13a6bfa0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: - id: isort - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.10.1 + rev: 23.11.0 hooks: - id: black From 7b59a2decc9bb5152dc9de435bfe12ce1f34ac1c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 14:41:07 +0100 Subject: [PATCH 136/406] deps: update dependency mypy to >=1.7,<1.8 (#325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate logo banner](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [mypy](https://www.mypy-lang.org/) ([source](https://togithub.com/python/mypy), [changelog](https://mypy-lang.blogspot.com/)) | `>=1.6,<1.7` -> `>=1.7,<1.8` | [![age](https://developer.mend.io/api/mc/badges/age/pypi/mypy/1.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/mypy/1.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/mypy/1.6.1/1.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/mypy/1.6.1/1.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
python/mypy (mypy) ### [`v1.7.0`](https://togithub.com/python/mypy/compare/v1.6.1...v1.7.0) [Compare Source](https://togithub.com/python/mypy/compare/v1.6.1...v1.7.0)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d396ecb8..6c93503e 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ "coverage>=7.3,<7.4", "pylint>=3,<3.1", "pytest>=7.4,<7.5", - "mypy>=1.6,<1.7", + "mypy>=1.7,<1.8", "types-python-dateutil", "types-requests", ], From 213b661d897cdd327f478b52aeb79844826694d8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 14:41:27 +0100 Subject: [PATCH 137/406] deps: update pre-commit hook pre-commit/mirrors-prettier to v3.1.0 (#326) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate logo banner](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [pre-commit/mirrors-prettier](https://togithub.com/pre-commit/mirrors-prettier) | repository | minor | `v3.0.3` -> `v3.1.0` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://togithub.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
pre-commit/mirrors-prettier (pre-commit/mirrors-prettier) ### [`v3.1.0`](https://togithub.com/pre-commit/mirrors-prettier/compare/v3.0.3...v3.1.0) [Compare Source](https://togithub.com/pre-commit/mirrors-prettier/compare/v3.0.3...v3.1.0)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 13a6bfa0..b5f81698 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.3 + rev: v3.1.0 hooks: - id: prettier files: \.(md|ya?ml|js|css)$ From 38e098a41154e6561578cd723608fcd7577c3d01 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Fri, 17 Nov 2023 15:05:52 +0100 Subject: [PATCH 138/406] feat: allow returning root_password in servers rebuild (#276) Fixes #181 Allow returning a full response during the server rebuild by passing an option to the rebuild method. The main reason for this is to not break user land and give users time to upgrade. We also don't want to introduce a new method `rebuild2` or `rebuild_with_full_response` to keep the API clean. The deprecation timeline is the following: - Introduce the `return_response` argument and deprecated the default behavior (`return_response=False`) - Wait for the next major release to remove the ability to only return the action and deprecated the usage of the `return_response` argument. - Wait for the next+1 major release to fully remove the `return_response` argument, or simply leave it unused. --- hcloud/servers/client.py | 42 ++++++++++++++++++++++++------- hcloud/servers/domain.py | 18 +++++++++++++ tests/unit/servers/test_client.py | 26 ++++++++++++++----- 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index ea728512..b6da0d3d 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient @@ -21,6 +22,7 @@ PrivateNet, PublicNetwork, PublicNetworkFirewall, + RebuildResponse, RequestConsoleResponse, ResetPasswordResponse, Server, @@ -299,13 +301,18 @@ def create_image( """ return self._client.create_image(self, description, type, labels) - def rebuild(self, image: Image | BoundImage) -> BoundAction: + def rebuild( + self, + image: Image | BoundImage, + *, + return_response: bool = False, + ) -> RebuildResponse | BoundAction: """Rebuilds a server overwriting its disk with the content of an image, thereby destroying all data on the target server. - :param image: :class:`BoundImage ` or :class:`Image ` - :return: :class:`BoundAction ` + :param image: Image to use for the rebuilt server + :param return_response: Whether to return the full response or only the action. """ - return self._client.rebuild(self, image) + return self._client.rebuild(self, image, return_response=return_response) def change_type( self, @@ -930,12 +937,14 @@ def rebuild( self, server: Server | BoundServer, image: Image | BoundImage, - ) -> BoundAction: + *, + return_response: bool = False, + ) -> RebuildResponse | BoundAction: """Rebuilds a server overwriting its disk with the content of an image, thereby destroying all data on the target server. - :param server: :class:`BoundServer ` or :class:`Server ` - :param image: :class:`BoundImage ` or :class:`Image ` - :return: :class:`BoundAction ` + :param server: Server to rebuild + :param image: Image to use for the rebuilt server + :param return_response: Whether to return the full response or only the action. """ data: dict[str, Any] = {"image": image.id_or_name} response = self._client.request( @@ -943,7 +952,22 @@ def rebuild( method="POST", json=data, ) - return BoundAction(self._client.actions, response["action"]) + + rebuild_response = RebuildResponse( + action=BoundAction(self._client.actions, response["action"]), + root_password=response.get("root_password"), + ) + + if not return_response: + warnings.warn( + "Returning only the 'action' is deprecated, please set the " + "'return_response' keyword argument to 'True' to return the full " + "rebuild response and update your code accordingly.", + DeprecationWarning, + stacklevel=2, + ) + return rebuild_response.action + return rebuild_response def enable_backup(self, server: Server | BoundServer) -> BoundAction: """Enables and configures the automatic daily backup option for the server. Enabling automatic backups will increase the price of the server by 20%. diff --git a/hcloud/servers/domain.py b/hcloud/servers/domain.py index 2d55fd30..3802020d 100644 --- a/hcloud/servers/domain.py +++ b/hcloud/servers/domain.py @@ -244,6 +244,24 @@ def __init__( self.password = password +class RebuildResponse(BaseDomain): + """Rebuild Response Domain + + :param action: Shows the progress of the server rebuild action + :param root_password: The root password of the server when not using SSH keys + """ + + __slots__ = ("action", "root_password") + + def __init__( + self, + action: BoundAction, + root_password: str | None, + ): + self.action = action + self.root_password = root_password + + class PublicNetwork(BaseDomain): """Public Network Domain diff --git a/tests/unit/servers/test_client.py b/tests/unit/servers/test_client.py index 2490ecfa..a8ba3556 100644 --- a/tests/unit/servers/test_client.py +++ b/tests/unit/servers/test_client.py @@ -307,15 +307,19 @@ def test_create_image( def test_rebuild(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action - action = bound_server.rebuild(Image(name="ubuntu-20.04")) + response = bound_server.rebuild( + Image(name="ubuntu-20.04"), + return_response=True, + ) hetzner_client.request.assert_called_with( url="/servers/14/actions/rebuild", method="POST", json={"image": "ubuntu-20.04"}, ) - assert action.id == 1 - assert action.progress == 0 + assert response.action.id == 1 + assert response.action.progress == 0 + assert response.root_password is None or isinstance(response.root_password, str) def test_enable_backup(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action @@ -1040,15 +1044,25 @@ def test_create_image(self, servers_client, server, response_server_create_image ) def test_rebuild(self, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action - action = servers_client.rebuild(server, Image(name="ubuntu-20.04")) + response = servers_client.rebuild( + server, + Image(name="ubuntu-20.04"), + return_response=True, + ) servers_client._client.request.assert_called_with( url="/servers/1/actions/rebuild", method="POST", json={"image": "ubuntu-20.04"}, ) - assert action.id == 1 - assert action.progress == 0 + assert response.action.id == 1 + assert response.action.progress == 0 + assert response.root_password is None or isinstance(response.root_password, str) + + def test_rebuild_return_response_deprecation(self, servers_client, generic_action): + servers_client._client.request.return_value = generic_action + with pytest.deprecated_call(): + servers_client.rebuild(Server(id=1), Image(name="ubuntu-20.04")) @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] From 155a565bd3dfbce3177554626ccea6ac35807786 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Fri, 17 Nov 2023 15:09:29 +0100 Subject: [PATCH 139/406] chore(main): release 1.32.0 (#323) :robot: I have created a release *beep* *boop* --- ## [1.32.0](https://github.com/hetznercloud/hcloud-python/compare/v1.31.0...v1.32.0) (2023-11-17) ### Features * allow returning root_password in servers rebuild ([#276](https://github.com/hetznercloud/hcloud-python/issues/276)) ([38e098a](https://github.com/hetznercloud/hcloud-python/commit/38e098a41154e6561578cd723608fcd7577c3d01)) ### Dependencies * update dependency mypy to >=1.7,<1.8 ([#325](https://github.com/hetznercloud/hcloud-python/issues/325)) ([7b59a2d](https://github.com/hetznercloud/hcloud-python/commit/7b59a2decc9bb5152dc9de435bfe12ce1f34ac1c)) * update pre-commit hook pre-commit/mirrors-prettier to v3.1.0 ([#326](https://github.com/hetznercloud/hcloud-python/issues/326)) ([213b661](https://github.com/hetznercloud/hcloud-python/commit/213b661d897cdd327f478b52aeb79844826694d8)) * update pre-commit hook psf/black-pre-commit-mirror to v23.10.1 ([#322](https://github.com/hetznercloud/hcloud-python/issues/322)) ([999afe3](https://github.com/hetznercloud/hcloud-python/commit/999afe37e02a113639930aff6879f50918ac0e89)) * update pre-commit hook psf/black-pre-commit-mirror to v23.11.0 ([#324](https://github.com/hetznercloud/hcloud-python/issues/324)) ([7b2a24e](https://github.com/hetznercloud/hcloud-python/commit/7b2a24ecf69c0bead7f9113053fda37a0cc31d1b)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 15 +++++++++++++++ hcloud/__version__.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49956def..7a8f4b74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [1.32.0](https://github.com/hetznercloud/hcloud-python/compare/v1.31.0...v1.32.0) (2023-11-17) + + +### Features + +* allow returning root_password in servers rebuild ([#276](https://github.com/hetznercloud/hcloud-python/issues/276)) ([38e098a](https://github.com/hetznercloud/hcloud-python/commit/38e098a41154e6561578cd723608fcd7577c3d01)) + + +### Dependencies + +* update dependency mypy to >=1.7,<1.8 ([#325](https://github.com/hetznercloud/hcloud-python/issues/325)) ([7b59a2d](https://github.com/hetznercloud/hcloud-python/commit/7b59a2decc9bb5152dc9de435bfe12ce1f34ac1c)) +* update pre-commit hook pre-commit/mirrors-prettier to v3.1.0 ([#326](https://github.com/hetznercloud/hcloud-python/issues/326)) ([213b661](https://github.com/hetznercloud/hcloud-python/commit/213b661d897cdd327f478b52aeb79844826694d8)) +* update pre-commit hook psf/black-pre-commit-mirror to v23.10.1 ([#322](https://github.com/hetznercloud/hcloud-python/issues/322)) ([999afe3](https://github.com/hetznercloud/hcloud-python/commit/999afe37e02a113639930aff6879f50918ac0e89)) +* update pre-commit hook psf/black-pre-commit-mirror to v23.11.0 ([#324](https://github.com/hetznercloud/hcloud-python/issues/324)) ([7b2a24e](https://github.com/hetznercloud/hcloud-python/commit/7b2a24ecf69c0bead7f9113053fda37a0cc31d1b)) + ## [1.31.0](https://github.com/hetznercloud/hcloud-python/compare/v1.30.0...v1.31.0) (2023-10-23) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index d7f97868..ce6064e7 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1,3 +1,3 @@ from __future__ import annotations -VERSION = "1.31.0" # x-release-please-version +VERSION = "1.32.0" # x-release-please-version From 1c94153d93acd567548604b08b5fabeabd8d33d9 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 27 Nov 2023 14:20:11 +0100 Subject: [PATCH 140/406] fix: fallback to error code when message is unset (#328) When the API return a `server_error`, it may not provide a message. This ensures that we print `server_none` instead of `None` when printing the exception. --- hcloud/_exceptions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hcloud/_exceptions.py b/hcloud/_exceptions.py index 51e3745a..877083f8 100644 --- a/hcloud/_exceptions.py +++ b/hcloud/_exceptions.py @@ -10,8 +10,8 @@ class HCloudException(Exception): class APIException(HCloudException): """There was an error while performing an API Request""" - def __init__(self, code: int | str, message: str, details: Any): - super().__init__(message) + def __init__(self, code: int | str, message: str | None, details: Any): + super().__init__(code if message is None and isinstance(code, str) else message) self.code = code self.message = message self.details = details From 7cc4335cacab6073cf39a0ecbecf8890903d5bca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 09:24:48 +0100 Subject: [PATCH 141/406] deps: update dependency sphinx-rtd-theme to v2 (#330) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate logo banner](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [sphinx-rtd-theme](https://togithub.com/readthedocs/sphinx_rtd_theme) | `>=1.3.0,<1.4` -> `>=2,<2.1` | [![age](https://developer.mend.io/api/mc/badges/age/pypi/sphinx-rtd-theme/2.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/sphinx-rtd-theme/2.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/sphinx-rtd-theme/1.3.0/2.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/sphinx-rtd-theme/1.3.0/2.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
readthedocs/sphinx_rtd_theme (sphinx-rtd-theme) ### [`v2.0.0`](https://togithub.com/readthedocs/sphinx_rtd_theme/compare/1.3.0...2.0.0) [Compare Source](https://togithub.com/readthedocs/sphinx_rtd_theme/compare/1.3.0...2.0.0)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6c93503e..14e2e2d1 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ extras_require={ "docs": [ "sphinx>=7.2.2,<7.3", - "sphinx-rtd-theme>=1.3.0,<1.4", + "sphinx-rtd-theme>=2,<2.1", "myst-parser>=2.0.0,<2.1", "watchdog>=3.0.0,<3.1", ], From 2ac252d18ba6079d5372c6ab9e3f67b4740db465 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 09:31:23 +0100 Subject: [PATCH 142/406] deps: update actions/setup-python action to v5 (#335) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/setup-python](https://togithub.com/actions/setup-python) | action | major | `v4` -> `v5` | --- ### Release Notes
actions/setup-python (actions/setup-python) ### [`v5`](https://togithub.com/actions/setup-python/compare/v4...v5) [Compare Source](https://togithub.com/actions/setup-python/compare/v4...v5)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/lint.yml | 4 ++-- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a824f818..4b09e8e7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.x @@ -28,7 +28,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.x diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e552128..8b24772a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.x diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 19718451..a286cac1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} From 3244cfef2f90ef52d0fb791d514d6afe481aa4d7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 09:31:39 +0100 Subject: [PATCH 143/406] deps: update pre-commit hook pycqa/isort to v5.13.0 (#336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [pycqa/isort](https://togithub.com/pycqa/isort) | repository | minor | `5.12.0` -> `5.13.0` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://togithub.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
pycqa/isort (pycqa/isort) ### [`v5.13.0`](https://togithub.com/pycqa/isort/blob/HEAD/CHANGELOG.md#5130-December-9-2023) [Compare Source](https://togithub.com/pycqa/isort/compare/5.12.0...5.13.0) - Cleanup deprecated extras ([#​2089](https://togithub.com/pycqa/isort/issues/2089)) [@​staticdev](https://togithub.com/staticdev) - Fixed [#​1989](https://togithub.com/pycqa/isort/issues/1989): settings lookup when working in stream based mode - Fixed 80 line length for wemake linter ([#​2183](https://togithub.com/pycqa/isort/issues/2183)) [@​skatromb](https://togithub.com/skatromb) - Add support for Python 3.12 ([#​2175](https://togithub.com/pycqa/isort/issues/2175)) [@​hugovk](https://togithub.com/hugovk) - Fixed: add newest version to pre-commit docs ([#​2190](https://togithub.com/pycqa/isort/issues/2190)) [@​AzulGarza](https://togithub.com/AzulGarza) - Fixed assertions in test_git_hook ([#​2196](https://togithub.com/pycqa/isort/issues/2196)) [@​mgorny](https://togithub.com/mgorny) - Removed check for include_trailing_comma for the Hanging Indent wrap mode ([#​2192](https://togithub.com/pycqa/isort/issues/2192)) [@​bp72](https://togithub.com/bp72) - Use the standard library tomllib on sufficiently new python ([#​2202](https://togithub.com/pycqa/isort/issues/2202)) [@​eli-schwartz](https://togithub.com/eli-schwartz) - Update pre-commit.md version number ([#​2197](https://togithub.com/pycqa/isort/issues/2197)) [@​nicobako](https://togithub.com/nicobako) - doc: Update black_compatibility.md ([#​2177](https://togithub.com/pycqa/isort/issues/2177)) [@​JSS95](https://togithub.com/JSS95) - Fixed safety sept 2023 ([#​2178](https://togithub.com/pycqa/isort/issues/2178)) [@​staticdev](https://togithub.com/staticdev) - docs: fix black profile documentation ([#​2163](https://togithub.com/pycqa/isort/issues/2163)) [@​nijel](https://togithub.com/nijel) - Fixed typo: indended -> indented ([#​2161](https://togithub.com/pycqa/isort/issues/2161)) [@​vadimkerr](https://togithub.com/vadimkerr) - Docs(configuration/options.md): fix missing trailing spaces for hard linebreak ([#​2157](https://togithub.com/pycqa/isort/issues/2157)) [@​JoeyTeng](https://togithub.com/JoeyTeng) - Update pre-commit.md ([#​2148](https://togithub.com/pycqa/isort/issues/2148)) [@​godiard](https://togithub.com/godiard) - chore: move configurations to pyproject.toml ([#​2115](https://togithub.com/pycqa/isort/issues/2115)) [@​SauravMaheshkar](https://togithub.com/SauravMaheshkar) - Fixed typo in README ([#​2112](https://togithub.com/pycqa/isort/issues/2112)) [@​stefmolin](https://togithub.com/stefmolin) - Update version in pre-commit setup to avoid installation issue with poetry ([#​2103](https://togithub.com/pycqa/isort/issues/2103)) [@​stefmolin](https://togithub.com/stefmolin) - Skip .pytype directory by default. ([#​2098](https://togithub.com/pycqa/isort/issues/2098)) [@​manueljacob](https://togithub.com/manueljacob) - Fixed a tip block styling in the Config Files section ([#​2097](https://togithub.com/pycqa/isort/issues/2097)) [@​Klavionik](https://togithub.com/Klavionik) - Do not cache configuration files ([#​1995](https://togithub.com/pycqa/isort/issues/1995)) [@​kaste](https://togithub.com/kaste) - Derive settings_path from --filename ([#​1992](https://togithub.com/pycqa/isort/issues/1992)) [@​kaste](https://togithub.com/kaste) - Fixed year of version 5.12.0 in CHANGELOG.md ([#​2082](https://togithub.com/pycqa/isort/issues/2082)) [@​DjLegolas](https://togithub.com/DjLegolas)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5f81698..af06baf3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,7 +36,7 @@ repos: args: [--py38-plus] - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 5.13.0 hooks: - id: isort From 020a0eff6bc2b63d16b339fd5d4c3ea3610c0509 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 11:38:35 +0100 Subject: [PATCH 144/406] deps: update pre-commit hook pycqa/isort to v5.13.1 (#337) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [pycqa/isort](https://togithub.com/pycqa/isort) | repository | patch | `5.13.0` -> `5.13.1` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://togithub.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
pycqa/isort (pycqa/isort) ### [`v5.13.1`](https://togithub.com/pycqa/isort/blob/HEAD/CHANGELOG.md#5131-December-11-2023) [Compare Source](https://togithub.com/pycqa/isort/compare/5.13.0...5.13.1) - Fixed integration tests ([#​2208](https://togithub.com/pycqa/isort/issues/2208)) [@​bp72](https://togithub.com/bp72) - Fixed normalizing imports from more than one level of parent modules (issue/2152) ([#​2191](https://togithub.com/pycqa/isort/issues/2191)) [@​bp72](https://togithub.com/bp72) - Remove optional dependencies without extras ([#​2207](https://togithub.com/pycqa/isort/issues/2207)) [@​staticdev](https://togithub.com/staticdev)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af06baf3..7dc5e29b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,7 +36,7 @@ repos: args: [--py38-plus] - repo: https://github.com/pycqa/isort - rev: 5.13.0 + rev: 5.13.1 hooks: - id: isort From 38e4748d3d194d37ea3d0c63683609f5db432e0d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:56:05 +0100 Subject: [PATCH 145/406] deps: update pre-commit hook psf/black-pre-commit-mirror to v23.12.0 (#338) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [psf/black-pre-commit-mirror](https://togithub.com/psf/black-pre-commit-mirror) | repository | minor | `23.11.0` -> `23.12.0` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://togithub.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
psf/black-pre-commit-mirror (psf/black-pre-commit-mirror) ### [`v23.12.0`](https://togithub.com/psf/black-pre-commit-mirror/compare/23.11.0...23.12.0) [Compare Source](https://togithub.com/psf/black-pre-commit-mirror/compare/23.11.0...23.12.0)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7dc5e29b..8f8abb52 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: - id: isort - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.11.0 + rev: 23.12.0 hooks: - id: black From b46df8cbb263945c59ce4408e0a7189d19d9c597 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:07:47 +0100 Subject: [PATCH 146/406] deps: update pre-commit hook pycqa/isort to v5.13.2 (#339) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [pycqa/isort](https://togithub.com/pycqa/isort) | repository | patch | `5.13.1` -> `5.13.2` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://togithub.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
pycqa/isort (pycqa/isort) ### [`v5.13.2`](https://togithub.com/pycqa/isort/blob/HEAD/CHANGELOG.md#5132-December-13-2023) [Compare Source](https://togithub.com/pycqa/isort/compare/5.13.1...5.13.2) - Apply the bracket fix from issue [#​471](https://togithub.com/pycqa/isort/issues/471) only for use_parentheses=True ([#​2184](https://togithub.com/pycqa/isort/issues/2184)) [@​bp72](https://togithub.com/bp72) - Confine pre-commit to stages ([#​2213](https://togithub.com/pycqa/isort/issues/2213)) [@​davidculley](https://togithub.com/davidculley) - Fixed colors extras ([#​2212](https://togithub.com/pycqa/isort/issues/2212)) [@​staticdev](https://togithub.com/staticdev)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8f8abb52..c732f569 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,7 +36,7 @@ repos: args: [--py38-plus] - repo: https://github.com/pycqa/isort - rev: 5.13.1 + rev: 5.13.2 hooks: - id: isort From ee3c54fd1b6963533bc9d1e1f9ff57f6c5872cd5 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Tue, 19 Dec 2023 08:55:58 +0100 Subject: [PATCH 147/406] feat: add metrics endpoint for load balancers and servers (#331) Adds the missing metrics endpoint for the load balancers and servers resources. - https://docs.hetzner.cloud/#load-balancers-get-metrics-for-a-loadbalancer - https://docs.hetzner.cloud/#servers-get-metrics-for-a-server --- examples/get_server_metrics.py | 36 +++++++++++++ hcloud/load_balancers/__init__.py | 1 + hcloud/load_balancers/client.py | 68 +++++++++++++++++++++++ hcloud/load_balancers/domain.py | 26 ++++++++- hcloud/metrics/__init__.py | 3 ++ hcloud/metrics/domain.py | 46 ++++++++++++++++ hcloud/servers/__init__.py | 1 + hcloud/servers/client.py | 69 ++++++++++++++++++++++++ hcloud/servers/domain.py | 25 ++++++++- tests/unit/load_balancers/conftest.py | 20 +++++++ tests/unit/load_balancers/test_client.py | 24 +++++++++ tests/unit/servers/conftest.py | 48 +++++++++++++++++ tests/unit/servers/test_client.py | 26 +++++++++ 13 files changed, 391 insertions(+), 2 deletions(-) create mode 100644 examples/get_server_metrics.py create mode 100644 hcloud/metrics/__init__.py create mode 100644 hcloud/metrics/domain.py diff --git a/examples/get_server_metrics.py b/examples/get_server_metrics.py new file mode 100644 index 00000000..cf91eaec --- /dev/null +++ b/examples/get_server_metrics.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import json +from datetime import datetime, timedelta +from os import environ + +from hcloud import Client +from hcloud.images import Image +from hcloud.server_types import ServerType + +assert ( + "HCLOUD_TOKEN" in environ +), "Please export your API token in the HCLOUD_TOKEN environment variable" +token = environ["HCLOUD_TOKEN"] + +client = Client(token=token) + +server = client.servers.get_by_name("my-server") +if server is None: + response = client.servers.create( + name="my-server", + server_type=ServerType("cx11"), + image=Image(name="ubuntu-22.04"), + ) + server = response.server + +end = datetime.now() +start = end - timedelta(hours=1) + +response = server.get_metrics( + type=["cpu", "network"], + start=start, + end=end, +) + +print(json.dumps(response.metrics)) diff --git a/hcloud/load_balancers/__init__.py b/hcloud/load_balancers/__init__.py index 4ac79ce5..4bfd7994 100644 --- a/hcloud/load_balancers/__init__.py +++ b/hcloud/load_balancers/__init__.py @@ -7,6 +7,7 @@ ) from .domain import ( # noqa: F401 CreateLoadBalancerResponse, + GetMetricsResponse, IPv4Address, IPv6Network, LoadBalancer, diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index 56b93c8b..e0635eec 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -1,16 +1,21 @@ from __future__ import annotations +from datetime import datetime from typing import TYPE_CHECKING, Any, NamedTuple +from dateutil.parser import isoparse + from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient from ..certificates import BoundCertificate from ..core import BoundModelBase, ClientEntityBase, Meta from ..load_balancer_types import BoundLoadBalancerType from ..locations import BoundLocation +from ..metrics import Metrics from ..networks import BoundNetwork from ..servers import BoundServer from .domain import ( CreateLoadBalancerResponse, + GetMetricsResponse, IPv4Address, IPv6Network, LoadBalancer, @@ -23,6 +28,7 @@ LoadBalancerTargetHealthStatus, LoadBalancerTargetIP, LoadBalancerTargetLabelSelector, + MetricsType, PrivateNet, PublicNetwork, ) @@ -177,6 +183,28 @@ def delete(self) -> bool: """ return self._client.delete(self) + def get_metrics( + self, + type: MetricsType, + start: datetime | str, + end: datetime | str, + step: float | None = None, + ) -> GetMetricsResponse: + """Get Metrics for a LoadBalancer. + + :param type: Type of metrics to get. + :param start: Start of period to get Metrics for (in ISO-8601 format). + :param end: End of period to get Metrics for (in ISO-8601 format). + :param step: Resolution of results in seconds. + """ + return self._client.get_metrics( + self, + type=type, + start=start, + end=end, + step=step, + ) + def get_actions_list( self, status: list[str] | None = None, @@ -533,6 +561,46 @@ def delete(self, load_balancer: LoadBalancer | BoundLoadBalancer) -> bool: ) return True + def get_metrics( + self, + load_balancer: LoadBalancer | BoundLoadBalancer, + type: MetricsType | list[MetricsType], + start: datetime | str, + end: datetime | str, + step: float | None = None, + ) -> GetMetricsResponse: + """Get Metrics for a LoadBalancer. + + :param load_balancer: The Load Balancer to get the metrics for. + :param type: Type of metrics to get. + :param start: Start of period to get Metrics for (in ISO-8601 format). + :param end: End of period to get Metrics for (in ISO-8601 format). + :param step: Resolution of results in seconds. + """ + if not isinstance(type, list): + type = [type] + if isinstance(start, str): + start = isoparse(start) + if isinstance(end, str): + end = isoparse(end) + + params: dict[str, Any] = { + "type": ",".join(type), + "start": start.isoformat(), + "end": end.isoformat(), + } + if step is not None: + params["step"] = step + + response = self._client.request( + url=f"/load_balancers/{load_balancer.id}/metrics", + method="GET", + params=params, + ) + return GetMetricsResponse( + metrics=Metrics(**response["metrics"]), + ) + def get_actions_list( self, load_balancer: LoadBalancer | BoundLoadBalancer, diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index 212bbdc0..2d480eec 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal from dateutil.parser import isoparse @@ -11,6 +11,7 @@ from ..certificates import BoundCertificate from ..load_balancer_types import BoundLoadBalancerType from ..locations import BoundLocation + from ..metrics import Metrics from ..networks import BoundNetwork from ..servers import BoundServer from .client import BoundLoadBalancer @@ -508,3 +509,26 @@ def __init__( ): self.load_balancer = load_balancer self.action = action + + +MetricsType = Literal[ + "open_connections", + "connections_per_second", + "requests_per_second", + "bandwidth", +] + + +class GetMetricsResponse(BaseDomain): + """Get a Load Balancer Metrics Response Domain + + :param metrics: The Load Balancer metrics + """ + + __slots__ = ("metrics",) + + def __init__( + self, + metrics: Metrics, + ): + self.metrics = metrics diff --git a/hcloud/metrics/__init__.py b/hcloud/metrics/__init__.py new file mode 100644 index 00000000..65d393c8 --- /dev/null +++ b/hcloud/metrics/__init__.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +from .domain import Metrics, TimeSeries # noqa: F401 diff --git a/hcloud/metrics/domain.py b/hcloud/metrics/domain.py new file mode 100644 index 00000000..4693f155 --- /dev/null +++ b/hcloud/metrics/domain.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Dict, List, Literal, Tuple + +from dateutil.parser import isoparse + +from ..core import BaseDomain + +TimeSeries = Dict[str, Dict[Literal["values"], List[Tuple[float, str]]]] + + +class Metrics(BaseDomain): + """Metrics Domain + + :param start: Start of period of metrics reported. + :param end: End of period of metrics reported. + :param step: Resolution of results in seconds. + :param time_series: Dict with time series data, using the name of the time series as + key. The metrics timestamps and values are stored in a list of tuples + ``[(timestamp, value), ...]``. + """ + + start: datetime + end: datetime + step: float + time_series: TimeSeries + + __slots__ = ( + "start", + "end", + "step", + "time_series", + ) + + def __init__( + self, + start: str, + end: str, + step: float, + time_series: TimeSeries, + ): + self.start = isoparse(start) + self.end = isoparse(end) + self.step = step + self.time_series = time_series diff --git a/hcloud/servers/__init__.py b/hcloud/servers/__init__.py index a7a61d4e..58c811e5 100644 --- a/hcloud/servers/__init__.py +++ b/hcloud/servers/__init__.py @@ -4,6 +4,7 @@ from .domain import ( # noqa: F401 CreateServerResponse, EnableRescueResponse, + GetMetricsResponse, IPv4Address, IPv6Network, PrivateNet, diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index b6da0d3d..e4b26795 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -1,8 +1,11 @@ from __future__ import annotations import warnings +from datetime import datetime from typing import TYPE_CHECKING, Any, NamedTuple +from dateutil.parser import isoparse + from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient from ..core import BoundModelBase, ClientEntityBase, Meta from ..datacenters import BoundDatacenter @@ -10,6 +13,7 @@ from ..floating_ips import BoundFloatingIP from ..images import BoundImage, CreateImageResponse from ..isos import BoundIso +from ..metrics import Metrics from ..placement_groups import BoundPlacementGroup from ..primary_ips import BoundPrimaryIP from ..server_types import BoundServerType @@ -17,8 +21,10 @@ from .domain import ( CreateServerResponse, EnableRescueResponse, + GetMetricsResponse, IPv4Address, IPv6Network, + MetricsType, PrivateNet, PublicNetwork, PublicNetworkFirewall, @@ -210,6 +216,29 @@ def update( """ return self._client.update(self, name, labels) + def get_metrics( + self, + type: MetricsType | list[MetricsType], + start: datetime | str, + end: datetime | str, + step: float | None = None, + ) -> GetMetricsResponse: + """Get Metrics for a Server. + + :param server: The Server to get the metrics for. + :param type: Type of metrics to get. + :param start: Start of period to get Metrics for (in ISO-8601 format). + :param end: End of period to get Metrics for (in ISO-8601 format). + :param step: Resolution of results in seconds. + """ + return self._client.get_metrics( + self, + type=type, + start=start, + end=end, + step=step, + ) + def delete(self) -> BoundAction: """Deletes a server. This immediately removes the server from your account, and it is no longer accessible. @@ -742,6 +771,46 @@ def update( ) return BoundServer(self, response["server"]) + def get_metrics( + self, + server: Server | BoundServer, + type: MetricsType | list[MetricsType], + start: datetime | str, + end: datetime | str, + step: float | None = None, + ) -> GetMetricsResponse: + """Get Metrics for a Server. + + :param server: The Server to get the metrics for. + :param type: Type of metrics to get. + :param start: Start of period to get Metrics for (in ISO-8601 format). + :param end: End of period to get Metrics for (in ISO-8601 format). + :param step: Resolution of results in seconds. + """ + if not isinstance(type, list): + type = [type] + if isinstance(start, str): + start = isoparse(start) + if isinstance(end, str): + end = isoparse(end) + + params: dict[str, Any] = { + "type": ",".join(type), + "start": start.isoformat(), + "end": end.isoformat(), + } + if step is not None: + params["step"] = step + + response = self._client.request( + url=f"/servers/{server.id}/metrics", + method="GET", + params=params, + ) + return GetMetricsResponse( + metrics=Metrics(**response["metrics"]), + ) + def delete(self, server: Server | BoundServer) -> BoundAction: """Deletes a server. This immediately removes the server from your account, and it is no longer accessible. diff --git a/hcloud/servers/domain.py b/hcloud/servers/domain.py index 3802020d..29afecf3 100644 --- a/hcloud/servers/domain.py +++ b/hcloud/servers/domain.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal from dateutil.parser import isoparse @@ -13,6 +13,7 @@ from ..floating_ips import BoundFloatingIP from ..images import BoundImage from ..isos import BoundIso + from ..metrics import Metrics from ..networks import BoundNetwork from ..placement_groups import BoundPlacementGroup from ..primary_ips import BoundPrimaryIP, PrimaryIP @@ -427,3 +428,25 @@ def __init__( self.ipv6 = ipv6 self.enable_ipv4 = enable_ipv4 self.enable_ipv6 = enable_ipv6 + + +MetricsType = Literal[ + "cpu", + "disk", + "network", +] + + +class GetMetricsResponse(BaseDomain): + """Get a Server Metrics Response Domain + + :param metrics: The Server metrics + """ + + __slots__ = ("metrics",) + + def __init__( + self, + metrics: Metrics, + ): + self.metrics = metrics diff --git a/tests/unit/load_balancers/conftest.py b/tests/unit/load_balancers/conftest.py index dff89b10..f19508ea 100644 --- a/tests/unit/load_balancers/conftest.py +++ b/tests/unit/load_balancers/conftest.py @@ -444,6 +444,26 @@ def response_simple_load_balancers(): } +@pytest.fixture() +def response_get_metrics(): + return { + "metrics": { + "start": "2023-12-14T16:55:32+01:00", + "end": "2023-12-14T17:25:32+01:00", + "step": 9.0, + "time_series": { + "requests_per_second": { + "values": [ + [1702571114, "0.000000"], + [1702571123, "0.000000"], + [1702571132, "0.000000"], + ] + } + }, + } + } + + @pytest.fixture() def response_add_service(): return { diff --git a/tests/unit/load_balancers/test_client.py b/tests/unit/load_balancers/test_client.py index 191334c3..3e9f281a 100644 --- a/tests/unit/load_balancers/test_client.py +++ b/tests/unit/load_balancers/test_client.py @@ -96,6 +96,30 @@ def test_delete(self, hetzner_client, generic_action, bound_load_balancer): assert delete_success is True + def test_get_metrics( + self, + hetzner_client, + response_get_metrics, + bound_load_balancer: BoundLoadBalancer, + ): + hetzner_client.request.return_value = response_get_metrics + response = bound_load_balancer.get_metrics( + type=["requests_per_second"], + start="2023-12-14T16:55:32+01:00", + end="2023-12-14T16:55:32+01:00", + ) + hetzner_client.request.assert_called_with( + url="/load_balancers/14/metrics", + method="GET", + params={ + "type": "requests_per_second", + "start": "2023-12-14T16:55:32+01:00", + "end": "2023-12-14T16:55:32+01:00", + }, + ) + assert "requests_per_second" in response.metrics.time_series + assert len(response.metrics.time_series["requests_per_second"]["values"]) == 3 + def test_add_service( self, hetzner_client, response_add_service, bound_load_balancer ): diff --git a/tests/unit/servers/conftest.py b/tests/unit/servers/conftest.py index d1c304e5..01649320 100644 --- a/tests/unit/servers/conftest.py +++ b/tests/unit/servers/conftest.py @@ -338,6 +338,54 @@ def response_update_server(): } +@pytest.fixture() +def response_get_metrics(): + return { + "metrics": { + "start": "2023-12-14T17:40:00+01:00", + "end": "2023-12-14T17:50:00+01:00", + "step": 3.0, + "time_series": { + "cpu": { + "values": [ + [1702572594, "0.3746000025854892"], + [1702572597, "0.35842215349409734"], + [1702572600, "0.7381525488039541"], + ] + }, + "disk.0.iops.read": { + "values": [ + [1702572594, "0"], + [1702572597, "0"], + [1702572600, "0"], + ] + }, + "disk.0.bandwidth.read": { + "values": [ + [1702572594, "0"], + [1702572597, "0"], + [1702572600, "0"], + ] + }, + "disk.0.bandwidth.write": { + "values": [ + [1702572594, "24064"], + [1702572597, "2048"], + [1702572600, "0"], + ] + }, + "disk.0.iops.write": { + "values": [ + [1702572594, "4.875"], + [1702572597, "0.25"], + [1702572600, "0"], + ] + }, + }, + } + } + + @pytest.fixture() def response_simple_servers(): return { diff --git a/tests/unit/servers/test_client.py b/tests/unit/servers/test_client.py index a8ba3556..70823c30 100644 --- a/tests/unit/servers/test_client.py +++ b/tests/unit/servers/test_client.py @@ -190,6 +190,32 @@ def test_delete(self, hetzner_client, bound_server, generic_action): assert action.id == 1 assert action.progress == 0 + def test_get_metrics( + self, + hetzner_client, + bound_server: BoundServer, + response_get_metrics, + ): + hetzner_client.request.return_value = response_get_metrics + response = bound_server.get_metrics( + type=["cpu", "disk"], + start="2023-12-14T17:40:00+01:00", + end="2023-12-14T17:50:00+01:00", + ) + hetzner_client.request.assert_called_with( + url="/servers/14/metrics", + method="GET", + params={ + "type": "cpu,disk", + "start": "2023-12-14T17:40:00+01:00", + "end": "2023-12-14T17:50:00+01:00", + }, + ) + + assert "cpu" in response.metrics.time_series + assert "disk.0.iops.read" in response.metrics.time_series + assert len(response.metrics.time_series["disk.0.iops.read"]["values"]) == 3 + def test_power_off(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.power_off() From b72fe0605ec6b43ab189038bf8be06100e83021b Mon Sep 17 00:00:00 2001 From: Jonas L Date: Tue, 19 Dec 2023 14:20:55 +0100 Subject: [PATCH 148/406] ci: update release-please-action to v4 (#341) --- .github/release-please-config.json | 13 +++++++++++++ .github/release-please-manifest.json | 1 + .github/workflows/release-please.yml | 13 +++++-------- 3 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 .github/release-please-config.json create mode 100644 .github/release-please-manifest.json diff --git a/.github/release-please-config.json b/.github/release-please-config.json new file mode 100644 index 00000000..4fccf055 --- /dev/null +++ b/.github/release-please-config.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "155a565bd3dfbce3177554626ccea6ac35807786", + "include-component-in-tag": false, + "include-v-in-tag": true, + "packages": { + ".": { + "release-type": "python", + "package-name": "hcloud", + "extra-files": ["hcloud/__version__.py"] + } + } +} diff --git a/.github/release-please-manifest.json b/.github/release-please-manifest.json new file mode 100644 index 00000000..a99de526 --- /dev/null +++ b/.github/release-please-manifest.json @@ -0,0 +1 @@ +{".":"1.32.0"} diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index b11e876c..ba90290e 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -1,4 +1,4 @@ -name: Release please +name: Release-please on: push: @@ -6,16 +6,13 @@ on: jobs: release-please: - # The secret HCLOUD_BOT_TOKEN is only available on the main repo, not in forks. + # Do not run on forks. if: github.repository == 'hetznercloud/hcloud-python' runs-on: ubuntu-latest steps: - - uses: google-github-actions/release-please-action@v3 + - uses: google-github-actions/release-please-action@v4 with: token: ${{ secrets.HCLOUD_BOT_TOKEN }} - release-type: python - package-name: hcloud - - extra-files: | - hcloud/__version__.py + config-file: .github/release-please-config.json + manifest-file: .github/release-please-manifest.json From e5bb4107699c0ebdc90d67c5d748719a31de3c29 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Tue, 19 Dec 2023 17:33:18 +0100 Subject: [PATCH 149/406] chore(main): release 1.33.0 (#329) :robot: I have created a release *beep* *boop* --- ## [1.33.0](https://github.com/hetznercloud/hcloud-python/compare/v1.32.0...v1.33.0) (2023-12-19) ### Features * add metrics endpoint for load balancers and servers ([#331](https://github.com/hetznercloud/hcloud-python/issues/331)) ([ee3c54f](https://github.com/hetznercloud/hcloud-python/commit/ee3c54fd1b6963533bc9d1e1f9ff57f6c5872cd5)) ### Bug Fixes * fallback to error code when message is unset ([#328](https://github.com/hetznercloud/hcloud-python/issues/328)) ([1c94153](https://github.com/hetznercloud/hcloud-python/commit/1c94153d93acd567548604b08b5fabeabd8d33d9)) ### Dependencies * update actions/setup-python action to v5 ([#335](https://github.com/hetznercloud/hcloud-python/issues/335)) ([2ac252d](https://github.com/hetznercloud/hcloud-python/commit/2ac252d18ba6079d5372c6ab9e3f67b4740db465)) * update dependency sphinx-rtd-theme to v2 ([#330](https://github.com/hetznercloud/hcloud-python/issues/330)) ([7cc4335](https://github.com/hetznercloud/hcloud-python/commit/7cc4335cacab6073cf39a0ecbecf8890903d5bca)) * update pre-commit hook psf/black-pre-commit-mirror to v23.12.0 ([#338](https://github.com/hetznercloud/hcloud-python/issues/338)) ([38e4748](https://github.com/hetznercloud/hcloud-python/commit/38e4748d3d194d37ea3d0c63683609f5db432e0d)) * update pre-commit hook pycqa/isort to v5.13.0 ([#336](https://github.com/hetznercloud/hcloud-python/issues/336)) ([3244cfe](https://github.com/hetznercloud/hcloud-python/commit/3244cfef2f90ef52d0fb791d514d6afe481aa4d7)) * update pre-commit hook pycqa/isort to v5.13.1 ([#337](https://github.com/hetznercloud/hcloud-python/issues/337)) ([020a0ef](https://github.com/hetznercloud/hcloud-python/commit/020a0eff6bc2b63d16b339fd5d4c3ea3610c0509)) * update pre-commit hook pycqa/isort to v5.13.2 ([#339](https://github.com/hetznercloud/hcloud-python/issues/339)) ([b46df8c](https://github.com/hetznercloud/hcloud-python/commit/b46df8cbb263945c59ce4408e0a7189d19d9c597)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 22 ++++++++++++++++++++++ hcloud/__version__.py | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a8f4b74..868ebe67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## [1.33.0](https://github.com/hetznercloud/hcloud-python/compare/v1.32.0...v1.33.0) (2023-12-19) + + +### Features + +* add metrics endpoint for load balancers and servers ([#331](https://github.com/hetznercloud/hcloud-python/issues/331)) ([ee3c54f](https://github.com/hetznercloud/hcloud-python/commit/ee3c54fd1b6963533bc9d1e1f9ff57f6c5872cd5)) + + +### Bug Fixes + +* fallback to error code when message is unset ([#328](https://github.com/hetznercloud/hcloud-python/issues/328)) ([1c94153](https://github.com/hetznercloud/hcloud-python/commit/1c94153d93acd567548604b08b5fabeabd8d33d9)) + + +### Dependencies + +* update actions/setup-python action to v5 ([#335](https://github.com/hetznercloud/hcloud-python/issues/335)) ([2ac252d](https://github.com/hetznercloud/hcloud-python/commit/2ac252d18ba6079d5372c6ab9e3f67b4740db465)) +* update dependency sphinx-rtd-theme to v2 ([#330](https://github.com/hetznercloud/hcloud-python/issues/330)) ([7cc4335](https://github.com/hetznercloud/hcloud-python/commit/7cc4335cacab6073cf39a0ecbecf8890903d5bca)) +* update pre-commit hook psf/black-pre-commit-mirror to v23.12.0 ([#338](https://github.com/hetznercloud/hcloud-python/issues/338)) ([38e4748](https://github.com/hetznercloud/hcloud-python/commit/38e4748d3d194d37ea3d0c63683609f5db432e0d)) +* update pre-commit hook pycqa/isort to v5.13.0 ([#336](https://github.com/hetznercloud/hcloud-python/issues/336)) ([3244cfe](https://github.com/hetznercloud/hcloud-python/commit/3244cfef2f90ef52d0fb791d514d6afe481aa4d7)) +* update pre-commit hook pycqa/isort to v5.13.1 ([#337](https://github.com/hetznercloud/hcloud-python/issues/337)) ([020a0ef](https://github.com/hetznercloud/hcloud-python/commit/020a0eff6bc2b63d16b339fd5d4c3ea3610c0509)) +* update pre-commit hook pycqa/isort to v5.13.2 ([#339](https://github.com/hetznercloud/hcloud-python/issues/339)) ([b46df8c](https://github.com/hetznercloud/hcloud-python/commit/b46df8cbb263945c59ce4408e0a7189d19d9c597)) + ## [1.32.0](https://github.com/hetznercloud/hcloud-python/compare/v1.31.0...v1.32.0) (2023-11-17) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index ce6064e7..26b9a054 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1,3 +1,3 @@ from __future__ import annotations -VERSION = "1.32.0" # x-release-please-version +VERSION = "1.33.0" # x-release-please-version From 7114c16eca19ae5839b8fb9be56414d1df903c91 Mon Sep 17 00:00:00 2001 From: jo Date: Tue, 19 Dec 2023 17:35:47 +0100 Subject: [PATCH 150/406] chore: fix release please manifest after release --- .github/release-please-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/release-please-manifest.json b/.github/release-please-manifest.json index a99de526..411e3438 100644 --- a/.github/release-please-manifest.json +++ b/.github/release-please-manifest.json @@ -1 +1 @@ -{".":"1.32.0"} +{".":"1.33.0"} From 984022fd3888ef856be83de82554d55a8af18dba Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 18:27:50 +0100 Subject: [PATCH 151/406] deps: update dependency mypy to >=1.8,<1.9 (#343) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [mypy](https://www.mypy-lang.org/) ([source](https://togithub.com/python/mypy), [changelog](https://mypy-lang.blogspot.com/)) | `>=1.7,<1.8` -> `>=1.8,<1.9` | [![age](https://developer.mend.io/api/mc/badges/age/pypi/mypy/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/mypy/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/mypy/1.7.1/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/mypy/1.7.1/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
python/mypy (mypy) ### [`v1.8.0`](https://togithub.com/python/mypy/compare/v1.7.1...v1.8.0) [Compare Source](https://togithub.com/python/mypy/compare/v1.7.1...v1.8.0)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 14e2e2d1..a8c38733 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ "coverage>=7.3,<7.4", "pylint>=3,<3.1", "pytest>=7.4,<7.5", - "mypy>=1.7,<1.8", + "mypy>=1.8,<1.9", "types-python-dateutil", "types-requests", ], From 3ac57117e8a68a02cba19c56f850f037c4aca462 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jan 2024 10:17:07 +0100 Subject: [PATCH 152/406] deps: update dependency coverage to >=7.4,<7.5 (#348) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [coverage](https://togithub.com/nedbat/coveragepy) | `>=7.3,<7.4` -> `>=7.4,<7.5` | [![age](https://developer.mend.io/api/mc/badges/age/pypi/coverage/7.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/coverage/7.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/coverage/7.3.4/7.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/coverage/7.3.4/7.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
nedbat/coveragepy (coverage) ### [`v7.4.0`](https://togithub.com/nedbat/coveragepy/blob/HEAD/CHANGES.rst#Version-740--2023-12-27) [Compare Source](https://togithub.com/nedbat/coveragepy/compare/7.3.4...7.4.0) - In Python 3.12 and above, you can try an experimental core based on the new :mod:`sys.monitoring ` module by defining a `COVERAGE_CORE=sysmon` environment variable. This should be faster, though plugins and dynamic contexts are not yet supported with it. I am very interested to hear how it works (or doesn't!) for you. .. \_changes\_7-3-4:
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a8c38733..c73f3a9e 100644 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ "watchdog>=3.0.0,<3.1", ], "test": [ - "coverage>=7.3,<7.4", + "coverage>=7.4,<7.5", "pylint>=3,<3.1", "pytest>=7.4,<7.5", "mypy>=1.8,<1.9", From 2c24efe93bc221846f8dcc91abcf1aad61547875 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jan 2024 10:17:24 +0100 Subject: [PATCH 153/406] deps: update pre-commit hook psf/black-pre-commit-mirror to v23.12.1 (#347) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [psf/black-pre-commit-mirror](https://togithub.com/psf/black-pre-commit-mirror) | repository | patch | `23.12.0` -> `23.12.1` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://togithub.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
psf/black-pre-commit-mirror (psf/black-pre-commit-mirror) ### [`v23.12.1`](https://togithub.com/psf/black-pre-commit-mirror/compare/23.12.0...23.12.1) [Compare Source](https://togithub.com/psf/black-pre-commit-mirror/compare/23.12.0...23.12.1)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c732f569..0df72161 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: - id: isort - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.12.0 + rev: 23.12.1 hooks: - id: black From 5281b0583541b6e0e9b8c7ad75faa42c5d379735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Kreileder?= Date: Tue, 2 Jan 2024 10:22:31 +0100 Subject: [PATCH 154/406] fix: private object not exported in top level module (#346) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #345 --------- Signed-off-by: Jürgen Kreileder --- hcloud/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/hcloud/__init__.py b/hcloud/__init__.py index 544a2196..6b69b169 100644 --- a/hcloud/__init__.py +++ b/hcloud/__init__.py @@ -1,4 +1,7 @@ from __future__ import annotations -from ._client import Client # noqa -from ._exceptions import APIException, HCloudException # noqa +from ._client import Client as Client # noqa pylint: disable=C0414 +from ._exceptions import ( # noqa pylint: disable=C0414 + APIException as APIException, + HCloudException as HCloudException, +) From d365726aa744589dec0912523c2bc95d51bb491f Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Tue, 2 Jan 2024 11:00:50 +0100 Subject: [PATCH 155/406] chore(main): release 1.33.1 (#344) :robot: I have created a release *beep* *boop* --- ## [1.33.1](https://github.com/hetznercloud/hcloud-python/compare/v1.33.0...v1.33.1) (2024-01-02) ### Bug Fixes * private object not exported in top level module ([#346](https://github.com/hetznercloud/hcloud-python/issues/346)) ([5281b05](https://github.com/hetznercloud/hcloud-python/commit/5281b0583541b6e0e9b8c7ad75faa42c5d379735)) ### Dependencies * update dependency coverage to >=7.4,<7.5 ([#348](https://github.com/hetznercloud/hcloud-python/issues/348)) ([3ac5711](https://github.com/hetznercloud/hcloud-python/commit/3ac57117e8a68a02cba19c56f850f037c4aca462)) * update dependency mypy to >=1.8,<1.9 ([#343](https://github.com/hetznercloud/hcloud-python/issues/343)) ([984022f](https://github.com/hetznercloud/hcloud-python/commit/984022fd3888ef856be83de82554d55a8af18dba)) * update pre-commit hook psf/black-pre-commit-mirror to v23.12.1 ([#347](https://github.com/hetznercloud/hcloud-python/issues/347)) ([2c24efe](https://github.com/hetznercloud/hcloud-python/commit/2c24efe93bc221846f8dcc91abcf1aad61547875)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- .github/release-please-manifest.json | 2 +- CHANGELOG.md | 14 ++++++++++++++ hcloud/__version__.py | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/release-please-manifest.json b/.github/release-please-manifest.json index 411e3438..2692f187 100644 --- a/.github/release-please-manifest.json +++ b/.github/release-please-manifest.json @@ -1 +1 @@ -{".":"1.33.0"} +{".":"1.33.1"} diff --git a/CHANGELOG.md b/CHANGELOG.md index 868ebe67..d74e6240 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [1.33.1](https://github.com/hetznercloud/hcloud-python/compare/v1.33.0...v1.33.1) (2024-01-02) + + +### Bug Fixes + +* private object not exported in top level module ([#346](https://github.com/hetznercloud/hcloud-python/issues/346)) ([5281b05](https://github.com/hetznercloud/hcloud-python/commit/5281b0583541b6e0e9b8c7ad75faa42c5d379735)) + + +### Dependencies + +* update dependency coverage to >=7.4,<7.5 ([#348](https://github.com/hetznercloud/hcloud-python/issues/348)) ([3ac5711](https://github.com/hetznercloud/hcloud-python/commit/3ac57117e8a68a02cba19c56f850f037c4aca462)) +* update dependency mypy to >=1.8,<1.9 ([#343](https://github.com/hetznercloud/hcloud-python/issues/343)) ([984022f](https://github.com/hetznercloud/hcloud-python/commit/984022fd3888ef856be83de82554d55a8af18dba)) +* update pre-commit hook psf/black-pre-commit-mirror to v23.12.1 ([#347](https://github.com/hetznercloud/hcloud-python/issues/347)) ([2c24efe](https://github.com/hetznercloud/hcloud-python/commit/2c24efe93bc221846f8dcc91abcf1aad61547875)) + ## [1.33.0](https://github.com/hetznercloud/hcloud-python/compare/v1.32.0...v1.33.0) (2023-12-19) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index 26b9a054..29ba7586 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1,3 +1,3 @@ from __future__ import annotations -VERSION = "1.33.0" # x-release-please-version +VERSION = "1.33.1" # x-release-please-version From 1a0e93bbf1ae6cc747e6c4d8305dafd3e49dbbdc Mon Sep 17 00:00:00 2001 From: Jonas L Date: Tue, 2 Jan 2024 18:09:49 +0100 Subject: [PATCH 156/406] ci: publish package to PyPI using OIDC (#350) Fixes #349 --- .github/workflows/release.yml | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8b24772a..36cdd8da 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,11 +25,32 @@ jobs: run: python3 -m build - name: Check - run: twine check dist/* + run: twine check --strict dist/* - - name: Publish + - name: Upload packages artifact if: github.event_name == 'release' - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: twine upload dist/* + uses: actions/upload-artifact@v4 + with: + name: python-packages + path: dist/ + + publish: + if: github.event_name == 'release' + needs: [build] + + environment: + name: pypi + url: https://pypi.org/p/hcloud + permissions: + id-token: write + + runs-on: ubuntu-latest + steps: + - name: Download packages artifact + uses: actions/download-artifact@v4 + with: + name: python-packages + path: dist/ + + - name: Publish packages to PyPI + uses: pypa/gh-action-pypi-publish@v1.8.11 From 2b0da051d268ca212923ada0855a03fc7c843cf8 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Tue, 2 Jan 2024 18:14:13 +0100 Subject: [PATCH 157/406] chore(main): release 1.33.2 (#351) :robot: I have created a release *beep* *boop* --- ## [1.33.2](https://github.com/hetznercloud/hcloud-python/compare/v1.33.1...v1.33.2) (2024-01-02) ### Bug Fixes * publish package to PyPI using OIDC auth ([1a0e93b](https://github.com/hetznercloud/hcloud-python/commit/1a0e93bbf1ae6cc747e6c4d8305dafd3e49dbbdc)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- .github/release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++++ hcloud/__version__.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/release-please-manifest.json b/.github/release-please-manifest.json index 2692f187..afa825b7 100644 --- a/.github/release-please-manifest.json +++ b/.github/release-please-manifest.json @@ -1 +1 @@ -{".":"1.33.1"} +{".":"1.33.2"} diff --git a/CHANGELOG.md b/CHANGELOG.md index d74e6240..9e717a3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.33.2](https://github.com/hetznercloud/hcloud-python/compare/v1.33.1...v1.33.2) (2024-01-02) + + +### Bug Fixes + +* publish package to PyPI using OIDC auth ([1a0e93b](https://github.com/hetznercloud/hcloud-python/commit/1a0e93bbf1ae6cc747e6c4d8305dafd3e49dbbdc)) + ## [1.33.1](https://github.com/hetznercloud/hcloud-python/compare/v1.33.0...v1.33.1) (2024-01-02) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index 29ba7586..e03c1b43 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1,3 +1,3 @@ from __future__ import annotations -VERSION = "1.33.1" # x-release-please-version +VERSION = "1.33.2" # x-release-please-version From 954295271e73c5a638e1ce3a548775a0b80cb906 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Thu, 4 Jan 2024 13:36:39 +0100 Subject: [PATCH 158/406] ci: upload test coverage to Codecov (#352) --- .github/workflows/test.yml | 5 ++++- setup.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a286cac1..61c31f4b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,4 +26,7 @@ jobs: run: pip install tox tox-gh-actions - name: Run tox - run: tox + run: tox -- --cov --cov-report=xml + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 diff --git a/setup.py b/setup.py index c73f3a9e..600d401b 100644 --- a/setup.py +++ b/setup.py @@ -54,6 +54,7 @@ "coverage>=7.4,<7.5", "pylint>=3,<3.1", "pytest>=7.4,<7.5", + "pytest-cov>=4.1.0,<4.2", "mypy>=1.8,<1.9", "types-python-dateutil", "types-requests", From a1c5c82cd207a5c2b9a844a1aea1c091d4a25641 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Thu, 4 Jan 2024 13:40:00 +0100 Subject: [PATCH 159/406] chore: add coverage badge (#353) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 95a217fd..35d0e945 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![](https://github.com/hetznercloud/hcloud-python/actions/workflows/test.yml/badge.svg)](https://github.com/hetznercloud/hcloud-python/actions/workflows/test.yml) [![](https://github.com/hetznercloud/hcloud-python/actions/workflows/lint.yml/badge.svg)](https://github.com/hetznercloud/hcloud-python/actions/workflows/lint.yml) +[![](https://codecov.io/github/hetznercloud/hcloud-python/graph/badge.svg?token=3YGRqB5t1L)](https://codecov.io/github/hetznercloud/hcloud-python/tree/main) [![](https://readthedocs.org/projects/hcloud-python/badge/?version=latest)](https://hcloud-python.readthedocs.io) [![](https://img.shields.io/pypi/pyversions/hcloud.svg)](https://pypi.org/project/hcloud/) From 66a582f3ce728d92045625885d0634fc96fbc6a0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 Jan 2024 10:14:16 +0100 Subject: [PATCH 160/406] deps: update pre-commit hook pycqa/flake8 to v7 (#354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [pycqa/flake8](https://togithub.com/pycqa/flake8) | repository | major | `6.1.0` -> `7.0.0` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://togithub.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
pycqa/flake8 (pycqa/flake8) ### [`v7.0.0`](https://togithub.com/pycqa/flake8/compare/6.1.0...7.0.0) [Compare Source](https://togithub.com/pycqa/flake8/compare/6.1.0...7.0.0)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0df72161..259a8131 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,6 +46,6 @@ repos: - id: black - repo: https://github.com/pycqa/flake8 - rev: 6.1.0 + rev: 7.0.0 hooks: - id: flake8 From f8f756fe0a492e284bd2a700514c0ba38358b4a8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 19:46:21 +0100 Subject: [PATCH 161/406] deps: update dependency pytest to v8 (#357) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [pytest](https://docs.pytest.org/en/latest/) ([source](https://togithub.com/pytest-dev/pytest), [changelog](https://docs.pytest.org/en/stable/changelog.html)) | `>=7.4,<7.5` -> `>=8,<8.1` | [![age](https://developer.mend.io/api/mc/badges/age/pypi/pytest/8.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/pytest/8.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/pytest/7.4.4/8.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/pytest/7.4.4/8.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
pytest-dev/pytest (pytest) ### [`v8.0.0`](https://togithub.com/pytest-dev/pytest/compare/7.4.4...8.0.0) [Compare Source](https://togithub.com/pytest-dev/pytest/compare/7.4.4...8.0.0)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 600d401b..1de3de5c 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ "test": [ "coverage>=7.4,<7.5", "pylint>=3,<3.1", - "pytest>=7.4,<7.5", + "pytest>=8,<8.1", "pytest-cov>=4.1.0,<4.2", "mypy>=1.8,<1.9", "types-python-dateutil", From b46397d761caa60014bd32f7142b79bef9a92e18 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 19:46:36 +0100 Subject: [PATCH 162/406] deps: update pre-commit hook psf/black-pre-commit-mirror to v24 (#356) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [psf/black-pre-commit-mirror](https://togithub.com/psf/black-pre-commit-mirror) | repository | major | `23.12.1` -> `24.1.0` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://togithub.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
psf/black-pre-commit-mirror (psf/black-pre-commit-mirror) ### [`v24.1.0`](https://togithub.com/psf/black-pre-commit-mirror/compare/23.12.1...24.1.0) [Compare Source](https://togithub.com/psf/black-pre-commit-mirror/compare/23.12.1...24.1.0)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: jo --- .pre-commit-config.yaml | 2 +- hcloud/load_balancers/domain.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 259a8131..746acc81 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: - id: isort - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.12.1 + rev: 24.1.0 hooks: - id: black diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index 2d480eec..ffc430ac 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -200,9 +200,9 @@ def to_payload(self) -> dict[str, Any]: 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 + 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 From 7e4645e3e38a106f38a7f63810d71a628fead939 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 19:54:38 +0100 Subject: [PATCH 163/406] deps: update pre-commit hook psf/black-pre-commit-mirror to v24.1.1 (#358) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [psf/black-pre-commit-mirror](https://togithub.com/psf/black-pre-commit-mirror) | repository | patch | `24.1.0` -> `24.1.1` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://togithub.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
psf/black-pre-commit-mirror (psf/black-pre-commit-mirror) ### [`v24.1.1`](https://togithub.com/psf/black-pre-commit-mirror/compare/24.1.0...24.1.1) [Compare Source](https://togithub.com/psf/black-pre-commit-mirror/compare/24.1.0...24.1.1)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 746acc81..9b076ef3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: - id: isort - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.1.0 + rev: 24.1.1 hooks: - id: black From a79897977abe970181d19584e51448ff5976b5e2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 Feb 2024 15:55:29 +0100 Subject: [PATCH 164/406] deps: update codecov/codecov-action action to v4 (#359) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [codecov/codecov-action](https://togithub.com/codecov/codecov-action) | action | major | `v3` -> `v4` | --- ### Release Notes
codecov/codecov-action (codecov/codecov-action) ### [`v4`](https://togithub.com/codecov/codecov-action/compare/v3...v4) [Compare Source](https://togithub.com/codecov/codecov-action/compare/v3...v4)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: jo --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 61c31f4b..f8503445 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,4 +29,6 @@ jobs: run: tox -- --cov --cov-report=xml - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} From cb8d38396a8665506e3be64a09450343d7671586 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 7 Feb 2024 10:14:20 +0100 Subject: [PATCH 165/406] deps: update dependency watchdog to v4 (#360) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [watchdog](https://togithub.com/gorakhargosh/watchdog) ([changelog](https://togithub.com/gorakhargosh/watchdog/blob/master/changelog.rst)) | `>=3.0.0,<3.1` -> `>=4,<4.1` | [![age](https://developer.mend.io/api/mc/badges/age/pypi/watchdog/4.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/watchdog/4.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/watchdog/3.0.0/4.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/watchdog/3.0.0/4.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
gorakhargosh/watchdog (watchdog) ### [`v4.0.0`](https://togithub.com/gorakhargosh/watchdog/releases/tag/v4.0.0): 4.0.0 [Compare Source](https://togithub.com/gorakhargosh/watchdog/compare/v3.0.0...v4.0.0) - Drop support for Python 3.7. - Add support for Python 3.12. - \[snapshot] Add typing to `dirsnapshot` ([#​1012](https://togithub.com/gorakhargosh/watchdog/issues/1012)) - \[snapshot] Added `DirectorySnapshotDiff.ContextManager` ([#​1011](https://togithub.com/gorakhargosh/watchdog/issues/1011)) - \[events] `FileSystemEvent`, and subclasses, are now `dataclass`es, and their `repr()` has changed - \[windows] `WinAPINativeEvent` is now a `dataclass`, and its `repr()` has changed - \[events] Log `FileOpenedEvent`, and `FileClosedEvent`, events in `LoggingEventHandler` - \[tests] Improve `FileSystemEvent` coverage - \[watchmedo] Log all events in `LoggerTrick` - \[windows] The `observers.read_directory_changes.WATCHDOG_TRAVERSE_MOVED_DIR_DELAY` hack was removed. The constant will be kept to prevent breaking other softwares. - Thanks to our beloved contributors: [@​BoboTiG](https://togithub.com/BoboTiG), [@​msabramo](https://togithub.com/msabramo)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1de3de5c..14180cda 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ "sphinx>=7.2.2,<7.3", "sphinx-rtd-theme>=2,<2.1", "myst-parser>=2.0.0,<2.1", - "watchdog>=3.0.0,<3.1", + "watchdog>=4,<4.1", ], "test": [ "coverage>=7.4,<7.5", From 5b56ace93b8b4fddddbf5610c11fd20bf6f9a561 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 14:32:24 +0100 Subject: [PATCH 166/406] deps: update pre-commit hook psf/black-pre-commit-mirror to v24.2.0 (#361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [psf/black-pre-commit-mirror](https://togithub.com/psf/black-pre-commit-mirror) | repository | minor | `24.1.1` -> `24.2.0` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://togithub.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
psf/black-pre-commit-mirror (psf/black-pre-commit-mirror) ### [`v24.2.0`](https://togithub.com/psf/black-pre-commit-mirror/compare/24.1.1...24.2.0) [Compare Source](https://togithub.com/psf/black-pre-commit-mirror/compare/24.1.1...24.2.0)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9b076ef3..b607bbcd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: - id: isort - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.1.1 + rev: 24.2.0 hooks: - id: black From dd2a521eccec8e15b6d1d7fd843d866bf6ea5bcf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 14:32:57 +0100 Subject: [PATCH 167/406] deps: update pre-commit hook asottile/pyupgrade to v3.15.1 (#362) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [asottile/pyupgrade](https://togithub.com/asottile/pyupgrade) | repository | patch | `v3.15.0` -> `v3.15.1` | Note: The `pre-commit` manager in Renovate is not supported by the `pre-commit` maintainers or community. Please do not report any problems there, instead [create a Discussion in the Renovate repository](https://togithub.com/renovatebot/renovate/discussions/new) if you have any questions. --- ### Release Notes
asottile/pyupgrade (asottile/pyupgrade) ### [`v3.15.1`](https://togithub.com/asottile/pyupgrade/compare/v3.15.0...v3.15.1) [Compare Source](https://togithub.com/asottile/pyupgrade/compare/v3.15.0...v3.15.1)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b607bbcd..e7b313fa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: exclude: ^CHANGELOG.md$ - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.15.1 hooks: - id: pyupgrade args: [--py38-plus] From 55db2551dd0f0ea6a29da4e7a6dce2af8de86eaf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Feb 2024 22:18:03 +0100 Subject: [PATCH 168/406] deps: update pypa/gh-action-pypi-publish action to v1.8.12 (#365) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [pypa/gh-action-pypi-publish](https://togithub.com/pypa/gh-action-pypi-publish) | action | patch | `v1.8.11` -> `v1.8.12` | --- ### Release Notes
pypa/gh-action-pypi-publish (pypa/gh-action-pypi-publish) ### [`v1.8.12`](https://togithub.com/pypa/gh-action-pypi-publish/compare/v1.8.11...v1.8.12) [Compare Source](https://togithub.com/pypa/gh-action-pypi-publish/compare/v1.8.11...v1.8.12)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 36cdd8da..ebb61c42 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,4 +53,4 @@ jobs: path: dist/ - name: Publish packages to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.11 + uses: pypa/gh-action-pypi-publish@v1.8.12 From d71d17fd6f2968a8c19052753265ef7f514a8955 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Feb 2024 22:18:34 +0100 Subject: [PATCH 169/406] deps: update dependency pylint to >=3,<3.2 (#364) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [pylint](https://togithub.com/pylint-dev/pylint) ([changelog](https://pylint.readthedocs.io/en/latest/whatsnew/3/)) | `>=3,<3.1` -> `>=3,<3.2` | [![age](https://developer.mend.io/api/mc/badges/age/pypi/pylint/3.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/pylint/3.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/pylint/3.0.4/3.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/pylint/3.0.4/3.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
pylint-dev/pylint (pylint) ### [`v3.1.0`](https://togithub.com/pylint-dev/pylint/releases/tag/v3.1.0) [Compare Source](https://togithub.com/pylint-dev/pylint/compare/v3.0.4...v3.1.0) Two new checks--`use-yield-from`, `deprecated-attribute`-- and a smattering of bug fixes. ## New Features - Skip `consider-using-join` check for non-empty separators if an `suggest-join-with-non-empty-separator` option is set to `no`. Closes [#​8701](https://togithub.com/pylint-dev/pylint/issues/8701) - Discover `.pyi` files when linting. These can be ignored with the `ignore-patterns` setting. Closes [#​9097](https://togithub.com/pylint-dev/pylint/issues/9097) - Check `TypeAlias` and `TypeVar` (PEP 695) nodes for `invalid-name`. Refs [#​9196](https://togithub.com/pylint-dev/pylint/issues/9196) - Support for resolving external toml files named pylintrc.toml and .pylintrc.toml. Closes [#​9228](https://togithub.com/pylint-dev/pylint/issues/9228) - Check for `.clear`, `.discard`, `.pop` and `remove` methods being called on a set while it is being iterated over. Closes [#​9334](https://togithub.com/pylint-dev/pylint/issues/9334) ## New Checks - New message `use-yield-from` added to the refactoring checker. This message is emitted when yielding from a loop can be replaced by `yield from`. Closes [#​9229](https://togithub.com/pylint-dev/pylint/issues/9229). - Added a `deprecated-attribute` message to check deprecated attributes in the stdlib. Closes [#​8855](https://togithub.com/pylint-dev/pylint/issues/8855) ## False Positives Fixed - Fixed false positive for `inherit-non-class` for generic Protocols. Closes [#​9106](https://togithub.com/pylint-dev/pylint/issues/9106) - Exempt `TypedDict` from `typing_extensions` from `too-many-ancestor` checks. Refs [#​9167](https://togithub.com/pylint-dev/pylint/issues/9167) ## False Negatives Fixed - Extend broad-exception-raised and broad-exception-caught to except\*. Closes [#​8827](https://togithub.com/pylint-dev/pylint/issues/8827) - Fix a false-negative for unnecessary if blocks using a different than expected ordering of arguments. Closes [#​8947](https://togithub.com/pylint-dev/pylint/issues/8947). ## Other Bug Fixes - Improve the message provided for wrong-import-order check. Instead of the import statement ("import x"), the message now specifies the import that is out of order and which imports should come after it. As reported in the issue, this is particularly helpful if there are multiple imports on a single line that do not follow the PEP8 convention. The message will report imports as follows: For "import X", it will report "(standard/third party/first party/local) import X" For "import X.Y" and "from X import Y", it will report "(standard/third party/first party/local) import X.Y" The import category is specified to provide explanation as to why pylint has issued the message and guidence to the developer on how to fix the problem. Closes [#​8808](https://togithub.com/pylint-dev/pylint/issues/8808) ## Other Changes - Print how many files were checked in verbose mode. Closes [#​8935](https://togithub.com/pylint-dev/pylint/issues/8935) - Fix a crash when an enum class which is also decorated with a `dataclasses.dataclass` decorator is defined. Closes [#​9100](https://togithub.com/pylint-dev/pylint/issues/9100) ## Internal Changes - Update astroid version to 3.1.0. Refs [#​9457](https://togithub.com/pylint-dev/pylint/issues/9457)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 14180cda..97843a72 100644 --- a/setup.py +++ b/setup.py @@ -52,7 +52,7 @@ ], "test": [ "coverage>=7.4,<7.5", - "pylint>=3,<3.1", + "pylint>=3,<3.2", "pytest>=8,<8.1", "pytest-cov>=4.1.0,<4.2", "mypy>=1.8,<1.9", From 8665dcff335c755c1ff4d95621334a3f5e196d34 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 09:54:02 +0100 Subject: [PATCH 170/406] deps: update dependency pytest to >=8,<8.2 (#366) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [pytest](https://docs.pytest.org/en/latest/) ([source](https://togithub.com/pytest-dev/pytest), [changelog](https://docs.pytest.org/en/stable/changelog.html)) | `>=8,<8.1` -> `>=8,<8.2` | [![age](https://developer.mend.io/api/mc/badges/age/pypi/pytest/8.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/pytest/8.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/pytest/8.0.2/8.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/pytest/8.0.2/8.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
pytest-dev/pytest (pytest) ### [`v8.1.0`](https://togithub.com/pytest-dev/pytest/releases/tag/8.1.0) [Compare Source](https://togithub.com/pytest-dev/pytest/compare/8.0.2...8.1.0) # pytest 8.1.0 (2024-03-03) ## Features - [#​11475](https://togithub.com/pytest-dev/pytest/issues/11475): Added the new `consider_namespace_packages`{.interpreted-text role="confval"} configuration option, defaulting to `False`. If set to `True`, pytest will attempt to identify modules that are part of [namespace packages](https://packaging.python.org/en/latest/guides/packaging-namespace-packages) when importing modules. - [#​11653](https://togithub.com/pytest-dev/pytest/issues/11653): Added the new `verbosity_test_cases`{.interpreted-text role="confval"} configuration option for fine-grained control of test execution verbosity. See `Fine-grained verbosity `{.interpreted-text role="ref"} for more details. ## Improvements - [#​10865](https://togithub.com/pytest-dev/pytest/issues/10865): `pytest.warns`{.interpreted-text role="func"} now validates that `warnings.warn`{.interpreted-text role="func"} was called with a \[str]{.title-ref} or a \[Warning]{.title-ref}. Currently in Python it is possible to use other types, however this causes an exception when `warnings.filterwarnings`{.interpreted-text role="func"} is used to filter those warnings (see [CPython #​103577](https://togithub.com/python/cpython/issues/103577) for a discussion). While this can be considered a bug in CPython, we decided to put guards in pytest as the error message produced without this check in place is confusing. - [#​11311](https://togithub.com/pytest-dev/pytest/issues/11311): When using `--override-ini` for paths in invocations without a configuration file defined, the current working directory is used as the relative directory. Previoulsy this would raise an `AssertionError`{.interpreted-text role="class"}. - [#​11475](https://togithub.com/pytest-dev/pytest/issues/11475): `--import-mode=importlib `{.interpreted-text role="ref"} now tries to import modules using the standard import mechanism (but still without changing :py`sys.path`{.interpreted-text role="data"}), falling back to importing modules directly only if that fails. This means that installed packages will be imported under their canonical name if possible first, for example `app.core.models`, instead of having the module name always be derived from their path (for example `.env310.lib.site_packages.app.core.models`). - [#​11801](https://togithub.com/pytest-dev/pytest/issues/11801): Added the `iter_parents() <_pytest.nodes.Node.iter_parents>`{.interpreted-text role="func"} helper method on nodes. It is similar to `listchain <_pytest.nodes.Node.listchain>`{.interpreted-text role="func"}, but goes from bottom to top, and returns an iterator, not a list. - [#​11850](https://togithub.com/pytest-dev/pytest/issues/11850): Added support for `sys.last_exc`{.interpreted-text role="data"} for post-mortem debugging on Python>=3.12. - [#​11962](https://togithub.com/pytest-dev/pytest/issues/11962): In case no other suitable candidates for configuration file are found, a `pyproject.toml` (even without a `[tool.pytest.ini_options]` table) will be considered as the configuration file and define the `rootdir`. - [#​11978](https://togithub.com/pytest-dev/pytest/issues/11978): Add `--log-file-mode` option to the logging plugin, enabling appending to log-files. This option accepts either `"w"` or `"a"` and defaults to `"w"`. Previously, the mode was hard-coded to be `"w"` which truncates the file before logging. - [#​12047](https://togithub.com/pytest-dev/pytest/issues/12047): When multiple finalizers of a fixture raise an exception, now all exceptions are reported as an exception group. Previously, only the first exception was reported. ## Bug Fixes - [#​11904](https://togithub.com/pytest-dev/pytest/issues/11904): Fixed a regression in pytest 8.0.0 that would cause test collection to fail due to permission errors when using `--pyargs`. This change improves the collection tree for tests specified using `--pyargs`, see `12043`{.interpreted-text role="pull"} for a comparison with pytest 8.0 and <8. - [#​12011](https://togithub.com/pytest-dev/pytest/issues/12011): Fixed a regression in 8.0.1 whereby `setup_module` xunit-style fixtures are not executed when `--doctest-modules` is passed. - [#​12014](https://togithub.com/pytest-dev/pytest/issues/12014): Fix the `stacklevel` used when warning about marks used on fixtures. - [#​12039](https://togithub.com/pytest-dev/pytest/issues/12039): Fixed a regression in `8.0.2` where tests created using `tmp_path`{.interpreted-text role="fixture"} have been collected multiple times in CI under Windows. ## Improved Documentation - [#​11790](https://togithub.com/pytest-dev/pytest/issues/11790): Documented the retention of temporary directories created using the `tmp_path` fixture in more detail. ## Trivial/Internal Changes - [#​11785](https://togithub.com/pytest-dev/pytest/issues/11785): Some changes were made to private functions which may affect plugins which access them: - `FixtureManager._getautousenames()` now takes a `Node` itself instead of the nodeid. - `FixtureManager.getfixturedefs()` now takes the `Node` itself instead of the nodeid. - The `_pytest.nodes.iterparentnodeids()` function is removed without replacement. Prefer to traverse the node hierarchy itself instead. If you really need to, copy the function from the previous pytest release.
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 97843a72..d462aaad 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ "test": [ "coverage>=7.4,<7.5", "pylint>=3,<3.2", - "pytest>=8,<8.1", + "pytest>=8,<8.2", "pytest-cov>=4.1.0,<4.2", "mypy>=1.8,<1.9", "types-python-dateutil", From 4b9328ceae1e393ff55b3ca6f030cb5ac565be00 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 11:07:32 +0100 Subject: [PATCH 171/406] deps: update dependency mypy to >=1.9,<1.10 (#368) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [mypy](https://www.mypy-lang.org/) ([source](https://togithub.com/python/mypy), [changelog](https://mypy-lang.blogspot.com/)) | `>=1.8,<1.9` -> `>=1.9,<1.10` | [![age](https://developer.mend.io/api/mc/badges/age/pypi/mypy/1.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/mypy/1.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/mypy/1.8.0/1.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/mypy/1.8.0/1.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
python/mypy (mypy) ### [`v1.9.0`](https://togithub.com/python/mypy/compare/v1.8.0...1.9.0) [Compare Source](https://togithub.com/python/mypy/compare/v1.8.0...1.9.0)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d462aaad..c720db2f 100644 --- a/setup.py +++ b/setup.py @@ -55,7 +55,7 @@ "pylint>=3,<3.2", "pytest>=8,<8.2", "pytest-cov>=4.1.0,<4.2", - "mypy>=1.8,<1.9", + "mypy>=1.9,<1.10", "types-python-dateutil", "types-requests", ], From 0cb615fe0d852cddbf636c1fdb8538ad60f5a3d9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 11:07:42 +0100 Subject: [PATCH 172/406] deps: update pypa/gh-action-pypi-publish action to v1.8.14 (#367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [pypa/gh-action-pypi-publish](https://togithub.com/pypa/gh-action-pypi-publish) | action | patch | `v1.8.12` -> `v1.8.14` | --- ### Release Notes
pypa/gh-action-pypi-publish (pypa/gh-action-pypi-publish) ### [`v1.8.14`](https://togithub.com/pypa/gh-action-pypi-publish/compare/v1.8.13...v1.8.14) [Compare Source](https://togithub.com/pypa/gh-action-pypi-publish/compare/v1.8.13...v1.8.14) ### [`v1.8.13`](https://togithub.com/pypa/gh-action-pypi-publish/compare/v1.8.12...v1.8.13) [Compare Source](https://togithub.com/pypa/gh-action-pypi-publish/compare/v1.8.12...v1.8.13)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ebb61c42..aaa2976e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,4 +53,4 @@ jobs: path: dist/ - name: Publish packages to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.12 + uses: pypa/gh-action-pypi-publish@v1.8.14 From 3bbac5dc41ca509d6679fd6b06ae99ca33fd62ee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 16 Mar 2024 07:57:11 +0000 Subject: [PATCH 173/406] deps: update pre-commit hook psf/black-pre-commit-mirror to v24.3.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e7b313fa..2afc0b24 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: - id: isort - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.2.0 + rev: 24.3.0 hooks: - id: black From 3d02ad71e9200f5cc94b2d33eea62035edc1e33a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 24 Mar 2024 18:37:10 +0000 Subject: [PATCH 174/406] deps: update pre-commit hook asottile/pyupgrade to v3.15.2 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2afc0b24..e49f0627 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: exclude: ^CHANGELOG.md$ - repo: https://github.com/asottile/pyupgrade - rev: v3.15.1 + rev: v3.15.2 hooks: - id: pyupgrade args: [--py38-plus] From 04a6a42028606ed66657605d98b1f21545eb2e0d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 26 Mar 2024 10:05:31 +0100 Subject: [PATCH 175/406] deps: update dependency pytest-cov to v5 (#371) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [pytest-cov](https://togithub.com/pytest-dev/pytest-cov) ([changelog](https://pytest-cov.readthedocs.io/en/latest/changelog.html)) | `>=4.1.0,<4.2` -> `>=5,<5.1` | [![age](https://developer.mend.io/api/mc/badges/age/pypi/pytest-cov/5.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/pytest-cov/5.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/pytest-cov/4.1.0/5.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/pytest-cov/4.1.0/5.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
pytest-dev/pytest-cov (pytest-cov) ### [`v5.0.0`](https://togithub.com/pytest-dev/pytest-cov/blob/HEAD/CHANGELOG.rst#500-2024-03-24) [Compare Source](https://togithub.com/pytest-dev/pytest-cov/compare/v4.1.0...v5.0.0) - Removed support for xdist rsync (now deprecated). Contributed by Matthias Reichenbach in `#​623 `\_. - Switched docs theme to Furo. - Various legacy Python cleanup and CI improvements. Contributed by Christian Clauss and Hugo van Kemenade in `#​630 `*, `#​631 `*, `#​632 `\_ and `#​633 `\_. - Added a `pyproject.toml` example in the docs. Contributed by Dawn James in `#​626 `\_. - Modernized project's pre-commit hooks to use ruff. Initial POC contributed by Christian Clauss in `#​584 `\_.
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/hetznercloud/hcloud-python). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c720db2f..b1002009 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ "coverage>=7.4,<7.5", "pylint>=3,<3.2", "pytest>=8,<8.2", - "pytest-cov>=4.1.0,<4.2", + "pytest-cov>=5,<5.1", "mypy>=1.9,<1.10", "types-python-dateutil", "types-requests", From 903e92faab745b7f8270f6195da67f4d9f8b1ba7 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Wed, 27 Mar 2024 10:24:57 +0100 Subject: [PATCH 176/406] fix: invalid type for load balancer private network property (#372) https://docs.hetzner.cloud/#load-balancers-get-a-load-balancer --- hcloud/load_balancers/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index ffc430ac..02d14805 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -76,7 +76,7 @@ def __init__( id: int, name: str | None = None, public_net: PublicNetwork | None = None, - private_net: PrivateNet | None = None, + private_net: list[PrivateNet] | None = None, location: BoundLocation | None = None, algorithm: LoadBalancerAlgorithm | None = None, services: list[LoadBalancerService] | None = None, From 6651b60823b9b58d877d13e1c227a3134e9efde9 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Wed, 27 Mar 2024 10:29:40 +0100 Subject: [PATCH 177/406] chore(main): release 1.33.3 (#355) :robot: I have created a release *beep* *boop* --- ## [1.33.3](https://github.com/hetznercloud/hcloud-python/compare/v1.33.2...v1.33.3) (2024-03-27) ### Bug Fixes * invalid type for load balancer private network property ([#372](https://github.com/hetznercloud/hcloud-python/issues/372)) ([903e92f](https://github.com/hetznercloud/hcloud-python/commit/903e92faab745b7f8270f6195da67f4d9f8b1ba7)) ### Dependencies * update codecov/codecov-action action to v4 ([#359](https://github.com/hetznercloud/hcloud-python/issues/359)) ([a798979](https://github.com/hetznercloud/hcloud-python/commit/a79897977abe970181d19584e51448ff5976b5e2)) * update dependency mypy to >=1.9,<1.10 ([#368](https://github.com/hetznercloud/hcloud-python/issues/368)) ([4b9328c](https://github.com/hetznercloud/hcloud-python/commit/4b9328ceae1e393ff55b3ca6f030cb5ac565be00)) * update dependency pylint to >=3,<3.2 ([#364](https://github.com/hetznercloud/hcloud-python/issues/364)) ([d71d17f](https://github.com/hetznercloud/hcloud-python/commit/d71d17fd6f2968a8c19052753265ef7f514a8955)) * update dependency pytest to >=8,<8.2 ([#366](https://github.com/hetznercloud/hcloud-python/issues/366)) ([8665dcf](https://github.com/hetznercloud/hcloud-python/commit/8665dcff335c755c1ff4d95621334a3f5e196d34)) * update dependency pytest to v8 ([#357](https://github.com/hetznercloud/hcloud-python/issues/357)) ([f8f756f](https://github.com/hetznercloud/hcloud-python/commit/f8f756fe0a492e284bd2a700514c0ba38358b4a8)) * update dependency pytest-cov to v5 ([#371](https://github.com/hetznercloud/hcloud-python/issues/371)) ([04a6a42](https://github.com/hetznercloud/hcloud-python/commit/04a6a42028606ed66657605d98b1f21545eb2e0d)) * update dependency watchdog to v4 ([#360](https://github.com/hetznercloud/hcloud-python/issues/360)) ([cb8d383](https://github.com/hetznercloud/hcloud-python/commit/cb8d38396a8665506e3be64a09450343d7671586)) * update pre-commit hook asottile/pyupgrade to v3.15.1 ([#362](https://github.com/hetznercloud/hcloud-python/issues/362)) ([dd2a521](https://github.com/hetznercloud/hcloud-python/commit/dd2a521eccec8e15b6d1d7fd843d866bf6ea5bcf)) * update pre-commit hook asottile/pyupgrade to v3.15.2 ([3d02ad7](https://github.com/hetznercloud/hcloud-python/commit/3d02ad71e9200f5cc94b2d33eea62035edc1e33a)) * update pre-commit hook psf/black-pre-commit-mirror to v24 ([#356](https://github.com/hetznercloud/hcloud-python/issues/356)) ([b46397d](https://github.com/hetznercloud/hcloud-python/commit/b46397d761caa60014bd32f7142b79bef9a92e18)) * update pre-commit hook psf/black-pre-commit-mirror to v24.1.1 ([#358](https://github.com/hetznercloud/hcloud-python/issues/358)) ([7e4645e](https://github.com/hetznercloud/hcloud-python/commit/7e4645e3e38a106f38a7f63810d71a628fead939)) * update pre-commit hook psf/black-pre-commit-mirror to v24.2.0 ([#361](https://github.com/hetznercloud/hcloud-python/issues/361)) ([5b56ace](https://github.com/hetznercloud/hcloud-python/commit/5b56ace93b8b4fddddbf5610c11fd20bf6f9a561)) * update pre-commit hook psf/black-pre-commit-mirror to v24.3.0 ([3bbac5d](https://github.com/hetznercloud/hcloud-python/commit/3bbac5dc41ca509d6679fd6b06ae99ca33fd62ee)) * update pre-commit hook pycqa/flake8 to v7 ([#354](https://github.com/hetznercloud/hcloud-python/issues/354)) ([66a582f](https://github.com/hetznercloud/hcloud-python/commit/66a582f3ce728d92045625885d0634fc96fbc6a0)) * update pypa/gh-action-pypi-publish action to v1.8.12 ([#365](https://github.com/hetznercloud/hcloud-python/issues/365)) ([55db255](https://github.com/hetznercloud/hcloud-python/commit/55db2551dd0f0ea6a29da4e7a6dce2af8de86eaf)) * update pypa/gh-action-pypi-publish action to v1.8.14 ([#367](https://github.com/hetznercloud/hcloud-python/issues/367)) ([0cb615f](https://github.com/hetznercloud/hcloud-python/commit/0cb615fe0d852cddbf636c1fdb8538ad60f5a3d9)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- .github/release-please-manifest.json | 2 +- CHANGELOG.md | 27 +++++++++++++++++++++++++++ hcloud/__version__.py | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/release-please-manifest.json b/.github/release-please-manifest.json index afa825b7..3ea8345f 100644 --- a/.github/release-please-manifest.json +++ b/.github/release-please-manifest.json @@ -1 +1 @@ -{".":"1.33.2"} +{".":"1.33.3"} diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e717a3c..44599eeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## [1.33.3](https://github.com/hetznercloud/hcloud-python/compare/v1.33.2...v1.33.3) (2024-03-27) + + +### Bug Fixes + +* invalid type for load balancer private network property ([#372](https://github.com/hetznercloud/hcloud-python/issues/372)) ([903e92f](https://github.com/hetznercloud/hcloud-python/commit/903e92faab745b7f8270f6195da67f4d9f8b1ba7)) + + +### Dependencies + +* update codecov/codecov-action action to v4 ([#359](https://github.com/hetznercloud/hcloud-python/issues/359)) ([a798979](https://github.com/hetznercloud/hcloud-python/commit/a79897977abe970181d19584e51448ff5976b5e2)) +* update dependency mypy to >=1.9,<1.10 ([#368](https://github.com/hetznercloud/hcloud-python/issues/368)) ([4b9328c](https://github.com/hetznercloud/hcloud-python/commit/4b9328ceae1e393ff55b3ca6f030cb5ac565be00)) +* update dependency pylint to >=3,<3.2 ([#364](https://github.com/hetznercloud/hcloud-python/issues/364)) ([d71d17f](https://github.com/hetznercloud/hcloud-python/commit/d71d17fd6f2968a8c19052753265ef7f514a8955)) +* update dependency pytest to >=8,<8.2 ([#366](https://github.com/hetznercloud/hcloud-python/issues/366)) ([8665dcf](https://github.com/hetznercloud/hcloud-python/commit/8665dcff335c755c1ff4d95621334a3f5e196d34)) +* update dependency pytest to v8 ([#357](https://github.com/hetznercloud/hcloud-python/issues/357)) ([f8f756f](https://github.com/hetznercloud/hcloud-python/commit/f8f756fe0a492e284bd2a700514c0ba38358b4a8)) +* update dependency pytest-cov to v5 ([#371](https://github.com/hetznercloud/hcloud-python/issues/371)) ([04a6a42](https://github.com/hetznercloud/hcloud-python/commit/04a6a42028606ed66657605d98b1f21545eb2e0d)) +* update dependency watchdog to v4 ([#360](https://github.com/hetznercloud/hcloud-python/issues/360)) ([cb8d383](https://github.com/hetznercloud/hcloud-python/commit/cb8d38396a8665506e3be64a09450343d7671586)) +* update pre-commit hook asottile/pyupgrade to v3.15.1 ([#362](https://github.com/hetznercloud/hcloud-python/issues/362)) ([dd2a521](https://github.com/hetznercloud/hcloud-python/commit/dd2a521eccec8e15b6d1d7fd843d866bf6ea5bcf)) +* update pre-commit hook asottile/pyupgrade to v3.15.2 ([3d02ad7](https://github.com/hetznercloud/hcloud-python/commit/3d02ad71e9200f5cc94b2d33eea62035edc1e33a)) +* update pre-commit hook psf/black-pre-commit-mirror to v24 ([#356](https://github.com/hetznercloud/hcloud-python/issues/356)) ([b46397d](https://github.com/hetznercloud/hcloud-python/commit/b46397d761caa60014bd32f7142b79bef9a92e18)) +* update pre-commit hook psf/black-pre-commit-mirror to v24.1.1 ([#358](https://github.com/hetznercloud/hcloud-python/issues/358)) ([7e4645e](https://github.com/hetznercloud/hcloud-python/commit/7e4645e3e38a106f38a7f63810d71a628fead939)) +* update pre-commit hook psf/black-pre-commit-mirror to v24.2.0 ([#361](https://github.com/hetznercloud/hcloud-python/issues/361)) ([5b56ace](https://github.com/hetznercloud/hcloud-python/commit/5b56ace93b8b4fddddbf5610c11fd20bf6f9a561)) +* update pre-commit hook psf/black-pre-commit-mirror to v24.3.0 ([3bbac5d](https://github.com/hetznercloud/hcloud-python/commit/3bbac5dc41ca509d6679fd6b06ae99ca33fd62ee)) +* update pre-commit hook pycqa/flake8 to v7 ([#354](https://github.com/hetznercloud/hcloud-python/issues/354)) ([66a582f](https://github.com/hetznercloud/hcloud-python/commit/66a582f3ce728d92045625885d0634fc96fbc6a0)) +* update pypa/gh-action-pypi-publish action to v1.8.12 ([#365](https://github.com/hetznercloud/hcloud-python/issues/365)) ([55db255](https://github.com/hetznercloud/hcloud-python/commit/55db2551dd0f0ea6a29da4e7a6dce2af8de86eaf)) +* update pypa/gh-action-pypi-publish action to v1.8.14 ([#367](https://github.com/hetznercloud/hcloud-python/issues/367)) ([0cb615f](https://github.com/hetznercloud/hcloud-python/commit/0cb615fe0d852cddbf636c1fdb8538ad60f5a3d9)) + ## [1.33.2](https://github.com/hetznercloud/hcloud-python/compare/v1.33.1...v1.33.2) (2024-01-02) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index e03c1b43..b4bf7700 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1,3 +1,3 @@ from __future__ import annotations -VERSION = "1.33.2" # x-release-please-version +VERSION = "1.33.3" # x-release-please-version From 8facaf6d4dd2bbfb4137e7066b49c5f4c1db773c Mon Sep 17 00:00:00 2001 From: Jonas L Date: Wed, 27 Mar 2024 15:50:37 +0100 Subject: [PATCH 178/406] feat: add `has_id_or_name` to `DomainIdentityMixin` (#373) - Add a method to check if 2 domains are equal (only check for same ID or same name) - Adds the `DomainIdentityMixin` to models that were missing it. --- hcloud/core/domain.py | 18 ++++++++++++++++++ hcloud/firewalls/domain.py | 4 ++-- hcloud/floating_ips/domain.py | 4 ++-- hcloud/load_balancers/domain.py | 4 ++-- hcloud/networks/domain.py | 4 ++-- hcloud/placement_groups/domain.py | 4 ++-- hcloud/primary_ips/domain.py | 4 ++-- hcloud/servers/domain.py | 4 ++-- tests/unit/core/test_domain.py | 17 ++++++++++++++++- 9 files changed, 48 insertions(+), 15 deletions(-) diff --git a/hcloud/core/domain.py b/hcloud/core/domain.py index 692f7488..bba954fe 100644 --- a/hcloud/core/domain.py +++ b/hcloud/core/domain.py @@ -34,6 +34,24 @@ def id_or_name(self) -> int | str: return self.name raise ValueError("id or name must be set") + def has_id_or_name(self, id_or_name: int | str) -> bool: + """ + Return whether this domain has the same id or same name as the other. + + The domain calling this method MUST be a bound domain or be populated, otherwise + the comparison will not work as expected (e.g. the domains are the same but + cannot be equal, if one provides an id and the other the name). + """ + values: list[int | str] = [] + if self.id is not None: + values.append(self.id) + if self.name is not None: + values.append(self.name) + if not values: + raise ValueError("id or name must be set") + + return id_or_name in values + class Pagination(BaseDomain): __slots__ = ( diff --git a/hcloud/firewalls/domain.py b/hcloud/firewalls/domain.py index 22e8c517..41959ce1 100644 --- a/hcloud/firewalls/domain.py +++ b/hcloud/firewalls/domain.py @@ -4,7 +4,7 @@ from dateutil.parser import isoparse -from ..core import BaseDomain +from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction @@ -12,7 +12,7 @@ from .client import BoundFirewall -class Firewall(BaseDomain): +class Firewall(BaseDomain, DomainIdentityMixin): """Firewall Domain :param id: int diff --git a/hcloud/floating_ips/domain.py b/hcloud/floating_ips/domain.py index 78d8e5af..78b701fe 100644 --- a/hcloud/floating_ips/domain.py +++ b/hcloud/floating_ips/domain.py @@ -4,7 +4,7 @@ from dateutil.parser import isoparse -from ..core import BaseDomain +from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction @@ -13,7 +13,7 @@ from .client import BoundFloatingIP -class FloatingIP(BaseDomain): +class FloatingIP(BaseDomain, DomainIdentityMixin): """Floating IP Domain :param id: int diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index 02d14805..fdf211a2 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -4,7 +4,7 @@ from dateutil.parser import isoparse -from ..core import BaseDomain +from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction @@ -17,7 +17,7 @@ from .client import BoundLoadBalancer -class LoadBalancer(BaseDomain): +class LoadBalancer(BaseDomain, DomainIdentityMixin): """LoadBalancer Domain :param id: int diff --git a/hcloud/networks/domain.py b/hcloud/networks/domain.py index 725cedad..2135d5fc 100644 --- a/hcloud/networks/domain.py +++ b/hcloud/networks/domain.py @@ -4,7 +4,7 @@ from dateutil.parser import isoparse -from ..core import BaseDomain +from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction @@ -12,7 +12,7 @@ from .client import BoundNetwork -class Network(BaseDomain): +class Network(BaseDomain, DomainIdentityMixin): """Network Domain :param id: int diff --git a/hcloud/placement_groups/domain.py b/hcloud/placement_groups/domain.py index b2dc27ca..7b7fcb34 100644 --- a/hcloud/placement_groups/domain.py +++ b/hcloud/placement_groups/domain.py @@ -4,14 +4,14 @@ from dateutil.parser import isoparse -from ..core import BaseDomain +from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction from .client import BoundPlacementGroup -class PlacementGroup(BaseDomain): +class PlacementGroup(BaseDomain, DomainIdentityMixin): """Placement Group Domain :param id: int diff --git a/hcloud/primary_ips/domain.py b/hcloud/primary_ips/domain.py index a499799f..bd470602 100644 --- a/hcloud/primary_ips/domain.py +++ b/hcloud/primary_ips/domain.py @@ -4,7 +4,7 @@ from dateutil.parser import isoparse -from ..core import BaseDomain +from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction @@ -12,7 +12,7 @@ from .client import BoundPrimaryIP -class PrimaryIP(BaseDomain): +class PrimaryIP(BaseDomain, DomainIdentityMixin): """Primary IP Domain :param id: int diff --git a/hcloud/servers/domain.py b/hcloud/servers/domain.py index 29afecf3..e886e1a1 100644 --- a/hcloud/servers/domain.py +++ b/hcloud/servers/domain.py @@ -4,7 +4,7 @@ from dateutil.parser import isoparse -from ..core import BaseDomain +from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction @@ -22,7 +22,7 @@ from .client import BoundServer -class Server(BaseDomain): +class Server(BaseDomain, DomainIdentityMixin): """Server Domain :param id: int diff --git a/tests/unit/core/test_domain.py b/tests/unit/core/test_domain.py index 456b3bb6..fabf7629 100644 --- a/tests/unit/core/test_domain.py +++ b/tests/unit/core/test_domain.py @@ -65,10 +65,25 @@ def test_id_or_name_exception(self): domain = SomeDomain() with pytest.raises(ValueError) as exception_info: - domain.id_or_name + _ = domain.id_or_name error = exception_info.value assert str(error) == "id or name must be set" + @pytest.mark.parametrize( + "other, expected", + [ + (SomeDomain(id=1), True), + (SomeDomain(name="name1"), True), + (SomeDomain(id=1, name="name1"), True), + (SomeDomain(id=2), False), + (SomeDomain(name="name2"), False), + (SomeDomain(id=2, name="name2"), False), + ], + ) + def test_has_id_or_name_exception(self, other, expected): + domain = SomeDomain(id=1, name="name1") + assert domain.has_id_or_name(other.id_or_name) == expected + class ActionDomain(BaseDomain, DomainIdentityMixin): __slots__ = ("id", "name", "started") From 2e9c5fc09e741fad21cb8af548a0500dce063bcc Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Wed, 27 Mar 2024 15:53:34 +0100 Subject: [PATCH 179/406] chore(main): release 1.34.0 (#374) :robot: I have created a release *beep* *boop* --- ## [1.34.0](https://github.com/hetznercloud/hcloud-python/compare/v1.33.3...v1.34.0) (2024-03-27) ### Features * add `has_id_or_name` to `DomainIdentityMixin` ([#373](https://github.com/hetznercloud/hcloud-python/issues/373)) ([8facaf6](https://github.com/hetznercloud/hcloud-python/commit/8facaf6d4dd2bbfb4137e7066b49c5f4c1db773c)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- .github/release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++++ hcloud/__version__.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/release-please-manifest.json b/.github/release-please-manifest.json index 3ea8345f..9edd4dd2 100644 --- a/.github/release-please-manifest.json +++ b/.github/release-please-manifest.json @@ -1 +1 @@ -{".":"1.33.3"} +{".":"1.34.0"} diff --git a/CHANGELOG.md b/CHANGELOG.md index 44599eeb..edecb97b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.34.0](https://github.com/hetznercloud/hcloud-python/compare/v1.33.3...v1.34.0) (2024-03-27) + + +### Features + +* add `has_id_or_name` to `DomainIdentityMixin` ([#373](https://github.com/hetznercloud/hcloud-python/issues/373)) ([8facaf6](https://github.com/hetznercloud/hcloud-python/commit/8facaf6d4dd2bbfb4137e7066b49c5f4c1db773c)) + ## [1.33.3](https://github.com/hetznercloud/hcloud-python/compare/v1.33.2...v1.33.3) (2024-03-27) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index b4bf7700..b592a1be 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1,3 +1,3 @@ from __future__ import annotations -VERSION = "1.33.3" # x-release-please-version +VERSION = "1.34.0" # x-release-please-version From b24de80684db142ebbe11b62a38d9c61f248e216 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Tue, 2 Apr 2024 16:26:49 +0200 Subject: [PATCH 180/406] fix: raise warnings for the `ImagesClient.get_by_name` deprecation (#376) Add missing warning and sphinx deprecation notice on an already deprecated method. --- hcloud/images/client.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/hcloud/images/client.py b/hcloud/images/client.py index 65b7546a..d60d9486 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient @@ -305,12 +306,19 @@ def get_all( def get_by_name(self, name: str) -> BoundImage | None: """Get image by name - Deprecated: Use get_by_name_and_architecture instead. - :param name: str Used to get image by name. :return: :class:`BoundImage ` + + .. deprecated:: 1.19 + Use :func:`hcloud.images.client.ImagesClient.get_by_name_and_architecture` instead. """ + warnings.warn( + "The 'hcloud.images.client.ImagesClient.get_by_name' method is deprecated, please use the " + "'hcloud.images.client.ImagesClient.get_by_name_and_architecture' method instead.", + DeprecationWarning, + stacklevel=2, + ) return self._get_first_by(name=name) def get_by_name_and_architecture( From 6d86f86677fec23e6fd8a69d20d787e234e0fb53 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Tue, 2 Apr 2024 16:30:52 +0200 Subject: [PATCH 181/406] feat: add `include_deprecated` option when fetching images by name (#375) Allow to get a deprecated image by name. --- hcloud/images/client.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/hcloud/images/client.py b/hcloud/images/client.py index d60d9486..a7d41e01 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -325,6 +325,8 @@ def get_by_name_and_architecture( self, name: str, architecture: str, + *, + include_deprecated: bool | None = None, ) -> BoundImage | None: """Get image by name @@ -332,9 +334,15 @@ def get_by_name_and_architecture( Used to identify the image. :param architecture: str Used to identify the image. + :param include_deprecated: bool (optional) + Include deprecated images. Default: False :return: :class:`BoundImage ` """ - return self._get_first_by(name=name, architecture=[architecture]) + return self._get_first_by( + name=name, + architecture=[architecture], + include_deprecated=include_deprecated, + ) def update( self, From 14eeb695459aa140bfcf5072af781656f896a3b7 Mon Sep 17 00:00:00 2001 From: Hetzner Cloud Bot <45457231+hcloud-bot@users.noreply.github.com> Date: Tue, 2 Apr 2024 19:10:19 +0200 Subject: [PATCH 182/406] chore(main): release 1.35.0 (#377) :robot: I have created a release *beep* *boop* --- ## [1.35.0](https://github.com/hetznercloud/hcloud-python/compare/v1.34.0...v1.35.0) (2024-04-02) ### Features * add `include_deprecated` option when fetching images by name ([#375](https://github.com/hetznercloud/hcloud-python/issues/375)) ([6d86f86](https://github.com/hetznercloud/hcloud-python/commit/6d86f86677fec23e6fd8a69d20d787e234e0fb53)) ### Bug Fixes * raise warnings for the `ImagesClient.get_by_name` deprecation ([#376](https://github.com/hetznercloud/hcloud-python/issues/376)) ([b24de80](https://github.com/hetznercloud/hcloud-python/commit/b24de80684db142ebbe11b62a38d9c61f248e216)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- .github/release-please-manifest.json | 2 +- CHANGELOG.md | 12 ++++++++++++ hcloud/__version__.py | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/release-please-manifest.json b/.github/release-please-manifest.json index 9edd4dd2..fde23122 100644 --- a/.github/release-please-manifest.json +++ b/.github/release-please-manifest.json @@ -1 +1 @@ -{".":"1.34.0"} +{".":"1.35.0"} diff --git a/CHANGELOG.md b/CHANGELOG.md index edecb97b..42d0aa8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [1.35.0](https://github.com/hetznercloud/hcloud-python/compare/v1.34.0...v1.35.0) (2024-04-02) + + +### Features + +* add `include_deprecated` option when fetching images by name ([#375](https://github.com/hetznercloud/hcloud-python/issues/375)) ([6d86f86](https://github.com/hetznercloud/hcloud-python/commit/6d86f86677fec23e6fd8a69d20d787e234e0fb53)) + + +### Bug Fixes + +* raise warnings for the `ImagesClient.get_by_name` deprecation ([#376](https://github.com/hetznercloud/hcloud-python/issues/376)) ([b24de80](https://github.com/hetznercloud/hcloud-python/commit/b24de80684db142ebbe11b62a38d9c61f248e216)) + ## [1.34.0](https://github.com/hetznercloud/hcloud-python/compare/v1.33.3...v1.34.0) (2024-03-27) diff --git a/hcloud/__version__.py b/hcloud/__version__.py index b592a1be..d350d713 100644 --- a/hcloud/__version__.py +++ b/hcloud/__version__.py @@ -1,3 +1,3 @@ from __future__ import annotations -VERSION = "1.34.0" # x-release-please-version +VERSION = "1.35.0" # x-release-please-version From 9db4e2a469e737a4e7fbafe68cb5145fd84fba16 Mon Sep 17 00:00:00 2001 From: Jonas L Date: Thu, 4 Apr 2024 12:42:56 +0200 Subject: [PATCH 183/406] chore: document how to implement deprecations (#378) --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 35d0e945..780be7c2 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,31 @@ You may also run the tests for multiple `python3` versions using `tox`: tox . ``` +### Deprecations implementation + +When deprecating a module or a function, you must: + +- Update the docstring with a `deprecated` notice: + +```py +"""Get image by name + +.. deprecated:: 1.19 + Use :func:`hcloud.images.client.ImagesClient.get_by_name_and_architecture` instead. +""" +``` + +- Raise a warning when the deprecated module or function is being used: + +```py +warnings.warn( + "The 'hcloud.images.client.ImagesClient.get_by_name' method is deprecated, please use the " + "'hcloud.images.client.ImagesClient.get_by_name_and_architecture' method instead.", + DeprecationWarning, + stacklevel=2, +) +``` + ## License The MIT License (MIT). Please see [`License File`](https://github.com/hetznercloud/hcloud-python/blob/main/LICENSE) for more information. From 5ef25ab3966d731c4c36ea3e785c2b5f20c69489 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 6 Apr 2024 19:54:30 +0000 Subject: [PATCH 184/406] deps: update pre-commit hook pre-commit/pre-commit-hooks to v4.6.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e49f0627..e1e8bc08 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-added-large-files - id: check-case-conflict From b745d4049f720b93d840a9204a99d246ecb499e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20Dar=C3=B3czi?= Date: Mon, 8 Apr 2024 11:05:49 +0200 Subject: [PATCH 185/406] docs: cx11 is name, not an id (#381) It doesn't make a real impact, as the `id_or_name` method is called on the object later, but to be technically correct, it would be better to pass the server name as `name` instead of `id`. --- examples/create_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/create_server.py b/examples/create_server.py index d85c1d9d..ce832d73 100644 --- a/examples/create_server.py +++ b/examples/create_server.py @@ -15,7 +15,7 @@ response = client.servers.create( name="my-server", - server_type=ServerType("cx11"), + server_type=ServerType(name="cx11"), image=Image(name="ubuntu-20.04"), ) server = response.server From 0941fbfab20ca8a59e768c4a5e6fc101393c97f0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 13 Apr 2024 07:31:12 +0000 Subject: [PATCH 186/406] deps: update pre-commit hook psf/black-pre-commit-mirror to v24.4.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e1e8bc08..0dc8025d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: - id: isort - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.3.0 + rev: 24.4.0 hooks: - id: black From 69c2e16073df9ef8520e3a635b3866403eba030e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 17 Apr 2024 08:36:00 +0200 Subject: [PATCH 187/406] deps: update dependency sphinx to >=7.3.4,<7.4 (#383) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [sphinx](https://togithub.com/sphinx-doc/sphinx) ([changelog](https://www.sphinx-doc.org/en/master/changes.html)) | `>=7.2.2,<7.3` -> `>=7.3.4,<7.4` | [![age](https://developer.mend.io/api/mc/badges/age/pypi/sphinx/7.3.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/sphinx/7.3.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/sphinx/7.2.6/7.3.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/sphinx/7.2.6/7.3.4?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
sphinx-doc/sphinx (sphinx) ### [`v7.3.4`](https://togithub.com/sphinx-doc/sphinx/releases/tag/v7.3.4): Sphinx 7.3.4 [Compare Source](https://togithub.com/sphinx-doc/sphinx/compare/v7.3.3...v7.3.4) Changelog: https://www.sphinx-doc.org/en/master/changes.html ### [`v7.3.3`](https://togithub.com/sphinx-doc/sphinx/blob/HEAD/CHANGES.rst#Release-733-in-development) [Compare Source](https://togithub.com/sphinx-doc/sphinx/compare/v7.3.2...v7.3.3) \============================== ## Bugs fixed - [#​12290](https://togithub.com/sphinx-doc/sphinx/issues/12290): Fix a false-positive warning when setting a configuration value with `Any` as the valid type to a type other than the value's default. Patch by Adam Turner. ### [`v7.3.2`](https://togithub.com/sphinx-doc/sphinx/blob/HEAD/CHANGES.rst#Release-732-released-Apr-17-2024) [Compare Source](https://togithub.com/sphinx-doc/sphinx/compare/v7.3.1...v7.3.2) \===================================== ## Bugs fixed - Preload all themes defined via entry points. Patch by Adam Turner. - Fix a bad interaction between the `'Furo'` theme and the new-style for configuration values. Patch by Adam Turner. ### [`v7.3.1`](https://togithub.com/sphinx-doc/sphinx/blob/HEAD/CHANGES.rst#Release-731-released-Apr-17-2024) [Compare Source](https://togithub.com/sphinx-doc/sphinx/compare/v7.3.0...v7.3.1) \===================================== ## Dependencies - Require `tomli` on Python 3.10 and earlier. Patch by Adam Turner. ### [`v7.3.0`](https://togithub.com/sphinx-doc/sphinx/blob/HEAD/CHANGES.rst#Release-730-released-Apr-16-2024) [Compare Source](https://togithub.com/sphinx-doc/sphinx/compare/v7.2.6...v7.3.0) \===================================== ## Dependencies - [#​11411](https://togithub.com/sphinx-doc/sphinx/issues/11411): Support `Docutils 0.21`\_. Patch by Adam Turner. .. \_Docutils 0.21: https://docutils.sourceforge.io/RELEASE-NOTES.html#release-0-21-2024-04-09 - [#​12012](https://togithub.com/sphinx-doc/sphinx/issues/12012): Use `types-docutils` instead of `docutils-stubs`. ## Deprecated - [#​11693](https://togithub.com/sphinx-doc/sphinx/issues/11693): Support for old-style :file:`Makefile` and :file:`make.bat` output in :program:`sphinx-quickstart`, and the associated options :option:`!-M`, :option:`!-m`, :option:`!--no-use-make-mode`, and :option:`!--use-make-mode`. - [#​11285](https://togithub.com/sphinx-doc/sphinx/issues/11285): Direct access to :attr:`!sphinx.testing.util.SphinxTestApp._status` or :attr:`!sphinx.testing.util.SphinxTestApp._warning` is deprecated. Use the public properties :attr:`!sphinx.testing.util.SphinxTestApp.status` and :attr:`!sphinx.testing.util.SphinxTestApp.warning` instead. Patch by Bénédikt Tran. - tests: :func:`!sphinx.testing.util.strip_escseq` is deprecated in favour of :func:`!sphinx.util.console.strip_colors`. Patch by Bénédikt Tran. ## Features added - [#​12265](https://togithub.com/sphinx-doc/sphinx/issues/12265): Support theme configuration via `theme.toml`. - [#​11701](https://togithub.com/sphinx-doc/sphinx/issues/11701): HTML Search: Adopt the new `\`\_ element. Patch by Bénédikt Tran. .. \_`\`: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/search - [#​11803](https://togithub.com/sphinx-doc/sphinx/issues/11803): autodoc: Use an overriden `__repr__()` function in an enum, if defined. Patch by Shengyu Zhang. - [#​11892](https://togithub.com/sphinx-doc/sphinx/issues/11892): Improved performance when resolving cross references in the C++ domain. Patch by Rouslan Korneychuk. - [#​11981](https://togithub.com/sphinx-doc/sphinx/issues/11981): Improve rendering of signatures using `slice` syntax, e.g., `def foo(arg: np.float64[:,:]) -> None: ...`. - The manpage builder now adds `OSC 8`\_ anchors to hyperlinks, using the `groff`\_ device control command. .. \_OSC 8: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda .. \_groff: https://lists.gnu.org/archive/html/groff/2021-10/msg00000.html - [#​11015](https://togithub.com/sphinx-doc/sphinx/issues/11015): Change the text of the :rst:dir:`versionadded` directive from `New in [...]` to `Added in [...]`. Patch by Bénédikt Tran. - [#​12131](https://togithub.com/sphinx-doc/sphinx/issues/12131): Added :confval:`show_warning_types` configuration option. Patch by Chris Sewell. - [#​12193](https://togithub.com/sphinx-doc/sphinx/issues/12193): Improve `external` warnings for unknown roles. In particular, suggest related role names if an object type is mistakenly used. Patch by Chris Sewell. - Add public type alias :class:`sphinx.util.typing.ExtensionMetadata`. This can be used by extension developers to annotate the return type of their `setup` function. Patch by Chris Sewell. ## Bugs fixed - [#​11668](https://togithub.com/sphinx-doc/sphinx/issues/11668): Raise a useful error when `theme.conf` is missing. Patch by Vinay Sajip. - [#​11622](https://togithub.com/sphinx-doc/sphinx/issues/11622): Ensure that the order of keys in `searchindex.js` is deterministic. Patch by Pietro Albini. - [#​11617](https://togithub.com/sphinx-doc/sphinx/issues/11617): ANSI control sequences are stripped from the output when writing to a warnings file with :option:`-w `. Patch by Bénédikt Tran. - [#​11666](https://togithub.com/sphinx-doc/sphinx/issues/11666): Skip all hidden directories in `CatalogRepository.pofiles`. Patch by Aryaz Eghbali. - [#​9686](https://togithub.com/sphinx-doc/sphinx/issues/9686): html builder: Fix MathJax lazy loading when equations appear in titles. Patch by Bénédikt Tran. - [#​11483](https://togithub.com/sphinx-doc/sphinx/issues/11483): singlehtml builder: Fix MathJax lazy loading when the index does not contain any math equations. Patch by Bénédikt Tran. - [#​11697](https://togithub.com/sphinx-doc/sphinx/issues/11697): HTML Search: add 'noindex' meta robots tag. Patch by James Addison. - [#​11678](https://togithub.com/sphinx-doc/sphinx/issues/11678): Fix a possible `ZeroDivisionError` in `sphinx.ext.coverage`. Patch by Stephen Finucane. - [#​11756](https://togithub.com/sphinx-doc/sphinx/issues/11756): LaTeX: build error with recent TeXLive due to missing `substitutefont` package (triggered if using `fontenc` with `T2A` option and document language is not a Cyrillic one). Patch by Jean-François B. - [#​11675](https://togithub.com/sphinx-doc/sphinx/issues/11675): Fix rendering of progression bars in environments that do not support ANSI control sequences. Patch by Bénédikt Tran. - [#​11715](https://togithub.com/sphinx-doc/sphinx/issues/11715): Apply `tls_verify` and `tls_cacerts` config to `ImageDownloader`. Patch by Nick Touran. - [#​11433](https://togithub.com/sphinx-doc/sphinx/issues/11433): Added the :confval:`linkcheck_allow_unauthorized` configuration option. Set this option to `False` to report HTTP 401 (unauthorized) server responses as broken. Patch by James Addison. - [#​11868](https://togithub.com/sphinx-doc/sphinx/issues/11868): linkcheck: added a distinct `timeout` reporting status code. This can be enabled by setting :confval:`linkcheck_report_timeouts_as_broken` to `False`. Patch by James Addison. - [#​11869](https://togithub.com/sphinx-doc/sphinx/issues/11869): Refresh the documentation for the `linkcheck_timeout` setting. Patch by James Addison. - [#​11874](https://togithub.com/sphinx-doc/sphinx/issues/11874): Configure a default 30-second value for `linkcheck_timeout`. Patch by James Addison. - [#​11886](https://togithub.com/sphinx-doc/sphinx/issues/11886): Print the Jinja2 template path chain in `TemplateNotFound` exceptions. Patch by Colin Marquardt. - [#​11598](https://togithub.com/sphinx-doc/sphinx/issues/11598): Do not use query components in URLs for assets in EPUB rendering. Patch by David Runge. - [#​11917](https://togithub.com/sphinx-doc/sphinx/issues/11917): Fix rendering of annotated inherited members for Python 3.9. Patch by Janet Carson. - [#​11925](https://togithub.com/sphinx-doc/sphinx/issues/11925): Blacklist the `sphinxprettysearchresults` extension; the functionality it provides was merged into Sphinx v2.0.0. Patch by James Addison. - [#​11353](https://togithub.com/sphinx-doc/sphinx/issues/11353): Support enumeration classes inheriting from mixin or data types. Patch by Bénédikt Tran. - [#​11962](https://togithub.com/sphinx-doc/sphinx/issues/11962): Fix target resolution when using `:paramtype:` fields. Patch by Bénédikt Tran. - [#​12008](https://togithub.com/sphinx-doc/sphinx/issues/12008): Fix case-sensitive lookup of `std:label` names in intersphinx inventory. Patch by Michael Goerz. - [#​11959](https://togithub.com/sphinx-doc/sphinx/issues/11959): Fix multiple term matching when word appears in both title and document. Patch by Will Lachance. - [#​11958](https://togithub.com/sphinx-doc/sphinx/issues/11958): HTML Search: Fix partial matches overwriting full matches. Patch by William Lachance. - [#​11944](https://togithub.com/sphinx-doc/sphinx/issues/11944): Use anchor in search preview. Patch by Will Lachance. - [#​11474](https://togithub.com/sphinx-doc/sphinx/issues/11474): Fix doctrees caching causing files not be rebuilt in some cases, e.g., when :confval:`numfig` is `True`. Patch by Bénédikt Tran. - [#​11278](https://togithub.com/sphinx-doc/sphinx/issues/11278): autodoc: Fix rendering of :class:`functools.singledispatchmethod` combined with :func:`@classmethod `. Patch by Bénédikt Tran. - [#​11894](https://togithub.com/sphinx-doc/sphinx/issues/11894): Do not add checksums to css files if building using the htmlhelp builder. Patch by mkay. - [#​12052](https://togithub.com/sphinx-doc/sphinx/issues/12052): Remove `