././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1636274 hcloud-2.17.0/0000755000175100017510000000000015152343221012523 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/CHANGELOG.md0000644000175100017510000014462515152343177014362 0ustar00runnerrunner# Changelog ## [v2.17.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.17.0) ### Features - parse nested load balancer `label_selector` targets (#633) ## [v2.16.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.16.0) ### Storage Boxes support is now generally available The experimental phase for Storage Boxes is over, and Storage Boxes support is now generally available. ### Features - **servers**: allow setting user_data for rebuild (#627) - Storage Box support no longer experimental (#626) ## [v2.15.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.15.0) ### Features - add name to Storage Box Subaccount (#621) ## [v2.14.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.14.0) ### Features - retry requests when the api returns a `timeout` error (#617) ## [v2.13.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.13.0) ### Features - add per primary ip actions list operations (#608) - deprecate datacenter in `primary ips` and `servers` (#609) ## [v2.12.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.12.0) ### Storage Box API Experimental This release adds support for the [Storage Box API](https://docs.hetzner.cloud/reference/hetzner#storage-boxes). The Storage Box integration will be introduced as an **experimental** feature. This experimental phase is expected to last at least until **12 January 2026**. During this period, upcoming minor releases of the project may include breaking changes to features related to Storage Boxes. This release includes all changes from the recent [Storage Box API changelog](https://docs.hetzner.cloud/changelog#2025-10-21-storage-box-api-update) entry. #### Examples ```python response = client.storage_boxes.create( name="string", location=Location(name="fsn1"), storage_box_type=StorageBoxType(name="bx11"), labels={ "environment": "prod", "example.com/my": "label", "just-a-key": "", }, password="my-password", access_settings=StorageBoxAccessSettings( reachable_externally=False, samba_enabled=False, ssh_enabled=False, webdav_enabled=False, zfs_enabled=False, ), ssh_keys=[SSHKey(public_key="ssh-rsa AAAjjk76kgf...Xt")], ) response.action.wait_until_finished() storage_box = response.storage_box ``` ### Features - add update rrset records action to zone client (#597) - add support for Storage Boxes (#524) ## [v2.11.1](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.11.1) ### Bug Fixes - support reloading sub resource bound models (#590) ## [v2.11.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.11.0) ### DNS API is now generally available The DNS API is now generally available, as well as support for features in this project that are related to the DNS API. To migrate existing zones to the new DNS API, see the [DNS migration guide](https://docs.hetzner.com/networking/dns/migration-to-hetzner-console/process/). See the [changelog](https://docs.hetzner.cloud/changelog#2025-11-10-dns-ga) for more details. ### Features - DNS support is now generally available (#581) ## [v2.10.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.10.0) ### Features - **exp**: add zone format txt record helper (#578) - add server and load balancer `private_net_for` helper method (#580) ## [v2.9.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.9.0) ### Features - support python 3.14 (#566) - drop support for python 3.9 (#574) ## [v2.8.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.8.0) ### DNS API Beta This release adds support for the new [DNS API](https://docs.hetzner.cloud/reference/cloud#dns). The DNS API is currently in **beta**, which will likely end on 10 November 2025. After the beta ended, it will no longer be possible to create new zones in the old DNS system. See the [DNS Beta FAQ](https://docs.hetzner.com/networking/dns/faq/beta/) for more details. Future minor releases of this project may include breaking changes for features that are related to the DNS API. See the [DNS API Beta changelog](https://docs.hetzner.cloud/changelog#2025-10-07-dns-beta) for more details. **Examples** ```py resp = client.zones.create( name="example.com", mode="primary", labels={"key": "value"}, rrsets=[ ZoneRRSet( name="@", type="A", records=[ ZoneRecord(value="201.180.75.2", comment="server1") ], ) ], ) resp.action.wait_until_finished() zone = resp.zone ``` ### Features - add new `ip_range` param to load balancer `attach_to_network` (#562) - add new `ip_range` param to server `attach_to_network` (#561) - support the new DNS API (#568) ### Bug Fixes - source_ips property is optional in firewall rule (#567) ## [v2.7.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.7.0) [Server Types](https://docs.hetzner.cloud/reference/cloud#server-types) now depend on [Locations](https://docs.hetzner.cloud/reference/cloud#locations). - We added a new `locations` property to the [Server Types](https://docs.hetzner.cloud/reference/cloud#server-types) resource. The new property defines a list of supported [Locations](https://docs.hetzner.cloud/reference/cloud#locations) and additional per [Locations](https://docs.hetzner.cloud/reference/cloud#locations) details such as deprecations information. - We deprecated the `deprecation` property from the [Server Types](https://docs.hetzner.cloud/reference/cloud#server-types) resource. The property will gradually be phased out as per [Locations](https://docs.hetzner.cloud/reference/cloud#locations) deprecations are being announced. Please use the new per [Locations](https://docs.hetzner.cloud/reference/cloud#locations) deprecation information instead. See our [changelog](https://docs.hetzner.cloud/changelog#2025-09-24-per-location-server-types) for more details. **Upgrading** ```py # Before def validate_server_type(server_type: ServerType): if server_type.deprecation is not None: raise ValueError(f"server type {server_type.name} is deprecated") ``` ```py # After def validate_server_type(server_type: ServerType, location: Location): found = [o for o in server_type.locations if location.name == o.location.name] if not found: raise ValueError( f"server type {server_type.name} is not supported in location {location.name}" ) server_type_location = found[0] if server_type_location.deprecation is not None: raise ValueError( f"server type {server_type.name} is deprecated in location {location.name}" ) ``` ### Features - per location server types (#558) ## [v2.6.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.6.0) ### Features - add category property to server type (#549) ### Bug Fixes - rename `ClientEntityBase` to `ResourceClientBase` (#532) ## [v2.5.4](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.5.4) ### Bug Fixes - typo in `LoadBalancerHealthCheckHttp` class name (#511) - equality for some domain classes (#510) - use valid license identifier (SPDX) (#514) ## [v2.5.3](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.5.3) ### Bug Fixes - invalid placement group id casting (#501) - handle string id when checking has_id_or_name (#504) ## [v2.5.2](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.5.2) ### Bug Fixes - listing page result always provide meta (#496) ## [v2.5.1](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.5.1) ### Bug Fixes - missing slots and api_properties for FirewallResourceLabelSelector (#492) ## [v2.5.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.5.0) ### Features - improve exception messages (#488) ## [v2.4.0](https://github.com/hetznercloud/hcloud-python/releases/tag/v2.4.0) ### Features - drop support for python 3.8 (#458) - add equality checks to domains (#481) ### Bug Fixes - server public ipv4 and ipv6 properties are nullable (#455) ## [2.3.0](https://github.com/hetznercloud/hcloud-python/compare/v2.2.1...v2.3.0) (2024-10-09) ### Features - support python 3.13 ([#451](https://github.com/hetznercloud/hcloud-python/issues/451)) ([4a514c7](https://github.com/hetznercloud/hcloud-python/commit/4a514c7a1136a4a8c592c77120c5be36cd221b33)) ### Bug Fixes - change floating ip labels type to `dict[str, str]` ([#444](https://github.com/hetznercloud/hcloud-python/issues/444)) ([1f6da4e](https://github.com/hetznercloud/hcloud-python/commit/1f6da4ef243321d3c6850b876f3c11fb1195edcf)) ## [2.2.1](https://github.com/hetznercloud/hcloud-python/compare/v2.2.0...v2.2.1) (2024-08-19) ### Bug Fixes - prices properties are list of dict ([#438](https://github.com/hetznercloud/hcloud-python/issues/438)) ([9621604](https://github.com/hetznercloud/hcloud-python/commit/96216048c9ba13b6286d584c2dd0ec440f484105)), closes [#437](https://github.com/hetznercloud/hcloud-python/issues/437) ## [2.2.0](https://github.com/hetznercloud/hcloud-python/compare/v2.1.1...v2.2.0) (2024-08-06) ### Features - retry requests when the api gateway errors ([#430](https://github.com/hetznercloud/hcloud-python/issues/430)) ([f63ac8b](https://github.com/hetznercloud/hcloud-python/commit/f63ac8b4d08d84804b5431295ba689275c9203f7)) - retry requests when the api returns a conflict error ([#430](https://github.com/hetznercloud/hcloud-python/issues/430)) ([f63ac8b](https://github.com/hetznercloud/hcloud-python/commit/f63ac8b4d08d84804b5431295ba689275c9203f7)) - retry requests when the network timed outs ([#430](https://github.com/hetznercloud/hcloud-python/issues/430)) ([f63ac8b](https://github.com/hetznercloud/hcloud-python/commit/f63ac8b4d08d84804b5431295ba689275c9203f7)) - retry requests when the rate limit was reached ([#430](https://github.com/hetznercloud/hcloud-python/issues/430)) ([f63ac8b](https://github.com/hetznercloud/hcloud-python/commit/f63ac8b4d08d84804b5431295ba689275c9203f7)) ### Bug Fixes - update network subnet types ([#431](https://github.com/hetznercloud/hcloud-python/issues/431)) ([c32a615](https://github.com/hetznercloud/hcloud-python/commit/c32a615db778d57324632d8df99356bb04a91efa)) ## [2.1.1](https://github.com/hetznercloud/hcloud-python/compare/v2.1.0...v2.1.1) (2024-07-30) ### Bug Fixes - do not sleep before checking for the reloaded action status ([#426](https://github.com/hetznercloud/hcloud-python/issues/426)) ([3e0a85b](https://github.com/hetznercloud/hcloud-python/commit/3e0a85b487fc15941008e4d610243de3cb0396cb)) - mark client retry backoff function as static ([#429](https://github.com/hetznercloud/hcloud-python/issues/429)) ([14ed130](https://github.com/hetznercloud/hcloud-python/commit/14ed130e989c68eacce2634c7983b200570de9c2)) ### Documentation - add api changes note in changelog ([#424](https://github.com/hetznercloud/hcloud-python/issues/424)) ([5cbe188](https://github.com/hetznercloud/hcloud-python/commit/5cbe1889a21c686588d91ab90306d345ba5b84dd)) ## [2.1.0](https://github.com/hetznercloud/hcloud-python/compare/v2.0.1...v2.1.0) (2024-07-25) ### API Changes for Traffic Prices and Server Type Included Traffic There will be a breaking change in the API regarding Traffic Prices and Server Type Included Traffic on 2024-08-05. This release marks the affected fields as `Deprecated`. Please check if this affects any of your code and switch to the replacement fields where necessary. You can learn more about this change in [our changelog](https://docs.hetzner.cloud/changelog#2024-07-25-cloud-api-returns-traffic-information-in-different-format). ### Features - add exponential and constant backoff function ([#416](https://github.com/hetznercloud/hcloud-python/issues/416)) ([fe7ddf6](https://github.com/hetznercloud/hcloud-python/commit/fe7ddf6da78f8dbbc395eb98ff1200b8117f0cc0)) - deprecate `ServerType` `included_traffic` property ([#423](https://github.com/hetznercloud/hcloud-python/issues/423)) ([3d56ac5](https://github.com/hetznercloud/hcloud-python/commit/3d56ac57d092bb30543fac9249c04393d0864c3b)) - use exponential backoff when retrying requests ([#417](https://github.com/hetznercloud/hcloud-python/issues/417)) ([f306073](https://github.com/hetznercloud/hcloud-python/commit/f3060737d0e2991a0abf69e4953a3967ac8f84ed)) ## [2.0.1](https://github.com/hetznercloud/hcloud-python/compare/v2.0.0...v2.0.1) (2024-07-03) ### Bug Fixes - `assignee_type` is required when creating a primary ip ([#409](https://github.com/hetznercloud/hcloud-python/issues/409)) ([bce5e94](https://github.com/hetznercloud/hcloud-python/commit/bce5e940e27f2c6d9d50016b5828c79aadfc4401)) - clean unused arguments in the `Client.servers.rebuild` method ([#407](https://github.com/hetznercloud/hcloud-python/issues/407)) ([6d33c3c](https://github.com/hetznercloud/hcloud-python/commit/6d33c3cff5443686c7ed37eb8635e0461bb3b928)) - details are optional in API errors ([#411](https://github.com/hetznercloud/hcloud-python/issues/411)) ([f1c6594](https://github.com/hetznercloud/hcloud-python/commit/f1c6594dee7088872f2375359ee259e4e93b31d2)) - rename `trace_id` variable to `correlation_id` ([#408](https://github.com/hetznercloud/hcloud-python/issues/408)) ([66a0f54](https://github.com/hetznercloud/hcloud-python/commit/66a0f546998193f9078f70a4a2fb1fc11937c086)) ## [2.0.0](https://github.com/hetznercloud/hcloud-python/compare/v1.35.0...v2.0.0) (2024-07-03) ### ⚠ BREAKING CHANGES - return full rebuild response in `Client.servers.rebuild` ([#406](https://github.com/hetznercloud/hcloud-python/issues/406)) - make `datacenter` argument optional when creating a primary ip ([#363](https://github.com/hetznercloud/hcloud-python/issues/363)) - remove deprecated `include_wildcard_architecture` argument in `IsosClient.get_list` and `IsosClient.get_all` ([#402](https://github.com/hetznercloud/hcloud-python/issues/402)) - make `Client.request` `tries` a private argument ([#399](https://github.com/hetznercloud/hcloud-python/issues/399)) - make `Client.poll_interval` a private property ([#398](https://github.com/hetznercloud/hcloud-python/issues/398)) - return empty dict on empty responses in `Client.request` ([#400](https://github.com/hetznercloud/hcloud-python/issues/400)) - remove deprecated `hcloud.hcloud` module ([#401](https://github.com/hetznercloud/hcloud-python/issues/401)) - move `hcloud.__version__.VERSION` to `hcloud.__version__` ([#397](https://github.com/hetznercloud/hcloud-python/issues/397)) ### Features - add `trace_id` to API exceptions ([#404](https://github.com/hetznercloud/hcloud-python/issues/404)) ([8375261](https://github.com/hetznercloud/hcloud-python/commit/8375261da3b84d6fece97263c7bea40ad2a6cfcf)) - allow using a custom poll_interval function ([#403](https://github.com/hetznercloud/hcloud-python/issues/403)) ([93eb56b](https://github.com/hetznercloud/hcloud-python/commit/93eb56ba4d1a69e175398bca42e723a7e8e46371)) - make `Client.poll_interval` a private property ([#398](https://github.com/hetznercloud/hcloud-python/issues/398)) ([d5f24db](https://github.com/hetznercloud/hcloud-python/commit/d5f24db2816a0d00b8c7936e2a0290d2c4bb1e92)) - make `Client.request` `tries` a private argument ([#399](https://github.com/hetznercloud/hcloud-python/issues/399)) ([428ea7e](https://github.com/hetznercloud/hcloud-python/commit/428ea7e3be03a16114f875146971db59aabaac2c)) - move `hcloud.__version__.VERSION` to `hcloud.__version__` ([#397](https://github.com/hetznercloud/hcloud-python/issues/397)) ([4e3f638](https://github.com/hetznercloud/hcloud-python/commit/4e3f638862c9d260df98182c3f7858282049c26c)), closes [#234](https://github.com/hetznercloud/hcloud-python/issues/234) - remove deprecated `hcloud.hcloud` module ([#401](https://github.com/hetznercloud/hcloud-python/issues/401)) ([db37e63](https://github.com/hetznercloud/hcloud-python/commit/db37e633ebbf73354d3b2f4858cf3eebf173bfbc)) - remove deprecated `include_wildcard_architecture` argument in `IsosClient.get_list` and `IsosClient.get_all` ([#402](https://github.com/hetznercloud/hcloud-python/issues/402)) ([6b977e2](https://github.com/hetznercloud/hcloud-python/commit/6b977e2da5cec30110c32a91d572003e5b5c400a)) - return empty dict on empty responses in `Client.request` ([#400](https://github.com/hetznercloud/hcloud-python/issues/400)) ([9f46adb](https://github.com/hetznercloud/hcloud-python/commit/9f46adb946eb2770ee4f3a4e87cfc1c8b9b33c28)) - return full rebuild response in `Client.servers.rebuild` ([#406](https://github.com/hetznercloud/hcloud-python/issues/406)) ([1970d84](https://github.com/hetznercloud/hcloud-python/commit/1970d84bec2106c8c53d8e611b74d41eb5286e9b)) ### Bug Fixes - make `datacenter` argument optional when creating a primary ip ([#363](https://github.com/hetznercloud/hcloud-python/issues/363)) ([ebef774](https://github.com/hetznercloud/hcloud-python/commit/ebef77464c4c3b0ce33460cad2747e89d35047c7)) ### Dependencies - update dependency coverage to >=7.5,<7.6 ([#386](https://github.com/hetznercloud/hcloud-python/issues/386)) ([5660691](https://github.com/hetznercloud/hcloud-python/commit/5660691ebd6122fa7ebec56a24bce9fce0577573)) - update dependency mypy to >=1.10,<1.11 ([#387](https://github.com/hetznercloud/hcloud-python/issues/387)) ([35c933b](https://github.com/hetznercloud/hcloud-python/commit/35c933bd2108d42e74b74b01d6db74e159ec9142)) - update dependency myst-parser to v3 ([#385](https://github.com/hetznercloud/hcloud-python/issues/385)) ([9f18270](https://github.com/hetznercloud/hcloud-python/commit/9f182704898cb96f1ea162511605906f87cff50c)) - update dependency pylint to >=3,<3.3 ([#391](https://github.com/hetznercloud/hcloud-python/issues/391)) ([4a6f005](https://github.com/hetznercloud/hcloud-python/commit/4a6f005cb0488291ae91390a612bab6afc6d80b6)) - update dependency pytest to >=8,<8.3 ([#390](https://github.com/hetznercloud/hcloud-python/issues/390)) ([584a36b](https://github.com/hetznercloud/hcloud-python/commit/584a36b658670297ffffa9afa70835d29d27fbca)) - update dependency sphinx to >=7.3.4,<7.4 ([#383](https://github.com/hetznercloud/hcloud-python/issues/383)) ([69c2e16](https://github.com/hetznercloud/hcloud-python/commit/69c2e16073df9ef8520e3a635b3866403eba030e)) - update pre-commit hook asottile/pyupgrade to v3.16.0 ([0ce5fbc](https://github.com/hetznercloud/hcloud-python/commit/0ce5fbccba4a4255e08a37abf1f21ab9cc85f287)) - update pre-commit hook pre-commit/pre-commit-hooks to v4.6.0 ([5ef25ab](https://github.com/hetznercloud/hcloud-python/commit/5ef25ab3966d731c4c36ea3e785c2b5f20c69489)) - update pre-commit hook psf/black-pre-commit-mirror to v24.4.0 ([0941fbf](https://github.com/hetznercloud/hcloud-python/commit/0941fbfab20ca8a59e768c4a5e6fc101393c97f0)) - update pre-commit hook psf/black-pre-commit-mirror to v24.4.1 ([fec08c5](https://github.com/hetznercloud/hcloud-python/commit/fec08c5323359d0a4f0771123f483ff975aa68b0)) - update pre-commit hook psf/black-pre-commit-mirror to v24.4.2 ([#389](https://github.com/hetznercloud/hcloud-python/issues/389)) ([2b2e21f](https://github.com/hetznercloud/hcloud-python/commit/2b2e21f61366b5ec0f2ff5558f652d2bfed9d138)) - update pre-commit hook pycqa/flake8 to v7.1.0 ([3bc651d](https://github.com/hetznercloud/hcloud-python/commit/3bc651d50d85aa92ba76dbfeef1d604cabaa4628)) ### Documentation - add v2 upgrade notes ([#405](https://github.com/hetznercloud/hcloud-python/issues/405)) ([c77f771](https://github.com/hetznercloud/hcloud-python/commit/c77f771e2bed176acd6aa5011be006c800181809)) - cx11 is name, not an id ([#381](https://github.com/hetznercloud/hcloud-python/issues/381)) ([b745d40](https://github.com/hetznercloud/hcloud-python/commit/b745d4049f720b93d840a9204a99d246ecb499e5)) ## [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) ### 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) ### 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) ### 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) ### 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) ### 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) ### 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) ### 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) ### 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) ### 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) ### 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) ### 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) ### 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) ### 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) ### 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) ### 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) ### 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) ### 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) ### 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) ### 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) ### 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) ### 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) ### 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/stable/ ## 0.1.0 (2018-12-20) - First release on GitHub. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/CONTRIBUTING.rst0000644000175100017510000000515515152343177015204 0ustar00runnerrunner============ Contributing ============ Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. You can contribute in many ways: Types of Contributions ----------------------- Report Bugs ~~~~~~~~~~~~ Report bugs at https://github.com/hetznercloud/hcloud-python/issues. If you are reporting a bug, please include: * Your operating system name and version. * Any details about your local setup that might be helpful in troubleshooting. * Detailed steps to reproduce the bug. Fix Bugs ~~~~~~~~~ Look through the GitHub issues for bugs. Anything tagged with "bug" and "help wanted" is open to whoever wants to implement it. Implement Features ~~~~~~~~~~~~~~~~~~~ Look through the GitHub issues for features. Anything tagged with "enhancement" and "help wanted" is open to whoever wants to implement it. Write Documentation ~~~~~~~~~~~~~~~~~~~~ Hetzner Cloud Python could always use more documentation, whether as part of the official Hetzner Cloud Python docs, in docstrings, or even on the web in blog posts, articles, and such. Submit Feedback ~~~~~~~~~~~~~~~~ The best way to send feedback is to file an issue at https://github.com/hetznercloud/hcloud-python/issues. If you are proposing a feature: * Explain in detail how it would work. * Keep the scope as narrow as possible, to make it easier to implement. * Remember that this is a volunteer-driven project, and that contributions are welcome :) Get Started! ------------- Ready to contribute? Here's how to set up ``hcloud-python`` for local development. 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. 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. 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 6. Submit a pull request through the GitHub website. Pull Request Guidelines ------------------------ 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.md. 3. The pull request should work for all the versions of Python the library supports, and for PyPy. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/LICENSE0000644000175100017510000000206415152343177013544 0ustar00runnerrunnerMIT License Copyright (c) 2019, Hetzner Cloud GmbH Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/MANIFEST.in0000644000175100017510000000043315152343177014273 0ustar00runnerrunnerinclude CHANGELOG.md include CONTRIBUTING.rst include LICENSE include README.md include hcloud/py.typed recursive-include tests * recursive-exclude * __pycache__ recursive-exclude * *.py[co] recursive-include docs conf.py Makefile make.bat *.rst *.md *.jpg *.png *.gif *.js *.svg ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1624205 hcloud-2.17.0/PKG-INFO0000644000175100017510000001671715152343221013634 0ustar00runnerrunnerMetadata-Version: 2.4 Name: hcloud Version: 2.17.0 Summary: Official Hetzner Cloud python library Home-page: https://github.com/hetznercloud/hcloud-python Author: Hetzner Cloud GmbH Author-email: support-cloud@hetzner.com License: MIT Project-URL: Bug Tracker, https://github.com/hetznercloud/hcloud-python/issues Project-URL: Documentation, https://hcloud-python.readthedocs.io/en/stable/ Project-URL: Changelog, https://github.com/hetznercloud/hcloud-python/blob/main/CHANGELOG.md Project-URL: Source Code, https://github.com/hetznercloud/hcloud-python Keywords: hcloud hetzner cloud Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Requires-Python: >=3.10 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: python-dateutil>=2.7.5 Requires-Dist: requests>=2.20 Provides-Extra: docs Requires-Dist: sphinx<9.2,>=9; extra == "docs" Requires-Dist: sphinx-rtd-theme<3.2,>=3; extra == "docs" Requires-Dist: myst-parser<5.1,>=5; extra == "docs" Requires-Dist: watchdog<6.1,>=6; extra == "docs" Provides-Extra: test Requires-Dist: coverage<7.14,>=7.13; extra == "test" Requires-Dist: pylint<4.1,>=4; extra == "test" Requires-Dist: pytest<9.1,>=9; extra == "test" Requires-Dist: pytest-cov<7.1,>=7; extra == "test" Requires-Dist: mypy<1.20,>=1.19; extra == "test" Requires-Dist: types-python-dateutil; extra == "test" Requires-Dist: types-requests; extra == "test" Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: description Dynamic: description-content-type Dynamic: home-page Dynamic: keywords Dynamic: license Dynamic: license-file Dynamic: project-url Dynamic: provides-extra Dynamic: requires-dist Dynamic: requires-python Dynamic: summary # 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://codecov.io/github/hetznercloud/hcloud-python/graph/badge.svg?token=3YGRqB5t1L)](https://codecov.io/github/hetznercloud/hcloud-python/tree/main) [![](https://app.readthedocs.org/projects/hcloud-python/badge/?version=latest)](https://hcloud-python.readthedocs.io/en/stable/) [![](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/en/stable/), the public API documentation is available at [docs.hetzner.cloud](https://docs.hetzner.cloud). > [!IMPORTANT] > Make sure to follow our API changelog available at > [docs.hetzner.cloud/changelog](https://docs.hetzner.cloud/changelog) (or the RSS feed > available at > [docs.hetzner.cloud/changelog/feed.rss](https://docs.hetzner.cloud/changelog/feed.rss)) > to be notified about additions, deprecations and removals. ## 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 import Image from hcloud.server_types import ServerType client = Client( token="{YOUR_API_TOKEN}", # Please paste your API token here application_name="my-app", application_version="v1.0.0", ) # Create a server named my-server response = client.servers.create( name="my-server", server_type=ServerType(name="cx23"), 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=}") ``` - To upgrade the package, please read the [instructions available in the documentation](https://hcloud-python.readthedocs.io/en/stable/upgrading.html). - For more details on the API, 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). ## Experimental features Experimental features are published as part of our regular releases (e.g. a product public beta). During an experimental phase, breaking changes on those features may occur within minor releases. The stability of experimental features is not related to the stability of its upstream API. Experimental features have different levels of maturity (e.g. experimental, alpha, beta) based on the maturity of the upstream API. While experimental features will be announced in the release notes, you can also find whether a python class or function is experimental in its docstring: ``` Experimental: $PRODUCT is $MATURITY, breaking changes may occur within minor releases. See https://docs.hetzner.cloud/changelog#$SLUG for more details. ``` ## 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 ``` Lint the code: ```sh make lint ``` Run tests using the current `python3` version: ```sh make test ``` You may also run the tests for multiple `python3` versions using `tox`: ```sh 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, ) ``` ### Releasing experimental features To publish experimental features as part of regular releases: - an announcement, including a link to a changelog entry, must be added to the release notes. - an `Experimental` notice, including a link to a changelog entry, must be added to the python classes and functions that are experimental: ```py """ Experimental: $PRODUCT is $MATURITY, breaking changes may occur within minor releases. See https://docs.hetzner.cloud/changelog#$SLUG for more details. """ ``` ## License The MIT License (MIT). Please see [`License File`](https://github.com/hetznercloud/hcloud-python/blob/main/LICENSE) for more information. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/README.md0000644000175100017510000001266115152343177014022 0ustar00runnerrunner# 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://codecov.io/github/hetznercloud/hcloud-python/graph/badge.svg?token=3YGRqB5t1L)](https://codecov.io/github/hetznercloud/hcloud-python/tree/main) [![](https://app.readthedocs.org/projects/hcloud-python/badge/?version=latest)](https://hcloud-python.readthedocs.io/en/stable/) [![](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/en/stable/), the public API documentation is available at [docs.hetzner.cloud](https://docs.hetzner.cloud). > [!IMPORTANT] > Make sure to follow our API changelog available at > [docs.hetzner.cloud/changelog](https://docs.hetzner.cloud/changelog) (or the RSS feed > available at > [docs.hetzner.cloud/changelog/feed.rss](https://docs.hetzner.cloud/changelog/feed.rss)) > to be notified about additions, deprecations and removals. ## 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 import Image from hcloud.server_types import ServerType client = Client( token="{YOUR_API_TOKEN}", # Please paste your API token here application_name="my-app", application_version="v1.0.0", ) # Create a server named my-server response = client.servers.create( name="my-server", server_type=ServerType(name="cx23"), 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=}") ``` - To upgrade the package, please read the [instructions available in the documentation](https://hcloud-python.readthedocs.io/en/stable/upgrading.html). - For more details on the API, 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). ## Experimental features Experimental features are published as part of our regular releases (e.g. a product public beta). During an experimental phase, breaking changes on those features may occur within minor releases. The stability of experimental features is not related to the stability of its upstream API. Experimental features have different levels of maturity (e.g. experimental, alpha, beta) based on the maturity of the upstream API. While experimental features will be announced in the release notes, you can also find whether a python class or function is experimental in its docstring: ``` Experimental: $PRODUCT is $MATURITY, breaking changes may occur within minor releases. See https://docs.hetzner.cloud/changelog#$SLUG for more details. ``` ## 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 ``` Lint the code: ```sh make lint ``` Run tests using the current `python3` version: ```sh make test ``` You may also run the tests for multiple `python3` versions using `tox`: ```sh 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, ) ``` ### Releasing experimental features To publish experimental features as part of regular releases: - an announcement, including a link to a changelog entry, must be added to the release notes. - an `Experimental` notice, including a link to a changelog entry, must be added to the python classes and functions that are experimental: ```py """ Experimental: $PRODUCT is $MATURITY, breaking changes may occur within minor releases. See https://docs.hetzner.cloud/changelog#$SLUG for more details. """ ``` ## License The MIT License (MIT). Please see [`License File`](https://github.com/hetznercloud/hcloud-python/blob/main/LICENSE) for more information. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1772734097.129484 hcloud-2.17.0/docs/0000755000175100017510000000000015152343221013453 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/Makefile0000644000175100017510000000117215152343177015126 0ustar00runnerrunner# Minimal makefile for Sphinx documentation # # 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 # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1298509 hcloud-2.17.0/docs/_static/0000755000175100017510000000000015152343221015101 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/_static/favicon.png0000644000175100017510000000126015152343177017245 0ustar00runnerrunnerPNG  IHDR DgAMA a cHRMz&u0`:pQ<PLTE - *)* ,0F_bwTk86Nf]sC\?Ylez>XSkꇗ遒ꅖ'&f{h}UlWn7Ri`vE^bKGD ,tIME WIDAT8͒I0 E; M-sT Element 1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.actions.rst0000644000175100017510000000050015152343177020062 0ustar00runnerrunnerActionsClient ================== .. autoclass:: hcloud.actions.client.ResourceActionsClient :members: .. autoclass:: hcloud.actions.client.ActionsClient :members: :inherited-members: .. autoclass:: hcloud.actions.client.BoundAction :members: .. autoclass:: hcloud.actions.domain.Action :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.certificates.rst0000644000175100017510000000040115152343177021067 0ustar00runnerrunnerCertificateClient ================== .. autoclass:: hcloud.certificates.client.CertificatesClient :members: .. autoclass:: hcloud.certificates.client.BoundCertificate :members: .. autoclass:: hcloud.certificates.domain.Certificate :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.datacenters.rst0000644000175100017510000000051115152343177020721 0ustar00runnerrunnerDatacentersClient ================== .. autoclass:: hcloud.datacenters.client.DatacentersClient :members: .. autoclass:: hcloud.datacenters.client.BoundDatacenter :members: .. autoclass:: hcloud.datacenters.domain.Datacenter :members: .. autoclass:: hcloud.datacenters.domain.DatacenterServerTypes :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.firewalls.rst0000644000175100017510000000070415152343177020420 0ustar00runnerrunnerFirewallsClient ================== .. autoclass:: hcloud.firewalls.client.FirewallsClient :members: .. autoclass:: hcloud.firewalls.client.BoundFirewall :members: .. autoclass:: hcloud.firewalls.domain.Firewall :members: .. autoclass:: hcloud.firewalls.domain.FirewallRule :members: .. autoclass:: hcloud.firewalls.domain.FirewallResource :members: .. autoclass:: hcloud.firewalls.domain.CreateFirewallResponse :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.floating_ips.rst0000644000175100017510000000052115152343177021103 0ustar00runnerrunnerFloating IPsClient ================== .. autoclass:: hcloud.floating_ips.client.FloatingIPsClient :members: .. autoclass:: hcloud.floating_ips.client.BoundFloatingIP :members: .. autoclass:: hcloud.floating_ips.domain.FloatingIP :members: .. autoclass:: hcloud.floating_ips.domain.CreateFloatingIPResponse :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.images.rst0000644000175100017510000000043715152343177017700 0ustar00runnerrunnerImagesClient ================== .. autoclass:: hcloud.images.client.ImagesClient :members: .. autoclass:: hcloud.images.client.BoundImage :members: .. autoclass:: hcloud.images.domain.Image :members: .. autoclass:: hcloud.images.domain.CreateImageResponse :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.isos.rst0000644000175100017510000000031215152343177017400 0ustar00runnerrunnerISOsClient ================== .. autoclass:: hcloud.isos.client.IsosClient :members: .. autoclass:: hcloud.isos.client.BoundIso :members: .. autoclass:: hcloud.isos.domain.Iso :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.load_balancer_types.rst0000644000175100017510000000033315152343177022420 0ustar00runnerrunnerLoadBalancerTypesClient ======================== .. autoclass:: hcloud.load_balancer_types.client.LoadBalancerTypesClient :members: .. autoclass:: hcloud.load_balancer_types.domain.LoadBalancerType :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.load_balancers.rst0000644000175100017510000000177315152343177021370 0ustar00runnerrunnerLoadBalancerClient ================== .. autoclass:: hcloud.load_balancers.client.LoadBalancersClient :members: .. autoclass:: hcloud.load_balancers.client.BoundLoadBalancer :members: .. autoclass:: hcloud.load_balancers.domain.LoadBalancer :members: .. autoclass:: hcloud.load_balancers.domain.LoadBalancerService :members: .. autoclass:: hcloud.load_balancers.domain.LoadBalancerServiceHttp :members: .. autoclass:: hcloud.load_balancers.domain.LoadBalancerHealthCheck :members: .. autoclass:: hcloud.load_balancers.domain.LoadBalancerHealthCheckHttp :members: .. autoclass:: hcloud.load_balancers.domain.LoadBalancerTarget :members: .. autoclass:: hcloud.load_balancers.domain.LoadBalancerTargetHealthStatus :members: .. autoclass:: hcloud.load_balancers.domain.LoadBalancerTargetLabelSelector :members: .. autoclass:: hcloud.load_balancers.domain.LoadBalancerTargetIP :members: .. autoclass:: hcloud.load_balancers.domain.LoadBalancerAlgorithm :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.locations.rst0000644000175100017510000000035515152343177020425 0ustar00runnerrunnerLocationsClient ================== .. autoclass:: hcloud.locations.client.LocationsClient :members: .. autoclass:: hcloud.locations.client.BoundLocation :members: .. autoclass:: hcloud.locations.domain.Location :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.networks.rst0000644000175100017510000000066615152343177020313 0ustar00runnerrunnerNetworksClient ================== .. autoclass:: hcloud.networks.client.NetworksClient :members: .. autoclass:: hcloud.networks.client.BoundNetwork :members: .. autoclass:: hcloud.networks.domain.Network :members: .. autoclass:: hcloud.networks.domain.NetworkSubnet :members: .. autoclass:: hcloud.networks.domain.NetworkRoute :members: .. autoclass:: hcloud.networks.domain.CreateNetworkResponse :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.placement_groups.rst0000644000175100017510000000056715152343177022006 0ustar00runnerrunnerPlacementGroupsClient ===================== .. autoclass:: hcloud.placement_groups.client.PlacementGroupsClient :members: .. autoclass:: hcloud.placement_groups.client.BoundPlacementGroup :members: .. autoclass:: hcloud.placement_groups.domain.PlacementGroup :members: .. autoclass:: hcloud.placement_groups.domain.CreatePlacementGroupResponse :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.primary_ips.rst0000644000175100017510000000036715152343177020773 0ustar00runnerrunnerPrimaryIPsClient ================== .. autoclass:: hcloud.primary_ips.client.PrimaryIPsClient :members: .. autoclass:: hcloud.primary_ips.client.BoundPrimaryIP :members: .. autoclass:: hcloud.primary_ips.domain.PrimaryIP :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.server_types.rst0000644000175100017510000000037515152343177021166 0ustar00runnerrunnerServerTypesClient ================== .. autoclass:: hcloud.server_types.client.ServerTypesClient :members: .. autoclass:: hcloud.server_types.client.BoundServerType :members: .. autoclass:: hcloud.server_types.domain.ServerType :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.servers.rst0000644000175100017510000000142615152343177020123 0ustar00runnerrunnerServersClient ================== .. autoclass:: hcloud.servers.client.ServersClient :members: .. autoclass:: hcloud.servers.client.BoundServer :members: .. autoclass:: hcloud.servers.domain.Server :members: .. autoclass:: hcloud.servers.domain.PublicNetwork :members: .. autoclass:: hcloud.servers.domain.IPv4Address :members: .. autoclass:: hcloud.servers.domain.IPv6Network :members: .. autoclass:: hcloud.servers.domain.CreateServerResponse :members: .. autoclass:: hcloud.servers.domain.ServerCreatePublicNetwork :members: .. autoclass:: hcloud.servers.domain.ResetPasswordResponse :members: .. autoclass:: hcloud.servers.domain.EnableRescueResponse :members: .. autoclass:: hcloud.servers.domain.RequestConsoleResponse :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.ssh_keys.rst0000644000175100017510000000034215152343177020256 0ustar00runnerrunnerSSHKeysClient ================== .. autoclass:: hcloud.ssh_keys.client.SSHKeysClient :members: .. autoclass:: hcloud.ssh_keys.client.BoundSSHKey :members: .. autoclass:: hcloud.ssh_keys.domain.SSHKey :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.storage_box_types.rst0000644000175100017510000000056715152343177022177 0ustar00runnerrunnerStorageBoxTypesClient ===================== .. autoclass:: hcloud.storage_box_types.client.StorageBoxTypesClient :members: .. autoclass:: hcloud.storage_box_types.client.StorageBoxTypesPageResult :members: .. autoclass:: hcloud.storage_box_types.client.BoundStorageBoxType :members: .. autoclass:: hcloud.storage_box_types.domain.StorageBoxType :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.storage_boxes.rst0000644000175100017510000000364015152343177021276 0ustar00runnerrunnerStorageBoxesClient ===================== .. autoclass:: hcloud.storage_boxes.client.StorageBoxesClient :members: .. autoclass:: hcloud.storage_boxes.client.StorageBoxesPageResult :members: .. autoclass:: hcloud.storage_boxes.client.StorageBoxSnapshotsPageResult :members: .. autoclass:: hcloud.storage_boxes.client.StorageBoxSubaccountsPageResult :members: .. autoclass:: hcloud.storage_boxes.client.BoundStorageBox :members: .. autoclass:: hcloud.storage_boxes.client.BoundStorageBoxSnapshot :members: .. autoclass:: hcloud.storage_boxes.client.BoundStorageBoxSubaccount :members: .. autoclass:: hcloud.storage_boxes.domain.StorageBox :members: .. autoclass:: hcloud.storage_boxes.domain.StorageBoxAccessSettings :members: .. autoclass:: hcloud.storage_boxes.domain.StorageBoxSnapshotPlan :members: .. autoclass:: hcloud.storage_boxes.domain.StorageBoxStats :members: .. autoclass:: hcloud.storage_boxes.domain.StorageBoxStatus :members: .. autoclass:: hcloud.storage_boxes.domain.StorageBoxSnapshot :members: .. autoclass:: hcloud.storage_boxes.domain.StorageBoxSnapshotStats :members: .. autoclass:: hcloud.storage_boxes.domain.StorageBoxSubaccount :members: .. autoclass:: hcloud.storage_boxes.domain.StorageBoxSubaccountAccessSettings :members: .. autoclass:: hcloud.storage_boxes.domain.CreateStorageBoxResponse :members: .. autoclass:: hcloud.storage_boxes.domain.CreateStorageBoxSnapshotResponse :members: .. autoclass:: hcloud.storage_boxes.domain.CreateStorageBoxSubaccountResponse :members: .. autoclass:: hcloud.storage_boxes.domain.StorageBoxFoldersResponse :members: .. autoclass:: hcloud.storage_boxes.domain.DeleteStorageBoxResponse :members: .. autoclass:: hcloud.storage_boxes.domain.DeleteStorageBoxSnapshotResponse :members: .. autoclass:: hcloud.storage_boxes.domain.DeleteStorageBoxSubaccountResponse :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.volumes.rst0000644000175100017510000000045015152343177020120 0ustar00runnerrunnerVolumesClient ================== .. autoclass:: hcloud.volumes.client.VolumesClient :members: .. autoclass:: hcloud.volumes.client.BoundVolume :members: .. autoclass:: hcloud.volumes.domain.Volume :members: .. autoclass:: hcloud.volumes.domain.CreateVolumeResponse :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.clients.zones.rst0000644000175100017510000000114715152343177017570 0ustar00runnerrunnerZonesClient ================== .. autoclass:: hcloud.zones.client.ZonesClient :members: .. autoclass:: hcloud.zones.client.BoundZone :members: .. autoclass:: hcloud.zones.client.BoundZoneRRSet :members: .. autoclass:: hcloud.zones.domain.Zone :members: .. autoclass:: hcloud.zones.domain.ZoneAuthoritativeNameservers :members: .. autoclass:: hcloud.zones.domain.ZonePrimaryNameserver :members: .. autoclass:: hcloud.zones.domain.ZoneRecord :members: .. autoclass:: hcloud.zones.domain.ZoneRRSet :members: .. autoclass:: hcloud.zones.domain.CreateZoneResponse :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.deprecation.rst0000644000175100017510000000015415152343177017264 0ustar00runnerrunnerDeprecation Info ================== .. autoclass:: hcloud.deprecation.domain.DeprecationInfo :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.helpers.rst0000644000175100017510000000013715152343177016432 0ustar00runnerrunnerHelpers ================== .. autoclass:: hcloud.helpers.labels.LabelValidator :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/api.rst0000644000175100017510000000116515152343177014773 0ustar00runnerrunnerAPI References ================== Main Interface --------------- .. autoclass:: hcloud.Client :members: API Clients ------------- .. toctree:: :maxdepth: 3 :glob: api.clients.* Exceptions --------------- .. autoclass:: hcloud.HCloudException :members: .. autoclass:: hcloud.APIException :members: .. autoclass:: hcloud.actions.domain.ActionException :members: .. autoclass:: hcloud.actions.domain.ActionFailedException :members: .. autoclass:: hcloud.actions.domain.ActionTimeoutException :members: Other ------------- .. toctree:: :maxdepth: 3 api.helpers api.deprecation ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/changelog.md0000644000175100017510000000004115152343177015731 0ustar00runnerrunner:::{include} ../CHANGELOG.md ::: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/conf.py0000644000175100017510000000366615152343177014777 0ustar00runnerrunnerfrom __future__ import annotations import os import sys from datetime import datetime sys.path.insert(0, os.path.abspath("..")) import hcloud # noqa # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = "Hetzner Cloud Python" author = "Hetzner Cloud GmbH" copyright = f"{datetime.now().year}, {author}" version = hcloud.__version__ release = hcloud.__version__ # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = ["myst_parser", "sphinx.ext.autodoc", "sphinx.ext.viewcode"] templates_path = ["_templates"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] source_suffix = { ".rst": "restructuredtext", ".md": "markdown", } # 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 # 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 html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] 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": "#fff", } html_css_files = [ "custom.css", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/contributing.rst0000644000175100017510000000004115152343177016721 0ustar00runnerrunner.. include:: ../CONTRIBUTING.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/index.md0000644000175100017510000000031615152343177015116 0ustar00runnerrunner:::{toctree} :maxdepth: 4 :hidden: self installation.rst api.rst Hetzner Cloud API Documentation contributing.rst upgrading.md changelog.md ::: :::{include} ../README.md ::: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/installation.rst0000644000175100017510000000250715152343177016724 0ustar00runnerrunner.. highlight:: shell ============ Installation ============ Stable release -------------- To install Hetzner Cloud Python, run this command in your terminal: .. code-block:: console $ pip install hcloud This is the preferred method to install Hetzner Cloud Python, as it will always install the most recent stable release. If you don't have `pip`_ installed, this `Python installation guide`_ can guide you through the process. .. _pip: https://pip.pypa.io .. _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 ------------ The sources for Hetzner Cloud Python can be downloaded from the Github repo. You can either clone the public repository: .. code-block:: console $ git clone git://github.com/hetznercloud/hcloud-python Or download the tarball: .. code-block:: console $ 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 $ pip install . ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/make.bat0000644000175100017510000000137515152343177015100 0ustar00runnerrunner@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. 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.https://www.sphinx-doc.org/ exit /b 1 ) if "%1" == "" goto help %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/docs/upgrading.md0000644000175100017510000000630615152343177015774 0ustar00runnerrunner# Upgrading This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Before upgrading, make sure to resolve any deprecation warnings. ## Upgrading to v2 - [#397](https://github.com/hetznercloud/hcloud-python/pull/397): The package version was moved from `hcloud.__version__.VERSION` to `hcloud.__version__`, make sure to update your import paths: ```diff -from hcloud.__version__ import VERSION +from hcloud import __version__ as VERSION ``` - [#401](https://github.com/hetznercloud/hcloud-python/pull/401): The deprecated `hcloud.hcloud` module was removed, make sure to update your import paths: ```diff -from hcloud.hcloud import Client +from hcloud import Client ``` - [#398](https://github.com/hetznercloud/hcloud-python/pull/398): The [`Client.poll_interval`](https://hcloud-python.readthedocs.io/en/stable/api.html#hcloud.Client) property is now private, make sure to configure it while creating the [`Client`](https://hcloud-python.readthedocs.io/en/stable/api.html#hcloud.Client): ```diff -client = Client(token=token) -client.poll_interval = 2 +client = Client( + token=token, + poll_interval=2, +) ``` - [#400](https://github.com/hetznercloud/hcloud-python/pull/400): The [`Client.request`](https://hcloud-python.readthedocs.io/en/stable/api.html#hcloud.Client.request) method now returns an empty dict instead of an empty string when the API response is empty: ```diff response = client.request(method="DELETE", url="/primary_ips/123456") -assert response == "" +assert response == {} ``` - [#402](https://github.com/hetznercloud/hcloud-python/pull/402): In the [`Client.isos.get_list`](https://hcloud-python.readthedocs.io/en/stable/api.clients.isos.html#hcloud.isos.client.IsosClient.get_list) and [`Client.isos.get_all`](https://hcloud-python.readthedocs.io/en/stable/api.clients.isos.html#hcloud.isos.client.IsosClient.get_all) methods, the deprecated `include_wildcard_architecture` argument was removed, make sure to use the `include_architecture_wildcard` argument instead: ```diff client.isos.get_all( - include_wildcard_architecture=True, + include_architecture_wildcard=True, ) ``` - [#363](https://github.com/hetznercloud/hcloud-python/pull/363): In the [`Client.primary_ips.create`](https://hcloud-python.readthedocs.io/en/stable/api.clients.primary_ips.html#hcloud.primary_ips.client.PrimaryIPsClient.create) method, the `datacenter` argument was moved after `name` argument and is now optional: ```diff client.primary_ips.create( "ipv4", - None, "my-ip", assignee_id=12345, ) ``` ```diff client.primary_ips.create( "ipv4", - Datacenter(name="fsn1-dc14"), "my-ip", + datacenter=Datacenter(name="fsn1-dc14"), ) ``` - [#406](https://github.com/hetznercloud/hcloud-python/pull/406): In the [`Client.servers.rebuild`](https://hcloud-python.readthedocs.io/en/stable/api.clients.servers.html#hcloud.servers.client.ServersClient.rebuild) method, the single action return value was deprecated and is now removed. The method now returns a full response wrapping the action and an optional root password: ```diff -action = client.servers.rebuild(server, image) +resp = client.servers.rebuild(server, image) +action = resp.action +root_password = resp.root_password ``` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1310012 hcloud-2.17.0/hcloud/0000755000175100017510000000000015152343221014001 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/__init__.py0000644000175100017510000000060315152343177016123 0ustar00runnerrunnerfrom __future__ import annotations from ._client import ( Client, constant_backoff_function, exponential_backoff_function, ) from ._exceptions import APIException, HCloudException from ._version import __version__ __all__ = [ "__version__", "Client", "constant_backoff_function", "exponential_backoff_function", "APIException", "HCloudException", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/_client.py0000644000175100017510000003362015152343177016006 0ustar00runnerrunnerfrom __future__ import annotations import time from http import HTTPStatus from random import uniform from typing import Any, Protocol import requests from ._exceptions import APIException from ._version import __version__ 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 .storage_box_types import StorageBoxTypesClient from .storage_boxes import StorageBoxesClient from .volumes import VolumesClient from .zones import ZonesClient class BackoffFunction(Protocol): def __call__(self, retries: int) -> float: """ Return a interval in seconds to wait between each API call. :param retries: Number of calls already made. """ def constant_backoff_function(interval: float) -> BackoffFunction: """ Return a backoff function, implementing a constant backoff. :param interval: Constant interval to return. """ # pylint: disable=unused-argument def func(retries: int) -> float: return interval return func def exponential_backoff_function( *, base: float, multiplier: int, cap: float, jitter: bool = False, ) -> BackoffFunction: """ Return a backoff function, implementing a truncated exponential backoff with optional full jitter. :param base: Base for the exponential backoff algorithm. :param multiplier: Multiplier for the exponential backoff algorithm. :param cap: Value at which the interval is truncated. :param jitter: Whether to add jitter. """ def func(retries: int) -> float: interval: float = base * multiplier**retries # Exponential backoff interval = min(cap, interval) # Cap backoff if jitter: interval = uniform(base, interval) # Add jitter return interval return func def _build_user_agent( application_name: str | None, application_version: str | None, ) -> str: """Build the user agent of the hcloud-python instance with the user application name (if specified) :return: The user agent of this hcloud-python instance """ parts = [] for name, version in [ (application_name, application_version), ("hcloud-python", __version__), ]: if name is not None: parts.append(name if version is None else f"{name}/{version}") return " ".join(parts) class Client: """ Client for the Hetzner Cloud API. The Hetzner Cloud API reference is available at https://docs.hetzner.cloud. Make sure to follow our API changelog available at https://docs.hetzner.cloud/changelog (or the RRS feed available at https://docs.hetzner.cloud/changelog/feed.rss) to be notified about additions, deprecations and removals. **Retry mechanism** The :attr:`Client.request` method will retry failed requests that match certain criteria. The default retry interval is defined by an exponential backoff algorithm truncated to 60s with jitter. The default maximal number of retries is 5. The following rules define when a request can be retried: - When the client returned a network timeout error. - When the API returned an HTTP error, with the status code: - ``502`` Bad Gateway - ``504`` Gateway Timeout - When the API returned an application error, with the code: - ``conflict`` - ``rate_limit_exceeded`` - ``timeout`` Changes to the retry policy might occur between releases, and will not be considered breaking changes. """ def __init__( self, token: str, api_endpoint: str = "https://api.hetzner.cloud/v1", application_name: str | None = None, application_version: str | None = None, poll_interval: int | float | BackoffFunction = 1.0, poll_max_retries: int = 120, timeout: float | tuple[float, float] | None = None, *, api_endpoint_hetzner: str = "https://api.hetzner.com/v1", ): """Create a new Client instance :param token: Hetzner Cloud API token :param api_endpoint: Hetzner Cloud API endpoint :param api_endpoint_hetzner: Hetzner API endpoint. :param application_name: Your application name :param application_version: Your application _version :param poll_interval: Interval in seconds to use when polling actions from the API. You may pass a function to compute a custom poll interval. :param poll_max_retries: Max retries before timeout when polling actions from the API. :param timeout: Requests timeout in seconds """ self._client = ClientBase( token=token, endpoint=api_endpoint, application_name=application_name, application_version=application_version, poll_interval=poll_interval, poll_max_retries=poll_max_retries, timeout=timeout, ) self._client_hetzner = ClientBase( token=token, endpoint=api_endpoint_hetzner, application_name=application_name, application_version=application_version, poll_interval=poll_interval, poll_max_retries=poll_max_retries, timeout=timeout, ) self.datacenters = DatacentersClient(self) """DatacentersClient Instance :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 ` """ self.zones = ZonesClient(self) """ZonesClient Instance :type: :class:`ZonesClient ` """ self.storage_box_types = StorageBoxTypesClient(self) """StorageBoxTypesClient Instance :type: :class:`StorageBoxTypesClient ` """ self.storage_boxes = StorageBoxesClient(self) """StorageBoxesClient Instance :type: :class:`StorageBoxesClient ` """ def request( # type: ignore[no-untyped-def] self, method: str, url: str, **kwargs, ) -> dict[str, Any]: """Perform a request to the Hetzner Cloud API. :param method: Method to perform the request. :param url: URL to perform the request. :param timeout: Requests timeout in seconds. """ return self._client.request(method, url, **kwargs) class ClientBase: def __init__( self, token: str, *, endpoint: str, application_name: str | None = None, application_version: str | None = None, poll_interval: int | float | BackoffFunction = 1.0, poll_max_retries: int = 120, timeout: float | tuple[float, float] | None = None, ): self._token = token self._endpoint = endpoint self._user_agent = _build_user_agent(application_name, application_version) self._headers = { "User-Agent": self._user_agent, "Authorization": f"Bearer {self._token}", "Accept": "application/json", } if isinstance(poll_interval, (int, float)): poll_interval_func = constant_backoff_function(poll_interval) else: poll_interval_func = poll_interval self._poll_interval_func = poll_interval_func self._poll_max_retries = poll_max_retries self._retry_interval_func = exponential_backoff_function( base=1.0, multiplier=2, cap=60.0, jitter=True ) self._retry_max_retries = 5 self._timeout = timeout self._session = requests.Session() def request( # type: ignore[no-untyped-def] self, method: str, url: str, **kwargs, ) -> dict[str, Any]: """Perform a request to the provided URL. :param method: Method to perform the request. :param url: URL to perform the request. :param timeout: Requests timeout in seconds. :return: Response """ kwargs.setdefault("timeout", self._timeout) url = self._endpoint + url headers = self._headers retries = 0 while True: try: response = self._session.request( method=method, url=url, headers=headers, **kwargs, ) return self._read_response(response) except APIException as exception: if retries < self._retry_max_retries and self._retry_policy(exception): time.sleep(self._retry_interval_func(retries)) retries += 1 continue raise except requests.exceptions.Timeout: if retries < self._retry_max_retries: time.sleep(self._retry_interval_func(retries)) retries += 1 continue raise def _read_response(self, response: requests.Response) -> dict[str, Any]: correlation_id = response.headers.get("X-Correlation-Id") payload = {} try: if len(response.content) > 0: payload = response.json() except (TypeError, ValueError) as exc: raise APIException( code=response.status_code, message=response.reason, details={"content": response.content}, correlation_id=correlation_id, ) from exc if not response.ok: if not payload or "error" not in payload: raise APIException( code=response.status_code, message=response.reason, details={"content": response.content}, correlation_id=correlation_id, ) error: dict[str, Any] = payload["error"] raise APIException( code=error["code"], message=error["message"], details=error.get("details"), correlation_id=correlation_id, ) return payload def _retry_policy(self, exception: APIException) -> bool: if isinstance(exception.code, str): return exception.code in ( "rate_limit_exceeded", "conflict", "timeout", ) if isinstance(exception.code, int): return exception.code in ( HTTPStatus.BAD_GATEWAY, HTTPStatus.GATEWAY_TIMEOUT, ) return False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/_exceptions.py0000644000175100017510000000154215152343177016707 0ustar00runnerrunnerfrom __future__ import annotations from typing import Any class HCloudException(Exception): """There was an error while using the hcloud library. All exceptions in the hcloud library inherit from this exception. It may be used as catch-all exception. """ class APIException(HCloudException): """There was an error while performing an API Request.""" def __init__( self, code: int | str, message: str, details: Any, *, correlation_id: str | None = None, ): extras = [str(code)] if correlation_id is not None: extras.append(correlation_id) error = f"{message} ({', '.join(extras)})" super().__init__(error) self.code = code self.message = message self.details = details self.correlation_id = correlation_id ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/_version.py0000644000175100017510000000013115152343177016204 0ustar00runnerrunnerfrom __future__ import annotations __version__ = "2.17.0" # x-releaser-pleaser-version ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1326985 hcloud-2.17.0/hcloud/actions/0000755000175100017510000000000015152343221015441 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/actions/__init__.py0000644000175100017510000000114515152343177017565 0ustar00runnerrunnerfrom __future__ import annotations from .client import ( ActionsClient, ActionSort, ActionsPageResult, BoundAction, ResourceActionsClient, ) from .domain import ( Action, ActionError, ActionException, ActionFailedException, ActionResource, ActionStatus, ActionTimeoutException, ) __all__ = [ "ActionsClient", "ActionsPageResult", "BoundAction", "ResourceActionsClient", "ActionSort", "ActionStatus", "Action", "ActionResource", "ActionError", "ActionException", "ActionFailedException", "ActionTimeoutException", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/actions/client.py0000644000175100017510000001622115152343177017305 0ustar00runnerrunnerfrom __future__ import annotations import time import warnings from typing import TYPE_CHECKING, Any, Literal, NamedTuple from ..core import BoundModelBase, Meta, ResourceClientBase from .domain import Action, ActionFailedException, ActionStatus, ActionTimeoutException if TYPE_CHECKING: from .._client import Client __all__ = [ "ActionsClient", "ActionsPageResult", "BoundAction", "ResourceActionsClient", "ActionSort", ] class BoundAction(BoundModelBase[Action], Action): _client: ActionsClient model = Action def wait_until_finished(self, max_retries: int | None = None) -> None: """Wait until the specific action has status=finished. :param max_retries: int Specify how many retries will be performed before an ActionTimeoutException will be raised. :raises: ActionFailedException when action is finished with status==error :raises: ActionTimeoutException when Action is still in status==running after max_retries is reached. """ if max_retries is None: # pylint: disable=protected-access max_retries = self._client._client._poll_max_retries retries = 0 while True: self.reload() if self.status != Action.STATUS_RUNNING: break retries += 1 if retries < max_retries: # pylint: disable=protected-access time.sleep(self._client._client._poll_interval_func(retries)) continue raise ActionTimeoutException(action=self) if self.status == Action.STATUS_ERROR: raise ActionFailedException(action=self) ActionSort = Literal[ "id", "id:asc", "id:desc", "command", "command:asc", "command:desc", "status", "status:asc", "status:desc", "started", "started:asc", "started:desc", "finished", "finished:asc", "finished:desc", ] class ActionsPageResult(NamedTuple): actions: list[BoundAction] meta: Meta class ResourceClientBaseActionsMixin(ResourceClientBase): def _get_action_by_id( self, base_url: str, id: int, ) -> BoundAction: response = self._client.request( method="GET", url=f"{base_url}/actions/{id}", ) return BoundAction( client=self._parent.actions, data=response["action"], ) def _get_actions_list( self, base_url: str, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: params: dict[str, Any] = {} if status is not None: params["status"] = status if sort is not None: params["sort"] = sort if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request( method="GET", url=f"{base_url}/actions", params=params, ) return ActionsPageResult( actions=[BoundAction(self._parent.actions, o) for o in response["actions"]], meta=Meta.parse_meta(response), ) class ResourceActionsClient( ResourceClientBaseActionsMixin, ResourceClientBase, ): _resource: str def __init__(self, client: ResourceClientBase | Client, resource: str | None): if isinstance(client, ResourceClientBase): super().__init__(client._parent) # Use the same base client as the the resource base client. Allows us to # choose the base client outside of the ResourceActionsClient. self._client = client._client else: # Backward compatibility, defaults to the parent ("top level") base client (`_client`). super().__init__(client) self._resource = resource or "" def get_by_id(self, id: int) -> BoundAction: """ Returns a specific Action by its ID. :param id: ID of the Action. """ return self._get_action_by_id(self._resource, id) def get_list( self, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._get_actions_list( self._resource, status=status, sort=sort, page=page, per_page=per_page, ) def get_all( self, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ 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[ActionStatus] | None = None, sort: list[ActionSort] | 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[ActionStatus] | None = None, sort: list[ActionSort] | 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/actions/domain.py0000644000175100017510000000607515152343177017304 0ustar00runnerrunnerfrom __future__ import annotations from typing import TYPE_CHECKING, Any, Literal, TypedDict from .._exceptions import HCloudException from ..core import BaseDomain if TYPE_CHECKING: from .client import BoundAction __all__ = [ "ActionStatus", "Action", "ActionResource", "ActionError", "ActionException", "ActionFailedException", "ActionTimeoutException", ] ActionStatus = Literal[ "running", "success", "error", ] class Action(BaseDomain): """Action Domain :param id: int ID of an action :param command: Command executed in the action :param status: Status of the action :param progress: Progress of action in percent :param started: Point in time when the action was started :param datetime,None finished: Point in time when the action was finished. Only set if the action is finished otherwise None :param resources: Resources the action relates to :param error: Error message for the action if error occurred, otherwise None. """ STATUS_RUNNING = "running" """Action Status running""" STATUS_SUCCESS = "success" """Action Status success""" STATUS_ERROR = "error" """Action Status error""" __api_properties__ = ( "id", "command", "status", "progress", "resources", "error", "started", "finished", ) __slots__ = __api_properties__ def __init__( self, id: int, command: str | None = None, status: ActionStatus | None = None, progress: int | None = None, started: str | None = None, finished: str | None = None, resources: list[ActionResource] | None = None, error: ActionError | None = None, ): self.id = id self.command = command self.status = status self.progress = progress self.started = self._parse_datetime(started) self.finished = self._parse_datetime(finished) self.resources = resources 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""" def __init__(self, action: Action | BoundAction): assert self.__doc__ is not None message = self.__doc__ extras = [] if ( action.error is not None and "code" in action.error and "message" in action.error ): message += f": {action.error['message']}" extras.append(action.error["code"]) else: if action.command is not None: extras.append(action.command) extras.append(str(action.id)) message += f" ({', '.join(extras)})" super().__init__(message) self.message = message self.action = action class ActionFailedException(ActionException): """The pending action failed""" class ActionTimeoutException(ActionException): """The pending action timed out""" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1772734097.133267 hcloud-2.17.0/hcloud/certificates/0000755000175100017510000000000015152343221016446 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/certificates/__init__.py0000644000175100017510000000075115152343177020574 0ustar00runnerrunnerfrom __future__ import annotations from .client import ( BoundCertificate, CertificatesClient, CertificatesPageResult, ) from .domain import ( Certificate, CreateManagedCertificateResponse, ManagedCertificateError, ManagedCertificateStatus, ) __all__ = [ "BoundCertificate", "Certificate", "CertificatesClient", "CertificatesPageResult", "CreateManagedCertificateResponse", "ManagedCertificateError", "ManagedCertificateStatus", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/certificates/client.py0000644000175100017510000003217315152343177020316 0ustar00runnerrunnerfrom __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ( ActionSort, ActionsPageResult, ActionStatus, BoundAction, ResourceActionsClient, ) from ..actions.client import ResourceClientBaseActionsMixin from ..core import BoundModelBase, Meta, ResourceClientBase from .domain import ( Certificate, CreateManagedCertificateResponse, ManagedCertificateError, ManagedCertificateStatus, ) if TYPE_CHECKING: from .._client import Client __all__ = [ "BoundCertificate", "CertificatesPageResult", "CertificatesClient", ] class BoundCertificate(BoundModelBase[Certificate], Certificate): _client: CertificatesClient model = Certificate 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") error = None if error_data: error = ManagedCertificateError( code=error_data["code"], message=error_data["message"] ) data["status"] = ManagedCertificateStatus( issuance=status["issuance"], renewal=status["renewal"], error=error ) super().__init__(client, data, complete) def get_actions_list( self, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Certificate. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._client.get_actions_list( self, status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Certificate. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ return self._client.get_actions( self, status=status, sort=sort, ) 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) New name to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundCertificate ` """ return self._client.update( self, name=name, labels=labels, ) def delete(self) -> bool: """Deletes a certificate. :return: boolean """ return self._client.delete(self) def retry_issuance(self) -> BoundAction: """Retry a failed Certificate issuance or renewal. :return: BoundAction """ return self._client.retry_issuance(self) class CertificatesPageResult(NamedTuple): certificates: list[BoundCertificate] meta: Meta class CertificatesClient( ResourceClientBaseActionsMixin, ResourceClientBase, ): _base_url = "/certificates" actions: ResourceActionsClient """Certificates scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self.actions = ResourceActionsClient(client, self._base_url) def get_by_id(self, id: int) -> BoundCertificate: """Get a specific certificate by its ID. :param id: int :return: :class:`BoundCertificate ` """ response = self._client.request(url=f"{self._base_url}/{id}", method="GET") return BoundCertificate(self, response["certificate"]) def get_list( self, 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) Can be used to filter certificates by their name. :param label_selector: str (optional) Can be used to filter certificates by labels. The response will only contain certificates matching the label selector. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundCertificate `], :class:`Meta `) """ params: dict[str, Any] = {} if name is not None: params["name"] = name 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 response = self._client.request(url=self._base_url, method="GET", params=params) certificates = [ BoundCertificate(self, certificate_data) for certificate_data in response["certificates"] ] return CertificatesPageResult(certificates, Meta.parse_meta(response)) def get_all( self, name: str | None = None, label_selector: str | None = None, ) -> list[BoundCertificate]: """Get all certificates :param name: str (optional) Can be used to filter certificates by their name. :param label_selector: str (optional) Can be used to filter certificates by labels. The response will only contain certificates matching the label selector. :return: List[:class:`BoundCertificate `] """ 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 :param name: str Used to get certificate by name. :return: :class:`BoundCertificate ` """ return self._get_first_by(self.get_list, name=name) 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` :param name: str :param certificate: str Certificate and chain in PEM format, in order so that each record directly certifies the one preceding :param private_key: str Certificate key in PEM format :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundCertificate ` """ data: dict[str, Any] = { "name": name, "certificate": certificate, "private_key": private_key, "type": Certificate.TYPE_UPLOADED, } if labels is not None: data["labels"] = labels response = self._client.request(url=self._base_url, method="POST", json=data) return BoundCertificate(self, response["certificate"]) 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` :param name: str :param domain_names: List[str] Domains and subdomains that should be contained in the Certificate :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundCertificate ` """ data: dict[str, Any] = { "name": name, "type": Certificate.TYPE_MANAGED, "domain_names": domain_names, } if labels is not None: data["labels"] = labels response = self._client.request(url=self._base_url, method="POST", json=data) return CreateManagedCertificateResponse( certificate=BoundCertificate(self, response["certificate"]), action=BoundAction(self._parent.actions, response["action"]), ) 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 ` :param name: str (optional) New name to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundCertificate ` """ data: dict[str, Any] = {} if name is not None: data["name"] = name if labels is not None: data["labels"] = labels response = self._client.request( url=f"{self._base_url}/{certificate.id}", method="PUT", json=data, ) return BoundCertificate(self, response["certificate"]) def delete(self, certificate: Certificate | BoundCertificate) -> bool: """Deletes a certificate. :param certificate: :class:`BoundCertificate ` or :class:`Certificate ` :return: True """ self._client.request( url=f"{self._base_url}/{certificate.id}", method="DELETE", ) # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised return True def get_actions_list( self, certificate: Certificate | BoundCertificate, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Certificate. :param certificate: Certificate to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._get_actions_list( f"{self._base_url}/{certificate.id}", status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, certificate: Certificate | BoundCertificate, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Certificate. :param certificate: Certificate to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ return self._iter_pages( self.get_actions_list, certificate, status=status, sort=sort, ) def retry_issuance( self, certificate: Certificate | BoundCertificate, ) -> BoundAction: """Returns all action objects for a Certificate. :param certificate: :class:`BoundCertificate ` or :class:`Certificate ` :return: :class:`BoundAction ` """ response = self._client.request( url=f"{self._base_url}/{certificate.id}/actions/retry", method="POST", ) return BoundAction(self._parent.actions, response["action"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/certificates/domain.py0000644000175100017510000001063015152343177020301 0ustar00runnerrunnerfrom __future__ import annotations from typing import TYPE_CHECKING from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction from .client import BoundCertificate __all__ = [ "Certificate", "ManagedCertificateStatus", "ManagedCertificateError", "CreateManagedCertificateResponse", ] class Certificate(BaseDomain, DomainIdentityMixin): """Certificate Domain :param id: int ID of Certificate :param name: str Name of Certificate :param certificate: str Certificate and chain in PEM format, in order so that each record directly certifies the one preceding :param not_valid_before: datetime Point in time when the Certificate becomes valid :param not_valid_after: datetime Point in time when the Certificate becomes invalid :param domain_names: List[str] List of domains and subdomains covered by this certificate :param fingerprint: str Fingerprint of the Certificate :param labels: dict User-defined labels (key-value pairs) :param created: datetime Point in time when the certificate was created :param type: str Type of Certificate :param status: ManagedCertificateStatus Current status of a type managed Certificate, always none for type uploaded Certificates """ __api_properties__ = ( "id", "name", "certificate", "not_valid_before", "not_valid_after", "domain_names", "fingerprint", "created", "labels", "type", "status", ) __slots__ = __api_properties__ TYPE_UPLOADED = "uploaded" TYPE_MANAGED = "managed" def __init__( self, 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 self.type = type self.certificate = certificate self.domain_names = domain_names self.fingerprint = fingerprint self.not_valid_before = self._parse_datetime(not_valid_before) self.not_valid_after = self._parse_datetime(not_valid_after) self.created = self._parse_datetime(created) self.labels = labels self.status = status class ManagedCertificateStatus(BaseDomain): """ManagedCertificateStatus Domain :param issuance: str Status of the issuance process of the Certificate :param renewal: str Status of the renewal process of the Certificate :param error: ManagedCertificateError If issuance or renewal reports failure, this property contains information about what happened """ __api_properties__ = ( "issuance", "renewal", "error", ) __slots__ = __api_properties__ def __init__( self, issuance: str | None = None, renewal: str | None = None, error: ManagedCertificateError | None = None, ): self.issuance = issuance self.renewal = renewal self.error = error class ManagedCertificateError(BaseDomain): """ManagedCertificateError Domain :param code: str Error code identifying the error :param message: Message detailing the error """ __api_properties__ = ( "code", "message", ) __slots__ = __api_properties__ def __init__(self, code: str | None = None, message: str | None = None): self.code = code self.message = message class CreateManagedCertificateResponse(BaseDomain): """Create Managed Certificate Response Domain :param certificate: :class:`BoundCertificate ` The created server :param action: :class:`BoundAction ` Shows the progress of the certificate creation """ __api_properties__ = ("certificate", "action") __slots__ = __api_properties__ def __init__( self, certificate: BoundCertificate, action: BoundAction, ): self.certificate = certificate self.action = action ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1338418 hcloud-2.17.0/hcloud/core/0000755000175100017510000000000015152343221014731 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/core/__init__.py0000644000175100017510000000052515152343177017056 0ustar00runnerrunnerfrom __future__ import annotations from .client import BoundModelBase, ClientEntityBase, ResourceClientBase from .domain import BaseDomain, DomainIdentityMixin, Meta, Pagination __all__ = [ "BaseDomain", "BoundModelBase", "ClientEntityBase", "DomainIdentityMixin", "Meta", "Pagination", "ResourceClientBase", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/core/client.py0000644000175100017510000001016515152343177016576 0ustar00runnerrunnerfrom __future__ import annotations import warnings from collections.abc import Callable from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar from .domain import BaseDomain if TYPE_CHECKING: from .._client import Client, ClientBase from .domain import Meta __all__ = [ "ResourceClientBase", "ClientEntityBase", "BoundModelBase", ] T = TypeVar("T") class ResourceClientBase: _base_url: ClassVar[str] _parent: Client _client: ClientBase max_per_page: int = 50 def __init__(self, client: Client): self._parent = client # Use the parent "default" base client. self._client = client._client def _iter_pages( # type: ignore[no-untyped-def] self, list_function: Callable[..., tuple[list[T], Meta]], *args, **kwargs, ) -> list[T]: results = [] page = 1 while page: # The *PageResult tuples MUST have the following structure # `(result: List[Bound*], meta: Meta)` result, meta = list_function( *args, page=page, per_page=self.max_per_page, **kwargs ) if result: results.extend(result) if meta and meta.pagination and meta.pagination.next_page: page = meta.pagination.next_page else: page = 0 return results 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 class ClientEntityBase(ResourceClientBase): """ Kept for backward compatibility. .. deprecated:: 2.6.0 Use :class:``hcloud.core.client.ResourceClientBase`` instead. """ def __init__(self, client: Client): warnings.warn( "The 'hcloud.core.client.ClientEntityBase' class is deprecated, please use the " "'hcloud.core.client.ResourceClientBase' class instead.", DeprecationWarning, stacklevel=2, ) super().__init__(client) Domain = TypeVar("Domain", bound=BaseDomain) class BoundModelBase(Generic[Domain]): """Bound Model Base""" model: type[Domain] def __init__( self, client: ResourceClientBase, data: dict[str, Any], complete: bool = True, ): """ :param client: The client for the specific model to use :param data: The data of the model :param complete: bool False if not all attributes of the model fetched """ self._client = client self.complete = complete 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 :param name: str :return: """ value = getattr(self.data_model, name) if not value and not self.complete: self.reload() value = getattr(self.data_model, name) return value def _get_self(self) -> BoundModelBase[Domain]: assert hasattr(self._client, "get_by_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""" bound_model = self._get_self() 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) def __eq__(self, other: Any) -> bool: """Compare a bound model object with another of the same type.""" if not isinstance(other, self.__class__): return NotImplemented return self.data_model == other.data_model ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/core/domain.py0000644000175100017510000000760115152343177016570 0ustar00runnerrunnerfrom __future__ import annotations from datetime import datetime from typing import Any, overload from dateutil.parser import isoparse __all__ = [ "BaseDomain", "DomainIdentityMixin", "Pagination", "Meta", ] class BaseDomain: __api_properties__: tuple[str, ...] @classmethod def from_dict(cls, data: dict[str, Any]): # 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.__api_properties__} return cls(**supported_data) def __repr__(self) -> str: 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: """Compare a domain object with another of the same type.""" if not isinstance(other, self.__class__): return NotImplemented for key in self.__api_properties__: if getattr(self, key) != getattr(other, key): return False return True @overload def _parse_datetime(self, value: str) -> datetime: ... @overload def _parse_datetime(self, value: None) -> None: ... def _parse_datetime(self, value: str | None) -> datetime | None: if value is None: return None return isoparse(value) class DomainIdentityMixin: id: int | None name: str | None @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 if self.name is not None: 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). """ result = None if self.id is not None: value = id_or_name if isinstance(id_or_name, str) and id_or_name.isnumeric(): value = int(id_or_name) result = result or self.id == value if self.name is not None: result = result or self.name == str(id_or_name) if result is None: raise ValueError("id or name must be set") return result class Pagination(BaseDomain): __api_properties__ = ( "page", "per_page", "previous_page", "next_page", "last_page", "total_entries", ) __slots__ = __api_properties__ def __init__( self, 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 self.previous_page = previous_page self.next_page = next_page self.last_page = last_page self.total_entries = total_entries class Meta(BaseDomain): __api_properties__ = ("pagination",) __slots__ = __api_properties__ def __init__(self, pagination: Pagination | None = None): self.pagination = pagination @classmethod def parse_meta(cls, response: dict[str, Any]) -> Meta: """ If present, extract the meta details from the response and return a meta object. """ meta = cls() if response and "meta" in response: try: meta.pagination = Pagination(**response["meta"]["pagination"]) except KeyError: pass return meta ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1344066 hcloud-2.17.0/hcloud/datacenters/0000755000175100017510000000000015152343221016276 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/datacenters/__init__.py0000644000175100017510000000050415152343177020420 0ustar00runnerrunnerfrom __future__ import annotations from .client import ( BoundDatacenter, DatacentersClient, DatacentersPageResult, ) from .domain import Datacenter, DatacenterServerTypes __all__ = [ "BoundDatacenter", "Datacenter", "DatacenterServerTypes", "DatacentersClient", "DatacentersPageResult", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/datacenters/client.py0000644000175100017510000001016415152343177020142 0ustar00runnerrunnerfrom __future__ import annotations from typing import Any, NamedTuple from ..core import BoundModelBase, Meta, ResourceClientBase from ..locations import BoundLocation from ..server_types import BoundServerType from .domain import Datacenter, DatacenterServerTypes __all__ = [ "BoundDatacenter", "DatacentersPageResult", "DatacentersClient", ] class BoundDatacenter(BoundModelBase[Datacenter], Datacenter): _client: DatacentersClient model = Datacenter 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) server_types = data.get("server_types") if server_types is not None: available = [ BoundServerType( client._parent.server_types, {"id": server_type}, complete=False ) for server_type in server_types["available"] ] supported = [ BoundServerType( client._parent.server_types, {"id": server_type}, complete=False ) for server_type in server_types["supported"] ] available_for_migration = [ BoundServerType( client._parent.server_types, {"id": server_type}, complete=False ) for server_type in server_types["available_for_migration"] ] data["server_types"] = DatacenterServerTypes( available=available, supported=supported, available_for_migration=available_for_migration, ) super().__init__(client, data) class DatacentersPageResult(NamedTuple): datacenters: list[BoundDatacenter] meta: Meta class DatacentersClient(ResourceClientBase): _base_url = "/datacenters" def get_by_id(self, id: int) -> BoundDatacenter: """Get a specific datacenter by its ID. :param id: int :return: :class:`BoundDatacenter ` """ response = self._client.request(url=f"{self._base_url}/{id}", method="GET") return BoundDatacenter(self, response["datacenter"]) def get_list( self, name: str | None = None, page: int | None = None, per_page: int | None = None, ) -> DatacentersPageResult: """Get a list of datacenters :param name: str (optional) Can be used to filter datacenters by their name. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundDatacenter `], :class:`Meta `) """ params: dict[str, Any] = {} if name is not None: params["name"] = name if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request(url=self._base_url, method="GET", params=params) datacenters = [ BoundDatacenter(self, datacenter_data) for datacenter_data in response["datacenters"] ] return DatacentersPageResult(datacenters, Meta.parse_meta(response)) def get_all(self, name: str | None = None) -> list[BoundDatacenter]: """Get all datacenters :param name: str (optional) Can be used to filter datacenters by their name. :return: List[:class:`BoundDatacenter `] """ return self._iter_pages(self.get_list, name=name) def get_by_name(self, name: str) -> BoundDatacenter | None: """Get datacenter by name :param name: str Used to get datacenter by name. :return: :class:`BoundDatacenter ` """ return self._get_first_by(self.get_list, name=name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/datacenters/domain.py0000644000175100017510000000431015152343177020127 0ustar00runnerrunnerfrom __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 __all__ = [ "Datacenter", "DatacenterServerTypes", ] class Datacenter(BaseDomain, DomainIdentityMixin): """Datacenter Domain :param id: int ID of Datacenter :param name: str Name of Datacenter :param description: str Description of Datacenter :param location: :class:`BoundLocation ` :param server_types: :class:`DatacenterServerTypes ` """ __api_properties__ = ("id", "name", "description", "location", "server_types") __slots__ = __api_properties__ def __init__( 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 self.description = description self.location = location self.server_types = server_types class DatacenterServerTypes(BaseDomain): """DatacenterServerTypes Domain :param available: List[:class:`BoundServerTypes `] All available server types for this datacenter :param supported: List[:class:`BoundServerTypes `] All supported server types for this datacenter :param available_for_migration: List[:class:`BoundServerTypes `] All available for migration (change type) server types for this datacenter """ __api_properties__ = ("available", "supported", "available_for_migration") __slots__ = __api_properties__ 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 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1772734097.134766 hcloud-2.17.0/hcloud/deprecation/0000755000175100017510000000000015152343221016276 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/deprecation/__init__.py0000644000175100017510000000015615152343177020423 0ustar00runnerrunnerfrom __future__ import annotations from .domain import DeprecationInfo __all__ = [ "DeprecationInfo", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/deprecation/domain.py0000644000175100017510000000215115152343177020130 0ustar00runnerrunnerfrom __future__ import annotations from ..core import BaseDomain __all__ = [ "DeprecationInfo", ] 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. """ __api_properties__ = ( "announced", "unavailable_after", ) __slots__ = __api_properties__ def __init__( self, announced: str | None = None, unavailable_after: str | None = None, ): self.announced = self._parse_datetime(announced) self.unavailable_after = self._parse_datetime(unavailable_after) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1351447 hcloud-2.17.0/hcloud/exp/0000755000175100017510000000000015152343221014575 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/exp/__init__.py0000644000175100017510000000023415152343177016717 0ustar00runnerrunner""" The `exp` module is a namespace that holds experimental features for the `hcloud-python` library, breaking changes may occur within minor releases. """ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/exp/zone.py0000644000175100017510000000172515152343177016141 0ustar00runnerrunner""" The `exp.zone` module is a namespace that holds experimental features for the `hcloud-python` library, breaking changes may occur within minor releases. """ from __future__ import annotations __all__ = [ "is_txt_record_quoted", "format_txt_record", ] def is_txt_record_quoted(value: str) -> bool: """ Check whether a TXT record is already quoted. - hello world => false - "hello world" => true """ return value.startswith('"') and value.endswith('"') def format_txt_record(value: str) -> str: """ Format a TXT record by splitting it in quoted strings of 255 characters. Existing quotes will be escaped. - hello world => "hello world" - hello "world" => "hello \"world\"" """ value = value.replace('"', '\\"') parts = [] for start in range(0, len(value), 255): end = min(start + 255, len(value)) parts.append('"' + value[start:end] + '"') value = " ".join(parts) return value ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1356864 hcloud-2.17.0/hcloud/firewalls/0000755000175100017510000000000015152343221015771 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/firewalls/__init__.py0000644000175100017510000000104015152343177020107 0ustar00runnerrunnerfrom __future__ import annotations from .client import BoundFirewall, FirewallsClient, FirewallsPageResult from .domain import ( CreateFirewallResponse, Firewall, FirewallResource, FirewallResourceAppliedToResources, FirewallResourceLabelSelector, FirewallRule, ) __all__ = [ "BoundFirewall", "CreateFirewallResponse", "Firewall", "FirewallResource", "FirewallResourceAppliedToResources", "FirewallResourceLabelSelector", "FirewallRule", "FirewallsClient", "FirewallsPageResult", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/firewalls/client.py0000644000175100017510000004407115152343177017641 0ustar00runnerrunnerfrom __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ( ActionSort, ActionsPageResult, ActionStatus, BoundAction, ResourceActionsClient, ) from ..actions.client import ResourceClientBaseActionsMixin from ..core import BoundModelBase, Meta, ResourceClientBase from .domain import ( CreateFirewallResponse, Firewall, FirewallResource, FirewallResourceAppliedToResources, FirewallResourceLabelSelector, FirewallRule, ) if TYPE_CHECKING: from .._client import Client __all__ = [ "BoundFirewall", "FirewallsPageResult", "FirewallsClient", ] class BoundFirewall(BoundModelBase[Firewall], Firewall): _client: FirewallsClient model = Firewall def __init__( self, client: FirewallsClient, data: dict[str, Any], complete: bool = True, ): rules = data.get("rules", []) if rules: rules = [ FirewallRule( direction=rule["direction"], source_ips=rule["source_ips"], destination_ips=rule["destination_ips"], protocol=rule["protocol"], port=rule["port"], description=rule["description"], ) for rule in rules ] data["rules"] = rules applied_to = data.get("applied_to", []) if applied_to: # pylint: disable=import-outside-toplevel from ..servers import BoundServer 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._parent.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( type=firewall_resource["type"], server=BoundServer( client._parent.servers, firewall_resource["server"], complete=False, ), applied_to_resources=applied_to_resources, ) ) elif firewall_resource["type"] == FirewallResource.TYPE_LABEL_SELECTOR: data_applied_to.append( FirewallResource( type=firewall_resource["type"], 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) def get_actions_list( self, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Firewall. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._client.get_actions_list( self, status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Firewall. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ return self._client.get_actions( self, status=status, sort=sort, ) 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) User-defined labels (key-value pairs) :param name: str (optional) New Name to set :return: :class:`BoundFirewall ` """ return self._client.update(self, name=name, labels=labels) def delete(self) -> bool: """Deletes a Firewall. :return: boolean """ return self._client.delete(self) 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 `] """ return self._client.set_rules(self, rules=rules) 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=resources) 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 `] """ return self._client.remove_from_resources(self, resources=resources) class FirewallsPageResult(NamedTuple): firewalls: list[BoundFirewall] meta: Meta class FirewallsClient( ResourceClientBaseActionsMixin, ResourceClientBase, ): _base_url = "/firewalls" actions: ResourceActionsClient """Firewalls scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self.actions = ResourceActionsClient(client, self._base_url) def get_actions_list( self, firewall: Firewall | BoundFirewall, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Firewall. :param firewall: Firewall to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._get_actions_list( f"{self._base_url}/{firewall.id}", status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, firewall: Firewall | BoundFirewall, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Firewall. :param firewall: Firewall to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ 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. :param id: int :return: :class:`BoundFirewall ` """ response = self._client.request(url=f"{self._base_url}/{id}", method="GET") return BoundFirewall(self, response["firewall"]) def get_list( self, 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) Can be used to filter Firewalls by labels. The response will only contain Firewalls matching the label selector 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 sort: List[str] (optional) 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: dict[str, Any] = {} 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 sort is not None: params["sort"] = sort response = self._client.request(url=self._base_url, method="GET", params=params) firewalls = [ BoundFirewall(self, firewall_data) for firewall_data in response["firewalls"] ] return FirewallsPageResult(firewalls, Meta.parse_meta(response)) 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) Can be used to filter Firewalls by labels. The response will only contain Firewalls matching the label selector values. :param name: str (optional) Can be used to filter networks by their name. :param sort: List[str] (optional) Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) :return: List[:class:`BoundFirewall `] """ 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 :param name: str Used to get Firewall by name. :return: :class:`BoundFirewall ` """ return self._get_first_by(self.get_list, name=name) def create( self, name: str, rules: list[FirewallRule] | None = None, labels: str | None = None, resources: list[FirewallResource] | None = None, ) -> CreateFirewallResponse: """Creates a new Firewall. :param name: str Firewall Name :param rules: List[:class:`FirewallRule `] (optional) :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :param resources: List[:class:`FirewallResource `] (optional) :return: :class:`CreateFirewallResponse ` """ data: dict[str, Any] = {"name": name} if labels is not None: data["labels"] = labels if rules is not None: data.update({"rules": []}) for rule in rules: data["rules"].append(rule.to_payload()) if resources is not None: data.update({"apply_to": []}) for resource in resources: data["apply_to"].append(resource.to_payload()) response = self._client.request(url=self._base_url, json=data, method="POST") actions = [] if response.get("actions") is not None: actions = [ BoundAction(self._parent.actions, action_data) for action_data in response["actions"] ] result = CreateFirewallResponse( firewall=BoundFirewall(self, response["firewall"]), actions=actions ) return result 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 ` :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :param name: str (optional) New name to set :return: :class:`BoundFirewall ` """ data: dict[str, Any] = {} if labels is not None: data["labels"] = labels if name is not None: data["name"] = name response = self._client.request( url=f"{self._base_url}/{firewall.id}", method="PUT", json=data, ) return BoundFirewall(self, response["firewall"]) def delete(self, firewall: Firewall | BoundFirewall) -> bool: """Deletes a Firewall. :param firewall: :class:`BoundFirewall ` or :class:`Firewall ` :return: boolean """ self._client.request( url=f"{self._base_url}/{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 return True 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: dict[str, Any] = {"rules": []} for rule in rules: data["rules"].append(rule.to_payload()) response = self._client.request( url=f"{self._base_url}/{firewall.id}/actions/set_rules", method="POST", json=data, ) return [ BoundAction(self._parent.actions, action_data) for action_data in response["actions"] ] 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: dict[str, Any] = {"apply_to": []} for resource in resources: data["apply_to"].append(resource.to_payload()) response = self._client.request( url=f"{self._base_url}/{firewall.id}/actions/apply_to_resources", method="POST", json=data, ) return [ BoundAction(self._parent.actions, action_data) for action_data in response["actions"] ] 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: dict[str, Any] = {"remove_from": []} for resource in resources: data["remove_from"].append(resource.to_payload()) response = self._client.request( url=f"{self._base_url}/{firewall.id}/actions/remove_from_resources", method="POST", json=data, ) return [ BoundAction(self._parent.actions, action_data) for action_data in response["actions"] ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/firewalls/domain.py0000644000175100017510000001646315152343177017636 0ustar00runnerrunnerfrom __future__ import annotations from typing import TYPE_CHECKING, Any from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction from ..servers import BoundServer, Server from .client import BoundFirewall __all__ = [ "Firewall", "FirewallRule", "FirewallResource", "FirewallResourceAppliedToResources", "FirewallResourceLabelSelector", "CreateFirewallResponse", ] class Firewall(BaseDomain, DomainIdentityMixin): """Firewall Domain :param id: int ID of the Firewall :param name: str Name of the Firewall :param labels: dict User-defined labels (key-value pairs) :param rules: List[:class:`FirewallRule `] Rules of the Firewall :param applied_to: List[:class:`FirewallResource `] Resources currently using the Firewall :param created: datetime Point in time when the image was created """ __api_properties__ = ("id", "name", "labels", "rules", "applied_to", "created") __slots__ = __api_properties__ def __init__( self, id: int | None = None, name: str | None = None, labels: dict[str, str] | None = None, rules: list[FirewallRule] | None = None, applied_to: list[FirewallResource] | None = None, created: str | None = None, ): self.id = id self.name = name self.rules = rules self.applied_to = applied_to self.labels = labels self.created = self._parse_datetime(created) class FirewallRule(BaseDomain): """Firewall Rule Domain :param direction: str The Firewall which was created :param port: str Port to which traffic will be allowed, only applicable for protocols TCP and UDP, specify port ranges by using - as a indicator, Sample: 80-85 means all ports between 80 & 85 (80, 82, 83, 84, 85) :param protocol: str Select traffic direction on which rule should be applied. Use source_ips for direction in and destination_ips for direction out. :param source_ips: List[str] List of permitted IPv4/IPv6 addresses in CIDR notation. Use 0.0.0.0/0 to allow all IPv4 addresses and ::/0 to allow all IPv6 addresses. You can specify 100 CIDRs at most. :param destination_ips: List[str] List of permitted IPv4/IPv6 addresses in CIDR notation. Use 0.0.0.0/0 to allow all IPv4 addresses and ::/0 to allow all IPv6 addresses. You can specify 100 CIDRs at most. :param description: str Short description of the firewall rule """ __api_properties__ = ( "direction", "port", "protocol", "source_ips", "destination_ips", "description", ) __slots__ = __api_properties__ DIRECTION_IN = "in" """Firewall Rule Direction In""" DIRECTION_OUT = "out" """Firewall Rule Direction Out""" PROTOCOL_UDP = "udp" """Firewall Rule Protocol UDP""" PROTOCOL_ICMP = "icmp" """Firewall Rule Protocol ICMP""" PROTOCOL_TCP = "tcp" """Firewall Rule Protocol TCP""" PROTOCOL_ESP = "esp" """Firewall Rule Protocol ESP""" PROTOCOL_GRE = "gre" """Firewall Rule Protocol GRE""" def __init__( self, direction: str, protocol: str, source_ips: list[str] | None = None, port: str | None = None, destination_ips: list[str] | None = None, description: str | None = None, ): self.direction = direction self.port = port self.protocol = protocol self.source_ips = source_ips or [] self.destination_ips = destination_ips or [] 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, } if len(self.source_ips) > 0: payload["source_ips"] = self.source_ips if len(self.destination_ips) > 0: payload["destination_ips"] = self.destination_ips if self.port is not None: payload["port"] = self.port if self.description is not None: payload["description"] = self.description return payload class FirewallResource(BaseDomain): """Firewall Used By Domain :param type: str Type of resource referenced :param server: Optional[Server] 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. """ __api_properties__ = ("type", "server", "label_selector", "applied_to_resources") __slots__ = __api_properties__ TYPE_SERVER = "server" """Firewall Used By Type Server""" TYPE_LABEL_SELECTOR = "label_selector" """Firewall Used By Type label_selector""" def __init__( self, 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]: """ 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} if self.label_selector is not None: payload["label_selector"] = {"selector": self.label_selector.selector} return payload class FirewallResourceAppliedToResources(BaseDomain): """Firewall Resource applied to Domain :param type: Type of resource referenced :param server: Server the Firewall is applied to """ __api_properties__ = ("type", "server") __slots__ = __api_properties__ def __init__( self, type: str, server: BoundServer | None = None, ): self.type = type self.server = server class FirewallResourceLabelSelector(BaseDomain): """FirewallResourceLabelSelector Domain :param selector: str Target label selector """ __api_properties__ = ("selector",) __slots__ = __api_properties__ def __init__(self, selector: str | None = None): self.selector = selector class CreateFirewallResponse(BaseDomain): """Create Firewall Response Domain :param firewall: :class:`BoundFirewall ` The Firewall which was created :param actions: List[:class:`BoundAction `] The Action which shows the progress of the Firewall Creation """ __api_properties__ = ("firewall", "actions") __slots__ = __api_properties__ def __init__( self, firewall: BoundFirewall, actions: list[BoundAction] | None, ): self.firewall = firewall self.actions = actions ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1362731 hcloud-2.17.0/hcloud/floating_ips/0000755000175100017510000000000015152343221016457 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/floating_ips/__init__.py0000644000175100017510000000057415152343177020610 0ustar00runnerrunnerfrom __future__ import annotations from .client import ( BoundFloatingIP, FloatingIPsClient, FloatingIPsPageResult, ) from .domain import CreateFloatingIPResponse, FloatingIP, FloatingIPProtection __all__ = [ "BoundFloatingIP", "CreateFloatingIPResponse", "FloatingIP", "FloatingIPProtection", "FloatingIPsClient", "FloatingIPsPageResult", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/floating_ips/client.py0000644000175100017510000004226415152343177020331 0ustar00runnerrunnerfrom __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ( ActionSort, ActionsPageResult, ActionStatus, BoundAction, ResourceActionsClient, ) from ..actions.client import ResourceClientBaseActionsMixin from ..core import BoundModelBase, Meta, ResourceClientBase from ..locations import BoundLocation from .domain import CreateFloatingIPResponse, FloatingIP if TYPE_CHECKING: from .._client import Client from ..locations import Location from ..servers import BoundServer, Server __all__ = [ "BoundFloatingIP", "FloatingIPsPageResult", "FloatingIPsClient", ] class BoundFloatingIP(BoundModelBase[FloatingIP], FloatingIP): _client: FloatingIPsClient model = FloatingIP def __init__( self, client: FloatingIPsClient, data: dict[str, Any], complete: bool = True, ): # pylint: disable=import-outside-toplevel from ..servers import BoundServer server = data.get("server") if server is not None: data["server"] = BoundServer( client._parent.servers, {"id": server}, complete=False ) home_location = data.get("home_location") if home_location is not None: data["home_location"] = BoundLocation( client._parent.locations, home_location ) super().__init__(client, data, complete) def get_actions_list( self, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Floating IP. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._client.get_actions_list( self, status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Floating IP. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ return self._client.get_actions(self, status=status, sort=sort) 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) New Description to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :param name: str (optional) New Name to set :return: :class:`BoundFloatingIP ` """ return self._client.update( self, description=description, labels=labels, name=name ) 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: bool | None = None) -> BoundAction: """Changes the protection configuration of the Floating IP. :param delete: boolean If true, prevents the Floating IP from being deleted :return: :class:`BoundAction ` """ return self._client.change_protection(self, delete=delete) def assign(self, server: Server | BoundServer) -> BoundAction: """Assigns a Floating IP to a server. :param server: :class:`BoundServer ` or :class:`Server ` Server the Floating IP shall be assigned to :return: :class:`BoundAction ` """ return self._client.assign(self, server=server) 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: str, dns_ptr: str) -> BoundAction: """Changes the hostname that will appear when getting the hostname belonging to this Floating 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=ip, dns_ptr=dns_ptr) class FloatingIPsPageResult(NamedTuple): floating_ips: list[BoundFloatingIP] meta: Meta class FloatingIPsClient( ResourceClientBaseActionsMixin, ResourceClientBase, ): _base_url = "/floating_ips" actions: ResourceActionsClient """Floating IPs scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self.actions = ResourceActionsClient(client, self._base_url) def get_actions_list( self, floating_ip: FloatingIP | BoundFloatingIP, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Floating IP. :param floating_ip: Floating IP to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._get_actions_list( f"{self._base_url}/{floating_ip.id}", status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, floating_ip: FloatingIP | BoundFloatingIP, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Floating IP. :param floating_ip: Floating IP to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ 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. :param id: int :return: :class:`BoundFloatingIP ` """ response = self._client.request(url=f"{self._base_url}/{id}", method="GET") return BoundFloatingIP(self, response["floating_ip"]) def get_list( self, 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) Can be used to filter Floating IPs by labels. The response will only contain Floating IPs matching the label selector.able 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. :return: (List[:class:`BoundFloatingIP `], :class:`Meta `) """ params: dict[str, Any] = {} 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 response = self._client.request(url=self._base_url, method="GET", params=params) floating_ips = [ BoundFloatingIP(self, floating_ip_data) for floating_ip_data in response["floating_ips"] ] return FloatingIPsPageResult(floating_ips, Meta.parse_meta(response)) 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) Can be used to filter Floating IPs by labels. The response will only contain Floating IPs matching the label selector.able values. :param name: str (optional) Can be used to filter networks by their name. :return: List[:class:`BoundFloatingIP `] """ 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 :param name: str Used to get Floating IP by name. :return: :class:`BoundFloatingIP ` """ return self._get_first_by(self.get_list, name=name) def create( self, type: str, description: str | None = None, labels: dict[str, 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 Floating IP type Choices: ipv4, ipv6 :param description: str (optional) :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :param home_location: :class:`BoundLocation ` or :class:`Location ` ( Home location (routing is optimized for that location). Only optional if server argument is passed. :param server: :class:`BoundServer ` or :class:`Server ` Server to assign the Floating IP to :param name: str (optional) :return: :class:`CreateFloatingIPResponse ` """ data: dict[str, Any] = {"type": type} if description is not None: data["description"] = description if labels is not None: data["labels"] = labels if home_location is not None: data["home_location"] = home_location.id_or_name if server is not None: data["server"] = server.id if name is not None: data["name"] = name response = self._client.request(url=self._base_url, json=data, method="POST") action = None if response.get("action") is not None: action = BoundAction(self._parent.actions, response["action"]) result = CreateFloatingIPResponse( floating_ip=BoundFloatingIP(self, response["floating_ip"]), action=action ) return result 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 ` :param description: str (optional) New Description to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :param name: str (optional) New name to set :return: :class:`BoundFloatingIP ` """ data: dict[str, Any] = {} if description is not None: data["description"] = description if labels is not None: data["labels"] = labels if name is not None: data["name"] = name response = self._client.request( url=f"{self._base_url}/{floating_ip.id}", method="PUT", json=data, ) return BoundFloatingIP(self, response["floating_ip"]) 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 ` :return: boolean """ self._client.request( url=f"{self._base_url}/{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 return True 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 ` :param delete: boolean If true, prevents the Floating IP from being deleted :return: :class:`BoundAction ` """ data: dict[str, Any] = {} if delete is not None: data.update({"delete": delete}) response = self._client.request( url=f"{self._base_url}/{floating_ip.id}/actions/change_protection", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) 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 ` :param server: :class:`BoundServer ` or :class:`Server ` Server the Floating IP shall be assigned to :return: :class:`BoundAction ` """ response = self._client.request( url=f"{self._base_url}/{floating_ip.id}/actions/assign", method="POST", json={"server": server.id}, ) return BoundAction(self._parent.actions, response["action"]) 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 ` :return: :class:`BoundAction ` """ response = self._client.request( url=f"{self._base_url}/{floating_ip.id}/actions/unassign", method="POST", ) return BoundAction(self._parent.actions, response["action"]) 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 ` :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=f"{self._base_url}/{floating_ip.id}/actions/change_dns_ptr", method="POST", json={"ip": ip, "dns_ptr": dns_ptr}, ) return BoundAction(self._parent.actions, response["action"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/floating_ips/domain.py0000644000175100017510000000663215152343177020321 0ustar00runnerrunnerfrom __future__ import annotations 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 __all__ = [ "FloatingIP", "FloatingIPProtection", "CreateFloatingIPResponse", ] class FloatingIP(BaseDomain, DomainIdentityMixin): """Floating IP Domain :param id: int ID of the Floating IP :param description: str, None Description of the Floating IP :param ip: str IP address of the Floating IP :param type: str Type of Floating IP. Choices: `ipv4`, `ipv6` :param server: :class:`BoundServer `, None Server the Floating IP is assigned to, None if it is not assigned at all :param dns_ptr: List[Dict] Array of reverse DNS entries :param home_location: :class:`BoundLocation ` Location the Floating IP was created in. Routing is optimized for this location. :param blocked: boolean Whether the IP is blocked :param protection: dict Protection configuration for the Floating IP :param labels: dict User-defined labels (key-value pairs) :param created: datetime Point in time when the Floating IP was created :param name: str Name of the Floating IP """ __api_properties__ = ( "id", "type", "description", "ip", "server", "dns_ptr", "home_location", "blocked", "protection", "labels", "name", "created", ) __slots__ = __api_properties__ def __init__( self, id: int | None = None, type: str | None = None, description: str | None = None, ip: str | None = None, server: BoundServer | None = None, dns_ptr: list[DNSPtr] | None = None, home_location: BoundLocation | None = None, blocked: bool | None = None, protection: FloatingIPProtection | None = None, labels: dict[str, str] | None = None, created: str | None = None, name: str | None = None, ): self.id = id self.type = type self.description = description self.ip = ip self.server = server self.dns_ptr = dns_ptr self.home_location = home_location self.blocked = blocked self.protection = protection self.labels = labels self.created = self._parse_datetime(created) self.name = name class FloatingIPProtection(TypedDict): delete: bool class CreateFloatingIPResponse(BaseDomain): """Create Floating IP Response Domain :param floating_ip: :class:`BoundFloatingIP ` The Floating IP which was created :param action: :class:`BoundAction ` The Action which shows the progress of the Floating IP Creation """ __api_properties__ = ("floating_ip", "action") __slots__ = __api_properties__ def __init__( self, floating_ip: BoundFloatingIP, action: BoundAction | None, ): self.floating_ip = floating_ip self.action = action ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1366513 hcloud-2.17.0/hcloud/helpers/0000755000175100017510000000000015152343221015443 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/helpers/__init__.py0000644000175100017510000000015415152343177017566 0ustar00runnerrunnerfrom __future__ import annotations from .labels import LabelValidator __all__ = [ "LabelValidator", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/helpers/labels.py0000644000175100017510000000317315152343177017275 0ustar00runnerrunnerfrom __future__ import annotations import re __all__ = [ "LabelValidator", ] class LabelValidator: KEY_REGEX = re.compile( 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( r"^(([a-z0-9A-Z](?:[\-_.]|[a-z0-9A-Z]){0,61})?[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 key, value in labels.items(): if LabelValidator.KEY_REGEX.match(key) is None: return False if LabelValidator.VALUE_REGEX.match(value) is None: return False return True @staticmethod 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. :return: bool, str """ 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, "" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1372116 hcloud-2.17.0/hcloud/images/0000755000175100017510000000000015152343221015246 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/images/__init__.py0000644000175100017510000000045715152343177017377 0ustar00runnerrunnerfrom __future__ import annotations from .client import BoundImage, ImagesClient, ImagesPageResult from .domain import CreateImageResponse, Image, ImageProtection __all__ = [ "BoundImage", "CreateImageResponse", "Image", "ImageProtection", "ImagesClient", "ImagesPageResult", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/images/client.py0000644000175100017510000003511115152343177017111 0ustar00runnerrunnerfrom __future__ import annotations import warnings from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ( ActionSort, ActionsPageResult, ActionStatus, BoundAction, ResourceActionsClient, ) from ..actions.client import ResourceClientBaseActionsMixin from ..core import BoundModelBase, Meta, ResourceClientBase from .domain import Image if TYPE_CHECKING: from .._client import Client __all__ = [ "BoundImage", "ImagesPageResult", "ImagesClient", ] class BoundImage(BoundModelBase[Image], Image): _client: ImagesClient model = Image def __init__( self, client: ImagesClient, data: dict[str, Any], ): # pylint: disable=import-outside-toplevel from ..servers import BoundServer created_from = data.get("created_from") if created_from is not None: data["created_from"] = BoundServer( client._parent.servers, created_from, complete=False ) bound_to = data.get("bound_to") if bound_to is not None: data["bound_to"] = BoundServer( client._parent.servers, {"id": bound_to}, complete=False ) super().__init__(client, data) def get_actions_list( self, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Image. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._client.get_actions_list( self, sort=sort, page=page, per_page=per_page, status=status, ) def get_actions( self, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Image. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ return self._client.get_actions( self, status=status, sort=sort, ) 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) New description of Image :param type: str (optional) Destination image type to convert to Choices: snapshot :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundImage ` """ return self._client.update( self, description=description, type=type, labels=labels ) 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: bool | None = None) -> BoundAction: """Changes the protection configuration of the image. Can only be used on snapshots. :param delete: bool If true, prevents the snapshot from being deleted :return: :class:`BoundAction ` """ return self._client.change_protection(self, delete=delete) class ImagesPageResult(NamedTuple): images: list[BoundImage] meta: Meta class ImagesClient( ResourceClientBaseActionsMixin, ResourceClientBase, ): _base_url = "/images" actions: ResourceActionsClient """Images scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self.actions = ResourceActionsClient(client, self._base_url) def get_actions_list( self, image: Image | BoundImage, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Image. :param image: Image to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._get_actions_list( f"{self._base_url}/{image.id}", status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, image: Image | BoundImage, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Image. :param image: Image to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ return self._iter_pages( self.get_actions_list, image, status=status, sort=sort, ) def get_by_id(self, id: int) -> BoundImage: """Get a specific Image :param id: int :return: :class:`BoundImage ImagesPageResult: """Get all images :param name: str (optional) Can be used to filter images by their name. :param label_selector: str (optional) Can be used to filter servers by labels. The response will only contain servers matching the label selector. :param bound_to: List[str] (optional) 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) Choices: id id:asc id:desc name name:asc name:desc created created:asc created:desc :param include_deprecated: bool (optional) Include deprecated images in the response. Default: False :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundImage `], :class:`Meta `) """ params: dict[str, Any] = {} if name is not None: params["name"] = name if label_selector is not None: params["label_selector"] = label_selector if bound_to is not None: 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: params["page"] = page if per_page is not None: params["per_page"] = per_page if status is not None: params["status"] = per_page if include_deprecated is not None: params["include_deprecated"] = include_deprecated response = self._client.request(url=self._base_url, method="GET", params=params) images = [BoundImage(self, image_data) for image_data in response["images"]] return ImagesPageResult(images, Meta.parse_meta(response)) def get_all( self, 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) Can be used to filter images by their name. :param label_selector: str (optional) Can be used to filter servers by labels. The response will only contain servers matching the label selector. :param bound_to: List[str] (optional) 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) Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) :param include_deprecated: bool (optional) Include deprecated images in the response. Default: False :return: List[:class:`BoundImage `] """ return self._iter_pages( self.get_list, name=name, label_selector=label_selector, bound_to=bound_to, type=type, architecture=architecture, sort=sort, status=status, include_deprecated=include_deprecated, ) def get_by_name(self, name: str) -> BoundImage | None: """Get image by name :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(self.get_list, name=name) def get_by_name_and_architecture( self, name: str, architecture: str, *, include_deprecated: bool | None = None, ) -> BoundImage | None: """Get image by name :param name: str 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( self.get_list, name=name, architecture=[architecture], include_deprecated=include_deprecated, ) 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 ` :param description: str (optional) New description of Image :param type: str (optional) Destination image type to convert to Choices: snapshot :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundImage ` """ data: dict[str, Any] = {} if description is not None: data.update({"description": description}) if type is not None: data.update({"type": type}) if labels is not None: data.update({"labels": labels}) response = self._client.request( url=f"{self._base_url}/{image.id}", method="PUT", json=data ) return BoundImage(self, response["image"]) 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 ` :return: bool """ self._client.request(url=f"{self._base_url}/{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 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 ` :param delete: bool If true, prevents the snapshot from being deleted :return: :class:`BoundAction ` """ data: dict[str, Any] = {} if delete is not None: data.update({"delete": delete}) response = self._client.request( url=f"{self._base_url}/{image.id}/actions/change_protection", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/images/domain.py0000644000175100017510000001111615152343177017101 0ustar00runnerrunnerfrom __future__ import annotations from typing import TYPE_CHECKING, TypedDict from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction from ..servers import BoundServer, Server from .client import BoundImage __all__ = [ "Image", "ImageProtection", "CreateImageResponse", ] class Image(BaseDomain, DomainIdentityMixin): """Image Domain :param id: int ID of the image :param type: str Type of the image Choices: `system`, `snapshot`, `backup`, `app` :param status: str Whether the image can be used or if it’s still being created Choices: `available`, `creating` :param name: str, None Unique identifier of the image. This value is only set for system images. :param description: str Description of the image :param image_size: number, None Size of the image file in our storage in GB. For snapshot images this is the value relevant for calculating costs for the image. :param disk_size: number Size of the disk contained in the image in GB. :param created: datetime Point in time when the image was created :param created_from: :class:`BoundServer `, None Information about the server the image was created from :param bound_to: :class:`BoundServer `, None ID of server the image is bound to. Only set for images of type `backup`. :param os_flavor: str 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 Protection configuration for the image :param deprecated: datetime, None Point in time when the image is considered to be deprecated (in ISO-8601 format) :param labels: Dict User-defined labels (key-value pairs) """ __api_properties__ = ( "id", "name", "type", "description", "image_size", "disk_size", "bound_to", "os_flavor", "os_version", "architecture", "rapid_deploy", "created_from", "status", "protection", "labels", "created", "deprecated", ) __slots__ = __api_properties__ # pylint: disable=too-many-locals def __init__( self, 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: ImageProtection | None = None, labels: dict[str, str] | None = None, status: str | None = None, ): self.id = id self.name = name self.type = type self.created = self._parse_datetime(created) self.description = description self.image_size = image_size self.disk_size = disk_size self.deprecated = self._parse_datetime(deprecated) 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 self.labels = labels self.status = status class ImageProtection(TypedDict): delete: bool class CreateImageResponse(BaseDomain): """Create Image Response Domain :param image: :class:`BoundImage ` The Image which was created :param action: :class:`BoundAction ` The Action which shows the progress of the Floating IP Creation """ __api_properties__ = ("action", "image") __slots__ = __api_properties__ def __init__( self, action: BoundAction, image: BoundImage, ): self.action = action self.image = image ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1377766 hcloud-2.17.0/hcloud/isos/0000755000175100017510000000000015152343221014756 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/isos/__init__.py0000644000175100017510000000030715152343177017101 0ustar00runnerrunnerfrom __future__ import annotations from .client import BoundIso, IsosClient, IsosPageResult from .domain import Iso __all__ = [ "BoundIso", "Iso", "IsosClient", "IsosPageResult", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/isos/client.py0000644000175100017510000000731415152343177016625 0ustar00runnerrunnerfrom __future__ import annotations from typing import Any, NamedTuple from ..core import BoundModelBase, Meta, ResourceClientBase from .domain import Iso __all__ = [ "BoundIso", "IsosPageResult", "IsosClient", ] class BoundIso(BoundModelBase[Iso], Iso): _client: IsosClient model = Iso class IsosPageResult(NamedTuple): isos: list[BoundIso] meta: Meta class IsosClient(ResourceClientBase): _base_url = "/isos" def get_by_id(self, id: int) -> BoundIso: """Get a specific ISO by its id :param id: int :return: :class:`BoundIso ` """ response = self._client.request(url=f"{self._base_url}/{id}", method="GET") return BoundIso(self, response["iso"]) def get_list( self, name: str | None = None, architecture: list[str] | 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) 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_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) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundIso `], :class:`Meta `) """ params: dict[str, Any] = {} if name is not None: params["name"] = name if architecture is not None: params["architecture"] = 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: params["per_page"] = per_page response = self._client.request(url=self._base_url, method="GET", params=params) isos = [BoundIso(self, iso_data) for iso_data in response["isos"]] return IsosPageResult(isos, Meta.parse_meta(response)) def get_all( self, name: str | None = None, architecture: list[str] | None = None, include_architecture_wildcard: bool | None = None, ) -> 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_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 `] """ return self._iter_pages( self.get_list, name=name, architecture=architecture, include_architecture_wildcard=include_architecture_wildcard, ) def get_by_name(self, name: str) -> BoundIso | None: """Get iso by name :param name: str Used to get iso by name. :return: :class:`BoundIso ` """ return self._get_first_by(self.get_list, name=name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/isos/domain.py0000644000175100017510000000476015152343177016620 0ustar00runnerrunnerfrom __future__ import annotations from datetime import datetime from typing import Any from warnings import warn from ..core import BaseDomain, DomainIdentityMixin from ..deprecation import DeprecationInfo __all__ = [ "Iso", ] class Iso(BaseDomain, DomainIdentityMixin): """Iso Domain :param id: int ID of the ISO :param name: str, None Unique identifier of the ISO. Only set for public ISOs :param description: str 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. 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. """ __api_properties__ = ( "id", "name", "type", "architecture", "description", "deprecation", ) __slots__ = __api_properties__ def __init__( self, id: int | None = None, name: str | None = None, type: str | None = None, architecture: str | None = None, description: str | None = None, deprecated: str | None = None, # pylint: disable=unused-argument deprecation: dict[str, Any] | None = None, ): self.id = id self.name = name self.type = type self.architecture = architecture self.description = description 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 # type: ignore[no-any-return] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1772734097.138346 hcloud-2.17.0/hcloud/load_balancer_types/0000755000175100017510000000000015152343221017773 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/load_balancer_types/__init__.py0000644000175100017510000000050015152343177022111 0ustar00runnerrunnerfrom __future__ import annotations from .client import ( BoundLoadBalancerType, LoadBalancerTypesClient, LoadBalancerTypesPageResult, ) from .domain import LoadBalancerType __all__ = [ "BoundLoadBalancerType", "LoadBalancerType", "LoadBalancerTypesClient", "LoadBalancerTypesPageResult", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/load_balancer_types/client.py0000644000175100017510000000613215152343177021637 0ustar00runnerrunnerfrom __future__ import annotations from typing import Any, NamedTuple from ..core import BoundModelBase, Meta, ResourceClientBase from .domain import LoadBalancerType __all__ = [ "BoundLoadBalancerType", "LoadBalancerTypesPageResult", "LoadBalancerTypesClient", ] class BoundLoadBalancerType(BoundModelBase[LoadBalancerType], LoadBalancerType): _client: LoadBalancerTypesClient model = LoadBalancerType class LoadBalancerTypesPageResult(NamedTuple): load_balancer_types: list[BoundLoadBalancerType] meta: Meta class LoadBalancerTypesClient(ResourceClientBase): _base_url = "/load_balancer_types" def get_by_id(self, id: int) -> BoundLoadBalancerType: """Returns a specific Load Balancer Type. :param id: int :return: :class:`BoundLoadBalancerType ` """ response = self._client.request( url=f"{self._base_url}/{id}", method="GET", ) return BoundLoadBalancerType(self, response["load_balancer_type"]) 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) Can be used to filter Load Balancer type by their name. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundLoadBalancerType `], :class:`Meta `) """ params: dict[str, Any] = {} if name is not None: params["name"] = name if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request(url=self._base_url, method="GET", params=params) load_balancer_types = [ BoundLoadBalancerType(self, load_balancer_type_data) for load_balancer_type_data in response["load_balancer_types"] ] return LoadBalancerTypesPageResult( load_balancer_types, Meta.parse_meta(response) ) def get_all(self, name: str | None = None) -> list[BoundLoadBalancerType]: """Get all Load Balancer types :param name: str (optional) Can be used to filter Load Balancer type by their name. :return: List[:class:`BoundLoadBalancerType `] """ return self._iter_pages(self.get_list, name=name) def get_by_name(self, name: str) -> BoundLoadBalancerType | None: """Get Load Balancer type by name :param name: str Used to get Load Balancer type by name. :return: :class:`BoundLoadBalancerType ` """ return self._get_first_by(self.get_list, name=name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/load_balancer_types/domain.py0000644000175100017510000000346415152343177021635 0ustar00runnerrunnerfrom __future__ import annotations from typing import Any from ..core import BaseDomain, DomainIdentityMixin __all__ = [ "LoadBalancerType", ] 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: List of dict Prices in different locations """ __api_properties__ = ( "id", "name", "description", "max_connections", "max_services", "max_targets", "max_assigned_certificates", "prices", ) __slots__ = __api_properties__ def __init__( self, 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: list[dict[str, Any]] | None = 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1389022 hcloud-2.17.0/hcloud/load_balancers/0000755000175100017510000000000015152343221016732 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/load_balancers/__init__.py0000644000175100017510000000236515152343177021063 0ustar00runnerrunnerfrom __future__ import annotations from .client import ( BoundLoadBalancer, LoadBalancersClient, LoadBalancersPageResult, ) from .domain import ( CreateLoadBalancerResponse, GetMetricsResponse, IPv4Address, IPv6Network, LoadBalancer, LoadBalancerAlgorithm, LoadBalancerHealtCheckHttp, LoadBalancerHealthCheck, LoadBalancerHealthCheckHttp, LoadBalancerProtection, LoadBalancerService, LoadBalancerServiceHttp, LoadBalancerTarget, LoadBalancerTargetHealthStatus, LoadBalancerTargetIP, LoadBalancerTargetLabelSelector, MetricsType, PrivateNet, PublicNetwork, ) __all__ = [ "BoundLoadBalancer", "CreateLoadBalancerResponse", "GetMetricsResponse", "IPv4Address", "IPv6Network", "LoadBalancer", "LoadBalancerProtection", "LoadBalancerAlgorithm", "LoadBalancerHealtCheckHttp", "LoadBalancerHealthCheckHttp", "LoadBalancerHealthCheck", "LoadBalancerService", "LoadBalancerServiceHttp", "LoadBalancerTarget", "LoadBalancerTargetHealthStatus", "LoadBalancerTargetIP", "LoadBalancerTargetLabelSelector", "LoadBalancersClient", "LoadBalancersPageResult", "PrivateNet", "PublicNetwork", "MetricsType", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/load_balancers/client.py0000644000175100017510000011431215152343177020576 0ustar00runnerrunnerfrom __future__ import annotations from datetime import datetime from typing import TYPE_CHECKING, Any, NamedTuple from dateutil.parser import isoparse from ..actions import ( ActionSort, ActionsPageResult, ActionStatus, BoundAction, ResourceActionsClient, ) from ..actions.client import ResourceClientBaseActionsMixin from ..certificates import BoundCertificate from ..core import BoundModelBase, Meta, ResourceClientBase 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, LoadBalancerAlgorithm, LoadBalancerHealthCheck, LoadBalancerHealthCheckHttp, LoadBalancerService, LoadBalancerServiceHttp, LoadBalancerTarget, LoadBalancerTargetHealthStatus, LoadBalancerTargetIP, LoadBalancerTargetLabelSelector, MetricsType, PrivateNet, PublicNetwork, ) if TYPE_CHECKING: from .._client import Client from ..load_balancer_types import LoadBalancerType from ..locations import Location from ..networks import Network __all__ = [ "BoundLoadBalancer", "LoadBalancersPageResult", "LoadBalancersClient", ] class BoundLoadBalancer(BoundModelBase[LoadBalancer], LoadBalancer): _client: LoadBalancersClient model = LoadBalancer # pylint: disable=too-many-branches,too-many-locals def __init__( self, client: LoadBalancersClient, data: dict[str, Any], complete: bool = True, ): algorithm = data.get("algorithm") if algorithm: data["algorithm"] = LoadBalancerAlgorithm(type=algorithm["type"]) 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"]) data["public_net"] = PublicNetwork( ipv4=ipv4_address, ipv6=ipv6_network, enabled=public_net["enabled"] ) private_nets = data.get("private_net") if private_nets: private_nets = [ PrivateNet( network=BoundNetwork( client._parent.networks, {"id": private_net["network"]}, complete=False, ), ip=private_net["ip"], ) for private_net in private_nets ] data["private_net"] = private_nets def _load_balancer_targets( raw_targets: list[dict[str, Any]], ) -> list[LoadBalancerTarget]: return [_load_balancer_target(raw_target) for raw_target in raw_targets] def _load_balancer_target( raw_target: dict[str, Any], ) -> LoadBalancerTarget: result = LoadBalancerTarget(type=raw_target["type"]) if raw_target["type"] == "ip": result.ip = LoadBalancerTargetIP( ip=raw_target["ip"]["ip"], ) elif raw_target["type"] == "server": result.server = BoundServer( client._parent.servers, # pylint: disable=protected-access data=raw_target["server"], complete=False, ) result.use_private_ip = raw_target["use_private_ip"] elif raw_target["type"] == "label_selector": result.label_selector = LoadBalancerTargetLabelSelector( selector=raw_target["label_selector"]["selector"] ) result.use_private_ip = raw_target["use_private_ip"] if (raw_nested_targets := raw_target.get("targets")) is not None: result.targets = _load_balancer_targets(raw_nested_targets) if (raw_health_status := raw_target.get("health_status")) is not None: result.health_status = [ LoadBalancerTargetHealthStatus( listen_port=item["listen_port"], status=item["status"], ) for item in raw_health_status ] return result if (raw_targets := data.get("targets")) is not None: data["targets"] = _load_balancer_targets(raw_targets) services = data.get("services") if services: tmp_services = [] for service in services: tmp_service = LoadBalancerService( protocol=service["protocol"], listen_port=service["listen_port"], destination_port=service["destination_port"], proxyprotocol=service["proxyprotocol"], ) if service["protocol"] != "tcp": tmp_service.http = LoadBalancerServiceHttp( sticky_sessions=service["http"]["sticky_sessions"], redirect_http=service["http"]["redirect_http"], cookie_name=service["http"]["cookie_name"], cookie_lifetime=service["http"]["cookie_lifetime"], ) tmp_service.http.certificates = [ BoundCertificate( client._parent.certificates, {"id": certificate}, complete=False, ) for certificate in service["http"]["certificates"] ] tmp_service.health_check = LoadBalancerHealthCheck( protocol=service["health_check"]["protocol"], port=service["health_check"]["port"], interval=service["health_check"]["interval"], retries=service["health_check"]["retries"], timeout=service["health_check"]["timeout"], ) if tmp_service.health_check.protocol != "tcp": tmp_service.health_check.http = LoadBalancerHealthCheckHttp( domain=service["health_check"]["http"]["domain"], path=service["health_check"]["http"]["path"], response=service["health_check"]["http"]["response"], tls=service["health_check"]["http"]["tls"], status_codes=service["health_check"]["http"]["status_codes"], ) tmp_services.append(tmp_service) data["services"] = tmp_services load_balancer_type = data.get("load_balancer_type") if load_balancer_type is not None: data["load_balancer_type"] = BoundLoadBalancerType( client._parent.load_balancer_types, load_balancer_type ) location = data.get("location") if location is not None: data["location"] = BoundLocation(client._parent.locations, location) super().__init__(client, data, complete) 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) New name to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundLoadBalancer ` """ return self._client.update(self, name=name, labels=labels) def delete(self) -> bool: """Deletes a Load Balancer. :return: boolean """ 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[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Load Balancer. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._client.get_actions_list( self, status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Load Balancer. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ return self._client.get_actions(self, status=status, sort=sort) def add_service(self, service: LoadBalancerService) -> BoundAction: """Adds a service to a Load Balancer. :param service: :class:`LoadBalancerService ` The LoadBalancerService you want to add to the Load Balancer :return: :class:`BoundAction ` """ return self._client.add_service(self, service=service) def update_service(self, service: LoadBalancerService) -> BoundAction: """Updates a service of an Load Balancer. :param service: :class:`LoadBalancerService ` The LoadBalancerService you want to update :return: :class:`BoundAction ` """ return self._client.update_service(self, service=service) def delete_service(self, service: LoadBalancerService) -> BoundAction: """Deletes a service from a Load Balancer. :param service: :class:`LoadBalancerService ` The LoadBalancerService you want to delete from the Load Balancer :return: :class:`BoundAction ` """ return self._client.delete_service(self, service=service) def add_target(self, target: LoadBalancerTarget) -> BoundAction: """Adds a target to a Load Balancer. :param target: :class:`LoadBalancerTarget ` The LoadBalancerTarget you want to add to the Load Balancer :return: :class:`BoundAction ` """ return self._client.add_target(self, target=target) def remove_target(self, target: LoadBalancerTarget) -> BoundAction: """Removes a target from a Load Balancer. :param target: :class:`LoadBalancerTarget ` The LoadBalancerTarget you want to remove from the Load Balancer :return: :class:`BoundAction ` """ return self._client.remove_target(self, target=target) def change_algorithm(self, algorithm: LoadBalancerAlgorithm) -> BoundAction: """Changes the algorithm used by the Load Balancer :param algorithm: :class:`LoadBalancerAlgorithm ` The LoadBalancerAlgorithm you want to use :return: :class:`BoundAction ` """ return self._client.change_algorithm(self, algorithm=algorithm) 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 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=ip, dns_ptr=dns_ptr) def change_protection(self, delete: bool) -> BoundAction: """Changes the protection configuration of a Load Balancer. :param delete: boolean If True, prevents the Load Balancer from being deleted :return: :class:`BoundAction ` """ return self._client.change_protection(self, delete=delete) def attach_to_network( self, network: Network | BoundNetwork, ip: str | None = None, ip_range: str | None = None, ) -> BoundAction: """Attaches a Load Balancer to a Network :param network: :class:`BoundNetwork ` or :class:`Network ` :param ip: str IP to request to be assigned to this Load Balancer :param ip_range: str IP range in CIDR block notation of the subnet to attach to. :return: :class:`BoundAction ` """ return self._client.attach_to_network( self, network=network, ip=ip, ip_range=ip_range, ) def detach_from_network(self, network: Network | BoundNetwork) -> BoundAction: """Detaches a Load Balancer from a Network. :param network: :class:`BoundNetwork ` or :class:`Network ` :return: :class:`BoundAction ` """ return self._client.detach_from_network(self, network=network) 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) -> 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: LoadBalancerType | BoundLoadBalancerType, ) -> BoundAction: """Changes the type of a Load Balancer. :param load_balancer_type: :class:`BoundLoadBalancerType ` or :class:`LoadBalancerType ` Load Balancer type the Load Balancer should migrate to :return: :class:`BoundAction ` """ return self._client.change_type(self, load_balancer_type=load_balancer_type) class LoadBalancersPageResult(NamedTuple): load_balancers: list[BoundLoadBalancer] meta: Meta class LoadBalancersClient( ResourceClientBaseActionsMixin, ResourceClientBase, ): _base_url = "/load_balancers" actions: ResourceActionsClient """Load Balancers scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self.actions = ResourceActionsClient(client, self._base_url) def get_by_id(self, id: int) -> BoundLoadBalancer: """Get a specific Load Balancer :param id: int :return: :class:`BoundLoadBalancer ` """ response = self._client.request( url=f"{self._base_url}/{id}", method="GET", ) return BoundLoadBalancer(self, response["load_balancer"]) def get_list( self, 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) Can be used to filter Load Balancers by their name. :param label_selector: str (optional) Can be used to filter Load Balancers by labels. The response will only contain Load Balancers matching the label selector. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundLoadBalancer `], :class:`Meta `) """ params: dict[str, Any] = {} if name is not None: params["name"] = name 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 response = self._client.request(url=self._base_url, method="GET", params=params) load_balancers = [ BoundLoadBalancer(self, load_balancer_data) for load_balancer_data in response["load_balancers"] ] return LoadBalancersPageResult(load_balancers, Meta.parse_meta(response)) 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) Can be used to filter Load Balancers by their name. :param label_selector: str (optional) 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 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 :param name: str Used to get Load Balancer by name. :return: :class:`BoundLoadBalancer ` """ return self._get_first_by(self.get_list, name=name) def create( self, 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 Name of the Load Balancer :param load_balancer_type: LoadBalancerType Type of the Load Balancer :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :param location: Location Location of the Load Balancer :param network_zone: str Network Zone of the Load Balancer :param algorithm: LoadBalancerAlgorithm (optional) The algorithm the Load Balancer is currently using :param services: LoadBalancerService The services the Load Balancer is currently serving :param targets: LoadBalancerTarget The targets the Load Balancer is currently serving :param public_interface: bool Enable or disable the public interface of the Load Balancer :param network: Network Adds the Load Balancer to a Network :return: :class:`CreateLoadBalancerResponse ` """ 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: data["public_interface"] = public_interface if labels is not None: data["labels"] = labels if algorithm is not None: data["algorithm"] = {"type": algorithm.type} if services is not None: data["services"] = [service.to_payload() for service in services] if targets is not None: 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: data["location"] = location.id_or_name response = self._client.request(url=self._base_url, method="POST", json=data) return CreateLoadBalancerResponse( load_balancer=BoundLoadBalancer(self, response["load_balancer"]), action=BoundAction(self._parent.actions, response["action"]), ) 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 ` :param name: str (optional) New name to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundLoadBalancer ` """ data: dict[str, Any] = {} if name is not None: data.update({"name": name}) if labels is not None: data.update({"labels": labels}) response = self._client.request( url=f"{self._base_url}/{load_balancer.id}", method="PUT", json=data, ) return BoundLoadBalancer(self, response["load_balancer"]) def delete(self, load_balancer: LoadBalancer | BoundLoadBalancer) -> bool: """Deletes a Load Balancer. :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` :return: boolean """ self._client.request( url=f"{self._base_url}/{load_balancer.id}", method="DELETE", ) 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"{self._base_url}/{load_balancer.id}/metrics", method="GET", params=params, ) return GetMetricsResponse( metrics=Metrics(**response["metrics"]), ) def get_actions_list( self, load_balancer: LoadBalancer | BoundLoadBalancer, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Load Balancer. :param load_balancer: Load Balancer to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._get_actions_list( f"{self._base_url}/{load_balancer.id}", status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, load_balancer: LoadBalancer | BoundLoadBalancer, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Load Balancer. :param load_balancer: Load Balancer to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ return self._iter_pages( self.get_actions_list, load_balancer, status=status, sort=sort, ) 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 ` :param service: :class:`LoadBalancerService ` The LoadBalancerService you want to add to the Load Balancer :return: :class:`BoundAction ` """ data: dict[str, Any] = service.to_payload() response = self._client.request( url=f"{self._base_url}/{load_balancer.id}/actions/add_service", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) 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 ` :param service: :class:`LoadBalancerService ` The LoadBalancerService with updated values within for the Load Balancer :return: :class:`BoundAction ` """ data: dict[str, Any] = service.to_payload() response = self._client.request( url=f"{self._base_url}/{load_balancer.id}/actions/update_service", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) 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 ` :param service: :class:`LoadBalancerService ` The LoadBalancerService you want to delete from the Load Balancer :return: :class:`BoundAction ` """ data: dict[str, Any] = {"listen_port": service.listen_port} response = self._client.request( url=f"{self._base_url}/{load_balancer.id}/actions/delete_service", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) 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 ` :param target: :class:`LoadBalancerTarget ` The LoadBalancerTarget you want to add to the Load Balancer :return: :class:`BoundAction ` """ data: dict[str, Any] = target.to_payload() response = self._client.request( url=f"{self._base_url}/{load_balancer.id}/actions/add_target", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) 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 ` :param target: :class:`LoadBalancerTarget ` The LoadBalancerTarget you want to remove from the Load Balancer :return: :class:`BoundAction ` """ 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"{self._base_url}/{load_balancer.id}/actions/remove_target", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) 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 ` :param algorithm: :class:`LoadBalancerAlgorithm ` The LoadBalancerSubnet you want to add to the Load Balancer :return: :class:`BoundAction ` """ data: dict[str, Any] = {"type": algorithm.type} response = self._client.request( url=f"{self._base_url}/{load_balancer.id}/actions/change_algorithm", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) 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 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=f"{self._base_url}/{load_balancer.id}/actions/change_dns_ptr", method="POST", json={"ip": ip, "dns_ptr": dns_ptr}, ) return BoundAction(self._parent.actions, response["action"]) 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 ` :param delete: boolean If True, prevents the Load Balancer from being deleted :return: :class:`BoundAction ` """ data: dict[str, Any] = {} if delete is not None: data.update({"delete": delete}) response = self._client.request( url=f"{self._base_url}/{load_balancer.id}/actions/change_protection", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) def attach_to_network( self, load_balancer: LoadBalancer | BoundLoadBalancer, network: Network | BoundNetwork, ip: str | None = None, ip_range: str | None = None, ) -> BoundAction: """Attach a Load Balancer to a Network. :param load_balancer: :class:` ` or :class:`LoadBalancer ` :param network: :class:`BoundNetwork ` or :class:`Network ` :param ip: str IP to request to be assigned to this Load Balancer :param ip_range: str IP range in CIDR block notation of the subnet to attach to. :return: :class:`BoundAction ` """ data: dict[str, Any] = {"network": network.id} if ip is not None: data.update({"ip": ip}) if ip_range is not None: data.update({"ip_range": ip_range}) response = self._client.request( url=f"{self._base_url}/{load_balancer.id}/actions/attach_to_network", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) 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: dict[str, Any] = {"network": network.id} response = self._client.request( url=f"{self._base_url}/{load_balancer.id}/actions/detach_from_network", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) 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 ` :return: :class:`BoundAction ` """ response = self._client.request( url=f"{self._base_url}/{load_balancer.id}/actions/enable_public_interface", method="POST", ) return BoundAction(self._parent.actions, response["action"]) 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 ` :return: :class:`BoundAction ` """ response = self._client.request( url=f"{self._base_url}/{load_balancer.id}/actions/disable_public_interface", method="POST", ) return BoundAction(self._parent.actions, response["action"]) 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 ` :param load_balancer_type: :class:`BoundLoadBalancerType ` or :class:`LoadBalancerType ` Load Balancer type the Load Balancer should migrate to :return: :class:`BoundAction ` """ data: dict[str, Any] = {"load_balancer_type": load_balancer_type.id_or_name} response = self._client.request( url=f"{self._base_url}/{load_balancer.id}/actions/change_type", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/load_balancers/domain.py0000644000175100017510000005012515152343177020570 0ustar00runnerrunnerfrom __future__ import annotations import warnings from typing import TYPE_CHECKING, Any, Literal, TypedDict from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction from ..certificates import BoundCertificate from ..load_balancer_types import BoundLoadBalancerType from ..locations import BoundLocation from ..metrics import Metrics from ..networks import BoundNetwork, Network from ..servers import BoundServer from .client import BoundLoadBalancer __all__ = [ "LoadBalancer", "LoadBalancerProtection", "LoadBalancerService", "LoadBalancerServiceHttp", "LoadBalancerHealthCheck", "LoadBalancerHealthCheckHttp", "LoadBalancerHealtCheckHttp", "LoadBalancerTarget", "LoadBalancerTargetHealthStatus", "LoadBalancerTargetLabelSelector", "LoadBalancerTargetIP", "LoadBalancerAlgorithm", "PublicNetwork", "IPv4Address", "IPv6Network", "PrivateNet", "CreateLoadBalancerResponse", "GetMetricsResponse", "MetricsType", ] class LoadBalancer(BaseDomain, DomainIdentityMixin): """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 PrivateNet | None: """ Returns the load balancer's network attachment information in the given Network, and None if no attachment was found. """ for o in self.private_net or []: if o.network.id == network.id: return o return None class LoadBalancerProtection(TypedDict): delete: bool class LoadBalancerService(BaseDomain): """LoadBalancerService Domain :param protocol: str Protocol of the service Choices: tcp, http, https :param listen_port: int Required when protocol is tcp, must be unique per Load Balancer. :param destination_port: int Required when protocol is tcp :param proxyprotocol: bool Enable proxyprotocol :param health_check: LoadBalancerHealthCheck Configuration for health checks :param http: LoadBalancerServiceHttp Configuration for http/https protocols, required when protocol is http/https """ def __init__( self, 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 self.destination_port = destination_port self.proxyprotocol = proxyprotocol 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: 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 :param cookie_name: str Name of the cookie used for Session Stickness :param cookie_lifetime: str Lifetime of the cookie used for Session Stickness :param certificates: list IDs of the Certificates to use for TLS/SSL termination by the Load Balancer; empty for TLS/SSL passthrough or if protocol is "http" :param redirect_http: bool Redirect traffic from http port 80 to port 443 :param sticky_sessions: bool Use sticky sessions. Only available if protocol is "http" or "https". """ __api_properties__ = ( "cookie_name", "cookie_lifetime", "certificates", "redirect_http", "sticky_sessions", ) __slots__ = __api_properties__ def __init__( self, 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 self.certificates = certificates self.redirect_http = redirect_http self.sticky_sessions = sticky_sessions class LoadBalancerHealthCheck(BaseDomain): """LoadBalancerHealthCheck Domain :param protocol: str Protocol of the service Choices: tcp, http, https :param port: int Port the healthcheck will be performed on :param interval: int Interval we trigger health check in :param timeout: int Timeout in sec after a try is assumed as timeout :param retries: int Retries we perform until we assume a target as unhealthy :param http: LoadBalancerHealthCheckHttp HTTP Config """ __api_properties__ = ( "protocol", "port", "interval", "timeout", "retries", "http", ) __slots__ = __api_properties__ def __init__( self, protocol: str | None = None, port: int | None = None, interval: int | None = None, timeout: int | None = None, retries: int | None = None, http: LoadBalancerHealthCheckHttp | None = None, ): self.protocol = protocol self.port = port self.interval = interval self.timeout = timeout self.retries = retries self.http = http class LoadBalancerHealthCheckHttp(BaseDomain): """LoadBalancerHealthCheckHttp Domain :param domain: str Domain name to send in HTTP request. Can be null: In that case we will not send a domain name :param path: str HTTP Path send in Request :param response: str Optional HTTP response to receive in order to pass the health check :param status_codes: list List of HTTP status codes to receive in order to pass the health check :param tls: bool Type of health check """ __api_properties__ = ( "domain", "path", "response", "status_codes", "tls", ) __slots__ = __api_properties__ def __init__( self, domain: str | None = None, path: str | None = None, response: str | None = None, status_codes: list[str] | None = None, tls: bool | None = None, ): self.domain = domain self.path = path self.response = response self.status_codes = status_codes self.tls = tls class LoadBalancerHealtCheckHttp(LoadBalancerHealthCheckHttp): """ Kept for backward compatibility. .. deprecated:: 2.5.4 Use :class:``hcloud.load_balancers.domain.LoadBalancerHealthCheckHttp`` instead. """ def __init__( self, domain: str | None = None, path: str | None = None, response: str | None = None, status_codes: list[str] | None = None, tls: bool | None = None, ): warnings.warn( "The 'hcloud.load_balancers.domain.LoadBalancerHealtCheckHttp' class is deprecated, please use the " "'hcloud.load_balancers.domain.LoadBalancerHealthCheckHttp' class instead.", DeprecationWarning, stacklevel=2, ) super().__init__(domain, path, response, status_codes, tls) class LoadBalancerTarget(BaseDomain): """LoadBalancerTarget Domain :param type: str Type of the resource, can be server or label_selector :param server: Server Target server :param label_selector: LoadBalancerTargetLabelSelector Target label selector :param ip: LoadBalancerTargetIP 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". :param targets: list List of resolved label selector targets. Only present for target types "label_selector". """ __api_properties__ = ( "type", "server", "label_selector", "ip", "use_private_ip", "health_status", "targets", ) __slots__ = __api_properties__ def __init__( self, type: str | None = None, server: BoundServer | None = None, label_selector: LoadBalancerTargetLabelSelector | None = None, ip: LoadBalancerTargetIP | None = None, use_private_ip: bool | None = None, health_status: list[LoadBalancerTargetHealthStatus] | None = None, targets: list[LoadBalancerTarget] | 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 self.targets = targets def to_payload(self) -> dict[str, Any]: """ Generates the request payload from this domain object. """ 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 LoadBalancerTargetHealthStatus(BaseDomain): """LoadBalancerTargetHealthStatus Domain :param listen_port: Load Balancer Target listen port :param status: Load Balancer Target status. Choices: healthy, unhealthy, unknown """ __api_properties__ = ( "listen_port", "status", ) __slots__ = __api_properties__ 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 :param selector: str Target label selector """ __api_properties__ = ("selector",) __slots__ = __api_properties__ def __init__(self, selector: str | None = None): self.selector = selector class LoadBalancerTargetIP(BaseDomain): """LoadBalancerTargetIP Domain :param ip: str Target IP """ __api_properties__ = ("ip",) __slots__ = __api_properties__ def __init__(self, ip: str | None = None): self.ip = ip class LoadBalancerAlgorithm(BaseDomain): """LoadBalancerAlgorithm Domain :param type: str Algorithm of the Load Balancer. Choices: round_robin, least_connections """ __api_properties__ = ("type",) __slots__ = __api_properties__ def __init__(self, type: str | None = None): self.type = type class PublicNetwork(BaseDomain): """Public Network Domain :param ipv4: :class:`IPv4Address ` :param ipv6: :class:`IPv6Network ` :param enabled: boolean """ __api_properties__ = ("ipv4", "ipv6", "enabled") __slots__ = __api_properties__ def __init__( self, ipv4: IPv4Address, ipv6: IPv6Network, enabled: bool, ): self.ipv4 = ipv4 self.ipv6 = ipv6 self.enabled = enabled class IPv4Address(BaseDomain): """IPv4 Address Domain :param ip: str The IPv4 Address """ __api_properties__ = ("ip", "dns_ptr") __slots__ = __api_properties__ def __init__( self, ip: str, dns_ptr: str, ): self.ip = ip self.dns_ptr = dns_ptr class IPv6Network(BaseDomain): """IPv6 Network Domain :param ip: str The IPv6 Network as CIDR Notation """ __api_properties__ = ("ip", "dns_ptr") __slots__ = __api_properties__ def __init__( self, ip: str, dns_ptr: 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 """ __api_properties__ = ("network", "ip") __slots__ = __api_properties__ def __init__( self, network: BoundNetwork, ip: 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 """ __api_properties__ = ("load_balancer", "action") __slots__ = __api_properties__ def __init__( self, load_balancer: BoundLoadBalancer, action: BoundAction, ): 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 """ __api_properties__ = ("metrics",) __slots__ = __api_properties__ def __init__( self, metrics: Metrics, ): self.metrics = metrics ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1395307 hcloud-2.17.0/hcloud/locations/0000755000175100017510000000000015152343221015774 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/locations/__init__.py0000644000175100017510000000035715152343177020124 0ustar00runnerrunnerfrom __future__ import annotations from .client import BoundLocation, LocationsClient, LocationsPageResult from .domain import Location __all__ = [ "BoundLocation", "Location", "LocationsClient", "LocationsPageResult", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/locations/client.py0000644000175100017510000000522415152343177017641 0ustar00runnerrunnerfrom __future__ import annotations from typing import Any, NamedTuple from ..core import BoundModelBase, Meta, ResourceClientBase from .domain import Location __all__ = [ "BoundLocation", "LocationsPageResult", "LocationsClient", ] class BoundLocation(BoundModelBase[Location], Location): _client: LocationsClient model = Location class LocationsPageResult(NamedTuple): locations: list[BoundLocation] meta: Meta class LocationsClient(ResourceClientBase): _base_url = "/locations" def get_by_id(self, id: int) -> BoundLocation: """Get a specific location by its ID. :param id: int :return: :class:`BoundLocation ` """ response = self._client.request(url=f"{self._base_url}/{id}", method="GET") return BoundLocation(self, response["location"]) 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) Can be used to filter locations by their name. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundLocation `], :class:`Meta `) """ params: dict[str, Any] = {} if name is not None: params["name"] = name if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request(url=self._base_url, method="GET", params=params) locations = [ BoundLocation(self, location_data) for location_data in response["locations"] ] return LocationsPageResult(locations, Meta.parse_meta(response)) def get_all(self, name: str | None = None) -> list[BoundLocation]: """Get all locations :param name: str (optional) Can be used to filter locations by their name. :return: List[:class:`BoundLocation `] """ return self._iter_pages(self.get_list, name=name) def get_by_name(self, name: str) -> BoundLocation | None: """Get location by name :param name: str Used to get location by name. :return: :class:`BoundLocation ` """ return self._get_first_by(self.get_list, name=name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/locations/domain.py0000644000175100017510000000305315152343177017630 0ustar00runnerrunnerfrom __future__ import annotations from ..core import BaseDomain, DomainIdentityMixin __all__ = [ "Location", ] class Location(BaseDomain, DomainIdentityMixin): """Location Domain :param id: int ID of location :param name: str Name of location :param description: str Description of location :param country: str ISO 3166-1 alpha-2 code of the country the location resides in :param city: str City the location is closest to :param latitude: float Latitude of the city closest to the location :param longitude: float Longitude of the city closest to the location :param network_zone: str Name of network zone this location resides in """ __api_properties__ = ( "id", "name", "description", "country", "city", "latitude", "longitude", "network_zone", ) __slots__ = __api_properties__ def __init__( self, 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 self.description = description self.country = country self.city = city self.latitude = latitude self.longitude = longitude self.network_zone = network_zone ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1398935 hcloud-2.17.0/hcloud/metrics/0000755000175100017510000000000015152343221015447 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/metrics/__init__.py0000644000175100017510000000017415152343177017574 0ustar00runnerrunnerfrom __future__ import annotations from .domain import Metrics, TimeSeries __all__ = [ "Metrics", "TimeSeries", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/metrics/domain.py0000644000175100017510000000223715152343177017306 0ustar00runnerrunnerfrom __future__ import annotations from datetime import datetime from typing import Literal from ..core import BaseDomain __all__ = [ "TimeSeries", "Metrics", ] 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 __api_properties__ = ( "start", "end", "step", "time_series", ) __slots__ = __api_properties__ def __init__( self, start: str, end: str, step: float, time_series: TimeSeries, ): self.start = self._parse_datetime(start) self.end = self._parse_datetime(end) self.step = step self.time_series = time_series ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1404564 hcloud-2.17.0/hcloud/networks/0000755000175100017510000000000015152343221015655 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/networks/__init__.py0000644000175100017510000000064615152343177020006 0ustar00runnerrunnerfrom __future__ import annotations from .client import BoundNetwork, NetworksClient, NetworksPageResult from .domain import ( CreateNetworkResponse, Network, NetworkProtection, NetworkRoute, NetworkSubnet, ) __all__ = [ "BoundNetwork", "CreateNetworkResponse", "Network", "NetworkProtection", "NetworkRoute", "NetworkSubnet", "NetworksClient", "NetworksPageResult", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/networks/client.py0000644000175100017510000005005115152343177017520 0ustar00runnerrunnerfrom __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ( ActionSort, ActionsPageResult, ActionStatus, BoundAction, ResourceActionsClient, ) from ..actions.client import ResourceClientBaseActionsMixin from ..core import BoundModelBase, Meta, ResourceClientBase from .domain import Network, NetworkRoute, NetworkSubnet if TYPE_CHECKING: from .._client import Client __all__ = [ "BoundNetwork", "NetworksPageResult", "NetworksClient", ] class BoundNetwork(BoundModelBase[Network], Network): _client: NetworksClient model = Network 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] data["subnets"] = subnets routes = data.get("routes", []) if routes is not None: 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", []) if servers is not None: servers = [ BoundServer(client._parent.servers, {"id": server}, complete=False) for server in servers ] data["servers"] = servers super().__init__(client, data, complete) def update( self, 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) 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=name, expose_routes_to_vswitch=expose_routes_to_vswitch, labels=labels, ) def delete(self) -> bool: """Deletes a network. :return: boolean """ return self._client.delete(self) def get_actions_list( self, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Network. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._client.get_actions_list( self, status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Network. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ return self._client.get_actions(self, status=status, sort=sort) def add_subnet(self, subnet: NetworkSubnet) -> BoundAction: """Adds a subnet entry to a network. :param subnet: :class:`NetworkSubnet ` The NetworkSubnet you want to add to the Network :return: :class:`BoundAction ` """ return self._client.add_subnet(self, subnet=subnet) def delete_subnet(self, subnet: NetworkSubnet) -> BoundAction: """Removes a subnet entry from a network :param subnet: :class:`NetworkSubnet ` The NetworkSubnet you want to remove from the Network :return: :class:`BoundAction ` """ return self._client.delete_subnet(self, subnet=subnet) def add_route(self, route: NetworkRoute) -> BoundAction: """Adds a route entry to a network. :param route: :class:`NetworkRoute ` The NetworkRoute you want to add to the Network :return: :class:`BoundAction ` """ return self._client.add_route(self, route=route) def delete_route(self, route: NetworkRoute) -> BoundAction: """Removes a route entry to a network. :param route: :class:`NetworkRoute ` The NetworkRoute you want to remove from the Network :return: :class:`BoundAction ` """ return self._client.delete_route(self, route=route) def change_ip_range(self, ip_range: str) -> BoundAction: """Changes the IP range of a network. :param ip_range: str The new prefix for the whole network. :return: :class:`BoundAction ` """ return self._client.change_ip_range(self, ip_range=ip_range) def change_protection(self, delete: bool | None = None) -> BoundAction: """Changes the protection configuration of a network. :param delete: boolean If True, prevents the network from being deleted :return: :class:`BoundAction ` """ return self._client.change_protection(self, delete=delete) class NetworksPageResult(NamedTuple): networks: list[BoundNetwork] meta: Meta class NetworksClient( ResourceClientBaseActionsMixin, ResourceClientBase, ): _base_url = "/networks" actions: ResourceActionsClient """Networks scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self.actions = ResourceActionsClient(client, self._base_url) def get_by_id(self, id: int) -> BoundNetwork: """Get a specific network :param id: int :return: :class:`BoundNetwork ` """ response = self._client.request(url=f"{self._base_url}/{id}", method="GET") return BoundNetwork(self, response["network"]) def get_list( self, 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) Can be used to filter networks by their name. :param label_selector: str (optional) Can be used to filter networks by labels. The response will only contain networks matching the label selector. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundNetwork `], :class:`Meta `) """ params: dict[str, Any] = {} if name is not None: params["name"] = name 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 response = self._client.request(url=self._base_url, method="GET", params=params) networks = [ BoundNetwork(self, network_data) for network_data in response["networks"] ] return NetworksPageResult(networks, Meta.parse_meta(response)) 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) Can be used to filter networks by their name. :param label_selector: str (optional) Can be used to filter networks by labels. The response will only contain networks matching the label selector. :return: List[:class:`BoundNetwork `] """ 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 :param name: str Used to get network by name. :return: :class:`BoundNetwork ` """ return self._get_first_by(self.get_list, name=name) def create( self, name: str, ip_range: str, subnets: list[NetworkSubnet] | None = None, routes: list[NetworkRoute] | None = None, expose_routes_to_vswitch: bool | None = None, labels: dict[str, str] | None = None, ) -> BoundNetwork: """Creates a network with range ip_range. :param name: str Name of the network :param ip_range: str IP range of the whole network which must span all included subnets and route destinations :param subnets: List[:class:`NetworkSubnet `] 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: dict[str, Any] = {"name": name, "ip_range": ip_range} if subnets is not None: data_subnets = [] for subnet in subnets: data_subnet: dict[str, Any] = { "type": subnet.type, "ip_range": subnet.ip_range, "network_zone": subnet.network_zone, } 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 response = self._client.request(url=self._base_url, method="POST", json=data) return BoundNetwork(self, response["network"]) 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 ` :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 ` """ data: dict[str, Any] = {} 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"{self._base_url}/{network.id}", method="PUT", json=data, ) return BoundNetwork(self, response["network"]) def delete(self, network: Network | BoundNetwork) -> bool: """Deletes a network. :param network: :class:`BoundNetwork ` or :class:`Network ` :return: boolean """ self._client.request(url=f"{self._base_url}/{network.id}", method="DELETE") return True def get_actions_list( self, network: Network | BoundNetwork, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Network. :param network: Network to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._get_actions_list( f"{self._base_url}/{network.id}", status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, network: Network | BoundNetwork, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Network. :param network: Network to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ return self._iter_pages( self.get_actions_list, network, status=status, sort=sort, ) def add_subnet( self, network: Network | BoundNetwork, subnet: NetworkSubnet, ) -> BoundAction: """Adds a subnet entry to a network. :param network: :class:`BoundNetwork ` or :class:`Network ` :param subnet: :class:`NetworkSubnet ` The NetworkSubnet you want to add to the Network :return: :class:`BoundAction ` """ data: dict[str, Any] = { "type": subnet.type, "network_zone": subnet.network_zone, } if subnet.ip_range is not None: data["ip_range"] = subnet.ip_range if subnet.vswitch_id is not None: data["vswitch_id"] = subnet.vswitch_id response = self._client.request( url=f"{self._base_url}/{network.id}/actions/add_subnet", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) def delete_subnet( self, network: Network | BoundNetwork, subnet: NetworkSubnet, ) -> BoundAction: """Removes a subnet entry from a network :param network: :class:`BoundNetwork ` or :class:`Network ` :param subnet: :class:`NetworkSubnet ` The NetworkSubnet you want to remove from the Network :return: :class:`BoundAction ` """ data: dict[str, Any] = {"ip_range": subnet.ip_range} response = self._client.request( url=f"{self._base_url}/{network.id}/actions/delete_subnet", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) def add_route( self, network: Network | BoundNetwork, route: NetworkRoute, ) -> BoundAction: """Adds a route entry to a network. :param network: :class:`BoundNetwork ` or :class:`Network ` :param route: :class:`NetworkRoute ` The NetworkRoute you want to add to the Network :return: :class:`BoundAction ` """ data: dict[str, Any] = { "destination": route.destination, "gateway": route.gateway, } response = self._client.request( url=f"{self._base_url}/{network.id}/actions/add_route", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) def delete_route( self, network: Network | BoundNetwork, route: NetworkRoute, ) -> BoundAction: """Removes a route entry to a network. :param network: :class:`BoundNetwork ` or :class:`Network ` :param route: :class:`NetworkRoute ` The NetworkRoute you want to remove from the Network :return: :class:`BoundAction ` """ data: dict[str, Any] = { "destination": route.destination, "gateway": route.gateway, } response = self._client.request( url=f"{self._base_url}/{network.id}/actions/delete_route", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) 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 ` :param ip_range: str The new prefix for the whole network. :return: :class:`BoundAction ` """ data: dict[str, Any] = {"ip_range": ip_range} response = self._client.request( url=f"{self._base_url}/{network.id}/actions/change_ip_range", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) 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 ` :param delete: boolean If True, prevents the network from being deleted :return: :class:`BoundAction ` """ data: dict[str, Any] = {} if delete is not None: data.update({"delete": delete}) response = self._client.request( url=f"{self._base_url}/{network.id}/actions/change_protection", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/networks/domain.py0000644000175100017510000001207515152343177017515 0ustar00runnerrunnerfrom __future__ import annotations import warnings from typing import TYPE_CHECKING, TypedDict from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction from ..servers import BoundServer from .client import BoundNetwork __all__ = [ "Network", "NetworkProtection", "NetworkSubnet", "NetworkRoute", "CreateNetworkResponse", ] class Network(BaseDomain, DomainIdentityMixin): """Network Domain :param id: int ID of the network :param name: str Name of the network :param ip_range: str IPv4 prefix of the whole network :param subnets: List[:class:`NetworkSubnet `] 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 Protection configuration for the network :param labels: dict User-defined labels (key-value pairs) """ __api_properties__ = ( "id", "name", "ip_range", "subnets", "routes", "expose_routes_to_vswitch", "servers", "protection", "labels", "created", ) __slots__ = __api_properties__ def __init__( self, 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: NetworkProtection | None = None, labels: dict[str, str] | None = None, ): self.id = id self.name = name self.created = self._parse_datetime(created) 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 class NetworkProtection(TypedDict): delete: bool class NetworkSubnet(BaseDomain): """Network Subnet Domain :param type: str Type of sub network. :param ip_range: str Range to allocate IPs from. :param network_zone: str Name of network zone. :param gateway: str Gateway for the route. :param vswitch_id: int ID of the vSwitch. """ @property def TYPE_SERVER(self) -> str: # pylint: disable=invalid-name """ Used to connect cloud servers and load balancers. .. deprecated:: 2.2.0 Use :attr:`NetworkSubnet.TYPE_CLOUD` instead. """ warnings.warn( "The 'NetworkSubnet.TYPE_SERVER' property is deprecated, please use the `NetworkSubnet.TYPE_CLOUD` property instead.", DeprecationWarning, stacklevel=2, ) return "server" TYPE_CLOUD = "cloud" """ Used to connect cloud servers and load balancers. """ TYPE_VSWITCH = "vswitch" """ Used to connect cloud servers and load balancers with dedicated servers. See https://docs.hetzner.com/networking/networks/connect-dedi-vswitch/ """ __api_properties__ = ("type", "ip_range", "network_zone", "gateway", "vswitch_id") __slots__ = __api_properties__ def __init__( self, ip_range: str, type: str | None = None, network_zone: str | None = None, gateway: str | None = None, vswitch_id: int | None = None, ): self.type = type self.ip_range = ip_range self.network_zone = network_zone self.gateway = gateway self.vswitch_id = vswitch_id class NetworkRoute(BaseDomain): """Network Route Domain :param destination: str Destination network or host of this route. :param gateway: str Gateway for the route. """ __api_properties__ = ("destination", "gateway") __slots__ = __api_properties__ def __init__(self, destination: str, gateway: str): self.destination = destination self.gateway = gateway class CreateNetworkResponse(BaseDomain): """Create Network Response Domain :param network: :class:`BoundNetwork ` The network which was created :param action: :class:`BoundAction ` The Action which shows the progress of the network Creation """ __api_properties__ = ("network", "action") __slots__ = __api_properties__ def __init__( self, network: BoundNetwork, action: BoundAction, ): self.network = network self.action = action ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1410162 hcloud-2.17.0/hcloud/placement_groups/0000755000175100017510000000000015152343221017350 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/placement_groups/__init__.py0000644000175100017510000000056215152343177021476 0ustar00runnerrunnerfrom __future__ import annotations from .client import ( BoundPlacementGroup, PlacementGroupsClient, PlacementGroupsPageResult, ) from .domain import CreatePlacementGroupResponse, PlacementGroup __all__ = [ "BoundPlacementGroup", "CreatePlacementGroupResponse", "PlacementGroup", "PlacementGroupsClient", "PlacementGroupsPageResult", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/placement_groups/client.py0000644000175100017510000001715415152343177021222 0ustar00runnerrunnerfrom __future__ import annotations from typing import Any, NamedTuple from ..actions import BoundAction from ..core import BoundModelBase, Meta, ResourceClientBase from .domain import CreatePlacementGroupResponse, PlacementGroup __all__ = [ "BoundPlacementGroup", "PlacementGroupsPageResult", "PlacementGroupsClient", ] class BoundPlacementGroup(BoundModelBase[PlacementGroup], PlacementGroup): _client: PlacementGroupsClient model = PlacementGroup 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) User-defined labels (key-value pairs) :param name: str, (optional) New Name to set :return: :class:`BoundPlacementGroup ` """ return self._client.update(self, labels=labels, name=name) def delete(self) -> bool: """Deletes a Placement Group :return: boolean """ return self._client.delete(self) class PlacementGroupsPageResult(NamedTuple): placement_groups: list[BoundPlacementGroup] meta: Meta class PlacementGroupsClient(ResourceClientBase): _base_url = "/placement_groups" def get_by_id(self, id: int) -> BoundPlacementGroup: """Returns a specific Placement Group object :param id: int :return: :class:`BoundPlacementGroup ` """ response = self._client.request( url=f"{self._base_url}/{id}", method="GET", ) return BoundPlacementGroup(self, response["placement_group"]) def get_list( self, 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) Can be used to filter Placement Groups by labels. The response will only contain Placement Groups matching the label selector 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 Placement Groups by their name. :param sort: List[str] (optional) Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) :return: (List[:class:`BoundPlacementGroup `], :class:`Meta `) """ params: dict[str, Any] = {} 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 sort is not None: params["sort"] = sort if type is not None: params["type"] = type response = self._client.request(url=self._base_url, method="GET", params=params) placement_groups = [ BoundPlacementGroup(self, placement_group_data) for placement_group_data in response["placement_groups"] ] return PlacementGroupsPageResult(placement_groups, Meta.parse_meta(response)) 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) Can be used to filter Placement Groups by labels. The response will only contain Placement Groups matching the label selector values. :param name: str (optional) Can be used to filter Placement Groups by their name. :param sort: List[str] (optional) Choices: id name created (You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default)) :return: List[:class:`BoundPlacementGroup `] """ 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 :param name: str Used to get Placement Group by name :return: class:`BoundPlacementGroup ` """ return self._get_first_by(self.get_list, name=name) def create( self, name: str, type: str, labels: dict[str, str] | None = None, ) -> CreatePlacementGroupResponse: """Creates a new Placement Group. :param name: str Placement Group Name :param type: str Type of the Placement Group :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`CreatePlacementGroupResponse ` """ data: dict[str, Any] = {"name": name, "type": type} if labels is not None: data["labels"] = labels response = self._client.request(url=self._base_url, json=data, method="POST") action = None if response.get("action") is not None: action = BoundAction(self._parent.actions, response["action"]) result = CreatePlacementGroupResponse( placement_group=BoundPlacementGroup(self, response["placement_group"]), action=action, ) return result 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 ` :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :param name: str (optional) New name to set :return: :class:`BoundPlacementGroup ` """ data: dict[str, Any] = {} if labels is not None: data["labels"] = labels if name is not None: data["name"] = name response = self._client.request( url=f"{self._base_url}/{placement_group.id}", method="PUT", json=data, ) return BoundPlacementGroup(self, response["placement_group"]) def delete(self, placement_group: PlacementGroup | BoundPlacementGroup) -> bool: """Deletes a Placement Group. :param placement_group: :class:`BoundPlacementGroup ` or :class:`PlacementGroup ` :return: boolean """ self._client.request( url=f"{self._base_url}/{placement_group.id}", method="DELETE", ) return True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/placement_groups/domain.py0000644000175100017510000000424715152343177021212 0ustar00runnerrunnerfrom __future__ import annotations from typing import TYPE_CHECKING from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction from .client import BoundPlacementGroup __all__ = [ "PlacementGroup", "CreatePlacementGroupResponse", ] class PlacementGroup(BaseDomain, DomainIdentityMixin): """Placement Group Domain :param id: int ID of the Placement Group :param name: str Name of the Placement Group :param labels: dict User-defined labels (key-value pairs) :param servers: List[ int ] List of server IDs assigned to the Placement Group :param type: str Type of the Placement Group :param created: datetime Point in time when the image was created """ __api_properties__ = ("id", "name", "labels", "servers", "type", "created") __slots__ = __api_properties__ """Placement Group type spread spreads all servers in the group on different vhosts """ TYPE_SPREAD = "spread" def __init__( self, id: int | None = None, name: str | None = None, labels: dict[str, str] | None = None, servers: list[int] | None = None, type: str | None = None, created: str | None = None, ): self.id = id self.name = name self.labels = labels self.servers = servers self.type = type self.created = self._parse_datetime(created) class CreatePlacementGroupResponse(BaseDomain): """Create Placement Group Response Domain :param placement_group: :class:`BoundPlacementGroup ` The Placement Group which was created :param action: :class:`BoundAction ` The Action which shows the progress of the Placement Group Creation """ __api_properties__ = ("placement_group", "action") __slots__ = __api_properties__ def __init__( self, placement_group: BoundPlacementGroup, action: BoundAction | None, ): self.placement_group = placement_group self.action = action ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1415808 hcloud-2.17.0/hcloud/primary_ips/0000755000175100017510000000000015152343221016337 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/primary_ips/__init__.py0000644000175100017510000000053715152343177020467 0ustar00runnerrunnerfrom __future__ import annotations from .client import BoundPrimaryIP, PrimaryIPsClient, PrimaryIPsPageResult from .domain import CreatePrimaryIPResponse, PrimaryIP, PrimaryIPProtection __all__ = [ "BoundPrimaryIP", "CreatePrimaryIPResponse", "PrimaryIP", "PrimaryIPProtection", "PrimaryIPsClient", "PrimaryIPsPageResult", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/primary_ips/client.py0000644000175100017510000004334015152343177020205 0ustar00runnerrunnerfrom __future__ import annotations import warnings from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ( ActionSort, ActionsPageResult, ActionStatus, BoundAction, ResourceActionsClient, ) from ..actions.client import ResourceClientBaseActionsMixin from ..core import BoundModelBase, Meta, ResourceClientBase from .domain import CreatePrimaryIPResponse, PrimaryIP if TYPE_CHECKING: from .._client import Client from ..datacenters import BoundDatacenter, Datacenter from ..locations import BoundLocation, Location __all__ = [ "BoundPrimaryIP", "PrimaryIPsPageResult", "PrimaryIPsClient", ] class BoundPrimaryIP(BoundModelBase[PrimaryIP], PrimaryIP): _client: PrimaryIPsClient model = PrimaryIP def __init__( self, client: PrimaryIPsClient, data: dict[str, Any], complete: bool = True, ): # pylint: disable=import-outside-toplevel from ..datacenters import BoundDatacenter from ..locations import BoundLocation raw = data.get("datacenter", {}) if raw: data["datacenter"] = BoundDatacenter(client._parent.datacenters, raw) raw = data.get("location", {}) if raw: data["location"] = BoundLocation(client._parent.locations, raw) super().__init__(client, data, complete) def get_actions_list( self, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Primary IP. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._client.get_actions_list( self, status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Primary IP. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ return self._client.get_actions( self, status=status, sort=sort, ) 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) 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) -> 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: bool | None = None) -> 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=delete) def assign(self, assignee_id: int, assignee_type: 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_id, assignee_type=assignee_type ) 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: str, dns_ptr: 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=ip, dns_ptr=dns_ptr) class PrimaryIPsPageResult(NamedTuple): primary_ips: list[BoundPrimaryIP] meta: Meta class PrimaryIPsClient( ResourceClientBaseActionsMixin, ResourceClientBase, ): _base_url = "/primary_ips" actions: ResourceActionsClient """Primary IPs scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self.actions = ResourceActionsClient(client, self._base_url) def get_actions_list( self, primary_ip: PrimaryIP | BoundPrimaryIP, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Primary IP. :param primary_ip: Primary IP to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._get_actions_list( f"{self._base_url}/{primary_ip.id}", status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, primary_ip: PrimaryIP | BoundPrimaryIP, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Primary IP. :param primary_ip: Primary IP to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ return self._iter_pages( self.get_actions_list, primary_ip, status=status, sort=sort, ) def get_by_id(self, id: int) -> BoundPrimaryIP: """Returns a specific Primary IP object. :param id: int :return: :class:`BoundPrimaryIP ` """ response = self._client.request(url=f"{self._base_url}/{id}", method="GET") return BoundPrimaryIP(self, response["primary_ip"]) def get_list( self, 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) 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: dict[str, Any] = {} 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=self._base_url, method="GET", params=params) primary_ips = [ BoundPrimaryIP(self, primary_ip_data) for primary_ip_data in response["primary_ips"] ] return PrimaryIPsPageResult(primary_ips, Meta.parse_meta(response)) 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) 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 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 :param name: str Used to get Primary IP by name. :return: :class:`BoundPrimaryIP ` """ return self._get_first_by(self.get_list, name=name) def create( self, type: str, name: str, datacenter: Datacenter | BoundDatacenter | None = None, location: Location | BoundLocation | None = None, assignee_type: str | None = "server", assignee_id: int | None = None, auto_delete: bool | None = False, labels: dict[str, str] | None = None, ) -> CreatePrimaryIPResponse: """Creates a new Primary IP assigned to a server. :param type: str Primary IP type Choices: ipv4, ipv6 :param name: str :param datacenter: Datacenter (optional) :param location: Location (optional) :param assignee_type: str (optional) :param assignee_id: int (optional) :param auto_delete: bool (optional) :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`CreatePrimaryIPResponse ` """ data: dict[str, Any] = { "name": name, "type": type, "assignee_type": assignee_type, "auto_delete": auto_delete, } if datacenter is not None: warnings.warn( "The 'datacenter' argument is deprecated and will be removed after 1 July 2026. " "Please use the 'location' argument instead. " "See https://docs.hetzner.cloud/changelog#2025-12-16-phasing-out-datacenters", DeprecationWarning, stacklevel=2, ) data["datacenter"] = datacenter.id_or_name if location is not None: data["location"] = location.id_or_name if assignee_id is not None: data["assignee_id"] = assignee_id if labels is not None: data["labels"] = labels response = self._client.request(url=self._base_url, json=data, method="POST") action = None if response.get("action") is not None: action = BoundAction(self._parent.actions, response["action"]) result = CreatePrimaryIPResponse( primary_ip=BoundPrimaryIP(self, response["primary_ip"]), action=action ) return result 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 ` :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: dict[str, Any] = {} 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=f"{self._base_url}/{primary_ip.id}", method="PUT", json=data, ) return BoundPrimaryIP(self, response["primary_ip"]) 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 ` :return: boolean """ self._client.request( url=f"{self._base_url}/{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: PrimaryIP | BoundPrimaryIP, delete: bool | None = None, ) -> 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: dict[str, Any] = {} if delete is not None: data.update({"delete": delete}) response = self._client.request( url=f"{self._base_url}/{primary_ip.id}/actions/change_protection", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) 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 ` :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=f"{self._base_url}/{primary_ip.id}/actions/assign", method="POST", json={"assignee_id": assignee_id, "assignee_type": assignee_type}, ) return BoundAction(self._parent.actions, response["action"]) 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 ` :return: :class:`BoundAction ` """ response = self._client.request( url=f"{self._base_url}/{primary_ip.id}/actions/unassign", method="POST", ) return BoundAction(self._parent.actions, response["action"]) 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 ` :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=f"{self._base_url}/{primary_ip.id}/actions/change_dns_ptr", method="POST", json={"ip": ip, "dns_ptr": dns_ptr}, ) return BoundAction(self._parent.actions, response["action"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/primary_ips/domain.py0000644000175100017510000001117515152343177020177 0ustar00runnerrunnerfrom __future__ import annotations import warnings from typing import TYPE_CHECKING, TypedDict from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction from ..datacenters import BoundDatacenter from ..locations import BoundLocation from ..rdns import DNSPtr from .client import BoundPrimaryIP __all__ = [ "PrimaryIP", "PrimaryIPProtection", "CreatePrimaryIPResponse", ] class PrimaryIP(BaseDomain, DomainIdentityMixin): """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. This property is deprecated and will be removed after 1 July 2026. Please use the ``location`` property instead. See https://docs.hetzner.cloud/changelog#2025-12-16-phasing-out-datacenters. :param location: :class:`Location ` Location 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. """ __properties__ = ( "id", "ip", "type", "dns_ptr", "location", "blocked", "protection", "labels", "created", "name", "assignee_id", "assignee_type", "auto_delete", ) __api_properties__ = ( *__properties__, "datacenter", ) __slots__ = ( *__properties__, "_datacenter", ) def __init__( self, id: int | None = None, type: str | None = None, ip: str | None = None, dns_ptr: list[DNSPtr] | None = None, datacenter: BoundDatacenter | None = None, location: BoundLocation | None = None, blocked: bool | 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, assignee_type: str | None = None, auto_delete: bool | None = None, ): self.id = id self.type = type self.ip = ip self.dns_ptr = dns_ptr self.datacenter = datacenter self.location = location self.blocked = blocked self.protection = protection self.labels = labels self.created = self._parse_datetime(created) self.name = name self.assignee_id = assignee_id self.assignee_type = assignee_type self.auto_delete = auto_delete @property def datacenter(self) -> BoundDatacenter | None: """ :meta private: """ warnings.warn( "The 'datacenter' property is deprecated and will be removed after 1 July 2026. " "Please use the 'location' property instead. " "See https://docs.hetzner.cloud/changelog#2025-12-16-phasing-out-datacenters.", DeprecationWarning, stacklevel=2, ) return self._datacenter @datacenter.setter def datacenter(self, value: BoundDatacenter | None) -> None: self._datacenter = value class PrimaryIPProtection(TypedDict): delete: bool 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 """ __api_properties__ = ("primary_ip", "action") __slots__ = __api_properties__ def __init__( self, primary_ip: BoundPrimaryIP, action: BoundAction | None, ): self.primary_ip = primary_ip self.action = action ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/py.typed0000644000175100017510000000003315152343177015506 0ustar00runnerrunner# Marker file for PEP 561. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1419601 hcloud-2.17.0/hcloud/rdns/0000755000175100017510000000000015152343221014747 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/rdns/__init__.py0000644000175100017510000000013415152343177017070 0ustar00runnerrunnerfrom __future__ import annotations from .domain import DNSPtr __all__ = [ "DNSPtr", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/rdns/domain.py0000644000175100017510000000022615152343177016602 0ustar00runnerrunnerfrom __future__ import annotations from typing import TypedDict __all__ = [ "DNSPtr", ] class DNSPtr(TypedDict): ip: str dns_ptr: str ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1425157 hcloud-2.17.0/hcloud/server_types/0000755000175100017510000000000015152343221016533 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/server_types/__init__.py0000644000175100017510000000047615152343177020665 0ustar00runnerrunnerfrom __future__ import annotations from .client import ( BoundServerType, ServerTypesClient, ServerTypesPageResult, ) from .domain import ServerType, ServerTypeLocation __all__ = [ "BoundServerType", "ServerType", "ServerTypeLocation", "ServerTypesClient", "ServerTypesPageResult", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/server_types/client.py0000644000175100017510000000663315152343177020405 0ustar00runnerrunnerfrom __future__ import annotations from typing import Any, NamedTuple from ..core import BoundModelBase, Meta, ResourceClientBase from ..locations import BoundLocation from .domain import ServerType, ServerTypeLocation __all__ = [ "BoundServerType", "ServerTypesPageResult", "ServerTypesClient", ] class BoundServerType(BoundModelBase[ServerType], ServerType): _client: ServerTypesClient model = ServerType def __init__( self, client: ServerTypesClient, data: dict[str, Any], complete: bool = True, ): raw = data.get("locations") if raw is not None: data["locations"] = [ ServerTypeLocation.from_dict( { "location": BoundLocation( client._parent.locations, o, complete=False ), **o, } ) for o in raw ] super().__init__(client, data, complete) class ServerTypesPageResult(NamedTuple): server_types: list[BoundServerType] meta: Meta class ServerTypesClient(ResourceClientBase): _base_url = "/server_types" def get_by_id(self, id: int) -> BoundServerType: """Returns a specific Server Type. :param id: int :return: :class:`BoundServerType ` """ response = self._client.request(url=f"{self._base_url}/{id}", method="GET") return BoundServerType(self, response["server_type"]) 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) Can be used to filter server type by their name. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundServerType `], :class:`Meta `) """ params: dict[str, Any] = {} if name is not None: params["name"] = name if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request(url=self._base_url, method="GET", params=params) server_types = [ BoundServerType(self, server_type_data) for server_type_data in response["server_types"] ] return ServerTypesPageResult(server_types, Meta.parse_meta(response)) def get_all(self, name: str | None = None) -> list[BoundServerType]: """Get all Server types :param name: str (optional) Can be used to filter server type by their name. :return: List[:class:`BoundServerType `] """ return self._iter_pages(self.get_list, name=name) def get_by_name(self, name: str) -> BoundServerType | None: """Get Server type by name :param name: str Used to get Server type by name. :return: :class:`BoundServerType ` """ return self._get_first_by(self.get_list, name=name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/server_types/domain.py0000644000175100017510000001552515152343177020376 0ustar00runnerrunnerfrom __future__ import annotations import warnings from typing import Any from ..core import BaseDomain, DomainIdentityMixin from ..deprecation import DeprecationInfo from ..locations import BoundLocation __all__ = [ "ServerType", "ServerTypeLocation", ] class ServerType(BaseDomain, DomainIdentityMixin): """ServerType Domain :param id: int ID of the server type :param name: str Unique identifier of the server type :param description: str Description of the server type :param category: str Category of the Server Type. :param cores: int Number of cpu cores a server of this type will have :param memory: int Memory a server of this type will have in GB :param disk: int Disk size a server of this type will have in GB :param prices: List of dict Prices in different locations :param storage_type: str 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. 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 :param locations: Supported Location of the Server Type. """ __properties__ = ( "id", "name", "description", "category", "cores", "memory", "disk", "prices", "storage_type", "cpu_type", "architecture", "locations", ) __api_properties__ = ( *__properties__, "deprecated", "deprecation", "included_traffic", ) __slots__ = ( *__properties__, "_deprecated", "_deprecation", "_included_traffic", ) # pylint: disable=too-many-locals def __init__( self, id: int | None = None, name: str | None = None, description: str | None = None, category: str | None = None, cores: int | None = None, memory: int | None = None, disk: int | 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[str, Any] | None = None, included_traffic: int | None = None, locations: list[ServerTypeLocation] | None = None, ): self.id = id self.name = name self.description = description self.category = category self.cores = cores self.memory = memory self.disk = disk self.prices = prices self.storage_type = storage_type self.cpu_type = cpu_type self.architecture = architecture self.locations = locations self.deprecated = deprecated self.deprecation = ( DeprecationInfo.from_dict(deprecation) if deprecation is not None else None ) self.included_traffic = included_traffic @property def deprecated(self) -> bool | None: """ .. deprecated:: 2.6.0 The 'deprecated' property is deprecated and will gradually be phased starting 24 September 2025. Please refer to the '.locations[].deprecation' property instead. See https://docs.hetzner.cloud/changelog#2025-09-24-per-location-server-types. """ warnings.warn( "The 'deprecated' property is deprecated and will gradually be phased starting 24 September 2025. " "Please refer to the '.locations[].deprecation' property instead. " "See https://docs.hetzner.cloud/changelog#2025-09-24-per-location-server-types", DeprecationWarning, stacklevel=2, ) return self._deprecated @deprecated.setter def deprecated(self, value: bool | None) -> None: self._deprecated = value @property def deprecation(self) -> DeprecationInfo | None: """ .. deprecated:: 2.6.0 The 'deprecation' property is deprecated and will gradually be phased starting 24 September 2025. Please refer to the '.locations[].deprecation' property instead. See https://docs.hetzner.cloud/changelog#2025-09-24-per-location-server-types. """ warnings.warn( "The 'deprecation' property is deprecated and will gradually be phased starting 24 September 2025. " "Please refer to the '.locations[].deprecation' property instead. " "See https://docs.hetzner.cloud/changelog#2025-09-24-per-location-server-types", DeprecationWarning, stacklevel=2, ) return self._deprecation @deprecation.setter def deprecation(self, value: DeprecationInfo | None) -> None: self._deprecation = value @property def included_traffic(self) -> int | None: """ .. deprecated:: 2.1.0 The 'included_traffic' property is deprecated and will be set to 'None' on 5 August 2024. Please refer to the 'prices' property instead. See https://docs.hetzner.cloud/changelog#2024-07-25-cloud-api-returns-traffic-information-in-different-format. """ warnings.warn( "The 'included_traffic' property is deprecated and will be set to 'None' on 5 August 2024. " "Please refer to the 'prices' property instead. " "See https://docs.hetzner.cloud/changelog#2024-07-25-cloud-api-returns-traffic-information-in-different-format", DeprecationWarning, stacklevel=2, ) return self._included_traffic @included_traffic.setter def included_traffic(self, value: int | None) -> None: self._included_traffic = value class ServerTypeLocation(BaseDomain): """Server Type Location Domain :param location: Location of the Server Type. :param deprecation: Wether the Server Type is deprecated in this Location. """ __api_properties__ = ( "location", "deprecation", ) __slots__ = __api_properties__ def __init__( self, *, location: BoundLocation, deprecation: dict[str, Any] | None, ): self.location = location self.deprecation = ( DeprecationInfo.from_dict(deprecation) if deprecation is not None else None ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1430817 hcloud-2.17.0/hcloud/servers/0000755000175100017510000000000015152343221015472 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/servers/__init__.py0000644000175100017510000000160015152343177017612 0ustar00runnerrunnerfrom __future__ import annotations from .client import BoundServer, ServersClient, ServersPageResult from .domain import ( CreateServerResponse, EnableRescueResponse, GetMetricsResponse, IPv4Address, IPv6Network, MetricsType, PrivateNet, PublicNetwork, PublicNetworkFirewall, RebuildResponse, RequestConsoleResponse, ResetPasswordResponse, Server, ServerCreatePublicNetwork, ServerProtection, ) __all__ = [ "BoundServer", "CreateServerResponse", "EnableRescueResponse", "GetMetricsResponse", "IPv4Address", "IPv6Network", "PrivateNet", "PublicNetwork", "PublicNetworkFirewall", "RequestConsoleResponse", "ResetPasswordResponse", "Server", "ServerProtection", "ServerCreatePublicNetwork", "ServersClient", "ServersPageResult", "RebuildResponse", "MetricsType", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/servers/client.py0000644000175100017510000014740015152343177017342 0ustar00runnerrunnerfrom __future__ import annotations import warnings from datetime import datetime from typing import TYPE_CHECKING, Any, NamedTuple from dateutil.parser import isoparse from ..actions import ( ActionSort, ActionsPageResult, ActionStatus, BoundAction, ResourceActionsClient, ) from ..actions.client import ResourceClientBaseActionsMixin from ..core import BoundModelBase, Meta, ResourceClientBase from ..datacenters import BoundDatacenter from ..firewalls import BoundFirewall from ..floating_ips import BoundFloatingIP from ..images import BoundImage, CreateImageResponse from ..isos import BoundIso from ..locations import BoundLocation, Location from ..metrics import Metrics from ..placement_groups import BoundPlacementGroup from ..primary_ips import BoundPrimaryIP from ..server_types import BoundServerType from ..volumes import BoundVolume from .domain import ( CreateServerResponse, EnableRescueResponse, GetMetricsResponse, IPv4Address, IPv6Network, MetricsType, PrivateNet, PublicNetwork, PublicNetworkFirewall, RebuildResponse, RequestConsoleResponse, ResetPasswordResponse, Server, ) if TYPE_CHECKING: from .._client import Client from ..datacenters import Datacenter from ..firewalls import Firewall from ..images import Image from ..isos import Iso from ..networks import BoundNetwork, Network from ..placement_groups import PlacementGroup from ..server_types import ServerType from ..ssh_keys import BoundSSHKey, SSHKey from ..volumes import Volume from .domain import ServerCreatePublicNetwork __all__ = [ "BoundServer", "ServersPageResult", "ServersClient", ] class BoundServer(BoundModelBase[Server], Server): _client: ServersClient model = Server # pylint: disable=too-many-locals def __init__( self, client: ServersClient, data: dict[str, Any], complete: bool = True, ): raw = data.get("datacenter") if raw: data["datacenter"] = BoundDatacenter(client._parent.datacenters, raw) raw = data.get("location") if raw: data["location"] = BoundLocation(client._parent.locations, raw) volumes = data.get("volumes", []) if volumes: volumes = [ BoundVolume(client._parent.volumes, {"id": volume}, complete=False) for volume in volumes ] data["volumes"] = volumes image = data.get("image", None) if image is not None: data["image"] = BoundImage(client._parent.images, image) iso = data.get("iso", None) if iso is not None: data["iso"] = BoundIso(client._parent.isos, iso) server_type = data.get("server_type") if server_type is not None: data["server_type"] = BoundServerType( client._parent.server_types, server_type ) public_net = data.get("public_net") if public_net: ipv4_address = ( IPv4Address.from_dict(public_net["ipv4"]) if public_net["ipv4"] is not None else None ) ipv4_primary_ip = ( BoundPrimaryIP( client._parent.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._parent.primary_ips, {"id": public_net["ipv6"]["id"]}, complete=False, ) if public_net["ipv6"] is not None else None ) floating_ips = [ BoundFloatingIP( client._parent.floating_ips, {"id": floating_ip}, complete=False ) for floating_ip in public_net["floating_ips"] ] firewalls = [ PublicNetworkFirewall( BoundFirewall( client._parent.firewalls, {"id": firewall["id"]}, complete=False ), status=firewall["status"], ) for firewall in public_net.get("firewalls", []) ] 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, ) private_nets = data.get("private_net") if private_nets: # pylint: disable=import-outside-toplevel from ..networks import BoundNetwork private_nets = [ PrivateNet( network=BoundNetwork( client._parent.networks, {"id": private_net["network"]}, complete=False, ), ip=private_net["ip"], alias_ips=private_net["alias_ips"], mac_address=private_net["mac_address"], ) for private_net in private_nets ] data["private_net"] = private_nets placement_group = data.get("placement_group") if placement_group: placement_group = BoundPlacementGroup( client._parent.placement_groups, placement_group ) data["placement_group"] = placement_group super().__init__(client, data, complete) def get_actions_list( self, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Server. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._client.get_actions_list( self, status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Server. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ return self._client.get_actions(self, status=status, sort=sort) 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) New name to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundServer ` """ return self._client.update(self, name=name, labels=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. :return: :class:`BoundAction ` """ return self._client.delete(self) 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) -> BoundAction: """Starts a server by turning its power on. :return: :class:`BoundAction ` """ return self._client.power_on(self) def reboot(self) -> BoundAction: """Reboots a server gracefully by sending an ACPI request. :return: :class:`BoundAction ` """ return self._client.reboot(self) def reset(self) -> BoundAction: """Cuts power to a server and starts it again. :return: :class:`BoundAction ` """ return self._client.reset(self) 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) -> 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: str | None = None, ssh_keys: list[str] | None = None, ) -> EnableRescueResponse: """Enable the Hetzner Rescue System for this server. :param type: str Type of rescue system to boot (default: linux64) Choices: linux64, linux32, freebsd64 :param ssh_keys: List[str] Array of SSH key IDs which should be injected into the rescue system. Only available for types: linux64 and linux32. :return: :class:`EnableRescueResponse ` """ return self._client.enable_rescue(self, type=type, ssh_keys=ssh_keys) 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: 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) Description of the image. If you do not set this we auto-generate one for you. :param type: str (optional) Type of image to create (default: snapshot) Choices: snapshot, backup :param labels: Dict[str, str] User-defined labels (key-value pairs) :return: :class:`CreateImageResponse ` """ return self._client.create_image( self, description=description, type=type, labels=labels ) def rebuild( self, image: Image | BoundImage, user_data: str | None = None, # pylint: disable=unused-argument **kwargs: Any, ) -> RebuildResponse: """Rebuilds a server overwriting its disk with the content of an image, thereby destroying all data on the target server. :param image: Image to use for the rebuilt server :param user_data: Cloud-Init user data to use during Server rebuild (optional) """ return self._client.rebuild(self, image=image, user_data=user_data) 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 ` Server type the server should migrate to :param upgrade_disk: boolean If false, do not upgrade the disk. This allows downgrading the server type later. :return: :class:`BoundAction ` """ return self._client.change_type( self, server_type=server_type, upgrade_disk=upgrade_disk ) 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) -> 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: Iso | BoundIso) -> BoundAction: """Attaches an ISO to a server. :param iso: :class:`BoundIso ` or :class:`Server ` :return: :class:`BoundAction ` """ return self._client.attach_iso(self, iso=iso) 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: 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 The IP address for which to set the reverse DNS entry :param dns_ptr: 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=ip, dns_ptr=dns_ptr) 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 ` :param delete: boolean If true, prevents the server from being deleted (currently delete and rebuild attribute needs to have the same value) :param rebuild: boolean If true, prevents the server from being rebuilt (currently delete and rebuild attribute needs to have the same value) :return: :class:`BoundAction ` """ return self._client.change_protection(self, delete=delete, rebuild=rebuild) 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: Network | BoundNetwork, ip: str | None = None, alias_ips: list[str] | None = None, ip_range: str | None = None, ) -> BoundAction: """Attaches a server to a network :param network: :class:`BoundNetwork ` or :class:`Network ` :param ip: str IP to request to be assigned to this server :param alias_ips: List[str] New alias IPs to set for this server. :param ip_range: str IP range in CIDR block notation of the subnet to attach to. :return: :class:`BoundAction ` """ return self._client.attach_to_network( self, network=network, ip=ip, alias_ips=alias_ips, ip_range=ip_range, ) def detach_from_network(self, network: Network | BoundNetwork) -> BoundAction: """Detaches a server from a network. :param network: :class:`BoundNetwork ` or :class:`Network ` :return: :class:`BoundAction ` """ return self._client.detach_from_network(self, network=network) 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 ` :param alias_ips: List[str] New alias IPs to set for this server. :return: :class:`BoundAction ` """ return self._client.change_alias_ips(self, network=network, alias_ips=alias_ips) 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 ` :return: :class:`BoundAction ` """ return self._client.add_to_placement_group( self, placement_group=placement_group ) def remove_from_placement_group(self) -> BoundAction: """Removes a server from a placement group. :return: :class:`BoundAction ` """ return self._client.remove_from_placement_group(self) class ServersPageResult(NamedTuple): servers: list[BoundServer] meta: Meta class ServersClient( ResourceClientBaseActionsMixin, ResourceClientBase, ): _base_url = "/servers" actions: ResourceActionsClient """Servers scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self.actions = ResourceActionsClient(client, self._base_url) def get_by_id(self, id: int) -> BoundServer: """Get a specific server :param id: int :return: :class:`BoundServer ` """ response = self._client.request(url=f"{self._base_url}/{id}", method="GET") return BoundServer(self, response["server"]) def get_list( self, 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) Can be used to filter servers by their name. :param label_selector: str (optional) Can be used to filter servers by labels. The response will only contain servers matching the label selector. :param status: List[str] (optional) Can be used to filter servers by their status. The response will only contain servers matching the status. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundServer `], :class:`Meta `) """ params: dict[str, Any] = {} if name is not None: params["name"] = name if label_selector is not None: params["label_selector"] = label_selector if status is not None: params["status"] = status if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request(url=self._base_url, method="GET", params=params) ass_servers = [ BoundServer(self, server_data) for server_data in response["servers"] ] return ServersPageResult(ass_servers, Meta.parse_meta(response)) 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) Can be used to filter servers by their name. :param label_selector: str (optional) Can be used to filter servers by labels. The response will only contain servers matching the label selector. :param status: List[str] (optional) Can be used to filter servers by their status. The response will only contain servers matching the status. :return: List[:class:`BoundServer `] """ 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 :param name: str Used to get server by name. :return: :class:`BoundServer ` """ return self._get_first_by(self.get_list, name=name) # pylint: disable=too-many-branches,too-many-locals def create( self, 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 Name of the server to create (must be unique per project and a valid hostname as per RFC 1123) :param server_type: :class:`BoundServerType ` or :class:`ServerType ` Server type this server should be created with :param image: :class:`BoundImage ` or :class:`Image ` Image the server is created from :param ssh_keys: List[:class:`BoundSSHKey ` or :class:`SSHKey `] (optional) SSH keys which should be injected into the server at creation time :param volumes: List[:class:`BoundVolume ` or :class:`Volume `] (optional) Volumes which should be attached to the server at the creation time. Volumes must be in the same location. :param networks: List[:class:`BoundNetwork ` or :class:`Network `] (optional) Networks which should be attached to the server at the creation time. :param user_data: str (optional) Cloud-Init user data to use during server creation. This field is limited to 32KiB. :param labels: Dict[str,str] (optional) User-defined labels (key-value pairs) :param location: :class:`BoundLocation ` or :class:`Location ` :param datacenter: :class:`BoundDatacenter ` or :class:`Datacenter ` :param start_after_create: boolean (optional) Start Server right after creation. Defaults to True. :param automount: boolean (optional) 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: dict[str, Any] = { "name": name, "server_type": server_type.id_or_name, "start_after_create": start_after_create, "image": image.id_or_name, } if location is not None: data["location"] = location.id_or_name if datacenter is not None: warnings.warn( "The 'datacenter' argument is deprecated and will be removed after 1 July 2026. " "Please use the 'location' argument instead. " "See https://docs.hetzner.cloud/changelog#2025-12-16-phasing-out-datacenters", DeprecationWarning, stacklevel=2, ) data["datacenter"] = datacenter.id_or_name if ssh_keys is not None: data["ssh_keys"] = [ssh_key.id_or_name for ssh_key in ssh_keys] if volumes is not None: data["volumes"] = [volume.id for volume in volumes] if networks is not None: data["networks"] = [network.id for network in networks] if firewalls is not None: data["firewalls"] = [{"firewall": firewall.id} for firewall in firewalls] if user_data is not None: data["user_data"] = user_data if labels is not None: data["labels"] = labels if automount is not None: data["automount"] = automount if placement_group is not None: data["placement_group"] = placement_group.id if public_net is not None: 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: data_public_net["ipv4"] = public_net.ipv4.id if public_net.ipv6 is not None: data_public_net["ipv6"] = public_net.ipv6.id data["public_net"] = data_public_net response = self._client.request(url=self._base_url, method="POST", json=data) result = CreateServerResponse( server=BoundServer(self, response["server"]), action=BoundAction(self._parent.actions, response["action"]), next_actions=[ BoundAction(self._parent.actions, action) for action in response["next_actions"] ], root_password=response["root_password"], ) return result def get_actions_list( self, server: Server | BoundServer, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Server. :param server: Server to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._get_actions_list( f"{self._base_url}/{server.id}", status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, server: Server | BoundServer, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Server. :param server: Server to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ return self._iter_pages( self.get_actions_list, server, status=status, sort=sort, ) 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 ` :param name: str (optional) New name to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundServer ` """ data: dict[str, Any] = {} if name is not None: data.update({"name": name}) if labels is not None: data.update({"labels": labels}) response = self._client.request( url=f"{self._base_url}/{server.id}", method="PUT", json=data, ) 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"{self._base_url}/{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. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ response = self._client.request( url=f"{self._base_url}/{server.id}", method="DELETE" ) return BoundAction(self._parent.actions, response["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 ` :return: :class:`BoundAction ` """ response = self._client.request( url=f"{self._base_url}/{server.id}/actions/poweroff", method="POST", ) return BoundAction(self._parent.actions, response["action"]) def power_on(self, server: Server | BoundServer) -> BoundAction: """Starts a server by turning its power on. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ response = self._client.request( url=f"{self._base_url}/{server.id}/actions/poweron", method="POST", ) return BoundAction(self._parent.actions, response["action"]) def reboot(self, server: Server | BoundServer) -> BoundAction: """Reboots a server gracefully by sending an ACPI request. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ response = self._client.request( url=f"{self._base_url}/{server.id}/actions/reboot", method="POST", ) return BoundAction(self._parent.actions, response["action"]) def reset(self, server: Server | BoundServer) -> BoundAction: """Cuts power to a server and starts it again. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ response = self._client.request( url=f"{self._base_url}/{server.id}/actions/reset", method="POST", ) return BoundAction(self._parent.actions, response["action"]) 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 ` :return: :class:`BoundAction ` """ response = self._client.request( url=f"{self._base_url}/{server.id}/actions/shutdown", method="POST", ) return BoundAction(self._parent.actions, response["action"]) 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 ` :return: :class:`ResetPasswordResponse ` """ response = self._client.request( url=f"{self._base_url}/{server.id}/actions/reset_password", method="POST", ) return ResetPasswordResponse( action=BoundAction(self._parent.actions, response["action"]), root_password=response["root_password"], ) 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 ` :param server_type: :class:`BoundServerType ` or :class:`ServerType ` Server type the server should migrate to :param upgrade_disk: boolean If false, do not upgrade the disk. This allows downgrading the server type later. :return: :class:`BoundAction ` """ data: dict[str, Any] = { "server_type": server_type.id_or_name, "upgrade_disk": upgrade_disk, } response = self._client.request( url=f"{self._base_url}/{server.id}/actions/change_type", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) 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 ` :param type: str Type of rescue system to boot (default: linux64) Choices: linux64, linux32, freebsd64 :param ssh_keys: List[str] Array of SSH key IDs which should be injected into the rescue system. Only available for types: linux64 and linux32. :return: :class:`EnableRescueResponse ` """ data: dict[str, Any] = {"type": type} if ssh_keys is not None: data.update({"ssh_keys": ssh_keys}) response = self._client.request( url=f"{self._base_url}/{server.id}/actions/enable_rescue", method="POST", json=data, ) return EnableRescueResponse( action=BoundAction(self._parent.actions, response["action"]), root_password=response["root_password"], ) def disable_rescue(self, server: Server | BoundServer) -> BoundAction: """Disables the Hetzner Rescue System for a server. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ response = self._client.request( url=f"{self._base_url}/{server.id}/actions/disable_rescue", method="POST", ) return BoundAction(self._parent.actions, response["action"]) 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 ` :param description: str (optional) Description of the image. If you do not set this we auto-generate one for you. :param type: str (optional) Type of image to create (default: snapshot) Choices: snapshot, backup :param labels: Dict[str, str] User-defined labels (key-value pairs) :return: :class:`CreateImageResponse ` """ data: dict[str, Any] = {} if description is not None: data.update({"description": description}) if type is not None: data.update({"type": type}) if labels is not None: data.update({"labels": labels}) response = self._client.request( url=f"{self._base_url}/{server.id}/actions/create_image", method="POST", json=data, ) return CreateImageResponse( action=BoundAction(self._parent.actions, response["action"]), image=BoundImage(self._parent.images, response["image"]), ) def rebuild( self, server: Server | BoundServer, image: Image | BoundImage, user_data: str | None = None, # pylint: disable=unused-argument **kwargs: Any, ) -> RebuildResponse: """Rebuilds a server overwriting its disk with the content of an image, thereby destroying all data on the target server. :param server: Server to rebuild :param image: Image to use for the rebuilt server :param user_data: Cloud-Init user data to use during Server rebuild (optional) """ data: dict[str, Any] = {"image": image.id_or_name} if user_data is not None: data["user_data"] = user_data response = self._client.request( url=f"{self._base_url}/{server.id}/actions/rebuild", method="POST", json=data, ) return RebuildResponse( action=BoundAction(self._parent.actions, response["action"]), root_password=response.get("root_password"), ) 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 ` :return: :class:`BoundAction ` """ response = self._client.request( url=f"{self._base_url}/{server.id}/actions/enable_backup", method="POST", ) return BoundAction(self._parent.actions, response["action"]) 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 ` :return: :class:`BoundAction ` """ response = self._client.request( url=f"{self._base_url}/{server.id}/actions/disable_backup", method="POST", ) return BoundAction(self._parent.actions, response["action"]) 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: dict[str, Any] = {"iso": iso.id_or_name} response = self._client.request( url=f"{self._base_url}/{server.id}/actions/attach_iso", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) def detach_iso(self, server: Server | BoundServer) -> BoundAction: """Detaches an ISO from a server. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ response = self._client.request( url=f"{self._base_url}/{server.id}/actions/detach_iso", method="POST", ) return BoundAction(self._parent.actions, response["action"]) 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 ` :param ip: str The IP address for which to set the reverse DNS entry :param dns_ptr: Hostname to set as a reverse DNS PTR entry, will reset to original default value if `None` :return: :class:`BoundAction ` """ data: dict[str, Any] = {"ip": ip, "dns_ptr": dns_ptr} response = self._client.request( url=f"{self._base_url}/{server.id}/actions/change_dns_ptr", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) 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 ` :param delete: boolean If true, prevents the server from being deleted (currently delete and rebuild attribute needs to have the same value) :param rebuild: boolean If true, prevents the server from being rebuilt (currently delete and rebuild attribute needs to have the same value) :return: :class:`BoundAction ` """ data: dict[str, Any] = {} if delete is not None: data.update({"delete": delete}) if rebuild is not None: data.update({"rebuild": rebuild}) response = self._client.request( url=f"{self._base_url}/{server.id}/actions/change_protection", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) 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 ` :return: :class:`RequestConsoleResponse ` """ response = self._client.request( url=f"{self._base_url}/{server.id}/actions/request_console", method="POST", ) return RequestConsoleResponse( action=BoundAction(self._parent.actions, response["action"]), wss_url=response["wss_url"], password=response["password"], ) def attach_to_network( self, server: Server | BoundServer, network: Network | BoundNetwork, ip: str | None = None, alias_ips: list[str] | None = None, ip_range: str | None = None, ) -> BoundAction: """Attaches a server to a network :param server: :class:`BoundServer ` or :class:`Server ` :param network: :class:`BoundNetwork ` or :class:`Network ` :param ip: str IP to request to be assigned to this server :param alias_ips: List[str] New alias IPs to set for this server. :param ip_range: str IP range in CIDR block notation of the subnet to attach to. :return: :class:`BoundAction ` """ data: dict[str, Any] = {"network": network.id} if ip is not None: data.update({"ip": ip}) if alias_ips is not None: data.update({"alias_ips": alias_ips}) if ip_range is not None: data.update({"ip_range": ip_range}) response = self._client.request( url=f"{self._base_url}/{server.id}/actions/attach_to_network", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) 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: dict[str, Any] = {"network": network.id} response = self._client.request( url=f"{self._base_url}/{server.id}/actions/detach_from_network", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) 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 ` :param network: :class:`BoundNetwork ` or :class:`Network ` :param alias_ips: List[str] New alias IPs to set for this server. :return: :class:`BoundAction ` """ data: dict[str, Any] = {"network": network.id, "alias_ips": alias_ips} response = self._client.request( url=f"{self._base_url}/{server.id}/actions/change_alias_ips", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) 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: dict[str, Any] = {"placement_group": placement_group.id} response = self._client.request( url=f"{self._base_url}/{server.id}/actions/add_to_placement_group", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) def remove_from_placement_group(self, server: Server | BoundServer) -> BoundAction: """Removes a server from a placement group. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ response = self._client.request( url=f"{self._base_url}/{server.id}/actions/remove_from_placement_group", method="POST", ) return BoundAction(self._parent.actions, response["action"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/servers/domain.py0000644000175100017510000004004015152343177017323 0ustar00runnerrunnerfrom __future__ import annotations import warnings from typing import TYPE_CHECKING, Literal, TypedDict from ..core import BaseDomain, DomainIdentityMixin 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 ..locations import BoundLocation from ..metrics import Metrics 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 __all__ = [ "Server", "ServerProtection", "CreateServerResponse", "ResetPasswordResponse", "EnableRescueResponse", "RequestConsoleResponse", "RebuildResponse", "PublicNetwork", "PublicNetworkFirewall", "IPv4Address", "IPv6Network", "PrivateNet", "ServerCreatePublicNetwork", "GetMetricsResponse", "MetricsType", ] class Server(BaseDomain, DomainIdentityMixin): """Server Domain :param id: int ID of the server :param name: str Name of the server (must be unique per project and a valid hostname as per RFC 1123) :param status: str Status of the server Choices: `running`, `initializing`, `starting`, `stopping`, `off`, `deleting`, `migrating`, `rebuilding`, `unknown` :param created: datetime Point in time when the server was created :param public_net: :class:`PublicNetwork ` Public network information. :param server_type: :class:`BoundServerType ` :param datacenter: :class:`BoundDatacenter ` This property is deprecated and will be removed after 1 July 2026. Please use the ``location`` property instead. See https://docs.hetzner.cloud/changelog#2025-12-16-phasing-out-datacenters. :param location: :class:`BoundLocation ` :param image: :class:`BoundImage `, None :param iso: :class:`BoundIso `, None :param rescue_enabled: bool True if rescue mode is enabled: Server will then boot into rescue system on next reboot. :param locked: bool True if server has been locked and is not available to user. :param backup_window: str, None Time window (UTC) in which the backup will run, or None if the backups are not enabled :param outgoing_traffic: int, None Outbound Traffic for the current billing period in bytes :param ingoing_traffic: int, None Inbound Traffic for the current billing period in bytes :param included_traffic: int Free Traffic for the current billing period in bytes :param primary_disk_size: int Size of the primary Disk :param protection: dict Protection configuration for the server :param labels: dict 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. """ STATUS_RUNNING = "running" """Server Status running""" STATUS_INIT = "initializing" """Server Status initializing""" STATUS_STARTING = "starting" """Server Status starting""" STATUS_STOPPING = "stopping" """Server Status stopping""" STATUS_OFF = "off" """Server Status off""" STATUS_DELETING = "deleting" """Server Status deleting""" STATUS_MIGRATING = "migrating" """Server Status migrating""" STATUS_REBUILDING = "rebuilding" """Server Status rebuilding""" STATUS_UNKNOWN = "unknown" """Server Status unknown""" __properties__ = ( "id", "name", "status", "public_net", "server_type", "location", "image", "iso", "rescue_enabled", "locked", "backup_window", "outgoing_traffic", "ingoing_traffic", "included_traffic", "protection", "labels", "volumes", "private_net", "created", "primary_disk_size", "placement_group", ) __api_properties__ = ( *__properties__, "datacenter", ) __slots__ = ( *__properties__, "_datacenter", ) # pylint: disable=too-many-locals def __init__( self, 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, location: BoundLocation | 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: ServerProtection | 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 self.status = status self.created = self._parse_datetime(created) self.public_net = public_net self.server_type = server_type self.datacenter = datacenter self.location = location self.image = image self.iso = iso self.rescue_enabled = rescue_enabled self.locked = locked self.backup_window = backup_window self.outgoing_traffic = outgoing_traffic self.ingoing_traffic = ingoing_traffic self.included_traffic = included_traffic self.protection = protection self.labels = labels self.volumes = volumes self.private_net = private_net self.primary_disk_size = primary_disk_size self.placement_group = placement_group def private_net_for(self, network: BoundNetwork | Network) -> PrivateNet | None: """ Returns the server's network attachment information in the given Network, and None if no attachment was found. """ for o in self.private_net or []: if o.network.id == network.id: return o return None @property def datacenter(self) -> BoundDatacenter | None: """ :meta private: """ warnings.warn( "The 'datacenter' property is deprecated and will be removed after 1 July 2026. " "Please use the 'location' property instead. " "See https://docs.hetzner.cloud/changelog#2025-12-16-phasing-out-datacenters.", DeprecationWarning, stacklevel=2, ) return self._datacenter @datacenter.setter def datacenter(self, value: BoundDatacenter | None) -> None: self._datacenter = value class ServerProtection(TypedDict): rebuild: bool delete: bool class CreateServerResponse(BaseDomain): """Create Server Response Domain :param server: :class:`BoundServer ` The created server :param action: :class:`BoundAction ` Shows the progress of the server creation :param next_actions: List[:class:`BoundAction `] Additional actions like a `start_server` action after the server creation :param root_password: str, None The root password of the server if no SSH-Key was given on server creation """ __api_properties__ = ("server", "action", "next_actions", "root_password") __slots__ = __api_properties__ def __init__( self, server: BoundServer, action: BoundAction, next_actions: list[BoundAction], root_password: str | None, ): self.server = server self.action = action self.next_actions = next_actions self.root_password = root_password class ResetPasswordResponse(BaseDomain): """Reset Password Response Domain :param action: :class:`BoundAction ` Shows the progress of the server passwort reset action :param root_password: str The root password of the server """ __api_properties__ = ("action", "root_password") __slots__ = __api_properties__ def __init__( self, action: BoundAction, root_password: str, ): self.action = action self.root_password = root_password class EnableRescueResponse(BaseDomain): """Enable Rescue Response Domain :param action: :class:`BoundAction ` Shows the progress of the server enable rescue action :param root_password: str The root password of the server in the rescue mode """ __api_properties__ = ("action", "root_password") __slots__ = __api_properties__ def __init__( self, action: BoundAction, root_password: str, ): self.action = action self.root_password = root_password class RequestConsoleResponse(BaseDomain): """Request Console Response Domain :param action: :class:`BoundAction ` Shows the progress of the server request console action :param wss_url: str URL of websocket proxy to use. This includes a token which is valid for a limited time only. :param password: str VNC password to use for this connection. This password only works in combination with a wss_url with valid token. """ __api_properties__ = ("action", "wss_url", "password") __slots__ = __api_properties__ def __init__( self, action: BoundAction, wss_url: str, password: str, ): self.action = action self.wss_url = wss_url 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 """ __api_properties__ = ("action", "root_password") __slots__ = __api_properties__ def __init__( self, action: BoundAction, root_password: str | None, ): self.action = action self.root_password = root_password class PublicNetwork(BaseDomain): """Public Network Domain :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 `] """ __api_properties__ = ( "ipv4", "ipv6", "floating_ips", "firewalls", "primary_ipv4", "primary_ipv6", ) __slots__ = __api_properties__ def __init__( self, ipv4: IPv4Address | None, ipv6: IPv6Network | None, floating_ips: list[BoundFloatingIP], primary_ipv4: BoundPrimaryIP | None, primary_ipv6: BoundPrimaryIP | None, firewalls: list[PublicNetworkFirewall] | None = None, ): 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): """Public Network Domain :param firewall: :class:`BoundFirewall ` :param status: str """ __api_properties__ = ("firewall", "status") __slots__ = __api_properties__ STATUS_APPLIED = "applied" """Public Network Firewall Status applied""" STATUS_PENDING = "pending" """Public Network Firewall Status pending""" def __init__( self, firewall: BoundFirewall, status: str, ): self.firewall = firewall self.status = status class IPv4Address(BaseDomain): """IPv4 Address Domain :param ip: str The IPv4 Address :param blocked: bool Determine if the IP is blocked :param dns_ptr: str DNS PTR for the ip """ __api_properties__ = ("ip", "blocked", "dns_ptr") __slots__ = __api_properties__ def __init__( self, ip: str, blocked: bool, dns_ptr: str, ): self.ip = ip self.blocked = blocked self.dns_ptr = dns_ptr class IPv6Network(BaseDomain): """IPv6 Network Domain :param ip: str The IPv6 Network as CIDR Notation :param blocked: bool Determine if the Network is blocked :param dns_ptr: dict DNS PTR Records for the Network as Dict :param network: str The network without the network mask :param network_mask: str The network mask """ __api_properties__ = ("ip", "blocked", "dns_ptr", "network", "network_mask") __slots__ = __api_properties__ def __init__( self, ip: str, blocked: bool, dns_ptr: list[DNSPtr], ): self.ip = ip self.blocked = blocked self.dns_ptr = dns_ptr ip_parts = self.ip.split("/") # 2001:db8::/64 to 2001:db8:: and 64 self.network = ip_parts[0] self.network_mask = ip_parts[1] class PrivateNet(BaseDomain): """PrivateNet Domain :param network: :class:`BoundNetwork ` The network the server is attached to :param ip: str The main IP Address of the server in the Network :param alias_ips: List[str] The alias ips for a server :param mac_address: str The mac address of the interface on the server """ __api_properties__ = ("network", "ip", "alias_ips", "mac_address") __slots__ = __api_properties__ def __init__( self, network: BoundNetwork, ip: str, alias_ips: list[str], mac_address: str, ): self.network = network 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 """ __api_properties__ = ("ipv4", "ipv6", "enable_ipv4", "enable_ipv6") __slots__ = __api_properties__ def __init__( self, ipv4: PrimaryIP | None = None, ipv6: PrimaryIP | None = None, enable_ipv4: bool = True, enable_ipv6: bool = True, ): self.ipv4 = ipv4 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 """ __api_properties__ = ("metrics",) __slots__ = __api_properties__ def __init__( self, metrics: Metrics, ): self.metrics = metrics ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1437201 hcloud-2.17.0/hcloud/ssh_keys/0000755000175100017510000000000015152343221015631 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/ssh_keys/__init__.py0000644000175100017510000000033715152343177017757 0ustar00runnerrunnerfrom __future__ import annotations from .client import BoundSSHKey, SSHKeysClient, SSHKeysPageResult from .domain import SSHKey __all__ = [ "BoundSSHKey", "SSHKey", "SSHKeysClient", "SSHKeysPageResult", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/ssh_keys/client.py0000644000175100017510000001640715152343177017503 0ustar00runnerrunnerfrom __future__ import annotations from typing import Any, NamedTuple from ..core import BoundModelBase, Meta, ResourceClientBase from .domain import SSHKey __all__ = [ "BoundSSHKey", "SSHKeysPageResult", "SSHKeysClient", ] class BoundSSHKey(BoundModelBase[SSHKey], SSHKey): _client: SSHKeysClient model = SSHKey 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) New Description to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundSSHKey ` """ return self._client.update(self, name=name, labels=labels) def delete(self) -> bool: """Deletes an SSH key. It cannot be used anymore. :return: boolean """ return self._client.delete(self) class SSHKeysPageResult(NamedTuple): ssh_keys: list[BoundSSHKey] meta: Meta class SSHKeysClient(ResourceClientBase): _base_url = "/ssh_keys" def get_by_id(self, id: int) -> BoundSSHKey: """Get a specific SSH Key by its ID :param id: int :return: :class:`BoundSSHKey ` """ response = self._client.request(url=f"{self._base_url}/{id}", method="GET") return BoundSSHKey(self, response["ssh_key"]) def get_list( self, 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) Can be used to filter SSH keys by their name. The response will only contain the SSH key matching the specified name. :param fingerprint: str (optional) Can be used to filter SSH keys by their fingerprint. The response will only contain the SSH key matching the specified fingerprint. :param label_selector: str (optional) Can be used to filter SSH keys by labels. The response will only contain SSH keys matching the label selector. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundSSHKey `], :class:`Meta `) """ params: dict[str, Any] = {} if name is not None: params["name"] = name if fingerprint is not None: params["fingerprint"] = fingerprint 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 response = self._client.request(url=self._base_url, method="GET", params=params) ssh_keys = [ BoundSSHKey(self, server_data) for server_data in response["ssh_keys"] ] return SSHKeysPageResult(ssh_keys, Meta.parse_meta(response)) 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) Can be used to filter SSH keys by their name. The response will only contain the SSH key matching the specified name. :param fingerprint: str (optional) Can be used to filter SSH keys by their fingerprint. The response will only contain the SSH key matching the specified fingerprint. :param label_selector: str (optional) 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 self._iter_pages( self.get_list, name=name, fingerprint=fingerprint, label_selector=label_selector, ) def get_by_name(self, name: str) -> BoundSSHKey | None: """Get ssh key by name :param name: str Used to get ssh key by name. :return: :class:`BoundSSHKey ` """ return self._get_first_by(self.get_list, name=name) def get_by_fingerprint(self, fingerprint: str) -> BoundSSHKey | None: """Get ssh key by fingerprint :param fingerprint: str Used to get ssh key by fingerprint. :return: :class:`BoundSSHKey ` """ return self._get_first_by(self.get_list, fingerprint=fingerprint) 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 :param public_key: str Public Key of the SSH Key you want create :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundSSHKey ` """ data: dict[str, Any] = {"name": name, "public_key": public_key} if labels is not None: data["labels"] = labels response = self._client.request(url=self._base_url, method="POST", json=data) return BoundSSHKey(self, response["ssh_key"]) 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 ` :param name: str (optional) New Description to set :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundSSHKey ` """ data: dict[str, Any] = {} if name is not None: data["name"] = name if labels is not None: data["labels"] = labels response = self._client.request( url=f"{self._base_url}/{ssh_key.id}", method="PUT", json=data, ) return BoundSSHKey(self, response["ssh_key"]) def delete(self, ssh_key: SSHKey | BoundSSHKey) -> bool: """Deletes an SSH key. It cannot be used anymore. :param ssh_key: :class:`BoundSSHKey ` or :class:`SSHKey ` :return: True """ self._client.request(url=f"{self._base_url}/{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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/ssh_keys/domain.py0000644000175100017510000000233515152343177017467 0ustar00runnerrunnerfrom __future__ import annotations from ..core import BaseDomain, DomainIdentityMixin __all__ = [ "SSHKey", ] class SSHKey(BaseDomain, DomainIdentityMixin): """SSHKey Domain :param id: int ID of the SSH key :param name: str Name of the SSH key (must be unique per project) :param fingerprint: str Fingerprint of public key :param public_key: str Public Key :param labels: Dict User-defined labels (key-value pairs) :param created: datetime Point in time when the SSH Key was created """ __api_properties__ = ( "id", "name", "fingerprint", "public_key", "labels", "created", ) __slots__ = __api_properties__ def __init__( self, 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 self.fingerprint = fingerprint self.public_key = public_key self.labels = labels self.created = self._parse_datetime(created) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1772734097.144306 hcloud-2.17.0/hcloud/storage_box_types/0000755000175100017510000000000015152343221017541 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/storage_box_types/__init__.py0000644000175100017510000000046015152343177021664 0ustar00runnerrunnerfrom __future__ import annotations from .client import ( BoundStorageBoxType, StorageBoxTypesClient, StorageBoxTypesPageResult, ) from .domain import StorageBoxType __all__ = [ "BoundStorageBoxType", "StorageBoxType", "StorageBoxTypesClient", "StorageBoxTypesPageResult", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/storage_box_types/client.py0000644000175100017510000000632415152343177021410 0ustar00runnerrunnerfrom __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..core import BoundModelBase, Meta, ResourceClientBase from .domain import StorageBoxType if TYPE_CHECKING: from .._client import Client __all__ = [ "BoundStorageBoxType", "StorageBoxTypesPageResult", "StorageBoxTypesClient", ] class BoundStorageBoxType(BoundModelBase[StorageBoxType], StorageBoxType): _client: StorageBoxTypesClient model = StorageBoxType class StorageBoxTypesPageResult(NamedTuple): storage_box_types: list[BoundStorageBoxType] meta: Meta class StorageBoxTypesClient(ResourceClientBase): """ A client for the Storage Box Types API. See https://docs.hetzner.cloud/reference/hetzner#storage-box-types. """ _base_url = "/storage_box_types" def __init__(self, client: Client): super().__init__(client) self._client = client._client_hetzner def get_by_id(self, id: int) -> BoundStorageBoxType: """ Returns a specific Storage Box Type. See https://docs.hetzner.cloud/reference/hetzner#storage-box-types-get-a-storage-box-type :param id: ID of the Storage Box Type. """ response = self._client.request( method="GET", url=f"{self._base_url}/{id}", ) return BoundStorageBoxType(self, response["storage_box_type"]) def get_by_name(self, name: str) -> BoundStorageBoxType | None: """ Returns a specific Storage Box Type. See https://docs.hetzner.cloud/reference/hetzner#storage-box-types-list-storage-box-types :param name: Name of the Storage Box Type. """ return self._get_first_by(self.get_list, name=name) def get_list( self, name: str | None = None, page: int | None = None, per_page: int | None = None, ) -> StorageBoxTypesPageResult: """ Returns a list of Storage Box Types for a specific page. See https://docs.hetzner.cloud/reference/hetzner#storage-box-types-list-storage-box-types :param name: Name of the Storage Box Type. :param page: Page number to return. :param per_page: Maximum number of entries returned per page. """ params: dict[str, Any] = {} if name is not None: params["name"] = name if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request( method="GET", url=f"{self._base_url}", params=params, ) return StorageBoxTypesPageResult( storage_box_types=[ BoundStorageBoxType(self, o) for o in response["storage_box_types"] ], meta=Meta.parse_meta(response), ) def get_all( self, name: str | None = None, ) -> list[BoundStorageBoxType]: """ Returns all Storage Box Types. See https://docs.hetzner.cloud/reference/hetzner#storage-box-types-list-storage-box-types :param name: Name of the Storage Box Type. """ return self._iter_pages( self.get_list, name=name, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/storage_box_types/domain.py0000644000175100017510000000271415152343177021400 0ustar00runnerrunnerfrom __future__ import annotations from typing import Any from ..core import BaseDomain, DomainIdentityMixin from ..deprecation import DeprecationInfo __all__ = [ "StorageBoxType", ] class StorageBoxType(BaseDomain, DomainIdentityMixin): """ Storage Box Type Domain. See https://docs.hetzner.cloud/reference/hetzner#storage-box-types. """ __api_properties__ = ( "id", "name", "description", "snapshot_limit", "automatic_snapshot_limit", "subaccounts_limit", "size", "deprecation", "prices", ) __slots__ = __api_properties__ def __init__( self, id: int | None = None, name: str | None = None, description: str | None = None, snapshot_limit: int | None = None, automatic_snapshot_limit: int | None = None, subaccounts_limit: int | None = None, size: int | None = None, prices: list[dict[str, Any]] | None = None, deprecation: dict[str, Any] | None = None, ): self.id = id self.name = name self.description = description self.snapshot_limit = snapshot_limit self.automatic_snapshot_limit = automatic_snapshot_limit self.subaccounts_limit = subaccounts_limit self.size = size self.prices = prices self.deprecation = ( DeprecationInfo.from_dict(deprecation) if deprecation is not None else None ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1448739 hcloud-2.17.0/hcloud/storage_boxes/0000755000175100017510000000000015152343221016645 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/storage_boxes/__init__.py0000644000175100017510000000276115152343177020776 0ustar00runnerrunnerfrom __future__ import annotations from .client import ( BoundStorageBox, BoundStorageBoxSnapshot, BoundStorageBoxSubaccount, StorageBoxesClient, StorageBoxesPageResult, StorageBoxSnapshotsPageResult, StorageBoxSubaccountsPageResult, ) from .domain import ( CreateStorageBoxResponse, CreateStorageBoxSnapshotResponse, CreateStorageBoxSubaccountResponse, DeleteStorageBoxResponse, DeleteStorageBoxSnapshotResponse, DeleteStorageBoxSubaccountResponse, StorageBox, StorageBoxAccessSettings, StorageBoxFoldersResponse, StorageBoxSnapshot, StorageBoxSnapshotPlan, StorageBoxSnapshotStats, StorageBoxStats, StorageBoxStatus, StorageBoxSubaccount, StorageBoxSubaccountAccessSettings, ) __all__ = [ "BoundStorageBox", "BoundStorageBoxSnapshot", "BoundStorageBoxSubaccount", "CreateStorageBoxResponse", "CreateStorageBoxSnapshotResponse", "CreateStorageBoxSubaccountResponse", "DeleteStorageBoxResponse", "DeleteStorageBoxSnapshotResponse", "DeleteStorageBoxSubaccountResponse", "StorageBox", "StorageBoxAccessSettings", "StorageBoxesClient", "StorageBoxesPageResult", "StorageBoxFoldersResponse", "StorageBoxSnapshot", "StorageBoxSnapshotPlan", "StorageBoxSnapshotsPageResult", "StorageBoxSnapshotStats", "StorageBoxStats", "StorageBoxStatus", "StorageBoxSubaccount", "StorageBoxSubaccountAccessSettings", "StorageBoxSubaccountsPageResult", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/storage_boxes/client.py0000644000175100017510000015601215152343177020514 0ustar00runnerrunnerfrom __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ( ActionSort, ActionsPageResult, ActionStatus, BoundAction, ResourceActionsClient, ) from ..actions.client import ResourceClientBaseActionsMixin from ..core import BoundModelBase, Meta, ResourceClientBase from ..locations import BoundLocation, Location from ..ssh_keys import BoundSSHKey, SSHKey from ..storage_box_types import BoundStorageBoxType, StorageBoxType from .domain import ( CreateStorageBoxResponse, CreateStorageBoxSnapshotResponse, CreateStorageBoxSubaccountResponse, DeleteStorageBoxResponse, DeleteStorageBoxSnapshotResponse, DeleteStorageBoxSubaccountResponse, StorageBox, StorageBoxAccessSettings, StorageBoxFoldersResponse, StorageBoxSnapshot, StorageBoxSnapshotPlan, StorageBoxSnapshotStats, StorageBoxStats, StorageBoxSubaccount, StorageBoxSubaccountAccessSettings, ) if TYPE_CHECKING: from .._client import Client __all__ = [ "BoundStorageBox", "BoundStorageBoxSnapshot", "BoundStorageBoxSubaccount", "StorageBoxesPageResult", "StorageBoxSnapshotsPageResult", "StorageBoxSubaccountsPageResult", "StorageBoxesClient", ] class BoundStorageBox(BoundModelBase[StorageBox], StorageBox): _client: StorageBoxesClient model = StorageBox def __init__( self, client: StorageBoxesClient, data: dict[str, Any], complete: bool = True, ): raw = data.get("storage_box_type") if raw is not None: data["storage_box_type"] = BoundStorageBoxType( client._parent.storage_box_types, raw ) raw = data.get("location") if raw is not None: data["location"] = BoundLocation(client._parent.locations, raw) raw = data.get("snapshot_plan") if raw is not None: data["snapshot_plan"] = StorageBoxSnapshotPlan.from_dict(raw) raw = data.get("access_settings") if raw is not None: data["access_settings"] = StorageBoxAccessSettings.from_dict(raw) raw = data.get("stats") if raw is not None: data["stats"] = StorageBoxStats.from_dict(raw) super().__init__(client, data, complete) def get_actions_list( self, *, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-actions-list-actions-for-a-storage-box :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._client.get_actions_list( self, status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, *, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-actions-list-actions-for-a-storage-box :param status: Filter the actions by status. The response will only contain actions matching the specified statuses. :param sort: Sort resources by field and direction. """ return self._client.get_actions( self, status=status, sort=sort, ) def update( self, *, name: str | None = None, labels: dict[str, str] | None = None, ) -> BoundStorageBox: """ Updates a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-boxes-update-a-storage-box :param name: Name of the Storage Box. :param labels: User-defined labels (key/value pairs) for the Storage Box. """ return self._client.update( self, name=name, labels=labels, ) def delete(self) -> DeleteStorageBoxResponse: """ Deletes a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-boxes-delete-a-storage-box """ return self._client.delete(self) def get_folders( self, *, path: str | None = None, ) -> StorageBoxFoldersResponse: """ Lists the (sub)folders contained in a Storage Box. Files are not part of the response. See https://docs.hetzner.cloud/reference/hetzner#storage-boxes-list-folders-of-a-storage-box :param path: Relative path to list the folders from. """ return self._client.get_folders( self, path=path, ) def change_protection( self, *, delete: bool | None = None, ) -> BoundAction: """ Changes the protection of a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-actions-change-protection :param delete: Prevents the Storage Box from being deleted. """ return self._client.change_protection( self, delete=delete, ) def change_type( self, storage_box_type: StorageBoxType | BoundStorageBoxType, ) -> BoundAction: """ Changes the type of a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-actions-change-type :param storage_box_type: Storage Box Type to change to. """ return self._client.change_type( self, storage_box_type=storage_box_type, ) def reset_password( self, password: str, ) -> BoundAction: """ Reset the password of a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-actions-reset-password :param password: New password. """ return self._client.reset_password( self, password=password, ) def update_access_settings( self, access_settings: StorageBoxAccessSettings, ) -> BoundAction: """ Update the access settings of a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-actions-update-access-settings :param access_settings: New access settings for the Storage Box. """ return self._client.update_access_settings( self, access_settings=access_settings, ) def rollback_snapshot( self, snapshot: StorageBoxSnapshot | BoundStorageBoxSnapshot, ) -> BoundAction: """ Rollback the Storage Box to the given snapshot. See https://docs.hetzner.cloud/reference/hetzner#storage-box-actions-rollback-snapshot :param snapshot: Snapshot to rollback to. """ return self._client.rollback_snapshot( self, snapshot=snapshot, ) def disable_snapshot_plan( self, ) -> BoundAction: """ Disable the snapshot plan of a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-actions-disable-snapshot-plan """ return self._client.disable_snapshot_plan(self) def enable_snapshot_plan( self, snapshot_plan: StorageBoxSnapshotPlan, ) -> BoundAction: """ Enable the snapshot plan of a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-actions-enable-snapshot-plan :param snapshot_plan: Snapshot Plan to enable. """ return self._client.enable_snapshot_plan( self, snapshot_plan=snapshot_plan, ) # Snapshots ########################################################################### def get_snapshot_by_id( self, id: int, ) -> BoundStorageBoxSnapshot: """ Returns a single Snapshot from a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-snapshots-get-a-snapshot :param id: ID of the Snapshot. """ return self._client.get_snapshot_by_id(self, id=id) def get_snapshot_by_name( self, name: str, ) -> BoundStorageBoxSnapshot | None: """ Returns a single Snapshot from a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-snapshots-list-snapshots :param name: Name of the Snapshot. """ return self._client.get_snapshot_by_name(self, name=name) def get_snapshot_list( self, *, name: str | None = None, is_automatic: bool | None = None, label_selector: str | None = None, sort: list[str] | None = None, ) -> StorageBoxSnapshotsPageResult: """ Returns all Snapshots for a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-snapshots-list-snapshots :param name: Filter resources by their name. The response will only contain the resources matching exactly the specified name. :param is_automatic: Filter wether the snapshot was made by a Snapshot Plan. :param label_selector: Filter resources by labels. The response will only contain resources matching the label selector. :param sort: Sort resources by field and direction. """ return self._client.get_snapshot_list( self, name=name, is_automatic=is_automatic, label_selector=label_selector, sort=sort, ) def get_snapshot_all( self, *, name: str | None = None, is_automatic: bool | None = None, label_selector: str | None = None, sort: list[str] | None = None, ) -> list[BoundStorageBoxSnapshot]: """ Returns all Snapshots for a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-snapshots-list-snapshots :param name: Filter resources by their name. The response will only contain the resources matching exactly the specified name. :param is_automatic: Filter whether the snapshot was made by a Snapshot Plan. :param label_selector: Filter resources by labels. The response will only contain resources matching the label selector. :param sort: Sort resources by field and direction. """ return self._client.get_snapshot_all( self, name=name, is_automatic=is_automatic, label_selector=label_selector, sort=sort, ) def create_snapshot( self, *, description: str | None = None, labels: dict[str, str] | None = None, ) -> CreateStorageBoxSnapshotResponse: """ Creates a Snapshot of the Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-snapshots-create-a-snapshot :param description: Description of the Snapshot. :param labels: User-defined labels (key/value pairs) for the Snapshot. """ return self._client.create_snapshot( self, description=description, labels=labels, ) # Subaccounts ########################################################################### def get_subaccount_by_id( self, id: int, ) -> BoundStorageBoxSubaccount: """ Returns a single Subaccount from a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts-get-a-subaccount :param id: ID of the Subaccount. """ return self._client.get_subaccount_by_id(self, id=id) def get_subaccount_by_name( self, name: str, ) -> BoundStorageBoxSubaccount | None: """ Returns a single Subaccount from a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts-list-subaccounts :param name: Name of the Subaccount. """ return self._client.get_subaccount_by_name(self, name=name) def get_subaccount_by_username( self, username: str, ) -> BoundStorageBoxSubaccount | None: """ Returns a single Subaccount from a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts-list-subaccounts :param username: User name of the Subaccount. """ return self._client.get_subaccount_by_username(self, username=username) def get_subaccount_list( self, *, name: str | None = None, username: str | None = None, label_selector: str | None = None, sort: list[str] | None = None, ) -> StorageBoxSubaccountsPageResult: """ Returns all Subaccounts for a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts-list-subaccounts :param name: Filter resources by their name. The response will only contain the resources matching exactly the specified name. :param username: Filter resources by their username. The response will only contain the resources matching exactly the specified username. :param label_selector: Filter resources by labels. The response will only contain resources matching the label selector. :param sort: Sort resources by field and direction. """ return self._client.get_subaccount_list( self, name=name, username=username, label_selector=label_selector, sort=sort, ) def get_subaccount_all( self, *, name: str | None = None, username: str | None = None, label_selector: str | None = None, sort: list[str] | None = None, ) -> list[BoundStorageBoxSubaccount]: """ Returns all Subaccounts for a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts-list-subaccounts :param name: Filter resources by their name. The response will only contain the resources matching exactly the specified name. :param username: Filter resources by their username. The response will only contain the resources matching exactly the specified username. :param label_selector: Filter resources by labels. The response will only contain resources matching the label selector. :param sort: Sort resources by field and direction. """ return self._client.get_subaccount_all( self, name=name, username=username, label_selector=label_selector, sort=sort, ) def create_subaccount( self, *, name: str | None = None, home_directory: str, password: str, access_settings: StorageBoxSubaccountAccessSettings | None = None, description: str | None = None, labels: dict[str, str] | None = None, ) -> CreateStorageBoxSubaccountResponse: """ Creates a Subaccount for the Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts-create-a-subaccount :param storage_box: Storage Box to create a Subaccount for. :param name: Name of the Subaccount. :param home_directory: Home directory of the Subaccount. :param password: Password of the Subaccount. :param access_settings: Access settings of the Subaccount. :param description: Description of the Subaccount. :param labels: User-defined labels (key/value pairs) for the Subaccount. """ return self._client.create_subaccount( self, name=name, home_directory=home_directory, password=password, access_settings=access_settings, description=description, labels=labels, ) class BoundStorageBoxSnapshot(BoundModelBase[StorageBoxSnapshot], StorageBoxSnapshot): _client: StorageBoxesClient model = StorageBoxSnapshot def __init__( self, client: StorageBoxesClient, data: dict[str, Any], complete: bool = True, ): raw = data.get("storage_box") if raw is not None: data["storage_box"] = BoundStorageBox( client, data={"id": raw}, complete=False ) raw = data.get("stats") if raw is not None: data["stats"] = StorageBoxSnapshotStats.from_dict(raw) 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, ) def update( self, *, description: str | None = None, labels: dict[str, str] | None = None, ) -> BoundStorageBoxSnapshot: """ Updates a Storage Box Snapshot. See https://docs.hetzner.cloud/reference/hetzner#storage-box-snapshots-update-a-snapshot :param description: Description of the Snapshot. :param labels: User-defined labels (key/value pairs) for the Snapshot. """ return self._client.update_snapshot( self, description=description, labels=labels, ) def delete( self, ) -> DeleteStorageBoxSnapshotResponse: """ Deletes a Storage Box Snapshot. See https://docs.hetzner.cloud/reference/hetzner#storage-box-snapshots-delete-a-snapshot """ return self._client.delete_snapshot(self) class BoundStorageBoxSubaccount( BoundModelBase[StorageBoxSubaccount], StorageBoxSubaccount ): _client: StorageBoxesClient model = StorageBoxSubaccount def __init__( self, client: StorageBoxesClient, data: dict[str, Any], complete: bool = True, ): raw = data.get("storage_box") if raw is not None: data["storage_box"] = BoundStorageBox( client, data={"id": raw}, complete=False ) raw = data.get("access_settings") if raw is not None: data["access_settings"] = StorageBoxSubaccountAccessSettings.from_dict(raw) 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, ) def update( self, *, name: str | None = None, description: str | None = None, labels: dict[str, str] | None = None, ) -> BoundStorageBoxSubaccount: """ Updates a Storage Box Subaccount. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts-update-a-subaccount :param name: Name of the Subaccount. :param description: Description of the Subaccount. :param labels: User-defined labels (key/value pairs) for the Subaccount. """ return self._client.update_subaccount( self, name=name, description=description, labels=labels, ) def delete( self, ) -> DeleteStorageBoxSubaccountResponse: """ Deletes a Storage Box Subaccount. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts-delete-a-subaccount """ return self._client.delete_subaccount(self) def change_home_directory( self, home_directory: str, ) -> BoundAction: """ Change the home directory of a Storage Box Subaccount. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccount-actions-change-home-directory :param home_directory: Home directory for the Subaccount. """ return self._client.change_subaccount_home_directory( self, home_directory=home_directory ) def reset_password( self, password: str, ) -> BoundAction: """ Reset the password of a Storage Box Subaccount. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccount-actions-reset-password :param password: Password for the Subaccount. """ return self._client.reset_subaccount_password(self, password=password) def update_access_settings( self, access_settings: StorageBoxSubaccountAccessSettings, ) -> BoundAction: """ Update the access settings of a Storage Box Subaccount. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccount-actions-update-access-settings :param access_settings: Access settings for the Subaccount. """ return self._client.update_subaccount_access_settings( self, access_settings=access_settings, ) class StorageBoxesPageResult(NamedTuple): storage_boxes: list[BoundStorageBox] meta: Meta class StorageBoxSnapshotsPageResult(NamedTuple): snapshots: list[BoundStorageBoxSnapshot] meta: Meta class StorageBoxSubaccountsPageResult(NamedTuple): subaccounts: list[BoundStorageBoxSubaccount] meta: Meta class StorageBoxesClient( ResourceClientBaseActionsMixin, ResourceClientBase, ): """ A client for the Storage Boxes API. See https://docs.hetzner.cloud/reference/hetzner#storage-boxes. """ _base_url = "/storage_boxes" actions: ResourceActionsClient """Storage Boxes scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self._client = client._client_hetzner self.actions = ResourceActionsClient(self, self._base_url) def get_by_id(self, id: int) -> BoundStorageBox: """ Returns a specific Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-boxes-get-a-storage-box :param id: ID of the Storage Box. """ response = self._client.request( method="GET", url=f"{self._base_url}/{id}", ) return BoundStorageBox(self, response["storage_box"]) def get_by_name(self, name: str) -> BoundStorageBox | None: """ Returns a specific Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-boxes-list-storage-boxes :param name: Name of the Storage Box. """ return self._get_first_by(self.get_list, name=name) def get_list( self, *, name: str | None = None, label_selector: str | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> StorageBoxesPageResult: """ Returns a paginated list of Storage Boxes for a specific page. See https://docs.hetzner.cloud/reference/hetzner#storage-boxes-list-storage-boxes :param name: Name of the Storage Box. :param label_selector: Filter resources by labels. The response will only contain resources matching the label selector. :param sort: Sort resources by field and direction. :param page: Page number to return. :param per_page: Maximum number of entries returned per page. """ params: dict[str, Any] = {} if name is not None: params["name"] = name 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 sort is not None: params["sort"] = sort response = self._client.request( method="GET", url=f"{self._base_url}", params=params, ) return StorageBoxesPageResult( storage_boxes=[BoundStorageBox(self, o) for o in response["storage_boxes"]], meta=Meta.parse_meta(response), ) def get_all( self, *, name: str | None = None, label_selector: str | None = None, sort: list[str] | None = None, ) -> list[BoundStorageBox]: """ Returns all Storage Boxes. See https://docs.hetzner.cloud/reference/hetzner#storage-boxes-list-storage-boxes :param name: Name of the Storage Box. :param label_selector: Filter resources by labels. The response will only contain resources matching the label selector. :param sort: Sort resources by field and direction. """ return self._iter_pages( self.get_list, name=name, label_selector=label_selector, sort=sort, ) def create( self, *, name: str, password: str, location: BoundLocation | Location, storage_box_type: BoundStorageBoxType | StorageBoxType, ssh_keys: list[str | SSHKey | BoundSSHKey] | None = None, access_settings: StorageBoxAccessSettings | None = None, labels: dict[str, str] | None = None, ) -> CreateStorageBoxResponse: """ Creates a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-boxes-create-a-storage-box :param name: Name of the Storage Box. :param password: Password of the Storage Box. :param location: Location of the Storage Box. :param storage_box_type: Type of the Storage Box. :param ssh_keys: SSH public keys of the Storage Box. :param access_settings: Access settings of the Storage Box. :param labels: User-defined labels (key/value pairs) for the Storage Box. """ data: dict[str, Any] = { "name": name, "password": password, "location": location.id_or_name, "storage_box_type": storage_box_type.id_or_name, } if ssh_keys is not None: data["ssh_keys"] = [ o.public_key if isinstance(o, (SSHKey, BoundSSHKey)) else o for o in ssh_keys ] if access_settings is not None: data["access_settings"] = access_settings.to_payload() if labels is not None: data["labels"] = labels response = self._client.request( method="POST", url=f"{self._base_url}", json=data, ) return CreateStorageBoxResponse( storage_box=BoundStorageBox(self, response["storage_box"]), action=BoundAction(self._parent.actions, response["action"]), ) def update( self, storage_box: BoundStorageBox | StorageBox, *, name: str | None = None, labels: dict[str, str] | None = None, ) -> BoundStorageBox: """ Updates a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-boxes-update-a-storage-box :param storage_box: Storage Box to update. :param name: Name of the Storage Box. :param labels: User-defined labels (key/value pairs) for the Storage Box. """ data: dict[str, Any] = {} if name is not None: data["name"] = name if labels is not None: data["labels"] = labels response = self._client.request( method="PUT", url=f"{self._base_url}/{storage_box.id}", json=data, ) return BoundStorageBox(self, response["storage_box"]) def delete( self, storage_box: BoundStorageBox | StorageBox, ) -> DeleteStorageBoxResponse: """ Deletes a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-boxes-delete-a-storage-box :param storage_box: Storage Box to delete. """ response = self._client.request( method="DELETE", url=f"{self._base_url}/{storage_box.id}", ) return DeleteStorageBoxResponse( action=BoundAction(self._parent.actions, response["action"]) ) def get_folders( self, storage_box: BoundStorageBox | StorageBox, *, path: str | None = None, ) -> StorageBoxFoldersResponse: """ Lists the (sub)folders contained in a Storage Box. Files are not part of the response. See https://docs.hetzner.cloud/reference/hetzner#storage-boxes-list-folders-of-a-storage-box :param storage_box: Storage Box to list the folders from. :param path: Relative path to list the folders from. """ params: dict[str, Any] = {} if path is not None: params["path"] = path response = self._client.request( method="GET", url=f"{self._base_url}/{storage_box.id}/folders", params=params, ) return StorageBoxFoldersResponse(folders=response["folders"]) def get_actions_list( self, storage_box: StorageBox | BoundStorageBox, *, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-actions-list-actions-for-a-storage-box :param storage_box: Storage Box to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._get_actions_list( f"{self._base_url}/{storage_box.id}", status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, storage_box: StorageBox | BoundStorageBox, *, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-actions-list-actions-for-a-storage-box :param storage_box: Storage Box to get the Actions for. :param status: Filter the actions by status. The response will only contain actions matching the specified statuses. :param sort: Sort resources by field and direction. """ return self._iter_pages( self.get_actions_list, storage_box, status=status, sort=sort, ) def change_protection( self, storage_box: StorageBox | BoundStorageBox, *, delete: bool | None = None, ) -> BoundAction: """ Changes the protection of a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-actions-change-protection :param storage_box: Storage Box to update. :param delete: Prevents the Storage Box from being deleted. """ data: dict[str, Any] = {} if delete is not None: data["delete"] = delete response = self._client.request( method="POST", url=f"{self._base_url}/{storage_box.id}/actions/change_protection", json=data, ) return BoundAction(self._parent.actions, response["action"]) def change_type( self, storage_box: StorageBox | BoundStorageBox, storage_box_type: StorageBoxType | BoundStorageBoxType, ) -> BoundAction: """ Changes the type of a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-actions-change-type :param storage_box: Storage Box to update. :param storage_box_type: Storage Box Type to change to. """ data: dict[str, Any] = { "storage_box_type": storage_box_type.id_or_name, } response = self._client.request( method="POST", url=f"{self._base_url}/{storage_box.id}/actions/change_type", json=data, ) return BoundAction(self._parent.actions, response["action"]) def reset_password( self, storage_box: StorageBox | BoundStorageBox, password: str, ) -> BoundAction: """ Reset the password of a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-actions-reset-password :param storage_box: Storage Box to update. :param password: New password. """ data: dict[str, Any] = { "password": password, } response = self._client.request( method="POST", url=f"{self._base_url}/{storage_box.id}/actions/reset_password", json=data, ) return BoundAction(self._parent.actions, response["action"]) def update_access_settings( self, storage_box: StorageBox | BoundStorageBox, access_settings: StorageBoxAccessSettings, ) -> BoundAction: """ Update the access settings of a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-actions-update-access-settings :param storage_box: Storage Box to update. :param access_settings: New access settings for the Storage Box. """ data: dict[str, Any] = access_settings.to_payload() response = self._client.request( method="POST", url=f"{self._base_url}/{storage_box.id}/actions/update_access_settings", json=data, ) return BoundAction(self._parent.actions, response["action"]) def rollback_snapshot( self, storage_box: StorageBox | BoundStorageBox, snapshot: StorageBoxSnapshot | BoundStorageBoxSnapshot, ) -> BoundAction: """ Rollback the Storage Box to the given snapshot. See https://docs.hetzner.cloud/reference/hetzner#storage-box-actions-rollback-snapshot :param storage_box: Storage Box to update. :param snapshot: Snapshot to rollback to. """ data: dict[str, Any] = { "snapshot": snapshot.id_or_name, } response = self._client.request( method="POST", url=f"{self._base_url}/{storage_box.id}/actions/rollback_snapshot", json=data, ) return BoundAction(self._parent.actions, response["action"]) def disable_snapshot_plan( self, storage_box: StorageBox | BoundStorageBox, ) -> BoundAction: """ Disable the snapshot plan of a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-actions-disable-snapshot-plan :param storage_box: Storage Box to update. """ response = self._client.request( method="POST", url=f"{self._base_url}/{storage_box.id}/actions/disable_snapshot_plan", ) return BoundAction(self._parent.actions, response["action"]) def enable_snapshot_plan( self, storage_box: StorageBox | BoundStorageBox, snapshot_plan: StorageBoxSnapshotPlan, ) -> BoundAction: """ Enable the snapshot plan of a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-actions-enable-snapshot-plan :param storage_box: Storage Box to update. :param snapshot_plan: Snapshot Plan to enable. """ data: dict[str, Any] = snapshot_plan.to_payload() response = self._client.request( method="POST", url=f"{self._base_url}/{storage_box.id}/actions/enable_snapshot_plan", json=data, ) return BoundAction(self._parent.actions, response["action"]) # Snapshots ########################################################################### def get_snapshot_by_id( self, storage_box: StorageBox | BoundStorageBox, id: int, ) -> BoundStorageBoxSnapshot: """ Returns a single Snapshot from a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-snapshots-get-a-snapshot :param storage_box: Storage Box to get the Snapshot from. :param id: ID of the Snapshot. """ response = self._client.request( method="GET", url=f"{self._base_url}/{storage_box.id}/snapshots/{id}", ) return BoundStorageBoxSnapshot(self, response["snapshot"]) def get_snapshot_by_name( self, storage_box: StorageBox | BoundStorageBox, name: str, ) -> BoundStorageBoxSnapshot | None: """ Returns a single Snapshot from a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-snapshots-list-snapshots :param storage_box: Storage Box to get the Snapshot from. :param name: Name of the Snapshot. """ return self._get_first_by(self.get_snapshot_list, storage_box, name=name) def get_snapshot_list( self, storage_box: StorageBox | BoundStorageBox, *, name: str | None = None, is_automatic: bool | None = None, label_selector: str | None = None, sort: list[str] | None = None, ) -> StorageBoxSnapshotsPageResult: """ Returns all Snapshots for a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-snapshots-list-snapshots :param storage_box: Storage Box to get the Snapshots from. :param name: Filter resources by their name. The response will only contain the resources matching exactly the specified name. :param is_automatic: Filter whether the snapshot was made by a Snapshot Plan. :param label_selector: Filter resources by labels. The response will only contain resources matching the label selector. :param sort: Sort resources by field and direction. """ params: dict[str, Any] = {} if name is not None: params["name"] = name if is_automatic is not None: params["is_automatic"] = is_automatic if label_selector is not None: params["label_selector"] = label_selector if sort is not None: params["sort"] = sort response = self._client.request( method="GET", url=f"{self._base_url}/{storage_box.id}/snapshots", params=params, ) return StorageBoxSnapshotsPageResult( snapshots=[ BoundStorageBoxSnapshot(self, item) for item in response["snapshots"] ], meta=Meta.parse_meta(response), ) def get_snapshot_all( self, storage_box: StorageBox | BoundStorageBox, *, name: str | None = None, is_automatic: bool | None = None, label_selector: str | None = None, sort: list[str] | None = None, ) -> list[BoundStorageBoxSnapshot]: """ Returns all Snapshots for a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-snapshots-list-snapshots :param storage_box: Storage Box to get the Snapshots from. :param name: Filter resources by their name. The response will only contain the resources matching exactly the specified name. :param is_automatic: Filter whether the snapshot was made by a Snapshot Plan. :param label_selector: Filter resources by labels. The response will only contain resources matching the label selector. :param sort: Sort resources by field and direction. """ # The endpoint does not have pagination, forward to the list method. result, _ = self.get_snapshot_list( storage_box, name=name, is_automatic=is_automatic, label_selector=label_selector, sort=sort, ) return result def create_snapshot( self, storage_box: StorageBox | BoundStorageBox, *, description: str | None = None, labels: dict[str, str] | None = None, ) -> CreateStorageBoxSnapshotResponse: """ Creates a Snapshot of the Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-snapshots-create-a-snapshot :param storage_box: Storage Box to create a Snapshot from. :param description: Description of the Snapshot. :param labels: User-defined labels (key/value pairs) for the Snapshot. """ data: dict[str, Any] = {} if description is not None: data["description"] = description if labels is not None: data["labels"] = labels response = self._client.request( method="POST", url=f"{self._base_url}/{storage_box.id}/snapshots", json=data, ) return CreateStorageBoxSnapshotResponse( snapshot=BoundStorageBoxSnapshot( self, response["snapshot"], # API only returns a partial object. complete=False, ), action=BoundAction(self._parent.actions, response["action"]), ) def update_snapshot( self, snapshot: StorageBoxSnapshot | BoundStorageBoxSnapshot, *, description: str | None = None, labels: dict[str, str] | None = None, ) -> BoundStorageBoxSnapshot: """ Updates a Storage Box Snapshot. See https://docs.hetzner.cloud/reference/hetzner#storage-box-snapshots-update-a-snapshot :param snapshot: Storage Box Snapshot to update. :param description: Description of the Snapshot. :param labels: User-defined labels (key/value pairs) for the Snapshot. """ if snapshot.storage_box is None: raise ValueError("snapshot storage_box property is none") data: dict[str, Any] = {} if description is not None: data["description"] = description if labels is not None: data["labels"] = labels response = self._client.request( method="PUT", url=f"{self._base_url}/{snapshot.storage_box.id}/snapshots/{snapshot.id}", json=data, ) return BoundStorageBoxSnapshot(self, response["snapshot"]) def delete_snapshot( self, snapshot: StorageBoxSnapshot | BoundStorageBoxSnapshot, ) -> DeleteStorageBoxSnapshotResponse: """ Deletes a Storage Box Snapshot. See https://docs.hetzner.cloud/reference/hetzner#storage-box-snapshots-delete-a-snapshot :param snapshot: Storage Box Snapshot to delete. """ if snapshot.storage_box is None: raise ValueError("snapshot storage_box property is none") response = self._client.request( method="DELETE", url=f"{self._base_url}/{snapshot.storage_box.id}/snapshots/{snapshot.id}", ) return DeleteStorageBoxSnapshotResponse( action=BoundAction(self._parent.actions, response["action"]), ) # Subaccounts ########################################################################### def get_subaccount_by_id( self, storage_box: StorageBox | BoundStorageBox, id: int, ) -> BoundStorageBoxSubaccount: """ Returns a single Subaccount from a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts-get-a-subaccount :param storage_box: Storage Box to get the Subaccount from. :param id: ID of the Subaccount. """ response = self._client.request( method="GET", url=f"{self._base_url}/{storage_box.id}/subaccounts/{id}", ) return BoundStorageBoxSubaccount(self, response["subaccount"]) def get_subaccount_by_name( self, storage_box: StorageBox | BoundStorageBox, name: str, ) -> BoundStorageBoxSubaccount | None: """ Returns a single Subaccount from a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts-list-subaccounts :param storage_box: Storage Box to get the Subaccount from. :param name: Name of the Subaccount. """ return self._get_first_by( self.get_subaccount_list, storage_box, name=name, ) def get_subaccount_by_username( self, storage_box: StorageBox | BoundStorageBox, username: str, ) -> BoundStorageBoxSubaccount | None: """ Returns a single Subaccount from a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts-list-subaccounts :param storage_box: Storage Box to get the Subaccount from. :param username: User name of the Subaccount. """ return self._get_first_by( self.get_subaccount_list, storage_box, username=username, ) def get_subaccount_list( self, storage_box: StorageBox | BoundStorageBox, *, name: str | None = None, username: str | None = None, label_selector: str | None = None, sort: list[str] | None = None, ) -> StorageBoxSubaccountsPageResult: """ Returns all Subaccounts for a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts-list-subaccounts :param storage_box: Storage Box to get the Subaccount from. :param name: Filter resources by their name. The response will only contain the resources matching exactly the specified name. :param username: Filter resources by their username. The response will only contain the resources matching exactly the specified username. :param label_selector: Filter resources by labels. The response will only contain resources matching the label selector. :param sort: Sort resources by field and direction. """ params: dict[str, Any] = {} if name is not None: params["name"] = name if username is not None: params["username"] = username if label_selector is not None: params["label_selector"] = label_selector if sort is not None: params["sort"] = sort response = self._client.request( method="GET", url=f"{self._base_url}/{storage_box.id}/subaccounts", params=params, ) return StorageBoxSubaccountsPageResult( subaccounts=[ BoundStorageBoxSubaccount(self, item) for item in response["subaccounts"] ], meta=Meta.parse_meta(response), ) def get_subaccount_all( self, storage_box: StorageBox | BoundStorageBox, *, name: str | None = None, username: str | None = None, label_selector: str | None = None, sort: list[str] | None = None, ) -> list[BoundStorageBoxSubaccount]: """ Returns all Subaccounts for a Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts-list-subaccounts :param storage_box: Storage Box to get the Subaccount from. :param name: Filter resources by their name. The response will only contain the resources matching exactly the specified name. :param username: Filter resources by their username. The response will only contain the resources matching exactly the specified username. :param label_selector: Filter resources by labels. The response will only contain resources matching the label selector. :param sort: Sort resources by field and direction. """ # The endpoint does not have pagination, forward to the list method. result, _ = self.get_subaccount_list( storage_box, name=name, username=username, label_selector=label_selector, sort=sort, ) return result def create_subaccount( self, storage_box: StorageBox | BoundStorageBox, *, name: str | None = None, home_directory: str, password: str, access_settings: StorageBoxSubaccountAccessSettings | None = None, description: str | None = None, labels: dict[str, str] | None = None, ) -> CreateStorageBoxSubaccountResponse: """ Creates a Subaccount for the Storage Box. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts-create-a-subaccount :param storage_box: Storage Box to create a Subaccount for. :param name: Name of the Subaccount. :param home_directory: Home directory of the Subaccount. :param password: Password of the Subaccount. :param access_settings: Access settings of the Subaccount. :param description: Description of the Subaccount. :param labels: User-defined labels (key/value pairs) for the Subaccount. """ data: dict[str, Any] = { "home_directory": home_directory, "password": password, } if name is not None: data["name"] = name if access_settings is not None: data["access_settings"] = access_settings.to_payload() if description is not None: data["description"] = description if labels is not None: data["labels"] = labels response = self._client.request( method="POST", url=f"{self._base_url}/{storage_box.id}/subaccounts", json=data, ) return CreateStorageBoxSubaccountResponse( subaccount=BoundStorageBoxSubaccount( self, response["subaccount"], # API only returns a partial object. complete=False, ), action=BoundAction(self._parent.actions, response["action"]), ) def update_subaccount( self, subaccount: StorageBoxSubaccount | BoundStorageBoxSubaccount, *, name: str | None = None, description: str | None = None, labels: dict[str, str] | None = None, ) -> BoundStorageBoxSubaccount: """ Updates a Storage Box Subaccount. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts-update-a-subaccount :param subaccount: Storage Box Subaccount to update. :param name: Name of the Subaccount. :param description: Description of the Subaccount. :param labels: User-defined labels (key/value pairs) for the Subaccount. """ if subaccount.storage_box is None: raise ValueError("subaccount storage_box property is none") data: dict[str, Any] = {} if name is not None: data["name"] = name if description is not None: data["description"] = description if labels is not None: data["labels"] = labels response = self._client.request( method="PUT", url=f"{self._base_url}/{subaccount.storage_box.id}/subaccounts/{subaccount.id}", json=data, ) return BoundStorageBoxSubaccount(self, response["subaccount"]) def delete_subaccount( self, subaccount: StorageBoxSubaccount | BoundStorageBoxSubaccount, ) -> DeleteStorageBoxSubaccountResponse: """ Deletes a Storage Box Subaccount. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccounts-delete-a-subaccount :param subaccount: Storage Box Subaccount to delete. """ if subaccount.storage_box is None: raise ValueError("subaccount storage_box property is none") response = self._client.request( method="DELETE", url=f"{self._base_url}/{subaccount.storage_box.id}/subaccounts/{subaccount.id}", ) return DeleteStorageBoxSubaccountResponse( action=BoundAction(self._parent.actions, response["action"]), ) def change_subaccount_home_directory( self, subaccount: StorageBoxSubaccount | BoundStorageBoxSubaccount, home_directory: str, ) -> BoundAction: """ Change the home directory of a Storage Box Subaccount. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccount-actions-change-home-directory :param subaccount: Storage Box Subaccount to update. :param home_directory: Home directory for the Subaccount. """ if subaccount.storage_box is None: raise ValueError("subaccount storage_box property is none") data: dict[str, Any] = { "home_directory": home_directory, } response = self._client.request( method="POST", url=f"{self._base_url}/{subaccount.storage_box.id}/subaccounts/{subaccount.id}/actions/change_home_directory", json=data, ) return BoundAction(self._parent.actions, response["action"]) def reset_subaccount_password( self, subaccount: StorageBoxSubaccount | BoundStorageBoxSubaccount, password: str, ) -> BoundAction: """ Reset the password of a Storage Box Subaccount. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccount-actions-reset-password :param subaccount: Storage Box Subaccount to update. :param password: Password for the Subaccount. """ if subaccount.storage_box is None: raise ValueError("subaccount storage_box property is none") data: dict[str, Any] = { "password": password, } response = self._client.request( method="POST", url=f"{self._base_url}/{subaccount.storage_box.id}/subaccounts/{subaccount.id}/actions/reset_subaccount_password", json=data, ) return BoundAction(self._parent.actions, response["action"]) def update_subaccount_access_settings( self, subaccount: StorageBoxSubaccount | BoundStorageBoxSubaccount, access_settings: StorageBoxSubaccountAccessSettings, ) -> BoundAction: """ Update the access settings of a Storage Box Subaccount. See https://docs.hetzner.cloud/reference/hetzner#storage-box-subaccount-actions-update-access-settings :param subaccount: Storage Box Subaccount to update. :param access_settings: Access settings for the Subaccount. """ if subaccount.storage_box is None: raise ValueError("subaccount storage_box property is none") data: dict[str, Any] = access_settings.to_payload() response = self._client.request( method="POST", url=f"{self._base_url}/{subaccount.storage_box.id}/subaccounts/{subaccount.id}/actions/update_access_settings", json=data, ) return BoundAction(self._parent.actions, response["action"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/storage_boxes/domain.py0000644000175100017510000003033015152343177020477 0ustar00runnerrunnerfrom __future__ import annotations from typing import TYPE_CHECKING, Any, Literal from ..actions import BoundAction from ..core import BaseDomain, DomainIdentityMixin from ..locations import BoundLocation, Location from ..storage_box_types import BoundStorageBoxType, StorageBoxType if TYPE_CHECKING: from .client import ( BoundStorageBox, BoundStorageBoxSnapshot, BoundStorageBoxSubaccount, ) __all__ = [ "StorageBox", "StorageBoxAccessSettings", "StorageBoxStats", "StorageBoxSnapshotPlan", "CreateStorageBoxResponse", "DeleteStorageBoxResponse", "StorageBoxFoldersResponse", "StorageBoxSnapshot", "StorageBoxSnapshotStats", "CreateStorageBoxSnapshotResponse", "DeleteStorageBoxSnapshotResponse", "StorageBoxSubaccount", "StorageBoxSubaccountAccessSettings", "CreateStorageBoxSubaccountResponse", "DeleteStorageBoxSubaccountResponse", "StorageBoxStatus", ] StorageBoxStatus = Literal[ "active", "initializing", "locked", ] class StorageBox(BaseDomain, DomainIdentityMixin): """ Storage Box Domain. See https://docs.hetzner.cloud/reference/hetzner#storage-boxes. """ STATUS_ACTIVE = "active" STATUS_INITIALIZING = "initializing" STATUS_LOCKED = "locked" __api_properties__ = ( "id", "name", "storage_box_type", "location", "system", "server", "username", "labels", "protection", "snapshot_plan", "access_settings", "stats", "status", "created", ) __slots__ = __api_properties__ def __init__( self, id: int | None = None, name: str | None = None, storage_box_type: BoundStorageBoxType | StorageBoxType | None = None, location: BoundLocation | Location | None = None, system: str | None = None, server: str | None = None, username: str | None = None, labels: dict[str, str] | None = None, protection: dict[str, bool] | None = None, snapshot_plan: StorageBoxSnapshotPlan | None = None, access_settings: StorageBoxAccessSettings | None = None, stats: StorageBoxStats | None = None, status: StorageBoxStatus | None = None, created: str | None = None, ): self.id = id self.name = name self.storage_box_type = storage_box_type self.location = location self.system = system self.server = server self.username = username self.labels = labels self.protection = protection self.snapshot_plan = snapshot_plan self.access_settings = access_settings self.stats = stats self.status = status self.created = self._parse_datetime(created) class StorageBoxAccessSettings(BaseDomain): """ Storage Box Access Settings Domain. """ __api_properties__ = ( "reachable_externally", "samba_enabled", "ssh_enabled", "webdav_enabled", "zfs_enabled", ) __slots__ = __api_properties__ def __init__( self, reachable_externally: bool | None = None, samba_enabled: bool | None = None, ssh_enabled: bool | None = None, webdav_enabled: bool | None = None, zfs_enabled: bool | None = None, ): self.reachable_externally = reachable_externally self.samba_enabled = samba_enabled self.ssh_enabled = ssh_enabled self.webdav_enabled = webdav_enabled self.zfs_enabled = zfs_enabled def to_payload(self) -> dict[str, Any]: """ Generates the request payload from this domain object. """ payload: dict[str, Any] = {} if self.reachable_externally is not None: payload["reachable_externally"] = self.reachable_externally if self.samba_enabled is not None: payload["samba_enabled"] = self.samba_enabled if self.ssh_enabled is not None: payload["ssh_enabled"] = self.ssh_enabled if self.webdav_enabled is not None: payload["webdav_enabled"] = self.webdav_enabled if self.zfs_enabled is not None: payload["zfs_enabled"] = self.zfs_enabled return payload class StorageBoxStats(BaseDomain): """ Storage Box Stats Domain. """ __api_properties__ = ( "size", "size_data", "size_snapshots", ) __slots__ = __api_properties__ def __init__( self, size: int | None = None, size_data: int | None = None, size_snapshots: int | None = None, ): self.size = size self.size_data = size_data self.size_snapshots = size_snapshots class StorageBoxSnapshotPlan(BaseDomain): """ Storage Box Snapshot Plan Domain. """ __api_properties__ = ( "max_snapshots", "hour", "minute", "day_of_week", "day_of_month", ) __slots__ = __api_properties__ def __init__( self, max_snapshots: int, hour: int, minute: int, day_of_week: int | None = None, day_of_month: int | None = None, ): self.max_snapshots = max_snapshots self.hour = hour self.minute = minute self.day_of_week = day_of_week self.day_of_month = day_of_month def to_payload(self) -> dict[str, Any]: """ Generates the request payload from this domain object. """ payload: dict[str, Any] = { "max_snapshots": self.max_snapshots, "hour": self.hour, "minute": self.minute, "day_of_week": self.day_of_week, # API default is null "day_of_month": self.day_of_month, # API default is null } return payload class CreateStorageBoxResponse(BaseDomain): """ Create Storage Box Response Domain. """ __api_properties__ = ( "storage_box", "action", ) __slots__ = __api_properties__ def __init__( self, storage_box: BoundStorageBox, action: BoundAction, ): self.storage_box = storage_box self.action = action class DeleteStorageBoxResponse(BaseDomain): """ Delete Storage Box Response Domain. """ __api_properties__ = ("action",) __slots__ = __api_properties__ def __init__( self, action: BoundAction, ): self.action = action class StorageBoxFoldersResponse(BaseDomain): """ Storage Box Folders Response Domain. """ __api_properties__ = ("folders",) __slots__ = __api_properties__ def __init__( self, folders: list[str], ): self.folders = folders # Snapshots ############################################################################### class StorageBoxSnapshot(BaseDomain, DomainIdentityMixin): """ Storage Box Snapshot Domain. """ __api_properties__ = ( "id", "name", "description", "is_automatic", "labels", "storage_box", "created", "stats", ) __slots__ = __api_properties__ def __init__( self, id: int | None = None, name: str | None = None, description: str | None = None, is_automatic: bool | None = None, labels: dict[str, str] | None = None, storage_box: BoundStorageBox | StorageBox | None = None, created: str | None = None, stats: StorageBoxSnapshotStats | None = None, ): self.id = id self.name = name self.description = description self.is_automatic = is_automatic self.labels = labels self.storage_box = storage_box self.created = self._parse_datetime(created) self.stats = stats class StorageBoxSnapshotStats(BaseDomain): """ Storage Box Snapshot Stats Domain. """ __api_properties__ = ( "size", "size_filesystem", ) __slots__ = __api_properties__ def __init__( self, size: int, size_filesystem: int, ): self.size = size self.size_filesystem = size_filesystem class CreateStorageBoxSnapshotResponse(BaseDomain): """ Create Storage Box Snapshot Response Domain. """ __api_properties__ = ( "snapshot", "action", ) __slots__ = __api_properties__ def __init__( self, snapshot: BoundStorageBoxSnapshot, action: BoundAction, ): self.snapshot = snapshot self.action = action class DeleteStorageBoxSnapshotResponse(BaseDomain): """ Delete Storage Box Snapshot Response Domain. """ __api_properties__ = ("action",) __slots__ = __api_properties__ def __init__( self, action: BoundAction, ): self.action = action # Subaccounts ############################################################################### class StorageBoxSubaccount(BaseDomain, DomainIdentityMixin): """ Storage Box Subaccount Domain. """ __api_properties__ = ( "id", "name", "username", "description", "server", "home_directory", "access_settings", "labels", "storage_box", "created", ) __slots__ = __api_properties__ def __init__( self, id: int | None = None, name: str | None = None, username: str | None = None, description: str | None = None, server: str | None = None, home_directory: str | None = None, access_settings: StorageBoxSubaccountAccessSettings | None = None, labels: dict[str, str] | None = None, storage_box: BoundStorageBox | StorageBox | None = None, created: str | None = None, ): self.id = id self.name = name self.username = username self.description = description self.server = server self.home_directory = home_directory self.access_settings = access_settings self.labels = labels self.storage_box = storage_box self.created = self._parse_datetime(created) class StorageBoxSubaccountAccessSettings(BaseDomain): """ Storage Box Subaccount Access Settings Domain. """ __api_properties__ = ( "reachable_externally", "samba_enabled", "ssh_enabled", "webdav_enabled", "readonly", ) __slots__ = __api_properties__ def __init__( self, reachable_externally: bool | None = None, samba_enabled: bool | None = None, ssh_enabled: bool | None = None, webdav_enabled: bool | None = None, readonly: bool | None = None, ): self.reachable_externally = reachable_externally self.samba_enabled = samba_enabled self.ssh_enabled = ssh_enabled self.webdav_enabled = webdav_enabled self.readonly = readonly def to_payload(self) -> dict[str, Any]: """ Generates the request payload from this domain object. """ payload: dict[str, Any] = {} if self.reachable_externally is not None: payload["reachable_externally"] = self.reachable_externally if self.samba_enabled is not None: payload["samba_enabled"] = self.samba_enabled if self.ssh_enabled is not None: payload["ssh_enabled"] = self.ssh_enabled if self.webdav_enabled is not None: payload["webdav_enabled"] = self.webdav_enabled if self.readonly is not None: payload["readonly"] = self.readonly return payload class CreateStorageBoxSubaccountResponse(BaseDomain): """ Create Storage Box Subaccount Response Domain. """ __api_properties__ = ( "subaccount", "action", ) __slots__ = __api_properties__ def __init__( self, subaccount: BoundStorageBoxSubaccount, action: BoundAction, ): self.subaccount = subaccount self.action = action class DeleteStorageBoxSubaccountResponse(BaseDomain): """ Delete Storage Box Subaccount Response Domain. """ __api_properties__ = ("action",) __slots__ = __api_properties__ def __init__( self, action: BoundAction, ): self.action = action ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1455138 hcloud-2.17.0/hcloud/volumes/0000755000175100017510000000000015152343221015473 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/volumes/__init__.py0000644000175100017510000000047315152343177017622 0ustar00runnerrunnerfrom __future__ import annotations from .client import BoundVolume, VolumesClient, VolumesPageResult from .domain import CreateVolumeResponse, Volume, VolumeProtection __all__ = [ "BoundVolume", "CreateVolumeResponse", "Volume", "VolumeProtection", "VolumesClient", "VolumesPageResult", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/volumes/client.py0000644000175100017510000004055015152343177017341 0ustar00runnerrunnerfrom __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ( ActionSort, ActionsPageResult, ActionStatus, BoundAction, ResourceActionsClient, ) from ..actions.client import ResourceClientBaseActionsMixin from ..core import BoundModelBase, Meta, ResourceClientBase from ..locations import BoundLocation from .domain import CreateVolumeResponse, Volume if TYPE_CHECKING: from .._client import Client from ..locations import Location from ..servers import BoundServer, Server __all__ = [ "BoundVolume", "VolumesPageResult", "VolumesClient", ] class BoundVolume(BoundModelBase[Volume], Volume): _client: VolumesClient model = Volume 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) # pylint: disable=import-outside-toplevel from ..servers import BoundServer server = data.get("server") if server is not None: data["server"] = BoundServer( client._parent.servers, {"id": server}, complete=False ) super().__init__(client, data, complete) def get_actions_list( self, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Volume. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._client.get_actions_list( self, status=status, sort=sort, page=page, per_page=per_page ) def get_actions( self, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Volume. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ return self._client.get_actions(self, status=status, sort=sort) def update( self, name: str | None = None, labels: dict[str, str] | None = None, ) -> BoundVolume: """Updates the volume properties. :param name: str (optional) New volume name :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundAction ` """ return self._client.update(self, name=name, labels=labels) 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: 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 ` :param automount: boolean :return: :class:`BoundAction ` """ return self._client.attach(self, server=server, automount=automount) 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: int) -> BoundAction: """Changes the size of a volume. Note that downsizing a volume is not possible. :param size: int New volume size in GB (must be greater than current size) :return: :class:`BoundAction ` """ return self._client.resize(self, size=size) def change_protection(self, delete: bool | None = None) -> BoundAction: """Changes the protection configuration of a volume. :param delete: boolean If True, prevents the volume from being deleted :return: :class:`BoundAction ` """ return self._client.change_protection(self, delete=delete) class VolumesPageResult(NamedTuple): volumes: list[BoundVolume] meta: Meta class VolumesClient( ResourceClientBaseActionsMixin, ResourceClientBase, ): _base_url = "/volumes" actions: ResourceActionsClient """Volumes scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self.actions = ResourceActionsClient(client, self._base_url) def get_by_id(self, id: int) -> BoundVolume: """Get a specific volume by its id :param id: int :return: :class:`BoundVolume ` """ response = self._client.request(url=f"{self._base_url}/{id}", method="GET") return BoundVolume(self, response["volume"]) def get_list( 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) Can be used to filter volumes by their name. :param label_selector: str (optional) Can be used to filter volumes by labels. The response will only contain volumes matching the label selector. :param status: List[str] (optional) Can be used to filter volumes by their status. The response will only contain volumes matching the status. :param page: int (optional) Specifies the page to fetch :param per_page: int (optional) Specifies how many results are returned by page :return: (List[:class:`BoundVolume `], :class:`Meta `) """ params: dict[str, Any] = {} if name is not None: params["name"] = name if label_selector is not None: params["label_selector"] = label_selector if status is not None: params["status"] = status if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request(url=self._base_url, method="GET", params=params) volumes = [ BoundVolume(self, volume_data) for volume_data in response["volumes"] ] return VolumesPageResult(volumes, Meta.parse_meta(response)) 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: Can be used to filter volumes by labels. The response will only contain volumes matching the label selector. :param status: List[str] (optional) Can be used to filter volumes by their status. The response will only contain volumes matching the status. :return: List[:class:`BoundVolume `] """ 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 :param name: str Used to get volume by name. :return: :class:`BoundVolume ` """ return self._get_first_by(self.get_list, name=name) def create( self, 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 Size of the volume in GB :param name: str Name of the volume :param labels: Dict[str,str] (optional) User-defined labels (key-value pairs) :param location: :class:`BoundLocation ` or :class:`Location ` :param server: :class:`BoundServer ` or :class:`Server ` :param automount: boolean (optional) Auto mount volumes after attach. :param format: str (optional) Format volume after creation. One of: xfs, ext4 :return: :class:`CreateVolumeResponse ` """ if size <= 0: raise ValueError("size must be greater than 0") 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} if labels is not None: data["labels"] = labels if location is not None: data["location"] = location.id_or_name if server is not None: data["server"] = server.id if automount is not None: data["automount"] = automount if format is not None: data["format"] = format response = self._client.request(url=self._base_url, json=data, method="POST") result = CreateVolumeResponse( volume=BoundVolume(self, response["volume"]), action=BoundAction(self._parent.actions, response["action"]), next_actions=[ BoundAction(self._parent.actions, action) for action in response["next_actions"] ], ) return result def get_actions_list( self, volume: Volume | BoundVolume, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Volume. :param volume: Volume to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._get_actions_list( f"{self._base_url}/{volume.id}", status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, volume: Volume | BoundVolume, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Volume. :param volume: Volume to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ return self._iter_pages( self.get_actions_list, volume, status=status, sort=sort, ) 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 ` :param name: str (optional) New volume name :param labels: Dict[str, str] (optional) User-defined labels (key-value pairs) :return: :class:`BoundAction ` """ data: dict[str, Any] = {} if name is not None: data.update({"name": name}) if labels is not None: data.update({"labels": labels}) response = self._client.request( url=f"{self._base_url}/{volume.id}", method="PUT", json=data, ) return BoundVolume(self, response["volume"]) 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 ` :return: boolean """ self._client.request(url=f"{self._base_url}/{volume.id}", method="DELETE") return True 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 ` :param size: int New volume size in GB (must be greater than current size) :return: :class:`BoundAction ` """ data = self._client.request( url=f"{self._base_url}/{volume.id}/actions/resize", json={"size": size}, method="POST", ) return BoundAction(self._parent.actions, data["action"]) 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 ` :param server: :class:`BoundServer ` or :class:`Server ` :param automount: boolean :return: :class:`BoundAction ` """ data: dict[str, Any] = {"server": server.id} if automount is not None: data["automount"] = automount data = self._client.request( url=f"{self._base_url}/{volume.id}/actions/attach", json=data, method="POST", ) return BoundAction(self._parent.actions, data["action"]) 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 ` :return: :class:`BoundAction ` """ data = self._client.request( url=f"{self._base_url}/{volume.id}/actions/detach", method="POST", ) return BoundAction(self._parent.actions, data["action"]) 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 ` :param delete: boolean If True, prevents the volume from being deleted :return: :class:`BoundAction ` """ data: dict[str, Any] = {} if delete is not None: data.update({"delete": delete}) response = self._client.request( url=f"{self._base_url}/{volume.id}/actions/change_protection", method="POST", json=data, ) return BoundAction(self._parent.actions, response["action"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/volumes/domain.py0000644000175100017510000000712515152343177017333 0ustar00runnerrunnerfrom __future__ import annotations from typing import TYPE_CHECKING, TypedDict 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 __all__ = [ "Volume", "VolumeProtection", "CreateVolumeResponse", ] class Volume(BaseDomain, DomainIdentityMixin): """Volume Domain :param id: int ID of the Volume :param name: str Name of the Volume :param server: :class:`BoundServer `, None Server the Volume is attached to, None if it is not attached at all. :param created: datetime Point in time when the Volume was created :param location: :class:`BoundLocation ` Location of the Volume. Volume can only be attached to Servers in the same location. :param size: int Size in GB of the Volume :param linux_device: str Device path on the file system for the Volume :param protection: dict Protection configuration for the Volume :param labels: dict User-defined labels (key-value pairs) :param status: str Current status of the volume Choices: `creating`, `available` :param format: str, None Filesystem of the volume if formatted on creation, None if not formatted on creation. """ STATUS_CREATING = "creating" """Volume Status creating""" STATUS_AVAILABLE = "available" """Volume Status available""" __api_properties__ = ( "id", "name", "server", "location", "size", "linux_device", "format", "protection", "labels", "status", "created", ) __slots__ = __api_properties__ def __init__( self, 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: VolumeProtection | None = None, labels: dict[str, str] | None = None, status: str | None = None, ): self.id = id self.name = name self.server = server self.created = self._parse_datetime(created) self.location = location self.size = size self.linux_device = linux_device self.format = format self.protection = protection self.labels = labels self.status = status class VolumeProtection(TypedDict): delete: bool class CreateVolumeResponse(BaseDomain): """Create Volume Response Domain :param volume: :class:`BoundVolume ` The created volume :param action: :class:`BoundAction ` The action that shows the progress of the Volume Creation :param next_actions: List[:class:`BoundAction `] List of actions that are performed after the creation, like attaching to a server """ __api_properties__ = ("volume", "action", "next_actions") __slots__ = __api_properties__ def __init__( self, volume: BoundVolume, action: BoundAction, next_actions: list[BoundAction], ): self.volume = volume self.action = action self.next_actions = next_actions ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1461086 hcloud-2.17.0/hcloud/zones/0000755000175100017510000000000015152343221015137 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/zones/__init__.py0000644000175100017510000000174515152343177017271 0ustar00runnerrunnerfrom __future__ import annotations from .client import ( BoundZone, BoundZoneRRSet, ZoneRRSetsPageResult, ZonesClient, ZonesPageResult, ) from .domain import ( CreateZoneResponse, CreateZoneRRSetResponse, DeleteZoneResponse, DeleteZoneRRSetResponse, ExportZonefileResponse, Zone, ZoneAuthoritativeNameservers, ZoneMode, ZonePrimaryNameserver, ZoneProtection, ZoneRecord, ZoneRegistrar, ZoneRRSet, ZoneRRSetProtection, ZoneStatus, ) __all__ = [ "BoundZone", "BoundZoneRRSet", "CreateZoneResponse", "Zone", "ZoneAuthoritativeNameservers", "ZonePrimaryNameserver", "ZoneRecord", "ZoneRRSet", "ZonesClient", "ZonesPageResult", "DeleteZoneRRSetResponse", "ZoneRRSetProtection", "DeleteZoneResponse", "ZoneRegistrar", "ZoneMode", "ZoneRRSetsPageResult", "ZoneProtection", "ExportZonefileResponse", "CreateZoneRRSetResponse", "ZoneStatus", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/zones/client.py0000644000175100017510000011772115152343177017012 0ustar00runnerrunnerfrom __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ( ActionSort, ActionsPageResult, ActionStatus, BoundAction, ResourceActionsClient, ) from ..actions.client import ResourceClientBaseActionsMixin from ..core import BoundModelBase, Meta, ResourceClientBase from .domain import ( CreateZoneResponse, CreateZoneRRSetResponse, DeleteZoneResponse, DeleteZoneRRSetResponse, ExportZonefileResponse, Zone, ZoneAuthoritativeNameservers, ZoneMode, ZonePrimaryNameserver, ZoneRecord, ZoneRRSet, ZoneRRSetType, ) if TYPE_CHECKING: from .._client import Client __all__ = [ "BoundZone", "BoundZoneRRSet", "ZonesPageResult", "ZoneRRSetsPageResult", "ZonesClient", ] class BoundZone(BoundModelBase[Zone], Zone): _client: ZonesClient model = Zone def __init__( self, client: ZonesClient, data: dict[str, Any], complete: bool = True, ): raw = data.get("primary_nameservers") if raw is not None: data["primary_nameservers"] = [ ZonePrimaryNameserver.from_dict(o) for o in raw ] raw = data.get("authoritative_nameservers") if raw: data["authoritative_nameservers"] = ZoneAuthoritativeNameservers.from_dict( raw ) super().__init__(client, data, complete) def update( self, *, labels: dict[str, str] | None = None, ) -> BoundZone: """ Updates the Zone. See https://docs.hetzner.cloud/reference/cloud#zones-update-a-zone :param labels: User-defined labels (key/value pairs) for the Resource. """ return self._client.update(self, labels=labels) def delete(self) -> DeleteZoneResponse: """ Deletes the Zone. See https://docs.hetzner.cloud/reference/cloud#zones-delete-a-zone """ return self._client.delete(self) def export_zonefile(self) -> ExportZonefileResponse: """ Returns a generated Zone file in BIND (RFC 1034/1035) format. See https://docs.hetzner.cloud/reference/cloud#zones-export-a-zone-file """ return self._client.export_zonefile(self) def get_actions_list( self, *, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Zone. See https://docs.hetzner.cloud/reference/cloud#zones-list-zones :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._client.get_actions_list( self, status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, *, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Zone. See https://docs.hetzner.cloud/reference/cloud#zones-list-zones :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ return self._client.get_actions( self, status=status, sort=sort, ) def import_zonefile( self, zonefile: str, ) -> BoundAction: """ Imports a zone file, replacing all resource record sets (ZoneRRSet). See https://docs.hetzner.cloud/reference/cloud#zone-actions-import-a-zone-file :param zonefile: Zone file to import. """ return self._client.import_zonefile(self, zonefile=zonefile) def change_protection( self, *, delete: bool | None = None, ) -> BoundAction: """ Changes the protection of the Zone. See https://docs.hetzner.cloud/reference/cloud#zone-actions-change-a-zones-protection :param delete: Prevents the Zone from being deleted. """ return self._client.change_protection(self, delete=delete) def change_ttl( self, ttl: int, ) -> BoundAction: """ Changes the TTL of the Zone. See https://docs.hetzner.cloud/reference/cloud#zone-actions-change-a-zones-default-ttl :param ttl: Default Time To Live (TTL) of the Zone. """ return self._client.change_ttl(self, ttl=ttl) def change_primary_nameservers( self, primary_nameservers: list[ZonePrimaryNameserver], ) -> BoundAction: """ Changes the primary nameservers of the Zone. See https://docs.hetzner.cloud/reference/cloud#zone-actions-change-a-zones-primary-nameservers :param primary_nameservers: Primary nameservers of the Zone. """ return self._client.change_primary_nameservers( self, primary_nameservers=primary_nameservers, ) def get_rrset( self, name: str, type: ZoneRRSetType, ) -> BoundZoneRRSet: """ Returns a single ZoneRRSet from the Zone. See https://docs.hetzner.cloud/reference/cloud#zone-rrsets-get-an-rrset :param name: Name of the RRSet. :param type: Type of the RRSet. """ return self._client.get_rrset(self, name=name, type=type) def get_rrset_list( self, *, name: str | None = None, type: list[ZoneRRSetType] | None = None, label_selector: str | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> ZoneRRSetsPageResult: """ Returns all ZoneRRSet in the Zone for a specific page. See https://docs.hetzner.cloud/reference/cloud#zone-rrsets-list-rrsets :param name: Filter resources by their name. The response will only contain the resources matching exactly the specified name. :param type: Filter resources by their type. The response will only contain the resources matching exactly the specified type. :param label_selector: Filter resources by labels. The response will only contain resources matching the label selector. :param sort: Sort resources by field and direction. :param page: Page number to return. :param per_page: Maximum number of entries returned per page. """ return self._client.get_rrset_list( self, name=name, type=type, label_selector=label_selector, sort=sort, page=page, per_page=per_page, ) def get_rrset_all( self, *, name: str | None = None, type: list[ZoneRRSetType] | None = None, label_selector: str | None = None, sort: list[str] | None = None, ) -> list[BoundZoneRRSet]: """ Returns all ZoneRRSet in the Zone. See https://docs.hetzner.cloud/reference/cloud#zone-rrsets-list-rrsets :param name: Filter resources by their name. The response will only contain the resources matching exactly the specified name. :param type: Filter resources by their type. The response will only contain the resources matching exactly the specified type. :param label_selector: Filter resources by labels. The response will only contain resources matching the label selector. :param sort: Sort resources by field and direction. """ return self._client.get_rrset_all( self, name=name, type=type, label_selector=label_selector, sort=sort, ) def create_rrset( self, *, name: str, type: ZoneRRSetType, ttl: int | None = None, labels: dict[str, str] | None = None, records: list[ZoneRecord] | None = None, ) -> CreateZoneRRSetResponse: """ Creates a ZoneRRSet in the Zone. See https://docs.hetzner.cloud/reference/cloud#zone-rrsets-create-an-rrset :param name: Name of the RRSet. :param type: Type of the RRSet. :param ttl: Time To Live (TTL) of the RRSet. :param labels: User-defined labels (key/value pairs) for the Resource. :param records: Records of the RRSet. """ return self._client.create_rrset( self, name=name, type=type, ttl=ttl, labels=labels, records=records, ) def update_rrset( self, rrset: ZoneRRSet | BoundZoneRRSet, *, labels: dict[str, str] | None = None, ) -> BoundZoneRRSet: """ Updates a ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrsets-update-an-rrset :param rrset: RRSet to update. :param labels: User-defined labels (key/value pairs) for the Resource. """ return self._client.update_rrset(rrset=rrset, labels=labels) def delete_rrset( self, rrset: ZoneRRSet | BoundZoneRRSet, ) -> DeleteZoneRRSetResponse: """ Deletes a ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrsets-delete-an-rrset :param rrset: RRSet to delete. """ return self._client.delete_rrset(rrset=rrset) def change_rrset_protection( self, rrset: ZoneRRSet | BoundZoneRRSet, *, change: bool | None = None, ) -> BoundAction: """ Changes the protection of a ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-change-an-rrsets-protection :param rrset: RRSet to update. :param change: Prevent the Zone from being changed (deletion and updates). """ return self._client.change_rrset_protection(rrset=rrset, change=change) def change_rrset_ttl( self, rrset: ZoneRRSet | BoundZoneRRSet, ttl: int | None, ) -> BoundAction: """ Changes the TTL of a ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-change-an-rrsets-ttl :param rrset: RRSet to update. :param change: Time To Live (TTL) of the RRSet. """ return self._client.change_rrset_ttl(rrset=rrset, ttl=ttl) def add_rrset_records( self, rrset: ZoneRRSet | BoundZoneRRSet, records: list[ZoneRecord], ttl: int | None = None, ) -> BoundAction: """ Adds records to a ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-add-records-to-an-rrset :param rrset: RRSet to update. :param records: Records to add to the RRSet. :param ttl: Time To Live (TTL) of the RRSet. """ return self._client.add_rrset_records(rrset=rrset, records=records, ttl=ttl) def update_rrset_records( self, rrset: ZoneRRSet | BoundZoneRRSet, records: list[ZoneRecord], ) -> BoundAction: """ Updates records in a ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-update-records-of-an-rrset :param rrset: RRSet to update. :param records: Records to update in the RRSet. """ return self._client.update_rrset_records(rrset=rrset, records=records) def remove_rrset_records( self, rrset: ZoneRRSet | BoundZoneRRSet, records: list[ZoneRecord], ) -> BoundAction: """ Removes records from a ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-remove-records-from-an-rrset :param rrset: RRSet to update. :param records: Records to remove from the RRSet. """ return self._client.remove_rrset_records(rrset=rrset, records=records) def set_rrset_records( self, rrset: ZoneRRSet | BoundZoneRRSet, records: list[ZoneRecord], ) -> BoundAction: """ Sets the records of a ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-set-records-of-an-rrset :param rrset: RRSet to update. :param records: Records to set in the RRSet. """ return self._client.set_rrset_records(rrset=rrset, records=records) class BoundZoneRRSet(BoundModelBase[ZoneRRSet], ZoneRRSet): _client: ZonesClient model = ZoneRRSet 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) raw = data.get("records") if raw is not None: data["records"] = [ZoneRecord.from_dict(o) for o in raw] 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, self.data_model.type, ) def update_rrset( self, *, labels: dict[str, str] | None = None, ) -> BoundZoneRRSet: """ Updates the ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrsets-update-an-rrset :param labels: User-defined labels (key/value pairs) for the Resource. """ return self._client.update_rrset(self, labels=labels) def delete_rrset( self, ) -> DeleteZoneRRSetResponse: """ Deletes the ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrsets-delete-an-rrset """ return self._client.delete_rrset(self) def change_rrset_protection( self, *, change: bool | None = None, ) -> BoundAction: """ Changes the protection of the ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-change-an-rrsets-protection :param change: Prevent the Zone from being changed (deletion and updates). """ return self._client.change_rrset_protection(self, change=change) def change_rrset_ttl( self, ttl: int | None, ) -> BoundAction: """ Changes the TTL of the ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-change-an-rrsets-ttl :param change: Time To Live (TTL) of the RRSet. """ return self._client.change_rrset_ttl(self, ttl=ttl) def add_rrset_records( self, records: list[ZoneRecord], ttl: int | None = None, ) -> BoundAction: """ Adds records to the ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-add-records-to-an-rrset :param records: Records to add to the RRSet. :param ttl: Time To Live (TTL) of the RRSet. """ return self._client.add_rrset_records(self, records=records, ttl=ttl) def update_rrset_records( self, records: list[ZoneRecord], ) -> BoundAction: """ Updates records in a ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-update-records-of-an-rrset :param records: Records to update in the RRSet. """ return self._client.update_rrset_records(self, records=records) def remove_rrset_records( self, records: list[ZoneRecord], ) -> BoundAction: """ Removes records from the ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-remove-records-from-an-rrset :param records: Records to remove from the RRSet. """ return self._client.remove_rrset_records(self, records=records) def set_rrset_records( self, records: list[ZoneRecord], ) -> BoundAction: """ Sets the records of the ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-set-records-of-an-rrset :param records: Records to set in the RRSet. """ return self._client.set_rrset_records(self, records=records) class ZonesPageResult(NamedTuple): zones: list[BoundZone] meta: Meta class ZoneRRSetsPageResult(NamedTuple): rrsets: list[BoundZoneRRSet] meta: Meta class ZonesClient( ResourceClientBaseActionsMixin, ResourceClientBase, ): """ ZoneClient is a client for the Zone (DNS) API. See https://docs.hetzner.cloud/reference/cloud#zones and https://docs.hetzner.cloud/reference/cloud#zone-rrsets. """ _base_url = "/zones" actions: ResourceActionsClient """Zones scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self.actions = ResourceActionsClient(client, self._base_url) def get(self, id_or_name: int | str) -> BoundZone: """ Returns a single Zone. See https://docs.hetzner.cloud/reference/cloud#zones-get-a-zone :param id_or_name: ID or Name of the Zone. """ response = self._client.request( method="GET", url=f"{self._base_url}/{id_or_name}", ) return BoundZone(self, response["zone"]) def get_list( self, *, name: str | None = None, mode: ZoneMode | None = None, label_selector: str | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> ZonesPageResult: """ Returns a list of Zone for a specific page. See https://docs.hetzner.cloud/reference/cloud#zones-list-zones :param name: Filter resources by their name. The response will only contain the resources matching exactly the specified name. :param mode: Filter resources by their mode. The response will only contain the resources matching exactly the specified mode. :param label_selector: Filter resources by labels. The response will only contain resources matching the label selector. :param sort: Sort resources by field and direction. :param page: Page number to return. :param per_page: Maximum number of entries returned per page. """ params: dict[str, Any] = {} if name is not None: params["name"] = name if mode is not None: params["mode"] = mode if label_selector is not None: params["label_selector"] = label_selector if sort is not None: params["sort"] = sort if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request( method="GET", url=f"{self._base_url}", params=params, ) return ZonesPageResult( zones=[BoundZone(self, item) for item in response["zones"]], meta=Meta.parse_meta(response), ) def get_all( self, *, name: str | None = None, mode: ZoneMode | None = None, label_selector: str | None = None, sort: list[str] | None = None, ) -> list[BoundZone]: """ Returns a list of all Zone. See https://docs.hetzner.cloud/reference/cloud#zones-list-zones :param name: Filter resources by their name. The response will only contain the resources matching exactly the specified name. :param mode: Filter resources by their mode. The response will only contain the resources matching exactly the specified mode. :param label_selector: Filter resources by labels. The response will only contain resources matching the label selector. :param sort: Sort resources by field and direction. """ return self._iter_pages( self.get_list, name=name, mode=mode, label_selector=label_selector, sort=sort, ) def create( self, *, name: str, mode: ZoneMode, ttl: int | None = None, labels: dict[str, str] | None = None, primary_nameservers: list[ZonePrimaryNameserver] | None = None, rrsets: list[ZoneRRSet] | None = None, zonefile: str | None = None, ) -> CreateZoneResponse: """ Creates a Zone. A default SOA and three NS resource records with the assigned Hetzner nameservers are created automatically. See https://docs.hetzner.cloud/reference/cloud#zones-create-a-zone :param name: Name of the Zone. :param mode: Mode of the Zone. :param ttl: Default Time To Live (TTL) of the Zone. :param labels: User-defined labels (key/value pairs) for the Resource. :param primary_nameservers: Primary nameservers of the Zone. :param rrsets: RRSets to be added to the Zone. :param zonefile: Zone file to import. """ data: dict[str, Any] = { "name": name, "mode": mode, } if ttl is not None: data["ttl"] = ttl if labels is not None: data["labels"] = labels if primary_nameservers is not None: data["primary_nameservers"] = [o.to_payload() for o in primary_nameservers] if rrsets is not None: data["rrsets"] = [o.to_payload() for o in rrsets] if zonefile is not None: data["zonefile"] = zonefile response = self._client.request( method="POST", url=f"{self._base_url}", json=data, ) return CreateZoneResponse( zone=BoundZone(self, response["zone"]), action=BoundAction(self._parent.actions, response["action"]), ) def update( self, zone: Zone | BoundZone, *, labels: dict[str, str] | None = None, ) -> BoundZone: """ Updates a Zone. See https://docs.hetzner.cloud/reference/cloud#zones-update-a-zone :param zone: Zone to update. :param labels: User-defined labels (key/value pairs) for the Resource. """ data: dict[str, Any] = {} if labels is not None: data["labels"] = labels response = self._client.request( method="PUT", url=f"{self._base_url}/{zone.id_or_name}", json=data, ) return BoundZone(self, response["zone"]) def delete( self, zone: Zone | BoundZone, ) -> DeleteZoneResponse: """ Deletes a Zone. See https://docs.hetzner.cloud/reference/cloud#zones-delete-a-zone :param zone: Zone to delete. """ response = self._client.request( method="DELETE", url=f"{self._base_url}/{zone.id_or_name}", ) return DeleteZoneResponse( action=BoundAction(self._parent.actions, response["action"]), ) def export_zonefile( self, zone: Zone | BoundZone, ) -> ExportZonefileResponse: """ Returns a generated Zone file in BIND (RFC 1034/1035) format. See https://docs.hetzner.cloud/reference/cloud#zones-export-a-zone-file :param zone: Zone to export the zone file from. """ response = self._client.request( method="GET", url=f"{self._base_url}/{zone.id_or_name}/zonefile", ) return ExportZonefileResponse(response["zonefile"]) def get_actions_list( self, zone: Zone | BoundZone, *, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ Returns a paginated list of Actions for a Zone. See https://docs.hetzner.cloud/reference/cloud#zones-list-zones :param zone: Zone to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. :param page: Page number to get. :param per_page: Maximum number of Actions returned per page. """ return self._get_actions_list( f"{self._base_url}/{zone.id_or_name}", status=status, sort=sort, page=page, per_page=per_page, ) def get_actions( self, zone: Zone | BoundZone, *, status: list[ActionStatus] | None = None, sort: list[ActionSort] | None = None, ) -> list[BoundAction]: """ Returns all Actions for a Zone. See https://docs.hetzner.cloud/reference/cloud#zones-list-zones :param zone: Zone to get the Actions for. :param status: Filter the Actions by status. :param sort: Sort Actions by field and direction. """ return self._iter_pages( self.get_actions_list, zone, status=status, sort=sort, ) def import_zonefile( self, zone: Zone | BoundZone, zonefile: str, ) -> BoundAction: """ Imports a zone file, replacing all resource record sets (ZoneRRSet). See https://docs.hetzner.cloud/reference/cloud#zone-actions-import-a-zone-file :param zone: Zone to import the zone file into. :param zonefile: Zone file to import. """ data: dict[str, Any] = { "zonefile": zonefile, } response = self._client.request( method="POST", url=f"{self._base_url}/{zone.id_or_name}/actions/import_zonefile", json=data, ) return BoundAction(self._parent.actions, response["action"]) def change_protection( self, zone: Zone | BoundZone, *, delete: bool | None = None, ) -> BoundAction: """ Changes the protection of a Zone. See https://docs.hetzner.cloud/reference/cloud#zone-actions-change-a-zones-protection :param zone: Zone to update. :param delete: Prevents the Zone from being deleted. """ data: dict[str, Any] = {} if delete is not None: data["delete"] = delete response = self._client.request( method="POST", url=f"{self._base_url}/{zone.id_or_name}/actions/change_protection", json=data, ) return BoundAction(self._parent.actions, response["action"]) def change_ttl( self, zone: Zone | BoundZone, ttl: int, ) -> BoundAction: """ Changes the TTL of a Zone. See https://docs.hetzner.cloud/reference/cloud#zone-actions-change-a-zones-default-ttl :param zone: Zone to update. :param ttl: Default Time To Live (TTL) of the Zone. """ data: dict[str, Any] = { "ttl": ttl, } response = self._client.request( method="POST", url=f"{self._base_url}/{zone.id_or_name}/actions/change_ttl", json=data, ) return BoundAction(self._parent.actions, response["action"]) def change_primary_nameservers( self, zone: Zone | BoundZone, primary_nameservers: list[ZonePrimaryNameserver], ) -> BoundAction: """ Changes the primary nameservers of a Zone. See https://docs.hetzner.cloud/reference/cloud#zone-actions-change-a-zones-primary-nameservers :param zone: Zone to update. :param primary_nameservers: Primary nameservers of the Zone. """ data: dict[str, Any] = { "primary_nameservers": [o.to_payload() for o in primary_nameservers], } response = self._client.request( method="POST", url=f"{self._base_url}/{zone.id_or_name}/actions/change_primary_nameservers", json=data, ) return BoundAction(self._parent.actions, response["action"]) def get_rrset( self, zone: Zone | BoundZone, name: str, type: ZoneRRSetType, ) -> BoundZoneRRSet: """ Returns a single ZoneRRSet from the Zone. See https://docs.hetzner.cloud/reference/cloud#zone-rrsets-get-an-rrset :param zone: Zone to fetch the RRSet from. :param name: Name of the RRSet. :param type: Type of the RRSet. """ response = self._client.request( method="GET", url=f"{self._base_url}/{zone.id_or_name}/rrsets/{name}/{type}", ) return BoundZoneRRSet(self, response["rrset"]) def get_rrset_list( self, zone: Zone | BoundZone, *, name: str | None = None, type: list[ZoneRRSetType] | None = None, label_selector: str | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> ZoneRRSetsPageResult: """ Returns all ZoneRRSet in the Zone for a specific page. See https://docs.hetzner.cloud/reference/cloud#zone-rrsets-list-rrsets :param zone: Zone to fetch the RRSets from. :param name: Filter resources by their name. The response will only contain the resources matching exactly the specified name. :param type: Filter resources by their type. The response will only contain the resources matching exactly the specified type. :param label_selector: Filter resources by labels. The response will only contain resources matching the label selector. :param sort: Sort resources by field and direction. :param page: Page number to return. :param per_page: Maximum number of entries returned per page. """ params: dict[str, Any] = {} if name is not None: params["name"] = name if type is not None: params["type"] = type if label_selector is not None: params["label_selector"] = label_selector if sort is not None: params["sort"] = sort if page is not None: params["page"] = page if per_page is not None: params["per_page"] = per_page response = self._client.request( method="GET", url=f"{self._base_url}/{zone.id_or_name}/rrsets", params=params, ) return ZoneRRSetsPageResult( rrsets=[BoundZoneRRSet(self, item) for item in response["rrsets"]], meta=Meta.parse_meta(response), ) def get_rrset_all( self, zone: Zone | BoundZone, *, name: str | None = None, type: list[ZoneRRSetType] | None = None, label_selector: str | None = None, sort: list[str] | None = None, ) -> list[BoundZoneRRSet]: """ Returns all ZoneRRSet in the Zone. See https://docs.hetzner.cloud/reference/cloud#zone-rrsets-list-rrsets :param zone: Zone to fetch the RRSets from. :param name: Filter resources by their name. The response will only contain the resources matching exactly the specified name. :param type: Filter resources by their type. The response will only contain the resources matching exactly the specified type. :param label_selector: Filter resources by labels. The response will only contain resources matching the label selector. :param sort: Sort resources by field and direction. """ return self._iter_pages( self.get_rrset_list, zone, name=name, type=type, label_selector=label_selector, sort=sort, ) def create_rrset( self, zone: Zone | BoundZone, *, name: str, type: ZoneRRSetType, ttl: int | None = None, labels: dict[str, str] | None = None, records: list[ZoneRecord] | None = None, ) -> CreateZoneRRSetResponse: """ Creates a ZoneRRSet in the Zone. See https://docs.hetzner.cloud/reference/cloud#zone-rrsets-create-an-rrset :param zone: Zone to create the RRSets in. :param name: Name of the RRSet. :param type: Type of the RRSet. :param ttl: Time To Live (TTL) of the RRSet. :param labels: User-defined labels (key/value pairs) for the Resource. :param records: Records of the RRSet. """ data: dict[str, Any] = { "name": name, "type": type, } if ttl is not None: data["ttl"] = ttl if labels is not None: data["labels"] = labels if records is not None: data["records"] = [o.to_payload() for o in records] response = self._client.request( method="POST", url=f"{self._base_url}/{zone.id_or_name}/rrsets", json=data, ) return CreateZoneRRSetResponse( rrset=BoundZoneRRSet(self, response["rrset"]), action=BoundAction(self._parent.actions, response["action"]), ) def update_rrset( self, rrset: ZoneRRSet | BoundZoneRRSet, *, labels: dict[str, str] | None = None, ) -> BoundZoneRRSet: """ Updates a ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrsets-update-an-rrset :param rrset: RRSet to update. :param labels: User-defined labels (key/value pairs) for the Resource. """ if rrset.zone is None: raise ValueError("rrset zone property is none") data: dict[str, Any] = {} if labels is not None: data["labels"] = labels response = self._client.request( method="PUT", url=f"{self._base_url}/{rrset.zone.id_or_name}/rrsets/{rrset.name}/{rrset.type}", json=data, ) return BoundZoneRRSet(self, response["rrset"]) def delete_rrset( self, rrset: ZoneRRSet | BoundZoneRRSet, ) -> DeleteZoneRRSetResponse: """ Deletes a ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrsets-delete-an-rrset :param rrset: RRSet to delete. """ if rrset.zone is None: raise ValueError("rrset zone property is none") response = self._client.request( method="DELETE", url=f"{self._base_url}/{rrset.zone.id_or_name}/rrsets/{rrset.name}/{rrset.type}", ) return DeleteZoneRRSetResponse( action=BoundAction(self._parent.actions, response["action"]), ) def change_rrset_protection( self, rrset: ZoneRRSet | BoundZoneRRSet, *, change: bool | None = None, ) -> BoundAction: """ Changes the protection of a ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-change-an-rrsets-protection :param rrset: RRSet to update. :param change: Prevent the Zone from being changed (deletion and updates). """ if rrset.zone is None: raise ValueError("rrset zone property is none") data: dict[str, Any] = {} if change is not None: data["change"] = change response = self._client.request( method="POST", url=f"{self._base_url}/{rrset.zone.id_or_name}/rrsets/{rrset.name}/{rrset.type}/actions/change_protection", json=data, ) return BoundAction(self._parent.actions, response["action"]) def change_rrset_ttl( self, rrset: ZoneRRSet | BoundZoneRRSet, ttl: int | None, ) -> BoundAction: """ Changes the TTL of a ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-change-an-rrsets-ttl :param rrset: RRSet to update. :param change: Time To Live (TTL) of the RRSet. """ if rrset.zone is None: raise ValueError("rrset zone property is none") data: dict[str, Any] = { "ttl": ttl, } response = self._client.request( method="POST", url=f"{self._base_url}/{rrset.zone.id_or_name}/rrsets/{rrset.name}/{rrset.type}/actions/change_ttl", json=data, ) return BoundAction(self._parent.actions, response["action"]) def add_rrset_records( self, rrset: ZoneRRSet | BoundZoneRRSet, records: list[ZoneRecord], ttl: int | None = None, ) -> BoundAction: """ Adds records to a ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-add-records-to-an-rrset :param rrset: RRSet to update. :param records: Records to add to the RRSet. :param ttl: Time To Live (TTL) of the RRSet. """ if rrset.zone is None: raise ValueError("rrset zone property is none") data: dict[str, Any] = { "records": [o.to_payload() for o in records], } if ttl is not None: data["ttl"] = ttl response = self._client.request( method="POST", url=f"{self._base_url}/{rrset.zone.id_or_name}/rrsets/{rrset.name}/{rrset.type}/actions/add_records", json=data, ) return BoundAction(self._parent.actions, response["action"]) def update_rrset_records( self, rrset: ZoneRRSet | BoundZoneRRSet, records: list[ZoneRecord], ) -> BoundAction: """ Updates records in a ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-update-records-of-an-rrset :param rrset: RRSet to update. :param records: Records to update in the RRSet. """ if rrset.zone is None: raise ValueError("rrset zone property is none") data: dict[str, Any] = { "records": [o.to_payload() for o in records], } response = self._client.request( method="POST", url=f"{self._base_url}/{rrset.zone.id_or_name}/rrsets/{rrset.name}/{rrset.type}/actions/update_records", json=data, ) return BoundAction(self._parent.actions, response["action"]) def remove_rrset_records( self, rrset: ZoneRRSet | BoundZoneRRSet, records: list[ZoneRecord], ) -> BoundAction: """ Removes records from a ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-remove-records-from-an-rrset :param rrset: RRSet to update. :param records: Records to remove from the RRSet. """ if rrset.zone is None: raise ValueError("rrset zone property is none") data: dict[str, Any] = { "records": [o.to_payload() for o in records], } response = self._client.request( method="POST", url=f"{self._base_url}/{rrset.zone.id_or_name}/rrsets/{rrset.name}/{rrset.type}/actions/remove_records", json=data, ) return BoundAction(self._parent.actions, response["action"]) def set_rrset_records( self, rrset: ZoneRRSet | BoundZoneRRSet, records: list[ZoneRecord], ) -> BoundAction: """ Sets the records of a ZoneRRSet. See https://docs.hetzner.cloud/reference/cloud#zone-rrset-actions-set-records-of-an-rrset :param rrset: RRSet to update. :param records: Records to set in the RRSet. """ if rrset.zone is None: raise ValueError("rrset zone property is none") data: dict[str, Any] = { "records": [o.to_payload() for o in records], } response = self._client.request( method="POST", url=f"{self._base_url}/{rrset.zone.id_or_name}/rrsets/{rrset.name}/{rrset.type}/actions/set_records", json=data, ) return BoundAction(self._parent.actions, response["action"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/hcloud/zones/domain.py0000644000175100017510000002447015152343177017001 0ustar00runnerrunnerfrom __future__ import annotations from typing import TYPE_CHECKING, Any, Literal, TypedDict from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction from .client import BoundZone, BoundZoneRRSet __all__ = [ "ZoneMode", "ZoneStatus", "ZoneRegistrar", "Zone", "ZonePrimaryNameserver", "ZoneAuthoritativeNameservers", "ZoneProtection", "CreateZoneResponse", "DeleteZoneResponse", "ExportZonefileResponse", "ZoneRRSet", "ZoneRRSetProtection", "ZoneRecord", "CreateZoneRRSetResponse", "DeleteZoneRRSetResponse", ] ZoneMode = Literal["primary", "secondary"] ZoneStatus = Literal["ok", "updating", "error"] ZoneRegistrar = Literal["hetzner", "other", "unknown"] class Zone(BaseDomain, DomainIdentityMixin): """ Zone Domain. See https://docs.hetzner.cloud/reference/cloud#zones. """ MODE_PRIMARY = "primary" """ Zone in primary mode, resource record sets (RRSets) and resource records (RRs) are managed via the Cloud API or Cloud Console. """ MODE_SECONDARY = "secondary" """ Zone in secondary mode, Hetzner's nameservers query RRSets and RRs from given primary nameservers via AXFR. """ STATUS_OK = "ok" """The Zone is pushed to the authoritative nameservers.""" STATUS_UPDATING = "updating" """The Zone is currently being published to the authoritative nameservers.""" STATUS_ERROR = "error" """The Zone could not be published to the authoritative nameservers.""" REGISTRAR_HETZNER = "hetzner" REGISTRAR_OTHER = "other" REGISTRAR_UNKNOWN = "unknown" __api_properties__ = ( "id", "name", "created", "mode", "ttl", "labels", "protection", "status", "record_count", "registrar", "primary_nameservers", "authoritative_nameservers", ) __slots__ = __api_properties__ def __init__( self, id: int | None = None, name: str | None = None, created: str | None = None, mode: ZoneMode | None = None, ttl: int | None = None, labels: dict[str, str] | None = None, protection: ZoneProtection | None = None, status: ZoneStatus | None = None, record_count: int | None = None, registrar: ZoneRegistrar | None = None, primary_nameservers: list[ZonePrimaryNameserver] | None = None, authoritative_nameservers: ZoneAuthoritativeNameservers | None = None, ): self.id = id self.name = name self.created = self._parse_datetime(created) self.mode = mode self.ttl = ttl self.labels = labels self.protection = protection self.status = status self.record_count = record_count self.registrar = registrar self.primary_nameservers = primary_nameservers self.authoritative_nameservers = authoritative_nameservers ZonePrimaryNameserverTSIGAlgorithm = Literal[ "hmac-md5", "hmac-sha1", "hmac-sha256", ] class ZonePrimaryNameserver(BaseDomain): """ Zone Primary Nameserver Domain. """ TSIG_ALGORITHM_HMAC_MD5 = "hmac-md5" """Transaction signature (TSIG) algorithm used to generate the TSIG key.""" TSIG_ALGORITHM_HMAC_SHA1 = "hmac-sha1" """Transaction signature (TSIG) algorithm used to generate the TSIG key.""" TSIG_ALGORITHM_HMAC_SHA256 = "hmac-sha256" """Transaction signature (TSIG) algorithm used to generate the TSIG key.""" __api_properties__ = ( "address", "port", "tsig_algorithm", "tsig_key", ) __slots__ = __api_properties__ def __init__( self, address: str, port: int | None = None, tsig_algorithm: ZonePrimaryNameserverTSIGAlgorithm | None = None, tsig_key: str | None = None, ): self.address = address self.port = port self.tsig_algorithm = tsig_algorithm self.tsig_key = tsig_key def to_payload(self) -> dict[str, Any]: """ Generates the request payload from this domain object. """ payload: dict[str, Any] = { "address": self.address, } if self.port is not None: payload["port"] = self.port if self.tsig_algorithm is not None: payload["tsig_algorithm"] = self.tsig_algorithm if self.tsig_key is not None: payload["tsig_key"] = self.tsig_key return payload ZoneAuthoritativeNameserversDelegationStatus = Literal[ "valid", "partially-valid", "invalid", "lame", "unregistered", "unknown", ] class ZoneAuthoritativeNameservers(BaseDomain): """ Zone Authoritative Nameservers Domain. """ DELEGATION_STATUS_VALID = "valid" DELEGATION_STATUS_PARTIALLY_VALID = "partially-valid" DELEGATION_STATUS_INVALID = "invalid" DELEGATION_STATUS_LAME = "lame" DELEGATION_STATUS_UNREGISTERED = "unregistered" DELEGATION_STATUS_UNKNOWN = "unknown" __api_properties__ = ( "assigned", "delegated", "delegation_last_check", "delegation_status", ) __slots__ = __api_properties__ def __init__( self, assigned: list[str] | None = None, delegated: list[str] | None = None, delegation_last_check: str | None = None, delegation_status: ZoneAuthoritativeNameserversDelegationStatus | None = None, ): self.assigned = assigned self.delegated = delegated self.delegation_last_check = self._parse_datetime(delegation_last_check) self.delegation_status = delegation_status class ZoneProtection(TypedDict): """ Zone Protection. """ delete: bool class CreateZoneResponse(BaseDomain): """ Create Zone Response Domain. """ __api_properties__ = ("zone", "action") __slots__ = __api_properties__ def __init__( self, zone: BoundZone, action: BoundAction, ): self.zone = zone self.action = action class DeleteZoneResponse(BaseDomain): """ Delete Zone Response Domain. """ __api_properties__ = ("action",) __slots__ = __api_properties__ def __init__( self, action: BoundAction, ): self.action = action class ExportZonefileResponse(BaseDomain): """ Export Zonefile Response Domain. """ __api_properties__ = ("zonefile",) __slots__ = __api_properties__ def __init__( self, zonefile: str, ): self.zonefile = zonefile ZoneRRSetType = Literal[ "A", "AAAA", "CAA", "CNAME", "DS", "HINFO", "HTTPS", "MX", "NS", "PTR", "RP", "SOA", "SRV", "SVCB", "TLSA", "TXT", ] class ZoneRRSet(BaseDomain): """ Zone RRSet Domain. See https://docs.hetzner.cloud/reference/cloud#zone-rrsets """ TYPE_A = "A" TYPE_AAAA = "AAAA" TYPE_CAA = "CAA" TYPE_CNAME = "CNAME" TYPE_DS = "DS" TYPE_HINFO = "HINFO" TYPE_HTTPS = "HTTPS" TYPE_MX = "MX" TYPE_NS = "NS" TYPE_PTR = "PTR" TYPE_RP = "RP" TYPE_SOA = "SOA" TYPE_SRV = "SRV" TYPE_SVCB = "SVCB" TYPE_TLSA = "TLSA" TYPE_TXT = "TXT" __api_properties__ = ( "name", "type", "ttl", "labels", "protection", "records", "id", "zone", ) __slots__ = __api_properties__ def __init__( self, name: str | None = None, type: ZoneRRSetType | None = None, ttl: int | None = None, labels: dict[str, str] | None = None, protection: ZoneRRSetProtection | None = None, records: list[ZoneRecord] | None = None, id: str | None = None, zone: BoundZone | Zone | None = None, ): # Ensure that 'id', 'name' and 'type' are always populated. if name is not None and type is not None: if id is None: id = f"{name}/{type}" else: if id is not None: name, _, type = id.partition("/") # type: ignore[assignment] else: raise ValueError("id or name and type must be set") self.name = name self.type = type self.ttl = ttl self.labels = labels self.protection = protection self.records = records self.id = id self.zone = zone def to_payload(self) -> dict[str, Any]: """ Generates the request payload from this domain object. """ payload: dict[str, Any] = { "name": self.name, "type": self.type, } if self.ttl is not None: payload["ttl"] = self.ttl if self.labels is not None: payload["labels"] = self.labels if self.protection is not None: payload["protection"] = self.protection if self.records is not None: payload["records"] = [o.to_payload() for o in self.records] return payload class ZoneRRSetProtection(TypedDict): """ Zone RRSet Protection. """ change: bool class ZoneRecord(BaseDomain): """ Zone Record Domain. """ __api_properties__ = ( "value", "comment", ) __slots__ = __api_properties__ def __init__( self, value: str, comment: str | None = None, ): self.value = value self.comment = comment def to_payload(self) -> dict[str, Any]: """ Generates the request payload from this domain object. """ payload: dict[str, Any] = { "value": self.value, } if self.comment is not None: payload["comment"] = self.comment return payload class CreateZoneRRSetResponse(BaseDomain): """ Create Zone RRSet Response Domain. """ __api_properties__ = ( "rrset", "action", ) __slots__ = __api_properties__ def __init__( self, rrset: BoundZoneRRSet, action: BoundAction, ): self.rrset = rrset self.action = action class DeleteZoneRRSetResponse(BaseDomain): """ Delete Zone RRSet Response Domain. """ __api_properties__ = ("action",) __slots__ = __api_properties__ def __init__( self, action: BoundAction, ): self.action = action ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1619976 hcloud-2.17.0/hcloud.egg-info/0000755000175100017510000000000015152343221015473 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734097.0 hcloud-2.17.0/hcloud.egg-info/PKG-INFO0000644000175100017510000001671715152343221016604 0ustar00runnerrunnerMetadata-Version: 2.4 Name: hcloud Version: 2.17.0 Summary: Official Hetzner Cloud python library Home-page: https://github.com/hetznercloud/hcloud-python Author: Hetzner Cloud GmbH Author-email: support-cloud@hetzner.com License: MIT Project-URL: Bug Tracker, https://github.com/hetznercloud/hcloud-python/issues Project-URL: Documentation, https://hcloud-python.readthedocs.io/en/stable/ Project-URL: Changelog, https://github.com/hetznercloud/hcloud-python/blob/main/CHANGELOG.md Project-URL: Source Code, https://github.com/hetznercloud/hcloud-python Keywords: hcloud hetzner cloud Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Requires-Python: >=3.10 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: python-dateutil>=2.7.5 Requires-Dist: requests>=2.20 Provides-Extra: docs Requires-Dist: sphinx<9.2,>=9; extra == "docs" Requires-Dist: sphinx-rtd-theme<3.2,>=3; extra == "docs" Requires-Dist: myst-parser<5.1,>=5; extra == "docs" Requires-Dist: watchdog<6.1,>=6; extra == "docs" Provides-Extra: test Requires-Dist: coverage<7.14,>=7.13; extra == "test" Requires-Dist: pylint<4.1,>=4; extra == "test" Requires-Dist: pytest<9.1,>=9; extra == "test" Requires-Dist: pytest-cov<7.1,>=7; extra == "test" Requires-Dist: mypy<1.20,>=1.19; extra == "test" Requires-Dist: types-python-dateutil; extra == "test" Requires-Dist: types-requests; extra == "test" Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: description Dynamic: description-content-type Dynamic: home-page Dynamic: keywords Dynamic: license Dynamic: license-file Dynamic: project-url Dynamic: provides-extra Dynamic: requires-dist Dynamic: requires-python Dynamic: summary # 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://codecov.io/github/hetznercloud/hcloud-python/graph/badge.svg?token=3YGRqB5t1L)](https://codecov.io/github/hetznercloud/hcloud-python/tree/main) [![](https://app.readthedocs.org/projects/hcloud-python/badge/?version=latest)](https://hcloud-python.readthedocs.io/en/stable/) [![](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/en/stable/), the public API documentation is available at [docs.hetzner.cloud](https://docs.hetzner.cloud). > [!IMPORTANT] > Make sure to follow our API changelog available at > [docs.hetzner.cloud/changelog](https://docs.hetzner.cloud/changelog) (or the RSS feed > available at > [docs.hetzner.cloud/changelog/feed.rss](https://docs.hetzner.cloud/changelog/feed.rss)) > to be notified about additions, deprecations and removals. ## 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 import Image from hcloud.server_types import ServerType client = Client( token="{YOUR_API_TOKEN}", # Please paste your API token here application_name="my-app", application_version="v1.0.0", ) # Create a server named my-server response = client.servers.create( name="my-server", server_type=ServerType(name="cx23"), 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=}") ``` - To upgrade the package, please read the [instructions available in the documentation](https://hcloud-python.readthedocs.io/en/stable/upgrading.html). - For more details on the API, 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). ## Experimental features Experimental features are published as part of our regular releases (e.g. a product public beta). During an experimental phase, breaking changes on those features may occur within minor releases. The stability of experimental features is not related to the stability of its upstream API. Experimental features have different levels of maturity (e.g. experimental, alpha, beta) based on the maturity of the upstream API. While experimental features will be announced in the release notes, you can also find whether a python class or function is experimental in its docstring: ``` Experimental: $PRODUCT is $MATURITY, breaking changes may occur within minor releases. See https://docs.hetzner.cloud/changelog#$SLUG for more details. ``` ## 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 ``` Lint the code: ```sh make lint ``` Run tests using the current `python3` version: ```sh make test ``` You may also run the tests for multiple `python3` versions using `tox`: ```sh 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, ) ``` ### Releasing experimental features To publish experimental features as part of regular releases: - an announcement, including a link to a changelog entry, must be added to the release notes. - an `Experimental` notice, including a link to a changelog entry, must be added to the python classes and functions that are experimental: ```py """ Experimental: $PRODUCT is $MATURITY, breaking changes may occur within minor releases. See https://docs.hetzner.cloud/changelog#$SLUG for more details. """ ``` ## License The MIT License (MIT). Please see [`License File`](https://github.com/hetznercloud/hcloud-python/blob/main/LICENSE) for more information. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734097.0 hcloud-2.17.0/hcloud.egg-info/SOURCES.txt0000644000175100017510000001464115152343221017365 0ustar00runnerrunnerCHANGELOG.md CONTRIBUTING.rst LICENSE MANIFEST.in README.md pyproject.toml setup.py docs/Makefile docs/api.clients.actions.rst docs/api.clients.certificates.rst docs/api.clients.datacenters.rst docs/api.clients.firewalls.rst docs/api.clients.floating_ips.rst docs/api.clients.images.rst docs/api.clients.isos.rst docs/api.clients.load_balancer_types.rst docs/api.clients.load_balancers.rst docs/api.clients.locations.rst docs/api.clients.networks.rst docs/api.clients.placement_groups.rst docs/api.clients.primary_ips.rst docs/api.clients.server_types.rst docs/api.clients.servers.rst docs/api.clients.ssh_keys.rst docs/api.clients.storage_box_types.rst docs/api.clients.storage_boxes.rst docs/api.clients.volumes.rst docs/api.clients.zones.rst docs/api.deprecation.rst docs/api.helpers.rst docs/api.rst docs/changelog.md docs/conf.py docs/contributing.rst docs/index.md docs/installation.rst docs/make.bat docs/upgrading.md docs/_static/favicon.png docs/_static/logo-hetzner.svg docs/_static/js/open_links_in_new_tab.js hcloud/__init__.py hcloud/_client.py hcloud/_exceptions.py hcloud/_version.py hcloud/py.typed hcloud.egg-info/PKG-INFO hcloud.egg-info/SOURCES.txt hcloud.egg-info/dependency_links.txt hcloud.egg-info/not-zip-safe hcloud.egg-info/requires.txt hcloud.egg-info/top_level.txt hcloud/actions/__init__.py hcloud/actions/client.py hcloud/actions/domain.py hcloud/certificates/__init__.py hcloud/certificates/client.py hcloud/certificates/domain.py hcloud/core/__init__.py hcloud/core/client.py hcloud/core/domain.py hcloud/datacenters/__init__.py hcloud/datacenters/client.py hcloud/datacenters/domain.py hcloud/deprecation/__init__.py hcloud/deprecation/domain.py hcloud/exp/__init__.py hcloud/exp/zone.py hcloud/firewalls/__init__.py hcloud/firewalls/client.py hcloud/firewalls/domain.py hcloud/floating_ips/__init__.py hcloud/floating_ips/client.py hcloud/floating_ips/domain.py hcloud/helpers/__init__.py hcloud/helpers/labels.py hcloud/images/__init__.py hcloud/images/client.py hcloud/images/domain.py hcloud/isos/__init__.py hcloud/isos/client.py hcloud/isos/domain.py hcloud/load_balancer_types/__init__.py hcloud/load_balancer_types/client.py hcloud/load_balancer_types/domain.py hcloud/load_balancers/__init__.py hcloud/load_balancers/client.py hcloud/load_balancers/domain.py hcloud/locations/__init__.py hcloud/locations/client.py hcloud/locations/domain.py hcloud/metrics/__init__.py hcloud/metrics/domain.py hcloud/networks/__init__.py hcloud/networks/client.py hcloud/networks/domain.py hcloud/placement_groups/__init__.py hcloud/placement_groups/client.py hcloud/placement_groups/domain.py hcloud/primary_ips/__init__.py hcloud/primary_ips/client.py hcloud/primary_ips/domain.py hcloud/rdns/__init__.py hcloud/rdns/domain.py hcloud/server_types/__init__.py hcloud/server_types/client.py hcloud/server_types/domain.py hcloud/servers/__init__.py hcloud/servers/client.py hcloud/servers/domain.py hcloud/ssh_keys/__init__.py hcloud/ssh_keys/client.py hcloud/ssh_keys/domain.py hcloud/storage_box_types/__init__.py hcloud/storage_box_types/client.py hcloud/storage_box_types/domain.py hcloud/storage_boxes/__init__.py hcloud/storage_boxes/client.py hcloud/storage_boxes/domain.py hcloud/volumes/__init__.py hcloud/volumes/client.py hcloud/volumes/domain.py hcloud/zones/__init__.py hcloud/zones/client.py hcloud/zones/domain.py tests/__init__.py tests/unit/__init__.py tests/unit/conftest.py tests/unit/test_client.py tests/unit/test_exceptions.py tests/unit/actions/__init__.py tests/unit/actions/test_client.py tests/unit/actions/test_domain.py tests/unit/certificates/__init__.py tests/unit/certificates/conftest.py tests/unit/certificates/test_client.py tests/unit/certificates/test_domain.py tests/unit/core/__init__.py tests/unit/core/test_client.py tests/unit/core/test_domain.py tests/unit/datacenters/__init__.py tests/unit/datacenters/conftest.py tests/unit/datacenters/test_client.py tests/unit/datacenters/test_domain.py tests/unit/deprecation/__init__.py tests/unit/deprecation/test_domain.py tests/unit/exp/__init__.py tests/unit/exp/test_zone.py tests/unit/firewalls/__init__.py tests/unit/firewalls/conftest.py tests/unit/firewalls/test_client.py tests/unit/firewalls/test_domain.py tests/unit/floating_ips/__init__.py tests/unit/floating_ips/conftest.py tests/unit/floating_ips/test_client.py tests/unit/floating_ips/test_domain.py tests/unit/helpers/__init__.py tests/unit/helpers/test_labels.py tests/unit/images/__init__.py tests/unit/images/conftest.py tests/unit/images/test_client.py tests/unit/images/test_domain.py tests/unit/isos/__init__.py tests/unit/isos/conftest.py tests/unit/isos/test_client.py tests/unit/isos/test_domain.py tests/unit/load_balancer_types/__init__.py tests/unit/load_balancer_types/conftest.py tests/unit/load_balancer_types/test_client.py tests/unit/load_balancer_types/test_domain.py tests/unit/load_balancers/__init__.py tests/unit/load_balancers/conftest.py tests/unit/load_balancers/test_client.py tests/unit/load_balancers/test_domain.py tests/unit/locations/__init__.py tests/unit/locations/conftest.py tests/unit/locations/test_client.py tests/unit/locations/test_domain.py tests/unit/networks/__init__.py tests/unit/networks/conftest.py tests/unit/networks/test_client.py tests/unit/networks/test_domain.py tests/unit/placement_groups/__init__.py tests/unit/placement_groups/conftest.py tests/unit/placement_groups/test_client.py tests/unit/placement_groups/test_domain.py tests/unit/primary_ips/__init__.py tests/unit/primary_ips/conftest.py tests/unit/primary_ips/test_client.py tests/unit/primary_ips/test_domain.py tests/unit/server_types/__init__.py tests/unit/server_types/conftest.py tests/unit/server_types/test_client.py tests/unit/server_types/test_domain.py tests/unit/servers/__init__.py tests/unit/servers/conftest.py tests/unit/servers/test_client.py tests/unit/servers/test_domain.py tests/unit/ssh_keys/__init__.py tests/unit/ssh_keys/conftest.py tests/unit/ssh_keys/test_client.py tests/unit/ssh_keys/test_domain.py tests/unit/storage_box_types/__init__.py tests/unit/storage_box_types/conftest.py tests/unit/storage_box_types/test_client.py tests/unit/storage_box_types/test_domain.py tests/unit/storage_boxes/__init__.py tests/unit/storage_boxes/conftest.py tests/unit/storage_boxes/test_client.py tests/unit/storage_boxes/test_domain.py tests/unit/volumes/__init__.py tests/unit/volumes/conftest.py tests/unit/volumes/test_client.py tests/unit/volumes/test_domain.py tests/unit/zones/__init__.py tests/unit/zones/conftest.py tests/unit/zones/test_client.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734097.0 hcloud-2.17.0/hcloud.egg-info/dependency_links.txt0000644000175100017510000000000115152343221021541 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734096.0 hcloud-2.17.0/hcloud.egg-info/not-zip-safe0000644000175100017510000000000115152343220017720 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734097.0 hcloud-2.17.0/hcloud.egg-info/requires.txt0000644000175100017510000000037715152343221020102 0ustar00runnerrunnerpython-dateutil>=2.7.5 requests>=2.20 [docs] sphinx<9.2,>=9 sphinx-rtd-theme<3.2,>=3 myst-parser<5.1,>=5 watchdog<6.1,>=6 [test] coverage<7.14,>=7.13 pylint<4.1,>=4 pytest<9.1,>=9 pytest-cov<7.1,>=7 mypy<1.20,>=1.19 types-python-dateutil types-requests ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734097.0 hcloud-2.17.0/hcloud.egg-info/top_level.txt0000644000175100017510000000000715152343221020222 0ustar00runnerrunnerhcloud ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/pyproject.toml0000644000175100017510000000146215152343177015454 0ustar00runnerrunner[tool.isort] profile = "black" combine_as_imports = true add_imports = ["from __future__ import annotations"] [tool.mypy] strict = true disallow_untyped_defs = true implicit_reexport = false [tool.coverage.run] source = ["hcloud"] [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" [tool.pylint.main] py-version = "3.10" recursive = true jobs = 0 [tool.pylint.reports] output-format = "colorized" [tool.pylint."messages control"] disable = [ "fixme", "line-too-long", "missing-class-docstring", "missing-module-docstring", "redefined-builtin", "duplicate-code", # Consider disabling line-by-line "too-few-public-methods", "too-many-public-methods", "too-many-arguments", "too-many-instance-attributes", "too-many-lines", "too-many-positional-arguments", ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1639833 hcloud-2.17.0/setup.cfg0000644000175100017510000000004615152343221014344 0ustar00runnerrunner[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/setup.py0000644000175100017510000000403415152343177014250 0ustar00runnerrunnerfrom __future__ import annotations from setuptools import find_packages, setup with open("README.md", encoding="utf-8") as readme_file: readme = readme_file.read() setup( name="hcloud", version="2.17.0", # x-releaser-pleaser-version 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", 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", classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ], python_requires=">=3.10", install_requires=[ "python-dateutil>=2.7.5", "requests>=2.20", ], extras_require={ "docs": [ "sphinx>=9,<9.2", "sphinx-rtd-theme>=3,<3.2", "myst-parser>=5,<5.1", "watchdog>=6,<6.1", ], "test": [ "coverage>=7.13,<7.14", "pylint>=4,<4.1", "pytest>=9,<9.1", "pytest-cov>=7,<7.1", "mypy>=1.19,<1.20", "types-python-dateutil", "types-requests", ], }, include_package_data=True, packages=find_packages(exclude=["examples", "tests*", "docs"]), zip_safe=False, ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1772734097.146331 hcloud-2.17.0/tests/0000755000175100017510000000000015152343221013665 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/__init__.py0000644000175100017510000000000015152343177015776 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1772734097.147045 hcloud-2.17.0/tests/unit/0000755000175100017510000000000015152343221014644 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/__init__.py0000644000175100017510000000000015152343177016755 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1476233 hcloud-2.17.0/tests/unit/actions/0000755000175100017510000000000015152343221016304 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/actions/__init__.py0000644000175100017510000000000015152343177020415 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/actions/test_client.py0000644000175100017510000003274615152343177021221 0ustar00runnerrunnerfrom __future__ import annotations import inspect from unittest import mock import pytest from hcloud import Client from hcloud.actions import ( ActionFailedException, ActionsClient, ActionTimeoutException, BoundAction, ResourceActionsClient, ) from hcloud.certificates import BoundCertificate, CertificatesClient from hcloud.core import BoundModelBase, ResourceClientBase from hcloud.firewalls import BoundFirewall, FirewallsClient from hcloud.floating_ips import BoundFloatingIP, FloatingIPsClient from hcloud.images import BoundImage, ImagesClient from hcloud.load_balancers import BoundLoadBalancer, LoadBalancersClient from hcloud.networks import BoundNetwork, NetworksClient from hcloud.primary_ips import BoundPrimaryIP, PrimaryIPsClient from hcloud.servers import BoundServer, ServersClient from hcloud.storage_boxes import BoundStorageBox, StorageBoxesClient from hcloud.volumes import BoundVolume, VolumesClient from hcloud.zones import BoundZone, ZonesClient from ..conftest import assert_bound_action1, assert_bound_action2 resources_with_actions: dict[str, tuple[ResourceClientBase, BoundModelBase]] = { "certificates": (CertificatesClient, BoundCertificate), "firewalls": (FirewallsClient, BoundFirewall), "floating_ips": (FloatingIPsClient, BoundFloatingIP), "images": (ImagesClient, BoundImage), "load_balancers": (LoadBalancersClient, BoundLoadBalancer), "networks": (NetworksClient, BoundNetwork), "primary_ips": (PrimaryIPsClient, BoundPrimaryIP), "servers": (ServersClient, BoundServer), "volumes": (VolumesClient, BoundVolume), "zones": (ZonesClient, BoundZone), "storage_boxes": (StorageBoxesClient, BoundStorageBox), } def test_resources_with_actions(client: Client): """ Ensure that the list of resource clients above is up to date. """ members = inspect.getmembers( client, predicate=lambda p: isinstance(p, ResourceClientBase) and hasattr(p, "actions"), ) for name, member in members: assert name in resources_with_actions resource_client_class, _ = resources_with_actions[name] assert member.__class__ is resource_client_class assert len(members) == len(resources_with_actions) class TestBoundAction: @pytest.fixture() def bound_running_action(self, client: Client, action1_running): return BoundAction(client=client.actions, data=action1_running) def test_wait_until_finished( self, request_mock: mock.MagicMock, bound_running_action, action1_running, action1_success, ): request_mock.side_effect = [ {"action": action1_running}, {"action": action1_success}, ] bound_running_action.wait_until_finished() request_mock.assert_called_with( method="GET", url="/actions/1", ) assert bound_running_action.status == "success" assert bound_running_action.id == 1 assert request_mock.call_count == 2 def test_wait_until_finished_with_error( self, request_mock: mock.MagicMock, bound_running_action, action1_running, action1_error, ): request_mock.side_effect = [ {"action": action1_running}, {"action": action1_error}, ] with pytest.raises(ActionFailedException) as exc: bound_running_action.wait_until_finished() assert bound_running_action.status == "error" assert bound_running_action.id == 1 assert exc.value.action.id == 1 assert request_mock.call_count == 2 def test_wait_until_finished_max_retries( self, request_mock: mock.MagicMock, bound_running_action, action1_running, action1_success, ): request_mock.side_effect = [ {"action": action1_running}, {"action": action1_running}, {"action": action1_success}, ] with pytest.raises(ActionTimeoutException) as exc: bound_running_action.wait_until_finished(max_retries=1) assert bound_running_action.status == "running" assert bound_running_action.id == 1 assert exc.value.action.id == 1 assert request_mock.call_count == 1 class TestResourceActionsClient: """ //actions //actions/ """ @pytest.fixture(params=resources_with_actions.keys()) def resource(self, request) -> str: return request.param @pytest.fixture() def resource_client(self, client: Client, resource: str) -> ResourceActionsClient: """ Extract the resource actions client from the client. """ return getattr(client, resource).actions def test_get_by_id( self, request_mock: mock.MagicMock, resource_client: ResourceActionsClient, resource: str, action_response, ): request_mock.return_value = action_response action = resource_client.get_by_id(1) request_mock.assert_called_with( method="GET", url=f"/{resource}/actions/1", ) assert_bound_action1(action, resource_client._parent.actions) @pytest.mark.parametrize( "params", [ {}, {"status": ["running"], "sort": ["status"], "page": 2, "per_page": 10}, ], ) def test_get_list( self, request_mock: mock.MagicMock, resource_client: ResourceActionsClient, resource: str, action_list_response, params, ): request_mock.return_value = action_list_response result = resource_client.get_list(**params) request_mock.assert_called_with( method="GET", url=f"/{resource}/actions", params=params, ) assert result.meta is not None actions = result.actions assert len(actions) == 2 assert_bound_action1(actions[0], resource_client._parent.actions) assert_bound_action2(actions[1], resource_client._parent.actions) @pytest.mark.parametrize( "params", [ {}, {"status": ["running"], "sort": ["status"]}, ], ) def test_get_all( self, request_mock: mock.MagicMock, resource_client: ResourceActionsClient, resource: str, action_list_response, params, ): request_mock.return_value = action_list_response actions = resource_client.get_all(**params) request_mock.assert_called_with( method="GET", url=f"/{resource}/actions", params={**params, "page": 1, "per_page": 50}, ) assert len(actions) == 2 assert_bound_action1(actions[0], resource_client._parent.actions) assert_bound_action2(actions[1], resource_client._parent.actions) class TestResourceObjectActionsClient: """ ///actions """ @pytest.fixture(params=resources_with_actions.keys()) def resource(self, request): return request.param @pytest.fixture() def resource_client(self, client: Client, resource: str) -> ResourceClientBase: return getattr(client, resource) @pytest.fixture() def bound_model(self, client: Client, resource: str) -> BoundModelBase: _, bound_model_class = resources_with_actions[resource] resource_client = getattr(client, resource) return bound_model_class(resource_client, data={"id": 1}) @pytest.mark.parametrize( "params", [ {}, {"status": ["running"], "sort": ["status"], "page": 2, "per_page": 10}, ], ) def test_get_actions_list( self, request_mock: mock.MagicMock, resource_client: ResourceClientBase, resource: str, bound_model: BoundModelBase, action_list_response, params, ): request_mock.return_value = action_list_response result = resource_client.get_actions_list(bound_model, **params) request_mock.assert_called_with( method="GET", url=f"/{resource}/1/actions", params=params, ) assert result.meta is not None actions = result.actions assert len(actions) == 2 assert_bound_action1(actions[0], resource_client._parent.actions) assert_bound_action2(actions[1], resource_client._parent.actions) @pytest.mark.parametrize( "params", [ {}, {"status": ["running"], "sort": ["status"]}, ], ) def test_get_actions( self, request_mock: mock.MagicMock, resource_client: ResourceClientBase, resource: str, bound_model: BoundModelBase, action_list_response, params, ): request_mock.return_value = action_list_response actions = resource_client.get_actions(bound_model, **params) request_mock.assert_called_with( method="GET", url=f"/{resource}/1/actions", params={**params, "page": 1, "per_page": 50}, ) assert len(actions) == 2 assert_bound_action1(actions[0], resource_client._parent.actions) assert_bound_action2(actions[1], resource_client._parent.actions) class TestBoundModelActions: """ ///actions """ @pytest.fixture(params=resources_with_actions.keys()) def resource(self, request): return request.param @pytest.fixture() def bound_model(self, client: Client, resource: str) -> ResourceClientBase: _, bound_model_class = resources_with_actions[resource] resource_client = getattr(client, resource) return bound_model_class(resource_client, data={"id": 1}) @pytest.mark.parametrize( "params", [ {}, {"status": ["running"], "sort": ["status"], "page": 2, "per_page": 10}, ], ) def test_get_actions_list( self, request_mock: mock.MagicMock, bound_model: BoundModelBase, resource: str, action_list_response, params, ): request_mock.return_value = action_list_response result = bound_model.get_actions_list(**params) request_mock.assert_called_with( method="GET", url=f"/{resource}/1/actions", params=params, ) assert result.meta is not None actions = result.actions assert len(actions) == 2 assert_bound_action1(actions[0], bound_model._client._parent.actions) assert_bound_action2(actions[1], bound_model._client._parent.actions) @pytest.mark.parametrize( "params", [ {}, {"status": ["running"], "sort": ["status"]}, ], ) def test_get_actions( self, request_mock: mock.MagicMock, bound_model: BoundModelBase, resource: str, action_list_response, params, ): request_mock.return_value = action_list_response actions = bound_model.get_actions(**params) request_mock.assert_called_with( method="GET", url=f"/{resource}/1/actions", params={**params, "page": 1, "per_page": 50}, ) assert len(actions) == 2 assert_bound_action1(actions[0], bound_model._client._parent.actions) assert_bound_action2(actions[1], bound_model._client._parent.actions) class TestActionsClient: @pytest.fixture() def actions_client(self, client: Client) -> ActionsClient: return client.actions def test_get_by_id( self, request_mock: mock.MagicMock, actions_client: ActionsClient, action_response, ): request_mock.return_value = action_response action = actions_client.get_by_id(1) request_mock.assert_called_with( method="GET", url="/actions/1", ) assert_bound_action1(action, actions_client) @pytest.mark.parametrize( "params", [ {}, {"status": ["running"], "sort": ["status"], "page": 2, "per_page": 10}, ], ) def test_get_list( self, request_mock: mock.MagicMock, actions_client: ActionsClient, action_list_response, params, ): request_mock.return_value = action_list_response with pytest.deprecated_call(): result = actions_client.get_list(**params) request_mock.assert_called_with( method="GET", url="/actions", params=params, ) assert result.meta is not None actions = result.actions assert len(actions) == 2 assert_bound_action1(actions[0], actions_client) assert_bound_action2(actions[1], actions_client) @pytest.mark.parametrize( "params", [ {}, {"status": ["running"], "sort": ["status"]}, ], ) def test_get_all( self, request_mock: mock.MagicMock, actions_client: ActionsClient, action_list_response, params, ): request_mock.return_value = action_list_response with pytest.deprecated_call(): actions = actions_client.get_all(**params) request_mock.assert_called_with( method="GET", url="/actions", params={**params, "page": 1, "per_page": 50}, ) assert len(actions) == 2 assert_bound_action1(actions[0], actions_client) assert_bound_action2(actions[1], actions_client) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/actions/test_domain.py0000644000175100017510000000420415152343177021176 0ustar00runnerrunnerfrom __future__ import annotations import datetime from datetime import timezone import pytest from hcloud.actions import ( Action, ActionException, ActionFailedException, ActionTimeoutException, ) @pytest.mark.parametrize( "value", [ (Action(id=1),), ], ) def test_eq(value): assert value.__eq__(value) 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" ) assert action.started == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) 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, } ) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1483817 hcloud-2.17.0/tests/unit/certificates/0000755000175100017510000000000015152343221017311 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/certificates/__init__.py0000644000175100017510000000000015152343177021422 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/certificates/conftest.py0000644000175100017510000001432415152343177021526 0ustar00runnerrunnerfrom __future__ import annotations import pytest @pytest.fixture() def certificate_response(): return { "certificate": { "id": 2323, "name": "My Certificate", "type": "managed", "labels": {}, "certificate": "-----BEGIN CERTIFICATE-----\n...", "created": "2019-01-08T12:10:00+00:00", "not_valid_before": "2019-01-08T10:00:00+00:00", "not_valid_after": "2019-07-08T09:59:59+00:00", "domain_names": ["example.com", "webmail.example.com", "www.example.com"], "fingerprint": "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f", "status": { "issuance": "failed", "renewal": "scheduled", "error": {"code": "error_code", "message": "error message"}, }, "used_by": [{"id": 42, "type": "server"}], } } @pytest.fixture() def create_managed_certificate_response(): return { "certificate": { "id": 2323, "name": "My Certificate", "type": "managed", "labels": {}, "certificate": "-----BEGIN CERTIFICATE-----\n...", "created": "2019-01-08T12:10:00+00:00", "not_valid_before": "2019-01-08T10:00:00+00:00", "not_valid_after": "2019-07-08T09:59:59+00:00", "domain_names": ["example.com", "webmail.example.com", "www.example.com"], "fingerprint": "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f", "status": {"issuance": "pending", "renewal": "scheduled", "error": None}, "used_by": [{"id": 42, "type": "load_balancer"}], }, "action": { "id": 14, "command": "issue_certificate", "status": "success", "progress": 100, "started": "2021-01-30T23:55:00+00:00", "finished": "2021-01-30T23:57:00+00:00", "resources": [{"id": 896, "type": "certificate"}], "error": {"code": "action_failed", "message": "Action failed"}, }, } @pytest.fixture() def two_certificates_response(): return { "certificates": [ { "id": 2323, "name": "My Certificate", "labels": {}, "type": "uploaded", "certificate": "-----BEGIN CERTIFICATE-----\n...", "created": "2019-01-08T12:10:00+00:00", "not_valid_before": "2019-01-08T10:00:00+00:00", "not_valid_after": "2019-07-08T09:59:59+00:00", "domain_names": [ "example.com", "webmail.example.com", "www.example.com", ], "fingerprint": "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f", "status": None, "used_by": [{"id": 42, "type": "load_balancer"}], }, { "id": 2324, "name": "My website cert", "labels": {}, "type": "uploaded", "certificate": "-----BEGIN CERTIFICATE-----\n...", "created": "2019-01-08T12:10:00+00:00", "not_valid_before": "2019-01-08T10:00:00+00:00", "not_valid_after": "2019-07-08T09:59:59+00:00", "domain_names": [ "example.com", "webmail.example.com", "www.example.com", ], "fingerprint": "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f", "status": None, "used_by": [{"id": 42, "type": "load_balancer"}], }, ] } @pytest.fixture() def one_certificates_response(): return { "certificates": [ { "id": 2323, "name": "My Certificate", "labels": {}, "type": "uploaded", "certificate": "-----BEGIN CERTIFICATE-----\n...", "created": "2019-01-08T12:10:00+00:00", "not_valid_before": "2019-01-08T10:00:00+00:00", "not_valid_after": "2019-07-08T09:59:59+00:00", "domain_names": [ "example.com", "webmail.example.com", "www.example.com", ], "fingerprint": "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f", "status": None, "used_by": [{"id": 42, "type": "load_balancer"}], } ] } @pytest.fixture() def response_update_certificate(): return { "certificate": { "id": 2323, "name": "New name", "labels": {}, "type": "uploaded", "certificate": "-----BEGIN CERTIFICATE-----\n...", "created": "2019-01-08T12:10:00+00:00", "not_valid_before": "2019-01-08T10:00:00+00:00", "not_valid_after": "2019-07-08T09:59:59+00:00", "domain_names": ["example.com", "webmail.example.com", "www.example.com"], "fingerprint": "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f", "status": None, "used_by": [{"id": 42, "type": "load_balancer"}], } } @pytest.fixture() def response_get_actions(): return { "actions": [ { "id": 13, "command": "change_protection", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 14, "type": "certificate"}], "error": {"code": "action_failed", "message": "Action failed"}, } ] } @pytest.fixture() def response_retry_issuance_action(): return { "action": { "id": 14, "command": "issue_certificate", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "certificate"}], "error": {"code": "action_failed", "message": "Action failed"}, } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/certificates/test_client.py0000644000175100017510000002145215152343177022216 0ustar00runnerrunnerfrom __future__ import annotations from unittest import mock import pytest from hcloud import Client from hcloud.certificates import ( BoundCertificate, Certificate, CertificatesClient, ManagedCertificateStatus, ) from ..conftest import BoundModelTestCase class TestBoundCertificate(BoundModelTestCase): methods = [ BoundCertificate.update, BoundCertificate.delete, BoundCertificate.retry_issuance, ] @pytest.fixture() def resource_client(self, client: Client): return client.certificates @pytest.fixture() def bound_model(self, resource_client, certificate_response): return BoundCertificate( resource_client, data=certificate_response["certificate"] ) def test_init(self, bound_model: BoundCertificate): o = bound_model assert o.id == 2323 assert o.name == "My Certificate" assert o.type == "managed" assert o.fingerprint == "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f" assert o.certificate == "-----BEGIN CERTIFICATE-----\n..." assert len(o.domain_names) == 3 assert o.domain_names[0] == "example.com" assert o.domain_names[1] == "webmail.example.com" assert o.domain_names[2] == "www.example.com" assert isinstance(o.status, ManagedCertificateStatus) assert o.status.issuance == "failed" assert o.status.renewal == "scheduled" assert o.status.error.code == "error_code" assert o.status.error.message == "error message" class TestCertificatesClient: @pytest.fixture() def certificates_client(self, client: Client): return CertificatesClient(client) def test_get_by_id( self, request_mock: mock.MagicMock, certificates_client: CertificatesClient, certificate_response, ): request_mock.return_value = certificate_response certificate = certificates_client.get_by_id(1) request_mock.assert_called_with( method="GET", url="/certificates/1", ) assert certificate._client is certificates_client assert certificate.id == 2323 assert certificate.name == "My Certificate" @pytest.mark.parametrize( "params", [ { "name": "My Certificate", "label_selector": "k==v", "page": 1, "per_page": 10, }, {"name": ""}, {}, ], ) def test_get_list( self, request_mock: mock.MagicMock, certificates_client: CertificatesClient, two_certificates_response, params, ): request_mock.return_value = two_certificates_response result = certificates_client.get_list(**params) request_mock.assert_called_with( method="GET", url="/certificates", params=params, ) certificates = result.certificates assert len(certificates) == 2 certificates1 = certificates[0] certificates2 = certificates[1] assert certificates1._client is certificates_client assert certificates1.id == 2323 assert certificates1.name == "My Certificate" assert certificates2._client is certificates_client assert certificates2.id == 2324 assert certificates2.name == "My website cert" @pytest.mark.parametrize( "params", [{"name": "My Certificate", "label_selector": "label1"}, {}] ) def test_get_all( self, request_mock: mock.MagicMock, certificates_client: CertificatesClient, two_certificates_response, params, ): request_mock.return_value = two_certificates_response certificates = certificates_client.get_all(**params) params.update({"page": 1, "per_page": 50}) request_mock.assert_called_with( method="GET", url="/certificates", params=params, ) assert len(certificates) == 2 certificates1 = certificates[0] certificates2 = certificates[1] assert certificates1._client is certificates_client assert certificates1.id == 2323 assert certificates1.name == "My Certificate" assert certificates2._client is certificates_client assert certificates2.id == 2324 assert certificates2.name == "My website cert" def test_get_by_name( self, request_mock: mock.MagicMock, certificates_client: CertificatesClient, one_certificates_response, ): request_mock.return_value = one_certificates_response certificates = certificates_client.get_by_name("My Certificate") params = {"name": "My Certificate"} request_mock.assert_called_with( method="GET", url="/certificates", params=params, ) assert certificates._client is certificates_client assert certificates.id == 2323 assert certificates.name == "My Certificate" def test_create( self, request_mock: mock.MagicMock, certificates_client: CertificatesClient, certificate_response, ): request_mock.return_value = certificate_response certificate = certificates_client.create( name="My Certificate", certificate="-----BEGIN CERTIFICATE-----\n...", private_key="-----BEGIN PRIVATE KEY-----\n...", ) request_mock.assert_called_with( method="POST", url="/certificates", json={ "name": "My Certificate", "certificate": "-----BEGIN CERTIFICATE-----\n...", "private_key": "-----BEGIN PRIVATE KEY-----\n...", "type": "uploaded", }, ) assert certificate.id == 2323 assert certificate.name == "My Certificate" def test_create_managed( self, request_mock: mock.MagicMock, certificates_client: CertificatesClient, create_managed_certificate_response, ): request_mock.return_value = create_managed_certificate_response create_managed_certificate_rsp = certificates_client.create_managed( name="My Certificate", domain_names=["example.com", "*.example.org"] ) request_mock.assert_called_with( method="POST", url="/certificates", json={ "name": "My Certificate", "domain_names": ["example.com", "*.example.org"], "type": "managed", }, ) assert create_managed_certificate_rsp.certificate.id == 2323 assert create_managed_certificate_rsp.certificate.name == "My Certificate" assert create_managed_certificate_rsp.action.id == 14 assert create_managed_certificate_rsp.action.command == "issue_certificate" @pytest.mark.parametrize( "certificate", [Certificate(id=1), BoundCertificate(mock.MagicMock(), dict(id=1))], ) def test_update( self, request_mock: mock.MagicMock, certificates_client: CertificatesClient, certificate, response_update_certificate, ): request_mock.return_value = response_update_certificate certificate = certificates_client.update(certificate, name="New name") request_mock.assert_called_with( method="PUT", url="/certificates/1", json={"name": "New name"}, ) assert certificate.id == 2323 assert certificate.name == "New name" @pytest.mark.parametrize( "certificate", [Certificate(id=1), BoundCertificate(mock.MagicMock(), dict(id=1))], ) def test_delete( self, request_mock: mock.MagicMock, certificates_client: CertificatesClient, certificate, action_response, ): request_mock.return_value = action_response delete_success = certificates_client.delete(certificate) request_mock.assert_called_with( method="DELETE", url="/certificates/1", ) assert delete_success is True @pytest.mark.parametrize( "certificate", [Certificate(id=1), BoundCertificate(mock.MagicMock(), dict(id=1))], ) def test_retry_issuance( self, request_mock: mock.MagicMock, certificates_client: CertificatesClient, certificate, response_retry_issuance_action, ): request_mock.return_value = response_retry_issuance_action action = certificates_client.retry_issuance(certificate) request_mock.assert_called_with( method="POST", url="/certificates/1/actions/retry", ) assert action.id == 14 assert action.command == "issue_certificate" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/certificates/test_domain.py0000644000175100017510000000207215152343177022204 0ustar00runnerrunnerfrom __future__ import annotations import datetime from datetime import timezone import pytest from hcloud.certificates import ( Certificate, ManagedCertificateError, ManagedCertificateStatus, ) @pytest.mark.parametrize( "value", [ (Certificate(id=1),), (ManagedCertificateError()), (ManagedCertificateStatus()), ], ) def test_eq(value): assert value.__eq__(value) class TestCertificate: def test_created_is_datetime(self): certificate = Certificate( id=1, created="2016-01-30T23:50+00:00", not_valid_after="2016-01-30T23:50+00:00", not_valid_before="2016-01-30T23:50+00:00", ) assert certificate.created == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) assert certificate.not_valid_after == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) assert certificate.not_valid_before == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/conftest.py0000644000175100017510000001503315152343177017057 0ustar00runnerrunner# pylint: disable=redefined-outer-name from __future__ import annotations import inspect import warnings from collections.abc import Callable from typing import ClassVar, TypedDict from unittest import mock import pytest from hcloud import Client from hcloud.actions import ActionsClient, BoundAction @pytest.fixture(autouse=True, scope="session") def patch_package_version(): with mock.patch("hcloud._client.__version__", "0.0.0"): yield @pytest.fixture() def request_mock() -> mock.MagicMock: return mock.MagicMock() @pytest.fixture() def client(request_mock) -> Client: c = Client( token="TOKEN", # Speed up tests that use `_poll_interval_func` poll_interval=0.0, poll_max_retries=3, ) c._client.request = request_mock c._client_hetzner.request = request_mock return c def assert_bound_action1(o: BoundAction, client: ActionsClient): assert o.id == 1 assert o.command == "command" assert o._client == client def assert_bound_action2(o: BoundAction, client: ActionsClient): assert o.id == 2 assert o.command == "command" assert o._client == client @pytest.fixture() def action1_running(): return { "id": 1, "command": "command", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "resource"}], "error": None, } @pytest.fixture() def action2_running(): return { "id": 2, "command": "command", "status": "running", "progress": 20, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 43, "type": "resource"}], "error": None, } @pytest.fixture() def action1_success(action1_running): return { **action1_running, "status": "success", "progress": 100, "finished": "2016-01-31T00:10+00:00", } @pytest.fixture() def action2_success(action2_running): return { **action2_running, "status": "success", "progress": 100, "finished": "2016-01-31T00:10+00:00", } @pytest.fixture() def action1_error(action1_running): return { **action1_running, "status": "error", "progress": 100, "finished": "2016-01-31T00:10+00:00", "error": {"code": "action_failed", "message": "Action failed"}, } @pytest.fixture() def action2_error(action2_running): return { **action2_running, "status": "error", "progress": 100, "finished": "2016-01-31T00:10+00:00", "error": {"code": "action_failed", "message": "Action failed"}, } @pytest.fixture() def action_response(action1_running): return { "action": action1_running, } @pytest.fixture() def action_list_response(action1_running, action2_running): return { "actions": [ action1_running, action2_running, ], } def build_kwargs_mock(func: Callable) -> dict[str, mock.Mock]: """ Generate a kwargs dict that may be passed to the provided function for testing purposes. """ s = inspect.signature(func) kwargs = {} for name, param in s.parameters.items(): if name in ("self",): continue if param.kind in (param.POSITIONAL_OR_KEYWORD, param.KEYWORD_ONLY): kwargs[name] = mock.Mock() continue # Ignore **kwargs if param.kind in (param.VAR_KEYWORD,): continue raise NotImplementedError(f"unsupported parameter kind: {param.kind}") return kwargs def pytest_generate_tests(metafunc: pytest.Metafunc): """ Magic function to generate a test for each bound model method. """ if "bound_model_method" in metafunc.fixturenames: metafunc.parametrize("bound_model_method", metafunc.cls.methods) class BoundModelTestOptions(TypedDict): sub_resource: bool client_method: str class BoundModelTestCase: methods: ClassVar[list[Callable | tuple[Callable, BoundModelTestOptions]]] def test_method_list(self, bound_model): """ Ensure the list of bound model methods is up to date. """ # Unpack methods methods = [m[0] if isinstance(m, tuple) else m for m in self.__class__.methods] members_count = 0 members_missing = [] with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) members = inspect.getmembers( bound_model, lambda m: inspect.ismethod(m) and m.__func__ in bound_model.__class__.__dict__.values(), ) for name, member in members: # Ignore private methods if name.startswith("_"): continue # Actions methods are already tested in TestBoundModelActions. if name in ("__init__", "get_actions", "get_actions_list"): continue if member.__func__ in methods: members_count += 1 else: members_missing.append(member.__func__.__qualname__) assert not members_missing, "untested methods:\n" + ",\n".join(members_missing) assert members_count == len(self.__class__.methods) def test_method( self, resource_client, bound_model, bound_model_method: Callable | tuple[Callable, BoundModelTestOptions], ): options = BoundModelTestOptions() if isinstance(bound_model_method, tuple): bound_model_method, options = bound_model_method resource_client_method_name = options.get( "client_method", bound_model_method.__name__, ) # Check if the resource client has a method named after the bound model method. assert hasattr(resource_client, resource_client_method_name) # Mock the resource client method. resource_client_method_mock = mock.MagicMock() setattr( resource_client, resource_client_method_name, resource_client_method_mock, ) kwargs = build_kwargs_mock(bound_model_method) # Call the bound model method result = getattr(bound_model, bound_model_method.__name__)(**kwargs) if options.get("sub_resource"): resource_client_method_mock.assert_called_with(**kwargs) else: resource_client_method_mock.assert_called_with(bound_model, **kwargs) assert result is resource_client_method_mock.return_value ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1489236 hcloud-2.17.0/tests/unit/core/0000755000175100017510000000000015152343221015574 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/core/__init__.py0000644000175100017510000000000015152343177017705 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/core/test_client.py0000644000175100017510000002023715152343177020501 0ustar00runnerrunnerfrom __future__ import annotations from typing import Any, NamedTuple from unittest import mock import pytest from hcloud.actions import ActionsPageResult from hcloud.core import BaseDomain, BoundModelBase, Meta, ResourceClientBase class TestBoundModelBase: @pytest.fixture() def bound_model_class(self): class Model(BaseDomain): __api_properties__ = ("id", "name", "description") __slots__ = __api_properties__ def __init__(self, id, name="", description=""): self.id = id self.name = name self.description = description class BoundModel(BoundModelBase, Model): model = Model return BoundModel @pytest.fixture() def client(self): client = mock.MagicMock() return client def test_get_exists_model_attribute_complete_model(self, bound_model_class, client): bound_model = bound_model_class( client=client, data={"id": 1, "name": "name", "description": "my_description"}, ) description = bound_model.description client.get_by_id.assert_not_called() assert description == "my_description" def test_get_non_exists_model_attribute_complete_model( self, bound_model_class, client ): bound_model = bound_model_class( client=client, data={"id": 1, "name": "name", "description": "description"} ) with pytest.raises(AttributeError): _ = bound_model.content client.get_by_id.assert_not_called() def test_get_exists_model_attribute_incomplete_model( self, bound_model_class, client ): bound_model = bound_model_class(client=client, data={"id": 101}, complete=False) client.get_by_id.return_value = bound_model_class( client=client, data={"id": 101, "name": "name", "description": "super_description"}, ) description = bound_model.description client.get_by_id.assert_called_once_with(101) assert description == "super_description" assert bound_model.complete is True def test_get_filled_model_attribute_incomplete_model( self, bound_model_class, client ): bound_model = bound_model_class(client=client, data={"id": 101}, complete=False) id = bound_model.id client.get_by_id.assert_not_called() assert id == 101 assert bound_model.complete is False def test_get_non_exists_model_attribute_incomplete_model( self, bound_model_class, client ): bound_model = bound_model_class(client=client, data={"id": 1}, complete=False) with pytest.raises(AttributeError): _ = bound_model.content client.get_by_id.assert_not_called() assert bound_model.complete is False def test_equality(self, bound_model_class, client): data = {"id": 1, "name": "name", "description": "my_description"} bound_model_a = bound_model_class(client=client, data=data) bound_model_b = bound_model_class(client=client, data=data) # Comparing a bound model with a base domain assert bound_model_a == bound_model_a.data_model # Identical bound models assert bound_model_a == bound_model_b assert bound_model_a == bound_model_b.data_model # Differing bound models bound_model_b.data_model.name = "changed_name" assert bound_model_a != bound_model_b assert bound_model_a != bound_model_b.data_model class TestResourceClientBase: @pytest.fixture() def client_class_constructor(self): def constructor(json_content_function): class CandiesPageResult(NamedTuple): candies: list[Any] meta: Meta class CandiesClient(ResourceClientBase): 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"] ] return CandiesPageResult(results, Meta.parse_meta(json_content)) return CandiesClient(mock.MagicMock()) return constructor @pytest.fixture() def client_class_with_actions_constructor(self): def constructor(json_content_function): class CandiesClient(ResourceClientBase): def get_actions_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["actions"] ] return ActionsPageResult(results, Meta.parse_meta(json_content)) return CandiesClient(mock.MagicMock()) return constructor def test_iter_pages_no_meta(self, client_class_constructor): json_content = {"candies": [1, 2]} def json_content_function(_): return json_content candies_client = client_class_constructor(json_content_function) result = candies_client._iter_pages(candies_client.get_list, status="sweet") assert result == [(1, 1, "sweet", 50), (2, 1, "sweet", 50)] 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}}, } def json_content_function(_): return json_content candies_client = client_class_constructor(json_content_function) result = candies_client._iter_pages(candies_client.get_list, status="sweet") assert result == [(1, 1, "sweet", 50), (2, 1, "sweet", 50)] def test_iter_pages_ok(self, client_class_constructor): def json_content_function(p): return { "candies": [10 + p, 20 + p], "meta": { "pagination": { "page": p, "per_page": 11, "next_page": p + 1 if p < 3 else None, } }, } candies_client = client_class_constructor(json_content_function) result = candies_client._iter_pages(candies_client.get_list, status="sweet") assert result == [ (11, 1, "sweet", 50), (21, 1, "sweet", 50), (12, 2, "sweet", 50), (22, 2, "sweet", 50), (13, 3, "sweet", 50), (23, 3, "sweet", 50), ] def test_get_actions_ok(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) result = candies_client._iter_pages( candies_client.get_actions_list, status="sweet" ) assert result == [ (11, 1, "sweet", 50), (21, 1, "sweet", 50), (12, 2, "sweet", 50), (22, 2, "sweet", 50), (13, 3, "sweet", 50), (23, 3, "sweet", 50), ] def test_get_first_by_result_exists(self, client_class_constructor): json_content = {"candies": [1]} def json_content_function(_): return json_content candies_client = client_class_constructor(json_content_function) result = candies_client._get_first_by(candies_client.get_list, status="sweet") assert result == (1, None, "sweet", None) def test_get_first_by_result_does_not_exist(self, client_class_constructor): json_content = {"candies": []} def json_content_function(_): return json_content candies_client = client_class_constructor(json_content_function) result = candies_client._get_first_by(candies_client.get_list, status="sweet") assert result is None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/core/test_domain.py0000644000175100017510000001377515152343177020503 0ustar00runnerrunnerfrom __future__ import annotations import pytest from dateutil.parser import isoparse from hcloud.core import BaseDomain, DomainIdentityMixin, Meta, Pagination class TestMeta: @pytest.mark.parametrize("json_content", [None, "", {}]) def test_parse_meta_empty_json(self, json_content): result = Meta.parse_meta(json_content) assert result is not None def test_parse_meta_json_no_paginaton(self): json_content = {"meta": {}} result = Meta.parse_meta(json_content) assert isinstance(result, Meta) assert result.pagination is None def test_parse_meta_json_ok(self): json_content = { "meta": { "pagination": { "page": 2, "per_page": 10, "previous_page": 1, "next_page": 3, "last_page": 10, "total_entries": 100, } } } result = Meta.parse_meta(json_content) assert isinstance(result, Meta) assert isinstance(result.pagination, Pagination) assert result.pagination.page == 2 assert result.pagination.per_page == 10 assert result.pagination.next_page == 3 assert result.pagination.last_page == 10 assert result.pagination.total_entries == 100 class SomeDomain(BaseDomain, DomainIdentityMixin): __api_properties__ = ("id", "name") __slots__ = __api_properties__ def __init__(self, id=None, name=None): self.id = id self.name = name class TestDomainIdentityMixin: @pytest.mark.parametrize( "domain,expected_result", [ (SomeDomain(id=1, name="name"), 1), (SomeDomain(id=1), 1), (SomeDomain(name="name"), "name"), ], ) def test_id_or_name_ok(self, domain, expected_result): assert domain.id_or_name == expected_result def test_id_or_name_exception(self): domain = SomeDomain() with pytest.raises(ValueError) as exception_info: _ = domain.id_or_name error = exception_info.value assert str(error) == "id or name must be set" @pytest.mark.parametrize( "domain, id_or_name, expected", [ (SomeDomain(id=1, name="name1"), 1, True), (SomeDomain(id=1, name="name1"), "1", True), (SomeDomain(id=1, name="name1"), "name1", True), (SomeDomain(id=1, name="name1"), 2, False), (SomeDomain(id=1, name="name1"), "2", False), (SomeDomain(id=1, name="name1"), "name2", False), (SomeDomain(id=1, name="3"), 3, True), (SomeDomain(id=3, name="1"), "3", True), ], ) def test_has_id_or_name( self, domain: SomeDomain, id_or_name: str | int, expected: bool, ): assert domain.has_id_or_name(id_or_name) == expected class ActionDomain(BaseDomain, DomainIdentityMixin): __api_properties__ = ("id", "name", "started") __slots__ = __api_properties__ def __init__(self, id, name="name1", started=None): self.id = id self.name = name self.started = self._parse_datetime(started) class SomeOtherDomain(BaseDomain): __api_properties__ = ("id", "name", "child") __slots__ = __api_properties__ 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", [ ({"id": 1}, {"id": 1, "name": "name1", "started": None}), ({"id": 2, "name": "name2"}, {"id": 2, "name": "name2", "started": None}), ( {"id": 3, "foo": "boo", "description": "new"}, {"id": 3, "name": "name1", "started": None}, ), ( { "id": 4, "foo": "boo", "description": "new", "name": "name-name3", "started": "2016-01-30T23:50+00:00", }, { "id": 4, "name": "name-name3", "started": isoparse("2016-01-30T23:50+00:00"), }, ), ], ) 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 def test__eq__(self): a1 = ActionDomain(id=1, name="action") assert a1 == ActionDomain(id=1, name="action") assert a1 != ActionDomain(id=2, name="action") assert a1 != ActionDomain(id=1, name="something") assert a1 != SomeOtherDomain(id=1, name="action") def test_nested__eq__(self): child1 = ActionDomain(id=1, name="child") d1 = SomeOtherDomain(id=1, name="parent", child=child1) d2 = SomeOtherDomain(id=1, name="parent", child=child1) assert d1 == d2 d2.child = ActionDomain(id=2, name="child2") assert d1 != d2 def test_nested_list__eq__(self): child1 = ActionDomain(id=1, name="child") d1 = SomeOtherDomain(id=1, name="parent", child=[child1]) d2 = SomeOtherDomain(id=1, name="parent", child=[child1]) assert d1 == d2 d2.child = [ActionDomain(id=2, name="child2")] assert d1 != d2 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1496668 hcloud-2.17.0/tests/unit/datacenters/0000755000175100017510000000000015152343221017141 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/datacenters/__init__.py0000644000175100017510000000000015152343177021252 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/datacenters/conftest.py0000644000175100017510000000574715152343177021367 0ustar00runnerrunnerfrom __future__ import annotations import pytest @pytest.fixture() def datacenter_response(): return { "datacenter": { "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, } } @pytest.fixture() def two_datacenters_response(): return { "datacenters": [ { "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, }, { "id": 2, "name": "nbg1-dc3", "description": "Nuremberg 1 DC 3", "location": { "id": 2, "name": "nbg1", "description": "Nuremberg DC Park 1", "country": "DE", "city": "Nuremberg", "latitude": 49.452102, "longitude": 11.076665, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, }, ], "recommendation": 1, } @pytest.fixture() def one_datacenters_response(): return { "datacenters": [ { "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, } ], "recommendation": 1, } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/datacenters/test_client.py0000644000175100017510000001407015152343177022044 0ustar00runnerrunnerfrom __future__ import annotations from unittest import mock # noqa: F401 import pytest # noqa: F401 from hcloud import Client from hcloud.datacenters import BoundDatacenter, DatacentersClient, DatacenterServerTypes from hcloud.locations import BoundLocation class TestBoundDatacenter: def test_bound_datacenter_init(self, datacenter_response): bound_datacenter = BoundDatacenter( client=mock.MagicMock(), data=datacenter_response["datacenter"] ) assert bound_datacenter.id == 1 assert bound_datacenter.name == "fsn1-dc8" assert bound_datacenter.description == "Falkenstein 1 DC 8" assert bound_datacenter.complete is True assert isinstance(bound_datacenter.location, BoundLocation) assert bound_datacenter.location.id == 1 assert bound_datacenter.location.name == "fsn1" assert bound_datacenter.location.complete is True assert isinstance(bound_datacenter.server_types, DatacenterServerTypes) assert len(bound_datacenter.server_types.supported) == 3 assert bound_datacenter.server_types.supported[0].id == 1 assert bound_datacenter.server_types.supported[0].complete is False assert bound_datacenter.server_types.supported[1].id == 2 assert bound_datacenter.server_types.supported[1].complete is False assert bound_datacenter.server_types.supported[2].id == 3 assert bound_datacenter.server_types.supported[2].complete is False assert len(bound_datacenter.server_types.available) == 3 assert bound_datacenter.server_types.available[0].id == 1 assert bound_datacenter.server_types.available[0].complete is False assert bound_datacenter.server_types.available[1].id == 2 assert bound_datacenter.server_types.available[1].complete is False assert bound_datacenter.server_types.available[2].id == 3 assert bound_datacenter.server_types.available[2].complete is False assert len(bound_datacenter.server_types.available_for_migration) == 3 assert bound_datacenter.server_types.available_for_migration[0].id == 1 assert ( bound_datacenter.server_types.available_for_migration[0].complete is False ) assert bound_datacenter.server_types.available_for_migration[1].id == 2 assert ( bound_datacenter.server_types.available_for_migration[1].complete is False ) assert bound_datacenter.server_types.available_for_migration[2].id == 3 assert ( bound_datacenter.server_types.available_for_migration[2].complete is False ) class TestDatacentersClient: @pytest.fixture() def datacenters_client(self, client: Client): return DatacentersClient(client) def test_get_by_id( self, request_mock: mock.MagicMock, datacenters_client: DatacentersClient, datacenter_response, ): request_mock.return_value = datacenter_response datacenter = datacenters_client.get_by_id(1) request_mock.assert_called_with( method="GET", url="/datacenters/1", ) assert datacenter._client is datacenters_client assert datacenter.id == 1 assert datacenter.name == "fsn1-dc8" @pytest.mark.parametrize( "params", [{"name": "fsn1", "page": 1, "per_page": 10}, {"name": ""}, {}] ) def test_get_list( self, request_mock: mock.MagicMock, datacenters_client: DatacentersClient, two_datacenters_response, params, ): request_mock.return_value = two_datacenters_response result = datacenters_client.get_list(**params) request_mock.assert_called_with( method="GET", url="/datacenters", params=params, ) datacenters = result.datacenters assert result.meta is not None assert len(datacenters) == 2 datacenter1 = datacenters[0] datacenter2 = datacenters[1] assert datacenter1._client is datacenters_client assert datacenter1.id == 1 assert datacenter1.name == "fsn1-dc8" assert isinstance(datacenter1.location, BoundLocation) assert datacenter2._client is datacenters_client assert datacenter2.id == 2 assert datacenter2.name == "nbg1-dc3" assert isinstance(datacenter2.location, BoundLocation) @pytest.mark.parametrize("params", [{"name": "fsn1"}, {}]) def test_get_all( self, request_mock: mock.MagicMock, datacenters_client: DatacentersClient, two_datacenters_response, params, ): request_mock.return_value = two_datacenters_response datacenters = datacenters_client.get_all(**params) params.update({"page": 1, "per_page": 50}) request_mock.assert_called_with( method="GET", url="/datacenters", params=params, ) assert len(datacenters) == 2 datacenter1 = datacenters[0] datacenter2 = datacenters[1] assert datacenter1._client is datacenters_client assert datacenter1.id == 1 assert datacenter1.name == "fsn1-dc8" assert isinstance(datacenter1.location, BoundLocation) assert datacenter2._client is datacenters_client assert datacenter2.id == 2 assert datacenter2.name == "nbg1-dc3" assert isinstance(datacenter2.location, BoundLocation) def test_get_by_name( self, request_mock: mock.MagicMock, datacenters_client: DatacentersClient, one_datacenters_response, ): request_mock.return_value = one_datacenters_response datacenter = datacenters_client.get_by_name("fsn1-dc8") params = {"name": "fsn1-dc8"} request_mock.assert_called_with( method="GET", url="/datacenters", params=params, ) assert datacenter._client is datacenters_client assert datacenter.id == 1 assert datacenter.name == "fsn1-dc8" assert isinstance(datacenter.location, BoundLocation) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/datacenters/test_domain.py0000644000175100017510000000052515152343177022035 0ustar00runnerrunnerfrom __future__ import annotations import pytest from hcloud.datacenters import Datacenter, DatacenterServerTypes @pytest.mark.parametrize( "value", [ (Datacenter(id=1),), (DatacenterServerTypes(available=[], available_for_migration=[], supported=[])), ], ) def test_eq(value): assert value.__eq__(value) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1500406 hcloud-2.17.0/tests/unit/deprecation/0000755000175100017510000000000015152343221017141 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/deprecation/__init__.py0000644000175100017510000000000015152343177021252 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/deprecation/test_domain.py0000644000175100017510000000035315152343177022034 0ustar00runnerrunnerfrom __future__ import annotations import pytest from hcloud.deprecation import DeprecationInfo @pytest.mark.parametrize( "value", [ (DeprecationInfo(),), ], ) def test_eq(value): assert value.__eq__(value) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1772734097.150403 hcloud-2.17.0/tests/unit/exp/0000755000175100017510000000000015152343221015440 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/exp/__init__.py0000644000175100017510000000000015152343177017551 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/exp/test_zone.py0000644000175100017510000000157215152343177020043 0ustar00runnerrunnerfrom __future__ import annotations import pytest from hcloud.exp.zone import format_txt_record, is_txt_record_quoted @pytest.mark.parametrize( ("value", "expected"), [ ("hello world", False), ('"hello world', False), ('"hello world"', True), ], ) def test_is_txt_record_quoted(value: str, expected: bool): assert is_txt_record_quoted(value) == expected MANY_A = "a" * 255 SOME_B = "b" * 10 @pytest.mark.parametrize( ("value", "expected"), [ ("", ""), ('""', '"\\"\\""'), ("hello world", '"hello world"'), ("hello\nworld", '"hello\nworld"'), ('hello "world"', '"hello \\"world\\""'), ('hello "world', '"hello \\"world"'), (MANY_A + SOME_B, f'"{MANY_A}" "{SOME_B}"'), ], ) def test_format_txt_record(value: str, expected: str): assert format_txt_record(value) == expected ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1510828 hcloud-2.17.0/tests/unit/firewalls/0000755000175100017510000000000015152343221016634 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/firewalls/__init__.py0000644000175100017510000000000015152343177020745 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/firewalls/conftest.py0000644000175100017510000002144115152343177021047 0ustar00runnerrunnerfrom __future__ import annotations import pytest @pytest.fixture() def response_create_firewall(): return { "firewall": { "id": 38, "name": "Corporate Intranet Protection", "labels": {}, "created": "2016-01-30T23:50:00+00:00", "rules": [ { "direction": "in", "source_ips": [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ], "destination_ips": [], "protocol": "tcp", "port": "80", "description": None, }, { "direction": "out", "source_ips": [], "destination_ips": [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ], "protocol": "tcp", "port": "80", "description": "allow http out", }, ], "applied_to": [ {"server": {"id": 42}, "type": "server"}, { "type": "label_selector", "label_selector": {"selector": "key==value"}, }, ], }, "actions": [ { "command": "set_firewall_rules", "error": {"code": "action_failed", "message": "Action failed"}, "finished": "2016-01-30T23:56:00+00:00", "id": 13, "progress": 100, "resources": [{"id": 38, "type": "firewall"}], "started": "2016-01-30T23:55:00+00:00", "status": "success", }, { "command": "apply_firewall", "error": {"code": "action_failed", "message": "Action failed"}, "finished": "2016-01-30T23:56:00+00:00", "id": 14, "progress": 100, "resources": [ {"id": 42, "type": "server"}, {"id": 38, "type": "firewall"}, ], "started": "2016-01-30T23:55:00+00:00", "status": "success", }, ], } @pytest.fixture() def firewall_response(): return { "firewall": { "id": 38, "name": "Corporate Intranet Protection", "labels": {}, "created": "2016-01-30T23:50:00+00:00", "rules": [ { "direction": "in", "source_ips": [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ], "destination_ips": [], "protocol": "tcp", "port": "80", "description": "allow http in", }, { "direction": "out", "source_ips": [], "destination_ips": [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ], "protocol": "tcp", "port": "80", "description": "allow http out", }, ], "applied_to": [ {"server": {"id": 42}, "type": "server"}, { "type": "label_selector", "label_selector": {"selector": "key==value"}, }, ], } } @pytest.fixture() def two_firewalls_response(): return { "firewalls": [ { "id": 38, "name": "Corporate Intranet Protection", "labels": {}, "created": "2016-01-30T23:50:00+00:00", "rules": [ { "direction": "in", "source_ips": [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ], "destination_ips": [], "protocol": "tcp", "port": "80", "description": "allow http in", } ], "applied_to": [{"server": {"id": 42}, "type": "server"}], }, { "id": 39, "name": "Corporate Extranet Protection", "labels": {}, "created": "2016-01-30T23:50:00+00:00", "rules": [ { "direction": "in", "destination_ips": [], "source_ips": [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ], "protocol": "tcp", "port": "443", "description": "allow https in", } ], "applied_to": [{"server": {"id": 42}, "type": "server"}], }, ] } @pytest.fixture() def one_firewalls_response(): return { "firewalls": [ { "id": 38, "name": "Corporate Intranet Protection", "labels": {}, "created": "2016-01-30T23:50:00+00:00", "rules": [ { "direction": "in", "destination_ips": [], "source_ips": [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ], "protocol": "tcp", "port": "80", "description": "allow http in", } ], "applied_to": [{"server": {"id": 42}, "type": "server"}], } ] } @pytest.fixture() def response_update_firewall(): return { "firewall": { "id": 38, "name": "New Corporate Intranet Protection", "labels": {}, "created": "2016-01-30T23:50:00+00:00", "rules": [ { "direction": "in", "source_ips": [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ], "destination_ips": [], "protocol": "tcp", "port": "80", "description": "allow http in", } ], "applied_to": [{"server": {"id": 42}, "type": "server"}], } } @pytest.fixture() def response_get_actions(): return { "actions": [ { "id": 13, "command": "set_firewall_rules", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 42, "type": "firewall"}], "error": {"code": "action_failed", "message": "Action failed"}, } ] } @pytest.fixture() def response_set_rules(): return { "actions": [ { "id": 13, "command": "set_firewall_rules", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 38, "type": "firewall"}], "error": {"code": "action_failed", "message": "Action failed"}, }, { "id": 14, "command": "apply_firewall", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [ {"id": 38, "type": "firewall"}, {"id": 42, "type": "server"}, ], "error": {"code": "action_failed", "message": "Action failed"}, }, ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/firewalls/test_client.py0000644000175100017510000003203215152343177021535 0ustar00runnerrunnerfrom __future__ import annotations from unittest import mock import pytest from hcloud import Client from hcloud.firewalls import ( BoundFirewall, Firewall, FirewallResource, FirewallResourceLabelSelector, FirewallRule, FirewallsClient, ) from hcloud.servers import Server from ..conftest import BoundModelTestCase class TestBoundFirewall(BoundModelTestCase): methods = [ BoundFirewall.update, BoundFirewall.delete, BoundFirewall.apply_to_resources, BoundFirewall.remove_from_resources, BoundFirewall.set_rules, ] @pytest.fixture() def resource_client(self, client: Client): return client.firewalls @pytest.fixture() def bound_model(self, resource_client, firewall_response): return BoundFirewall(resource_client, data=firewall_response["firewall"]) def test_init(self, bound_model: BoundFirewall): o = bound_model assert o.id == 38 assert o.name == "Corporate Intranet Protection" assert o.labels == {} assert isinstance(o.rules, list) assert len(o.rules) == 2 assert isinstance(o.applied_to, list) assert len(o.applied_to) == 2 assert o.applied_to[0].server.id == 42 assert o.applied_to[0].type == "server" assert o.applied_to[1].label_selector.selector == "key==value" assert o.applied_to[1].type == "label_selector" firewall_in_rule = o.rules[0] assert isinstance(firewall_in_rule, FirewallRule) assert firewall_in_rule.direction == FirewallRule.DIRECTION_IN assert firewall_in_rule.protocol == FirewallRule.PROTOCOL_TCP assert firewall_in_rule.port == "80" assert isinstance(firewall_in_rule.source_ips, list) assert len(firewall_in_rule.source_ips) == 3 assert firewall_in_rule.source_ips == [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ] assert isinstance(firewall_in_rule.destination_ips, list) assert len(firewall_in_rule.destination_ips) == 0 assert firewall_in_rule.description == "allow http in" firewall_out_rule = o.rules[1] assert isinstance(firewall_out_rule, FirewallRule) assert firewall_out_rule.direction == FirewallRule.DIRECTION_OUT assert firewall_out_rule.protocol == FirewallRule.PROTOCOL_TCP assert firewall_out_rule.port == "80" assert isinstance(firewall_out_rule.source_ips, list) assert len(firewall_out_rule.source_ips) == 0 assert isinstance(firewall_out_rule.destination_ips, list) assert len(firewall_out_rule.destination_ips) == 3 assert firewall_out_rule.destination_ips == [ "28.239.13.1/32", "28.239.14.0/24", "ff21:1eac:9a3b:ee58:5ca:990c:8bc9:c03b/128", ] assert firewall_out_rule.description == "allow http out" class TestFirewallsClient: @pytest.fixture() def firewalls_client(self, client: Client): return FirewallsClient(client) def test_get_by_id( self, request_mock: mock.MagicMock, firewalls_client: FirewallsClient, firewall_response, ): request_mock.return_value = firewall_response firewall = firewalls_client.get_by_id(1) request_mock.assert_called_with( method="GET", url="/firewalls/1", ) assert firewall._client is firewalls_client assert firewall.id == 38 assert firewall.name == "Corporate Intranet Protection" @pytest.mark.parametrize( "params", [ { "name": "Corporate Intranet Protection", "sort": "id", "label_selector": "k==v", "page": 1, "per_page": 10, }, {"name": ""}, {}, ], ) def test_get_list( self, request_mock: mock.MagicMock, firewalls_client: FirewallsClient, two_firewalls_response, params, ): request_mock.return_value = two_firewalls_response result = firewalls_client.get_list(**params) request_mock.assert_called_with( method="GET", url="/firewalls", params=params, ) firewalls = result.firewalls assert result.meta is not None assert len(firewalls) == 2 firewalls1 = firewalls[0] firewalls2 = firewalls[1] assert firewalls1._client is firewalls_client assert firewalls1.id == 38 assert firewalls1.name == "Corporate Intranet Protection" assert firewalls2._client is firewalls_client assert firewalls2.id == 39 assert firewalls2.name == "Corporate Extranet Protection" @pytest.mark.parametrize( "params", [ { "name": "Corporate Intranet Protection", "sort": "id", "label_selector": "k==v", }, {}, ], ) def test_get_all( self, request_mock: mock.MagicMock, firewalls_client: FirewallsClient, two_firewalls_response, params, ): request_mock.return_value = two_firewalls_response firewalls = firewalls_client.get_all(**params) params.update({"page": 1, "per_page": 50}) request_mock.assert_called_with( method="GET", url="/firewalls", params=params, ) assert len(firewalls) == 2 firewalls1 = firewalls[0] firewalls2 = firewalls[1] assert firewalls1._client is firewalls_client assert firewalls1.id == 38 assert firewalls1.name == "Corporate Intranet Protection" assert firewalls2._client is firewalls_client assert firewalls2.id == 39 assert firewalls2.name == "Corporate Extranet Protection" def test_get_by_name( self, request_mock: mock.MagicMock, firewalls_client: FirewallsClient, one_firewalls_response, ): request_mock.return_value = one_firewalls_response firewall = firewalls_client.get_by_name("Corporate Intranet Protection") params = {"name": "Corporate Intranet Protection"} request_mock.assert_called_with( method="GET", url="/firewalls", params=params, ) assert firewall._client is firewalls_client assert firewall.id == 38 assert firewall.name == "Corporate Intranet Protection" def test_create( self, request_mock: mock.MagicMock, firewalls_client: FirewallsClient, response_create_firewall, ): request_mock.return_value = response_create_firewall response = firewalls_client.create( "Corporate Intranet Protection", rules=[ FirewallRule( direction=FirewallRule.DIRECTION_IN, protocol=FirewallRule.PROTOCOL_ICMP, source_ips=["0.0.0.0/0"], ) ], resources=[ FirewallResource( type=FirewallResource.TYPE_SERVER, server=Server(id=4711) ), FirewallResource( type=FirewallResource.TYPE_LABEL_SELECTOR, label_selector=FirewallResourceLabelSelector(selector="key==value"), ), ], ) request_mock.assert_called_with( method="POST", url="/firewalls", json={ "name": "Corporate Intranet Protection", "rules": [ {"direction": "in", "protocol": "icmp", "source_ips": ["0.0.0.0/0"]} ], "apply_to": [ {"type": "server", "server": {"id": 4711}}, { "type": "label_selector", "label_selector": {"selector": "key==value"}, }, ], }, ) bound_firewall = response.firewall actions = response.actions assert bound_firewall._client is firewalls_client assert bound_firewall.id == 38 assert bound_firewall.name == "Corporate Intranet Protection" assert len(bound_firewall.applied_to) == 2 assert len(actions) == 2 @pytest.mark.parametrize( "firewall", [Firewall(id=38), BoundFirewall(mock.MagicMock(), dict(id=38))] ) def test_update( self, request_mock: mock.MagicMock, firewalls_client: FirewallsClient, firewall, response_update_firewall, ): request_mock.return_value = response_update_firewall firewall = firewalls_client.update( firewall, name="New Corporate Intranet Protection", labels={} ) request_mock.assert_called_with( method="PUT", url="/firewalls/38", json={"name": "New Corporate Intranet Protection", "labels": {}}, ) assert firewall.id == 38 assert firewall.name == "New Corporate Intranet Protection" @pytest.mark.parametrize( "firewall", [Firewall(id=1), BoundFirewall(mock.MagicMock(), dict(id=1))] ) def test_set_rules( self, request_mock: mock.MagicMock, firewalls_client: FirewallsClient, firewall, response_set_rules, ): request_mock.return_value = response_set_rules actions = firewalls_client.set_rules( firewall, [ FirewallRule( direction=FirewallRule.DIRECTION_IN, protocol=FirewallRule.PROTOCOL_ICMP, source_ips=["0.0.0.0/0", "::/0"], description="Allow ICMP from everywhere", ), FirewallRule( direction=FirewallRule.DIRECTION_IN, protocol=FirewallRule.PROTOCOL_TCP, port="80", source_ips=["0.0.0.0/0", "::/0"], description="Allow HTTP from everywhere", ), ], ) request_mock.assert_called_with( method="POST", url="/firewalls/1/actions/set_rules", json={ "rules": [ { "direction": "in", "protocol": "icmp", "source_ips": ["0.0.0.0/0", "::/0"], "description": "Allow ICMP from everywhere", }, { "direction": "in", "protocol": "tcp", "port": "80", "source_ips": ["0.0.0.0/0", "::/0"], "description": "Allow HTTP from everywhere", }, ] }, ) assert actions[0].id == 13 assert actions[0].progress == 100 @pytest.mark.parametrize( "firewall", [Firewall(id=1), BoundFirewall(mock.MagicMock(), dict(id=1))] ) def test_delete( self, request_mock: mock.MagicMock, firewalls_client: FirewallsClient, firewall, ): delete_success = firewalls_client.delete(firewall) request_mock.assert_called_with( method="DELETE", url="/firewalls/1", ) assert delete_success is True @pytest.mark.parametrize( "firewall", [Firewall(id=1), BoundFirewall(mock.MagicMock(), dict(id=1))] ) def test_apply_to_resources( self, request_mock: mock.MagicMock, firewalls_client: FirewallsClient, firewall, response_set_rules, ): request_mock.return_value = response_set_rules actions = firewalls_client.apply_to_resources( firewall, [FirewallResource(type=FirewallResource.TYPE_SERVER, server=Server(id=5))], ) request_mock.assert_called_with( method="POST", url="/firewalls/1/actions/apply_to_resources", json={"apply_to": [{"type": "server", "server": {"id": 5}}]}, ) assert actions[0].id == 13 assert actions[0].progress == 100 @pytest.mark.parametrize( "firewall", [Firewall(id=1), BoundFirewall(mock.MagicMock(), dict(id=1))] ) def test_remove_from_resources( self, request_mock: mock.MagicMock, firewalls_client: FirewallsClient, firewall, response_set_rules, ): request_mock.return_value = response_set_rules actions = firewalls_client.remove_from_resources( firewall, [FirewallResource(type=FirewallResource.TYPE_SERVER, server=Server(id=5))], ) request_mock.assert_called_with( method="POST", url="/firewalls/1/actions/remove_from_resources", json={"remove_from": [{"type": "server", "server": {"id": 5}}]}, ) assert actions[0].id == 13 assert actions[0].progress == 100 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/firewalls/test_domain.py0000644000175100017510000000154015152343177021526 0ustar00runnerrunnerfrom __future__ import annotations import datetime from datetime import timezone import pytest from hcloud.firewalls import ( Firewall, FirewallResource, FirewallResourceAppliedToResources, FirewallResourceLabelSelector, FirewallRule, ) @pytest.mark.parametrize( "value", [ (Firewall(id=1),), (FirewallRule(direction="in", protocol="icmp", source_ips=[]),), (FirewallResource(type="server"),), (FirewallResourceAppliedToResources(type="server"),), (FirewallResourceLabelSelector(),), ], ) def test_eq(value): assert value.__eq__(value) 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=timezone.utc ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1518252 hcloud-2.17.0/tests/unit/floating_ips/0000755000175100017510000000000015152343221017322 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/floating_ips/__init__.py0000644000175100017510000000000015152343177021433 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/floating_ips/conftest.py0000644000175100017510000001405715152343177021542 0ustar00runnerrunnerfrom __future__ import annotations import pytest @pytest.fixture() def floating_ip_response(): return { "floating_ip": { "id": 4711, "description": "Web Frontend", "name": "Web Frontend", "created": "2016-01-30T23:50+00:00", "ip": "131.232.99.1", "type": "ipv4", "server": 42, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], "home_location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "blocked": False, "protection": {"delete": False}, "labels": {}, } } @pytest.fixture() def one_floating_ips_response(): return { "floating_ips": [ { "id": 4711, "description": "Web Frontend", "name": "Web Frontend", "created": "2016-01-30T23:50+00:00", "ip": "131.232.99.1", "type": "ipv4", "server": 42, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], "home_location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "blocked": False, "protection": {"delete": False}, "labels": {}, } ] } @pytest.fixture() def two_floating_ips_response(): return { "floating_ips": [ { "id": 4711, "description": "Web Frontend", "name": "Web Frontend", "created": "2016-01-30T23:50+00:00", "ip": "131.232.99.1", "type": "ipv4", "server": 42, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], "home_location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "blocked": False, "protection": {"delete": False}, "labels": {}, }, { "id": 4712, "description": "Web Backend", "name": "Web Backend", "created": "2016-01-30T23:50+00:00", "ip": "131.232.99.2", "type": "ipv4", "server": 42, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], "home_location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "blocked": False, "protection": {"delete": False}, "labels": {}, }, ] } @pytest.fixture() def floating_ip_create_response(): return { "floating_ip": { "id": 4711, "description": "Web Frontend", "name": "Web Frontend", "created": "2016-01-30T23:50+00:00", "ip": "131.232.99.1", "type": "ipv4", "server": 42, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], "home_location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "blocked": False, "protection": {"delete": False}, "labels": {}, }, "action": { "id": 13, "command": "assign_floating_ip", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, }, } @pytest.fixture() def response_update_floating_ip(): return { "floating_ip": { "id": 4711, "description": "New description", "name": "New name", "created": "2016-01-30T23:50+00:00", "ip": "131.232.99.1", "type": "ipv4", "server": 42, "dns_ptr": [{"ip": "2001:db8::1", "dns_ptr": "server.example.com"}], "home_location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "blocked": False, "protection": {"delete": False}, "labels": {}, } } @pytest.fixture() def response_get_actions(): return { "actions": [ { "id": 13, "command": "assign_floating_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"}, } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/floating_ips/test_client.py0000644000175100017510000003050215152343177022223 0ustar00runnerrunnerfrom __future__ import annotations from unittest import mock import pytest from hcloud import Client from hcloud.floating_ips import BoundFloatingIP, FloatingIP, FloatingIPsClient from hcloud.locations import BoundLocation, Location from hcloud.servers import BoundServer, Server from ..conftest import BoundModelTestCase class TestBoundFloatingIP(BoundModelTestCase): methods = [ BoundFloatingIP.update, BoundFloatingIP.delete, BoundFloatingIP.change_protection, BoundFloatingIP.change_dns_ptr, BoundFloatingIP.assign, BoundFloatingIP.unassign, ] @pytest.fixture() def resource_client(self, client: Client): return client.floating_ips @pytest.fixture() def bound_model(self, resource_client, floating_ip_response): return BoundFloatingIP( resource_client, data=floating_ip_response["floating_ip"] ) def test_init(self, bound_model: BoundFloatingIP): o = bound_model assert o.id == 4711 assert o.description == "Web Frontend" assert o.name == "Web Frontend" assert o.ip == "131.232.99.1" assert o.type == "ipv4" assert o.protection == {"delete": False} assert o.labels == {} assert o.blocked is False assert isinstance(o.server, BoundServer) assert o.server.id == 42 assert isinstance(o.home_location, BoundLocation) assert o.home_location.id == 1 assert o.home_location.name == "fsn1" assert o.home_location.description == "Falkenstein DC Park 1" assert o.home_location.country == "DE" assert o.home_location.city == "Falkenstein" assert o.home_location.latitude == 50.47612 assert o.home_location.longitude == 12.370071 class TestFloatingIPsClient: @pytest.fixture() def floating_ips_client(self, client: Client): return FloatingIPsClient(client) def test_get_by_id( self, request_mock: mock.MagicMock, floating_ips_client: FloatingIPsClient, floating_ip_response, ): request_mock.return_value = floating_ip_response bound_floating_ip = floating_ips_client.get_by_id(1) request_mock.assert_called_with( method="GET", url="/floating_ips/1", ) assert bound_floating_ip._client is floating_ips_client assert bound_floating_ip.id == 4711 assert bound_floating_ip.description == "Web Frontend" def test_get_by_name( self, request_mock: mock.MagicMock, floating_ips_client: FloatingIPsClient, one_floating_ips_response, ): request_mock.return_value = one_floating_ips_response bound_floating_ip = floating_ips_client.get_by_name("Web Frontend") request_mock.assert_called_with( method="GET", url="/floating_ips", params={"name": "Web Frontend"}, ) assert bound_floating_ip._client is floating_ips_client assert bound_floating_ip.id == 4711 assert bound_floating_ip.name == "Web Frontend" assert bound_floating_ip.description == "Web Frontend" @pytest.mark.parametrize( "params", [{"label_selector": "label1", "page": 1, "per_page": 10}, {"name": ""}, {}], ) def test_get_list( self, request_mock: mock.MagicMock, floating_ips_client: FloatingIPsClient, two_floating_ips_response, params, ): request_mock.return_value = two_floating_ips_response result = floating_ips_client.get_list(**params) request_mock.assert_called_with( method="GET", url="/floating_ips", params=params, ) bound_floating_ips = result.floating_ips assert result.meta is not None assert len(bound_floating_ips) == 2 bound_floating_ip1 = bound_floating_ips[0] bound_floating_ip2 = bound_floating_ips[1] assert bound_floating_ip1._client is floating_ips_client assert bound_floating_ip1.id == 4711 assert bound_floating_ip1.description == "Web Frontend" assert bound_floating_ip2._client is floating_ips_client assert bound_floating_ip2.id == 4712 assert bound_floating_ip2.description == "Web Backend" @pytest.mark.parametrize("params", [{"label_selector": "label1"}, {}]) def test_get_all( self, request_mock: mock.MagicMock, floating_ips_client: FloatingIPsClient, two_floating_ips_response, params, ): request_mock.return_value = two_floating_ips_response bound_floating_ips = floating_ips_client.get_all(**params) params.update({"page": 1, "per_page": 50}) request_mock.assert_called_with( method="GET", url="/floating_ips", params=params, ) assert len(bound_floating_ips) == 2 bound_floating_ip1 = bound_floating_ips[0] bound_floating_ip2 = bound_floating_ips[1] assert bound_floating_ip1._client is floating_ips_client assert bound_floating_ip1.id == 4711 assert bound_floating_ip1.description == "Web Frontend" assert bound_floating_ip2._client is floating_ips_client assert bound_floating_ip2.id == 4712 assert bound_floating_ip2.description == "Web Backend" def test_create_with_location( self, request_mock: mock.MagicMock, floating_ips_client: FloatingIPsClient, floating_ip_response, ): request_mock.return_value = floating_ip_response response = floating_ips_client.create( "ipv6", "Web Frontend", home_location=Location(name="location") ) request_mock.assert_called_with( method="POST", url="/floating_ips", json={ "description": "Web Frontend", "type": "ipv6", "home_location": "location", }, ) bound_floating_ip = response.floating_ip action = response.action assert bound_floating_ip._client is floating_ips_client assert bound_floating_ip.id == 4711 assert bound_floating_ip.description == "Web Frontend" assert action is None @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_create_with_server( self, request_mock: mock.MagicMock, floating_ips_client: FloatingIPsClient, server, floating_ip_create_response, ): request_mock.return_value = floating_ip_create_response response = floating_ips_client.create( type="ipv6", description="Web Frontend", server=server ) request_mock.assert_called_with( method="POST", url="/floating_ips", json={"description": "Web Frontend", "type": "ipv6", "server": 1}, ) bound_floating_ip = response.floating_ip action = response.action assert bound_floating_ip._client is floating_ips_client assert bound_floating_ip.id == 4711 assert bound_floating_ip.description == "Web Frontend" assert action.id == 13 def test_create_with_name( self, request_mock: mock.MagicMock, floating_ips_client: FloatingIPsClient, floating_ip_create_response, ): request_mock.return_value = floating_ip_create_response response = floating_ips_client.create( type="ipv6", description="Web Frontend", name="Web Frontend" ) request_mock.assert_called_with( method="POST", url="/floating_ips", json={ "description": "Web Frontend", "type": "ipv6", "name": "Web Frontend", }, ) bound_floating_ip = response.floating_ip action = response.action assert bound_floating_ip._client is floating_ips_client assert bound_floating_ip.id == 4711 assert bound_floating_ip.description == "Web Frontend" assert bound_floating_ip.name == "Web Frontend" assert action.id == 13 @pytest.mark.parametrize( "floating_ip", [FloatingIP(id=1), BoundFloatingIP(mock.MagicMock(), dict(id=1))] ) def test_update( self, request_mock: mock.MagicMock, floating_ips_client: FloatingIPsClient, floating_ip, response_update_floating_ip, ): request_mock.return_value = response_update_floating_ip floating_ip = floating_ips_client.update( floating_ip, description="New description", name="New name" ) request_mock.assert_called_with( method="PUT", url="/floating_ips/1", json={"description": "New description", "name": "New name"}, ) assert floating_ip.id == 4711 assert floating_ip.description == "New description" assert floating_ip.name == "New name" @pytest.mark.parametrize( "floating_ip", [FloatingIP(id=1), BoundFloatingIP(mock.MagicMock(), dict(id=1))] ) def test_change_protection( self, request_mock: mock.MagicMock, floating_ips_client: FloatingIPsClient, floating_ip, action_response, ): request_mock.return_value = action_response action = floating_ips_client.change_protection(floating_ip, True) request_mock.assert_called_with( method="POST", url="/floating_ips/1/actions/change_protection", json={"delete": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "floating_ip", [FloatingIP(id=1), BoundFloatingIP(mock.MagicMock(), dict(id=1))] ) def test_delete( self, request_mock: mock.MagicMock, floating_ips_client: FloatingIPsClient, floating_ip, action_response, ): request_mock.return_value = action_response delete_success = floating_ips_client.delete(floating_ip) request_mock.assert_called_with( method="DELETE", url="/floating_ips/1", ) assert delete_success is True @pytest.mark.parametrize( "server,floating_ip", [ (Server(id=1), FloatingIP(id=12)), ( BoundServer(mock.MagicMock(), dict(id=1)), BoundFloatingIP(mock.MagicMock(), dict(id=12)), ), ], ) def test_assign( self, request_mock: mock.MagicMock, floating_ips_client: FloatingIPsClient, server, floating_ip, action_response, ): request_mock.return_value = action_response action = floating_ips_client.assign(floating_ip, server) request_mock.assert_called_with( method="POST", url="/floating_ips/12/actions/assign", json={"server": 1}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "floating_ip", [FloatingIP(id=12), BoundFloatingIP(mock.MagicMock(), dict(id=12))], ) def test_unassign( self, request_mock: mock.MagicMock, floating_ips_client: FloatingIPsClient, floating_ip, action_response, ): request_mock.return_value = action_response action = floating_ips_client.unassign(floating_ip) request_mock.assert_called_with( method="POST", url="/floating_ips/12/actions/unassign", ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "floating_ip", [FloatingIP(id=12), BoundFloatingIP(mock.MagicMock(), dict(id=12))], ) def test_change_dns_ptr( self, request_mock: mock.MagicMock, floating_ips_client: FloatingIPsClient, floating_ip, action_response, ): request_mock.return_value = action_response action = floating_ips_client.change_dns_ptr( floating_ip, "1.2.3.4", "server02.example.com" ) request_mock.assert_called_with( method="POST", url="/floating_ips/12/actions/change_dns_ptr", json={"ip": "1.2.3.4", "dns_ptr": "server02.example.com"}, ) assert action.id == 1 assert action.progress == 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/floating_ips/test_domain.py0000644000175100017510000000102615152343177022213 0ustar00runnerrunnerfrom __future__ import annotations import datetime from datetime import timezone import pytest from hcloud.floating_ips import FloatingIP @pytest.mark.parametrize( "value", [ (FloatingIP(id=1),), ], ) def test_eq(value): assert value.__eq__(value) class TestFloatingIP: def test_created_is_datetime(self): floating_ip = FloatingIP(id=1, created="2016-01-30T23:50+00:00") assert floating_ip.created == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1772734097.152239 hcloud-2.17.0/tests/unit/helpers/0000755000175100017510000000000015152343221016306 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/helpers/__init__.py0000644000175100017510000000000015152343177020417 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/helpers/test_labels.py0000644000175100017510000001241415152343177021175 0ustar00runnerrunnerfrom __future__ import annotations 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, ), ( { "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), ({"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", ), ( { "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"), ({"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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1529188 hcloud-2.17.0/tests/unit/images/0000755000175100017510000000000015152343221016111 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/images/__init__.py0000644000175100017510000000000015152343177020222 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/images/conftest.py0000644000175100017510000001055715152343177020332 0ustar00runnerrunnerfrom __future__ import annotations import pytest @pytest.fixture() def image_response(): return { "image": { "id": 4711, "type": "snapshot", "status": "available", "name": "ubuntu-20.04", "description": "Ubuntu 20.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "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", "labels": {}, } } @pytest.fixture() def two_images_response(): return { "images": [ { "id": 4711, "type": "snapshot", "status": "available", "name": "ubuntu-20.04", "description": "Ubuntu 20.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "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", "labels": {}, }, { "id": 4712, "type": "system", "status": "available", "name": "ubuntu-18.10", "description": "Ubuntu 18.10 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "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", "labels": {}, }, ] } @pytest.fixture() def one_images_response(): return { "images": [ { "id": 4711, "type": "snapshot", "status": "available", "name": "ubuntu-20.04", "description": "Ubuntu 20.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "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", "labels": {}, } ] } @pytest.fixture() def response_update_image(): return { "image": { "id": 4711, "type": "snapshot", "status": "available", "name": None, "description": "My new Image description", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "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", "labels": {}, } } @pytest.fixture() def response_get_actions(): return { "actions": [ { "id": 13, "command": "change_protection", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 42, "type": "image"}], "error": {"code": "action_failed", "message": "Action failed"}, } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/images/test_client.py0000644000175100017510000002004315152343177021011 0ustar00runnerrunnerfrom __future__ import annotations import datetime from datetime import timezone from unittest import mock import pytest from hcloud import Client from hcloud.images import BoundImage, Image, ImagesClient from hcloud.servers import BoundServer from ..conftest import BoundModelTestCase class TestBoundImage(BoundModelTestCase): methods = [ BoundImage.update, BoundImage.delete, BoundImage.change_protection, ] @pytest.fixture() def resource_client(self, client: Client): return client.images @pytest.fixture() def bound_model(self, resource_client): return BoundImage(resource_client, data=dict(id=14)) def test_init(self, image_response): bound_image = BoundImage(client=mock.MagicMock(), data=image_response["image"]) assert bound_image.id == 4711 assert bound_image.type == "snapshot" assert bound_image.status == "available" assert bound_image.name == "ubuntu-20.04" assert bound_image.description == "Ubuntu 20.04 Standard 64 bit" 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=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=timezone.utc ) assert isinstance(bound_image.created_from, BoundServer) assert bound_image.created_from.id == 1 assert bound_image.created_from.name == "Server" assert bound_image.created_from.complete is False assert isinstance(bound_image.bound_to, BoundServer) assert bound_image.bound_to.id == 1 assert bound_image.bound_to.complete is False class TestImagesClient: @pytest.fixture() def images_client(self, client: Client): return ImagesClient(client) def test_get_by_id( self, request_mock: mock.MagicMock, images_client: ImagesClient, image_response, ): request_mock.return_value = image_response image = images_client.get_by_id(1) request_mock.assert_called_with( method="GET", url="/images/1", ) assert image._client is images_client assert image.id == 4711 assert image.name == "ubuntu-20.04" @pytest.mark.parametrize( "params", [ { "name": "ubuntu-20.04", "type": "system", "sort": "id", "bound_to": "1", "label_selector": "k==v", "page": 1, "per_page": 10, }, {"name": ""}, {"include_deprecated": True}, {}, ], ) def test_get_list( self, request_mock: mock.MagicMock, images_client: ImagesClient, two_images_response, params, ): request_mock.return_value = two_images_response result = images_client.get_list(**params) request_mock.assert_called_with( method="GET", url="/images", params=params, ) images = result.images assert result.meta is not None assert len(images) == 2 images1 = images[0] images2 = images[1] assert images1._client is images_client assert images1.id == 4711 assert images1.name == "ubuntu-20.04" assert images2._client is images_client assert images2.id == 4712 assert images2.name == "ubuntu-18.10" @pytest.mark.parametrize( "params", [ { "name": "ubuntu-20.04", "type": "system", "sort": "id", "bound_to": "1", "label_selector": "k==v", }, {"include_deprecated": True}, {}, ], ) def test_get_all( self, request_mock: mock.MagicMock, images_client: ImagesClient, two_images_response, params, ): request_mock.return_value = two_images_response images = images_client.get_all(**params) params.update({"page": 1, "per_page": 50}) request_mock.assert_called_with( method="GET", url="/images", params=params, ) assert len(images) == 2 images1 = images[0] images2 = images[1] assert images1._client is images_client assert images1.id == 4711 assert images1.name == "ubuntu-20.04" assert images2._client is images_client assert images2.id == 4712 assert images2.name == "ubuntu-18.10" def test_get_by_name( self, request_mock: mock.MagicMock, images_client: ImagesClient, one_images_response, ): request_mock.return_value = one_images_response with pytest.deprecated_call(): image = images_client.get_by_name("ubuntu-20.04") params = {"name": "ubuntu-20.04"} request_mock.assert_called_with( method="GET", url="/images", params=params, ) assert image._client is images_client assert image.id == 4711 assert image.name == "ubuntu-20.04" def test_get_by_name_and_architecture( self, request_mock: mock.MagicMock, images_client: ImagesClient, one_images_response, ): request_mock.return_value = one_images_response image = images_client.get_by_name_and_architecture("ubuntu-20.04", "x86") params = {"name": "ubuntu-20.04", "architecture": ["x86"]} request_mock.assert_called_with( method="GET", url="/images", 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))] ) def test_update( self, request_mock: mock.MagicMock, images_client: ImagesClient, image, response_update_image, ): request_mock.return_value = response_update_image image = images_client.update( image, description="My new Image description", type="snapshot", labels={} ) request_mock.assert_called_with( method="PUT", url="/images/1", json={ "description": "My new Image description", "type": "snapshot", "labels": {}, }, ) assert image.id == 4711 assert image.description == "My new Image description" @pytest.mark.parametrize( "image", [Image(id=1), BoundImage(mock.MagicMock(), dict(id=1))] ) def test_change_protection( self, request_mock: mock.MagicMock, images_client: ImagesClient, image, action_response, ): request_mock.return_value = action_response action = images_client.change_protection(image, True) request_mock.assert_called_with( method="POST", url="/images/1/actions/change_protection", json={"delete": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "image", [Image(id=1), BoundImage(mock.MagicMock(), dict(id=1))] ) def test_delete( self, request_mock: mock.MagicMock, images_client: ImagesClient, image, action_response, ): request_mock.return_value = action_response delete_success = images_client.delete(image) request_mock.assert_called_with( method="DELETE", url="/images/1", ) assert delete_success is True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/images/test_domain.py0000644000175100017510000000076015152343177021006 0ustar00runnerrunnerfrom __future__ import annotations import datetime from datetime import timezone import pytest from hcloud.images import Image @pytest.mark.parametrize( "value", [ (Image(id=1),), ], ) def test_eq(value): assert value.__eq__(value) 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=timezone.utc ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1536703 hcloud-2.17.0/tests/unit/isos/0000755000175100017510000000000015152343221015621 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/isos/__init__.py0000644000175100017510000000000015152343177017732 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/isos/conftest.py0000644000175100017510000000366715152343177020046 0ustar00runnerrunnerfrom __future__ import annotations import pytest @pytest.fixture() def iso_response(): return { "iso": { "id": 4711, "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "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", }, } } @pytest.fixture() def two_isos_response(): return { "isos": [ { "id": 4711, "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "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, "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "type": "public", "architecture": "x86", "deprecated": None, }, ] } @pytest.fixture() def one_isos_response(): return { "isos": [ { "id": 4711, "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "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", }, } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/isos/test_client.py0000644000175100017510000001025315152343177020523 0ustar00runnerrunnerfrom __future__ import annotations import datetime from datetime import timezone from unittest import mock import pytest from hcloud import Client from hcloud.isos import BoundIso, IsosClient class TestBoundIso: @pytest.fixture() def bound_iso(self, client: Client): return BoundIso(client.isos, data=dict(id=14)) def test_bound_iso_init(self, iso_response): bound_iso = BoundIso(client=mock.MagicMock(), data=iso_response["iso"]) assert bound_iso.id == 4711 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" 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 ) class TestIsosClient: @pytest.fixture() def isos_client(self, client: Client): return IsosClient(client) def test_get_by_id( self, request_mock: mock.MagicMock, isos_client: IsosClient, iso_response, ): request_mock.return_value = iso_response iso = isos_client.get_by_id(1) request_mock.assert_called_with( method="GET", url="/isos/1", ) assert iso._client is isos_client assert iso.id == 4711 assert iso.name == "FreeBSD-11.0-RELEASE-amd64-dvd1" @pytest.mark.parametrize( "params", [ {}, {"name": ""}, {"name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "page": 1, "per_page": 2}, ], ) def test_get_list( self, request_mock: mock.MagicMock, isos_client: IsosClient, two_isos_response, params, ): request_mock.return_value = two_isos_response result = isos_client.get_list(**params) request_mock.assert_called_with( method="GET", url="/isos", params=params, ) isos = result.isos assert result.meta is not None assert len(isos) == 2 isos1 = isos[0] isos2 = isos[1] assert isos1._client is isos_client assert isos1.id == 4711 assert isos1.name == "FreeBSD-11.0-RELEASE-amd64-dvd1" assert isos2._client is isos_client assert isos2.id == 4712 assert isos2.name == "FreeBSD-11.0-RELEASE-amd64-dvd1" @pytest.mark.parametrize( "params", [{}, {"name": "FreeBSD-11.0-RELEASE-amd64-dvd1"}] ) def test_get_all( self, request_mock: mock.MagicMock, isos_client: IsosClient, two_isos_response, params, ): request_mock.return_value = two_isos_response isos = isos_client.get_all(**params) params.update({"page": 1, "per_page": 50}) request_mock.assert_called_with( method="GET", url="/isos", params=params, ) assert len(isos) == 2 isos1 = isos[0] isos2 = isos[1] assert isos1._client is isos_client assert isos1.id == 4711 assert isos1.name == "FreeBSD-11.0-RELEASE-amd64-dvd1" assert isos2._client is isos_client assert isos2.id == 4712 assert isos2.name == "FreeBSD-11.0-RELEASE-amd64-dvd1" def test_get_by_name( self, request_mock: mock.MagicMock, isos_client: IsosClient, one_isos_response, ): request_mock.return_value = one_isos_response iso = isos_client.get_by_name("FreeBSD-11.0-RELEASE-amd64-dvd1") params = {"name": "FreeBSD-11.0-RELEASE-amd64-dvd1"} request_mock.assert_called_with( method="GET", url="/isos", params=params, ) assert iso._client is isos_client assert iso.id == 4711 assert iso.name == "FreeBSD-11.0-RELEASE-amd64-dvd1" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/isos/test_domain.py0000644000175100017510000000253415152343177020517 0ustar00runnerrunnerfrom __future__ import annotations from datetime import datetime, timezone import pytest from hcloud.isos import Iso @pytest.mark.parametrize( "value", [ (Iso(id=1),), ], ) def test_eq(value): assert value.__eq__(value) class TestIso: @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): 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 ) assert deprecated_iso.deprecation.unavailable_after == datetime( 2023, 11, 5, 8, 27, 1, tzinfo=timezone.utc ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1772734097.154425 hcloud-2.17.0/tests/unit/load_balancer_types/0000755000175100017510000000000015152343221020636 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/load_balancer_types/__init__.py0000644000175100017510000000000015152343177022747 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/load_balancer_types/conftest.py0000644000175100017510000000672315152343177023057 0ustar00runnerrunnerfrom __future__ import annotations import pytest @pytest.fixture() def load_balancer_type_response(): return { "load_balancer_type": { "id": 1, "name": "LB11", "description": "LB11", "max_connections": 1, "max_services": 1, "max_targets": 1, "max_assigned_certificates": 1, "deprecated": None, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], } } @pytest.fixture() def two_load_balancer_types_response(): return { "load_balancer_types": [ { "id": 1, "name": "LB11", "description": "LB11D", "max_connections": 1, "max_services": 1, "max_targets": 1, "max_assigned_certificates": 1, "deprecated": None, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], }, { "id": 2, "name": "LB21", "description": "LB21D", "max_connections": 2, "max_services": 2, "max_targets": 2, "max_assigned_certificates": 2, "deprecated": None, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], }, ] } @pytest.fixture() def one_load_balancer_types_response(): return { "load_balancer_types": [ { "id": 2, "name": "LB21", "description": "LB21D", "max_connections": 2, "max_services": 2, "max_targets": 2, "max_assigned_certificates": 2, "deprecated": None, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/load_balancer_types/test_client.py0000644000175100017510000000747015152343177023547 0ustar00runnerrunnerfrom __future__ import annotations from unittest import mock import pytest from hcloud import Client from hcloud.load_balancer_types import LoadBalancerTypesClient class TestLoadBalancerTypesClient: @pytest.fixture() def load_balancer_types_client(self, client: Client): return LoadBalancerTypesClient(client) def test_get_by_id( self, request_mock: mock.MagicMock, load_balancer_types_client: LoadBalancerTypesClient, load_balancer_type_response, ): request_mock.return_value = load_balancer_type_response load_balancer_type = load_balancer_types_client.get_by_id(1) request_mock.assert_called_with( method="GET", url="/load_balancer_types/1", ) assert load_balancer_type._client is load_balancer_types_client assert load_balancer_type.id == 1 assert load_balancer_type.name == "LB11" @pytest.mark.parametrize( "params", [{"name": "LB11", "page": 1, "per_page": 10}, {"name": ""}, {}] ) def test_get_list( self, request_mock: mock.MagicMock, load_balancer_types_client: LoadBalancerTypesClient, two_load_balancer_types_response, params, ): request_mock.return_value = two_load_balancer_types_response result = load_balancer_types_client.get_list(**params) request_mock.assert_called_with( method="GET", url="/load_balancer_types", params=params, ) load_balancer_types = result.load_balancer_types assert result.meta is not None assert len(load_balancer_types) == 2 load_balancer_types1 = load_balancer_types[0] load_balancer_types2 = load_balancer_types[1] assert load_balancer_types1._client is load_balancer_types_client assert load_balancer_types1.id == 1 assert load_balancer_types1.name == "LB11" assert load_balancer_types2._client is load_balancer_types_client assert load_balancer_types2.id == 2 assert load_balancer_types2.name == "LB21" @pytest.mark.parametrize("params", [{"name": "LB21"}]) def test_get_all( self, request_mock: mock.MagicMock, load_balancer_types_client: LoadBalancerTypesClient, two_load_balancer_types_response, params, ): request_mock.return_value = two_load_balancer_types_response load_balancer_types = load_balancer_types_client.get_all(**params) params.update({"page": 1, "per_page": 50}) request_mock.assert_called_with( method="GET", url="/load_balancer_types", params=params, ) assert len(load_balancer_types) == 2 load_balancer_types1 = load_balancer_types[0] load_balancer_types2 = load_balancer_types[1] assert load_balancer_types1._client is load_balancer_types_client assert load_balancer_types1.id == 1 assert load_balancer_types1.name == "LB11" assert load_balancer_types2._client is load_balancer_types_client assert load_balancer_types2.id == 2 assert load_balancer_types2.name == "LB21" def test_get_by_name( self, request_mock: mock.MagicMock, load_balancer_types_client: LoadBalancerTypesClient, one_load_balancer_types_response, ): request_mock.return_value = one_load_balancer_types_response load_balancer_type = load_balancer_types_client.get_by_name("LB21") params = {"name": "LB21"} request_mock.assert_called_with( method="GET", url="/load_balancer_types", params=params, ) assert load_balancer_type._client is load_balancer_types_client assert load_balancer_type.id == 2 assert load_balancer_type.name == "LB21" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/load_balancer_types/test_domain.py0000644000175100017510000000037115152343177023531 0ustar00runnerrunnerfrom __future__ import annotations import pytest from hcloud.load_balancer_types import LoadBalancerType @pytest.mark.parametrize( "value", [ (LoadBalancerType(id=1),), ], ) def test_eq(value): assert value.__eq__(value) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1550648 hcloud-2.17.0/tests/unit/load_balancers/0000755000175100017510000000000015152343221017575 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/load_balancers/__init__.py0000644000175100017510000000000015152343177021706 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/load_balancers/conftest.py0000644000175100017510000005645515152343177022025 0ustar00runnerrunnerfrom __future__ import annotations import pytest @pytest.fixture() def response_load_balancer(): return { "load_balancer": { "id": 4711, "name": "Web Frontend", "ipv4": "131.232.99.1", "ipv6": "2001:db8::1", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central", }, "load_balancer_type": { "id": 1, "name": "lb11", "description": "lb11", "max_connections": 20000, "max_services": 5, "max_targets": 25, "max_assigned_certificates": 10, "deprecated": "2016-01-30T23:50:00+00:00", "prices": [ { "location": "fsn-1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], }, "protection": {"delete": False}, "labels": {}, "created": "2016-01-30T23:50:00+00:00", "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "services": [ { "protocol": "https", "listen_port": 443, "destination_port": 80, "proxyprotocol": False, "http": { "cookie_name": "HCLBSTICKY", "cookie_lifetime": 300, "certificates": [897], "redirect_http": True, "sticky_sessions": True, }, "health_check": { "protocol": "http", "port": 4711, "interval": 15, "timeout": 10, "retries": 3, "http": { "domain": "example.com", "path": "/", "response": '{"status": "ok"}', "status_codes": [200], "tls": False, }, }, } ], "targets": [ { "type": "server", "server": {"id": 80}, "health_status": [{"listen_port": 443, "status": "healthy"}], "label_selector": None, "use_private_ip": False, }, { "type": "label_selector", "label_selector": {"selector": "env=prod"}, "use_private_ip": True, "targets": [ { "type": "server", "server": {"id": 105054278}, "use_private_ip": True, "health_status": [ {"listen_port": 443, "status": "healthy"}, {"listen_port": 3000, "status": "healthy"}, ], } ], }, ], "algorithm": {"type": "round_robin"}, } } @pytest.fixture() def response_create_load_balancer(): return { "load_balancer": { "id": 1, "name": "my-balancer", "load_balancer_type": { "id": 1, "name": "lb11", "description": "lb11", "max_connections": 20000, "max_services": 5, "max_targets": 25, "max_assigned_certificates": 10, "deprecated": "2016-01-30T23:50:00+00:00", "prices": [ { "location": "fsn-1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], }, "network_zone": "eu-central", "algorithm": {"type": "round_robin"}, "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "services": [ { "protocol": "https", "listen_port": 443, "destination_port": 80, "proxyprotocol": False, "http": { "cookie_name": "HCLBSTICKY", "cookie_lifetime": 300, "certificates": [897], "redirect_http": True, "sticky_sessions": True, }, "health_check": { "protocol": "http", "port": 4711, "interval": 15, "timeout": 10, "retries": 3, "http": { "domain": "example.com", "path": "/", "response": '{"status": "ok"}', "status_codes": [200], "tls": False, }, }, } ], "targets": [ { "type": "server", "server": {"id": 80}, "label_selector": None, "use_private_ip": False, } ], }, "action": { "id": 1, "command": "create_load_balancer", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, }, } @pytest.fixture() def response_update_load_balancer(): return { "load_balancer": { "id": 4711, "name": "new-name", "ipv4": "131.232.99.1", "ipv6": "2001:db8::1", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central", }, "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "load_balancer_type": { "id": 1, "name": "lb11", "description": "lb11", "max_connections": 20000, "max_services": 5, "max_targets": 25, "max_assigned_certificates": 10, "deprecated": "2016-01-30T23:50:00+00:00", "prices": [ { "location": "fsn-1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], }, "protection": {"delete": False}, "labels": {"labelkey": "value"}, "created": "2016-01-30T23:50:00+00:00", "services": [ { "protocol": "https", "listen_port": 443, "destination_port": 80, "proxyprotocol": False, "http": { "cookie_name": "HCLBSTICKY", "cookie_lifetime": 300, "certificates": [897], "redirect_http": True, "sticky_sessions": True, }, "health_check": { "protocol": "http", "port": 4711, "interval": 15, "timeout": 10, "retries": 3, "http": { "domain": "example.com", "path": "/", "response": '{"status": "ok"}', "status_codes": [200], "tls": False, }, }, } ], "targets": [ { "type": "server", "server": {"id": 80}, "use_private_ip": False, "health_status": [{"listen_port": 443, "status": "healthy"}], "label_selector": None, } ], "algorithm": {"type": "round_robin"}, } } @pytest.fixture() def response_simple_load_balancers(): return { "load_balancers": [ { "id": 4711, "name": "Web Frontend", "ipv4": "131.232.99.1", "ipv6": "2001:db8::1", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central", }, "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "load_balancer_type": { "id": 1, "name": "lb11", "description": "lb11", "max_connections": 20000, "max_services": 5, "max_targets": 25, "max_assigned_certificates": 10, "deprecated": "2016-01-30T23:50:00+00:00", "prices": [ { "location": "fsn-1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], }, "protection": {"delete": False}, "labels": {}, "created": "2016-01-30T23:50:00+00:00", "services": [ { "protocol": "https", "listen_port": 443, "destination_port": 80, "proxyprotocol": False, "http": { "sticky_sessions": True, "cookie_name": "HCLBSTICKY", "cookie_lifetime": 300, "certificates": [897], "redirect_http": True, }, "health_check": { "protocol": "http", "port": 4711, "interval": 15, "timeout": 10, "retries": 3, "http": { "domain": "example.com", "path": "/", "response": '{"status": "ok"}', "status_codes": [200], "tls": False, }, }, } ], "targets": [ { "type": "server", "server": {"id": 80}, "use_private_ip": False, "health_status": [{"listen_port": 443, "status": "healthy"}], "label_selector": None, } ], "algorithm": {"type": "round_robin"}, }, { "id": 4712, "name": "Web Frontend2", "ipv4": "131.232.99.1", "ipv6": "2001:db8::1", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central", }, "load_balancer_type": { "id": 1, "name": "lb11", "description": "lb11", "max_connections": 20000, "max_services": 5, "max_targets": 25, "max_assigned_certificates": 10, "deprecated": "2016-01-30T23:50:00+00:00", "prices": [ { "location": "fsn-1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], }, "protection": {"delete": False}, "labels": {}, "created": "2016-01-30T23:50:00+00:00", "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "services": [ { "protocol": "https", "listen_port": 443, "destination_port": 80, "proxyprotocol": False, "http": { "sticky_sessions": True, "cookie_name": "HCLBSTICKY", "cookie_lifetime": 300, "certificates": [897], "redirect_http": True, }, "health_check": { "protocol": "http", "port": 4711, "interval": 15, "timeout": 10, "retries": 3, "http": { "domain": "example.com", "path": "/", "response": '{"status": "ok"}', "status_codes": [200], "tls": False, }, }, } ], "targets": [ { "type": "server", "server": {"id": 80}, "health_status": [{"listen_port": 443, "status": "healthy"}], "label_selector": None, "use_private_ip": False, } ], "algorithm": {"type": "round_robin"}, }, ] } @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 { "action": { "id": 13, "command": "add_service", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_delete_service(): return { "action": { "id": 13, "command": "delete_service", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_add_target(): return { "action": { "id": 13, "command": "add_target", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_remove_target(): return { "action": { "id": 13, "command": "remove_target", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_update_service(): return { "action": { "id": 13, "command": "update_service", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_change_algorithm(): return { "action": { "id": 13, "command": "change_algorithm", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_change_reverse_dns_entry(): return { "action": { "id": 13, "command": "change_dns_ptr", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 42, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_change_protection(): return { "action": { "id": 13, "command": "change_protection", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_enable_public_interface(): return { "action": { "id": 13, "command": "enable_public_interface", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_disable_public_interface(): return { "action": { "id": 13, "command": "disable_public_interface", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_attach_load_balancer_to_network(): return { "action": { "id": 13, "command": "attach_to_network", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_detach_from_network(): return { "action": { "id": 13, "command": "detach_from_network", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_get_actions(): return { "actions": [ { "id": 13, "command": "change_protection", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 14, "type": "load_balancer"}], "error": {"code": "action_failed", "message": "Action failed"}, } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/load_balancers/test_client.py0000644000175100017510000005711415152343177022506 0ustar00runnerrunnerfrom __future__ import annotations from unittest import mock import pytest from hcloud import Client from hcloud.load_balancer_types import LoadBalancerType from hcloud.load_balancers import ( BoundLoadBalancer, LoadBalancer, LoadBalancerAlgorithm, LoadBalancerHealthCheck, LoadBalancersClient, LoadBalancerService, LoadBalancerTarget, LoadBalancerTargetIP, LoadBalancerTargetLabelSelector, ) from hcloud.locations import Location from hcloud.networks import Network from hcloud.servers import BoundServer, Server from ..conftest import BoundModelTestCase class TestBoundLoadBalancer(BoundModelTestCase): methods = [ BoundLoadBalancer.update, BoundLoadBalancer.delete, BoundLoadBalancer.change_algorithm, BoundLoadBalancer.change_dns_ptr, BoundLoadBalancer.change_protection, BoundLoadBalancer.change_type, BoundLoadBalancer.add_service, BoundLoadBalancer.update_service, BoundLoadBalancer.delete_service, BoundLoadBalancer.add_target, BoundLoadBalancer.remove_target, BoundLoadBalancer.attach_to_network, BoundLoadBalancer.detach_from_network, BoundLoadBalancer.disable_public_interface, BoundLoadBalancer.enable_public_interface, BoundLoadBalancer.get_metrics, ] @pytest.fixture() def resource_client(self, client: Client): return client.load_balancers @pytest.fixture() def bound_model(self, resource_client: LoadBalancersClient): return BoundLoadBalancer(resource_client, data=dict(id=1)) def test_init(self, response_load_balancer): bound_load_balancer = BoundLoadBalancer( client=mock.MagicMock(), data=response_load_balancer["load_balancer"] ) assert bound_load_balancer.id == 4711 assert bound_load_balancer.name == "Web Frontend" def test_init_label_selector_nested_targets(self, response_load_balancer): bound_load_balancer = BoundLoadBalancer( client=mock.MagicMock(), data=response_load_balancer["load_balancer"] ) label_selector_target = bound_load_balancer.targets[1] assert label_selector_target.type == "label_selector" assert label_selector_target.label_selector.selector == "env=prod" assert label_selector_target.use_private_ip is True assert label_selector_target.targets is not None assert len(label_selector_target.targets) == 1 nested = label_selector_target.targets[0] assert nested.type == "server" assert isinstance(nested.server, BoundServer) assert nested.server.id == 105054278 assert nested.use_private_ip is True assert nested.health_status is not None assert len(nested.health_status) == 2 assert nested.health_status[0].listen_port == 443 assert nested.health_status[0].status == "healthy" assert nested.health_status[1].listen_port == 3000 assert nested.health_status[1].status == "healthy" class TestLoadBalancerslient: @pytest.fixture() def resource_client(self, client: Client): return client.load_balancers def test_get_by_id( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, response_load_balancer, ): request_mock.return_value = response_load_balancer bound_load_balancer = resource_client.get_by_id(1) request_mock.assert_called_with( method="GET", url="/load_balancers/1", ) assert bound_load_balancer._client is resource_client assert bound_load_balancer.id == 4711 assert bound_load_balancer.name == "Web Frontend" assert bound_load_balancer.outgoing_traffic == 123456 assert bound_load_balancer.ingoing_traffic == 123456 assert bound_load_balancer.included_traffic == 654321 @pytest.mark.parametrize( "params", [ { "name": "load_balancer1", "label_selector": "label1", "page": 1, "per_page": 10, }, {"name": ""}, {}, ], ) def test_get_list( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, response_simple_load_balancers, params, ): request_mock.return_value = response_simple_load_balancers result = resource_client.get_list(**params) request_mock.assert_called_with( method="GET", url="/load_balancers", params=params, ) bound_load_balancers = result.load_balancers assert result.meta is not None assert len(bound_load_balancers) == 2 bound_load_balancer1 = bound_load_balancers[0] bound_load_balancer2 = bound_load_balancers[1] assert bound_load_balancer1._client is resource_client assert bound_load_balancer1.id == 4711 assert bound_load_balancer1.name == "Web Frontend" assert bound_load_balancer2._client is resource_client assert bound_load_balancer2.id == 4712 assert bound_load_balancer2.name == "Web Frontend2" @pytest.mark.parametrize( "params", [{"name": "loadbalancer1", "label_selector": "label1"}, {}] ) def test_get_all( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, response_simple_load_balancers, params, ): request_mock.return_value = response_simple_load_balancers bound_load_balancers = resource_client.get_all(**params) params.update({"page": 1, "per_page": 50}) request_mock.assert_called_with( method="GET", url="/load_balancers", params=params, ) assert len(bound_load_balancers) == 2 bound_load_balancer1 = bound_load_balancers[0] bound_load_balancer2 = bound_load_balancers[1] assert bound_load_balancer1._client is resource_client assert bound_load_balancer1.id == 4711 assert bound_load_balancer1.name == "Web Frontend" assert bound_load_balancer2._client is resource_client assert bound_load_balancer2.id == 4712 assert bound_load_balancer2.name == "Web Frontend2" def test_get_by_name( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, response_simple_load_balancers, ): request_mock.return_value = response_simple_load_balancers bound_load_balancer = resource_client.get_by_name("Web Frontend") params = {"name": "Web Frontend"} request_mock.assert_called_with( method="GET", url="/load_balancers", params=params, ) assert bound_load_balancer._client is resource_client assert bound_load_balancer.id == 4711 assert bound_load_balancer.name == "Web Frontend" def test_create( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, response_create_load_balancer, ): request_mock.return_value = response_create_load_balancer response = resource_client.create( "my-balancer", load_balancer_type=LoadBalancerType(name="lb11"), location=Location(id=1), ) request_mock.assert_called_with( method="POST", url="/load_balancers", json={"name": "my-balancer", "load_balancer_type": "lb11", "location": 1}, ) bound_load_balancer = response.load_balancer assert bound_load_balancer._client is resource_client assert bound_load_balancer.id == 1 assert bound_load_balancer.name == "my-balancer" @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) def test_change_type_with_load_balancer_type_name( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, load_balancer, action_response, ): request_mock.return_value = action_response action = resource_client.change_type( load_balancer, LoadBalancerType(name="lb11") ) request_mock.assert_called_with( method="POST", url="/load_balancers/1/actions/change_type", json={"load_balancer_type": "lb11"}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) def test_change_type_with_load_balancer_type_id( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, load_balancer, action_response, ): request_mock.return_value = action_response action = resource_client.change_type(load_balancer, LoadBalancerType(id=1)) request_mock.assert_called_with( method="POST", url="/load_balancers/1/actions/change_type", json={"load_balancer_type": 1}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) def test_update( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, load_balancer, response_update_load_balancer, ): request_mock.return_value = response_update_load_balancer load_balancer = resource_client.update( load_balancer, name="new-name", labels={} ) request_mock.assert_called_with( method="PUT", url="/load_balancers/1", json={"name": "new-name", "labels": {}}, ) assert load_balancer.id == 4711 assert load_balancer.name == "new-name" @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) def test_delete( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, load_balancer, action_response, ): request_mock.return_value = action_response delete_success = resource_client.delete(load_balancer) request_mock.assert_called_with( method="DELETE", url="/load_balancers/1", ) assert delete_success is True @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) def test_get_metrics( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, load_balancer, response_get_metrics, ): request_mock.return_value = response_get_metrics response = resource_client.get_metrics( load_balancer, type=["requests_per_second"], start="2023-12-14T16:55:32+01:00", end="2023-12-14T16:55:32+01:00", ) request_mock.assert_called_with( method="GET", url="/load_balancers/1/metrics", 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 @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) def test_add_service( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, load_balancer, response_add_service, ): request_mock.return_value = response_add_service service = LoadBalancerService(listen_port=80, protocol="http") action = resource_client.add_service(load_balancer, service) request_mock.assert_called_with( method="POST", url="/load_balancers/1/actions/add_service", json={"protocol": "http", "listen_port": 80}, ) assert action.id == 13 assert action.progress == 100 assert action.command == "add_service" @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) def test_delete_service( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, load_balancer, response_delete_service, ): request_mock.return_value = response_delete_service service = LoadBalancerService(listen_port=12) action = resource_client.delete_service(load_balancer, service) request_mock.assert_called_with( method="POST", url="/load_balancers/1/actions/delete_service", json={"listen_port": 12}, ) assert action.id == 13 assert action.progress == 100 assert action.command == "delete_service" @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) @pytest.mark.parametrize( "target,params", [ ( LoadBalancerTarget( type="server", server=Server(id=1), use_private_ip=True ), {"server": {"id": 1}, "use_private_ip": True}, ), ( LoadBalancerTarget(type="ip", ip=LoadBalancerTargetIP(ip="127.0.0.1")), {"ip": {"ip": "127.0.0.1"}}, ), ( LoadBalancerTarget( type="label_selector", label_selector=LoadBalancerTargetLabelSelector(selector="abc=def"), ), {"label_selector": {"selector": "abc=def"}}, ), ], ) def test_add_target( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, load_balancer, response_add_target, target, params, ): request_mock.return_value = response_add_target action = resource_client.add_target(load_balancer, target) params.update({"type": target.type}) request_mock.assert_called_with( method="POST", url="/load_balancers/1/actions/add_target", json=params, ) assert action.id == 13 assert action.progress == 100 assert action.command == "add_target" @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) @pytest.mark.parametrize( "target,params", [ ( LoadBalancerTarget( type="server", server=Server(id=1), use_private_ip=True ), {"server": {"id": 1}}, ), ( LoadBalancerTarget(type="ip", ip=LoadBalancerTargetIP(ip="127.0.0.1")), {"ip": {"ip": "127.0.0.1"}}, ), ( LoadBalancerTarget( type="label_selector", label_selector=LoadBalancerTargetLabelSelector(selector="abc=def"), ), {"label_selector": {"selector": "abc=def"}}, ), ], ) def test_remove_target( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, load_balancer, response_remove_target, target, params, ): request_mock.return_value = response_remove_target action = resource_client.remove_target(load_balancer, target) params.update({"type": target.type}) request_mock.assert_called_with( method="POST", url="/load_balancers/1/actions/remove_target", json=params, ) assert action.id == 13 assert action.progress == 100 assert action.command == "remove_target" @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) def test_update_service( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, load_balancer, response_update_service, ): request_mock.return_value = response_update_service new_health_check = LoadBalancerHealthCheck( protocol="http", port=13, interval=1, timeout=1, retries=1 ) service = LoadBalancerService(listen_port=12, health_check=new_health_check) action = resource_client.update_service(load_balancer, service) request_mock.assert_called_with( method="POST", url="/load_balancers/1/actions/update_service", json={ "listen_port": 12, "health_check": { "protocol": "http", "port": 13, "interval": 1, "timeout": 1, "retries": 1, }, }, ) assert action.id == 13 assert action.progress == 100 assert action.command == "update_service" @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) def test_change_algorithm( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, load_balancer, response_change_algorithm, ): request_mock.return_value = response_change_algorithm algorithm = LoadBalancerAlgorithm(type="round_robin") action = resource_client.change_algorithm(load_balancer, algorithm) request_mock.assert_called_with( method="POST", url="/load_balancers/1/actions/change_algorithm", json={"type": "round_robin"}, ) assert action.id == 13 assert action.progress == 100 assert action.command == "change_algorithm" @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) def test_change_dns_ptr( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, load_balancer, response_change_reverse_dns_entry, ): request_mock.return_value = response_change_reverse_dns_entry action = resource_client.change_dns_ptr( load_balancer, ip="1.2.3.4", dns_ptr="lb1.example.com" ) request_mock.assert_called_with( method="POST", url="/load_balancers/1/actions/change_dns_ptr", json={"dns_ptr": "lb1.example.com", "ip": "1.2.3.4"}, ) assert action.id == 13 assert action.progress == 100 assert action.command == "change_dns_ptr" @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) def test_change_protection( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, load_balancer, response_change_protection, ): request_mock.return_value = response_change_protection action = resource_client.change_protection(load_balancer, delete=True) request_mock.assert_called_with( method="POST", url="/load_balancers/1/actions/change_protection", json={"delete": True}, ) assert action.id == 13 assert action.progress == 100 assert action.command == "change_protection" @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) def test_enable_public_interface( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, load_balancer, response_enable_public_interface, ): request_mock.return_value = response_enable_public_interface action = resource_client.enable_public_interface(load_balancer) request_mock.assert_called_with( method="POST", url="/load_balancers/1/actions/enable_public_interface", ) assert action.id == 13 assert action.progress == 100 assert action.command == "enable_public_interface" @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) def test_disable_public_interface( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, load_balancer, response_disable_public_interface, ): request_mock.return_value = response_disable_public_interface action = resource_client.disable_public_interface(load_balancer) request_mock.assert_called_with( method="POST", url="/load_balancers/1/actions/disable_public_interface", ) assert action.id == 13 assert action.progress == 100 assert action.command == "disable_public_interface" @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) def test_attach_to_network( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, load_balancer, response_attach_load_balancer_to_network, ): request_mock.return_value = response_attach_load_balancer_to_network action = resource_client.attach_to_network(load_balancer, Network(id=1)) request_mock.assert_called_with( method="POST", url="/load_balancers/1/actions/attach_to_network", json={"network": 1}, ) assert action.id == 13 assert action.progress == 100 assert action.command == "attach_to_network" @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) def test_detach_from_network( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, load_balancer, response_detach_from_network, ): request_mock.return_value = response_detach_from_network action = resource_client.detach_from_network(load_balancer, Network(id=1)) request_mock.assert_called_with( method="POST", url="/load_balancers/1/actions/detach_from_network", json={"network": 1}, ) assert action.id == 13 assert action.progress == 100 assert action.command == "detach_from_network" @pytest.mark.parametrize( "load_balancer", [LoadBalancer(id=1), BoundLoadBalancer(mock.MagicMock(), dict(id=1))], ) def test_change_type( self, request_mock: mock.MagicMock, resource_client: LoadBalancersClient, load_balancer, action_response, ): request_mock.return_value = action_response action = resource_client.change_type( load_balancer, LoadBalancerType(name="lb21") ) request_mock.assert_called_with( method="POST", url="/load_balancers/1/actions/change_type", json={"load_balancer_type": "lb21"}, ) assert action.id == 1 assert action.progress == 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/load_balancers/test_domain.py0000644000175100017510000000514315152343177022472 0ustar00runnerrunnerfrom __future__ import annotations import datetime from datetime import timezone from unittest import mock import pytest from hcloud.load_balancers import ( BoundLoadBalancer, IPv4Address, IPv6Network, LoadBalancer, LoadBalancerAlgorithm, LoadBalancerHealthCheck, LoadBalancerHealthCheckHttp, LoadBalancerService, LoadBalancerServiceHttp, LoadBalancerTarget, LoadBalancerTargetHealthStatus, LoadBalancerTargetIP, LoadBalancerTargetLabelSelector, PrivateNet, PublicNetwork, ) from hcloud.networks import Network @pytest.mark.parametrize( "value", [ (LoadBalancer(id=1),), (LoadBalancerService,), (LoadBalancerServiceHttp(),), (LoadBalancerHealthCheck(),), (LoadBalancerHealthCheckHttp(),), (LoadBalancerTarget(),), (LoadBalancerTargetHealthStatus(),), (LoadBalancerTargetLabelSelector(),), (LoadBalancerTargetIP(),), (LoadBalancerAlgorithm(),), ( PublicNetwork( ipv4=IPv4Address(ip="127.0.0.1", dns_ptr="example.com"), ipv6=IPv6Network("2001:0db8::0/64", dns_ptr="example.com"), enabled=True, ), ), (IPv4Address(ip="127.0.0.1", dns_ptr="example.com"),), (IPv6Network("2001:0db8::0/64", dns_ptr="example.com"),), (PrivateNet(network=object(), ip="127.0.0.1"),), ], ) def test_eq(value): assert value.__eq__(value) 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=timezone.utc) def test_private_net_for(self): network1 = Network(id=1) network2 = Network(id=2) network3 = Network(id=3) load_balancer = LoadBalancer( id=42, private_net=[ PrivateNet(network=network1, ip="127.0.0.1"), PrivateNet(network=network2, ip="127.0.0.1"), ], ) assert load_balancer.private_net_for(network1).network.id == 1 assert load_balancer.private_net_for(network3) is None load_balancer = BoundLoadBalancer( client=mock.MagicMock(), data={ "id": 42, "private_net": [ {"network": 1, "ip": "127.0.0.1"}, {"network": 2, "ip": "127.0.0.1"}, ], }, ) assert load_balancer.private_net_for(network1).network.id == 1 assert load_balancer.private_net_for(network3) is None ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1557271 hcloud-2.17.0/tests/unit/locations/0000755000175100017510000000000015152343221016637 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/locations/__init__.py0000644000175100017510000000000015152343177020750 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/locations/conftest.py0000644000175100017510000000314515152343177021053 0ustar00runnerrunnerfrom __future__ import annotations import pytest @pytest.fixture() def location_response(): return { "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central", } } @pytest.fixture() def two_locations_response(): return { "locations": [ { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central", }, { "id": 2, "name": "nbg1", "description": "Nuremberg DC Park 1", "country": "DE", "city": "Nuremberg", "latitude": 49.452102, "longitude": 11.076665, "network_zone": "eu-central", }, ] } @pytest.fixture() def one_locations_response(): return { "locations": [ { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, "network_zone": "eu-central", } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/locations/test_client.py0000644000175100017510000000674215152343177021551 0ustar00runnerrunnerfrom __future__ import annotations from unittest import mock import pytest # noqa: F401 from hcloud import Client from hcloud.locations import LocationsClient class TestLocationsClient: @pytest.fixture() def locations_client(self, client: Client): return LocationsClient(client) def test_get_by_id( self, request_mock: mock.MagicMock, locations_client: LocationsClient, location_response, ): request_mock.return_value = location_response location = locations_client.get_by_id(1) request_mock.assert_called_with( method="GET", url="/locations/1", ) assert location._client is locations_client assert location.id == 1 assert location.name == "fsn1" assert location.network_zone == "eu-central" @pytest.mark.parametrize( "params", [{"name": "fsn1", "page": 1, "per_page": 10}, {"name": ""}, {}] ) def test_get_list( self, request_mock: mock.MagicMock, locations_client: LocationsClient, two_locations_response, params, ): request_mock.return_value = two_locations_response result = locations_client.get_list(**params) request_mock.assert_called_with( method="GET", url="/locations", params=params, ) locations = result.locations assert result.meta is not None assert len(locations) == 2 location1 = locations[0] location2 = locations[1] assert location1._client is locations_client assert location1.id == 1 assert location1.name == "fsn1" assert location1.network_zone == "eu-central" assert location2._client is locations_client assert location2.id == 2 assert location2.name == "nbg1" assert location2.network_zone == "eu-central" @pytest.mark.parametrize("params", [{"name": "fsn1"}, {}]) def test_get_all( self, request_mock: mock.MagicMock, locations_client: LocationsClient, two_locations_response, params, ): request_mock.return_value = two_locations_response locations = locations_client.get_all(**params) params.update({"page": 1, "per_page": 50}) request_mock.assert_called_with( method="GET", url="/locations", params=params, ) assert len(locations) == 2 location1 = locations[0] location2 = locations[1] assert location1._client is locations_client assert location1.id == 1 assert location1.name == "fsn1" assert location1.network_zone == "eu-central" assert location2._client is locations_client assert location2.id == 2 assert location2.name == "nbg1" assert location2.network_zone == "eu-central" def test_get_by_name( self, request_mock: mock.MagicMock, locations_client: LocationsClient, one_locations_response, ): request_mock.return_value = one_locations_response location = locations_client.get_by_name("fsn1") params = {"name": "fsn1"} request_mock.assert_called_with( method="GET", url="/locations", params=params, ) assert location._client is locations_client assert location.id == 1 assert location.name == "fsn1" assert location.network_zone == "eu-central" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/locations/test_domain.py0000644000175100017510000000033715152343177021534 0ustar00runnerrunnerfrom __future__ import annotations import pytest from hcloud.locations import Location @pytest.mark.parametrize( "value", [ (Location(id=1),), ], ) def test_eq(value): assert value.__eq__(value) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1563551 hcloud-2.17.0/tests/unit/networks/0000755000175100017510000000000015152343221016520 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/networks/__init__.py0000644000175100017510000000000015152343177020631 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/networks/conftest.py0000644000175100017510000001463315152343177020740 0ustar00runnerrunnerfrom __future__ import annotations import pytest @pytest.fixture() def network_response(): return { "network": { "id": 1, "name": "mynet", "created": "2016-01-30T23:50:11+00:00", "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", }, { "type": "vswitch", "ip_range": "10.0.3.0/24", "network_zone": "eu-central", "gateway": "10.0.3.1", }, ], "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], "expose_routes_to_vswitch": False, "servers": [42], "protection": {"delete": False}, "labels": {}, } } @pytest.fixture() def two_networks_response(): return { "networks": [ { "id": 1, "name": "mynet", "created": "2016-01-30T23:50:11+00:00", "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", }, { "type": "vswitch", "ip_range": "10.0.3.0/24", "network_zone": "eu-central", "gateway": "10.0.3.1", }, ], "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], "expose_routes_to_vswitch": False, "servers": [42], "protection": {"delete": False}, "labels": {}, }, { "id": 2, "name": "myanothernet", "created": "2016-01-30T23:50:11+00:00", "ip_range": "12.0.0.0/8", "subnets": [ { "type": "cloud", "ip_range": "12.0.1.0/24", "network_zone": "eu-central", "gateway": "12.0.0.1", } ], "routes": [{"destination": "12.100.1.0/24", "gateway": "12.0.1.1"}], "expose_routes_to_vswitch": False, "servers": [45], "protection": {"delete": False}, "labels": {}, }, ] } @pytest.fixture() def one_network_response(): return { "networks": [ { "id": 1, "name": "mynet", "created": "2016-01-30T23:50:11+00:00", "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", }, { "type": "vswitch", "ip_range": "10.0.3.0/24", "network_zone": "eu-central", "gateway": "10.0.3.1", }, ], "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], "expose_routes_to_vswitch": False, "servers": [42], "protection": {"delete": False}, "labels": {}, } ] } @pytest.fixture() def network_create_response(): 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": 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": {}, "created": "2016-01-30T23:50:00+00:00", } } @pytest.fixture() def response_update_network(): return { "network": { "id": 4711, "name": "new-name", "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": {}, "created": "2016-01-30T23:50:00+00:00", } } @pytest.fixture() def response_get_actions(): return { "actions": [ { "id": 13, "command": "add_subnet", "status": "success", "progress": 100, "started": "2016-01-30T23:55:00+00:00", "finished": "2016-01-30T23:56:00+00:00", "resources": [{"id": 4711, "type": "network"}], "error": {"code": "action_failed", "message": "Action failed"}, } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/networks/test_client.py0000644000175100017510000004307515152343177021432 0ustar00runnerrunnerfrom __future__ import annotations from unittest import mock import pytest from dateutil.parser import isoparse from hcloud import Client from hcloud.networks import ( BoundNetwork, Network, NetworkRoute, NetworksClient, NetworkSubnet, ) from hcloud.servers import BoundServer from ..conftest import BoundModelTestCase class TestBoundNetwork(BoundModelTestCase): methods = [ BoundNetwork.update, BoundNetwork.delete, BoundNetwork.add_subnet, BoundNetwork.delete_subnet, BoundNetwork.add_route, BoundNetwork.delete_route, BoundNetwork.change_ip_range, BoundNetwork.change_protection, ] @pytest.fixture() def resource_client(self, client: Client): return client.networks @pytest.fixture() def bound_model(self, resource_client: NetworksClient): return BoundNetwork(resource_client, data=dict(id=14)) def test_init(self, network_response): bound_network = BoundNetwork( client=mock.MagicMock(), data=network_response["network"] ) assert bound_network.id == 1 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 assert len(bound_network.servers) == 1 assert isinstance(bound_network.servers[0], BoundServer) assert bound_network.servers[0].id == 42 assert bound_network.servers[0].complete is False assert len(bound_network.subnets) == 2 assert isinstance(bound_network.subnets[0], NetworkSubnet) assert bound_network.subnets[0].type == NetworkSubnet.TYPE_CLOUD assert bound_network.subnets[0].ip_range == "10.0.1.0/24" assert bound_network.subnets[0].network_zone == "eu-central" assert bound_network.subnets[0].gateway == "10.0.0.1" assert len(bound_network.routes) == 1 assert isinstance(bound_network.routes[0], NetworkRoute) assert bound_network.routes[0].destination == "10.100.1.0/24" assert bound_network.routes[0].gateway == "10.0.1.1" class TestNetworksClient: @pytest.fixture() def networks_client(self, client: Client): return NetworksClient(client) @pytest.fixture() def network_subnet(self): return NetworkSubnet( type=NetworkSubnet.TYPE_CLOUD, ip_range="10.0.1.0/24", network_zone="eu-central", ) @pytest.fixture() def network_vswitch_subnet(self): return NetworkSubnet( type=NetworkSubnet.TYPE_VSWITCH, ip_range="10.0.1.0/24", network_zone="eu-central", vswitch_id=123, ) @pytest.fixture() def network_route(self): return NetworkRoute(destination="10.100.1.0/24", gateway="10.0.1.1") def test_get_by_id( self, request_mock: mock.MagicMock, networks_client: NetworksClient, network_response, ): request_mock.return_value = network_response bound_network = networks_client.get_by_id(1) request_mock.assert_called_with( method="GET", url="/networks/1", ) assert bound_network._client is networks_client assert bound_network.id == 1 assert bound_network.name == "mynet" @pytest.mark.parametrize( "params", [{"label_selector": "label1", "page": 1, "per_page": 10}, {"name": ""}, {}], ) def test_get_list( self, request_mock: mock.MagicMock, networks_client: NetworksClient, two_networks_response, params, ): request_mock.return_value = two_networks_response result = networks_client.get_list(**params) request_mock.assert_called_with( method="GET", url="/networks", params=params, ) bound_networks = result.networks assert result.meta is not None assert len(bound_networks) == 2 bound_network1 = bound_networks[0] bound_network2 = bound_networks[1] assert bound_network1._client is networks_client assert bound_network1.id == 1 assert bound_network1.name == "mynet" assert bound_network2._client is networks_client assert bound_network2.id == 2 assert bound_network2.name == "myanothernet" @pytest.mark.parametrize("params", [{"label_selector": "label1"}]) def test_get_all( self, request_mock: mock.MagicMock, networks_client: NetworksClient, two_networks_response, params, ): request_mock.return_value = two_networks_response bound_networks = networks_client.get_all(**params) params.update({"page": 1, "per_page": 50}) request_mock.assert_called_with( method="GET", url="/networks", params=params, ) assert len(bound_networks) == 2 bound_network1 = bound_networks[0] bound_network2 = bound_networks[1] assert bound_network1._client is networks_client assert bound_network1.id == 1 assert bound_network1.name == "mynet" assert bound_network2._client is networks_client assert bound_network2.id == 2 assert bound_network2.name == "myanothernet" def test_get_by_name( self, request_mock: mock.MagicMock, networks_client: NetworksClient, one_network_response, ): request_mock.return_value = one_network_response bound_network = networks_client.get_by_name("mynet") params = {"name": "mynet"} request_mock.assert_called_with( method="GET", url="/networks", params=params, ) assert bound_network._client is networks_client assert bound_network.id == 1 assert bound_network.name == "mynet" def test_create( self, request_mock: mock.MagicMock, networks_client: NetworksClient, network_create_response, ): request_mock.return_value = network_create_response networks_client.create(name="mynet", ip_range="10.0.0.0/8") request_mock.assert_called_with( method="POST", url="/networks", json={"name": "mynet", "ip_range": "10.0.0.0/8"}, ) def test_create_with_expose_routes_to_vswitch( self, request_mock: mock.MagicMock, networks_client: NetworksClient, network_create_response_with_expose_routes_to_vswitch, ): request_mock.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 ) request_mock.assert_called_with( method="POST", url="/networks", json={ "name": "mynet", "ip_range": "10.0.0.0/8", "expose_routes_to_vswitch": True, }, ) def test_create_with_subnet( self, request_mock: mock.MagicMock, networks_client: NetworksClient, network_subnet, network_create_response, ): request_mock.return_value = network_create_response networks_client.create( name="mynet", ip_range="10.0.0.0/8", subnets=[network_subnet] ) request_mock.assert_called_with( method="POST", url="/networks", json={ "name": "mynet", "ip_range": "10.0.0.0/8", "subnets": [ { "type": NetworkSubnet.TYPE_CLOUD, "ip_range": "10.0.1.0/24", "network_zone": "eu-central", } ], }, ) def test_create_with_subnet_vswitch( self, request_mock: mock.MagicMock, networks_client: NetworksClient, network_subnet, network_create_response, ): request_mock.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] ) request_mock.assert_called_with( method="POST", url="/networks", 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, request_mock: mock.MagicMock, networks_client: NetworksClient, network_route, network_create_response, ): request_mock.return_value = network_create_response networks_client.create( name="mynet", ip_range="10.0.0.0/8", routes=[network_route] ) request_mock.assert_called_with( method="POST", url="/networks", json={ "name": "mynet", "ip_range": "10.0.0.0/8", "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], }, ) def test_create_with_route_and_expose_routes_to_vswitch( self, request_mock: mock.MagicMock, networks_client, network_route, network_create_response_with_expose_routes_to_vswitch, ): request_mock.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, ) request_mock.assert_called_with( method="POST", url="/networks", 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, request_mock: mock.MagicMock, networks_client: NetworksClient, network_subnet, network_route, network_create_response, ): request_mock.return_value = network_create_response networks_client.create( name="mynet", ip_range="10.0.0.0/8", subnets=[network_subnet], routes=[network_route], ) request_mock.assert_called_with( method="POST", url="/networks", json={ "name": "mynet", "ip_range": "10.0.0.0/8", "subnets": [ { "type": NetworkSubnet.TYPE_CLOUD, "ip_range": "10.0.1.0/24", "network_zone": "eu-central", } ], "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], }, ) @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_update( self, request_mock: mock.MagicMock, networks_client: NetworksClient, network, response_update_network, ): request_mock.return_value = response_update_network network = networks_client.update( network, name="new-name", expose_routes_to_vswitch=True ) request_mock.assert_called_with( method="PUT", url="/networks/1", 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))] ) def test_change_protection( self, request_mock: mock.MagicMock, networks_client: NetworksClient, network, action_response, ): request_mock.return_value = action_response action = networks_client.change_protection(network, True) request_mock.assert_called_with( method="POST", url="/networks/1/actions/change_protection", json={"delete": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_delete( self, request_mock: mock.MagicMock, networks_client: NetworksClient, network, action_response, ): request_mock.return_value = action_response delete_success = networks_client.delete(network) request_mock.assert_called_with( method="DELETE", url="/networks/1", ) assert delete_success is True @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_add_subnet( self, request_mock: mock.MagicMock, networks_client: NetworksClient, network, action_response, network_subnet, ): request_mock.return_value = action_response action = networks_client.add_subnet(network, network_subnet) request_mock.assert_called_with( method="POST", url="/networks/1/actions/add_subnet", json={ "type": NetworkSubnet.TYPE_CLOUD, "ip_range": "10.0.1.0/24", "network_zone": "eu-central", }, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_add_subnet_vswitch( self, request_mock: mock.MagicMock, networks_client: NetworksClient, network, action_response, network_vswitch_subnet, ): request_mock.return_value = action_response action = networks_client.add_subnet(network, network_vswitch_subnet) request_mock.assert_called_with( method="POST", url="/networks/1/actions/add_subnet", json={ "type": NetworkSubnet.TYPE_VSWITCH, "ip_range": "10.0.1.0/24", "network_zone": "eu-central", "vswitch_id": 123, }, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_delete_subnet( self, request_mock: mock.MagicMock, networks_client: NetworksClient, network, action_response, network_subnet, ): request_mock.return_value = action_response action = networks_client.delete_subnet(network, network_subnet) request_mock.assert_called_with( method="POST", url="/networks/1/actions/delete_subnet", json={"ip_range": "10.0.1.0/24"}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_add_route( self, request_mock: mock.MagicMock, networks_client: NetworksClient, network, action_response, network_route, ): request_mock.return_value = action_response action = networks_client.add_route(network, network_route) request_mock.assert_called_with( method="POST", url="/networks/1/actions/add_route", json={"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_delete_route( self, request_mock: mock.MagicMock, networks_client: NetworksClient, network, action_response, network_route, ): request_mock.return_value = action_response action = networks_client.delete_route(network, network_route) request_mock.assert_called_with( method="POST", url="/networks/1/actions/delete_route", json={"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_change_ip_range( self, request_mock: mock.MagicMock, networks_client: NetworksClient, network, action_response, ): request_mock.return_value = action_response action = networks_client.change_ip_range(network, "10.0.0.0/12") request_mock.assert_called_with( method="POST", url="/networks/1/actions/change_ip_range", json={"ip_range": "10.0.0.0/12"}, ) assert action.id == 1 assert action.progress == 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/networks/test_domain.py0000644000175100017510000000122215152343177021407 0ustar00runnerrunnerfrom __future__ import annotations import datetime from datetime import timezone import pytest from hcloud.networks import Network, NetworkRoute, NetworkSubnet @pytest.mark.parametrize( "value", [ (Network(id=1),), (NetworkSubnet(ip_range="10.0.1.0/24"),), (NetworkRoute(destination="10.0.1.2", gateway="10.0.1.1"),), ], ) def test_eq(value): assert value.__eq__(value) 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=timezone.utc ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1569653 hcloud-2.17.0/tests/unit/placement_groups/0000755000175100017510000000000015152343221020213 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/placement_groups/__init__.py0000644000175100017510000000000015152343177022324 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/placement_groups/conftest.py0000644000175100017510000000336615152343177022434 0ustar00runnerrunnerfrom __future__ import annotations import pytest @pytest.fixture() def response_create_placement_group(): return { "placement_group": { "created": "2019-01-08T12:10:00+00:00", "id": 897, "labels": {"key": "value"}, "name": "my Placement Group", "servers": [], "type": "spread", } } @pytest.fixture() def one_placement_group_response(): return { "placement_groups": [ { "created": "2019-01-08T12:10:00+00:00", "id": 897, "labels": {"key": "value"}, "name": "my Placement Group", "servers": [4711, 4712], "type": "spread", } ] } @pytest.fixture() def two_placement_groups_response(): return { "placement_groups": [ { "created": "2019-01-08T12:10:00+00:00", "id": 897, "labels": {"key": "value"}, "name": "my Placement Group", "servers": [4711, 4712], "type": "spread", }, { "created": "2019-01-08T12:10:00+00:00", "id": 898, "labels": {"key": "value"}, "name": "my Placement Group", "servers": [4713, 4714, 4715], "type": "spread", }, ] } @pytest.fixture() def placement_group_response(): return { "placement_group": { "created": "2019-01-08T12:10:00+00:00", "id": 897, "labels": {"key": "value"}, "name": "my Placement Group", "servers": [4711, 4712], "type": "spread", } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/placement_groups/test_client.py0000644000175100017510000001711515152343177023121 0ustar00runnerrunnerfrom __future__ import annotations from unittest import mock import pytest from hcloud import Client from hcloud.placement_groups import ( BoundPlacementGroup, PlacementGroupsClient, ) from ..conftest import BoundModelTestCase def check_variables(placement_group: BoundPlacementGroup, expected): assert placement_group.id == expected["id"] assert placement_group.name == expected["name"] assert placement_group.labels == expected["labels"] assert placement_group.servers == expected["servers"] assert placement_group.type == expected["type"] class TestBoundPlacementGroup(BoundModelTestCase): methods = [ BoundPlacementGroup.update, BoundPlacementGroup.delete, ] @pytest.fixture() def resource_client(self, client: Client): return client.placement_groups @pytest.fixture() def bound_model(self, resource_client: PlacementGroupsClient): return BoundPlacementGroup(resource_client, data=dict(id=897)) def test_init(self, placement_group_response): bound_placement_group = BoundPlacementGroup( client=mock.MagicMock(), data=placement_group_response["placement_group"] ) check_variables( bound_placement_group, placement_group_response["placement_group"] ) class TestPlacementGroupsClient: @pytest.fixture() def resource_client(self, client: Client): return client.placement_groups @pytest.fixture() def bound_model(self, resource_client: PlacementGroupsClient): return BoundPlacementGroup(resource_client, data=dict(id=897)) def test_get_by_id( self, request_mock: mock.MagicMock, resource_client: PlacementGroupsClient, placement_group_response, ): request_mock.return_value = placement_group_response placement_group = resource_client.get_by_id( placement_group_response["placement_group"]["id"] ) request_mock.assert_called_with( method="GET", url="/placement_groups/897", ) assert placement_group._client is resource_client check_variables(placement_group, placement_group_response["placement_group"]) @pytest.mark.parametrize( "params", [ { "name": "my Placement Group", "sort": "id", "label_selector": "key==value", "page": 1, "per_page": 10, }, {"name": ""}, {}, ], ) def test_get_list( self, request_mock: mock.MagicMock, resource_client: PlacementGroupsClient, two_placement_groups_response, params, ): request_mock.return_value = two_placement_groups_response result = resource_client.get_list(**params) request_mock.assert_called_with( method="GET", url="/placement_groups", params=params, ) placement_groups = result.placement_groups assert result.meta is not None assert len(placement_groups) == len( two_placement_groups_response["placement_groups"] ) for placement_group, expected in zip( placement_groups, two_placement_groups_response["placement_groups"] ): assert placement_group._client is resource_client check_variables(placement_group, expected) @pytest.mark.parametrize( "params", [ { "name": "Corporate Intranet Protection", "sort": "id", "label_selector": "key==value", }, {}, ], ) def test_get_all( self, request_mock: mock.MagicMock, resource_client: PlacementGroupsClient, two_placement_groups_response, params, ): request_mock.return_value = two_placement_groups_response placement_groups = resource_client.get_all(**params) params.update({"page": 1, "per_page": 50}) request_mock.assert_called_with( method="GET", url="/placement_groups", params=params, ) assert len(placement_groups) == len( two_placement_groups_response["placement_groups"] ) for placement_group, expected in zip( placement_groups, two_placement_groups_response["placement_groups"] ): assert placement_group._client is resource_client check_variables(placement_group, expected) def test_get_by_name( self, request_mock: mock.MagicMock, resource_client: PlacementGroupsClient, one_placement_group_response, ): request_mock.return_value = one_placement_group_response placement_group = resource_client.get_by_name( one_placement_group_response["placement_groups"][0]["name"] ) params = {"name": one_placement_group_response["placement_groups"][0]["name"]} request_mock.assert_called_with( method="GET", url="/placement_groups", params=params, ) check_variables( placement_group, one_placement_group_response["placement_groups"][0] ) def test_create( self, request_mock: mock.MagicMock, resource_client: PlacementGroupsClient, response_create_placement_group, ): request_mock.return_value = response_create_placement_group response = resource_client.create( name=response_create_placement_group["placement_group"]["name"], type=response_create_placement_group["placement_group"]["type"], labels=response_create_placement_group["placement_group"]["labels"], ) json = { "name": response_create_placement_group["placement_group"]["name"], "labels": response_create_placement_group["placement_group"]["labels"], "type": response_create_placement_group["placement_group"]["type"], } request_mock.assert_called_with( method="POST", url="/placement_groups", json=json, ) bound_placement_group = response.placement_group assert bound_placement_group._client is resource_client check_variables( bound_placement_group, response_create_placement_group["placement_group"] ) def test_update( self, request_mock: mock.MagicMock, resource_client: PlacementGroupsClient, bound_model, placement_group_response, ): request_mock.return_value = placement_group_response placement_group = resource_client.update( bound_model, name=placement_group_response["placement_group"]["name"], labels=placement_group_response["placement_group"]["labels"], ) request_mock.assert_called_with( method="PUT", url="/placement_groups/897", json={ "labels": placement_group_response["placement_group"]["labels"], "name": placement_group_response["placement_group"]["name"], }, ) check_variables(placement_group, placement_group_response["placement_group"]) def test_delete( self, request_mock: mock.MagicMock, resource_client: PlacementGroupsClient, bound_model, ): delete_success = resource_client.delete(bound_model) request_mock.assert_called_with( method="DELETE", url="/placement_groups/897", ) assert delete_success is True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/placement_groups/test_domain.py0000644000175100017510000000106215152343177023104 0ustar00runnerrunnerfrom __future__ import annotations import datetime from datetime import timezone import pytest from hcloud.placement_groups import PlacementGroup @pytest.mark.parametrize( "value", [ (PlacementGroup(id=1),), ], ) def test_eq(value): assert value.__eq__(value) 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=timezone.utc ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1575994 hcloud-2.17.0/tests/unit/primary_ips/0000755000175100017510000000000015152343221017202 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/primary_ips/__init__.py0000644000175100017510000000000015152343177021313 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/primary_ips/conftest.py0000644000175100017510000002031215152343177021411 0ustar00runnerrunnerfrom __future__ import annotations 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"}, } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/primary_ips/test_client.py0000644000175100017510000002420115152343177022102 0ustar00runnerrunnerfrom __future__ import annotations from unittest import mock import pytest from hcloud import Client from hcloud.datacenters import BoundDatacenter, Datacenter from hcloud.primary_ips import BoundPrimaryIP, PrimaryIP, PrimaryIPsClient from ..conftest import BoundModelTestCase class TestBoundPrimaryIP(BoundModelTestCase): methods = [ BoundPrimaryIP.update, BoundPrimaryIP.delete, BoundPrimaryIP.change_dns_ptr, BoundPrimaryIP.change_protection, BoundPrimaryIP.assign, BoundPrimaryIP.unassign, ] @pytest.fixture() def resource_client(self, client: Client): return client.primary_ips @pytest.fixture() def bound_model(self, resource_client: PrimaryIPsClient): return BoundPrimaryIP(resource_client, data=dict(id=14)) def test_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" with pytest.deprecated_call(): datacenter = bound_primary_ip.datacenter assert isinstance(datacenter, BoundDatacenter) assert datacenter.id == 42 assert datacenter.name == "fsn1-dc8" assert datacenter.description == "Falkenstein DC Park 8" assert datacenter.location.country == "DE" assert datacenter.location.city == "Falkenstein" assert datacenter.location.latitude == 50.47612 assert datacenter.location.longitude == 12.370071 class TestPrimaryIPsClient: @pytest.fixture() def primary_ips_client(self, client: Client): return PrimaryIPsClient(client) def test_get_by_id( self, request_mock: mock.MagicMock, primary_ips_client: PrimaryIPsClient, primary_ip_response, ): request_mock.return_value = primary_ip_response bound_primary_ip = primary_ips_client.get_by_id(1) request_mock.assert_called_with( method="GET", url="/primary_ips/1", ) assert bound_primary_ip._client is primary_ips_client assert bound_primary_ip.id == 42 def test_get_by_name( self, request_mock: mock.MagicMock, primary_ips_client: PrimaryIPsClient, one_primary_ips_response, ): request_mock.return_value = one_primary_ips_response bound_primary_ip = primary_ips_client.get_by_name("my-resource") request_mock.assert_called_with( method="GET", url="/primary_ips", 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, request_mock: mock.MagicMock, primary_ips_client: PrimaryIPsClient, all_primary_ips_response, params, ): request_mock.return_value = all_primary_ips_response bound_primary_ips = primary_ips_client.get_all(**params) params.update({"page": 1, "per_page": 50}) request_mock.assert_called_with( method="GET", url="/primary_ips", 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, request_mock: mock.MagicMock, primary_ips_client: PrimaryIPsClient, primary_ip_response, ): request_mock.return_value = primary_ip_response with pytest.deprecated_call(): response = primary_ips_client.create( type="ipv6", name="my-resource", datacenter=Datacenter(name="datacenter"), ) request_mock.assert_called_with( method="POST", url="/primary_ips", json={ "name": "my-resource", "type": "ipv6", "assignee_type": "server", "datacenter": "datacenter", "auto_delete": False, }, ) 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, request_mock: mock.MagicMock, primary_ips_client: PrimaryIPsClient, primary_ip_create_response, ): request_mock.return_value = primary_ip_create_response response = primary_ips_client.create( type="ipv6", name="my-ip", assignee_id=17, assignee_type="server", ) request_mock.assert_called_with( method="POST", url="/primary_ips", json={ "name": "my-ip", "type": "ipv6", "assignee_id": 17, "assignee_type": "server", "auto_delete": False, }, ) 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, request_mock: mock.MagicMock, primary_ips_client: PrimaryIPsClient, primary_ip, response_update_primary_ip, ): request_mock.return_value = response_update_primary_ip primary_ip = primary_ips_client.update( primary_ip, auto_delete=True, name="my-resource" ) request_mock.assert_called_with( method="PUT", url="/primary_ips/1", 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, request_mock: mock.MagicMock, primary_ips_client: PrimaryIPsClient, primary_ip, action_response, ): request_mock.return_value = action_response action = primary_ips_client.change_protection(primary_ip, True) request_mock.assert_called_with( method="POST", url="/primary_ips/1/actions/change_protection", 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, request_mock: mock.MagicMock, primary_ips_client: PrimaryIPsClient, primary_ip, action_response, ): request_mock.return_value = action_response delete_success = primary_ips_client.delete(primary_ip) request_mock.assert_called_with( method="DELETE", url="/primary_ips/1", ) 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, request_mock: mock.MagicMock, primary_ips_client: PrimaryIPsClient, assignee_id, assignee_type, primary_ip, action_response, ): request_mock.return_value = action_response action = primary_ips_client.assign(primary_ip, assignee_id, assignee_type) request_mock.assert_called_with( method="POST", url="/primary_ips/12/actions/assign", 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, request_mock: mock.MagicMock, primary_ips_client: PrimaryIPsClient, primary_ip, action_response, ): request_mock.return_value = action_response action = primary_ips_client.unassign(primary_ip) request_mock.assert_called_with( method="POST", url="/primary_ips/12/actions/unassign", ) 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, request_mock: mock.MagicMock, primary_ips_client: PrimaryIPsClient, primary_ip, action_response, ): request_mock.return_value = action_response action = primary_ips_client.change_dns_ptr( primary_ip, "1.2.3.4", "server02.example.com" ) request_mock.assert_called_with( method="POST", url="/primary_ips/12/actions/change_dns_ptr", json={"ip": "1.2.3.4", "dns_ptr": "server02.example.com"}, ) assert action.id == 1 assert action.progress == 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/primary_ips/test_domain.py0000644000175100017510000000101715152343177022073 0ustar00runnerrunnerfrom __future__ import annotations import datetime from datetime import timezone import pytest from hcloud.primary_ips import PrimaryIP @pytest.mark.parametrize( "value", [ (PrimaryIP(id=1),), ], ) def test_eq(value): assert value.__eq__(value) class TestPrimaryIP: def test_created_is_datetime(self): primary_ip = PrimaryIP(id=1, created="2016-01-30T23:50+00:00") assert primary_ip.created == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1582277 hcloud-2.17.0/tests/unit/server_types/0000755000175100017510000000000015152343221017376 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/server_types/__init__.py0000644000175100017510000000000015152343177021507 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/server_types/conftest.py0000644000175100017510000001242115152343177021607 0ustar00runnerrunnerfrom __future__ import annotations import pytest @pytest.fixture() def server_type_response(): return { "server_type": { "id": 1, "name": "cx11", "description": "CX11", "category": "Shared vCPU", "cores": 1, "memory": 1, "disk": 25, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], "storage_type": "local", "cpu_type": "shared", "architecture": "x86", "included_traffic": 21990232555520, "deprecated": True, "deprecation": { "announced": "2023-06-01T00:00:00Z", "unavailable_after": "2023-09-01T00:00:00Z", }, "locations": [ { "id": 1, "name": "nbg1", "deprecation": None, }, { "id": 2, "name": "fsn1", "deprecation": { "announced": "2023-06-01T00:00:00Z", "unavailable_after": "2023-09-01T00:00:00Z", }, }, ], } } @pytest.fixture() def two_server_types_response(): return { "server_types": [ { "id": 1, "name": "cx11", "description": "CX11", "cores": 1, "memory": 1, "disk": 25, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], "storage_type": "local", "cpu_type": "shared", "architecture": "x86", "included_traffic": 21990232555520, "deprecated": True, "deprecation": { "announced": "2023-06-01T00:00:00Z", "unavailable_after": "2023-09-01T00:00:00Z", }, }, { "id": 2, "name": "cx21", "description": "CX21", "cores": 2, "memory": 4.0, "disk": 40, "prices": [ { "location": "fsn1", "price_hourly": { "net": "0.0080000000", "gross": "0.0095200000000000", }, "price_monthly": { "net": "4.9000000000", "gross": "5.8310000000000000", }, }, { "location": "nbg1", "price_hourly": { "net": "0.0080000000", "gross": "0.0095200000000000", }, "price_monthly": { "net": "4.9000000000", "gross": "5.8310000000000000", }, }, ], "storage_type": "local", "cpu_type": "shared", "architecture": "x86", "included_traffic": 21990232555520, "deprecated": False, "deprecation": None, }, ] } @pytest.fixture() def one_server_types_response(): return { "server_types": [ { "id": 1, "name": "cx11", "description": "CX11", "cores": 1, "memory": 1, "disk": 25, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], "storage_type": "local", "cpu_type": "shared", "architecture": "x86", "included_traffic": 21990232555520, "deprecated": True, "deprecation": { "announced": "2023-06-01T00:00:00Z", "unavailable_after": "2023-09-01T00:00:00Z", }, } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/server_types/test_client.py0000644000175100017510000001177415152343177022311 0ustar00runnerrunnerfrom __future__ import annotations from datetime import datetime, timezone from unittest import mock import pytest from hcloud import Client from hcloud.server_types import BoundServerType, ServerTypesClient class TestBoundServerType: @pytest.fixture() def bound_server_type(self, client: Client): return BoundServerType(client.server_types, data=dict(id=14)) def test_init(self, server_type_response): o = BoundServerType( client=mock.MagicMock(), data=server_type_response["server_type"] ) assert o.id == 1 assert o.name == "cx11" assert o.description == "CX11" assert o.category == "Shared vCPU" assert o.cores == 1 assert o.memory == 1 assert o.disk == 25 assert o.storage_type == "local" assert o.cpu_type == "shared" assert o.architecture == "x86" assert len(o.locations) == 2 assert o.locations[0].location.id == 1 assert o.locations[0].location.name == "nbg1" assert o.locations[0].deprecation is None assert o.locations[1].location.id == 2 assert o.locations[1].location.name == "fsn1" assert ( o.locations[1].deprecation.announced.isoformat() == "2023-06-01T00:00:00+00:00" ) assert ( o.locations[1].deprecation.unavailable_after.isoformat() == "2023-09-01T00:00:00+00:00" ) with pytest.deprecated_call(): assert o.deprecated is True assert o.deprecation is not None assert o.deprecation.announced == datetime(2023, 6, 1, tzinfo=timezone.utc) assert o.deprecation.unavailable_after == datetime( 2023, 9, 1, tzinfo=timezone.utc ) assert o.included_traffic == 21990232555520 class TestServerTypesClient: @pytest.fixture() def server_types_client(self, client: Client): return ServerTypesClient(client) def test_get_by_id( self, request_mock: mock.MagicMock, server_types_client: ServerTypesClient, server_type_response, ): request_mock.return_value = server_type_response server_type = server_types_client.get_by_id(1) request_mock.assert_called_with( method="GET", url="/server_types/1", ) assert server_type._client is server_types_client assert server_type.id == 1 assert server_type.name == "cx11" @pytest.mark.parametrize( "params", [{"name": "cx11", "page": 1, "per_page": 10}, {"name": ""}, {}] ) def test_get_list( self, request_mock: mock.MagicMock, server_types_client: ServerTypesClient, two_server_types_response, params, ): request_mock.return_value = two_server_types_response result = server_types_client.get_list(**params) request_mock.assert_called_with( method="GET", url="/server_types", params=params, ) server_types = result.server_types assert result.meta is not None assert len(server_types) == 2 server_types1 = server_types[0] server_types2 = server_types[1] assert server_types1._client is server_types_client assert server_types1.id == 1 assert server_types1.name == "cx11" assert server_types2._client is server_types_client assert server_types2.id == 2 assert server_types2.name == "cx21" @pytest.mark.parametrize("params", [{"name": "cx11"}]) def test_get_all( self, request_mock: mock.MagicMock, server_types_client: ServerTypesClient, two_server_types_response, params, ): request_mock.return_value = two_server_types_response server_types = server_types_client.get_all(**params) params.update({"page": 1, "per_page": 50}) request_mock.assert_called_with( method="GET", url="/server_types", params=params, ) assert len(server_types) == 2 server_types1 = server_types[0] server_types2 = server_types[1] assert server_types1._client is server_types_client assert server_types1.id == 1 assert server_types1.name == "cx11" assert server_types2._client is server_types_client assert server_types2.id == 2 assert server_types2.name == "cx21" def test_get_by_name( self, request_mock: mock.MagicMock, server_types_client: ServerTypesClient, one_server_types_response, ): request_mock.return_value = one_server_types_response server_type = server_types_client.get_by_name("cx11") params = {"name": "cx11"} request_mock.assert_called_with( method="GET", url="/server_types", params=params, ) assert server_type._client is server_types_client assert server_type.id == 1 assert server_type.name == "cx11" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/server_types/test_domain.py0000644000175100017510000000034615152343177022273 0ustar00runnerrunnerfrom __future__ import annotations import pytest from hcloud.server_types import ServerType @pytest.mark.parametrize( "value", [ (ServerType(id=1),), ], ) def test_eq(value): assert value.__eq__(value) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1588554 hcloud-2.17.0/tests/unit/servers/0000755000175100017510000000000015152343221016335 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/servers/__init__.py0000644000175100017510000000000015152343177020446 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/servers/conftest.py0000644000175100017510000007564615152343177020570 0ustar00runnerrunnerfrom __future__ import annotations import pytest @pytest.fixture() def response_simple_server(): return { "server": { "id": 1, "name": "my-server", "status": "running", "created": "2016-01-30T23:50+00:00", "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], "firewalls": [{"id": 38, "status": "applied"}], }, "private_net": [ { "network": 4711, "ip": "10.1.1.5", "alias_ips": ["10.1.1.8"], "mac_address": "86:00:ff:2a:7d:e1", } ], "server_type": { "id": 1, "name": "cx11", "description": "CX11", "cores": 1, "memory": 1, "disk": 25, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], "storage_type": "local", "cpu_type": "shared", }, "datacenter": { "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, }, "image": { "id": 4711, "type": "snapshot", "status": "available", "name": "ubuntu-20.04", "description": "Ubuntu 20.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": False, "protection": {"delete": False, "rebuild": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, }, "iso": None, "rescue_enabled": False, "locked": False, "backup_window": "22-02", "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "primary_disk_size": 20, "protection": {}, "labels": {}, "volumes": [], } } @pytest.fixture() def response_create_simple_server(): return { "server": { "id": 1, "name": "my-server", "status": "running", "created": "2016-01-30T23:50+00:00", "primary_disk_size": 20, "public_net": { "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": [], "firewalls": [{"id": 38, "status": "applied"}], }, "private_net": [], "server_type": { "id": 1, "name": "cx11", "description": "CX11", "cores": 1, "memory": 1, "disk": 25, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], "storage_type": "local", "cpu_type": "shared", }, "datacenter": { "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, }, "image": { "id": 4711, "type": "snapshot", "status": "available", "name": "ubuntu-20.04", "description": "Ubuntu 20.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": False, "protection": {"delete": False, "rebuild": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, }, "iso": {"id": 4711}, "rescue_enabled": False, "locked": False, "backup_window": "22-02", "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "protection": {}, "labels": {}, "volumes": [], }, "action": { "id": 1, "command": "create_server", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, }, "next_actions": [ { "id": 13, "command": "start_server", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, } ], "root_password": "YItygq1v3GYjjMomLaKc", } @pytest.fixture() def response_update_server(): return { "server": { "id": 14, "name": "new-name", "status": "running", "created": "2016-01-30T23:50+00:00", "public_net": { "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], "firewalls": [], }, "private_net": [], "server_type": { "id": 1, "name": "cx11", "description": "CX11", "cores": 1, "memory": 1, "disk": 25, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], "storage_type": "local", "cpu_type": "shared", }, "datacenter": { "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, }, "image": { "id": 4711, "type": "snapshot", "status": "available", "name": "ubuntu-20.04", "description": "Ubuntu 20.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": False, "protection": {"delete": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, }, "iso": { "id": 4711, "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "type": "public", "deprecated": "2018-02-28T00:00:00+00:00", }, "rescue_enabled": False, "locked": False, "backup_window": "22-02", "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "protection": {"delete": False, "rebuild": False}, "labels": {}, "volumes": [], } } @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 { "servers": [ { "id": 1, "name": "my-server", "status": "running", "created": "2016-01-30T23:50+00:00", "public_net": { "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"} ], }, "floating_ips": [478], "firewalls": [], }, "private_net": [ { "network": 4711, "ip": "10.1.1.5", "alias_ips": ["10.1.1.8"], "mac_address": "86:00:ff:2a:7d:e1", } ], "server_type": { "id": 1, "name": "cx11", "description": "CX11", "cores": 1, "memory": 1, "disk": 25, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], "storage_type": "local", "cpu_type": "shared", }, "datacenter": { "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, }, "image": { "id": 4711, "type": "snapshot", "status": "available", "name": "ubuntu-20.04", "description": "Ubuntu 20.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": False, "protection": {"delete": False, "rebuild": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, }, "iso": None, "rescue_enabled": False, "locked": False, "backup_window": "22-02", "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "protection": {}, "labels": {}, "volumes": [], }, { "id": 2, "name": "my-server2", "status": "running", "created": "2016-03-30T23:50+00:00", "public_net": { "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"} ], }, "floating_ips": [478], "firewalls": [], }, "private_net": [ { "network": 4711, "ip": "10.1.1.7", "alias_ips": ["10.1.1.99"], "mac_address": "86:00:ff:2a:7d:e1", } ], "server_type": { "id": 1, "name": "cx11", "description": "CX11", "cores": 1, "memory": 1, "disk": 25, "prices": [ { "location": "fsn1", "price_hourly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, "price_monthly": { "net": "1.0000000000", "gross": "1.1900000000000000", }, } ], "storage_type": "local", "cpu_type": "shared", }, "datacenter": { "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, }, "image": { "id": 4711, "type": "snapshot", "status": "available", "name": "ubuntu-20.04", "description": "Ubuntu 20.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": False, "protection": {"delete": False, "rebuild": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, }, "iso": None, "rescue_enabled": False, "locked": False, "backup_window": "22-02", "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "primary_disk_size": 20, "protection": {}, "labels": {}, "volumes": [], }, ] } @pytest.fixture() def response_full_server(): return { "server": { "id": 42, "name": "my-server", "status": "running", "created": "2016-01-30T23:50+00:00", "primary_disk_size": 20, "public_net": { "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], "firewalls": [{"id": 38, "status": "applied"}], }, "private_net": [ { "network": 4711, "ip": "10.1.1.5", "alias_ips": ["10.1.1.8"], "mac_address": "86:00:ff:2a:7d:e1", } ], "server_type": { "id": 1, "name": "cx11", "description": "CX11", "cores": 1, "memory": 1, "disk": 25, "prices": [], "storage_type": "local", "cpu_type": "shared", }, "datacenter": { "id": 1, "name": "fsn1-dc8", "description": "Falkenstein 1 DC 8", "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "server_types": { "supported": [1, 2, 3], "available": [1, 2, 3], "available_for_migration": [1, 2, 3], }, }, "image": { "id": 4711, "type": "snapshot", "status": "available", "name": "ubuntu-20.04", "description": "Ubuntu 20.04 Standard 64 bit", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": False, "protection": {"delete": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, }, "iso": { "id": 4711, "name": "FreeBSD-11.0-RELEASE-amd64-dvd1", "description": "FreeBSD 11.0 x64", "type": "public", "deprecated": "2018-02-28T00:00:00+00:00", }, "placement_group": { "created": "2019-01-08T12:10:00+00:00", "id": 897, "labels": {"key": "value"}, "name": "my Placement Group", "servers": [4711, 4712], "type": "spread", }, "rescue_enabled": False, "locked": False, "backup_window": "22-02", "outgoing_traffic": 123456, "ingoing_traffic": 123456, "included_traffic": 654321, "protection": {}, "labels": {}, "volumes": [1, 2], } } @pytest.fixture() def response_server_reset_password(): return { "action": { "id": 1, "command": "reset_password", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, }, "root_password": "YItygq1v3GYjjMomLaKc", } @pytest.fixture() def response_server_enable_rescue(): return { "action": { "id": 1, "command": "enable_rescue", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, }, "root_password": "YItygq1v3GYjjMomLaKc", } @pytest.fixture() def response_server_create_image(): return { "image": { "id": 4711, "type": "snapshot", "status": "creating", "name": None, "description": "my image", "image_size": 2.3, "disk_size": 10, "created": "2016-01-30T23:50+00:00", "created_from": {"id": 1, "name": "Server"}, "bound_to": None, "os_flavor": "ubuntu", "os_version": "16.04", "rapid_deploy": False, "protection": {"delete": False}, "deprecated": "2018-02-28T00:00:00+00:00", "labels": {}, }, "action": { "id": 1, "command": "enable_rescue", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, }, } @pytest.fixture() def response_server_request_console(): return { "wss_url": "wss://console.hetzner.cloud/?server_id=1&token=3db32d15-af2f-459c-8bf8-dee1fd05f49c", "password": "9MQaTg2VAGI0FIpc10k3UpRXcHj2wQ6x", "action": { "id": 1, "command": "request_console", "status": "success", "progress": 0, "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"}, }, } @pytest.fixture() def response_get_actions(): return { "actions": [ { "id": 13, "command": "start_server", "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"}, } ] } @pytest.fixture() def response_attach_to_network(): return { "action": { "id": 1, "command": "attach_to_network", "status": "running", "progress": 0, "started": "2016-01-30T23:50:00+00:00", "finished": None, "resources": [ {"id": 42, "type": "server"}, {"id": 4711, "type": "network"}, ], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_detach_from_network(): return { "action": { "id": 1, "command": "detach_from_network", "status": "running", "progress": 0, "started": "2016-01-30T23:50:00+00:00", "finished": None, "resources": [ {"id": 42, "type": "server"}, {"id": 4711, "type": "network"}, ], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_change_alias_ips(): return { "action": { "id": 1, "command": "change_alias_ips", "status": "running", "progress": 0, "started": "2016-01-30T23:50:00+00:00", "finished": None, "resources": [ {"id": 42, "type": "server"}, {"id": 4711, "type": "network"}, ], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_apply_firewall(): return { "action": { "id": 1, "command": "apply_firewall", "status": "running", "progress": 0, "started": "2016-01-30T23:50:00+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_remove_firewall(): return { "action": { "id": 1, "command": "remove_firewall", "status": "running", "progress": 0, "started": "2016-01-30T23:50:00+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, } } @pytest.fixture() def response_add_to_placement_group(): return { "action": { "command": "add_to_placement_group", "error": {"code": "action_failed", "message": "Action failed"}, "finished": None, "id": 13, "progress": 0, "resources": [{"id": 42, "type": "server"}], "started": "2016-01-30T23:50:00+00:00", "status": "running", } } @pytest.fixture() def response_remove_from_placement_group(): return { "action": { "command": "remove_from_placement_group", "error": {"code": "action_failed", "message": "Action failed"}, "finished": "2016-01-30T23:56:00+00:00", "id": 13, "progress": 100, "resources": [{"id": 42, "type": "server"}], "started": "2016-01-30T23:55:00+00:00", "status": "success", } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/servers/test_client.py0000644000175100017510000011221015152343177021233 0ustar00runnerrunnerfrom __future__ import annotations from unittest import mock import pytest from hcloud import Client 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 import BoundVolume, Volume from ..conftest import BoundModelTestCase class TestBoundServer(BoundModelTestCase): methods = [ BoundServer.update, BoundServer.delete, BoundServer.add_to_placement_group, BoundServer.remove_from_placement_group, BoundServer.attach_iso, BoundServer.detach_iso, BoundServer.attach_to_network, BoundServer.detach_from_network, BoundServer.change_alias_ips, BoundServer.change_dns_ptr, BoundServer.change_protection, BoundServer.change_type, BoundServer.create_image, BoundServer.disable_backup, BoundServer.enable_backup, BoundServer.disable_rescue, BoundServer.enable_rescue, BoundServer.get_metrics, BoundServer.power_off, BoundServer.power_on, BoundServer.reboot, BoundServer.rebuild, BoundServer.shutdown, BoundServer.reset, BoundServer.request_console, BoundServer.reset_password, ] @pytest.fixture() def resource_client(self, client: Client): return client.servers @pytest.fixture() def bound_model(self, resource_client: ServersClient): return BoundServer(resource_client, data=dict(id=14)) # pylint: disable=too-many-statements def test_init(self, response_full_server): bound_server = BoundServer( client=mock.MagicMock(), data=response_full_server["server"] ) assert bound_server.id == 42 assert bound_server.name == "my-server" assert bound_server.primary_disk_size == 20 assert isinstance(bound_server.public_net, PublicNetwork) assert isinstance(bound_server.public_net.ipv4, IPv4Address) assert bound_server.public_net.ipv4.ip == "1.2.3.4" assert bound_server.public_net.ipv4.blocked is False assert bound_server.public_net.ipv4.dns_ptr == "server01.example.com" assert isinstance(bound_server.public_net.ipv6, IPv6Network) assert bound_server.public_net.ipv6.ip == "2001:db8::/64" assert bound_server.public_net.ipv6.blocked is False assert bound_server.public_net.ipv6.network == "2001:db8::" assert bound_server.public_net.ipv6.network_mask == "64" assert isinstance(bound_server.public_net.firewalls, list) assert isinstance(bound_server.public_net.firewalls[0], PublicNetworkFirewall) firewall = bound_server.public_net.firewalls[0] assert isinstance(firewall.firewall, BoundFirewall) assert bound_server.public_net.ipv6.blocked is False assert firewall.status == PublicNetworkFirewall.STATUS_APPLIED assert isinstance(bound_server.public_net.floating_ips[0], BoundFloatingIP) assert bound_server.public_net.floating_ips[0].id == 478 assert bound_server.public_net.floating_ips[0].complete is False with pytest.deprecated_call(): datacenter = bound_server.datacenter assert isinstance(datacenter, BoundDatacenter) assert datacenter._client == bound_server._client._parent.datacenters assert datacenter.id == 1 assert datacenter.complete is True assert isinstance(bound_server.server_type, BoundServerType) assert ( bound_server.server_type._client == bound_server._client._parent.server_types ) assert bound_server.server_type.id == 1 assert bound_server.server_type.complete is True assert len(bound_server.volumes) == 2 assert isinstance(bound_server.volumes[0], BoundVolume) assert bound_server.volumes[0]._client == bound_server._client._parent.volumes assert bound_server.volumes[0].id == 1 assert bound_server.volumes[0].complete is False assert isinstance(bound_server.volumes[1], BoundVolume) assert bound_server.volumes[1]._client == bound_server._client._parent.volumes assert bound_server.volumes[1].id == 2 assert bound_server.volumes[1].complete is False assert isinstance(bound_server.image, BoundImage) assert bound_server.image._client == bound_server._client._parent.images assert bound_server.image.id == 4711 assert bound_server.image.name == "ubuntu-20.04" assert bound_server.image.complete is True assert isinstance(bound_server.iso, BoundIso) assert bound_server.iso._client == bound_server._client._parent.isos assert bound_server.iso.id == 4711 assert bound_server.iso.name == "FreeBSD-11.0-RELEASE-amd64-dvd1" assert bound_server.iso.complete is True assert len(bound_server.private_net) == 1 assert isinstance(bound_server.private_net[0], PrivateNet) assert ( bound_server.private_net[0].network._client == bound_server._client._parent.networks ) assert bound_server.private_net[0].ip == "10.1.1.5" assert bound_server.private_net[0].mac_address == "86:00:ff:2a:7d:e1" assert len(bound_server.private_net[0].alias_ips) == 1 assert bound_server.private_net[0].alias_ips[0] == "10.1.1.8" assert isinstance(bound_server.placement_group, BoundPlacementGroup) assert ( bound_server.placement_group._client == bound_server._client._parent.placement_groups ) assert bound_server.placement_group.id == 897 assert bound_server.placement_group.name == "my Placement Group" assert bound_server.placement_group.complete is True class TestServersClient: @pytest.fixture() def servers_client(self, client: Client): return ServersClient(client) def test_get_by_id( self, request_mock: mock.MagicMock, servers_client: ServersClient, response_simple_server, ): request_mock.return_value = response_simple_server bound_server = servers_client.get_by_id(1) request_mock.assert_called_with( method="GET", url="/servers/1", ) assert bound_server._client is servers_client assert bound_server.id == 1 assert bound_server.name == "my-server" @pytest.mark.parametrize( "params", [ {"name": "server1", "label_selector": "label1", "page": 1, "per_page": 10}, {"name": ""}, {}, ], ) def test_get_list( self, request_mock: mock.MagicMock, servers_client: ServersClient, response_simple_servers, params, ): request_mock.return_value = response_simple_servers result = servers_client.get_list(**params) request_mock.assert_called_with( method="GET", url="/servers", params=params, ) bound_servers = result.servers assert result.meta is not None assert len(bound_servers) == 2 bound_server1 = bound_servers[0] bound_server2 = bound_servers[1] assert bound_server1._client is servers_client assert bound_server1.id == 1 assert bound_server1.name == "my-server" assert bound_server2._client is servers_client assert bound_server2.id == 2 assert bound_server2.name == "my-server2" @pytest.mark.parametrize( "params", [{"name": "server1", "label_selector": "label1"}, {}] ) def test_get_all( self, request_mock: mock.MagicMock, servers_client: ServersClient, response_simple_servers, params, ): request_mock.return_value = response_simple_servers bound_servers = servers_client.get_all(**params) params.update({"page": 1, "per_page": 50}) request_mock.assert_called_with( method="GET", url="/servers", params=params, ) assert len(bound_servers) == 2 bound_server1 = bound_servers[0] bound_server2 = bound_servers[1] assert bound_server1._client is servers_client assert bound_server1.id == 1 assert bound_server1.name == "my-server" assert bound_server2._client is servers_client assert bound_server2.id == 2 assert bound_server2.name == "my-server2" def test_get_by_name( self, request_mock: mock.MagicMock, servers_client: ServersClient, response_simple_servers, ): request_mock.return_value = response_simple_servers bound_server = servers_client.get_by_name("my-server") params = {"name": "my-server"} request_mock.assert_called_with( method="GET", url="/servers", params=params, ) assert bound_server._client is servers_client assert bound_server.id == 1 assert bound_server.name == "my-server" def test_create_with_datacenter( self, request_mock: mock.MagicMock, servers_client: ServersClient, response_create_simple_server, ): request_mock.return_value = response_create_simple_server with pytest.deprecated_call(): response = servers_client.create( "my-server", server_type=ServerType(name="cx11"), image=Image(id=4711), datacenter=Datacenter(id=1), ) request_mock.assert_called_with( method="POST", url="/servers", json={ "name": "my-server", "server_type": "cx11", "image": 4711, "datacenter": 1, "start_after_create": True, }, ) 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._parent.actions assert bound_action.id == 1 assert bound_action.command == "create_server" def test_create_with_location( self, request_mock: mock.MagicMock, servers_client: ServersClient, response_create_simple_server, ): request_mock.return_value = response_create_simple_server response = servers_client.create( "my-server", server_type=ServerType(name="cx11"), image=Image(name="ubuntu-20.04"), location=Location(name="fsn1"), ) request_mock.assert_called_with( method="POST", url="/servers", json={ "name": "my-server", "server_type": "cx11", "image": "ubuntu-20.04", "location": "fsn1", "start_after_create": True, }, ) 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._parent.actions assert bound_action.id == 1 assert bound_action.command == "create_server" def test_create_with_volumes( self, request_mock: mock.MagicMock, servers_client: ServersClient, response_create_simple_server, ): request_mock.return_value = response_create_simple_server volumes = [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=2))] response = servers_client.create( "my-server", server_type=ServerType(name="cx11"), image=Image(id=4711), volumes=volumes, start_after_create=False, ) request_mock.assert_called_with( method="POST", url="/servers", json={ "name": "my-server", "server_type": "cx11", "image": 4711, "volumes": [1, 2], "start_after_create": False, }, ) bound_server = response.server bound_action = response.action next_actions = response.next_actions root_password = response.root_password assert root_password == "YItygq1v3GYjjMomLaKc" 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._parent.actions assert bound_action.id == 1 assert bound_action.command == "create_server" assert next_actions[0].id == 13 def test_create_with_networks( self, request_mock: mock.MagicMock, servers_client: ServersClient, response_create_simple_server, ): request_mock.return_value = response_create_simple_server networks = [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=2))] response = servers_client.create( "my-server", server_type=ServerType(name="cx11"), image=Image(id=4711), networks=networks, start_after_create=False, ) request_mock.assert_called_with( method="POST", url="/servers", json={ "name": "my-server", "server_type": "cx11", "image": 4711, "networks": [1, 2], "start_after_create": False, }, ) bound_server = response.server bound_action = response.action next_actions = response.next_actions root_password = response.root_password assert root_password == "YItygq1v3GYjjMomLaKc" 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._parent.actions assert bound_action.id == 1 assert bound_action.command == "create_server" assert next_actions[0].id == 13 def test_create_with_firewalls( self, request_mock: mock.MagicMock, servers_client: ServersClient, response_create_simple_server, ): request_mock.return_value = response_create_simple_server firewalls = [Firewall(id=1), BoundFirewall(mock.MagicMock(), dict(id=2))] response = servers_client.create( "my-server", server_type=ServerType(name="cx11"), image=Image(id=4711), firewalls=firewalls, start_after_create=False, ) request_mock.assert_called_with( method="POST", url="/servers", json={ "name": "my-server", "server_type": "cx11", "image": 4711, "firewalls": [{"firewall": 1}, {"firewall": 2}], "start_after_create": False, }, ) bound_server = response.server bound_action = response.action next_actions = response.next_actions root_password = response.root_password assert root_password == "YItygq1v3GYjjMomLaKc" 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._parent.actions assert bound_action.id == 1 assert bound_action.command == "create_server" assert next_actions[0].id == 13 def test_create_with_placement_group( self, request_mock: mock.MagicMock, servers_client: ServersClient, response_create_simple_server, ): request_mock.return_value = response_create_simple_server placement_group = PlacementGroup(id=1) response = servers_client.create( "my-server", server_type=ServerType(name="cx11"), image=Image(id=4711), start_after_create=False, placement_group=placement_group, ) request_mock.assert_called_with( method="POST", url="/servers", json={ "name": "my-server", "server_type": "cx11", "image": 4711, "placement_group": 1, "start_after_create": False, }, ) bound_server = response.server bound_action = response.action next_actions = response.next_actions root_password = response.root_password assert root_password == "YItygq1v3GYjjMomLaKc" 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._parent.actions assert bound_action.id == 1 assert bound_action.command == "create_server" assert next_actions[0].id == 13 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_update( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, response_update_server, ): request_mock.return_value = response_update_server server = servers_client.update(server, name="new-name", labels={}) request_mock.assert_called_with( method="PUT", url="/servers/1", json={"name": "new-name", "labels": {}}, ) assert server.id == 14 assert server.name == "new-name" @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_delete( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, action_response, ): request_mock.return_value = action_response action = servers_client.delete(server) request_mock.assert_called_with( method="DELETE", url="/servers/1", ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_power_off( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, action_response, ): request_mock.return_value = action_response action = servers_client.power_off(server) request_mock.assert_called_with( method="POST", url="/servers/1/actions/poweroff", ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_power_on( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, action_response, ): request_mock.return_value = action_response action = servers_client.power_on(server) request_mock.assert_called_with( method="POST", url="/servers/1/actions/poweron", ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_reboot( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, action_response, ): request_mock.return_value = action_response action = servers_client.reboot(server) request_mock.assert_called_with( method="POST", url="/servers/1/actions/reboot", ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_reset( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, action_response, ): request_mock.return_value = action_response action = servers_client.reset(server) request_mock.assert_called_with( method="POST", url="/servers/1/actions/reset", ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_shutdown( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, action_response, ): request_mock.return_value = action_response action = servers_client.shutdown(server) request_mock.assert_called_with( method="POST", url="/servers/1/actions/shutdown", ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_reset_password( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, response_server_reset_password, ): request_mock.return_value = response_server_reset_password response = servers_client.reset_password(server) request_mock.assert_called_with( method="POST", url="/servers/1/actions/reset_password", ) assert response.action.id == 1 assert response.action.progress == 0 assert response.root_password == "YItygq1v3GYjjMomLaKc" @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_change_type_with_server_type_name( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, action_response, ): request_mock.return_value = action_response action = servers_client.change_type( server, ServerType(name="cx11"), upgrade_disk=True ) request_mock.assert_called_with( method="POST", url="/servers/1/actions/change_type", json={"server_type": "cx11", "upgrade_disk": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_change_type_with_server_type_id( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, action_response, ): request_mock.return_value = action_response action = servers_client.change_type(server, ServerType(id=1), upgrade_disk=True) request_mock.assert_called_with( method="POST", url="/servers/1/actions/change_type", json={"server_type": 1, "upgrade_disk": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_change_type_with_blank_server_type( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, ): with pytest.raises(ValueError) as e: servers_client.change_type(server, ServerType(), upgrade_disk=True) assert str(e.value) == "id or name must be set" request_mock.assert_not_called() @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_enable_rescue( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, response_server_enable_rescue, ): request_mock.return_value = response_server_enable_rescue response = servers_client.enable_rescue(server, "linux64", [2323]) request_mock.assert_called_with( method="POST", url="/servers/1/actions/enable_rescue", json={"type": "linux64", "ssh_keys": [2323]}, ) assert response.action.id == 1 assert response.action.progress == 0 assert response.root_password == "YItygq1v3GYjjMomLaKc" @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_disable_rescue( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, action_response, ): request_mock.return_value = action_response action = servers_client.disable_rescue(server) request_mock.assert_called_with( method="POST", url="/servers/1/actions/disable_rescue", ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_create_image( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, response_server_create_image, ): request_mock.return_value = response_server_create_image response = servers_client.create_image( server, description="my image", type="snapshot", labels={"key": "value"} ) request_mock.assert_called_with( method="POST", url="/servers/1/actions/create_image", json={ "description": "my image", "type": "snapshot", "labels": {"key": "value"}, }, ) assert response.action.id == 1 assert response.action.progress == 0 assert response.image.description == "my image" @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_rebuild( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, action_response, ): request_mock.return_value = action_response response = servers_client.rebuild( server, Image(name="ubuntu-20.04"), return_response=True, ) request_mock.assert_called_with( method="POST", url="/servers/1/actions/rebuild", json={"image": "ubuntu-20.04"}, ) assert response.action.id == 1 assert response.action.progress == 0 assert response.root_password is None or isinstance(response.root_password, str) @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_enable_backup( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, action_response, ): request_mock.return_value = action_response action = servers_client.enable_backup(server) request_mock.assert_called_with( method="POST", url="/servers/1/actions/enable_backup", ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_disable_backup( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, action_response, ): request_mock.return_value = action_response action = servers_client.disable_backup(server) request_mock.assert_called_with( method="POST", url="/servers/1/actions/disable_backup", ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_attach_iso( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, action_response, ): request_mock.return_value = action_response action = servers_client.attach_iso( server, Iso(name="FreeBSD-11.0-RELEASE-amd64-dvd1") ) request_mock.assert_called_with( method="POST", url="/servers/1/actions/attach_iso", json={"iso": "FreeBSD-11.0-RELEASE-amd64-dvd1"}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_detach_iso( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, action_response, ): request_mock.return_value = action_response action = servers_client.detach_iso(server) request_mock.assert_called_with( method="POST", url="/servers/1/actions/detach_iso", ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_change_dns_ptr( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, action_response, ): request_mock.return_value = action_response action = servers_client.change_dns_ptr(server, "1.2.3.4", "example.com") request_mock.assert_called_with( method="POST", url="/servers/1/actions/change_dns_ptr", json={"ip": "1.2.3.4", "dns_ptr": "example.com"}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_change_protection( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, action_response, ): request_mock.return_value = action_response action = servers_client.change_protection(server, True, True) request_mock.assert_called_with( method="POST", url="/servers/1/actions/change_protection", json={"delete": True, "rebuild": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_request_console( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, response_server_request_console, ): request_mock.return_value = response_server_request_console response = servers_client.request_console(server) request_mock.assert_called_with( method="POST", url="/servers/1/actions/request_console", ) assert response.action.id == 1 assert response.action.progress == 0 assert ( response.wss_url == "wss://console.hetzner.cloud/?server_id=1&token=3db32d15-af2f-459c-8bf8-dee1fd05f49c" ) assert response.password == "9MQaTg2VAGI0FIpc10k3UpRXcHj2wQ6x" @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) @pytest.mark.parametrize( "network", [Network(id=4711), BoundNetwork(mock.MagicMock(), dict(id=4711))] ) def test_attach_to_network( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, network, response_attach_to_network, ): request_mock.return_value = response_attach_to_network action = servers_client.attach_to_network( server, network, "10.0.1.1", ["10.0.1.2", "10.0.1.3"] ) request_mock.assert_called_with( method="POST", url="/servers/1/actions/attach_to_network", json={ "network": 4711, "ip": "10.0.1.1", "alias_ips": ["10.0.1.2", "10.0.1.3"], }, ) assert action.id == 1 assert action.progress == 0 assert action.command == "attach_to_network" @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) @pytest.mark.parametrize( "network", [Network(id=4711), BoundNetwork(mock.MagicMock(), dict(id=4711))] ) def test_detach_from_network( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, network, response_detach_from_network, ): request_mock.return_value = response_detach_from_network action = servers_client.detach_from_network(server, network) request_mock.assert_called_with( method="POST", url="/servers/1/actions/detach_from_network", json={"network": 4711}, ) assert action.id == 1 assert action.progress == 0 assert action.command == "detach_from_network" @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) @pytest.mark.parametrize( "network", [Network(id=4711), BoundNetwork(mock.MagicMock(), dict(id=4711))] ) def test_change_alias_ips( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, network, response_change_alias_ips, ): request_mock.return_value = response_change_alias_ips action = servers_client.change_alias_ips( server, network, ["10.0.1.2", "10.0.1.3"] ) request_mock.assert_called_with( method="POST", url="/servers/1/actions/change_alias_ips", json={"network": 4711, "alias_ips": ["10.0.1.2", "10.0.1.3"]}, ) assert action.id == 1 assert action.progress == 0 assert action.command == "change_alias_ips" @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) @pytest.mark.parametrize( "placement_group", [PlacementGroup(id=897), BoundPlacementGroup(mock.MagicMock, dict(id=897))], ) def test_add_to_placement_group( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, placement_group, response_add_to_placement_group, ): request_mock.return_value = response_add_to_placement_group action = servers_client.add_to_placement_group(server, placement_group) request_mock.assert_called_with( method="POST", url="/servers/1/actions/add_to_placement_group", json={"placement_group": 897}, ) assert action.id == 13 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_remove_from_placement_group( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, response_remove_from_placement_group, ): request_mock.return_value = response_remove_from_placement_group action = servers_client.remove_from_placement_group(server) request_mock.assert_called_with( method="POST", url="/servers/1/actions/remove_from_placement_group", ) assert action.id == 13 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_get_metrics( self, request_mock: mock.MagicMock, servers_client: ServersClient, server, response_get_metrics, ): request_mock.return_value = response_get_metrics response = servers_client.get_metrics( server, type=["cpu", "disk"], start="2023-12-14T17:40:00+01:00", end="2023-12-14T17:50:00+01:00", ) request_mock.assert_called_with( method="GET", url="/servers/1/metrics", 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/servers/test_domain.py0000644000175100017510000000511715152343177021233 0ustar00runnerrunnerfrom __future__ import annotations import datetime from datetime import timezone from unittest import mock import pytest from hcloud.networks import Network from hcloud.servers import ( BoundServer, IPv4Address, IPv6Network, PrivateNet, PublicNetwork, PublicNetworkFirewall, Server, ServerCreatePublicNetwork, ) @pytest.mark.parametrize( "value", [ (Server(id=1),), ( PublicNetwork( ipv4=None, ipv6=None, floating_ips=[], primary_ipv4=None, primary_ipv6=None, ), ), (PublicNetworkFirewall(firewall=object(), status="pending"),), (IPv4Address(ip="127.0.0.1", blocked=False, dns_ptr="example.com"),), (IPv6Network("2001:0db8::0/64", blocked=False, dns_ptr="example.com"),), (PrivateNet(network=object(), ip="127.0.0.1", alias_ips=[], mac_address=""),), (ServerCreatePublicNetwork(),), ], ) def test_eq(value): assert value.__eq__(value) 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=timezone.utc ) def test_private_net_for(self): network1 = Network(id=1) network2 = Network(id=2) network3 = Network(id=3) server = Server( id=42, private_net=[ PrivateNet( network=network1, ip="127.0.0.1", alias_ips=[], mac_address="" ), PrivateNet( network=network2, ip="127.0.0.1", alias_ips=[], mac_address="" ), ], ) assert server.private_net_for(network1).network.id == 1 assert server.private_net_for(network3) is None server = BoundServer( client=mock.MagicMock(), data={ "id": 42, "private_net": [ { "network": 1, "ip": "127.0.0.1", "alias_ips": [], "mac_address": "", }, { "network": 2, "ip": "127.0.0.1", "alias_ips": [], "mac_address": "", }, ], }, ) assert server.private_net_for(network1).network.id == 1 assert server.private_net_for(network3) is None ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1595116 hcloud-2.17.0/tests/unit/ssh_keys/0000755000175100017510000000000015152343221016474 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/ssh_keys/__init__.py0000644000175100017510000000000015152343177020605 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/ssh_keys/conftest.py0000644000175100017510000000352415152343177020711 0ustar00runnerrunnerfrom __future__ import annotations import pytest @pytest.fixture() def ssh_key_response(): return { "ssh_key": { "id": 2323, "name": "My ssh key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f", "public_key": "ssh-rsa AAAjjk76kgf...Xt", "labels": {}, "created": "2016-01-30T23:50:00+00:00", } } @pytest.fixture() def two_ssh_keys_response(): return { "ssh_keys": [ { "id": 2323, "name": "SSH-Key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f", "public_key": "ssh-rsa AAAjjk76kgf...Xt", "labels": {}, "created": "2016-01-30T23:50:00+00:00", }, { "id": 2324, "name": "SSH-Key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f", "public_key": "ssh-rsa AAAjjk76kgf...Xt", "labels": {}, "created": "2016-01-30T23:50:00+00:00", }, ] } @pytest.fixture() def one_ssh_keys_response(): return { "ssh_keys": [ { "id": 2323, "name": "SSH-Key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f", "public_key": "ssh-rsa AAAjjk76kgf...Xt", "labels": {}, } ] } @pytest.fixture() def response_update_ssh_key(): return { "ssh_key": { "id": 2323, "name": "New name", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f", "public_key": "ssh-rsa AAAjjk76kgf...Xt", "labels": {}, "created": "2016-01-30T23:50:00+00:00", } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/ssh_keys/test_client.py0000644000175100017510000001527515152343177021407 0ustar00runnerrunnerfrom __future__ import annotations from unittest import mock import pytest from hcloud import Client from hcloud.ssh_keys import BoundSSHKey, SSHKey, SSHKeysClient from ..conftest import BoundModelTestCase class TestBoundSSHKey(BoundModelTestCase): methods = [ BoundSSHKey.update, BoundSSHKey.delete, ] @pytest.fixture() def resource_client(self, client: Client) -> SSHKeysClient: return client.ssh_keys @pytest.fixture() def bound_model(self, resource_client: SSHKeysClient) -> BoundSSHKey: return BoundSSHKey(resource_client, data=dict(id=14)) def test_init(self, ssh_key_response): bound_ssh_key = BoundSSHKey( client=mock.MagicMock(), data=ssh_key_response["ssh_key"] ) assert bound_ssh_key.id == 2323 assert bound_ssh_key.name == "My ssh key" assert ( bound_ssh_key.fingerprint == "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f" ) assert bound_ssh_key.public_key == "ssh-rsa AAAjjk76kgf...Xt" class TestSSHKeysClient: @pytest.fixture() def ssh_keys_client(self, client: Client): return SSHKeysClient(client) def test_get_by_id( self, request_mock: mock.MagicMock, ssh_keys_client: SSHKeysClient, ssh_key_response, ): request_mock.return_value = ssh_key_response ssh_key = ssh_keys_client.get_by_id(1) request_mock.assert_called_with( method="GET", url="/ssh_keys/1", ) assert ssh_key._client is ssh_keys_client assert ssh_key.id == 2323 assert ssh_key.name == "My ssh key" @pytest.mark.parametrize( "params", [ { "name": "My ssh key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f", "label_selector": "k==v", "page": 1, "per_page": 10, }, {"name": ""}, {}, ], ) def test_get_list( self, request_mock: mock.MagicMock, ssh_keys_client: SSHKeysClient, two_ssh_keys_response, params, ): request_mock.return_value = two_ssh_keys_response result = ssh_keys_client.get_list(**params) request_mock.assert_called_with( method="GET", url="/ssh_keys", params=params, ) ssh_keys = result.ssh_keys assert len(ssh_keys) == 2 ssh_keys1 = ssh_keys[0] ssh_keys2 = ssh_keys[1] assert ssh_keys1._client is ssh_keys_client assert ssh_keys1.id == 2323 assert ssh_keys1.name == "SSH-Key" assert ssh_keys2._client is ssh_keys_client assert ssh_keys2.id == 2324 assert ssh_keys2.name == "SSH-Key" @pytest.mark.parametrize( "params", [{"name": "My ssh key", "label_selector": "label1"}, {}] ) def test_get_all( self, request_mock: mock.MagicMock, ssh_keys_client: SSHKeysClient, two_ssh_keys_response, params, ): request_mock.return_value = two_ssh_keys_response ssh_keys = ssh_keys_client.get_all(**params) params.update({"page": 1, "per_page": 50}) request_mock.assert_called_with( method="GET", url="/ssh_keys", params=params, ) assert len(ssh_keys) == 2 ssh_keys1 = ssh_keys[0] ssh_keys2 = ssh_keys[1] assert ssh_keys1._client is ssh_keys_client assert ssh_keys1.id == 2323 assert ssh_keys1.name == "SSH-Key" assert ssh_keys2._client is ssh_keys_client assert ssh_keys2.id == 2324 assert ssh_keys2.name == "SSH-Key" def test_get_by_name( self, request_mock: mock.MagicMock, ssh_keys_client: SSHKeysClient, one_ssh_keys_response, ): request_mock.return_value = one_ssh_keys_response ssh_keys = ssh_keys_client.get_by_name("SSH-Key") params = {"name": "SSH-Key"} request_mock.assert_called_with( method="GET", url="/ssh_keys", params=params, ) assert ssh_keys._client is ssh_keys_client assert ssh_keys.id == 2323 assert ssh_keys.name == "SSH-Key" def test_get_by_fingerprint( self, request_mock: mock.MagicMock, ssh_keys_client: SSHKeysClient, one_ssh_keys_response, ): request_mock.return_value = one_ssh_keys_response ssh_keys = ssh_keys_client.get_by_fingerprint( "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f" ) params = {"fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f"} request_mock.assert_called_with( method="GET", url="/ssh_keys", params=params, ) assert ssh_keys._client is ssh_keys_client assert ssh_keys.id == 2323 assert ssh_keys.name == "SSH-Key" def test_create( self, request_mock: mock.MagicMock, ssh_keys_client: SSHKeysClient, ssh_key_response, ): request_mock.return_value = ssh_key_response ssh_key = ssh_keys_client.create( name="My ssh key", public_key="ssh-rsa AAAjjk76kgf...Xt" ) request_mock.assert_called_with( method="POST", url="/ssh_keys", json={"name": "My ssh key", "public_key": "ssh-rsa AAAjjk76kgf...Xt"}, ) assert ssh_key.id == 2323 assert ssh_key.name == "My ssh key" @pytest.mark.parametrize( "ssh_key", [SSHKey(id=1), BoundSSHKey(mock.MagicMock(), dict(id=1))] ) def test_update( self, request_mock: mock.MagicMock, ssh_keys_client: SSHKeysClient, ssh_key, response_update_ssh_key, ): request_mock.return_value = response_update_ssh_key ssh_key = ssh_keys_client.update(ssh_key, name="New name") request_mock.assert_called_with( method="PUT", url="/ssh_keys/1", json={"name": "New name"}, ) assert ssh_key.id == 2323 assert ssh_key.name == "New name" @pytest.mark.parametrize( "ssh_key", [SSHKey(id=1), BoundSSHKey(mock.MagicMock(), dict(id=1))] ) def test_delete( self, request_mock: mock.MagicMock, ssh_keys_client: SSHKeysClient, ssh_key, action_response, ): request_mock.return_value = action_response delete_success = ssh_keys_client.delete(ssh_key) request_mock.assert_called_with( method="DELETE", url="/ssh_keys/1", ) assert delete_success is True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/ssh_keys/test_domain.py0000644000175100017510000000077215152343177021374 0ustar00runnerrunnerfrom __future__ import annotations import datetime from datetime import timezone import pytest from hcloud.ssh_keys import SSHKey @pytest.mark.parametrize( "value", [ (SSHKey(id=1),), ], ) def test_eq(value): assert value.__eq__(value) class TestSSHKey: def test_created_is_datetime(self): ssh_key = SSHKey(id=1, created="2016-01-30T23:50+00:00") assert ssh_key.created == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1601489 hcloud-2.17.0/tests/unit/storage_box_types/0000755000175100017510000000000015152343221020404 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/storage_box_types/__init__.py0000644000175100017510000000000015152343177022515 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/storage_box_types/conftest.py0000644000175100017510000000254515152343177022623 0ustar00runnerrunnerfrom __future__ import annotations import pytest @pytest.fixture() def storage_box_type1(): return { "id": 42, "name": "bx11", "description": "BX11", "snapshot_limit": 10, "automatic_snapshot_limit": 10, "subaccounts_limit": 100, "size": 1099511627776, "prices": [ { "location": "fsn1", "price_hourly": {"gross": "0.0051", "net": "0.0051"}, "price_monthly": {"gross": "3.2000", "net": "3.2000"}, "setup_fee": {"gross": "0.0000", "net": "0.0000"}, } ], "deprecation": { "unavailable_after": "2023-09-01T00:00:00+00:00", "announced": "2023-06-01T00:00:00+00:00", }, } @pytest.fixture() def storage_box_type2(): return { "id": 43, "name": "bx21", "description": "BX21", "snapshot_limit": 20, "automatic_snapshot_limit": 20, "subaccounts_limit": 100, "size": 5497558138880, "prices": [ { "location": "fsn1", "price_hourly": {"net": "1.0000", "gross": "1.1900"}, "price_monthly": {"net": "1.0000", "gross": "1.1900"}, "setup_fee": {"net": "1.0000", "gross": "1.1900"}, } ], "deprecation": None, } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/storage_box_types/test_client.py0000644000175100017510000001055315152343177023311 0ustar00runnerrunner# pylint: disable=protected-access from __future__ import annotations from unittest import mock import pytest from dateutil.parser import isoparse from hcloud import Client from hcloud.storage_box_types import ( BoundStorageBoxType, StorageBoxTypesClient, ) def assert_bound_model( o: BoundStorageBoxType, client: StorageBoxTypesClient, ): assert isinstance(o, BoundStorageBoxType) assert o._client is client assert o.id == 42 assert o.name == "bx11" class TestClient: @pytest.fixture() def resource_client(self, client: Client) -> StorageBoxTypesClient: return client.storage_box_types def test_get_by_id( self, request_mock: mock.MagicMock, resource_client: StorageBoxTypesClient, storage_box_type1, ): request_mock.return_value = {"storage_box_type": storage_box_type1} result = resource_client.get_by_id(42) request_mock.assert_called_with( method="GET", url="/storage_box_types/42", ) assert_bound_model(result, resource_client) assert result.description == "BX11" assert result.snapshot_limit == 10 assert result.automatic_snapshot_limit == 10 assert result.subaccounts_limit == 100 assert result.size == 1099511627776 assert result.prices == [ { "location": "fsn1", "price_hourly": {"gross": "0.0051", "net": "0.0051"}, "price_monthly": {"gross": "3.2000", "net": "3.2000"}, "setup_fee": {"gross": "0.0000", "net": "0.0000"}, } ] assert result.deprecation.announced == isoparse("2023-06-01T00:00:00+00:00") assert result.deprecation.unavailable_after == isoparse( "2023-09-01T00:00:00+00:00" ) @pytest.mark.parametrize( "params", [ {"name": "bx11", "page": 1, "per_page": 10}, {}, ], ) def test_get_list( self, request_mock: mock.MagicMock, resource_client: StorageBoxTypesClient, storage_box_type1, storage_box_type2, params, ): request_mock.return_value = { "storage_box_types": [storage_box_type1, storage_box_type2] } result = resource_client.get_list(**params) request_mock.assert_called_with( url="/storage_box_types", method="GET", params=params, ) assert result.meta is not None assert len(result.storage_box_types) == 2 result1 = result.storage_box_types[0] result2 = result.storage_box_types[1] assert result1._client is resource_client assert result1.id == 42 assert result1.name == "bx11" assert result2._client is resource_client assert result2.id == 43 assert result2.name == "bx21" @pytest.mark.parametrize( "params", [ {"name": "bx11"}, {}, ], ) def test_get_all( self, request_mock: mock.MagicMock, resource_client: StorageBoxTypesClient, storage_box_type1, storage_box_type2, params, ): request_mock.return_value = { "storage_box_types": [storage_box_type1, storage_box_type2] } result = resource_client.get_all(**params) request_mock.assert_called_with( url="/storage_box_types", method="GET", params={**params, "page": 1, "per_page": 50}, ) assert len(result) == 2 result1 = result[0] result2 = result[1] assert result1._client is resource_client assert result1.id == 42 assert result1.name == "bx11" assert result2._client is resource_client assert result2.id == 43 assert result2.name == "bx21" def test_get_by_name( self, request_mock: mock.MagicMock, resource_client: StorageBoxTypesClient, storage_box_type1, ): request_mock.return_value = {"storage_box_types": [storage_box_type1]} result = resource_client.get_by_name("bx11") params = {"name": "bx11"} request_mock.assert_called_with( method="GET", url="/storage_box_types", params=params, ) assert_bound_model(result, resource_client) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/storage_box_types/test_domain.py0000644000175100017510000000036315152343177023300 0ustar00runnerrunnerfrom __future__ import annotations import pytest from hcloud.storage_box_types import StorageBoxType @pytest.mark.parametrize( "value", [ (StorageBoxType(id=1),), ], ) def test_eq(value): assert value.__eq__(value) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1607637 hcloud-2.17.0/tests/unit/storage_boxes/0000755000175100017510000000000015152343221017510 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/storage_boxes/__init__.py0000644000175100017510000000000015152343177021621 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/storage_boxes/conftest.py0000644000175100017510000001011615152343177021720 0ustar00runnerrunnerfrom __future__ import annotations import pytest @pytest.fixture() def storage_box1(): return { "id": 42, "name": "storage-box1", "created": "2025-01-30T23:55:00+00:00", "status": "active", "system": "FSN1-BX355", "server": "u1337.your-storagebox.de", "username": "u12345", "storage_box_type": { "id": 42, "name": "bx11", }, "location": { "id": 1, "name": "fsn1", }, "access_settings": { "reachable_externally": False, "samba_enabled": False, "ssh_enabled": False, "webdav_enabled": False, "zfs_enabled": False, }, "snapshot_plan": { "max_snapshots": 20, "minute": 0, "hour": 7, "day_of_week": 7, "day_of_month": None, }, "stats": { "size": 2342236717056, "size_data": 2102612983808, "size_snapshots": 239623733248, }, "labels": { "key": "value", }, "protection": {"delete": False}, } @pytest.fixture() def storage_box2(): return { "id": 43, "name": "storage-box2", "created": "2022-09-30T10:30:09.000Z", "status": "active", "system": "FSN1-BX355", "server": "u1337.your-storagebox.de", "username": "u12345", "storage_box_type": { "id": 1334, "name": "bx21", }, "location": { "id": 1, "name": "fsn1", }, "access_settings": { "webdav_enabled": False, "zfs_enabled": False, "samba_enabled": False, "ssh_enabled": True, "reachable_externally": True, }, "snapshot_plan": { "max_snapshots": 20, "minute": 0, "hour": 7, "day_of_week": 7, "day_of_month": None, }, "stats": { "size": 2342236717056, "size_data": 2102612983808, "size_snapshots": 239623733248, }, "labels": {}, "protection": {"delete": False}, } @pytest.fixture() def storage_box_snapshot1(): return { "id": 34, "name": "storage-box-snapshot1", "description": "", "is_automatic": False, "stats": { "size": 394957594, "size_filesystem": 3949572745, }, "labels": { "key": "value", }, "created": "2025-11-10T19:16:57Z", "storage_box": 42, } @pytest.fixture() def storage_box_snapshot2(): return { "id": 35, "name": "storage-box-snapshot2", "description": "", "is_automatic": True, "stats": { "size": 0, "size_filesystem": 0, }, "labels": {}, "created": "2025-11-10T19:18:57Z", "storage_box": 42, } @pytest.fixture() def storage_box_subaccount1(): return { "id": 45, "username": "u42-sub1", "server": "u42-sub1.your-storagebox.de", "home_directory": "tmp/", "description": "Required by foo", "access_settings": { "samba_enabled": False, "ssh_enabled": True, "webdav_enabled": False, "reachable_externally": True, "readonly": False, }, "labels": { "key": "value", }, "created": "2025-11-10T19:18:57Z", "storage_box": 42, } @pytest.fixture() def storage_box_subaccount2(): return { "id": 46, "username": "u42-sub2", "server": "u42-sub2.your-storagebox.de", "home_directory": "backup/", "description": "", "access_settings": { "samba_enabled": False, "ssh_enabled": True, "webdav_enabled": False, "reachable_externally": True, "readonly": False, }, "labels": {}, "created": "2025-11-10T19:18:57Z", "storage_box": 42, } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/storage_boxes/test_client.py0000644000175100017510000010662415152343177022422 0ustar00runnerrunner# pylint: disable=protected-access from __future__ import annotations from unittest import mock import pytest from dateutil.parser import isoparse from hcloud import Client from hcloud.locations import Location from hcloud.storage_box_types import StorageBoxType from hcloud.storage_boxes import ( BoundStorageBox, BoundStorageBoxSnapshot, BoundStorageBoxSubaccount, StorageBox, StorageBoxAccessSettings, StorageBoxesClient, StorageBoxSnapshot, StorageBoxSnapshotPlan, StorageBoxSubaccount, StorageBoxSubaccountAccessSettings, ) from ..conftest import BoundModelTestCase, assert_bound_action1 def assert_bound_storage_box( o: BoundStorageBox, resource_client: StorageBoxesClient, ): assert isinstance(o, BoundStorageBox) assert o._client is resource_client assert o.id == 42 assert o.name == "storage-box1" def assert_bound_storage_box_snapshot( o: BoundStorageBoxSnapshot, resource_client: StorageBoxesClient, ): assert isinstance(o, BoundStorageBoxSnapshot) assert o._client is resource_client assert o.id == 34 assert o.name == "storage-box-snapshot1" def assert_bound_storage_box_subaccount( o: BoundStorageBoxSubaccount, resource_client: StorageBoxesClient, ): assert isinstance(o, BoundStorageBoxSubaccount) assert o._client is resource_client assert o.id == 45 assert o.username == "u42-sub1" class TestBoundStorageBox(BoundModelTestCase): methods = [ BoundStorageBox.update, BoundStorageBox.delete, BoundStorageBox.get_folders, BoundStorageBox.change_protection, BoundStorageBox.change_type, BoundStorageBox.disable_snapshot_plan, BoundStorageBox.enable_snapshot_plan, BoundStorageBox.reset_password, BoundStorageBox.rollback_snapshot, BoundStorageBox.update_access_settings, # Snapshots BoundStorageBox.create_snapshot, BoundStorageBox.get_snapshot_all, BoundStorageBox.get_snapshot_by_id, BoundStorageBox.get_snapshot_by_name, BoundStorageBox.get_snapshot_list, # Subaccounts BoundStorageBox.create_subaccount, BoundStorageBox.get_subaccount_all, BoundStorageBox.get_subaccount_by_id, BoundStorageBox.get_subaccount_by_name, BoundStorageBox.get_subaccount_by_username, BoundStorageBox.get_subaccount_list, ] @pytest.fixture() def resource_client(self, client: Client) -> StorageBoxesClient: return client.storage_boxes @pytest.fixture() def bound_model( self, resource_client: StorageBoxesClient, storage_box1, ) -> BoundStorageBox: return BoundStorageBox(resource_client, data=storage_box1) def test_init(self, bound_model: BoundStorageBox, resource_client): o = bound_model assert_bound_storage_box(o, resource_client) assert o.storage_box_type.id == 42 assert o.storage_box_type.name == "bx11" assert o.location.id == 1 assert o.location.name == "fsn1" assert o.system == "FSN1-BX355" assert o.server == "u1337.your-storagebox.de" assert o.username == "u12345" assert o.labels == {"key": "value"} assert o.protection == {"delete": False} assert o.snapshot_plan.max_snapshots == 20 assert o.snapshot_plan.minute == 0 assert o.snapshot_plan.hour == 7 assert o.snapshot_plan.day_of_week == 7 assert o.snapshot_plan.day_of_month is None assert o.access_settings.reachable_externally is False assert o.access_settings.samba_enabled is False assert o.access_settings.ssh_enabled is False assert o.access_settings.webdav_enabled is False assert o.access_settings.zfs_enabled is False assert o.stats.size == 2342236717056 assert o.stats.size_data == 2102612983808 assert o.stats.size_snapshots == 239623733248 assert o.status == "active" assert o.created == isoparse("2025-01-30T23:55:00Z") class TestBoundStorageBoxSnapshot(BoundModelTestCase): methods = [ (BoundStorageBoxSnapshot.update, {"client_method": "update_snapshot"}), (BoundStorageBoxSnapshot.delete, {"client_method": "delete_snapshot"}), ] @pytest.fixture() def resource_client(self, client: Client) -> StorageBoxesClient: return client.storage_boxes @pytest.fixture() def bound_model( self, resource_client: StorageBoxesClient, storage_box_snapshot1, ) -> BoundStorageBoxSnapshot: return BoundStorageBoxSnapshot(resource_client, data=storage_box_snapshot1) def test_init(self, bound_model: BoundStorageBoxSnapshot, resource_client): o = bound_model assert_bound_storage_box_snapshot(o, resource_client) assert isinstance(o.storage_box, BoundStorageBox) assert o.storage_box.id == 42 assert o.description == "" assert o.is_automatic is False assert o.labels == {"key": "value"} assert o.stats.size == 394957594 assert o.stats.size_filesystem == 3949572745 assert o.created == isoparse("2025-11-10T19:16:57Z") def test_reload( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box_snapshot1, ): o = BoundStorageBoxSnapshot(resource_client, data={"id": 34, "storage_box": 42}) request_mock.return_value = {"snapshot": storage_box_snapshot1} o.reload() request_mock.assert_called_with( method="GET", url="/storage_boxes/42/snapshots/34", ) assert o.labels is not None class TestBoundStorageBoxSubaccount(BoundModelTestCase): methods = [ ( BoundStorageBoxSubaccount.update, {"client_method": "update_subaccount"}, ), ( BoundStorageBoxSubaccount.delete, {"client_method": "delete_subaccount"}, ), ( BoundStorageBoxSubaccount.change_home_directory, {"client_method": "change_subaccount_home_directory"}, ), ( BoundStorageBoxSubaccount.reset_password, {"client_method": "reset_subaccount_password"}, ), ( BoundStorageBoxSubaccount.update_access_settings, {"client_method": "update_subaccount_access_settings"}, ), ] @pytest.fixture() def resource_client(self, client: Client) -> StorageBoxesClient: return client.storage_boxes @pytest.fixture() def bound_model( self, resource_client: StorageBoxesClient, storage_box_subaccount1, ) -> BoundStorageBoxSubaccount: return BoundStorageBoxSubaccount(resource_client, data=storage_box_subaccount1) def test_init(self, bound_model: BoundStorageBoxSubaccount, resource_client): o = bound_model assert_bound_storage_box_subaccount(o, resource_client) assert isinstance(o.storage_box, BoundStorageBox) assert o.storage_box.id == 42 assert o.username == "u42-sub1" assert o.description == "Required by foo" assert o.server == "u42-sub1.your-storagebox.de" assert o.home_directory == "tmp/" assert o.access_settings.reachable_externally is True assert o.access_settings.samba_enabled is False assert o.access_settings.ssh_enabled is True assert o.access_settings.webdav_enabled is False assert o.access_settings.readonly is False assert o.labels == {"key": "value"} assert o.created == isoparse("2025-11-10T19:18:57Z") def test_reload( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box_subaccount1, ): o = BoundStorageBoxSubaccount( resource_client, data={"id": 45, "storage_box": 42} ) request_mock.return_value = {"subaccount": storage_box_subaccount1} o.reload() request_mock.assert_called_with( method="GET", url="/storage_boxes/42/subaccounts/45", ) assert o.labels is not None class TestStorageBoxClient: @pytest.fixture() def resource_client(self, client: Client) -> StorageBoxesClient: return client.storage_boxes def test_get_by_id( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box1, ): request_mock.return_value = {"storage_box": storage_box1} result = resource_client.get_by_id(42) request_mock.assert_called_with( method="GET", url="/storage_boxes/42", ) assert_bound_storage_box(result, resource_client) @pytest.mark.parametrize( "params", [ {"name": "storage-box1"}, {"label_selector": "key=value"}, {"page": 1, "per_page": 10}, {"sort": ["id:asc"]}, {}, ], ) def test_get_list( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box1, storage_box2, params, ): request_mock.return_value = {"storage_boxes": [storage_box1, storage_box2]} result = resource_client.get_list(**params) request_mock.assert_called_with( url="/storage_boxes", method="GET", params=params, ) assert result.meta is not None assert len(result.storage_boxes) == 2 result1 = result.storage_boxes[0] result2 = result.storage_boxes[1] assert result1._client is resource_client assert result1.id == 42 assert result1.name == "storage-box1" assert result2._client is resource_client assert result2.id == 43 assert result2.name == "storage-box2" @pytest.mark.parametrize( "params", [ {"name": "storage-box1"}, {"label_selector": "key=value"}, {"sort": ["id:asc"]}, {}, ], ) def test_get_all( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box1, storage_box2, params, ): request_mock.return_value = {"storage_boxes": [storage_box1, storage_box2]} result = resource_client.get_all(**params) request_mock.assert_called_with( url="/storage_boxes", method="GET", params={**params, "page": 1, "per_page": 50}, ) assert len(result) == 2 result1 = result[0] result2 = result[1] assert result1._client is resource_client assert result1.id == 42 assert result1.name == "storage-box1" assert result2._client is resource_client assert result2.id == 43 assert result2.name == "storage-box2" def test_get_by_name( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box1, ): request_mock.return_value = {"storage_boxes": [storage_box1]} result = resource_client.get_by_name("bx11") params = {"name": "bx11"} request_mock.assert_called_with( method="GET", url="/storage_boxes", params=params, ) assert_bound_storage_box(result, resource_client) def test_create( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box1, action1_running, ): request_mock.return_value = { "storage_box": storage_box1, "action": action1_running, } result = resource_client.create( name="storage-box1", password="secret-password", location=Location(name="fsn1"), storage_box_type=StorageBoxType(name="bx11"), ssh_keys=[], access_settings=StorageBoxAccessSettings( reachable_externally=True, ssh_enabled=True, samba_enabled=False, ), labels={"key": "value"}, ) request_mock.assert_called_with( method="POST", url="/storage_boxes", json={ "name": "storage-box1", "password": "secret-password", "location": "fsn1", "storage_box_type": "bx11", "ssh_keys": [], "access_settings": { "reachable_externally": True, "samba_enabled": False, "ssh_enabled": True, }, "labels": {"key": "value"}, }, ) assert_bound_storage_box(result.storage_box, resource_client) def test_update( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box1, ): request_mock.return_value = { "storage_box": storage_box1, } result = resource_client.update( StorageBox(id=42), name="name", labels={"key": "value"}, ) request_mock.assert_called_with( method="PUT", url="/storage_boxes/42", json={ "name": "name", "labels": {"key": "value"}, }, ) assert_bound_storage_box(result, resource_client) def test_delete( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, action1_running, ): request_mock.return_value = { "action": action1_running, } result = resource_client.delete(StorageBox(id=42)) request_mock.assert_called_with( method="DELETE", url="/storage_boxes/42", ) assert_bound_action1(result.action, resource_client._parent.actions) @pytest.mark.parametrize( "params", [ {"path": "dir1/path"}, {}, ], ) def test_get_folders( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, params, ): request_mock.return_value = { "folders": ["dir1", "dir2"], } result = resource_client.get_folders(StorageBox(id=42), **params) request_mock.assert_called_with( method="GET", url="/storage_boxes/42/folders", params=params ) assert result.folders == ["dir1", "dir2"] def test_change_protection( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, action_response, ): request_mock.return_value = action_response action = resource_client.change_protection(StorageBox(id=42), delete=True) request_mock.assert_called_with( method="POST", url="/storage_boxes/42/actions/change_protection", json={"delete": True}, ) assert_bound_action1(action, resource_client._parent.actions) def test_change_type( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, action_response, ): request_mock.return_value = action_response action = resource_client.change_type( StorageBox(id=42), StorageBoxType(name="bx21"), ) request_mock.assert_called_with( method="POST", url="/storage_boxes/42/actions/change_type", json={"storage_box_type": "bx21"}, ) assert_bound_action1(action, resource_client._parent.actions) def test_reset_password( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, action_response, ): request_mock.return_value = action_response action = resource_client.reset_password( StorageBox(id=42), password="password", ) request_mock.assert_called_with( method="POST", url="/storage_boxes/42/actions/reset_password", json={"password": "password"}, ) assert_bound_action1(action, resource_client._parent.actions) def test_update_access_settings( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, action_response, ): request_mock.return_value = action_response action = resource_client.update_access_settings( StorageBox(id=42), StorageBoxAccessSettings( reachable_externally=True, ssh_enabled=True, webdav_enabled=False, ), ) request_mock.assert_called_with( method="POST", url="/storage_boxes/42/actions/update_access_settings", json={ "reachable_externally": True, "ssh_enabled": True, "webdav_enabled": False, }, ) assert_bound_action1(action, resource_client._parent.actions) def test_rollback_snapshot( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, action_response, ): request_mock.return_value = action_response action = resource_client.rollback_snapshot( StorageBox(id=42), StorageBoxSnapshot(id=32), ) request_mock.assert_called_with( method="POST", url="/storage_boxes/42/actions/rollback_snapshot", json={"snapshot": 32}, ) assert_bound_action1(action, resource_client._parent.actions) def test_disable_snapshot_plan( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, action_response, ): request_mock.return_value = action_response action = resource_client.disable_snapshot_plan( StorageBox(id=42), ) request_mock.assert_called_with( method="POST", url="/storage_boxes/42/actions/disable_snapshot_plan", ) assert_bound_action1(action, resource_client._parent.actions) def test_enable_snapshot_plan( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, action_response, ): request_mock.return_value = action_response action = resource_client.enable_snapshot_plan( StorageBox(id=42), StorageBoxSnapshotPlan( max_snapshots=10, hour=3, minute=30, day_of_week=None, ), ) request_mock.assert_called_with( method="POST", url="/storage_boxes/42/actions/enable_snapshot_plan", json={ "max_snapshots": 10, "hour": 3, "minute": 30, "day_of_week": None, "day_of_month": None, }, ) assert_bound_action1(action, resource_client._parent.actions) # Snapshots ########################################################################### def test_get_snapshot_by_id( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box_snapshot1, ): request_mock.return_value = {"snapshot": storage_box_snapshot1} result = resource_client.get_snapshot_by_id(StorageBox(42), 34) request_mock.assert_called_with( method="GET", url="/storage_boxes/42/snapshots/34", ) assert_bound_storage_box_snapshot(result, resource_client) @pytest.mark.parametrize( "params", [ {"name": "storage-box-snapshot1"}, {"is_automatic": True}, {"label_selector": "key=value"}, # {"page": 1, "per_page": 10} # No pagination {"sort": ["id:asc"]}, {}, ], ) def test_get_snapshot_list( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box_snapshot1, storage_box_snapshot2, params, ): request_mock.return_value = { "snapshots": [storage_box_snapshot1, storage_box_snapshot2] } result = resource_client.get_snapshot_list(StorageBox(42), **params) request_mock.assert_called_with( url="/storage_boxes/42/snapshots", method="GET", params=params, ) assert result.meta is not None assert len(result.snapshots) == 2 result1 = result.snapshots[0] result2 = result.snapshots[1] assert result1._client is resource_client assert result1.id == 34 assert result1.name == "storage-box-snapshot1" assert isinstance(result1.storage_box, BoundStorageBox) assert result1.storage_box.id == 42 assert result2._client is resource_client assert result2.id == 35 assert result2.name == "storage-box-snapshot2" assert isinstance(result2.storage_box, BoundStorageBox) assert result2.storage_box.id == 42 @pytest.mark.parametrize( "params", [ {"name": "storage-box-snapshot1"}, {"is_automatic": True}, {"label_selector": "key=value"}, {"sort": ["id:asc"]}, {}, ], ) def test_get_snapshot_all( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box_snapshot1, storage_box_snapshot2, params, ): request_mock.return_value = { "snapshots": [storage_box_snapshot1, storage_box_snapshot2] } result = resource_client.get_snapshot_all(StorageBox(42), **params) request_mock.assert_called_with( url="/storage_boxes/42/snapshots", method="GET", params=params, ) assert len(result) == 2 result1 = result[0] result2 = result[1] assert result1._client is resource_client assert result1.id == 34 assert result1.name == "storage-box-snapshot1" assert isinstance(result1.storage_box, BoundStorageBox) assert result1.storage_box.id == 42 assert result2._client is resource_client assert result2.id == 35 assert result2.name == "storage-box-snapshot2" assert isinstance(result2.storage_box, BoundStorageBox) assert result2.storage_box.id == 42 def test_get_snapshot_by_name( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box_snapshot1, ): request_mock.return_value = {"snapshots": [storage_box_snapshot1]} result = resource_client.get_snapshot_by_name( StorageBox(42), "storage-box-snapshot1" ) request_mock.assert_called_with( method="GET", url="/storage_boxes/42/snapshots", params={"name": "storage-box-snapshot1"}, ) assert_bound_storage_box_snapshot(result, resource_client) def test_create_snapshot( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box_snapshot1: dict, action1_running, ): request_mock.return_value = { "snapshot": { # Only a partial object is returned key: storage_box_snapshot1[key] for key in ["id", "storage_box"] }, "action": action1_running, } result = resource_client.create_snapshot( StorageBox(42), description="something", labels={"key": "value"}, ) request_mock.assert_called_with( method="POST", url="/storage_boxes/42/snapshots", json={ "description": "something", "labels": {"key": "value"}, }, ) assert isinstance(result.snapshot, BoundStorageBoxSnapshot) assert result.snapshot._client is resource_client assert result.snapshot.id == 34 assert_bound_action1(result.action, resource_client._parent.actions) def test_update_snapshot( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box_snapshot1, ): request_mock.return_value = { "snapshot": storage_box_snapshot1, } result = resource_client.update_snapshot( StorageBoxSnapshot(id=34, storage_box=StorageBox(42)), description="something", labels={"key": "value"}, ) request_mock.assert_called_with( method="PUT", url="/storage_boxes/42/snapshots/34", json={ "description": "something", "labels": {"key": "value"}, }, ) assert_bound_storage_box_snapshot(result, resource_client) def test_delete_snapshot( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, action1_running, ): request_mock.return_value = { "action": action1_running, } result = resource_client.delete_snapshot( StorageBoxSnapshot(id=34, storage_box=StorageBox(42)) ) request_mock.assert_called_with( method="DELETE", url="/storage_boxes/42/snapshots/34", ) assert_bound_action1(result.action, resource_client._parent.actions) # Subaccounts ########################################################################### def test_get_subaccount_by_id( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box_subaccount1, ): request_mock.return_value = {"subaccount": storage_box_subaccount1} result = resource_client.get_subaccount_by_id(StorageBox(42), 45) request_mock.assert_called_with( method="GET", url="/storage_boxes/42/subaccounts/45", ) assert_bound_storage_box_subaccount(result, resource_client) def test_get_subaccount_by_name( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box_subaccount1, ): request_mock.return_value = {"subaccounts": [storage_box_subaccount1]} result = resource_client.get_subaccount_by_name(StorageBox(42), "subaccount1") request_mock.assert_called_with( method="GET", url="/storage_boxes/42/subaccounts", params={"name": "subaccount1"}, ) assert_bound_storage_box_subaccount(result, resource_client) @pytest.mark.parametrize( "params", [ {"name": "subaccount1"}, {"username": "u42-sub1"}, {"label_selector": "key=value"}, # {"page": 1, "per_page": 10} # No pagination {"sort": ["id:asc"]}, {}, ], ) def test_get_subaccount_list( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box_subaccount1, storage_box_subaccount2, params, ): request_mock.return_value = { "subaccounts": [storage_box_subaccount1, storage_box_subaccount2] } result = resource_client.get_subaccount_list(StorageBox(42), **params) request_mock.assert_called_with( url="/storage_boxes/42/subaccounts", method="GET", params=params, ) assert result.meta is not None assert len(result.subaccounts) == 2 result1 = result.subaccounts[0] result2 = result.subaccounts[1] assert result1._client is resource_client assert result1.id == 45 assert result1.username == "u42-sub1" assert isinstance(result1.storage_box, BoundStorageBox) assert result1.storage_box.id == 42 assert result2._client is resource_client assert result2.id == 46 assert result2.username == "u42-sub2" assert isinstance(result2.storage_box, BoundStorageBox) assert result2.storage_box.id == 42 @pytest.mark.parametrize( "params", [ {"name": "subaccount1"}, {"username": "u42-sub1"}, {"label_selector": "key=value"}, {"sort": ["id:asc"]}, {}, ], ) def test_get_subaccount_all( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box_subaccount1, storage_box_subaccount2, params, ): request_mock.return_value = { "subaccounts": [storage_box_subaccount1, storage_box_subaccount2] } result = resource_client.get_subaccount_all(StorageBox(42), **params) request_mock.assert_called_with( url="/storage_boxes/42/subaccounts", method="GET", params=params, ) assert len(result) == 2 result1 = result[0] result2 = result[1] assert result1._client is resource_client assert result1.id == 45 assert result1.username == "u42-sub1" assert isinstance(result1.storage_box, BoundStorageBox) assert result1.storage_box.id == 42 assert result2._client is resource_client assert result2.id == 46 assert result2.username == "u42-sub2" assert isinstance(result2.storage_box, BoundStorageBox) assert result2.storage_box.id == 42 def test_get_subaccount_by_username( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box_subaccount1, ): request_mock.return_value = {"subaccounts": [storage_box_subaccount1]} result = resource_client.get_subaccount_by_username(StorageBox(42), "u42-sub1") request_mock.assert_called_with( method="GET", url="/storage_boxes/42/subaccounts", params={"username": "u42-sub1"}, ) assert_bound_storage_box_subaccount(result, resource_client) def test_create_subaccount( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box_subaccount1: dict, action1_running, ): request_mock.return_value = { "subaccount": { # Only a partial object is returned key: storage_box_subaccount1[key] for key in ["id", "storage_box"] }, "action": action1_running, } result = resource_client.create_subaccount( StorageBox(42), name="subaccount1", home_directory="tmp", password="secret", access_settings=StorageBoxSubaccountAccessSettings( reachable_externally=True, ssh_enabled=True, readonly=False, ), description="something", labels={"key": "value"}, ) request_mock.assert_called_with( method="POST", url="/storage_boxes/42/subaccounts", json={ "name": "subaccount1", "home_directory": "tmp", "password": "secret", "access_settings": { "reachable_externally": True, "ssh_enabled": True, "readonly": False, }, "description": "something", "labels": {"key": "value"}, }, ) assert isinstance(result.subaccount, BoundStorageBoxSubaccount) assert result.subaccount._client is resource_client assert result.subaccount.id == 45 assert_bound_action1(result.action, resource_client._parent.actions) def test_update_subaccount( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, storage_box_subaccount1, ): request_mock.return_value = { "subaccount": storage_box_subaccount1, } result = resource_client.update_subaccount( StorageBoxSubaccount(id=45, storage_box=StorageBox(42)), description="something", labels={"key": "value"}, ) request_mock.assert_called_with( method="PUT", url="/storage_boxes/42/subaccounts/45", json={ "description": "something", "labels": {"key": "value"}, }, ) assert_bound_storage_box_subaccount(result, resource_client) def test_delete_subaccount( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, action1_running, ): request_mock.return_value = { "action": action1_running, } result = resource_client.delete_subaccount( StorageBoxSubaccount(id=45, storage_box=StorageBox(42)), ) request_mock.assert_called_with( method="DELETE", url="/storage_boxes/42/subaccounts/45", ) assert_bound_action1(result.action, resource_client._parent.actions) def test_change_subaccount_home_directory( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, action_response, ): request_mock.return_value = action_response action = resource_client.change_subaccount_home_directory( StorageBoxSubaccount(id=45, storage_box=StorageBox(42)), home_directory="path", ) request_mock.assert_called_with( method="POST", url="/storage_boxes/42/subaccounts/45/actions/change_home_directory", json={ "home_directory": "path", }, ) assert_bound_action1(action, resource_client._parent.actions) def test_reset_subaccount_password( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, action_response, ): request_mock.return_value = action_response action = resource_client.reset_subaccount_password( StorageBoxSubaccount(id=45, storage_box=StorageBox(42)), password="password", ) request_mock.assert_called_with( method="POST", url="/storage_boxes/42/subaccounts/45/actions/reset_subaccount_password", json={ "password": "password", }, ) assert_bound_action1(action, resource_client._parent.actions) def test_update_subaccount_access_settings( self, request_mock: mock.MagicMock, resource_client: StorageBoxesClient, action_response, ): request_mock.return_value = action_response action = resource_client.update_subaccount_access_settings( StorageBoxSubaccount(id=45, storage_box=StorageBox(42)), access_settings=StorageBoxSubaccountAccessSettings( reachable_externally=True, ssh_enabled=True, samba_enabled=False, ), ) request_mock.assert_called_with( method="POST", url="/storage_boxes/42/subaccounts/45/actions/update_access_settings", json={ "reachable_externally": True, "ssh_enabled": True, "samba_enabled": False, }, ) assert_bound_action1(action, resource_client._parent.actions) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/storage_boxes/test_domain.py0000644000175100017510000000034715152343177022406 0ustar00runnerrunnerfrom __future__ import annotations import pytest from hcloud.storage_boxes import StorageBox @pytest.mark.parametrize( "value", [ (StorageBox(id=1),), ], ) def test_eq(value): assert value.__eq__(value) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/test_client.py0000644000175100017510000002511415152343177017550 0ustar00runnerrunnerfrom __future__ import annotations from http import HTTPStatus from json import dumps from typing import Any from unittest import mock import pytest import requests from hcloud import ( APIException, Client, constant_backoff_function, exponential_backoff_function, ) from hcloud._client import ClientBase, _build_user_agent def test_exponential_backoff_function(): backoff = exponential_backoff_function( base=1.0, multiplier=2, cap=60.0, ) max_retries = 5 results = [backoff(i) for i in range(max_retries)] assert sum(results) == 31.0 assert results == [1.0, 2.0, 4.0, 8.0, 16.0] def test_constant_backoff_function(): backoff = constant_backoff_function(interval=1.0) max_retries = 5 for i in range(max_retries): assert backoff(i) == 1.0 def test_build_user_agent(): assert _build_user_agent(None, None) == "hcloud-python/0.0.0" assert _build_user_agent("my-app", None) == "my-app hcloud-python/0.0.0" assert _build_user_agent("my-app", "1.0.0") == "my-app/1.0.0 hcloud-python/0.0.0" assert _build_user_agent(None, "1.0.0") == "hcloud-python/0.0.0" class TestClient: @pytest.fixture() def client(self): return Client(token="TOKEN") def test_request(self, client: Client): client._client.request = mock.MagicMock() client.request(method="GET", url="/path") client._client.request.assert_called_once_with("GET", "/path") def make_response( status: HTTPStatus, *, json: Any | None = None, text: str | None = None, ) -> requests.Response: response = requests.Response() response.status_code = status.value response.reason = status.phrase if json is not None: response.headers["Content-type"] = "application/json" response._content = dumps(json).encode("utf-8") elif text is not None: response.headers["Content-type"] = "text/plain" response._content = text.encode("utf-8") return response class TestBaseClient: @pytest.fixture() def client(self): client = ClientBase( token="TOKEN", endpoint="https://api.hetzner.cloud/v1", ) client._session = mock.MagicMock() return client def test_init(self, client: ClientBase): assert client._user_agent == "hcloud-python/0.0.0" assert client._headers == { "User-Agent": "hcloud-python/0.0.0", "Authorization": "Bearer TOKEN", "Accept": "application/json", } assert client._poll_interval_func(1) == 1.0 assert client._retry_interval_func(1) == pytest.approx(1.5, rel=0.5) # Jitter @pytest.mark.parametrize( ("exception", "expected"), [ ( APIException(code="rate_limit_exceeded", message="Error", details=None), True, ), ( APIException(code="conflict", message="Error", details=None), True, ), ( APIException(code=409, message="Conflict", details=None), False, ), ( APIException(code=429, message="Too Many Requests", details=None), False, ), ( APIException(code=502, message="Bad Gateway", details=None), True, ), ( APIException(code=503, message="Service Unavailable", details=None), False, ), ( APIException(code=504, message="Gateway Timeout", details=None), True, ), ], ) def test_retry_policy( self, client: ClientBase, exception: APIException, expected: bool, ): assert client._retry_policy(exception) == expected def test_request_200(self, client: ClientBase): client._session.request.return_value = make_response( status=HTTPStatus.OK, json={"result": "data"}, ) result = client.request( method="POST", url="/path", params={"argument": "value"}, timeout=2, ) client._session.request.assert_called_once_with( method="POST", url="https://api.hetzner.cloud/v1/path", headers={ "User-Agent": "hcloud-python/0.0.0", "Authorization": "Bearer TOKEN", "Accept": "application/json", }, params={"argument": "value"}, timeout=2, ) assert result == {"result": "data"} def test_request_200_empty_content(self, client: ClientBase): client._session.request.return_value = make_response( status=HTTPStatus.OK, text="", ) result = client.request(method="POST", url="/path") assert result == {} def test_request_fail_200_invalid_json(self, client: ClientBase): client._session.request.return_value = make_response( status=HTTPStatus.OK, text="{'key': 'value'", ) with pytest.raises(APIException) as exc: client.request(method="POST", url="/path") assert exc.value.code == 200 assert exc.value.message == "OK" assert exc.value.details["content"] == b"{'key': 'value'" def test_request_fail_422(self, client: ClientBase): client._session.request.return_value = make_response( status=HTTPStatus.UNPROCESSABLE_ENTITY, json={ "error": { "code": "invalid_input", "message": "invalid input in field 'broken_field': is too long", "details": { "fields": [ {"name": "broken_field", "messages": ["is too long"]} ] }, } }, ) with pytest.raises(APIException) as exc: client.request(method="POST", url="/path") assert exc.value.code == "invalid_input" assert exc.value.message == "invalid input in field 'broken_field': is too long" assert exc.value.details["fields"][0]["name"] == "broken_field" def test_request_fail_422_correlation_id(self, client: ClientBase): response = make_response( status=HTTPStatus.UNPROCESSABLE_ENTITY, json={ "error": { "code": "service_error", "message": "Something crashed", } }, ) response.headers["X-Correlation-Id"] = "67ed842dc8bc8673" client._session.request.return_value = response with pytest.raises(APIException) as exc: client.request(method="POST", url="/path") assert exc.value.code == "service_error" assert exc.value.message == "Something crashed" assert exc.value.details is None assert exc.value.correlation_id == "67ed842dc8bc8673" assert str(exc.value) == "Something crashed (service_error, 67ed842dc8bc8673)" def test_request_fail_500(self, client: ClientBase): client._session.request.return_value = make_response( status=HTTPStatus.INTERNAL_SERVER_ERROR, text="Internal Server Error", ) with pytest.raises(APIException) as exc: client.request(method="POST", url="/path") assert exc.value.code == 500 assert exc.value.message == "Internal Server Error" assert exc.value.details["content"] == b"Internal Server Error" def test_request_fail_500_no_content(self, client: ClientBase): client._session.request.return_value = make_response( status=HTTPStatus.INTERNAL_SERVER_ERROR, ) with pytest.raises(APIException) as exc: client.request(method="POST", url="/path") assert exc.value.code == 500 assert exc.value.message == "Internal Server Error" assert exc.value.details["content"] is None assert str(exc.value) == "Internal Server Error (500)" def test_request_fail_419(self, client: ClientBase): client._retry_interval_func = constant_backoff_function(0.0) client._session.request.return_value = make_response( status=HTTPStatus.TOO_MANY_REQUESTS, json={ "error": { "code": "rate_limit_exceeded", "message": "limit of 3600 requests per hour reached", "details": None, } }, ) with pytest.raises(APIException) as exc: client.request(method="POST", url="/path") assert client._session.request.call_count == 6 assert exc.value.code == "rate_limit_exceeded" assert exc.value.message == "limit of 3600 requests per hour reached" def test_request_fail_419_recover(self, client: ClientBase): client._retry_interval_func = constant_backoff_function(0.0) client._session.request.side_effect = [ make_response( status=HTTPStatus.TOO_MANY_REQUESTS, json={ "error": { "code": "rate_limit_exceeded", "message": "limit of 3600 requests per hour reached", "details": None, } }, ), make_response( status=HTTPStatus.OK, json={"result": "data"}, ), ] result = client.request(method="GET", url="/path") assert client._session.request.call_count == 2 assert result == {"result": "data"} def test_request_fail_timeout(self, client: ClientBase): client._retry_interval_func = constant_backoff_function(0.0) client._session.request.side_effect = requests.exceptions.Timeout("timeout") with pytest.raises(requests.exceptions.Timeout) as exc: client.request(method="GET", url="/path") assert str(exc.value) == "timeout" assert client._session.request.call_count == 6 def test_request_fail_timeout_recover(self, client: ClientBase): client._retry_interval_func = constant_backoff_function(0.0) client._session.request.side_effect = [ requests.exceptions.Timeout("timeout"), make_response( status=HTTPStatus.OK, json={"result": "data"}, ), ] result = client.request(method="GET", url="/path") assert client._session.request.call_count == 2 assert result == {"result": "data"} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/test_exceptions.py0000644000175100017510000000310315152343177020445 0ustar00runnerrunnerfrom __future__ import annotations import pytest from hcloud import ( APIException, HCloudException, ) from hcloud.actions import Action, ActionFailedException, ActionTimeoutException running_action = Action( id=12345, command="action_command", status=Action.STATUS_RUNNING, ) failed_action = Action( id=12345, command="action_command", status=Action.STATUS_ERROR, error={"code": "action_failed", "message": "Action failed"}, ) @pytest.mark.parametrize( ("exception", "expected"), [ ( # Should never be raised by itself HCloudException(), "", ), ( # Should never be raised by itself HCloudException("A test error"), "A test error", ), ( APIException(code="conflict", message="API error message", details=None), "API error message (conflict)", ), ( APIException( code="conflict", message="API error message", details=None, correlation_id="fddea8fabd02fb21", ), "API error message (conflict, fddea8fabd02fb21)", ), ( ActionFailedException(failed_action), "The pending action failed: Action failed (action_failed, 12345)", ), ( ActionTimeoutException(running_action), "The pending action timed out (action_command, 12345)", ), ], ) def test_exceptions(exception, expected): assert str(exception) == expected ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1772734097.161404 hcloud-2.17.0/tests/unit/volumes/0000755000175100017510000000000015152343221016336 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/volumes/__init__.py0000644000175100017510000000000015152343177020447 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/volumes/conftest.py0000644000175100017510000001404515152343177020553 0ustar00runnerrunnerfrom __future__ import annotations import pytest @pytest.fixture() def volume_response(): return { "volume": { "id": 1, "created": "2016-01-30T23:50:11+00:00", "name": "database-storage", "server": 12, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "size": 42, "linux_device": "/dev/disk/by-id/scsi-0HC_Volume_4711", "protection": {"delete": False}, "format": "xfs", "labels": {}, "status": "available", } } @pytest.fixture() def two_volumes_response(): return { "volumes": [ { "id": 1, "created": "2016-01-30T23:50:11+00:00", "name": "database-storage", "server": 12, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "size": 42, "linux_device": "/dev/disk/by-id/scsi-0HC_Volume_4711", "protection": {"delete": False}, "format": "xfs", "labels": {}, "status": "available", }, { "id": 2, "created": "2016-01-30T23:50:11+00:00", "name": "vault-storage", "server": 10, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 2", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "size": 42, "linux_device": "/dev/disk/by-id/scsi-0HC_Volume_4711", "protection": {"delete": False}, "format": "xfs", "labels": {}, "status": "available", }, ] } @pytest.fixture() def one_volumes_response(): return { "volumes": [ { "id": 1, "created": "2016-01-30T23:50:11+00:00", "name": "database-storage", "server": 12, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "size": 42, "linux_device": "/dev/disk/by-id/scsi-0HC_Volume_4711", "protection": {"delete": False}, "format": "xfs", "labels": {}, "status": "available", } ] } @pytest.fixture() def volume_create_response(): return { "volume": { "id": 4711, "created": "2016-01-30T23:50:11+00:00", "name": "database-storage", "server": 12, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "size": 42, "linux_device": "/dev/disk/by-id/scsi-0HC_Volume_4711", "protection": {"delete": False}, "format": "xfs", "labels": {}, "status": "available", }, "action": { "id": 13, "command": "create_volume", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, }, "next_actions": [ { "id": 13, "command": "start_server", "status": "running", "progress": 0, "started": "2016-01-30T23:50+00:00", "finished": None, "resources": [{"id": 42, "type": "server"}], "error": {"code": "action_failed", "message": "Action failed"}, } ], } @pytest.fixture() def response_update_volume(): return { "volume": { "id": 4711, "created": "2016-01-30T23:50:11+00:00", "name": "new-name", "server": 12, "location": { "id": 1, "name": "fsn1", "description": "Falkenstein DC Park 1", "country": "DE", "city": "Falkenstein", "latitude": 50.47612, "longitude": 12.370071, }, "format": "xfs", "size": 42, "linux_device": "/dev/disk/by-id/scsi-0HC_Volume_4711", "protection": {"delete": False}, "labels": {}, "status": "available", } } @pytest.fixture() def response_get_actions(): return { "actions": [ { "id": 13, "command": "attach_volume", "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"}, } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/volumes/test_client.py0000644000175100017510000002726215152343177021250 0ustar00runnerrunnerfrom __future__ import annotations from unittest import mock import pytest from dateutil.parser import isoparse from hcloud import Client from hcloud.locations import BoundLocation, Location from hcloud.servers import BoundServer, Server from hcloud.volumes import BoundVolume, Volume, VolumesClient from ..conftest import BoundModelTestCase class TestBoundVolume(BoundModelTestCase): methods = [ BoundVolume.update, BoundVolume.delete, BoundVolume.change_protection, BoundVolume.attach, BoundVolume.detach, BoundVolume.resize, ] @pytest.fixture() def resource_client(self, client: Client): return client.volumes @pytest.fixture() def bound_model(self, resource_client): return BoundVolume(resource_client, data=dict(id=14)) def test_bound_volume_init(self, volume_response): bound_volume = BoundVolume( client=mock.MagicMock(), data=volume_response["volume"] ) assert bound_volume.id == 1 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 assert bound_volume.size == 42 assert bound_volume.linux_device == "/dev/disk/by-id/scsi-0HC_Volume_4711" assert bound_volume.protection == {"delete": False} assert bound_volume.labels == {} assert bound_volume.status == "available" assert isinstance(bound_volume.location, BoundLocation) assert bound_volume.location.id == 1 assert bound_volume.location.name == "fsn1" assert bound_volume.location.description == "Falkenstein DC Park 1" assert bound_volume.location.country == "DE" assert bound_volume.location.city == "Falkenstein" assert bound_volume.location.latitude == 50.47612 assert bound_volume.location.longitude == 12.370071 class TestVolumesClient: @pytest.fixture() def volumes_client(self, client: Client): return VolumesClient(client) def test_get_by_id( self, request_mock: mock.MagicMock, volumes_client: VolumesClient, volume_response, ): request_mock.return_value = volume_response bound_volume = volumes_client.get_by_id(1) request_mock.assert_called_with( method="GET", url="/volumes/1", ) assert bound_volume._client is volumes_client assert bound_volume.id == 1 assert bound_volume.name == "database-storage" @pytest.mark.parametrize( "params", [{"label_selector": "label1", "page": 1, "per_page": 10}, {"name": ""}, {}], ) def test_get_list( self, request_mock: mock.MagicMock, volumes_client: VolumesClient, two_volumes_response, params, ): request_mock.return_value = two_volumes_response result = volumes_client.get_list(**params) request_mock.assert_called_with( method="GET", url="/volumes", params=params, ) bound_volumes = result.volumes assert result.meta is not None assert len(bound_volumes) == 2 bound_volume1 = bound_volumes[0] bound_volume2 = bound_volumes[1] assert bound_volume1._client is volumes_client assert bound_volume1.id == 1 assert bound_volume1.name == "database-storage" assert bound_volume2._client is volumes_client assert bound_volume2.id == 2 assert bound_volume2.name == "vault-storage" @pytest.mark.parametrize("params", [{"label_selector": "label1"}]) def test_get_all( self, request_mock: mock.MagicMock, volumes_client: VolumesClient, two_volumes_response, params, ): request_mock.return_value = two_volumes_response bound_volumes = volumes_client.get_all(**params) params.update({"page": 1, "per_page": 50}) request_mock.assert_called_with( method="GET", url="/volumes", params=params, ) assert len(bound_volumes) == 2 bound_volume1 = bound_volumes[0] bound_volume2 = bound_volumes[1] assert bound_volume1._client is volumes_client assert bound_volume1.id == 1 assert bound_volume1.name == "database-storage" assert bound_volume2._client is volumes_client assert bound_volume2.id == 2 assert bound_volume2.name == "vault-storage" def test_get_by_name( self, request_mock: mock.MagicMock, volumes_client: VolumesClient, one_volumes_response, ): request_mock.return_value = one_volumes_response bound_volume = volumes_client.get_by_name("database-storage") params = {"name": "database-storage"} request_mock.assert_called_with( method="GET", url="/volumes", params=params, ) assert bound_volume._client is volumes_client assert bound_volume.id == 1 assert bound_volume.name == "database-storage" def test_create_with_location( self, request_mock: mock.MagicMock, volumes_client: VolumesClient, volume_create_response, ): request_mock.return_value = volume_create_response response = volumes_client.create( 100, "database-storage", location=Location(name="location"), automount=False, format="xfs", ) request_mock.assert_called_with( method="POST", url="/volumes", json={ "name": "database-storage", "size": 100, "location": "location", "automount": False, "format": "xfs", }, ) bound_volume = response.volume action = response.action next_actions = response.next_actions assert bound_volume._client is volumes_client assert bound_volume.id == 4711 assert bound_volume.name == "database-storage" assert action.id == 13 assert next_actions[0].command == "start_server" @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_create_with_server( self, request_mock: mock.MagicMock, volumes_client: VolumesClient, server, volume_create_response, ): request_mock.return_value = volume_create_response volumes_client.create( size=100, name="database-storage", server=server, automount=False, format="xfs", ) request_mock.assert_called_with( method="POST", url="/volumes", json={ "name": "database-storage", "size": 100, "server": 1, "automount": False, "format": "xfs", }, ) def test_create_negative_size( self, request_mock: mock.MagicMock, volumes_client, ): with pytest.raises(ValueError) as e: volumes_client.create( -100, "database-storage", location=Location(name="location") ) assert str(e.value) == "size must be greater than 0" request_mock.assert_not_called() @pytest.mark.parametrize( "location,server", [(None, None), ("location", Server(id=1))] ) def test_create_wrong_location_server_combination( self, request_mock: mock.MagicMock, volumes_client: VolumesClient, location, server, ): with pytest.raises(ValueError) as e: volumes_client.create( 100, "database-storage", location=location, server=server ) assert str(e.value) == "only one of server or location must be provided" request_mock.assert_not_called() @pytest.mark.parametrize( "volume", [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=1))] ) def test_update( self, request_mock: mock.MagicMock, volumes_client: VolumesClient, volume, response_update_volume, ): request_mock.return_value = response_update_volume volume = volumes_client.update(volume, name="new-name") request_mock.assert_called_with( method="PUT", url="/volumes/1", json={"name": "new-name"}, ) assert volume.id == 4711 assert volume.name == "new-name" @pytest.mark.parametrize( "volume", [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=1))] ) def test_change_protection( self, request_mock: mock.MagicMock, volumes_client: VolumesClient, volume, action_response, ): request_mock.return_value = action_response action = volumes_client.change_protection(volume, True) request_mock.assert_called_with( method="POST", url="/volumes/1/actions/change_protection", json={"delete": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "volume", [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=1))] ) def test_delete( self, request_mock: mock.MagicMock, volumes_client: VolumesClient, volume, action_response, ): request_mock.return_value = action_response delete_success = volumes_client.delete(volume) request_mock.assert_called_with( method="DELETE", url="/volumes/1", ) assert delete_success is True @pytest.mark.parametrize( "server,volume", [ (Server(id=1), Volume(id=12)), ( BoundServer(mock.MagicMock(), dict(id=1)), BoundVolume(mock.MagicMock(), dict(id=12)), ), ], ) def test_attach( self, request_mock: mock.MagicMock, volumes_client: VolumesClient, server, volume, action_response, ): request_mock.return_value = action_response action = volumes_client.attach(volume, server, True) request_mock.assert_called_with( method="POST", url="/volumes/12/actions/attach", json={"server": 1, "automount": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "volume", [Volume(id=12), BoundVolume(mock.MagicMock(), dict(id=12))] ) def test_detach( self, request_mock: mock.MagicMock, volumes_client: VolumesClient, volume, action_response, ): request_mock.return_value = action_response action = volumes_client.detach(volume) request_mock.assert_called_with( method="POST", url="/volumes/12/actions/detach", ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "volume", [Volume(id=12), BoundVolume(mock.MagicMock(), dict(id=12))] ) def test_resize( self, request_mock: mock.MagicMock, volumes_client: VolumesClient, volume, action_response, ): request_mock.return_value = action_response action = volumes_client.resize(volume, 50) request_mock.assert_called_with( method="POST", url="/volumes/12/actions/resize", json={"size": 50}, ) assert action.id == 1 assert action.progress == 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/volumes/test_domain.py0000644000175100017510000000076715152343177021242 0ustar00runnerrunnerfrom __future__ import annotations import datetime from datetime import timezone import pytest from hcloud.volumes import Volume @pytest.mark.parametrize( "value", [ (Volume(id=1),), ], ) def test_eq(value): assert value.__eq__(value) 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=timezone.utc ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1772734097.1618428 hcloud-2.17.0/tests/unit/zones/0000755000175100017510000000000015152343221016002 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/zones/__init__.py0000644000175100017510000000000015152343177020113 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/zones/conftest.py0000644000175100017510000000655215152343177020223 0ustar00runnerrunnerfrom __future__ import annotations import pytest @pytest.fixture() def zone1(): return { "id": 42, "name": "example1.com", "created": "2016-01-30T23:55:00+00:00", "mode": "primary", "ttl": 10800, "protection": { "delete": False, }, "labels": { "key": "value", }, "primary_nameservers": [ {"address": "198.51.100.1", "port": 53}, {"address": "203.0.113.1", "port": 53}, ], "record_count": 0, "status": "ok", "registrar": "hetzner", "authoritative_nameservers": { "assigned": [ "hydrogen.ns.hetzner.com.", "oxygen.ns.hetzner.com.", "helium.ns.hetzner.de.", ], "delegated": [ "hydrogen.ns.hetzner.com.", "oxygen.ns.hetzner.com.", "helium.ns.hetzner.de.", ], "delegation_last_check": "2016-01-30T23:55:00+00:00", "delegation_status": "valid", }, } @pytest.fixture() def zone2(): return { "id": 43, "name": "example2.com", "created": "2016-01-30T23:55:00+00:00", "mode": "secondary", "ttl": 10800, "protection": { "delete": False, }, "labels": { "key": "value", }, "primary_nameservers": [ {"address": "198.51.100.1", "port": 53}, {"address": "203.0.113.1", "port": 53}, ], "record_count": 0, "status": "ok", "registrar": "hetzner", "authoritative_nameservers": { "assigned": [], "delegated": [ "hydrogen.ns.hetzner.com.", "oxygen.ns.hetzner.com.", "helium.ns.hetzner.de.", ], "delegation_last_check": "2016-01-30T23:55:00+00:00", "delegation_status": "valid", }, } @pytest.fixture() def zone_rrset1(): return { "zone": 42, "id": "www/A", "name": "www", "type": "A", "ttl": 3600, "labels": {"key": "value"}, "protection": {"change": False}, "records": [ {"value": "198.51.100.1", "comment": "web server"}, ], } @pytest.fixture() def zone_rrset2(): return { "zone": 42, "id": "blog/A", "name": "blog", "type": "A", "ttl": 3600, "labels": {"key": "value"}, "protection": {"change": False}, "records": [ {"value": "198.51.100.1", "comment": "web server"}, ], } @pytest.fixture() def zone_response(zone1): return {"zone": zone1} @pytest.fixture() def zone_list_response(zone1, zone2): return { "zones": [zone1, zone2], } @pytest.fixture() def zone_create_response(zone1, action1_running): return { "zone": zone1, "action": action1_running, } @pytest.fixture() def zone_rrset_response(zone_rrset1): return { "rrset": zone_rrset1, } @pytest.fixture() def zone_rrset_list_response(zone_rrset1, zone_rrset2): return { "rrsets": [zone_rrset1, zone_rrset2], } @pytest.fixture() def zone_rrset_create_response(zone_rrset1, action1_running): return { "rrset": zone_rrset1, "action": action1_running, } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1772734079.0 hcloud-2.17.0/tests/unit/zones/test_client.py0000644000175100017510000007211715152343177020713 0ustar00runnerrunner# pylint: disable=protected-access from __future__ import annotations from unittest import mock import pytest from dateutil.parser import isoparse from hcloud import Client from hcloud.zones import ( BoundZone, BoundZoneRRSet, Zone, ZoneAuthoritativeNameservers, ZonePrimaryNameserver, ZoneRecord, ZoneRRSet, ZonesClient, ) from ..conftest import BoundModelTestCase, assert_bound_action1 def assert_bound_zone1(o: BoundZone, client: ZonesClient): assert isinstance(o, BoundZone) assert o._client is client assert o.id == 42 assert o.name == "example1.com" def assert_bound_zone2(o: BoundZone, client: ZonesClient): assert isinstance(o, BoundZone) assert o._client is client assert o.id == 43 assert o.name == "example2.com" def assert_bound_zone_rrset1(o: BoundZoneRRSet, client: ZonesClient): assert isinstance(o, BoundZoneRRSet) assert o._client is client assert o.name == "www" assert o.type == "A" assert o.id == "www/A" def assert_bound_zone_rrset2(o: BoundZoneRRSet, client: ZonesClient): assert isinstance(o, BoundZoneRRSet) assert o._client is client assert o.name == "blog" assert o.type == "A" assert o.id == "blog/A" class TestZonesClient: @pytest.fixture() def resource_client(self, client: Client) -> ZonesClient: return client.zones def test_get_using_id( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone_response, ): request_mock.return_value = zone_response result = resource_client.get(42) request_mock.assert_called_with( method="GET", url="/zones/42", ) assert_bound_zone1(result, resource_client) assert result.created == isoparse("2016-01-30T23:55:00+00:00") assert result.mode == "primary" assert result.ttl == 10800 assert result.protection == {"delete": False} assert result.labels == {"key": "value"} assert result.primary_nameservers[0].address == "198.51.100.1" assert result.primary_nameservers[0].port == 53 assert result.primary_nameservers[1].address == "203.0.113.1" assert result.primary_nameservers[1].port == 53 assert ( result.authoritative_nameservers.assigned[0] == "hydrogen.ns.hetzner.com." ) assert ( result.authoritative_nameservers.delegated[0] == "hydrogen.ns.hetzner.com." ) assert result.authoritative_nameservers.delegation_last_check == isoparse( "2016-01-30T23:55:00+00:00" ) assert result.authoritative_nameservers.delegation_status == "valid" assert result.record_count == 0 assert result.status == "ok" assert result.registrar == "hetzner" def test_get_using_name( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone_response, ): request_mock.return_value = zone_response result = resource_client.get("example.com") request_mock.assert_called_with( method="GET", url="/zones/example.com", ) assert_bound_zone1(result, resource_client) @pytest.mark.parametrize( "params", [ {"mode": "primary"}, {"label_selector": "key=value", "page": 2, "per_page": 10, "sort": "id"}, {"name": "example.com"}, {}, ], ) def test_get_list( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone_list_response, params, ): request_mock.return_value = zone_list_response resp = resource_client.get_list(**params) request_mock.assert_called_with( method="GET", url="/zones", params=params, ) assert resp.meta is not None assert len(resp.zones) == 2 assert_bound_zone1(resp.zones[0], resource_client) assert_bound_zone2(resp.zones[1], resource_client) @pytest.mark.parametrize( "params", [ {"label_selector": "key=value"}, {}, ], ) def test_get_all( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone_list_response, params, ): request_mock.return_value = zone_list_response result = resource_client.get_all(**params) request_mock.assert_called_with( method="GET", url="/zones", params={**params, "page": 1, "per_page": 50}, ) assert len(result) == 2 assert_bound_zone1(result[0], resource_client) assert_bound_zone2(result[1], resource_client) def test_create( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone_create_response, ): request_mock.return_value = zone_create_response resp = resource_client.create( name="example.com", mode="primary", ttl=3600, labels={"key": "value"}, primary_nameservers=[ ZonePrimaryNameserver(address="198.51.100.1", port=53), ZonePrimaryNameserver(address="203.0.113.1"), ], rrsets=[ZoneRRSet(name="www", type="A", records=[ZoneRecord("127.0.0.1")])], ) request_mock.assert_called_with( url="/zones", method="POST", json={ "name": "example.com", "mode": "primary", "ttl": 3600, "labels": {"key": "value"}, "primary_nameservers": [ {"address": "198.51.100.1", "port": 53}, {"address": "203.0.113.1"}, ], "rrsets": [ {"name": "www", "type": "A", "records": [{"value": "127.0.0.1"}]} ], }, ) assert_bound_zone1(resp.zone, resource_client) assert_bound_action1(resp.action, resource_client._parent.actions) @pytest.mark.parametrize( "zone", [ Zone(name="example.com"), BoundZone(client=mock.MagicMock(), data={"id": 42}), ], ) def test_update( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone: Zone, zone_response, ): request_mock.return_value = zone_response result = resource_client.update(zone, labels={"key": "new value"}) request_mock.assert_called_with( method="PUT", url=f"/zones/{zone.id_or_name}", json={"labels": {"key": "new value"}}, ) assert_bound_zone1(result, resource_client) @pytest.mark.parametrize( "zone", [ Zone(name="example.com"), BoundZone(client=mock.MagicMock(), data={"id": 42}), ], ) def test_delete( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone: Zone, action_response, ): request_mock.return_value = action_response resp = resource_client.delete(zone) request_mock.assert_called_with( method="DELETE", url=f"/zones/{zone.id_or_name}", ) assert_bound_action1(resp.action, resource_client._parent.actions) @pytest.mark.parametrize( "zone", [ Zone(name="example.com"), BoundZone(client=mock.MagicMock(), data={"id": 42}), ], ) def test_export_zonefile( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone: Zone, ): request_mock.return_value = {"zonefile": "content"} resp = resource_client.export_zonefile(zone) request_mock.assert_called_with( method="GET", url=f"/zones/{zone.id_or_name}/zonefile", ) assert resp.zonefile == "content" @pytest.mark.parametrize( "zone", [ Zone(name="example.com"), BoundZone(client=mock.MagicMock(), data={"id": 42}), ], ) def test_import_zonefile( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone: Zone, action_response, ): request_mock.return_value = action_response action = resource_client.import_zonefile(zone, "content") request_mock.assert_called_with( method="POST", url=f"/zones/{zone.id_or_name}/actions/import_zonefile", json={"zonefile": "content"}, ) assert_bound_action1(action, resource_client._parent.actions) @pytest.mark.parametrize( "zone", [ Zone(name="example.com"), BoundZone(client=mock.MagicMock(), data={"id": 42}), ], ) def test_change_protection( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone: Zone, action_response, ): request_mock.return_value = action_response action = resource_client.change_protection(zone, delete=True) request_mock.assert_called_with( method="POST", url=f"/zones/{zone.id_or_name}/actions/change_protection", json={"delete": True}, ) assert_bound_action1(action, resource_client._parent.actions) @pytest.mark.parametrize( "zone", [ Zone(name="example.com"), BoundZone(client=mock.MagicMock(), data={"id": 42}), ], ) def test_change_primary_nameservers( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone: Zone, action_response, ): request_mock.return_value = action_response action = resource_client.change_primary_nameservers( zone, primary_nameservers=[ ZonePrimaryNameserver(address="198.51.100.1", port=53), ZonePrimaryNameserver(address="203.0.113.1"), ], ) request_mock.assert_called_with( method="POST", url=f"/zones/{zone.id_or_name}/actions/change_primary_nameservers", json={ "primary_nameservers": [ {"address": "198.51.100.1", "port": 53}, {"address": "203.0.113.1"}, ] }, ) assert_bound_action1(action, resource_client._parent.actions) @pytest.mark.parametrize( "zone", [ Zone(name="example.com"), BoundZone(client=mock.MagicMock(), data={"id": 42}), ], ) def test_change_ttl( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone: Zone, action_response, ): request_mock.return_value = action_response action = resource_client.change_ttl(zone, 3600) request_mock.assert_called_with( method="POST", url=f"/zones/{zone.id_or_name}/actions/change_ttl", json={"ttl": 3600}, ) assert_bound_action1(action, resource_client._parent.actions) # ============ RRSETS ============ @pytest.mark.parametrize( "zone", [ Zone(name="example.com"), BoundZone(client=mock.MagicMock(), data={"id": 42}), ], ) def test_get_rrset( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone: Zone, zone_rrset_response, ): request_mock.return_value = zone_rrset_response result = resource_client.get_rrset(zone, "www", "A") request_mock.assert_called_with( method="GET", url=f"/zones/{zone.id_or_name}/rrsets/www/A", ) assert_bound_zone_rrset1(result, resource_client) assert result.ttl == 3600 assert result.protection == {"change": False} assert result.labels == {"key": "value"} assert result.records[0].value == "198.51.100.1" assert result.records[0].comment == "web server" @pytest.mark.parametrize( "zone", [ Zone(name="example.com"), BoundZone(client=mock.MagicMock(), data={"id": 42}), ], ) @pytest.mark.parametrize( "params", [ {"type": ["A"]}, {"label_selector": "key=value", "page": 2, "per_page": 10, "sort": "id"}, {"name": "www"}, {}, ], ) def test_get_rrset_list( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone: Zone, zone_rrset_list_response, params, ): request_mock.return_value = zone_rrset_list_response resp = resource_client.get_rrset_list(zone, **params) request_mock.assert_called_with( method="GET", url=f"/zones/{zone.id_or_name}/rrsets", params=params, ) assert resp.meta is not None assert len(resp.rrsets) == 2 assert_bound_zone_rrset1(resp.rrsets[0], resource_client) assert_bound_zone_rrset2(resp.rrsets[1], resource_client) @pytest.mark.parametrize( "zone", [ Zone(name="example.com"), BoundZone(client=mock.MagicMock(), data={"id": 42}), ], ) @pytest.mark.parametrize( "params", [ {"label_selector": "key=value"}, {}, ], ) def test_get_rrset_all( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone: Zone, zone_rrset_list_response, params, ): request_mock.return_value = zone_rrset_list_response result = resource_client.get_rrset_all(zone, **params) request_mock.assert_called_with( method="GET", url=f"/zones/{zone.id_or_name}/rrsets", params={**params, "page": 1, "per_page": 50}, ) assert len(result) == 2 assert_bound_zone_rrset1(result[0], resource_client) assert_bound_zone_rrset2(result[1], resource_client) @pytest.mark.parametrize( "zone", [ Zone(name="example.com"), BoundZone(client=mock.MagicMock(), data={"id": 42}), ], ) def test_create_rrset( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone: Zone, zone_rrset_create_response, ): request_mock.return_value = zone_rrset_create_response resp = resource_client.create_rrset( zone, name="www", type="A", ttl=3600, labels={"key": "value"}, records=[ ZoneRecord("198.51.100.1", "web server"), ZoneRecord("127.0.0.1"), ], ) request_mock.assert_called_with( method="POST", url=f"/zones/{zone.id_or_name}/rrsets", json={ "name": "www", "type": "A", "ttl": 3600, "labels": {"key": "value"}, "records": [ {"value": "198.51.100.1", "comment": "web server"}, {"value": "127.0.0.1"}, ], }, ) assert_bound_zone_rrset1(resp.rrset, resource_client) assert_bound_action1(resp.action, resource_client._parent.actions) @pytest.mark.parametrize( "zone", [ Zone(name="example.com"), BoundZone(client=mock.MagicMock(), data={"id": 42}), ], ) @pytest.mark.parametrize( "rrset", [ ZoneRRSet(name="www", type="A"), BoundZoneRRSet(client=mock.MagicMock(), data={"id": "www/A"}), ], ) def test_update_rrset( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone: Zone, rrset: ZoneRRSet, zone_rrset_response, ): rrset.zone = zone request_mock.return_value = zone_rrset_response result = resource_client.update_rrset(rrset, labels={"key": "new value"}) request_mock.assert_called_with( method="PUT", url=f"/zones/{zone.id_or_name}/rrsets/www/A", json={"labels": {"key": "new value"}}, ) assert_bound_zone_rrset1(result, resource_client) @pytest.mark.parametrize( "zone", [ Zone(name="example.com"), BoundZone(client=mock.MagicMock(), data={"id": 42}), ], ) @pytest.mark.parametrize( "rrset", [ ZoneRRSet(name="www", type="A"), BoundZoneRRSet(client=mock.MagicMock(), data={"id": "www/A"}), ], ) def test_delete_rrset( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone: Zone, rrset: ZoneRRSet, action_response, ): rrset.zone = zone request_mock.return_value = action_response resp = resource_client.delete_rrset(rrset) request_mock.assert_called_with( method="DELETE", url=f"/zones/{zone.id_or_name}/rrsets/www/A", ) assert_bound_action1(resp.action, resource_client._parent.actions) @pytest.mark.parametrize( "zone", [ Zone(name="example.com"), BoundZone(client=mock.MagicMock(), data={"id": 42}), ], ) @pytest.mark.parametrize( "rrset", [ ZoneRRSet(name="www", type="A"), BoundZoneRRSet(client=mock.MagicMock(), data={"id": "www/A"}), ], ) def test_change_rrset_protection( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone: Zone, rrset: ZoneRRSet, action_response, ): rrset.zone = zone request_mock.return_value = action_response action = resource_client.change_rrset_protection(rrset, change=True) request_mock.assert_called_with( method="POST", url=f"/zones/{zone.id_or_name}/rrsets/{rrset.name}/{rrset.type}/actions/change_protection", json={"change": True}, ) assert_bound_action1(action, resource_client._parent.actions) @pytest.mark.parametrize( "zone", [ Zone(name="example.com"), BoundZone(client=mock.MagicMock(), data={"id": 42}), ], ) @pytest.mark.parametrize( "rrset", [ ZoneRRSet(name="www", type="A"), BoundZoneRRSet(client=mock.MagicMock(), data={"id": "www/A"}), ], ) def test_change_rrset_ttl( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone: Zone, rrset: ZoneRRSet, action_response, ): rrset.zone = zone request_mock.return_value = action_response action = resource_client.change_rrset_ttl(rrset, ttl=3600) request_mock.assert_called_with( method="POST", url=f"/zones/{zone.id_or_name}/rrsets/{rrset.name}/{rrset.type}/actions/change_ttl", json={"ttl": 3600}, ) assert_bound_action1(action, resource_client._parent.actions) @pytest.mark.parametrize( "zone", [ Zone(name="example.com"), BoundZone(client=mock.MagicMock(), data={"id": 42}), ], ) @pytest.mark.parametrize( "rrset", [ ZoneRRSet(name="www", type="A"), BoundZoneRRSet(client=mock.MagicMock(), data={"id": "www/A"}), ], ) def test_add_rrset_records( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone: Zone, rrset: ZoneRRSet, action_response, ): rrset.zone = zone request_mock.return_value = action_response action = resource_client.add_rrset_records( rrset, records=[ ZoneRecord("198.51.100.1", "web server"), ZoneRecord("127.0.0.1"), ], ttl=300, ) request_mock.assert_called_with( method="POST", url=f"/zones/{zone.id_or_name}/rrsets/{rrset.name}/{rrset.type}/actions/add_records", json={ "records": [ {"value": "198.51.100.1", "comment": "web server"}, {"value": "127.0.0.1"}, ], "ttl": 300, }, ) assert_bound_action1(action, resource_client._parent.actions) @pytest.mark.parametrize( "zone", [ Zone(name="example.com"), BoundZone(client=mock.MagicMock(), data={"id": 42}), ], ) @pytest.mark.parametrize( "rrset", [ ZoneRRSet(name="www", type="A"), BoundZoneRRSet(client=mock.MagicMock(), data={"id": "www/A"}), ], ) def test_update_rrset_records( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone: Zone, rrset: ZoneRRSet, action_response, ): rrset.zone = zone request_mock.return_value = action_response action = resource_client.update_rrset_records( rrset, records=[ ZoneRecord("198.51.100.1", "web server"), ZoneRecord("198.51.100.2", ""), ZoneRecord("127.0.0.1"), ], ) request_mock.assert_called_with( method="POST", url=f"/zones/{zone.id_or_name}/rrsets/{rrset.name}/{rrset.type}/actions/update_records", json={ "records": [ {"value": "198.51.100.1", "comment": "web server"}, {"value": "198.51.100.2", "comment": ""}, {"value": "127.0.0.1"}, ], }, ) assert_bound_action1(action, resource_client._parent.actions) @pytest.mark.parametrize( "zone", [ Zone(name="example.com"), BoundZone(client=mock.MagicMock(), data={"id": 42}), ], ) @pytest.mark.parametrize( "rrset", [ ZoneRRSet(name="www", type="A"), BoundZoneRRSet(client=mock.MagicMock(), data={"id": "www/A"}), ], ) def test_remove_rrset_records( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone: Zone, rrset: ZoneRRSet, action_response, ): rrset.zone = zone request_mock.return_value = action_response action = resource_client.remove_rrset_records( rrset, records=[ ZoneRecord("198.51.100.1", "web server"), ZoneRecord("127.0.0.1"), ], ) request_mock.assert_called_with( method="POST", url=f"/zones/{zone.id_or_name}/rrsets/{rrset.name}/{rrset.type}/actions/remove_records", json={ "records": [ {"value": "198.51.100.1", "comment": "web server"}, {"value": "127.0.0.1"}, ] }, ) assert_bound_action1(action, resource_client._parent.actions) @pytest.mark.parametrize( "zone", [ Zone(name="example.com"), BoundZone(client=mock.MagicMock(), data={"id": 42}), ], ) @pytest.mark.parametrize( "rrset", [ ZoneRRSet(name="www", type="A"), BoundZoneRRSet(client=mock.MagicMock(), data={"id": "www/A"}), ], ) def test_set_rrset_records( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone: Zone, rrset: ZoneRRSet, action_response, ): rrset.zone = zone request_mock.return_value = action_response action = resource_client.set_rrset_records( rrset, records=[ ZoneRecord("198.51.100.1", "web server"), ZoneRecord("127.0.0.1"), ], ) request_mock.assert_called_with( method="POST", url=f"/zones/{zone.id_or_name}/rrsets/{rrset.name}/{rrset.type}/actions/set_records", json={ "records": [ {"value": "198.51.100.1", "comment": "web server"}, {"value": "127.0.0.1"}, ] }, ) assert_bound_action1(action, resource_client._parent.actions) class TestBoundZone(BoundModelTestCase): methods = [ BoundZone.update, BoundZone.delete, BoundZone.import_zonefile, BoundZone.export_zonefile, BoundZone.change_primary_nameservers, BoundZone.change_ttl, BoundZone.change_protection, BoundZone.get_rrset_all, BoundZone.get_rrset_list, BoundZone.get_rrset, BoundZone.create_rrset, # With rrset sub resource (BoundZone.update_rrset, {"sub_resource": True}), (BoundZone.delete_rrset, {"sub_resource": True}), (BoundZone.change_rrset_protection, {"sub_resource": True}), (BoundZone.change_rrset_ttl, {"sub_resource": True}), (BoundZone.add_rrset_records, {"sub_resource": True}), (BoundZone.update_rrset_records, {"sub_resource": True}), (BoundZone.remove_rrset_records, {"sub_resource": True}), (BoundZone.set_rrset_records, {"sub_resource": True}), ] @pytest.fixture() def resource_client(self, client: Client): return client.zones @pytest.fixture() def bound_model(self, resource_client: ZonesClient, zone1): return BoundZone(resource_client, data=zone1) def test_init(self, resource_client: ZonesClient, bound_model: BoundZone): o = bound_model assert_bound_zone1(o, resource_client) assert o.id == 42 assert o.name == "example1.com" assert o.created == isoparse("2016-01-30T23:55:00+00:00") assert o.mode == "primary" assert o.ttl == 10800 assert o.protection == {"delete": False} assert o.labels == {"key": "value"} assert len(o.primary_nameservers) == 2 assert isinstance(o.primary_nameservers[0], ZonePrimaryNameserver) assert o.primary_nameservers[0].address == "198.51.100.1" assert o.primary_nameservers[0].port == 53 assert isinstance(o.primary_nameservers[1], ZonePrimaryNameserver) assert o.primary_nameservers[1].address == "203.0.113.1" assert o.primary_nameservers[1].port == 53 assert isinstance(o.authoritative_nameservers, ZoneAuthoritativeNameservers) assert o.authoritative_nameservers.assigned == [ "hydrogen.ns.hetzner.com.", "oxygen.ns.hetzner.com.", "helium.ns.hetzner.de.", ] assert o.authoritative_nameservers.delegated == [ "hydrogen.ns.hetzner.com.", "oxygen.ns.hetzner.com.", "helium.ns.hetzner.de.", ] assert o.authoritative_nameservers.delegation_last_check == isoparse( "2016-01-30T23:55:00+00:00" ) assert o.authoritative_nameservers.delegation_status == "valid" assert o.record_count == 0 assert o.status == "ok" assert o.registrar == "hetzner" class TestBoundZoneRRSet(BoundModelTestCase): methods = [ BoundZoneRRSet.update_rrset, BoundZoneRRSet.delete_rrset, BoundZoneRRSet.change_rrset_protection, BoundZoneRRSet.change_rrset_ttl, BoundZoneRRSet.add_rrset_records, BoundZoneRRSet.update_rrset_records, BoundZoneRRSet.remove_rrset_records, BoundZoneRRSet.set_rrset_records, ] @pytest.fixture() def resource_client(self, client: Client): return client.zones @pytest.fixture() def bound_model(self, resource_client: ZonesClient, zone_rrset1): return BoundZoneRRSet(resource_client, data=zone_rrset1) def test_init(self, resource_client: ZonesClient, bound_model: BoundZoneRRSet): o = bound_model assert_bound_zone_rrset1(o, resource_client) assert o.id == "www/A" assert o.name == "www" assert o.type == "A" assert o.ttl == 3600 assert o.labels == {"key": "value"} assert o.protection == {"change": False} assert len(o.records) == 1 assert isinstance(o.records[0], ZoneRecord) assert o.records[0].value == "198.51.100.1" assert o.records[0].comment == "web server" assert isinstance(o.zone, BoundZone) assert o.zone.id == 42 def test_reload( self, request_mock: mock.MagicMock, resource_client: ZonesClient, zone_rrset1, ): o = BoundZoneRRSet( resource_client, data={"id": "www/A", "zone": 42}, complete=False, ) request_mock.return_value = {"rrset": zone_rrset1} o.reload() request_mock.assert_called_with( method="GET", url="/zones/42/rrsets/www/A", ) assert o.labels is not None