diff --git a/hcloud/_client.py b/hcloud/_client.py index 2bf147ec..c42f4fc7 100644 --- a/hcloud/_client.py +++ b/hcloud/_client.py @@ -3,7 +3,7 @@ import time from http import HTTPStatus from random import uniform -from typing import Protocol +from typing import Any, Protocol import requests @@ -72,7 +72,7 @@ def exponential_backoff_function( """ def func(retries: int) -> float: - interval = base * multiplier**retries # Exponential backoff + interval: float = base * multiplier**retries # Exponential backoff interval = min(cap, interval) # Cap backoff if jitter: interval = uniform(base, interval) # Add jitter @@ -292,7 +292,7 @@ def request( # type: ignore[no-untyped-def] method: str, url: str, **kwargs, - ) -> dict: + ) -> dict[str, Any]: """Perform a request to the Hetzner Cloud API. :param method: Method to perform the request. @@ -345,7 +345,7 @@ def request( # type: ignore[no-untyped-def] method: str, url: str, **kwargs, - ) -> dict: + ) -> dict[str, Any]: """Perform a request to the provided URL. :param method: Method to perform the request. @@ -381,7 +381,7 @@ def request( # type: ignore[no-untyped-def] continue raise - def _read_response(self, response: requests.Response) -> dict: + def _read_response(self, response: requests.Response) -> dict[str, Any]: correlation_id = response.headers.get("X-Correlation-Id") payload = {} try: @@ -404,7 +404,7 @@ def _read_response(self, response: requests.Response) -> dict: correlation_id=correlation_id, ) - error: dict = payload["error"] + error: dict[str, Any] = payload["error"] raise APIException( code=error["code"], message=error["message"], diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index a0c0cc49..0e7f8461 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -11,7 +11,7 @@ from .._client import Client -class BoundAction(BoundModelBase, Action): +class BoundAction(BoundModelBase[Action], Action): _client: ActionsClient model = Action diff --git a/hcloud/actions/domain.py b/hcloud/actions/domain.py index fd14bfcf..b85aca99 100644 --- a/hcloud/actions/domain.py +++ b/hcloud/actions/domain.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, TypedDict from .._exceptions import HCloudException from ..core import BaseDomain @@ -49,8 +49,8 @@ def __init__( progress: int | None = None, started: str | None = None, finished: str | None = None, - resources: list[dict] | None = None, - error: dict | None = None, + resources: list[ActionResource] | None = None, + error: ActionError | None = None, ): self.id = id self.command = command @@ -63,6 +63,17 @@ def __init__( self.error = error +class ActionResource(TypedDict): + id: int + type: str + + +class ActionError(TypedDict): + code: str + message: str + details: dict[str, Any] + + class ActionException(HCloudException): """A generic action exception""" @@ -80,7 +91,8 @@ def __init__(self, action: Action | BoundAction): extras.append(action.error["code"]) else: - extras.append(action.command) + if action.command is not None: + extras.append(action.command) extras.append(str(action.id)) message += f" ({', '.join(extras)})" diff --git a/hcloud/certificates/client.py b/hcloud/certificates/client.py index 8b2a7ef7..851178e8 100644 --- a/hcloud/certificates/client.py +++ b/hcloud/certificates/client.py @@ -15,12 +15,17 @@ from .._client import Client -class BoundCertificate(BoundModelBase, Certificate): +class BoundCertificate(BoundModelBase[Certificate], Certificate): _client: CertificatesClient model = Certificate - def __init__(self, client: CertificatesClient, data: dict, complete: bool = True): + def __init__( + self, + client: CertificatesClient, + data: dict[str, Any], + complete: bool = True, + ): status = data.get("status") if status is not None: error_data = status.get("error") diff --git a/hcloud/core/client.py b/hcloud/core/client.py index f0e67a7f..86bb1bb7 100644 --- a/hcloud/core/client.py +++ b/hcloud/core/client.py @@ -2,11 +2,16 @@ import warnings from collections.abc import Callable -from typing import TYPE_CHECKING, Any, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar + +from .domain import BaseDomain if TYPE_CHECKING: from .._client import Client, ClientBase - from .domain import BaseDomain + from .domain import Meta + + +T = TypeVar("T") class ResourceClientBase: @@ -23,10 +28,10 @@ def __init__(self, client: Client): def _iter_pages( # type: ignore[no-untyped-def] self, - list_function: Callable, + list_function: Callable[..., tuple[list[T], Meta]], *args, **kwargs, - ) -> list: + ) -> list[T]: results = [] page = 1 @@ -46,7 +51,12 @@ def _iter_pages( # type: ignore[no-untyped-def] return results - def _get_first_by(self, list_function: Callable, *args, **kwargs): # type: ignore[no-untyped-def] + def _get_first_by( # type: ignore[no-untyped-def] + self, + list_function: Callable[..., tuple[list[T], Meta]], + *args, + **kwargs, + ) -> T | None: entities, _ = list_function(*args, **kwargs) return entities[0] if entities else None @@ -69,15 +79,18 @@ def __init__(self, client: Client): super().__init__(client) -class BoundModelBase: +Domain = TypeVar("Domain", bound=BaseDomain) + + +class BoundModelBase(Generic[Domain]): """Bound Model Base""" - model: type[BaseDomain] + model: type[Domain] def __init__( self, client: ResourceClientBase, - data: dict, + data: dict[str, Any], complete: bool = True, ): """ @@ -90,7 +103,7 @@ def __init__( """ self._client = client self.complete = complete - self.data_model = self.model.from_dict(data) + self.data_model: Domain = self.model.from_dict(data) def __getattr__(self, name: str): # type: ignore[no-untyped-def] """Allow magical access to the properties of the model @@ -103,9 +116,10 @@ def __getattr__(self, name: str): # type: ignore[no-untyped-def] value = getattr(self.data_model, name) return value - def _get_self(self) -> BoundModelBase: + def _get_self(self) -> BoundModelBase[Domain]: assert hasattr(self._client, "get_by_id") - return self._client.get_by_id(self.data_model.id) + assert hasattr(self.data_model, "id") + return self._client.get_by_id(self.data_model.id) # type: ignore def reload(self) -> None: """Reloads the model and tries to get all data from the API""" diff --git a/hcloud/core/domain.py b/hcloud/core/domain.py index 245d658b..de75ffc5 100644 --- a/hcloud/core/domain.py +++ b/hcloud/core/domain.py @@ -7,10 +7,10 @@ class BaseDomain: - __api_properties__: tuple + __api_properties__: tuple[str, ...] @classmethod - def from_dict(cls, data: dict): # type: ignore[no-untyped-def] + def from_dict(cls, data: dict[str, Any]): # type: ignore[no-untyped-def] """ Build the domain object from the data dict. """ @@ -18,7 +18,7 @@ def from_dict(cls, data: dict): # type: ignore[no-untyped-def] return cls(**supported_data) def __repr__(self) -> str: - kwargs = [f"{key}={getattr(self, key)!r}" for key in self.__api_properties__] # type: ignore[var-annotated] + kwargs = [f"{key}={getattr(self, key)!r}" for key in self.__api_properties__] return f"{self.__class__.__qualname__}({', '.join(kwargs)})" def __eq__(self, other: Any) -> bool: @@ -119,7 +119,7 @@ def __init__(self, pagination: Pagination | None = None): self.pagination = pagination @classmethod - def parse_meta(cls, response: dict) -> Meta: + def parse_meta(cls, response: dict[str, Any]) -> Meta: """ If present, extract the meta details from the response and return a meta object. """ diff --git a/hcloud/datacenters/client.py b/hcloud/datacenters/client.py index ae338a5a..192abaf5 100644 --- a/hcloud/datacenters/client.py +++ b/hcloud/datacenters/client.py @@ -8,12 +8,12 @@ from .domain import Datacenter, DatacenterServerTypes -class BoundDatacenter(BoundModelBase, Datacenter): +class BoundDatacenter(BoundModelBase[Datacenter], Datacenter): _client: DatacentersClient model = Datacenter - def __init__(self, client: DatacentersClient, data: dict): + def __init__(self, client: DatacentersClient, data: dict[str, Any]): location = data.get("location") if location is not None: data["location"] = BoundLocation(client._parent.locations, location) diff --git a/hcloud/firewalls/client.py b/hcloud/firewalls/client.py index a088947a..3caf1be6 100644 --- a/hcloud/firewalls/client.py +++ b/hcloud/firewalls/client.py @@ -17,12 +17,17 @@ from .._client import Client -class BoundFirewall(BoundModelBase, Firewall): +class BoundFirewall(BoundModelBase[Firewall], Firewall): _client: FirewallsClient model = Firewall - def __init__(self, client: FirewallsClient, data: dict, complete: bool = True): + def __init__( + self, + client: FirewallsClient, + data: dict[str, Any], + complete: bool = True, + ): rules = data.get("rules", []) if rules: rules = [ diff --git a/hcloud/floating_ips/client.py b/hcloud/floating_ips/client.py index ce28be3d..771a094a 100644 --- a/hcloud/floating_ips/client.py +++ b/hcloud/floating_ips/client.py @@ -13,12 +13,17 @@ from ..servers import BoundServer, Server -class BoundFloatingIP(BoundModelBase, FloatingIP): +class BoundFloatingIP(BoundModelBase[FloatingIP], FloatingIP): _client: FloatingIPsClient model = FloatingIP - def __init__(self, client: FloatingIPsClient, data: dict, complete: bool = True): + def __init__( + self, + client: FloatingIPsClient, + data: dict[str, Any], + complete: bool = True, + ): # pylint: disable=import-outside-toplevel from ..servers import BoundServer diff --git a/hcloud/floating_ips/domain.py b/hcloud/floating_ips/domain.py index a9114170..6d24e77f 100644 --- a/hcloud/floating_ips/domain.py +++ b/hcloud/floating_ips/domain.py @@ -1,12 +1,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypedDict from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction from ..locations import BoundLocation + from ..rdns import DNSPtr from ..servers import BoundServer from .client import BoundFloatingIP @@ -63,10 +64,10 @@ def __init__( description: str | None = None, ip: str | None = None, server: BoundServer | None = None, - dns_ptr: list[dict] | None = None, + dns_ptr: list[DNSPtr] | None = None, home_location: BoundLocation | None = None, blocked: bool | None = None, - protection: dict | None = None, + protection: FloatingIPProtection | None = None, labels: dict[str, str] | None = None, created: str | None = None, name: str | None = None, @@ -85,6 +86,10 @@ def __init__( self.name = name +class FloatingIPProtection(TypedDict): + delete: bool + + class CreateFloatingIPResponse(BaseDomain): """Create Floating IP Response Domain diff --git a/hcloud/images/client.py b/hcloud/images/client.py index b9425b78..822ff14a 100644 --- a/hcloud/images/client.py +++ b/hcloud/images/client.py @@ -11,12 +11,16 @@ from .._client import Client -class BoundImage(BoundModelBase, Image): +class BoundImage(BoundModelBase[Image], Image): _client: ImagesClient model = Image - def __init__(self, client: ImagesClient, data: dict): + def __init__( + self, + client: ImagesClient, + data: dict[str, Any], + ): # pylint: disable=import-outside-toplevel from ..servers import BoundServer diff --git a/hcloud/images/domain.py b/hcloud/images/domain.py index ee610ff9..ee653628 100644 --- a/hcloud/images/domain.py +++ b/hcloud/images/domain.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypedDict from ..core import BaseDomain, DomainIdentityMixin @@ -87,7 +87,7 @@ def __init__( architecture: str | None = None, rapid_deploy: bool | None = None, created_from: Server | BoundServer | None = None, - protection: dict | None = None, + protection: ImageProtection | None = None, labels: dict[str, str] | None = None, status: str | None = None, ): @@ -110,6 +110,10 @@ def __init__( self.status = status +class ImageProtection(TypedDict): + delete: bool + + class CreateImageResponse(BaseDomain): """Create Image Response Domain diff --git a/hcloud/isos/client.py b/hcloud/isos/client.py index d5e44963..9c5ea5ac 100644 --- a/hcloud/isos/client.py +++ b/hcloud/isos/client.py @@ -6,7 +6,7 @@ from .domain import Iso -class BoundIso(BoundModelBase, Iso): +class BoundIso(BoundModelBase[Iso], Iso): _client: IsosClient model = Iso diff --git a/hcloud/isos/domain.py b/hcloud/isos/domain.py index 2a5667e4..c3ebd662 100644 --- a/hcloud/isos/domain.py +++ b/hcloud/isos/domain.py @@ -1,6 +1,7 @@ from __future__ import annotations from datetime import datetime +from typing import Any from warnings import warn from ..core import BaseDomain, DomainIdentityMixin @@ -45,7 +46,7 @@ def __init__( architecture: str | None = None, description: str | None = None, deprecated: str | None = None, # pylint: disable=unused-argument - deprecation: dict | None = None, + deprecation: dict[str, Any] | None = None, ): self.id = id self.name = name @@ -67,4 +68,4 @@ def deprecated(self) -> datetime | None: ) if self.deprecation is None: return None - return self.deprecation.unavailable_after + return self.deprecation.unavailable_after # type: ignore[no-any-return] diff --git a/hcloud/load_balancer_types/client.py b/hcloud/load_balancer_types/client.py index fbfb08d1..b639a37d 100644 --- a/hcloud/load_balancer_types/client.py +++ b/hcloud/load_balancer_types/client.py @@ -6,7 +6,7 @@ from .domain import LoadBalancerType -class BoundLoadBalancerType(BoundModelBase, LoadBalancerType): +class BoundLoadBalancerType(BoundModelBase[LoadBalancerType], LoadBalancerType): _client: LoadBalancerTypesClient model = LoadBalancerType diff --git a/hcloud/load_balancer_types/domain.py b/hcloud/load_balancer_types/domain.py index 9142cb58..8492ea7b 100644 --- a/hcloud/load_balancer_types/domain.py +++ b/hcloud/load_balancer_types/domain.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + from ..core import BaseDomain, DomainIdentityMixin @@ -46,7 +48,7 @@ def __init__( max_services: int | None = None, max_targets: int | None = None, max_assigned_certificates: int | None = None, - prices: list[dict] | None = None, + prices: list[dict[str, Any]] | None = None, ): self.id = id self.name = name diff --git a/hcloud/load_balancers/client.py b/hcloud/load_balancers/client.py index 2539101b..c0d373ce 100644 --- a/hcloud/load_balancers/client.py +++ b/hcloud/load_balancers/client.py @@ -40,13 +40,18 @@ from ..networks import Network -class BoundLoadBalancer(BoundModelBase, LoadBalancer): +class BoundLoadBalancer(BoundModelBase[LoadBalancer], LoadBalancer): _client: LoadBalancersClient model = LoadBalancer # pylint: disable=too-many-branches,too-many-locals - def __init__(self, client: LoadBalancersClient, data: dict, complete: bool = True): + def __init__( + self, + client: LoadBalancersClient, + data: dict[str, Any], + complete: bool = True, + ): algorithm = data.get("algorithm") if algorithm: data["algorithm"] = LoadBalancerAlgorithm(type=algorithm["type"]) diff --git a/hcloud/load_balancers/domain.py b/hcloud/load_balancers/domain.py index 8aa8b1f5..a5b62b50 100644 --- a/hcloud/load_balancers/domain.py +++ b/hcloud/load_balancers/domain.py @@ -1,7 +1,7 @@ from __future__ import annotations import warnings -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Any, Literal, TypedDict from ..core import BaseDomain, DomainIdentityMixin @@ -81,7 +81,7 @@ def __init__( algorithm: LoadBalancerAlgorithm | None = None, services: list[LoadBalancerService] | None = None, load_balancer_type: BoundLoadBalancerType | None = None, - protection: dict | None = None, + protection: LoadBalancerProtection | None = None, labels: dict[str, str] | None = None, targets: list[LoadBalancerTarget] | None = None, created: str | None = None, @@ -116,6 +116,10 @@ def private_net_for(self, network: BoundNetwork | Network) -> PrivateNet | None: return None +class LoadBalancerProtection(TypedDict): + delete: bool + + class LoadBalancerService(BaseDomain): """LoadBalancerService Domain @@ -334,7 +338,7 @@ def __init__( domain: str | None = None, path: str | None = None, response: str | None = None, - status_codes: list | None = None, + status_codes: list[str] | None = None, tls: bool | None = None, ): self.domain = domain @@ -357,7 +361,7 @@ def __init__( domain: str | None = None, path: str | None = None, response: str | None = None, - status_codes: list | None = None, + status_codes: list[str] | None = None, tls: bool | None = None, ): warnings.warn( diff --git a/hcloud/locations/client.py b/hcloud/locations/client.py index e6f685db..8c7b5e5e 100644 --- a/hcloud/locations/client.py +++ b/hcloud/locations/client.py @@ -6,7 +6,7 @@ from .domain import Location -class BoundLocation(BoundModelBase, Location): +class BoundLocation(BoundModelBase[Location], Location): _client: LocationsClient model = Location diff --git a/hcloud/networks/client.py b/hcloud/networks/client.py index abef2b63..438d4a98 100644 --- a/hcloud/networks/client.py +++ b/hcloud/networks/client.py @@ -10,12 +10,17 @@ from .._client import Client -class BoundNetwork(BoundModelBase, Network): +class BoundNetwork(BoundModelBase[Network], Network): _client: NetworksClient model = Network - def __init__(self, client: NetworksClient, data: dict, complete: bool = True): + def __init__( + self, + client: NetworksClient, + data: dict[str, Any], + complete: bool = True, + ): subnets = data.get("subnets", []) if subnets is not None: subnets = [NetworkSubnet.from_dict(subnet) for subnet in subnets] diff --git a/hcloud/networks/domain.py b/hcloud/networks/domain.py index 63c0d7b7..cf6f1fc8 100644 --- a/hcloud/networks/domain.py +++ b/hcloud/networks/domain.py @@ -1,7 +1,7 @@ from __future__ import annotations import warnings -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypedDict from ..core import BaseDomain, DomainIdentityMixin @@ -58,7 +58,7 @@ def __init__( routes: list[NetworkRoute] | None = None, expose_routes_to_vswitch: bool | None = None, servers: list[BoundServer] | None = None, - protection: dict | None = None, + protection: NetworkProtection | None = None, labels: dict[str, str] | None = None, ): self.id = id @@ -73,6 +73,10 @@ def __init__( self.labels = labels +class NetworkProtection(TypedDict): + delete: bool + + class NetworkSubnet(BaseDomain): """Network Subnet Domain diff --git a/hcloud/placement_groups/client.py b/hcloud/placement_groups/client.py index 94939341..f59e7e6c 100644 --- a/hcloud/placement_groups/client.py +++ b/hcloud/placement_groups/client.py @@ -7,7 +7,7 @@ from .domain import CreatePlacementGroupResponse, PlacementGroup -class BoundPlacementGroup(BoundModelBase, PlacementGroup): +class BoundPlacementGroup(BoundModelBase[PlacementGroup], PlacementGroup): _client: PlacementGroupsClient model = PlacementGroup diff --git a/hcloud/primary_ips/client.py b/hcloud/primary_ips/client.py index 4845a18d..f7f36a80 100644 --- a/hcloud/primary_ips/client.py +++ b/hcloud/primary_ips/client.py @@ -11,12 +11,17 @@ from ..datacenters import BoundDatacenter, Datacenter -class BoundPrimaryIP(BoundModelBase, PrimaryIP): +class BoundPrimaryIP(BoundModelBase[PrimaryIP], PrimaryIP): _client: PrimaryIPsClient model = PrimaryIP - def __init__(self, client: PrimaryIPsClient, data: dict, complete: bool = True): + def __init__( + self, + client: PrimaryIPsClient, + data: dict[str, Any], + complete: bool = True, + ): # pylint: disable=import-outside-toplevel from ..datacenters import BoundDatacenter @@ -307,7 +312,7 @@ def create( assignee_type: str | None = "server", assignee_id: int | None = None, auto_delete: bool | None = False, - labels: dict | None = None, + labels: dict[str, str] | None = None, ) -> CreatePrimaryIPResponse: """Creates a new Primary IP assigned to a server. diff --git a/hcloud/primary_ips/domain.py b/hcloud/primary_ips/domain.py index e6217b01..9a7e3d2b 100644 --- a/hcloud/primary_ips/domain.py +++ b/hcloud/primary_ips/domain.py @@ -1,12 +1,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypedDict from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction from ..datacenters import BoundDatacenter + from ..rdns import DNSPtr from .client import BoundPrimaryIP @@ -63,11 +64,11 @@ def __init__( id: int | None = None, type: str | None = None, ip: str | None = None, - dns_ptr: list[dict] | None = None, + dns_ptr: list[DNSPtr] | None = None, datacenter: BoundDatacenter | None = None, blocked: bool | None = None, - protection: dict | None = None, - labels: dict[str, dict] | None = None, + protection: PrimaryIPProtection | None = None, + labels: dict[str, str] | None = None, created: str | None = None, name: str | None = None, assignee_id: int | None = None, @@ -89,6 +90,10 @@ def __init__( self.auto_delete = auto_delete +class PrimaryIPProtection(TypedDict): + delete: bool + + class CreatePrimaryIPResponse(BaseDomain): """Create Primary IP Response Domain diff --git a/hcloud/rdns/__init__.py b/hcloud/rdns/__init__.py new file mode 100644 index 00000000..116a632b --- /dev/null +++ b/hcloud/rdns/__init__.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from .domain import DNSPtr + +__all__ = [ + "DNSPtr", +] diff --git a/hcloud/rdns/domain.py b/hcloud/rdns/domain.py new file mode 100644 index 00000000..0f8b0341 --- /dev/null +++ b/hcloud/rdns/domain.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from typing import TypedDict + + +class DNSPtr(TypedDict): + ip: str + dns_ptr: str diff --git a/hcloud/server_types/client.py b/hcloud/server_types/client.py index db0ef54a..33f5ebda 100644 --- a/hcloud/server_types/client.py +++ b/hcloud/server_types/client.py @@ -7,7 +7,7 @@ from .domain import ServerType, ServerTypeLocation -class BoundServerType(BoundModelBase, ServerType): +class BoundServerType(BoundModelBase[ServerType], ServerType): _client: ServerTypesClient model = ServerType @@ -15,7 +15,7 @@ class BoundServerType(BoundModelBase, ServerType): def __init__( self, client: ServerTypesClient, - data: dict, + data: dict[str, Any], complete: bool = True, ): raw = data.get("locations") diff --git a/hcloud/server_types/domain.py b/hcloud/server_types/domain.py index ff9e3fd4..1c470636 100644 --- a/hcloud/server_types/domain.py +++ b/hcloud/server_types/domain.py @@ -1,6 +1,7 @@ from __future__ import annotations import warnings +from typing import Any from ..core import BaseDomain, DomainIdentityMixin from ..deprecation import DeprecationInfo @@ -79,12 +80,12 @@ def __init__( cores: int | None = None, memory: int | None = None, disk: int | None = None, - prices: list[dict] | None = None, + prices: list[dict[str, Any]] | None = None, storage_type: str | None = None, cpu_type: str | None = None, architecture: str | None = None, deprecated: bool | None = None, - deprecation: dict | None = None, + deprecation: dict[str, Any] | None = None, included_traffic: int | None = None, locations: list[ServerTypeLocation] | None = None, ): @@ -191,7 +192,7 @@ def __init__( self, *, location: BoundLocation, - deprecation: dict | None, + deprecation: dict[str, Any] | None, ): self.location = location self.deprecation = ( diff --git a/hcloud/servers/client.py b/hcloud/servers/client.py index 626c6c36..d5abb28d 100644 --- a/hcloud/servers/client.py +++ b/hcloud/servers/client.py @@ -48,13 +48,18 @@ from .domain import ServerCreatePublicNetwork -class BoundServer(BoundModelBase, Server): +class BoundServer(BoundModelBase[Server], Server): _client: ServersClient model = Server # pylint: disable=too-many-locals - def __init__(self, client: ServersClient, data: dict, complete: bool = True): + def __init__( + self, + client: ServersClient, + data: dict[str, Any], + complete: bool = True, + ): datacenter = data.get("datacenter") if datacenter is not None: data["datacenter"] = BoundDatacenter(client._parent.datacenters, datacenter) diff --git a/hcloud/servers/domain.py b/hcloud/servers/domain.py index d94068eb..215fa9de 100644 --- a/hcloud/servers/domain.py +++ b/hcloud/servers/domain.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Literal, TypedDict from ..core import BaseDomain, DomainIdentityMixin @@ -15,6 +15,7 @@ from ..networks import BoundNetwork, Network from ..placement_groups import BoundPlacementGroup from ..primary_ips import BoundPrimaryIP, PrimaryIP + from ..rdns import DNSPtr from ..server_types import BoundServerType from ..volumes import BoundVolume from .client import BoundServer @@ -123,7 +124,7 @@ def __init__( outgoing_traffic: int | None = None, ingoing_traffic: int | None = None, included_traffic: int | None = None, - protection: dict | None = None, + protection: ServerProtection | None = None, labels: dict[str, str] | None = None, volumes: list[BoundVolume] | None = None, private_net: list[PrivateNet] | None = None, @@ -163,6 +164,11 @@ def private_net_for(self, network: BoundNetwork | Network) -> PrivateNet | None: return None +class ServerProtection(TypedDict): + rebuild: bool + delete: bool + + class CreateServerResponse(BaseDomain): """Create Server Response Domain @@ -387,7 +393,7 @@ def __init__( self, ip: str, blocked: bool, - dns_ptr: list, + dns_ptr: list[DNSPtr], ): self.ip = ip self.blocked = blocked diff --git a/hcloud/ssh_keys/client.py b/hcloud/ssh_keys/client.py index a565469b..fe2be196 100644 --- a/hcloud/ssh_keys/client.py +++ b/hcloud/ssh_keys/client.py @@ -6,7 +6,7 @@ from .domain import SSHKey -class BoundSSHKey(BoundModelBase, SSHKey): +class BoundSSHKey(BoundModelBase[SSHKey], SSHKey): _client: SSHKeysClient model = SSHKey diff --git a/hcloud/storage_box_types/client.py b/hcloud/storage_box_types/client.py index 05cb03e8..2f001e27 100644 --- a/hcloud/storage_box_types/client.py +++ b/hcloud/storage_box_types/client.py @@ -9,7 +9,7 @@ from .._client import Client -class BoundStorageBoxType(BoundModelBase, StorageBoxType): +class BoundStorageBoxType(BoundModelBase[StorageBoxType], StorageBoxType): _client: StorageBoxTypesClient model = StorageBoxType diff --git a/hcloud/storage_box_types/domain.py b/hcloud/storage_box_types/domain.py index b807ce27..dc90a8d4 100644 --- a/hcloud/storage_box_types/domain.py +++ b/hcloud/storage_box_types/domain.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + from ..core import BaseDomain, DomainIdentityMixin from ..deprecation import DeprecationInfo @@ -33,8 +35,8 @@ def __init__( automatic_snapshot_limit: int | None = None, subaccounts_limit: int | None = None, size: int | None = None, - prices: list[dict] | None = None, - deprecation: dict | None = None, + prices: list[dict[str, Any]] | None = None, + deprecation: dict[str, Any] | None = None, ): self.id = id self.name = name diff --git a/hcloud/storage_boxes/client.py b/hcloud/storage_boxes/client.py index 0cb3c2db..f81e3c81 100644 --- a/hcloud/storage_boxes/client.py +++ b/hcloud/storage_boxes/client.py @@ -29,7 +29,7 @@ from .._client import Client -class BoundStorageBox(BoundModelBase, StorageBox): +class BoundStorageBox(BoundModelBase[StorageBox], StorageBox): _client: StorageBoxesClient model = StorageBox @@ -322,7 +322,7 @@ def get_snapshot_by_id( def get_snapshot_by_name( self, name: str, - ) -> BoundStorageBoxSnapshot: + ) -> BoundStorageBoxSnapshot | None: """ Returns a single Snapshot from a Storage Box. @@ -438,7 +438,7 @@ def get_subaccount_by_id( def get_subaccount_by_username( self, username: str, - ) -> BoundStorageBoxSubaccount: + ) -> BoundStorageBoxSubaccount | None: """ Returns a single Subaccount from a Storage Box. @@ -537,7 +537,7 @@ def create_subaccount( ) -class BoundStorageBoxSnapshot(BoundModelBase, StorageBoxSnapshot): +class BoundStorageBoxSnapshot(BoundModelBase[StorageBoxSnapshot], StorageBoxSnapshot): _client: StorageBoxesClient model = StorageBoxSnapshot @@ -561,6 +561,8 @@ def __init__( super().__init__(client, data, complete) def _get_self(self) -> BoundStorageBoxSnapshot: + assert self.data_model.storage_box is not None + assert self.data_model.id is not None return self._client.get_snapshot_by_id( self.data_model.storage_box, self.data_model.id, @@ -603,7 +605,9 @@ def delete( return self._client.delete_snapshot(self) -class BoundStorageBoxSubaccount(BoundModelBase, StorageBoxSubaccount): +class BoundStorageBoxSubaccount( + BoundModelBase[StorageBoxSubaccount], StorageBoxSubaccount +): _client: StorageBoxesClient model = StorageBoxSubaccount @@ -627,6 +631,8 @@ def __init__( super().__init__(client, data, complete) def _get_self(self) -> BoundStorageBoxSubaccount: + assert self.data_model.storage_box is not None + assert self.data_model.id is not None return self._client.get_subaccount_by_id( self.data_model.storage_box, self.data_model.id, @@ -1279,7 +1285,7 @@ def get_snapshot_by_name( self, storage_box: StorageBox | BoundStorageBox, name: str, - ) -> BoundStorageBoxSnapshot: + ) -> BoundStorageBoxSnapshot | None: """ Returns a single Snapshot from a Storage Box. @@ -1500,7 +1506,7 @@ def get_subaccount_by_username( self, storage_box: StorageBox | BoundStorageBox, username: str, - ) -> BoundStorageBoxSubaccount: + ) -> BoundStorageBoxSubaccount | None: """ Returns a single Subaccount from a Storage Box. diff --git a/hcloud/volumes/client.py b/hcloud/volumes/client.py index 9d1cded6..2e36fd13 100644 --- a/hcloud/volumes/client.py +++ b/hcloud/volumes/client.py @@ -13,12 +13,17 @@ from ..servers import BoundServer, Server -class BoundVolume(BoundModelBase, Volume): +class BoundVolume(BoundModelBase[Volume], Volume): _client: VolumesClient model = Volume - def __init__(self, client: VolumesClient, data: dict, complete: bool = True): + def __init__( + self, + client: VolumesClient, + data: dict[str, Any], + complete: bool = True, + ): location = data.get("location") if location is not None: data["location"] = BoundLocation(client._parent.locations, location) diff --git a/hcloud/volumes/domain.py b/hcloud/volumes/domain.py index 4c4fcb7f..1e2e4379 100644 --- a/hcloud/volumes/domain.py +++ b/hcloud/volumes/domain.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypedDict from ..core import BaseDomain, DomainIdentityMixin @@ -68,7 +68,7 @@ def __init__( size: int | None = None, linux_device: str | None = None, format: str | None = None, - protection: dict | None = None, + protection: VolumeProtection | None = None, labels: dict[str, str] | None = None, status: str | None = None, ): @@ -85,6 +85,10 @@ def __init__( self.status = status +class VolumeProtection(TypedDict): + delete: bool + + class CreateVolumeResponse(BaseDomain): """Create Volume Response Domain diff --git a/hcloud/zones/client.py b/hcloud/zones/client.py index b3346042..3a1d5c7b 100644 --- a/hcloud/zones/client.py +++ b/hcloud/zones/client.py @@ -23,7 +23,7 @@ from .._client import Client -class BoundZone(BoundModelBase, Zone): +class BoundZone(BoundModelBase[Zone], Zone): _client: ZonesClient model = Zone @@ -405,12 +405,17 @@ def set_rrset_records( return self._client.set_rrset_records(rrset=rrset, records=records) -class BoundZoneRRSet(BoundModelBase, ZoneRRSet): +class BoundZoneRRSet(BoundModelBase[ZoneRRSet], ZoneRRSet): _client: ZonesClient model = ZoneRRSet - def __init__(self, client: ZonesClient, data: dict, complete: bool = True): + def __init__( + self, + client: ZonesClient, + data: dict[str, Any], + complete: bool = True, + ): raw = data.get("zone") if raw is not None: data["zone"] = BoundZone(client, data={"id": raw}, complete=False) @@ -422,6 +427,8 @@ def __init__(self, client: ZonesClient, data: dict, complete: bool = True): super().__init__(client, data, complete) def _get_self(self) -> BoundZoneRRSet: + assert self.data_model.zone is not None + assert self.data_model.type is not None return self._client.get_rrset( self.data_model.zone, self.data_model.name, diff --git a/pyproject.toml b/pyproject.toml index b35f889d..cbf37daf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ combine_as_imports = true add_imports = ["from __future__ import annotations"] [tool.mypy] +strict = true disallow_untyped_defs = true implicit_reexport = false