pax_global_header00006660000000000000000000000064147014762250014522gustar00rootroot0000000000000052 comment=55e451366019d6fbe477635b1062d7739d3457e2 hcloud-python-2.3.0/000077500000000000000000000000001470147622500143215ustar00rootroot00000000000000hcloud-python-2.3.0/.flake8000066400000000000000000000000741470147622500154750ustar00rootroot00000000000000[flake8] extend-ignore = E501 extend-exclude = docs hcloud-python-2.3.0/.github/000077500000000000000000000000001470147622500156615ustar00rootroot00000000000000hcloud-python-2.3.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001470147622500200445ustar00rootroot00000000000000hcloud-python-2.3.0/.github/ISSUE_TEMPLATE/Bug_Report.md000066400000000000000000000012451470147622500224400ustar00rootroot00000000000000--- name: 🐛 Bug Report about: If something isn't working as expected 🤔. --- ## Bug Report **Current Behavior** A clear and concise description of the behavior. **Input Code** - REPL or Repo link if applicable: ```python your = "code" + "here" ``` **Expected behavior/code** A clear and concise description of what you expected to happen (or code). **Environment** - Python Version: [e.g. v2.6, v2.7, v3.0] - Hcloud-Python Version: [e.g. v0.1, v1.0] **Possible Solution** **Additional context/Screenshots** Add any other context about the problem here. If applicable, add screenshots to help explain. hcloud-python-2.3.0/.github/ISSUE_TEMPLATE/Feature_Request.md000066400000000000000000000013321470147622500234700ustar00rootroot00000000000000--- name: 🚀 Feature Request about: I have a suggestion (and may want to implement it 🙂)! --- ## Feature Request **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I have an issue when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. Add any considered drawbacks. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Teachability, Documentation, Adoption, Migration Strategy** If you can, explain how users will be able to use this and possibly write out a version the docs. Maybe a screenshot or design? hcloud-python-2.3.0/.github/release-please-config.json000066400000000000000000000005721470147622500227120ustar00rootroot00000000000000{ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", "bootstrap-sha": "155a565bd3dfbce3177554626ccea6ac35807786", "include-component-in-tag": false, "include-v-in-tag": true, "packages": { ".": { "release-type": "python", "package-name": "hcloud", "extra-files": ["hcloud/_version.py"] } } } hcloud-python-2.3.0/.github/release-please-manifest.json000066400000000000000000000000161470147622500232440ustar00rootroot00000000000000{".":"2.3.0"} hcloud-python-2.3.0/.github/workflows/000077500000000000000000000000001470147622500177165ustar00rootroot00000000000000hcloud-python-2.3.0/.github/workflows/lint.yml000066400000000000000000000013261470147622500214110ustar00rootroot00000000000000name: Lint on: push: branches: [main] pull_request: jobs: pre-commit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup python uses: actions/setup-python@v5 with: python-version: 3.x - name: Install dependencies run: pip install pre-commit - name: Run pre-commit run: pre-commit run --all-files --show-diff-on-failure lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup python uses: actions/setup-python@v5 with: python-version: 3.x - name: Install dependencies run: make venv - name: Run lint run: make lint hcloud-python-2.3.0/.github/workflows/release-please.yml000066400000000000000000000006651470147622500233370ustar00rootroot00000000000000name: Release-please on: push: branches: [main] jobs: release-please: # Do not run on forks. if: github.repository == 'hetznercloud/hcloud-python' runs-on: ubuntu-latest steps: - uses: googleapis/release-please-action@v4 with: token: ${{ secrets.HCLOUD_BOT_TOKEN }} config-file: .github/release-please-config.json manifest-file: .github/release-please-manifest.json hcloud-python-2.3.0/.github/workflows/release.yml000066400000000000000000000021571470147622500220660ustar00rootroot00000000000000name: Release on: push: branches: [main] pull_request: release: types: [created] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: 3.x - name: Install dependencies run: pip install build twine - name: Build run: python3 -m build - name: Check run: twine check --strict dist/* - name: Upload packages artifact if: github.event_name == 'release' uses: actions/upload-artifact@v4 with: name: python-packages path: dist/ publish: if: github.event_name == 'release' needs: [build] environment: name: pypi url: https://pypi.org/p/hcloud permissions: id-token: write runs-on: ubuntu-latest steps: - name: Download packages artifact uses: actions/download-artifact@v4 with: name: python-packages path: dist/ - name: Publish packages to PyPI uses: pypa/gh-action-pypi-publish@v1.10.3 hcloud-python-2.3.0/.github/workflows/stale.yml000066400000000000000000000003251470147622500215510ustar00rootroot00000000000000name: Close stale issues on: schedule: - cron: "30 12 * * *" jobs: stale: permissions: issues: write pull-requests: write uses: hetznercloud/.github/.github/workflows/stale.yml@main hcloud-python-2.3.0/.github/workflows/test.yml000066400000000000000000000015261470147622500214240ustar00rootroot00000000000000name: Test on: push: branches: [main] pull_request: jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] name: Python ${{ matrix.python-version }} steps: - uses: actions/checkout@v4 - name: Setup python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: pip install tox tox-gh-actions - name: Run tox run: tox -- --cov --cov-report=xml - name: Upload coverage reports to Codecov if: > !startsWith(github.head_ref, 'renovate/') && !startsWith(github.head_ref, 'release-please--') uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} hcloud-python-2.3.0/.gitignore000066400000000000000000000106221470147622500163120ustar00rootroot00000000000000 # Created by https://www.gitignore.io/api/vim,osx,python,windows,pycharm+all,visualstudiocode ### OSX ### # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### PyCharm+all ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. # .idea/modules.xml # .idea/*.iml # .idea/modules # CMake cmake-build-*/ # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser ### PyCharm+all Patch ### # Ignores the whole .idea folder and all .iml files # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 .idea/ # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 *.iml modules.xml .idea/misc.xml *.ipr ### Python ### # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json ### Python Patch ### .venv/ ### Python.VirtualEnv Stack ### # Virtualenv # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ [Bb]in [Ii]nclude [Ll]ib [Ll]ib64 [Ll]ocal [Ss]cripts pyvenv.cfg pip-selfcheck.json ### Vim ### # Swap [._]*.s[a-v][a-z] [._]*.sw[a-p] [._]s[a-rt-v][a-z] [._]ss[a-gi-z] [._]sw[a-p] # Session Session.vim # Temporary .netrwhist *~ # Auto-generated tag files tags # Persistent undo [._]*.un~ ### VisualStudioCode ### .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json ### Windows ### # Windows thumbnail cache files Thumbs.db ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msix *.msm *.msp # Windows shortcuts *.lnk # End of https://www.gitignore.io/api/vim,osx,python,windows,pycharm+all,visualstudiocode hcloud-python-2.3.0/.gitlab-ci.yml000066400000000000000000000011071470147622500167540ustar00rootroot00000000000000include: - project: cloud/integrations/ci file: - default.yml - pre-commit.yml - workflows/feature-branches.yml stages: - test pre-commit: extends: [.pre-commit] lint: stage: test image: python:3.13-alpine before_script: - apk add make bash - make venv script: - make lint test: stage: test parallel: matrix: - python_version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] image: python:${python_version}-alpine before_script: - apk add make - pip install tox script: - tox -e ${python_version} hcloud-python-2.3.0/.pre-commit-config.yaml000066400000000000000000000023041470147622500206010ustar00rootroot00000000000000--- # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: check-added-large-files - id: check-case-conflict - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable - id: check-symlinks - id: destroyed-symlinks - id: check-json - id: check-yaml - id: check-toml - id: check-merge-conflict - id: end-of-file-fixer - id: mixed-line-ending args: [--fix=lf] - id: trailing-whitespace - repo: https://github.com/pre-commit/mirrors-prettier rev: v3.1.0 hooks: - id: prettier files: \.(md|ya?ml|js|css)$ exclude: ^CHANGELOG.md$ - repo: https://github.com/asottile/pyupgrade rev: v3.17.0 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/pycqa/isort rev: 5.13.2 hooks: - id: isort - repo: https://github.com/psf/black-pre-commit-mirror rev: 24.10.0 hooks: - id: black - repo: https://github.com/pycqa/flake8 rev: 7.1.1 hooks: - id: flake8 hcloud-python-2.3.0/.readthedocs.yaml000066400000000000000000000012121470147622500175440ustar00rootroot00000000000000--- # Read the Docs configuration file for Sphinx projects # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the OS, Python version and other tools you might need build: os: ubuntu-22.04 tools: python: "3.11" # Build documentation in the "docs/" directory with Sphinx sphinx: configuration: docs/conf.py fail_on_warning: true # Optionally build your docs in additional formats such as PDF and ePub formats: [pdf, epub] # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - method: pip path: . extra_requirements: [docs] hcloud-python-2.3.0/CHANGELOG.md000066400000000000000000001250011470147622500161310ustar00rootroot00000000000000# Changelog ## [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/latest/ ## 0.1.0 (2018-12-20) - First release on GitHub. hcloud-python-2.3.0/CONTRIBUTING.rst000066400000000000000000000051551470147622500167700ustar00rootroot00000000000000============ 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. hcloud-python-2.3.0/LICENSE000066400000000000000000000020641470147622500153300ustar00rootroot00000000000000MIT 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. hcloud-python-2.3.0/MANIFEST.in000066400000000000000000000004331470147622500160570ustar00rootroot00000000000000include 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 hcloud-python-2.3.0/Makefile000066400000000000000000000013031470147622500157560ustar00rootroot00000000000000SHELL := bash .PHONY: test coverage docs clean venv: python3 -m venv venv venv/bin/pip install -e .[docs,test] lint: venv venv/bin/pylint hcloud venv/bin/mypy hcloud test: venv venv/bin/pytest -v coverage: venv venv/bin/coverage run -m pytest -v venv/bin/coverage report --show-missing venv/bin/coverage html xdg-open htmlcov/index.html export SPHINXBUILD=../venv/bin/sphinx-build docs: venv $(MAKE) -C docs clean $(MAKE) -C docs html xdg-open docs/_build/html/index.html docs-dev: venv docs venv/bin/watchmedo shell-command \ --patterns="*.py;*.rst;*.md;*.css" \ --ignore-pattern=".git/*" \ --recursive \ --drop \ --command="$(MAKE) -C docs html" \ . clean: git clean -xdf hcloud-python-2.3.0/README.md000066400000000000000000000073671470147622500156150ustar00rootroot00000000000000# 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://readthedocs.org/projects/hcloud-python/badge/?version=latest)](https://hcloud-python.readthedocs.io) [![](https://img.shields.io/pypi/pyversions/hcloud.svg)](https://pypi.org/project/hcloud/) Official Hetzner Cloud python library. The library's documentation is available at [hcloud-python.readthedocs.io](https://hcloud-python.readthedocs.io), the public API documentation is available at [docs.hetzner.cloud](https://docs.hetzner.cloud). ## Usage Install the `hcloud` library: ```sh pip install hcloud ``` For more installation details, please see the [installation docs](https://hcloud-python.readthedocs.io/en/stable/installation.html). Here is an example that creates a server and list them: ```python from hcloud import Client from hcloud.images import Image from hcloud.server_types import ServerType client = Client(token="{YOUR_API_TOKEN}") # Please paste your API token here # Create a server named my-server response = client.servers.create( name="my-server", server_type=ServerType(name="cx22"), 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). ## 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, ) ``` ## License The MIT License (MIT). Please see [`License File`](https://github.com/hetznercloud/hcloud-python/blob/main/LICENSE) for more information. hcloud-python-2.3.0/docs/000077500000000000000000000000001470147622500152515ustar00rootroot00000000000000hcloud-python-2.3.0/docs/Makefile000066400000000000000000000011721470147622500167120ustar00rootroot00000000000000# 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) hcloud-python-2.3.0/docs/_static/000077500000000000000000000000001470147622500166775ustar00rootroot00000000000000hcloud-python-2.3.0/docs/_static/.gitkeep000066400000000000000000000000001470147622500203160ustar00rootroot00000000000000hcloud-python-2.3.0/docs/_static/custom.css000066400000000000000000000004051470147622500207220ustar00rootroot00000000000000.wy-side-nav-search > div.version { /* Version in Nav is off-white by default, but we restyle the header to have an off-white bg */ color: #404040; } .wy-side-nav-search input[type="text"] { border-color: #404040; } .logo { margin: 1rem !important; } hcloud-python-2.3.0/docs/_static/favicon.png000066400000000000000000000012601470147622500210310ustar00rootroot00000000000000PNG  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 hcloud-python-2.3.0/docs/api.clients.actions.rst000066400000000000000000000005001470147622500216460ustar00rootroot00000000000000ActionsClient ================== .. 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: hcloud-python-2.3.0/docs/api.clients.certificates.rst000066400000000000000000000004011470147622500226530ustar00rootroot00000000000000CertificateClient ================== .. autoclass:: hcloud.certificates.client.CertificatesClient :members: .. autoclass:: hcloud.certificates.client.BoundCertificate :members: .. autoclass:: hcloud.certificates.domain.Certificate :members: hcloud-python-2.3.0/docs/api.clients.datacenters.rst000066400000000000000000000005111470147622500225050ustar00rootroot00000000000000DatacentersClient ================== .. autoclass:: hcloud.datacenters.client.DatacentersClient :members: .. autoclass:: hcloud.datacenters.client.BoundDatacenter :members: .. autoclass:: hcloud.datacenters.domain.Datacenter :members: .. autoclass:: hcloud.datacenters.domain.DatacenterServerTypes :members: hcloud-python-2.3.0/docs/api.clients.firewalls.rst000066400000000000000000000007041470147622500222040ustar00rootroot00000000000000FirewallsClient ================== .. 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: hcloud-python-2.3.0/docs/api.clients.floating_ips.rst000066400000000000000000000005211470147622500226670ustar00rootroot00000000000000Floating 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: hcloud-python-2.3.0/docs/api.clients.images.rst000066400000000000000000000004371470147622500214640ustar00rootroot00000000000000ImagesClient ================== .. autoclass:: hcloud.images.client.ImagesClient :members: .. autoclass:: hcloud.images.client.BoundImage :members: .. autoclass:: hcloud.images.domain.Image :members: .. autoclass:: hcloud.images.domain.CreateImageResponse :members: hcloud-python-2.3.0/docs/api.clients.isos.rst000066400000000000000000000003121470147622500211640ustar00rootroot00000000000000ISOsClient ================== .. autoclass:: hcloud.isos.client.IsosClient :members: .. autoclass:: hcloud.isos.client.BoundIso :members: .. autoclass:: hcloud.isos.domain.Iso :members: hcloud-python-2.3.0/docs/api.clients.load_balancer_types.rst000066400000000000000000000003331470147622500242040ustar00rootroot00000000000000LoadBalancerTypesClient ======================== .. autoclass:: hcloud.load_balancer_types.client.LoadBalancerTypesClient :members: .. autoclass:: hcloud.load_balancer_types.domain.LoadBalancerType :members: hcloud-python-2.3.0/docs/api.clients.load_balancers.rst000066400000000000000000000017721470147622500231530ustar00rootroot00000000000000LoadBalancerClient ================== .. 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.LoadBalancerHealtCheckHttp :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: hcloud-python-2.3.0/docs/api.clients.locations.rst000066400000000000000000000003551470147622500222110ustar00rootroot00000000000000LocationsClient ================== .. autoclass:: hcloud.locations.client.LocationsClient :members: .. autoclass:: hcloud.locations.client.BoundLocation :members: .. autoclass:: hcloud.locations.domain.Location :members: hcloud-python-2.3.0/docs/api.clients.networks.rst000066400000000000000000000006661470147622500220770ustar00rootroot00000000000000NetworksClient ================== .. 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: hcloud-python-2.3.0/docs/api.clients.placement_groups.rst000066400000000000000000000005671470147622500235720ustar00rootroot00000000000000PlacementGroupsClient ===================== .. 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: hcloud-python-2.3.0/docs/api.clients.primary_ips.rst000066400000000000000000000003671470147622500225570ustar00rootroot00000000000000PrimaryIPsClient ================== .. autoclass:: hcloud.primary_ips.client.PrimaryIPsClient :members: .. autoclass:: hcloud.primary_ips.client.BoundPrimaryIP :members: .. autoclass:: hcloud.primary_ips.domain.PrimaryIP :members: hcloud-python-2.3.0/docs/api.clients.server_types.rst000066400000000000000000000003751470147622500227520ustar00rootroot00000000000000ServerTypesClient ================== .. autoclass:: hcloud.server_types.client.ServerTypesClient :members: .. autoclass:: hcloud.server_types.client.BoundServerType :members: .. autoclass:: hcloud.server_types.domain.ServerType :members: hcloud-python-2.3.0/docs/api.clients.servers.rst000066400000000000000000000014261470147622500217070ustar00rootroot00000000000000ServersClient ================== .. 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: hcloud-python-2.3.0/docs/api.clients.ssh_keys.rst000066400000000000000000000003421470147622500220420ustar00rootroot00000000000000SSHKeysClient ================== .. autoclass:: hcloud.ssh_keys.client.SSHKeysClient :members: .. autoclass:: hcloud.ssh_keys.client.BoundSSHKey :members: .. autoclass:: hcloud.ssh_keys.domain.SSHKey :members: hcloud-python-2.3.0/docs/api.clients.volumes.rst000066400000000000000000000004501470147622500217040ustar00rootroot00000000000000VolumesClient ================== .. autoclass:: hcloud.volumes.client.VolumesClient :members: .. autoclass:: hcloud.volumes.client.BoundVolume :members: .. autoclass:: hcloud.volumes.domain.Volume :members: .. autoclass:: hcloud.volumes.domain.CreateVolumeResponse :members: hcloud-python-2.3.0/docs/api.deprecation.rst000066400000000000000000000001541470147622500210500ustar00rootroot00000000000000Deprecation Info ================== .. autoclass:: hcloud.deprecation.domain.DeprecationInfo :members: hcloud-python-2.3.0/docs/api.helpers.rst000066400000000000000000000001371470147622500202160ustar00rootroot00000000000000Helpers ================== .. autoclass:: hcloud.helpers.labels.LabelValidator :members: hcloud-python-2.3.0/docs/api.rst000066400000000000000000000011651470147622500165570ustar00rootroot00000000000000API 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 hcloud-python-2.3.0/docs/changelog.md000066400000000000000000000000411470147622500175150ustar00rootroot00000000000000:::{include} ../CHANGELOG.md ::: hcloud-python-2.3.0/docs/conf.py000066400000000000000000000036661470147622500165630ustar00rootroot00000000000000from __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", ] hcloud-python-2.3.0/docs/contributing.rst000066400000000000000000000000411470147622500205050ustar00rootroot00000000000000.. include:: ../CONTRIBUTING.rst hcloud-python-2.3.0/docs/index.md000066400000000000000000000003161470147622500167020ustar00rootroot00000000000000:::{toctree} :maxdepth: 4 :hidden: self installation.rst api.rst Hetzner Cloud API Documentation contributing.rst upgrading.md changelog.md ::: :::{include} ../README.md ::: hcloud-python-2.3.0/docs/installation.rst000066400000000000000000000025071470147622500205100ustar00rootroot00000000000000.. 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 . hcloud-python-2.3.0/docs/make.bat000066400000000000000000000013751470147622500166640ustar00rootroot00000000000000@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 hcloud-python-2.3.0/docs/upgrading.md000066400000000000000000000054071470147622500175610ustar00rootroot00000000000000# 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`](#hcloud.Client) property is now private, make sure to configure it while creating the [`Client`](#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`](#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`](#hcloud.isos.client.IsosClient.get_list) and [`Client.isos.get_all`](#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`](#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`](#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 ``` hcloud-python-2.3.0/examples/000077500000000000000000000000001470147622500161375ustar00rootroot00000000000000hcloud-python-2.3.0/examples/create_server.py000066400000000000000000000010631470147622500213420ustar00rootroot00000000000000from __future__ import annotations from os import environ from hcloud import Client from hcloud.images import Image from hcloud.server_types import ServerType assert ( "HCLOUD_TOKEN" in environ ), "Please export your API token in the HCLOUD_TOKEN environment variable" token = environ["HCLOUD_TOKEN"] client = Client(token=token) response = client.servers.create( name="my-server", server_type=ServerType(name="cx22"), image=Image(name="ubuntu-24.04"), ) server = response.server print(server) print("Root Password" + response.root_password) hcloud-python-2.3.0/examples/get_server_metrics.py000066400000000000000000000014771470147622500224150ustar00rootroot00000000000000from __future__ import annotations import json from datetime import datetime, timedelta from os import environ from hcloud import Client from hcloud.images import Image from hcloud.server_types import ServerType assert ( "HCLOUD_TOKEN" in environ ), "Please export your API token in the HCLOUD_TOKEN environment variable" token = environ["HCLOUD_TOKEN"] client = Client(token=token) server = client.servers.get_by_name("my-server") if server is None: response = client.servers.create( name="my-server", server_type=ServerType(name="cx22"), image=Image(name="ubuntu-24.04"), ) server = response.server end = datetime.now() start = end - timedelta(hours=1) response = server.get_metrics( type=["cpu", "network"], start=start, end=end, ) print(json.dumps(response.metrics)) hcloud-python-2.3.0/examples/list_servers.py000066400000000000000000000004711470147622500212370ustar00rootroot00000000000000from __future__ import annotations from os import environ from hcloud import Client assert ( "HCLOUD_TOKEN" in environ ), "Please export your API token in the HCLOUD_TOKEN environment variable" token = environ["HCLOUD_TOKEN"] client = Client(token=token) servers = client.servers.get_all() print(servers) hcloud-python-2.3.0/examples/usage_oop.py000066400000000000000000000022631470147622500204750ustar00rootroot00000000000000from __future__ import annotations from os import environ from hcloud import Client from hcloud.images import Image from hcloud.server_types import ServerType assert ( "HCLOUD_TOKEN" in environ ), "Please export your API token in the HCLOUD_TOKEN environment variable" token = environ["HCLOUD_TOKEN"] # Create a client client = Client(token=token) # Create 2 servers # Create 2 servers response1 = client.servers.create( "Server1", server_type=ServerType(name="cx22"), image=Image(id=4711) ) response2 = client.servers.create( "Server2", server_type=ServerType(name="cx22"), image=Image(id=4711) ) # Get all servers server1 = response1.server server2 = response2.server servers = client.servers.get_all() assert servers[0].id == server1.id assert servers[1].id == server2.id # Create 2 volumes response1 = client.volumes.create(size=15, name="Volume1", location=server1.location) response2 = client.volumes.create(size=10, name="Volume2", location=server2.location) volume1 = response1.volume volume2 = response2.volume # Attach volume to server volume1.attach(server1) volume2.attach(server2) # Detach second volume volume2.detach() # Poweroff 2nd server server2.power_off() hcloud-python-2.3.0/examples/usage_procedurale.py000066400000000000000000000032461470147622500222070ustar00rootroot00000000000000from __future__ import annotations from os import environ from hcloud import Client from hcloud.images import Image from hcloud.server_types import ServerType from hcloud.servers import Server from hcloud.volumes import Volume assert ( "HCLOUD_TOKEN" in environ ), "Please export your API token in the HCLOUD_TOKEN environment variable" token = environ["HCLOUD_TOKEN"] client = Client(token=token) # Create 2 servers response1 = client.servers.create( name="Server1", server_type=ServerType(name="cx22"), image=Image(id=4711) ) response2 = client.servers.create( "Server2", server_type=ServerType(name="cx22"), image=Image(id=4711) ) server1 = response1.server server2 = response2.server # Get all servers servers = client.servers.get_all() assert servers[0].id == server1.id assert servers[1].id == server2.id # Create 2 volumes response1 = client.volumes.create(size=15, name="Volume1", location=server1.location) response2 = client.volumes.create(size=10, name="Volume2", location=server2.location) volume1 = response1.volume volume2 = response2.volume # Attach volume to server client.volumes.attach(server1, volume1) client.volumes.attach(server2, volume2) # Detach second volume client.volumes.detach(volume2) # Poweroff 2nd server client.servers.power_off(server2) # Create one more volume and attach it to server with id=33 server33 = Server(id=33) response = client.volumes.create(size=33, name="Volume33", server=server33) print(response.action.status) # Create one more server and attach 2 volumes to it client.servers.create( "Server3", server_type=ServerType(name="cx22"), image=Image(id=4711), volumes=[Volume(id=221), Volume(id=222)], ) hcloud-python-2.3.0/hcloud/000077500000000000000000000000001470147622500155775ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/__init__.py000066400000000000000000000006361470147622500177150ustar00rootroot00000000000000from __future__ import annotations from ._client import ( # noqa pylint: disable=C0414 Client as Client, constant_backoff_function as constant_backoff_function, exponential_backoff_function as exponential_backoff_function, ) from ._exceptions import ( # noqa pylint: disable=C0414 APIException as APIException, HCloudException as HCloudException, ) from ._version import __version__ # noqa hcloud-python-2.3.0/hcloud/_client.py000066400000000000000000000267341470147622500176020ustar00rootroot00000000000000from __future__ import annotations import time from http import HTTPStatus from random import uniform from typing import 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 .volumes import VolumesClient 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 = base * multiplier**retries # Exponential backoff interval = min(cap, interval) # Cap backoff if jitter: interval = uniform(base, interval) # Add jitter return interval return func class Client: """ Client for the Hetzner Cloud API. The Hetzner Cloud API reference is available at https://docs.hetzner.cloud. **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`` Changes to the retry policy might occur between releases, and will not be considered breaking changes. """ _version = __version__ __user_agent_prefix = "hcloud-python" _retry_interval = staticmethod( exponential_backoff_function(base=1.0, multiplier=2, cap=60.0, jitter=True) ) _retry_max_retries = 5 def __init__( self, token: str, 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, ): """Create a new Client instance :param token: Hetzner Cloud API token :param api_endpoint: Hetzner Cloud API endpoint :param application_name: Your application name :param application_version: Your application _version :param poll_interval: Interval 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.token = token self._api_endpoint = api_endpoint self._application_name = application_name self._application_version = application_version self._requests_session = requests.Session() self._requests_timeout = timeout if isinstance(poll_interval, (int, float)): self._poll_interval_func = constant_backoff_function(poll_interval) else: self._poll_interval_func = poll_interval self._poll_max_retries = poll_max_retries self.datacenters = DatacentersClient(self) """DatacentersClient Instance :type: :class:`DatacentersClient ` """ self.locations = LocationsClient(self) """LocationsClient Instance :type: :class:`LocationsClient ` """ self.servers = ServersClient(self) """ServersClient Instance :type: :class:`ServersClient ` """ self.server_types = ServerTypesClient(self) """ServerTypesClient Instance :type: :class:`ServerTypesClient ` """ self.volumes = VolumesClient(self) """VolumesClient Instance :type: :class:`VolumesClient ` """ self.actions = ActionsClient(self) """ActionsClient Instance :type: :class:`ActionsClient ` """ self.images = ImagesClient(self) """ImagesClient Instance :type: :class:`ImagesClient ` """ self.isos = IsosClient(self) """ImagesClient Instance :type: :class:`IsosClient ` """ self.ssh_keys = SSHKeysClient(self) """SSHKeysClient Instance :type: :class:`SSHKeysClient ` """ self.floating_ips = FloatingIPsClient(self) """FloatingIPsClient Instance :type: :class:`FloatingIPsClient ` """ self.primary_ips = PrimaryIPsClient(self) """PrimaryIPsClient Instance :type: :class:`PrimaryIPsClient ` """ self.networks = NetworksClient(self) """NetworksClient Instance :type: :class:`NetworksClient ` """ self.certificates = CertificatesClient(self) """CertificatesClient Instance :type: :class:`CertificatesClient ` """ self.load_balancers = LoadBalancersClient(self) """LoadBalancersClient Instance :type: :class:`LoadBalancersClient ` """ self.load_balancer_types = LoadBalancerTypesClient(self) """LoadBalancerTypesClient Instance :type: :class:`LoadBalancerTypesClient ` """ self.firewalls = FirewallsClient(self) """FirewallsClient Instance :type: :class:`FirewallsClient ` """ self.placement_groups = PlacementGroupsClient(self) """PlacementGroupsClient Instance :type: :class:`PlacementGroupsClient ` """ def _get_user_agent(self) -> str: """Get the user agent of the hcloud-python instance with the user application name (if specified) :return: The user agent of this hcloud-python instance """ user_agents = [] for name, version in [ (self._application_name, self._application_version), (self.__user_agent_prefix, self._version), ]: if name is not None: user_agents.append(name if version is None else f"{name}/{version}") return " ".join(user_agents) def _get_headers(self) -> dict: headers = { "User-Agent": self._get_user_agent(), "Authorization": f"Bearer {self.token}", } return headers def request( # type: ignore[no-untyped-def] self, method: str, url: str, **kwargs, ) -> dict: """Perform a request to the Hetzner Cloud API, wrapper around requests.request :param method: HTTP Method to perform the Request :param url: URL of the Endpoint :param timeout: Requests timeout in seconds :return: Response """ kwargs.setdefault("timeout", self._requests_timeout) url = self._api_endpoint + url headers = self._get_headers() retries = 0 while True: try: response = self._requests_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(retries)) retries += 1 continue raise except requests.exceptions.Timeout: if retries < self._retry_max_retries: time.sleep(self._retry_interval(retries)) retries += 1 continue raise def _read_response(self, response: requests.Response) -> dict: 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 = 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", ) if isinstance(exception.code, int): return exception.code in ( HTTPStatus.BAD_GATEWAY, HTTPStatus.GATEWAY_TIMEOUT, ) return False hcloud-python-2.3.0/hcloud/_exceptions.py000066400000000000000000000013511470147622500204710ustar00rootroot00000000000000from __future__ import annotations from typing import Any class HCloudException(Exception): """There was an error while using the hcloud library""" class APIException(HCloudException): """There was an error while performing an API Request""" def __init__( self, code: 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 hcloud-python-2.3.0/hcloud/_version.py000066400000000000000000000001261470147622500177740ustar00rootroot00000000000000from __future__ import annotations __version__ = "2.3.0" # x-release-please-version hcloud-python-2.3.0/hcloud/actions/000077500000000000000000000000001470147622500172375ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/actions/__init__.py000066400000000000000000000004361470147622500213530ustar00rootroot00000000000000from __future__ import annotations from .client import ( # noqa: F401 ActionsClient, ActionsPageResult, BoundAction, ResourceActionsClient, ) from .domain import ( # noqa: F401 Action, ActionException, ActionFailedException, ActionTimeoutException, ) hcloud-python-2.3.0/hcloud/actions/client.py000066400000000000000000000145621470147622500210770ustar00rootroot00000000000000from __future__ import annotations import time import warnings from typing import TYPE_CHECKING, Any, NamedTuple from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import Action, ActionFailedException, ActionTimeoutException if TYPE_CHECKING: from .._client import Client class BoundAction(BoundModelBase, 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) class ActionsPageResult(NamedTuple): actions: list[BoundAction] meta: Meta | None class ResourceActionsClient(ClientEntityBase): _resource: str def __init__(self, client: Client, resource: str | None): super().__init__(client) self._resource = resource or "" def get_by_id(self, id: int) -> BoundAction: """Get a specific action by its ID. :param id: int :return: :class:`BoundAction ` """ response = self._client.request( url=f"{self._resource}/actions/{id}", method="GET", ) return BoundAction(self._client.actions, response["action"]) def get_list( self, status: list[str] | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """Get a list of actions. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `command` `status` `progress` `started` `finished` . You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default) :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:`BoundAction `], :class:`Meta `) """ 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( url=f"{self._resource}/actions", method="GET", params=params, ) actions = [ BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] return ActionsPageResult(actions, Meta.parse_meta(response)) def get_all( self, status: list[str] | None = None, sort: list[str] | None = None, ) -> list[BoundAction]: """Get all actions. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `command` `status` `progress` `started` `finished` . You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default) :return: List[:class:`BoundAction `] """ return self._iter_pages(self.get_list, status=status, sort=sort) class ActionsClient(ResourceActionsClient): def __init__(self, client: Client): super().__init__(client, None) def get_list( self, status: list[str] | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """ .. deprecated:: 1.28 Use :func:`client..actions.get_list` instead, e.g. using :attr:`hcloud.certificates.client.CertificatesClient.actions`. `Starting 1 October 2023, it will no longer be available. `_ """ warnings.warn( "The 'client.actions.get_list' method is deprecated, please use the " "'client..actions.get_list' method instead (e.g. " "'client.certificates.actions.get_list').", DeprecationWarning, stacklevel=2, ) return super().get_list(status=status, sort=sort, page=page, per_page=per_page) def get_all( self, status: list[str] | None = None, sort: list[str] | None = None, ) -> list[BoundAction]: """ .. deprecated:: 1.28 Use :func:`client..actions.get_all` instead, e.g. using :attr:`hcloud.certificates.client.CertificatesClient.actions`. `Starting 1 October 2023, it will no longer be available. `_ """ warnings.warn( "The 'client.actions.get_all' method is deprecated, please use the " "'client..actions.get_all' method instead (e.g. " "'client.certificates.actions.get_all').", DeprecationWarning, stacklevel=2, ) return super().get_all(status=status, sort=sort) hcloud-python-2.3.0/hcloud/actions/domain.py000066400000000000000000000045551470147622500210710ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from dateutil.parser import isoparse from .._exceptions import HCloudException from ..core import BaseDomain if TYPE_CHECKING: from .client import BoundAction class Action(BaseDomain): """Action Domain :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: str | None = None, progress: int | None = None, started: str | None = None, finished: str | None = None, resources: list[dict] | None = None, error: dict | None = None, ): self.id = id self.command = command self.status = status self.progress = progress self.started = isoparse(started) if started else None self.finished = isoparse(finished) if finished else None self.resources = resources self.error = error class ActionException(HCloudException): """A generic action exception""" def __init__(self, action: Action | BoundAction): assert self.__doc__ is not None message = self.__doc__ if action.error is not None and "message" in action.error: message += f": {action.error['message']}" super().__init__(message) self.message = message self.action = action class ActionFailedException(ActionException): """The pending action failed""" class ActionTimeoutException(ActionException): """The pending action timed out""" hcloud-python-2.3.0/hcloud/certificates/000077500000000000000000000000001470147622500202445ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/certificates/__init__.py000066400000000000000000000004541470147622500223600ustar00rootroot00000000000000from __future__ import annotations from .client import ( # noqa: F401 BoundCertificate, CertificatesClient, CertificatesPageResult, ) from .domain import ( # noqa: F401 Certificate, CreateManagedCertificateResponse, ManagedCertificateError, ManagedCertificateStatus, ) hcloud-python-2.3.0/hcloud/certificates/client.py000066400000000000000000000361741470147622500221070ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import ( Certificate, CreateManagedCertificateResponse, ManagedCertificateError, ManagedCertificateStatus, ) if TYPE_CHECKING: from .._client import Client class BoundCertificate(BoundModelBase, Certificate): _client: CertificatesClient model = Certificate def __init__(self, client: CertificatesClient, data: dict, 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[str] | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """Returns all action objects for a Certificate. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :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:`BoundAction `], :class:`Meta `) """ return self._client.get_actions_list(self, status, sort, page, per_page) def get_actions( self, status: list[str] | None = None, sort: list[str] | None = None, ) -> list[BoundAction]: """Returns all action objects for a Certificate. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._client.get_actions(self, status, sort) 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, 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 | None class CertificatesClient(ClientEntityBase): _client: Client actions: ResourceActionsClient """Certificates scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self.actions = ResourceActionsClient(client, "/certificates") def get_by_id(self, id: int) -> BoundCertificate: """Get a specific certificate by its ID. :param id: int :return: :class:`BoundCertificate ` """ response = self._client.request(url=f"/certificates/{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="/certificates", 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(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="/certificates", 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="/certificates", method="POST", json=data) return CreateManagedCertificateResponse( certificate=BoundCertificate(self, response["certificate"]), action=BoundAction(self._client.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"/certificates/{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"/certificates/{certificate.id}", method="DELETE", ) # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised return True def get_actions_list( self, certificate: Certificate | BoundCertificate, status: list[str] | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """Returns all action objects for a Certificate. :param certificate: :class:`BoundCertificate ` or :class:`Certificate ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :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:`BoundAction `], :class:`Meta `) """ 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( url=f"/certificates/{certificate.id}/actions", method="GET", params=params, ) actions = [ BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions( self, certificate: Certificate | BoundCertificate, status: list[str] | None = None, sort: list[str] | None = None, ) -> list[BoundAction]: """Returns all action objects for a Certificate. :param certificate: :class:`BoundCertificate ` or :class:`Certificate ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._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"/certificates/{certificate.id}/actions/retry", method="POST", ) return BoundAction(self._client.actions, response["action"]) hcloud-python-2.3.0/hcloud/certificates/domain.py000066400000000000000000000101731470147622500220670ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from dateutil.parser import isoparse from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction from .client import BoundCertificate class Certificate(BaseDomain, DomainIdentityMixin): """Certificate Domain :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 = isoparse(not_valid_before) if not_valid_before else None self.not_valid_after = isoparse(not_valid_after) if not_valid_after else None self.created = isoparse(created) if created else None self.labels = labels self.status = status 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 """ 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 """ 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 hcloud-python-2.3.0/hcloud/core/000077500000000000000000000000001470147622500165275ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/core/__init__.py000066400000000000000000000002731470147622500206420ustar00rootroot00000000000000from __future__ import annotations from .client import BoundModelBase, ClientEntityBase # noqa: F401 from .domain import BaseDomain, DomainIdentityMixin, Meta, Pagination # noqa: F401 hcloud-python-2.3.0/hcloud/core/client.py000066400000000000000000000053751470147622500203710ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, Callable if TYPE_CHECKING: from .._client import Client class ClientEntityBase: _client: Client max_per_page: int = 50 def __init__(self, client: Client): """ :param client: Client :return self """ self._client = client def _iter_pages( # type: ignore[no-untyped-def] self, list_function: Callable, *args, **kwargs, ) -> list: 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(self, **kwargs): # type: ignore[no-untyped-def] assert hasattr(self, "get_list") # pylint: disable=no-member entities, _ = self.get_list(**kwargs) return entities[0] if entities else None class BoundModelBase: """Bound Model Base""" model: Any def __init__( self, client: ClientEntityBase, data: dict, 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 = 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 reload(self) -> None: """Reloads the model and tries to get all data from the APIx""" assert hasattr(self._client, "get_by_id") bound_model = self._client.get_by_id(self.data_model.id) self.data_model = bound_model.data_model self.complete = True 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) hcloud-python-2.3.0/hcloud/core/domain.py000066400000000000000000000057021470147622500203540ustar00rootroot00000000000000from __future__ import annotations class BaseDomain: __api_properties__: tuple @classmethod def from_dict(cls, data: dict): # type: ignore[no-untyped-def] """ Build the domain object from the data dict. """ supported_data = {k: v for k, v in data.items() if k in cls.__api_properties__} return cls(**supported_data) def __repr__(self) -> str: kwargs = [f"{key}={getattr(self, key)!r}" for key in self.__api_properties__] # type: ignore[var-annotated] return f"{self.__class__.__qualname__}({', '.join(kwargs)})" 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). """ values: list[int | str] = [] if self.id is not None: values.append(self.id) if self.name is not None: values.append(self.name) if not values: raise ValueError("id or name must be set") return id_or_name in values class Pagination(BaseDomain): __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) -> Meta | None: """ If present, extract the meta details from the response and return a meta object. """ meta = None if response and "meta" in response: meta = cls() try: meta.pagination = Pagination(**response["meta"]["pagination"]) except KeyError: pass return meta hcloud-python-2.3.0/hcloud/datacenters/000077500000000000000000000000001470147622500200745ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/datacenters/__init__.py000066400000000000000000000003251470147622500222050ustar00rootroot00000000000000from __future__ import annotations from .client import ( # noqa: F401 BoundDatacenter, DatacentersClient, DatacentersPageResult, ) from .domain import Datacenter, DatacenterServerTypes # noqa: F401 hcloud-python-2.3.0/hcloud/datacenters/client.py000066400000000000000000000100521470147622500217220ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..core import BoundModelBase, ClientEntityBase, Meta from ..locations import BoundLocation from ..server_types import BoundServerType from .domain import Datacenter, DatacenterServerTypes if TYPE_CHECKING: from .._client import Client class BoundDatacenter(BoundModelBase, Datacenter): _client: DatacentersClient model = Datacenter def __init__(self, client: DatacentersClient, data: dict): location = data.get("location") if location is not None: data["location"] = BoundLocation(client._client.locations, location) server_types = data.get("server_types") if server_types is not None: available = [ BoundServerType( client._client.server_types, {"id": server_type}, complete=False ) for server_type in server_types["available"] ] supported = [ BoundServerType( client._client.server_types, {"id": server_type}, complete=False ) for server_type in server_types["supported"] ] available_for_migration = [ BoundServerType( client._client.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 | None class DatacentersClient(ClientEntityBase): _client: Client 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"/datacenters/{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="/datacenters", 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(name=name) hcloud-python-2.3.0/hcloud/datacenters/domain.py000066400000000000000000000042121470147622500217140ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..locations import Location from ..server_types import BoundServerType class Datacenter(BaseDomain, DomainIdentityMixin): """Datacenter Domain :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 hcloud-python-2.3.0/hcloud/deprecation/000077500000000000000000000000001470147622500200745ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/deprecation/__init__.py000066400000000000000000000001261470147622500222040ustar00rootroot00000000000000from __future__ import annotations from .domain import DeprecationInfo # noqa: F401 hcloud-python-2.3.0/hcloud/deprecation/domain.py000066400000000000000000000022371470147622500217210ustar00rootroot00000000000000from __future__ import annotations from dateutil.parser import isoparse from ..core import BaseDomain class DeprecationInfo(BaseDomain): """Describes if, when & how the resources was deprecated. If this field is set to ``None`` the resource is not deprecated. If it has a value, it is considered deprecated. :param announced: datetime Date of when the deprecation was announced. :param unavailable_after: datetime After the time in this field, the resource will not be available from the general listing endpoint of the resource type, and it can not be used in new resources. For example, if this is an image, you can not create new servers with this image after the mentioned date. """ __api_properties__ = ( "announced", "unavailable_after", ) __slots__ = __api_properties__ def __init__( self, announced: str | None = None, unavailable_after: str | None = None, ): self.announced = isoparse(announced) if announced else None self.unavailable_after = ( isoparse(unavailable_after) if unavailable_after else None ) hcloud-python-2.3.0/hcloud/firewalls/000077500000000000000000000000001470147622500175675ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/firewalls/__init__.py000066400000000000000000000004751470147622500217060ustar00rootroot00000000000000from __future__ import annotations from .client import BoundFirewall, FirewallsClient, FirewallsPageResult # noqa: F401 from .domain import ( # noqa: F401 CreateFirewallResponse, Firewall, FirewallResource, FirewallResourceAppliedToResources, FirewallResourceLabelSelector, FirewallRule, ) hcloud-python-2.3.0/hcloud/firewalls/client.py000066400000000000000000000500101470147622500214130ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import ( CreateFirewallResponse, Firewall, FirewallResource, FirewallResourceAppliedToResources, FirewallResourceLabelSelector, FirewallRule, ) if TYPE_CHECKING: from .._client import Client class BoundFirewall(BoundModelBase, Firewall): _client: FirewallsClient model = Firewall def __init__(self, client: FirewallsClient, data: dict, 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._client.servers, resource.get("server"), complete=False, ) if resource.get("server") is not None else None ), ) for resource in firewall_resource.get("applied_to_resources") ] if firewall_resource["type"] == FirewallResource.TYPE_SERVER: data_applied_to.append( FirewallResource( type=firewall_resource["type"], server=BoundServer( client._client.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[str] | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """Returns all action objects for a Firewall. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :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:`BoundAction `], :class:`Meta `) """ return self._client.get_actions_list(self, status, sort, page, per_page) def get_actions( self, status: list[str] | None = None, sort: list[str] | None = None, ) -> list[BoundAction]: """Returns all action objects for a Firewall. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._client.get_actions(self, status, sort) 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, labels, name) 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) def apply_to_resources( self, resources: list[FirewallResource], ) -> list[BoundAction]: """Applies one Firewall to multiple resources. :param resources: List[:class:`FirewallResource `] :return: List[:class:`BoundAction `] """ return self._client.apply_to_resources(self, resources) def remove_from_resources( self, resources: 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) class FirewallsPageResult(NamedTuple): firewalls: list[BoundFirewall] meta: Meta | None class FirewallsClient(ClientEntityBase): _client: Client actions: ResourceActionsClient """Firewalls scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self.actions = ResourceActionsClient(client, "/firewalls") def get_actions_list( self, firewall: Firewall | BoundFirewall, status: list[str] | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """Returns all action objects for a Firewall. :param firewall: :class:`BoundFirewall ` or :class:`Firewall ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :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:`BoundAction `], :class:`Meta `) """ 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( url=f"/firewalls/{firewall.id}/actions", method="GET", params=params, ) actions = [ BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions( self, firewall: Firewall | BoundFirewall, status: list[str] | None = None, sort: list[str] | None = None, ) -> list[BoundAction]: """Returns all action objects for a Firewall. :param firewall: :class:`BoundFirewall ` or :class:`Firewall ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._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"/firewalls/{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="/firewalls", 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(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="/firewalls", json=data, method="POST") actions = [] if response.get("actions") is not None: actions = [ BoundAction(self._client.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"/firewalls/{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"/firewalls/{firewall.id}", method="DELETE", ) # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised 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"/firewalls/{firewall.id}/actions/set_rules", method="POST", json=data, ) return [ BoundAction(self._client.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"/firewalls/{firewall.id}/actions/apply_to_resources", method="POST", json=data, ) return [ BoundAction(self._client.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"/firewalls/{firewall.id}/actions/remove_from_resources", method="POST", json=data, ) return [ BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] hcloud-python-2.3.0/hcloud/firewalls/domain.py000066400000000000000000000160341470147622500214140ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any from dateutil.parser import isoparse from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction from ..servers import BoundServer, Server from .client import BoundFirewall 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 = isoparse(created) if created else None 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], 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 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, "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 """ 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 hcloud-python-2.3.0/hcloud/floating_ips/000077500000000000000000000000001470147622500202555ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/floating_ips/__init__.py000066400000000000000000000003301470147622500223620ustar00rootroot00000000000000from __future__ import annotations from .client import ( # noqa: F401 BoundFloatingIP, FloatingIPsClient, FloatingIPsPageResult, ) from .domain import CreateFloatingIPResponse, FloatingIP # noqa: F401 hcloud-python-2.3.0/hcloud/floating_ips/client.py000066400000000000000000000463211470147622500221130ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient from ..core import BoundModelBase, ClientEntityBase, Meta from ..locations import BoundLocation from .domain import CreateFloatingIPResponse, FloatingIP if TYPE_CHECKING: from .._client import Client from ..locations import Location from ..servers import BoundServer, Server class BoundFloatingIP(BoundModelBase, FloatingIP): _client: FloatingIPsClient model = FloatingIP def __init__(self, client: FloatingIPsClient, data: dict, complete: bool = True): # pylint: disable=import-outside-toplevel from ..servers import BoundServer server = data.get("server") if server is not None: data["server"] = BoundServer( client._client.servers, {"id": server}, complete=False ) home_location = data.get("home_location") if home_location is not None: data["home_location"] = BoundLocation( client._client.locations, home_location ) super().__init__(client, data, complete) def get_actions_list( self, status: list[str] | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """Returns all action objects for a Floating IP. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :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:`BoundAction `], :class:`Meta `) """ return self._client.get_actions_list(self, status, sort, page, per_page) def get_actions( self, status: list[str] | None = None, sort: list[str] | None = None, ) -> list[BoundAction]: """Returns all action objects for a Floating IP. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._client.get_actions(self, status, sort) 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, labels, 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) 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) 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, dns_ptr) class FloatingIPsPageResult(NamedTuple): floating_ips: list[BoundFloatingIP] meta: Meta | None class FloatingIPsClient(ClientEntityBase): _client: Client actions: ResourceActionsClient """Floating IPs scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self.actions = ResourceActionsClient(client, "/floating_ips") def get_actions_list( self, floating_ip: FloatingIP | BoundFloatingIP, status: list[str] | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """Returns all action objects for a Floating IP. :param floating_ip: :class:`BoundFloatingIP ` or :class:`FloatingIP ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :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:`BoundAction `], :class:`Meta `) """ 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( url=f"/floating_ips/{floating_ip.id}/actions", method="GET", params=params, ) actions = [ BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions( self, floating_ip: FloatingIP | BoundFloatingIP, status: list[str] | None = None, sort: list[str] | None = None, ) -> list[BoundAction]: """Returns all action objects for a Floating IP. :param floating_ip: :class:`BoundFloatingIP ` or :class:`FloatingIP ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._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"/floating_ips/{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="/floating_ips", 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(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="/floating_ips", json=data, method="POST") action = None if response.get("action") is not None: action = BoundAction(self._client.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"/floating_ips/{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"/floating_ips/{floating_ip.id}", method="DELETE", ) # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised 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"/floating_ips/{floating_ip.id}/actions/change_protection", method="POST", json=data, ) return BoundAction(self._client.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"/floating_ips/{floating_ip.id}/actions/assign", method="POST", json={"server": server.id}, ) return BoundAction(self._client.actions, response["action"]) def unassign(self, floating_ip: 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"/floating_ips/{floating_ip.id}/actions/unassign", method="POST", ) return BoundAction(self._client.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"/floating_ips/{floating_ip.id}/actions/change_dns_ptr", method="POST", json={"ip": ip, "dns_ptr": dns_ptr}, ) return BoundAction(self._client.actions, response["action"]) hcloud-python-2.3.0/hcloud/floating_ips/domain.py000066400000000000000000000063661470147622500221110ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from dateutil.parser import isoparse from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction from ..locations import BoundLocation from ..servers import BoundServer from .client import BoundFloatingIP 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[dict] | None = None, home_location: BoundLocation | None = None, blocked: bool | None = None, protection: dict | None = None, labels: dict[str, str] | None = None, created: str | None = None, name: str | None = None, ): self.id = id self.type = type 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 = isoparse(created) if created else None self.name = name 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 hcloud-python-2.3.0/hcloud/helpers/000077500000000000000000000000001470147622500172415ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/helpers/__init__.py000066400000000000000000000001251470147622500213500ustar00rootroot00000000000000from __future__ import annotations from .labels import LabelValidator # noqa: F401 hcloud-python-2.3.0/hcloud/helpers/labels.py000066400000000000000000000031261470147622500210570ustar00rootroot00000000000000from __future__ import annotations import re 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, "" hcloud-python-2.3.0/hcloud/images/000077500000000000000000000000001470147622500170445ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/images/__init__.py000066400000000000000000000002561470147622500211600ustar00rootroot00000000000000from __future__ import annotations from .client import BoundImage, ImagesClient, ImagesPageResult # noqa: F401 from .domain import CreateImageResponse, Image # noqa: F401 hcloud-python-2.3.0/hcloud/images/client.py000066400000000000000000000407251470147622500207040ustar00rootroot00000000000000from __future__ import annotations import warnings from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import Image if TYPE_CHECKING: from .._client import Client class BoundImage(BoundModelBase, Image): _client: ImagesClient model = Image def __init__(self, client: ImagesClient, data: dict): # 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._client.servers, created_from, complete=False ) bound_to = data.get("bound_to") if bound_to is not None: data["bound_to"] = BoundServer( client._client.servers, {"id": bound_to}, complete=False ) super().__init__(client, data) def get_actions_list( self, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, status: list[str] | None = None, ) -> ActionsPageResult: """Returns a list of action objects for the image. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :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:`BoundAction `], :class:`Meta `) """ return self._client.get_actions_list( self, sort=sort, page=page, per_page=per_page, status=status ) def get_actions( self, sort: list[str] | None = None, status: list[str] | None = None, ) -> list[BoundAction]: """Returns all action objects for the image. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._client.get_actions(self, status=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, type, 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) class ImagesPageResult(NamedTuple): images: list[BoundImage] meta: Meta | None class ImagesClient(ClientEntityBase): _client: Client actions: ResourceActionsClient """Images scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self.actions = ResourceActionsClient(client, "/images") def get_actions_list( self, image: Image | BoundImage, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, status: list[str] | None = None, ) -> ActionsPageResult: """Returns a list of action objects for an image. :param image: :class:`BoundImage ` or :class:`Image ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :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:`BoundAction `], :class:`Meta `) """ params: dict[str, Any] = {} if sort is not None: params["sort"] = sort 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=f"/images/{image.id}/actions", method="GET", params=params, ) actions = [ BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions( self, image: Image | BoundImage, sort: list[str] | None = None, status: list[str] | None = None, ) -> list[BoundAction]: """Returns all action objects for an image. :param image: :class:`BoundImage ` or :class:`Image ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `command` `status` `progress` `started` `finished` . You can add one of ":asc", ":desc" to modify sort order. ( ":asc" is default) :return: List[:class:`BoundAction `] """ return self._iter_pages( self.get_actions_list, image, sort=sort, status=status, ) def get_by_id(self, id: int) -> BoundImage: """Get a specific Image :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="/images", 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(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( 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"/images/{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"/images/{image.id}", method="DELETE") # Return allays true, because the API does not return an action for it. When an error occurs a APIException will be raised return True 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"/images/{image.id}/actions/change_protection", method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) hcloud-python-2.3.0/hcloud/images/domain.py000066400000000000000000000107571470147622500206770ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from dateutil.parser import isoparse from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction from ..servers import BoundServer, Server from .client import BoundImage class Image(BaseDomain, DomainIdentityMixin): """Image Domain :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: dict | None = None, labels: dict[str, str] | None = None, status: str | None = None, ): self.id = id self.name = name self.type = type self.created = isoparse(created) if created else None self.description = description self.image_size = image_size self.disk_size = disk_size self.deprecated = isoparse(deprecated) if deprecated else None self.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 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 hcloud-python-2.3.0/hcloud/isos/000077500000000000000000000000001470147622500165545ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/isos/__init__.py000066400000000000000000000002211470147622500206600ustar00rootroot00000000000000from __future__ import annotations from .client import BoundIso, IsosClient, IsosPageResult # noqa: F401 from .domain import Iso # noqa: F401 hcloud-python-2.3.0/hcloud/isos/client.py000066400000000000000000000072411470147622500204100ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import Iso if TYPE_CHECKING: from .._client import Client class BoundIso(BoundModelBase, Iso): _client: IsosClient model = Iso class IsosPageResult(NamedTuple): isos: list[BoundIso] meta: Meta | None class IsosClient(ClientEntityBase): _client: Client 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"/isos/{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="/isos", 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(name=name) hcloud-python-2.3.0/hcloud/isos/domain.py000066400000000000000000000046261470147622500204050ustar00rootroot00000000000000from __future__ import annotations from datetime import datetime from warnings import warn from ..core import BaseDomain, DomainIdentityMixin from ..deprecation import DeprecationInfo 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 | 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 hcloud-python-2.3.0/hcloud/load_balancer_types/000077500000000000000000000000001470147622500215715ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/load_balancer_types/__init__.py000066400000000000000000000003261470147622500237030ustar00rootroot00000000000000from __future__ import annotations from .client import ( # noqa: F401 BoundLoadBalancerType, LoadBalancerTypesClient, LoadBalancerTypesPageResult, ) from .domain import LoadBalancerType # noqa: F401 hcloud-python-2.3.0/hcloud/load_balancer_types/client.py000066400000000000000000000060401470147622500234210ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import LoadBalancerType if TYPE_CHECKING: from .._client import Client class BoundLoadBalancerType(BoundModelBase, LoadBalancerType): _client: LoadBalancerTypesClient model = LoadBalancerType class LoadBalancerTypesPageResult(NamedTuple): load_balancer_types: list[BoundLoadBalancerType] meta: Meta | None class LoadBalancerTypesClient(ClientEntityBase): _client: Client def get_by_id(self, id: int) -> BoundLoadBalancerType: """Returns a specific Load Balancer Type. :param id: int :return: :class:`BoundLoadBalancerType ` """ response = self._client.request( url=f"/load_balancer_types/{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="/load_balancer_types", 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(name=name) hcloud-python-2.3.0/hcloud/load_balancer_types/domain.py000066400000000000000000000033531470147622500234160ustar00rootroot00000000000000from __future__ import annotations from ..core import BaseDomain, DomainIdentityMixin class LoadBalancerType(BaseDomain, DomainIdentityMixin): """LoadBalancerType Domain :param id: int ID of the Load Balancer type :param name: str Name of the Load Balancer type :param description: str Description of the Load Balancer type :param max_connections: int Max amount of connections the Load Balancer can handle :param max_services: int Max amount of services the Load Balancer can handle :param max_targets: int Max amount of targets the Load Balancer can handle :param max_assigned_certificates: int Max amount of certificates the Load Balancer can serve :param prices: 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] | 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 hcloud-python-2.3.0/hcloud/load_balancers/000077500000000000000000000000001470147622500205305ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/load_balancers/__init__.py000066400000000000000000000011251470147622500226400ustar00rootroot00000000000000from __future__ import annotations from .client import ( # noqa: F401 BoundLoadBalancer, LoadBalancersClient, LoadBalancersPageResult, ) from .domain import ( # noqa: F401 CreateLoadBalancerResponse, GetMetricsResponse, IPv4Address, IPv6Network, LoadBalancer, LoadBalancerAlgorithm, LoadBalancerHealtCheckHttp, LoadBalancerHealthCheck, LoadBalancerService, LoadBalancerServiceHttp, LoadBalancerTarget, LoadBalancerTargetHealthStatus, LoadBalancerTargetIP, LoadBalancerTargetLabelSelector, PrivateNet, PublicNetwork, ) hcloud-python-2.3.0/hcloud/load_balancers/client.py000066400000000000000000001166561470147622500223770ustar00rootroot00000000000000from __future__ import annotations from datetime import datetime from typing import TYPE_CHECKING, Any, NamedTuple from dateutil.parser import isoparse from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient from ..certificates import BoundCertificate from ..core import BoundModelBase, ClientEntityBase, Meta from ..load_balancer_types import BoundLoadBalancerType from ..locations import BoundLocation from ..metrics import Metrics from ..networks import BoundNetwork from ..servers import BoundServer from .domain import ( CreateLoadBalancerResponse, GetMetricsResponse, IPv4Address, IPv6Network, LoadBalancer, LoadBalancerAlgorithm, LoadBalancerHealtCheckHttp, LoadBalancerHealthCheck, 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 class BoundLoadBalancer(BoundModelBase, LoadBalancer): _client: LoadBalancersClient model = LoadBalancer # pylint: disable=too-many-branches,too-many-locals def __init__(self, client: LoadBalancersClient, data: dict, complete: bool = True): algorithm = data.get("algorithm") if algorithm: 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._client.networks, {"id": private_net["network"]}, complete=False, ), ip=private_net["ip"], ) for private_net in private_nets ] data["private_net"] = private_nets targets = data.get("targets") if targets: tmp_targets = [] for target in targets: tmp_target = LoadBalancerTarget(type=target["type"]) if target["type"] == "server": tmp_target.server = BoundServer( client._client.servers, data=target["server"], complete=False ) tmp_target.use_private_ip = target["use_private_ip"] elif target["type"] == "label_selector": tmp_target.label_selector = LoadBalancerTargetLabelSelector( selector=target["label_selector"]["selector"] ) tmp_target.use_private_ip = target["use_private_ip"] elif target["type"] == "ip": tmp_target.ip = LoadBalancerTargetIP(ip=target["ip"]["ip"]) target_health_status = target.get("health_status") if target_health_status is not None: tmp_target.health_status = [ LoadBalancerTargetHealthStatus( listen_port=target_health_status_item["listen_port"], status=target_health_status_item["status"], ) for target_health_status_item in target_health_status ] tmp_targets.append(tmp_target) data["targets"] = tmp_targets 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._client.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 = LoadBalancerHealtCheckHttp( 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._client.load_balancer_types, load_balancer_type ) location = data.get("location") if location is not None: data["location"] = BoundLocation(client._client.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, 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[str] | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """Returns all action objects for a Load Balancer. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :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:`BoundAction `], :class:`Meta `) """ return self._client.get_actions_list(self, status, sort, page, per_page) def get_actions( self, status: list[str] | None = None, sort: list[str] | None = None, ) -> list[BoundAction]: """Returns all action objects for a Load Balancer. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._client.get_actions(self, status, sort) 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) 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) 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) 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) 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, 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) def attach_to_network( self, network: Network | BoundNetwork, ip: str | None = None, ) -> BoundAction: """Attaches a Load Balancer to a Network :param network: :class:`BoundNetwork ` or :class:`Network ` :param ip: str IP to request to be assigned to this Load Balancer :return: :class:`BoundAction ` """ return self._client.attach_to_network(self, network, ip) 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) 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) class LoadBalancersPageResult(NamedTuple): load_balancers: list[BoundLoadBalancer] meta: Meta | None class LoadBalancersClient(ClientEntityBase): _client: Client actions: ResourceActionsClient """Load Balancers scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self.actions = ResourceActionsClient(client, "/load_balancers") def get_by_id(self, id: int) -> BoundLoadBalancer: """Get a specific Load Balancer :param id: int :return: :class:`BoundLoadBalancer ` """ response = self._client.request( url=f"/load_balancers/{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="/load_balancers", 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(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="/load_balancers", method="POST", json=data) return CreateLoadBalancerResponse( load_balancer=BoundLoadBalancer(self, response["load_balancer"]), action=BoundAction(self._client.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"/load_balancers/{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"/load_balancers/{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"/load_balancers/{load_balancer.id}/metrics", method="GET", params=params, ) return GetMetricsResponse( metrics=Metrics(**response["metrics"]), ) def get_actions_list( self, load_balancer: LoadBalancer | BoundLoadBalancer, status: list[str] | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """Returns all action objects for a Load Balancer. :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :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:`BoundAction `], :class:`Meta `) """ 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( url=f"/load_balancers/{load_balancer.id}/actions", method="GET", params=params, ) actions = [ BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions( self, load_balancer: LoadBalancer | BoundLoadBalancer, status: list[str] | None = None, sort: list[str] | None = None, ) -> list[BoundAction]: """Returns all action objects for a Load Balancer. :param load_balancer: :class:`BoundLoadBalancer ` or :class:`LoadBalancer ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._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"/load_balancers/{load_balancer.id}/actions/add_service", method="POST", json=data, ) return BoundAction(self._client.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"/load_balancers/{load_balancer.id}/actions/update_service", method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def delete_service( self, load_balancer: 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"/load_balancers/{load_balancer.id}/actions/delete_service", method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def add_target( self, load_balancer: 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"/load_balancers/{load_balancer.id}/actions/add_target", method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def remove_target( self, load_balancer: 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"/load_balancers/{load_balancer.id}/actions/remove_target", method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def change_algorithm( self, load_balancer: 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"/load_balancers/{load_balancer.id}/actions/change_algorithm", method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def change_dns_ptr( self, load_balancer: 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"/load_balancers/{load_balancer.id}/actions/change_dns_ptr", method="POST", json={"ip": ip, "dns_ptr": dns_ptr}, ) return BoundAction(self._client.actions, response["action"]) def change_protection( self, load_balancer: 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"/load_balancers/{load_balancer.id}/actions/change_protection", method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def attach_to_network( self, load_balancer: LoadBalancer | BoundLoadBalancer, network: Network | BoundNetwork, ip: str | None = None, ) -> BoundAction: """Attach a Load Balancer to a Network. :param load_balancer: :class:` ` or :class:`LoadBalancer ` :param network: :class:`BoundNetwork ` or :class:`Network ` :param ip: str IP to request to be assigned to this Load Balancer :return: :class:`BoundAction ` """ data: dict[str, Any] = {"network": network.id} if ip is not None: data.update({"ip": ip}) response = self._client.request( url=f"/load_balancers/{load_balancer.id}/actions/attach_to_network", method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def detach_from_network( self, load_balancer: 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"/load_balancers/{load_balancer.id}/actions/detach_from_network", method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) def enable_public_interface( self, load_balancer: 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"/load_balancers/{load_balancer.id}/actions/enable_public_interface", method="POST", ) return BoundAction(self._client.actions, response["action"]) def disable_public_interface( self, load_balancer: 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"/load_balancers/{load_balancer.id}/actions/disable_public_interface", method="POST", ) return BoundAction(self._client.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"/load_balancers/{load_balancer.id}/actions/change_type", method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) hcloud-python-2.3.0/hcloud/load_balancers/domain.py000066400000000000000000000420741470147622500223600ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, Literal from dateutil.parser import isoparse 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 from ..servers import BoundServer from .client import BoundLoadBalancer 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 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". """ 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: LoadBalancerHealtCheckHttp HTTP Config """ def __init__( self, protocol: str | None = None, port: int | None = None, interval: int | None = None, timeout: int | None = None, retries: int | None = None, http: LoadBalancerHealtCheckHttp | None = None, ): self.protocol = protocol self.port = port self.interval = interval self.timeout = timeout self.retries = retries self.http = http class LoadBalancerHealtCheckHttp(BaseDomain): """LoadBalancerHealtCheckHttp 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 """ def __init__( self, domain: str | None = None, path: str | None = None, response: str | None = None, status_codes: list | None = None, tls: bool | None = None, ): self.domain = domain self.path = path self.response = response self.status_codes = status_codes self.tls = 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". """ 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, ): self.type = type self.server = server self.label_selector = label_selector self.ip = ip self.use_private_ip = use_private_ip self.health_status = health_status def to_payload(self) -> dict[str, Any]: """ 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 """ 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 """ def __init__(self, selector: str | None = None): self.selector = selector class LoadBalancerTargetIP(BaseDomain): """LoadBalancerTargetIP Domain :param ip: str Target IP """ 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 """ 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 hcloud-python-2.3.0/hcloud/locations/000077500000000000000000000000001470147622500175725ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/locations/__init__.py000066400000000000000000000002451470147622500217040ustar00rootroot00000000000000from __future__ import annotations from .client import BoundLocation, LocationsClient, LocationsPageResult # noqa: F401 from .domain import Location # noqa: F401 hcloud-python-2.3.0/hcloud/locations/client.py000066400000000000000000000051321470147622500214230ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import Location if TYPE_CHECKING: from .._client import Client class BoundLocation(BoundModelBase, Location): _client: LocationsClient model = Location class LocationsPageResult(NamedTuple): locations: list[BoundLocation] meta: Meta | None class LocationsClient(ClientEntityBase): _client: Client 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"/locations/{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="/locations", 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(name=name) hcloud-python-2.3.0/hcloud/locations/domain.py000066400000000000000000000030141470147622500214110ustar00rootroot00000000000000from __future__ import annotations from ..core import BaseDomain, DomainIdentityMixin 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 hcloud-python-2.3.0/hcloud/metrics/000077500000000000000000000000001470147622500172455ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/metrics/__init__.py000066400000000000000000000001321470147622500213520ustar00rootroot00000000000000from __future__ import annotations from .domain import Metrics, TimeSeries # noqa: F401 hcloud-python-2.3.0/hcloud/metrics/domain.py000066400000000000000000000022171470147622500210700ustar00rootroot00000000000000from __future__ import annotations from datetime import datetime from typing import Dict, List, Literal, Tuple from dateutil.parser import isoparse from ..core import BaseDomain TimeSeries = Dict[str, Dict[Literal["values"], List[Tuple[float, str]]]] class Metrics(BaseDomain): """Metrics Domain :param start: Start of period of metrics reported. :param end: End of period of metrics reported. :param step: Resolution of results in seconds. :param time_series: Dict with time series data, using the name of the time series as key. The metrics timestamps and values are stored in a list of tuples ``[(timestamp, value), ...]``. """ start: datetime end: datetime step: float time_series: TimeSeries __api_properties__ = ( "start", "end", "step", "time_series", ) __slots__ = __api_properties__ def __init__( self, start: str, end: str, step: float, time_series: TimeSeries, ): self.start = isoparse(start) self.end = isoparse(end) self.step = step self.time_series = time_series hcloud-python-2.3.0/hcloud/networks/000077500000000000000000000000001470147622500174535ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/networks/__init__.py000066400000000000000000000003521470147622500215640ustar00rootroot00000000000000from __future__ import annotations from .client import BoundNetwork, NetworksClient, NetworksPageResult # noqa: F401 from .domain import ( # noqa: F401 CreateNetworkResponse, Network, NetworkRoute, NetworkSubnet, ) hcloud-python-2.3.0/hcloud/networks/client.py000066400000000000000000000540461470147622500213140ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import Network, NetworkRoute, NetworkSubnet if TYPE_CHECKING: from .._client import Client class BoundNetwork(BoundModelBase, Network): _client: NetworksClient model = Network def __init__(self, client: NetworksClient, data: dict, complete: bool = True): subnets = data.get("subnets", []) if subnets is not None: subnets = [NetworkSubnet.from_dict(subnet) for subnet in subnets] 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._client.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[str] | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """Returns all action objects for a network. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :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:`BoundAction `], :class:`Meta `) """ return self._client.get_actions_list(self, status, sort, page, per_page) def get_actions( self, status: list[str] | None = None, sort: list[str] | None = None, ) -> list[BoundAction]: """Returns all action objects for a network. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._client.get_actions(self, status, sort) 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 | None class NetworksClient(ClientEntityBase): _client: Client actions: ResourceActionsClient """Networks scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self.actions = ResourceActionsClient(client, "/networks") def get_by_id(self, id: int) -> BoundNetwork: """Get a specific network :param id: int :return: :class:`BoundNetwork ` """ response = self._client.request(url=f"/networks/{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="/networks", 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(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="/networks", 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"/networks/{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"/networks/{network.id}", method="DELETE") return True def get_actions_list( self, network: Network | BoundNetwork, status: list[str] | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """Returns all action objects for a network. :param network: :class:`BoundNetwork ` or :class:`Network ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :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:`BoundAction `], :class:`Meta `) """ 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( url=f"/networks/{network.id}/actions", method="GET", params=params, ) actions = [ BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions( self, network: Network | BoundNetwork, status: list[str] | None = None, sort: list[str] | None = None, ) -> list[BoundAction]: """Returns all action objects for a network. :param network: :class:`BoundNetwork ` or :class:`Network ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._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"/networks/{network.id}/actions/add_subnet", method="POST", json=data, ) return BoundAction(self._client.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"/networks/{network.id}/actions/delete_subnet", method="POST", json=data, ) return BoundAction(self._client.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"/networks/{network.id}/actions/add_route", method="POST", json=data, ) return BoundAction(self._client.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"/networks/{network.id}/actions/delete_route", method="POST", json=data, ) return BoundAction(self._client.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"/networks/{network.id}/actions/change_ip_range", method="POST", json=data, ) return BoundAction(self._client.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"/networks/{network.id}/actions/change_protection", method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) hcloud-python-2.3.0/hcloud/networks/domain.py000066400000000000000000000116331470147622500213000ustar00rootroot00000000000000from __future__ import annotations import warnings from typing import TYPE_CHECKING from dateutil.parser import isoparse from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction from ..servers import BoundServer from .client import BoundNetwork 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: dict | None = None, labels: dict[str, str] | None = None, ): self.id = id self.name = name self.created = isoparse(created) if created else None 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 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/cloud/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 hcloud-python-2.3.0/hcloud/placement_groups/000077500000000000000000000000001470147622500211465ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/placement_groups/__init__.py000066400000000000000000000003541470147622500232610ustar00rootroot00000000000000from __future__ import annotations from .client import ( # noqa: F401 BoundPlacementGroup, PlacementGroupsClient, PlacementGroupsPageResult, ) from .domain import CreatePlacementGroupResponse, PlacementGroup # noqa: F401 hcloud-python-2.3.0/hcloud/placement_groups/client.py000066400000000000000000000171101470147622500227760ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import BoundAction from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import CreatePlacementGroupResponse, PlacementGroup if TYPE_CHECKING: from .._client import Client class BoundPlacementGroup(BoundModelBase, 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, 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 | None class PlacementGroupsClient(ClientEntityBase): _client: Client 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"/placement_groups/{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="/placement_groups", 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(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="/placement_groups", json=data, method="POST" ) action = None if response.get("action") is not None: action = BoundAction(self._client.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"/placement_groups/{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"/placement_groups/{placement_group.id}", method="DELETE", ) return True hcloud-python-2.3.0/hcloud/placement_groups/domain.py000066400000000000000000000042151470147622500227710ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from dateutil.parser import isoparse from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction from .client import BoundPlacementGroup 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 = isoparse(created) if created else None 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 hcloud-python-2.3.0/hcloud/primary_ips/000077500000000000000000000000001470147622500201355ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/primary_ips/__init__.py000066400000000000000000000003021470147622500222410ustar00rootroot00000000000000from __future__ import annotations from .client import BoundPrimaryIP, PrimaryIPsClient, PrimaryIPsPageResult # noqa: F401 from .domain import CreatePrimaryIPResponse, PrimaryIP # noqa: F401 hcloud-python-2.3.0/hcloud/primary_ips/client.py000066400000000000000000000336371470147622500220010ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import BoundAction, ResourceActionsClient from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import CreatePrimaryIPResponse, PrimaryIP if TYPE_CHECKING: from .._client import Client from ..datacenters import BoundDatacenter, Datacenter class BoundPrimaryIP(BoundModelBase, PrimaryIP): _client: PrimaryIPsClient model = PrimaryIP def __init__(self, client: PrimaryIPsClient, data: dict, complete: bool = True): # pylint: disable=import-outside-toplevel from ..datacenters import BoundDatacenter datacenter = data.get("datacenter", {}) if datacenter: data["datacenter"] = BoundDatacenter(client._client.datacenters, datacenter) super().__init__(client, data, complete) 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) 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_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, dns_ptr) class PrimaryIPsPageResult(NamedTuple): primary_ips: list[BoundPrimaryIP] meta: Meta | None class PrimaryIPsClient(ClientEntityBase): _client: Client actions: ResourceActionsClient """Primary IPs scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self.actions = ResourceActionsClient(client, "/primary_ips") def get_by_id(self, id: int) -> BoundPrimaryIP: """Returns a specific Primary IP object. :param id: int :return: :class:`BoundPrimaryIP ` """ response = self._client.request(url=f"/primary_ips/{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="/primary_ips", 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(name=name) def create( self, type: str, name: str, datacenter: Datacenter | BoundDatacenter | None = None, assignee_type: str | None = "server", assignee_id: int | None = None, auto_delete: bool | None = False, labels: dict | None = None, ) -> CreatePrimaryIPResponse: """Creates a new Primary IP assigned to a server. :param type: str Primary IP type Choices: ipv4, ipv6 :param name: str :param datacenter: Datacenter (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: data["datacenter"] = datacenter.id_or_name if assignee_id is not None: data["assignee_id"] = assignee_id if labels is not None: data["labels"] = labels response = self._client.request(url="/primary_ips", json=data, method="POST") action = None if response.get("action") is not None: action = BoundAction(self._client.actions, response["action"]) result = CreatePrimaryIPResponse( primary_ip=BoundPrimaryIP(self, response["primary_ip"]), action=action ) return result def update( self, primary_ip: 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"/primary_ips/{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"/primary_ips/{primary_ip.id}", method="DELETE", ) # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised 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"/primary_ips/{primary_ip.id}/actions/change_protection", method="POST", json=data, ) return BoundAction(self._client.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"/primary_ips/{primary_ip.id}/actions/assign", method="POST", json={"assignee_id": assignee_id, "assignee_type": assignee_type}, ) return BoundAction(self._client.actions, response["action"]) def unassign(self, primary_ip: 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"/primary_ips/{primary_ip.id}/actions/unassign", method="POST", ) return BoundAction(self._client.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"/primary_ips/{primary_ip.id}/actions/change_dns_ptr", method="POST", json={"ip": ip, "dns_ptr": dns_ptr}, ) return BoundAction(self._client.actions, response["action"]) hcloud-python-2.3.0/hcloud/primary_ips/domain.py000066400000000000000000000064631470147622500217670ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from dateutil.parser import isoparse from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction from ..datacenters import BoundDatacenter from .client import BoundPrimaryIP 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. :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. """ __api_properties__ = ( "id", "ip", "type", "dns_ptr", "datacenter", "blocked", "protection", "labels", "created", "name", "assignee_id", "assignee_type", "auto_delete", ) __slots__ = __api_properties__ def __init__( self, id: int | None = None, type: str | None = None, ip: str | None = None, dns_ptr: list[dict] | None = None, datacenter: BoundDatacenter | None = None, blocked: bool | None = None, protection: dict | None = None, labels: dict[str, dict] | None = None, created: str | None = None, name: str | None = None, assignee_id: int | None = None, assignee_type: str | None = None, auto_delete: bool | None = None, ): self.id = id self.type = type self.ip = ip self.dns_ptr = dns_ptr self.datacenter = datacenter self.blocked = blocked self.protection = protection self.labels = labels self.created = isoparse(created) if created else None self.name = name self.assignee_id = assignee_id self.assignee_type = assignee_type self.auto_delete = auto_delete class CreatePrimaryIPResponse(BaseDomain): """Create Primary IP Response Domain :param primary_ip: :class:`BoundPrimaryIP ` The Primary IP which was created :param action: :class:`BoundAction ` The Action which shows the progress of the Primary IP Creation """ __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 hcloud-python-2.3.0/hcloud/py.typed000066400000000000000000000000331470147622500172720ustar00rootroot00000000000000# Marker file for PEP 561. hcloud-python-2.3.0/hcloud/server_types/000077500000000000000000000000001470147622500203315ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/server_types/__init__.py000066400000000000000000000002761470147622500224470ustar00rootroot00000000000000from __future__ import annotations from .client import ( # noqa: F401 BoundServerType, ServerTypesClient, ServerTypesPageResult, ) from .domain import ServerType # noqa: F401 hcloud-python-2.3.0/hcloud/server_types/client.py000066400000000000000000000053221470147622500221630ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import ServerType if TYPE_CHECKING: from .._client import Client class BoundServerType(BoundModelBase, ServerType): _client: ServerTypesClient model = ServerType class ServerTypesPageResult(NamedTuple): server_types: list[BoundServerType] meta: Meta | None class ServerTypesClient(ClientEntityBase): _client: Client 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"/server_types/{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="/server_types", 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(name=name) hcloud-python-2.3.0/hcloud/server_types/domain.py000066400000000000000000000076201470147622500221570ustar00rootroot00000000000000from __future__ import annotations import warnings from ..core import BaseDomain, DomainIdentityMixin from ..deprecation import DeprecationInfo 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 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 """ __properties__ = ( "id", "name", "description", "cores", "memory", "disk", "prices", "storage_type", "cpu_type", "architecture", "deprecated", "deprecation", ) __api_properties__ = ( *__properties__, "included_traffic", ) __slots__ = ( *__properties__, "_included_traffic", ) def __init__( self, id: int | None = None, name: str | None = None, description: str | None = None, cores: int | None = None, memory: int | None = None, disk: int | None = None, prices: list[dict] | None = None, storage_type: str | None = None, cpu_type: str | None = None, architecture: str | None = None, deprecated: bool | None = None, deprecation: dict | None = None, included_traffic: int | None = None, ): self.id = id self.name = name self.description = description 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.deprecated = deprecated self.deprecation = ( DeprecationInfo.from_dict(deprecation) if deprecation is not None else None ) self.included_traffic = included_traffic @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 hcloud-python-2.3.0/hcloud/servers/000077500000000000000000000000001470147622500172705ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/servers/__init__.py000066400000000000000000000006501470147622500214020ustar00rootroot00000000000000from __future__ import annotations from .client import BoundServer, ServersClient, ServersPageResult # noqa: F401 from .domain import ( # noqa: F401 CreateServerResponse, EnableRescueResponse, GetMetricsResponse, IPv4Address, IPv6Network, PrivateNet, PublicNetwork, PublicNetworkFirewall, RequestConsoleResponse, ResetPasswordResponse, Server, ServerCreatePublicNetwork, ) hcloud-python-2.3.0/hcloud/servers/client.py000066400000000000000000001501561470147622500211300ustar00rootroot00000000000000from __future__ import annotations from datetime import datetime from typing import TYPE_CHECKING, Any, NamedTuple from dateutil.parser import isoparse from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient from ..core import BoundModelBase, ClientEntityBase, Meta from ..datacenters import BoundDatacenter from ..firewalls import BoundFirewall from ..floating_ips import BoundFloatingIP from ..images import BoundImage, CreateImageResponse from ..isos import BoundIso from ..metrics import Metrics from ..placement_groups import BoundPlacementGroup from ..primary_ips import BoundPrimaryIP from ..server_types import BoundServerType 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 ..locations import BoundLocation, Location from ..networks import BoundNetwork, Network from ..placement_groups import PlacementGroup from ..server_types import ServerType from ..ssh_keys import BoundSSHKey, SSHKey from ..volumes import Volume from .domain import ServerCreatePublicNetwork class BoundServer(BoundModelBase, Server): _client: ServersClient model = Server # pylint: disable=too-many-locals def __init__(self, client: ServersClient, data: dict, complete: bool = True): datacenter = data.get("datacenter") if datacenter is not None: data["datacenter"] = BoundDatacenter(client._client.datacenters, datacenter) volumes = data.get("volumes", []) if volumes: volumes = [ BoundVolume(client._client.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._client.images, image) iso = data.get("iso", None) if iso is not None: data["iso"] = BoundIso(client._client.isos, iso) server_type = data.get("server_type") if server_type is not None: data["server_type"] = BoundServerType( client._client.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._client.primary_ips, {"id": public_net["ipv4"]["id"]}, complete=False, ) if public_net["ipv4"] is not None else None ) ipv6_network = ( IPv6Network.from_dict(public_net["ipv6"]) if public_net["ipv6"] is not None else None ) ipv6_primary_ip = ( BoundPrimaryIP( client._client.primary_ips, {"id": public_net["ipv6"]["id"]}, complete=False, ) if public_net["ipv6"] is not None else None ) floating_ips = [ BoundFloatingIP( client._client.floating_ips, {"id": floating_ip}, complete=False ) for floating_ip in public_net["floating_ips"] ] firewalls = [ PublicNetworkFirewall( BoundFirewall( client._client.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._client.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._client.placement_groups, placement_group ) data["placement_group"] = placement_group super().__init__(client, data, complete) def get_actions_list( self, status: list[str] | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """Returns all action objects for a server. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :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:`BoundAction `], :class:`Meta `) """ return self._client.get_actions_list(self, status, sort, page, per_page) def get_actions( self, status: list[str] | None = None, sort: list[str] | None = None, ) -> list[BoundAction]: """Returns all action objects for a server. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._client.get_actions(self, status, sort) 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, 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, type, labels) def rebuild( self, image: Image | BoundImage, # 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 """ return self._client.rebuild(self, image) 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, 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) 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, 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, 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, ) -> 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. :return: :class:`BoundAction ` """ return self._client.attach_to_network(self, network, ip, alias_ips) 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) 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, 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) 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 | None class ServersClient(ClientEntityBase): _client: Client actions: ResourceActionsClient """Servers scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self.actions = ResourceActionsClient(client, "/servers") def get_by_id(self, id: int) -> BoundServer: """Get a specific server :param id: int :return: :class:`BoundServer ` """ response = self._client.request(url=f"/servers/{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="/servers", 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(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: 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="/servers", method="POST", json=data) result = CreateServerResponse( server=BoundServer(self, response["server"]), action=BoundAction(self._client.actions, response["action"]), next_actions=[ BoundAction(self._client.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[str] | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """Returns all action objects for a server. :param server: :class:`BoundServer ` or :class:`Server ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :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:`BoundAction `], :class:`Meta `) """ 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( url=f"/servers/{server.id}/actions", method="GET", params=params, ) actions = [ BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions( self, server: Server | BoundServer, status: list[str] | None = None, sort: list[str] | None = None, ) -> list[BoundAction]: """Returns all action objects for a server. :param server: :class:`BoundServer ` or :class:`Server ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._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"/servers/{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"/servers/{server.id}/metrics", method="GET", params=params, ) return GetMetricsResponse( metrics=Metrics(**response["metrics"]), ) def delete(self, server: Server | BoundServer) -> BoundAction: """Deletes a server. This immediately removes the server from your account, and it is no longer accessible. :param server: :class:`BoundServer ` or :class:`Server ` :return: :class:`BoundAction ` """ response = self._client.request(url=f"/servers/{server.id}", method="DELETE") return BoundAction(self._client.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"/servers/{server.id}/actions/poweroff", method="POST", ) return BoundAction(self._client.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"/servers/{server.id}/actions/poweron", method="POST", ) return BoundAction(self._client.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"/servers/{server.id}/actions/reboot", method="POST", ) return BoundAction(self._client.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"/servers/{server.id}/actions/reset", method="POST", ) return BoundAction(self._client.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"/servers/{server.id}/actions/shutdown", method="POST", ) return BoundAction(self._client.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"/servers/{server.id}/actions/reset_password", method="POST", ) return ResetPasswordResponse( action=BoundAction(self._client.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"/servers/{server.id}/actions/change_type", method="POST", json=data, ) return BoundAction(self._client.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"/servers/{server.id}/actions/enable_rescue", method="POST", json=data, ) return EnableRescueResponse( action=BoundAction(self._client.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"/servers/{server.id}/actions/disable_rescue", method="POST", ) return BoundAction(self._client.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"/servers/{server.id}/actions/create_image", method="POST", json=data, ) return CreateImageResponse( action=BoundAction(self._client.actions, response["action"]), image=BoundImage(self._client.images, response["image"]), ) def rebuild( self, server: Server | BoundServer, image: Image | BoundImage, # 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 """ data: dict[str, Any] = {"image": image.id_or_name} response = self._client.request( url=f"/servers/{server.id}/actions/rebuild", method="POST", json=data, ) return RebuildResponse( action=BoundAction(self._client.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"/servers/{server.id}/actions/enable_backup", method="POST", ) return BoundAction(self._client.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"/servers/{server.id}/actions/disable_backup", method="POST", ) return BoundAction(self._client.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"/servers/{server.id}/actions/attach_iso", method="POST", json=data, ) return BoundAction(self._client.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"/servers/{server.id}/actions/detach_iso", method="POST", ) return BoundAction(self._client.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"/servers/{server.id}/actions/change_dns_ptr", method="POST", json=data, ) return BoundAction(self._client.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"/servers/{server.id}/actions/change_protection", method="POST", json=data, ) return BoundAction(self._client.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"/servers/{server.id}/actions/request_console", method="POST", ) return RequestConsoleResponse( action=BoundAction(self._client.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, ) -> 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. :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}) response = self._client.request( url=f"/servers/{server.id}/actions/attach_to_network", method="POST", json=data, ) return BoundAction(self._client.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"/servers/{server.id}/actions/detach_from_network", method="POST", json=data, ) return BoundAction(self._client.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"/servers/{server.id}/actions/change_alias_ips", method="POST", json=data, ) return BoundAction(self._client.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": str(placement_group.id)} response = self._client.request( url=f"/servers/{server.id}/actions/add_to_placement_group", method="POST", json=data, ) return BoundAction(self._client.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"/servers/{server.id}/actions/remove_from_placement_group", method="POST", ) return BoundAction(self._client.actions, response["action"]) hcloud-python-2.3.0/hcloud/servers/domain.py000066400000000000000000000341141470147622500211140ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Literal from dateutil.parser import isoparse 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 ..metrics import Metrics from ..networks import BoundNetwork from ..placement_groups import BoundPlacementGroup from ..primary_ips import BoundPrimaryIP, PrimaryIP from ..server_types import BoundServerType from ..volumes import BoundVolume from .client import BoundServer class Server(BaseDomain, 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 ` :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""" __api_properties__ = ( "id", "name", "status", "public_net", "server_type", "datacenter", "image", "iso", "rescue_enabled", "locked", "backup_window", "outgoing_traffic", "ingoing_traffic", "included_traffic", "protection", "labels", "volumes", "private_net", "created", "primary_disk_size", "placement_group", ) __slots__ = __api_properties__ # 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, image: BoundImage | None = None, iso: BoundIso | None = None, rescue_enabled: bool | None = None, locked: bool | None = None, backup_window: str | None = None, outgoing_traffic: int | None = None, ingoing_traffic: int | None = None, included_traffic: int | None = None, protection: dict | None = None, labels: dict[str, str] | None = None, volumes: list[BoundVolume] | None = None, private_net: list[PrivateNet] | None = None, primary_disk_size: int | None = None, placement_group: BoundPlacementGroup | None = None, ): self.id = id self.name = name self.status = status self.created = isoparse(created) if created else None self.public_net = public_net self.server_type = server_type self.datacenter = datacenter 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 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, ipv6: IPv6Network, floating_ips: list[BoundFloatingIP], primary_ipv4: BoundPrimaryIP | None, primary_ipv6: BoundPrimaryIP | None, firewalls: list[PublicNetworkFirewall] | None = None, ): self.ipv4 = ipv4 self.ipv6 = ipv6 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, ): 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 hcloud-python-2.3.0/hcloud/ssh_keys/000077500000000000000000000000001470147622500174275ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/ssh_keys/__init__.py000066400000000000000000000002351470147622500215400ustar00rootroot00000000000000from __future__ import annotations from .client import BoundSSHKey, SSHKeysClient, SSHKeysPageResult # noqa: F401 from .domain import SSHKey # noqa: F401 hcloud-python-2.3.0/hcloud/ssh_keys/client.py000066400000000000000000000162501470147622500212630ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..core import BoundModelBase, ClientEntityBase, Meta from .domain import SSHKey if TYPE_CHECKING: from .._client import Client class BoundSSHKey(BoundModelBase, 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, 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 | None class SSHKeysClient(ClientEntityBase): _client: Client 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"/ssh_keys/{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="/ssh_keys", 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(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(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="/ssh_keys", 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"/ssh_keys/{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"/ssh_keys/{ssh_key.id}", method="DELETE") # Return always true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised return True hcloud-python-2.3.0/hcloud/ssh_keys/domain.py000066400000000000000000000023571470147622500212570ustar00rootroot00000000000000from __future__ import annotations from dateutil.parser import isoparse from ..core import BaseDomain, DomainIdentityMixin 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 = isoparse(created) if created else None hcloud-python-2.3.0/hcloud/volumes/000077500000000000000000000000001470147622500172715ustar00rootroot00000000000000hcloud-python-2.3.0/hcloud/volumes/__init__.py000066400000000000000000000002631470147622500214030ustar00rootroot00000000000000from __future__ import annotations from .client import BoundVolume, VolumesClient, VolumesPageResult # noqa: F401 from .domain import CreateVolumeResponse, Volume # noqa: F401 hcloud-python-2.3.0/hcloud/volumes/client.py000066400000000000000000000445531470147622500211340ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient from ..core import BoundModelBase, ClientEntityBase, Meta from ..locations import BoundLocation from .domain import CreateVolumeResponse, Volume if TYPE_CHECKING: from .._client import Client from ..locations import Location from ..servers import BoundServer, Server class BoundVolume(BoundModelBase, Volume): _client: VolumesClient model = Volume def __init__(self, client: VolumesClient, data: dict, complete: bool = True): location = data.get("location") if location is not None: data["location"] = BoundLocation(client._client.locations, location) # pylint: disable=import-outside-toplevel from ..servers import BoundServer server = data.get("server") if server is not None: data["server"] = BoundServer( client._client.servers, {"id": server}, complete=False ) super().__init__(client, data, complete) def get_actions_list( self, status: list[str] | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """Returns all action objects for a volume. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :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:`BoundAction `], :class:`Meta `) """ return self._client.get_actions_list(self, status, sort, page, per_page) def get_actions( self, status: list[str] | None = None, sort: list[str] | None = None, ) -> list[BoundAction]: """Returns all action objects for a volume. :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._client.get_actions(self, status, sort) 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, 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, 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) 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) class VolumesPageResult(NamedTuple): volumes: list[BoundVolume] meta: Meta | None class VolumesClient(ClientEntityBase): _client: Client actions: ResourceActionsClient """Volumes scoped actions client :type: :class:`ResourceActionsClient ` """ def __init__(self, client: Client): super().__init__(client) self.actions = ResourceActionsClient(client, "/volumes") def get_by_id(self, id: int) -> BoundVolume: """Get a specific volume by its id :param id: int :return: :class:`BoundVolume ` """ response = self._client.request(url=f"/volumes/{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="/volumes", 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(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="/volumes", json=data, method="POST") result = CreateVolumeResponse( volume=BoundVolume(self, response["volume"]), action=BoundAction(self._client.actions, response["action"]), next_actions=[ BoundAction(self._client.actions, action) for action in response["next_actions"] ], ) return result def get_actions_list( self, volume: Volume | BoundVolume, status: list[str] | None = None, sort: list[str] | None = None, page: int | None = None, per_page: int | None = None, ) -> ActionsPageResult: """Returns all action objects for a volume. :param volume: :class:`BoundVolume ` or :class:`Volume ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :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:`BoundAction `], :class:`Meta `) """ 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( url=f"/volumes/{volume.id}/actions", method="GET", params=params, ) actions = [ BoundAction(self._client.actions, action_data) for action_data in response["actions"] ] return ActionsPageResult(actions, Meta.parse_meta(response)) def get_actions( self, volume: Volume | BoundVolume, status: list[str] | None = None, sort: list[str] | None = None, ) -> list[BoundAction]: """Returns all action objects for a volume. :param volume: :class:`BoundVolume ` or :class:`Volume ` :param status: List[str] (optional) Response will have only actions with specified statuses. Choices: `running` `success` `error` :param sort: List[str] (optional) Specify how the results are sorted. Choices: `id` `id:asc` `id:desc` `command` `command:asc` `command:desc` `status` `status:asc` `status:desc` `progress` `progress:asc` `progress:desc` `started` `started:asc` `started:desc` `finished` `finished:asc` `finished:desc` :return: List[:class:`BoundAction `] """ return self._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"/volumes/{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"/volumes/{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"/volumes/{volume.id}/actions/resize", json={"size": size}, method="POST", ) return BoundAction(self._client.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"/volumes/{volume.id}/actions/attach", json=data, method="POST", ) return BoundAction(self._client.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"/volumes/{volume.id}/actions/detach", method="POST", ) return BoundAction(self._client.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"/volumes/{volume.id}/actions/change_protection", method="POST", json=data, ) return BoundAction(self._client.actions, response["action"]) hcloud-python-2.3.0/hcloud/volumes/domain.py000066400000000000000000000067461470147622500211270ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from dateutil.parser import isoparse from ..core import BaseDomain, DomainIdentityMixin if TYPE_CHECKING: from ..actions import BoundAction from ..locations import BoundLocation, Location from ..servers import BoundServer, Server from .client import BoundVolume class Volume(BaseDomain, DomainIdentityMixin): """Volume Domain :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: dict | None = None, labels: dict[str, str] | None = None, status: str | None = None, ): self.id = id self.name = name self.server = server self.created = isoparse(created) if created else None self.location = location self.size = size self.linux_device = linux_device self.format = format self.protection = protection self.labels = labels self.status = status 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 hcloud-python-2.3.0/pyproject.toml000066400000000000000000000014111470147622500172320ustar00rootroot00000000000000[tool.isort] profile = "black" combine_as_imports = true add_imports = ["from __future__ import annotations"] [tool.mypy] disallow_untyped_defs = true [tool.coverage.run] source = ["hcloud"] [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" [tool.pylint.main] py-version = "3.8" recursive = true jobs = 0 [tool.pylint.reports] output-format = "colorized" [tool.pylint."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", ] hcloud-python-2.3.0/renovate.json000066400000000000000000000002051470147622500170340ustar00rootroot00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["github>hetznercloud/.github//renovate/default"] } hcloud-python-2.3.0/setup.py000066400000000000000000000041441470147622500160360ustar00rootroot00000000000000from __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.3.0", 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 license", classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ], python_requires=">=3.8", install_requires=[ "python-dateutil>=2.7.5", "requests>=2.20", ], extras_require={ "docs": [ "sphinx>=8,<8.1", "sphinx-rtd-theme>=3,<3.1", "myst-parser>=4,<4.1", "watchdog>=5,<5.1", ], "test": [ "coverage>=7.6,<7.7", "pylint>=3,<3.4", "pytest>=8,<8.4", "pytest-cov>=5,<5.1", "mypy>=1.11,<1.12", "types-python-dateutil", "types-requests", ], }, include_package_data=True, packages=find_packages(exclude=["examples", "tests*", "docs"]), zip_safe=False, ) hcloud-python-2.3.0/tests/000077500000000000000000000000001470147622500154635ustar00rootroot00000000000000hcloud-python-2.3.0/tests/__init__.py000066400000000000000000000000001470147622500175620ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/000077500000000000000000000000001470147622500164425ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/__init__.py000066400000000000000000000000001470147622500205410ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/actions/000077500000000000000000000000001470147622500201025ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/actions/__init__.py000066400000000000000000000000001470147622500222010ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/actions/conftest.py000066400000000000000000000044441470147622500223070ustar00rootroot00000000000000from __future__ import annotations import pytest @pytest.fixture() def generic_action_list(): return { "actions": [ { "id": 1, "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"}, }, { "id": 2, "command": "stop_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 running_action(): return { "action": { "id": 2, "command": "stop_server", "status": "running", "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 successfully_action(): return { "action": { "id": 2, "command": "stop_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 failed_action(): return { "action": { "id": 2, "command": "stop_server", "status": "error", "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"}, } } hcloud-python-2.3.0/tests/unit/actions/test_client.py000066400000000000000000000155471470147622500230050ustar00rootroot00000000000000from __future__ import annotations from unittest import mock import pytest from hcloud.actions import ( Action, ActionFailedException, ActionsClient, ActionTimeoutException, BoundAction, ResourceActionsClient, ) class TestBoundAction: @pytest.fixture() def bound_running_action(self, mocked_requests): action_client = ActionsClient(client=mocked_requests) # Speed up tests that run `wait_until_finished` action_client._client._poll_interval_func = lambda _: 0.0 action_client._client._poll_max_retries = 3 return BoundAction( client=action_client, data=dict(id=14, status=Action.STATUS_RUNNING), ) def test_wait_until_finished( self, bound_running_action, mocked_requests, running_action, successfully_action ): mocked_requests.request.side_effect = [running_action, successfully_action] bound_running_action.wait_until_finished() mocked_requests.request.assert_called_with(url="/actions/2", method="GET") assert bound_running_action.status == "success" assert mocked_requests.request.call_count == 2 def test_wait_until_finished_with_error( self, bound_running_action, mocked_requests, running_action, failed_action ): mocked_requests.request.side_effect = [running_action, failed_action] with pytest.raises(ActionFailedException) as exception_info: bound_running_action.wait_until_finished() assert bound_running_action.status == "error" assert exception_info.value.action.id == 2 def test_wait_until_finished_max_retries( self, bound_running_action, mocked_requests, running_action, successfully_action ): mocked_requests.request.side_effect = [ running_action, running_action, successfully_action, ] with pytest.raises(ActionTimeoutException) as exception_info: bound_running_action.wait_until_finished(max_retries=1) assert bound_running_action.status == "running" assert exception_info.value.action.id == 2 assert mocked_requests.request.call_count == 1 class TestResourceActionsClient: @pytest.fixture() def actions_client(self): return ResourceActionsClient(client=mock.MagicMock(), resource="/resource") def test_get_by_id(self, actions_client, generic_action): actions_client._client.request.return_value = generic_action action = actions_client.get_by_id(1) actions_client._client.request.assert_called_with( url="/resource/actions/1", method="GET" ) assert action._client == actions_client._client.actions assert action.id == 1 assert action.command == "stop_server" @pytest.mark.parametrize( "params", [{}, {"status": ["active"], "sort": ["status"], "page": 2, "per_page": 10}], ) def test_get_list(self, actions_client, generic_action_list, params): actions_client._client.request.return_value = generic_action_list result = actions_client.get_list(**params) actions_client._client.request.assert_called_with( url="/resource/actions", method="GET", params=params ) assert result.meta is None actions = result.actions assert len(actions) == 2 action1 = actions[0] action2 = actions[1] assert action1._client == actions_client._client.actions assert action1.id == 1 assert action1.command == "start_server" assert action2._client == actions_client._client.actions assert action2.id == 2 assert action2.command == "stop_server" @pytest.mark.parametrize("params", [{}, {"status": ["active"], "sort": ["status"]}]) def test_get_all(self, actions_client, generic_action_list, params): actions_client._client.request.return_value = generic_action_list actions = actions_client.get_all(**params) params.update({"page": 1, "per_page": 50}) actions_client._client.request.assert_called_with( url="/resource/actions", method="GET", params=params ) assert len(actions) == 2 action1 = actions[0] action2 = actions[1] assert action1._client == actions_client._client.actions assert action1.id == 1 assert action1.command == "start_server" assert action2._client == actions_client._client.actions assert action2.id == 2 assert action2.command == "stop_server" class TestActionsClient: @pytest.fixture() def actions_client(self): return ActionsClient(client=mock.MagicMock()) def test_get_by_id(self, actions_client, generic_action): actions_client._client.request.return_value = generic_action action = actions_client.get_by_id(1) actions_client._client.request.assert_called_with( url="/actions/1", method="GET" ) assert action._client == actions_client._client.actions assert action.id == 1 assert action.command == "stop_server" @pytest.mark.parametrize( "params", [{}, {"status": ["active"], "sort": ["status"], "page": 2, "per_page": 10}], ) def test_get_list(self, actions_client, generic_action_list, params): actions_client._client.request.return_value = generic_action_list with pytest.deprecated_call(): result = actions_client.get_list(**params) actions_client._client.request.assert_called_with( url="/actions", method="GET", params=params ) assert result.meta is None actions = result.actions assert len(actions) == 2 action1 = actions[0] action2 = actions[1] assert action1._client == actions_client._client.actions assert action1.id == 1 assert action1.command == "start_server" assert action2._client == actions_client._client.actions assert action2.id == 2 assert action2.command == "stop_server" @pytest.mark.parametrize("params", [{}, {"status": ["active"], "sort": ["status"]}]) def test_get_all(self, actions_client, generic_action_list, params): actions_client._client.request.return_value = generic_action_list with pytest.deprecated_call(): actions = actions_client.get_all(**params) params.update({"page": 1, "per_page": 50}) actions_client._client.request.assert_called_with( url="/actions", method="GET", params=params ) assert len(actions) == 2 action1 = actions[0] action2 = actions[1] assert action1._client == actions_client._client.actions assert action1.id == 1 assert action1.command == "start_server" assert action2._client == actions_client._client.actions assert action2.id == 2 assert action2.command == "stop_server" hcloud-python-2.3.0/tests/unit/actions/test_domain.py000066400000000000000000000040001470147622500227540ustar00rootroot00000000000000from __future__ import annotations import datetime from datetime import timezone import pytest from hcloud.actions import ( Action, ActionException, ActionFailedException, ActionTimeoutException, ) 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, } ) ) hcloud-python-2.3.0/tests/unit/certificates/000077500000000000000000000000001470147622500211075ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/certificates/__init__.py000066400000000000000000000000001470147622500232060ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/certificates/conftest.py000066400000000000000000000143241470147622500233120ustar00rootroot00000000000000from __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"}, } } hcloud-python-2.3.0/tests/unit/certificates/test_client.py000066400000000000000000000315561470147622500240100ustar00rootroot00000000000000from __future__ import annotations from unittest import mock import pytest from hcloud.actions import BoundAction from hcloud.certificates import ( BoundCertificate, Certificate, CertificatesClient, ManagedCertificateStatus, ) class TestBoundCertificate: @pytest.fixture() def bound_certificate(self, hetzner_client): return BoundCertificate(client=hetzner_client.certificates, data=dict(id=14)) @pytest.mark.parametrize("params", [{"page": 1, "per_page": 10}, {}]) def test_get_actions_list( self, hetzner_client, bound_certificate, response_get_actions, params ): hetzner_client.request.return_value = response_get_actions result = bound_certificate.get_actions_list(**params) hetzner_client.request.assert_called_with( url="/certificates/14/actions", method="GET", params=params ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" def test_get_actions(self, hetzner_client, bound_certificate, response_get_actions): hetzner_client.request.return_value = response_get_actions actions = bound_certificate.get_actions() params = {"page": 1, "per_page": 50} hetzner_client.request.assert_called_with( url="/certificates/14/actions", method="GET", params=params ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" def test_bound_certificate_init(self, certificate_response): bound_certificate = BoundCertificate( client=mock.MagicMock(), data=certificate_response["certificate"] ) assert bound_certificate.id == 2323 assert bound_certificate.name == "My Certificate" assert bound_certificate.type == "managed" assert ( bound_certificate.fingerprint == "03:c7:55:9b:2a:d1:04:17:09:f6:d0:7f:18:34:63:d4:3e:5f" ) assert bound_certificate.certificate == "-----BEGIN CERTIFICATE-----\n..." assert len(bound_certificate.domain_names) == 3 assert bound_certificate.domain_names[0] == "example.com" assert bound_certificate.domain_names[1] == "webmail.example.com" assert bound_certificate.domain_names[2] == "www.example.com" assert isinstance(bound_certificate.status, ManagedCertificateStatus) assert bound_certificate.status.issuance == "failed" assert bound_certificate.status.renewal == "scheduled" assert bound_certificate.status.error.code == "error_code" assert bound_certificate.status.error.message == "error message" def test_update( self, hetzner_client, bound_certificate, response_update_certificate ): hetzner_client.request.return_value = response_update_certificate certificate = bound_certificate.update(name="New name") hetzner_client.request.assert_called_with( url="/certificates/14", method="PUT", json={"name": "New name"} ) assert certificate.id == 2323 assert certificate.name == "New name" def test_delete(self, hetzner_client, bound_certificate, generic_action): hetzner_client.request.return_value = generic_action delete_success = bound_certificate.delete() hetzner_client.request.assert_called_with( url="/certificates/14", method="DELETE" ) assert delete_success is True def test_retry_issuance( self, hetzner_client, bound_certificate, response_retry_issuance_action ): hetzner_client.request.return_value = response_retry_issuance_action action = bound_certificate.retry_issuance() hetzner_client.request.assert_called_with( url="/certificates/14/actions/retry", method="POST" ) assert action.id == 14 assert action.command == "issue_certificate" class TestCertificatesClient: @pytest.fixture() def certificates_client(self): return CertificatesClient(client=mock.MagicMock()) def test_get_by_id(self, certificates_client, certificate_response): certificates_client._client.request.return_value = certificate_response certificate = certificates_client.get_by_id(1) certificates_client._client.request.assert_called_with( url="/certificates/1", method="GET" ) 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, certificates_client, two_certificates_response, params): certificates_client._client.request.return_value = two_certificates_response result = certificates_client.get_list(**params) certificates_client._client.request.assert_called_with( url="/certificates", method="GET", 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, certificates_client, two_certificates_response, params): certificates_client._client.request.return_value = two_certificates_response certificates = certificates_client.get_all(**params) params.update({"page": 1, "per_page": 50}) certificates_client._client.request.assert_called_with( url="/certificates", method="GET", 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, certificates_client, one_certificates_response): certificates_client._client.request.return_value = one_certificates_response certificates = certificates_client.get_by_name("My Certificate") params = {"name": "My Certificate"} certificates_client._client.request.assert_called_with( url="/certificates", method="GET", params=params ) assert certificates._client is certificates_client assert certificates.id == 2323 assert certificates.name == "My Certificate" def test_create(self, certificates_client, certificate_response): certificates_client._client.request.return_value = certificate_response certificate = certificates_client.create( name="My Certificate", certificate="-----BEGIN CERTIFICATE-----\n...", private_key="-----BEGIN PRIVATE KEY-----\n...", ) certificates_client._client.request.assert_called_with( url="/certificates", method="POST", 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, certificates_client, create_managed_certificate_response ): certificates_client._client.request.return_value = ( create_managed_certificate_response ) create_managed_certificate_rsp = certificates_client.create_managed( name="My Certificate", domain_names=["example.com", "*.example.org"] ) certificates_client._client.request.assert_called_with( url="/certificates", method="POST", 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, certificates_client, certificate, response_update_certificate ): certificates_client._client.request.return_value = response_update_certificate certificate = certificates_client.update(certificate, name="New name") certificates_client._client.request.assert_called_with( url="/certificates/1", method="PUT", 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, certificates_client, certificate, generic_action): certificates_client._client.request.return_value = generic_action delete_success = certificates_client.delete(certificate) certificates_client._client.request.assert_called_with( url="/certificates/1", method="DELETE" ) assert delete_success is True @pytest.mark.parametrize( "certificate", [Certificate(id=1), BoundCertificate(mock.MagicMock(), dict(id=1))], ) def test_retry_issuance( self, certificates_client, certificate, response_retry_issuance_action ): certificates_client._client.request.return_value = ( response_retry_issuance_action ) action = certificates_client.retry_issuance(certificate) certificates_client._client.request.assert_called_with( url="/certificates/1/actions/retry", method="POST" ) assert action.id == 14 assert action.command == "issue_certificate" def test_actions_get_by_id(self, certificates_client, response_get_actions): certificates_client._client.request.return_value = { "action": response_get_actions["actions"][0] } action = certificates_client.actions.get_by_id(13) certificates_client._client.request.assert_called_with( url="/certificates/actions/13", method="GET" ) assert isinstance(action, BoundAction) assert action._client == certificates_client._client.actions assert action.id == 13 assert action.command == "change_protection" def test_actions_get_list(self, certificates_client, response_get_actions): certificates_client._client.request.return_value = response_get_actions result = certificates_client.actions.get_list() certificates_client._client.request.assert_called_with( url="/certificates/actions", method="GET", params={}, ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == certificates_client._client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" def test_actions_get_all(self, certificates_client, response_get_actions): certificates_client._client.request.return_value = response_get_actions actions = certificates_client.actions.get_all() certificates_client._client.request.assert_called_with( url="/certificates/actions", method="GET", params={"page": 1, "per_page": 50}, ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == certificates_client._client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" hcloud-python-2.3.0/tests/unit/certificates/test_domain.py000066400000000000000000000014231470147622500237670ustar00rootroot00000000000000from __future__ import annotations import datetime from datetime import timezone from hcloud.certificates import Certificate 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 ) hcloud-python-2.3.0/tests/unit/conftest.py000066400000000000000000000016261470147622500206460ustar00rootroot00000000000000from __future__ import annotations from unittest import mock import pytest from hcloud import Client @pytest.fixture(autouse=True, scope="function") def mocked_requests(): patcher = mock.patch("hcloud._client.requests") mocked_requests = patcher.start() yield mocked_requests patcher.stop() @pytest.fixture() def generic_action(): return { "action": { "id": 1, "command": "stop_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 hetzner_client(): client = Client(token="token") patcher = mock.patch.object(client, "request") patcher.start() yield client patcher.stop() hcloud-python-2.3.0/tests/unit/core/000077500000000000000000000000001470147622500173725ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/core/__init__.py000066400000000000000000000000001470147622500214710ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/core/test_client.py000066400000000000000000000166321470147622500222710ustar00rootroot00000000000000from __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, ClientEntityBase, Meta 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 class TestClientEntityBase: @pytest.fixture() def client_class_constructor(self): def constructor(json_content_function): class CandiesPageResult(NamedTuple): candies: list[Any] meta: Meta class CandiesClient(ClientEntityBase): 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(ClientEntityBase): 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(p): 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(p): 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(p): return json_content candies_client = client_class_constructor(json_content_function) result = candies_client._get_first_by(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(p): return json_content candies_client = client_class_constructor(json_content_function) result = candies_client._get_first_by(status="sweet") assert result is None hcloud-python-2.3.0/tests/unit/core/test_domain.py000066400000000000000000000116031470147622500222530ustar00rootroot00000000000000from __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 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( "other, expected", [ (SomeDomain(id=1), True), (SomeDomain(name="name1"), True), (SomeDomain(id=1, name="name1"), True), (SomeDomain(id=2), False), (SomeDomain(name="name2"), False), (SomeDomain(id=2, name="name2"), False), ], ) def test_has_id_or_name_exception(self, other, expected): domain = SomeDomain(id=1, name="name1") assert domain.has_id_or_name(other.id_or_name) == expected class ActionDomain(BaseDomain, DomainIdentityMixin): __api_properties__ = ("id", "name", "started") __slots__ = __api_properties__ def __init__(self, id, name="name1", started=None): self.id = id self.name = name self.started = isoparse(started) if started else None 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 hcloud-python-2.3.0/tests/unit/datacenters/000077500000000000000000000000001470147622500207375ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/datacenters/__init__.py000066400000000000000000000000001470147622500230360ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/datacenters/conftest.py000066400000000000000000000057471470147622500231530ustar00rootroot00000000000000from __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, } hcloud-python-2.3.0/tests/unit/datacenters/test_client.py000066400000000000000000000133731470147622500236350ustar00rootroot00000000000000from __future__ import annotations from unittest import mock # noqa: F401 import pytest # noqa: F401 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): return DatacentersClient(client=mock.MagicMock()) def test_get_by_id(self, datacenters_client, datacenter_response): datacenters_client._client.request.return_value = datacenter_response datacenter = datacenters_client.get_by_id(1) datacenters_client._client.request.assert_called_with( url="/datacenters/1", method="GET" ) 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, datacenters_client, two_datacenters_response, params): datacenters_client._client.request.return_value = two_datacenters_response result = datacenters_client.get_list(**params) datacenters_client._client.request.assert_called_with( url="/datacenters", method="GET", params=params ) datacenters = result.datacenters assert result.meta is 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, datacenters_client, two_datacenters_response, params): datacenters_client._client.request.return_value = two_datacenters_response datacenters = datacenters_client.get_all(**params) params.update({"page": 1, "per_page": 50}) datacenters_client._client.request.assert_called_with( url="/datacenters", method="GET", 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, datacenters_client, one_datacenters_response): datacenters_client._client.request.return_value = one_datacenters_response datacenter = datacenters_client.get_by_name("fsn1-dc8") params = {"name": "fsn1-dc8"} datacenters_client._client.request.assert_called_with( url="/datacenters", method="GET", params=params ) assert datacenter._client is datacenters_client assert datacenter.id == 1 assert datacenter.name == "fsn1-dc8" assert isinstance(datacenter.location, BoundLocation) hcloud-python-2.3.0/tests/unit/firewalls/000077500000000000000000000000001470147622500204325ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/firewalls/__init__.py000066400000000000000000000000001470147622500225310ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/firewalls/conftest.py000066400000000000000000000214411470147622500226330ustar00rootroot00000000000000from __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"}, }, ] } hcloud-python-2.3.0/tests/unit/firewalls/test_client.py000066400000000000000000000453661470147622500233370ustar00rootroot00000000000000from __future__ import annotations from unittest import mock import pytest from hcloud.actions import BoundAction from hcloud.firewalls import ( BoundFirewall, Firewall, FirewallResource, FirewallResourceLabelSelector, FirewallRule, FirewallsClient, ) from hcloud.servers import Server class TestBoundFirewall: @pytest.fixture() def bound_firewall(self, hetzner_client): return BoundFirewall(client=hetzner_client.firewalls, data=dict(id=1)) def test_bound_firewall_init(self, firewall_response): bound_firewall = BoundFirewall( client=mock.MagicMock(), data=firewall_response["firewall"] ) assert bound_firewall.id == 38 assert bound_firewall.name == "Corporate Intranet Protection" assert bound_firewall.labels == {} assert isinstance(bound_firewall.rules, list) assert len(bound_firewall.rules) == 2 assert isinstance(bound_firewall.applied_to, list) assert len(bound_firewall.applied_to) == 2 assert bound_firewall.applied_to[0].server.id == 42 assert bound_firewall.applied_to[0].type == "server" assert bound_firewall.applied_to[1].label_selector.selector == "key==value" assert bound_firewall.applied_to[1].type == "label_selector" firewall_in_rule = bound_firewall.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 = bound_firewall.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" @pytest.mark.parametrize( "params", [{}, {"sort": ["created"], "page": 1, "per_page": 2}] ) def test_get_actions_list( self, hetzner_client, bound_firewall, response_get_actions, params ): hetzner_client.request.return_value = response_get_actions result = bound_firewall.get_actions_list(**params) hetzner_client.request.assert_called_with( url="/firewalls/1/actions", method="GET", params=params ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "set_firewall_rules" @pytest.mark.parametrize("params", [{}, {"sort": ["created"]}]) def test_get_actions( self, hetzner_client, bound_firewall, response_get_actions, params ): hetzner_client.request.return_value = response_get_actions actions = bound_firewall.get_actions(**params) params.update({"page": 1, "per_page": 50}) hetzner_client.request.assert_called_with( url="/firewalls/1/actions", method="GET", params=params ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "set_firewall_rules" def test_update(self, hetzner_client, bound_firewall, response_update_firewall): hetzner_client.request.return_value = response_update_firewall firewall = bound_firewall.update( name="New Corporate Intranet Protection", labels={} ) hetzner_client.request.assert_called_with( url="/firewalls/1", method="PUT", json={"name": "New Corporate Intranet Protection", "labels": {}}, ) assert firewall.id == 38 assert firewall.name == "New Corporate Intranet Protection" def test_delete(self, hetzner_client, bound_firewall): delete_success = bound_firewall.delete() hetzner_client.request.assert_called_with(url="/firewalls/1", method="DELETE") assert delete_success is True def test_set_rules(self, hetzner_client, bound_firewall, response_set_rules): hetzner_client.request.return_value = response_set_rules actions = bound_firewall.set_rules( [ FirewallRule( direction=FirewallRule.DIRECTION_IN, protocol=FirewallRule.PROTOCOL_ICMP, source_ips=["0.0.0.0/0", "::/0"], description="New firewall description", ) ] ) hetzner_client.request.assert_called_with( url="/firewalls/1/actions/set_rules", method="POST", json={ "rules": [ { "direction": "in", "protocol": "icmp", "source_ips": ["0.0.0.0/0", "::/0"], "description": "New firewall description", } ] }, ) assert actions[0].id == 13 assert actions[0].progress == 100 def test_apply_to_resources( self, hetzner_client, bound_firewall, response_set_rules ): hetzner_client.request.return_value = response_set_rules actions = bound_firewall.apply_to_resources( [FirewallResource(type=FirewallResource.TYPE_SERVER, server=Server(id=5))] ) hetzner_client.request.assert_called_with( url="/firewalls/1/actions/apply_to_resources", method="POST", json={"apply_to": [{"type": "server", "server": {"id": 5}}]}, ) assert actions[0].id == 13 assert actions[0].progress == 100 def test_remove_from_resources( self, hetzner_client, bound_firewall, response_set_rules ): hetzner_client.request.return_value = response_set_rules actions = bound_firewall.remove_from_resources( [FirewallResource(type=FirewallResource.TYPE_SERVER, server=Server(id=5))] ) hetzner_client.request.assert_called_with( url="/firewalls/1/actions/remove_from_resources", method="POST", json={"remove_from": [{"type": "server", "server": {"id": 5}}]}, ) assert actions[0].id == 13 assert actions[0].progress == 100 class TestFirewallsClient: @pytest.fixture() def firewalls_client(self): return FirewallsClient(client=mock.MagicMock()) def test_get_by_id(self, firewalls_client, firewall_response): firewalls_client._client.request.return_value = firewall_response firewall = firewalls_client.get_by_id(1) firewalls_client._client.request.assert_called_with( url="/firewalls/1", method="GET" ) 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, firewalls_client, two_firewalls_response, params): firewalls_client._client.request.return_value = two_firewalls_response result = firewalls_client.get_list(**params) firewalls_client._client.request.assert_called_with( url="/firewalls", method="GET", params=params ) firewalls = result.firewalls assert result.meta is 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, firewalls_client, two_firewalls_response, params): firewalls_client._client.request.return_value = two_firewalls_response firewalls = firewalls_client.get_all(**params) params.update({"page": 1, "per_page": 50}) firewalls_client._client.request.assert_called_with( url="/firewalls", method="GET", 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, firewalls_client, one_firewalls_response): firewalls_client._client.request.return_value = one_firewalls_response firewall = firewalls_client.get_by_name("Corporate Intranet Protection") params = {"name": "Corporate Intranet Protection"} firewalls_client._client.request.assert_called_with( url="/firewalls", method="GET", params=params ) assert firewall._client is firewalls_client assert firewall.id == 38 assert firewall.name == "Corporate Intranet Protection" @pytest.mark.parametrize( "firewall", [Firewall(id=1), BoundFirewall(mock.MagicMock(), dict(id=1))] ) def test_get_actions_list(self, firewalls_client, firewall, response_get_actions): firewalls_client._client.request.return_value = response_get_actions result = firewalls_client.get_actions_list(firewall) firewalls_client._client.request.assert_called_with( url="/firewalls/1/actions", method="GET", params={} ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == firewalls_client._client.actions assert actions[0].id == 13 assert actions[0].command == "set_firewall_rules" def test_create(self, firewalls_client, response_create_firewall): firewalls_client._client.request.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"), ), ], ) firewalls_client._client.request.assert_called_with( url="/firewalls", method="POST", 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, firewalls_client, firewall, response_update_firewall): firewalls_client._client.request.return_value = response_update_firewall firewall = firewalls_client.update( firewall, name="New Corporate Intranet Protection", labels={} ) firewalls_client._client.request.assert_called_with( url="/firewalls/38", method="PUT", 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, firewalls_client, firewall, response_set_rules): firewalls_client._client.request.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"], ) ], ) firewalls_client._client.request.assert_called_with( url="/firewalls/1/actions/set_rules", method="POST", json={ "rules": [ { "direction": "in", "protocol": "icmp", "source_ips": ["0.0.0.0/0", "::/0"], } ] }, ) 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, firewalls_client, firewall): delete_success = firewalls_client.delete(firewall) firewalls_client._client.request.assert_called_with( url="/firewalls/1", method="DELETE" ) assert delete_success is True @pytest.mark.parametrize( "firewall", [Firewall(id=1), BoundFirewall(mock.MagicMock(), dict(id=1))] ) def test_apply_to_resources(self, firewalls_client, firewall, response_set_rules): firewalls_client._client.request.return_value = response_set_rules actions = firewalls_client.apply_to_resources( firewall, [FirewallResource(type=FirewallResource.TYPE_SERVER, server=Server(id=5))], ) firewalls_client._client.request.assert_called_with( url="/firewalls/1/actions/apply_to_resources", method="POST", 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, firewalls_client, firewall, response_set_rules ): firewalls_client._client.request.return_value = response_set_rules actions = firewalls_client.remove_from_resources( firewall, [FirewallResource(type=FirewallResource.TYPE_SERVER, server=Server(id=5))], ) firewalls_client._client.request.assert_called_with( url="/firewalls/1/actions/remove_from_resources", method="POST", json={"remove_from": [{"type": "server", "server": {"id": 5}}]}, ) assert actions[0].id == 13 assert actions[0].progress == 100 def test_actions_get_by_id(self, firewalls_client, response_get_actions): firewalls_client._client.request.return_value = { "action": response_get_actions["actions"][0] } action = firewalls_client.actions.get_by_id(13) firewalls_client._client.request.assert_called_with( url="/firewalls/actions/13", method="GET" ) assert isinstance(action, BoundAction) assert action._client == firewalls_client._client.actions assert action.id == 13 assert action.command == "set_firewall_rules" def test_actions_get_list(self, firewalls_client, response_get_actions): firewalls_client._client.request.return_value = response_get_actions result = firewalls_client.actions.get_list() firewalls_client._client.request.assert_called_with( url="/firewalls/actions", method="GET", params={}, ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == firewalls_client._client.actions assert actions[0].id == 13 assert actions[0].command == "set_firewall_rules" def test_actions_get_all(self, firewalls_client, response_get_actions): firewalls_client._client.request.return_value = response_get_actions actions = firewalls_client.actions.get_all() firewalls_client._client.request.assert_called_with( url="/firewalls/actions", method="GET", params={"page": 1, "per_page": 50}, ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == firewalls_client._client.actions assert actions[0].id == 13 assert actions[0].command == "set_firewall_rules" hcloud-python-2.3.0/tests/unit/firewalls/test_domain.py000066400000000000000000000005601470147622500233130ustar00rootroot00000000000000from __future__ import annotations import datetime from datetime import timezone from hcloud.firewalls import Firewall 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 ) hcloud-python-2.3.0/tests/unit/floating_ips/000077500000000000000000000000001470147622500211205ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/floating_ips/__init__.py000066400000000000000000000000001470147622500232170ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/floating_ips/conftest.py000066400000000000000000000140571470147622500233260ustar00rootroot00000000000000from __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"}, } ] } hcloud-python-2.3.0/tests/unit/floating_ips/test_client.py000066400000000000000000000434501470147622500240150ustar00rootroot00000000000000from __future__ import annotations from unittest import mock import pytest from hcloud.actions import BoundAction from hcloud.floating_ips import BoundFloatingIP, FloatingIP, FloatingIPsClient from hcloud.locations import BoundLocation, Location from hcloud.servers import BoundServer, Server class TestBoundFloatingIP: @pytest.fixture() def bound_floating_ip(self, hetzner_client): return BoundFloatingIP(client=hetzner_client.floating_ips, data=dict(id=14)) def test_bound_floating_ip_init(self, floating_ip_response): bound_floating_ip = BoundFloatingIP( client=mock.MagicMock(), data=floating_ip_response["floating_ip"] ) assert bound_floating_ip.id == 4711 assert bound_floating_ip.description == "Web Frontend" assert bound_floating_ip.name == "Web Frontend" assert bound_floating_ip.ip == "131.232.99.1" assert bound_floating_ip.type == "ipv4" assert bound_floating_ip.protection == {"delete": False} assert bound_floating_ip.labels == {} assert bound_floating_ip.blocked is False assert isinstance(bound_floating_ip.server, BoundServer) assert bound_floating_ip.server.id == 42 assert isinstance(bound_floating_ip.home_location, BoundLocation) assert bound_floating_ip.home_location.id == 1 assert bound_floating_ip.home_location.name == "fsn1" assert bound_floating_ip.home_location.description == "Falkenstein DC Park 1" assert bound_floating_ip.home_location.country == "DE" assert bound_floating_ip.home_location.city == "Falkenstein" assert bound_floating_ip.home_location.latitude == 50.47612 assert bound_floating_ip.home_location.longitude == 12.370071 def test_get_actions(self, hetzner_client, bound_floating_ip, response_get_actions): hetzner_client.request.return_value = response_get_actions actions = bound_floating_ip.get_actions(sort="id") hetzner_client.request.assert_called_with( url="/floating_ips/14/actions", method="GET", params={"sort": "id", "page": 1, "per_page": 50}, ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "assign_floating_ip" def test_update( self, hetzner_client, bound_floating_ip, response_update_floating_ip ): hetzner_client.request.return_value = response_update_floating_ip floating_ip = bound_floating_ip.update( description="New description", name="New name" ) hetzner_client.request.assert_called_with( url="/floating_ips/14", method="PUT", json={"description": "New description", "name": "New name"}, ) assert floating_ip.id == 4711 assert floating_ip.description == "New description" assert floating_ip.name == "New name" def test_delete(self, hetzner_client, bound_floating_ip, generic_action): hetzner_client.request.return_value = generic_action delete_success = bound_floating_ip.delete() hetzner_client.request.assert_called_with( url="/floating_ips/14", method="DELETE" ) assert delete_success is True def test_change_protection(self, hetzner_client, bound_floating_ip, generic_action): hetzner_client.request.return_value = generic_action action = bound_floating_ip.change_protection(True) hetzner_client.request.assert_called_with( url="/floating_ips/14/actions/change_protection", method="POST", json={"delete": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", (Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))) ) def test_assign(self, hetzner_client, bound_floating_ip, server, generic_action): hetzner_client.request.return_value = generic_action action = bound_floating_ip.assign(server) hetzner_client.request.assert_called_with( url="/floating_ips/14/actions/assign", method="POST", json={"server": 1} ) assert action.id == 1 assert action.progress == 0 def test_unassign(self, hetzner_client, bound_floating_ip, generic_action): hetzner_client.request.return_value = generic_action action = bound_floating_ip.unassign() hetzner_client.request.assert_called_with( url="/floating_ips/14/actions/unassign", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_change_dns_ptr(self, hetzner_client, bound_floating_ip, generic_action): hetzner_client.request.return_value = generic_action action = bound_floating_ip.change_dns_ptr("1.2.3.4", "server02.example.com") hetzner_client.request.assert_called_with( url="/floating_ips/14/actions/change_dns_ptr", method="POST", json={"ip": "1.2.3.4", "dns_ptr": "server02.example.com"}, ) assert action.id == 1 assert action.progress == 0 class TestFloatingIPsClient: @pytest.fixture() def floating_ips_client(self): return FloatingIPsClient(client=mock.MagicMock()) def test_get_by_id(self, floating_ips_client, floating_ip_response): floating_ips_client._client.request.return_value = floating_ip_response bound_floating_ip = floating_ips_client.get_by_id(1) floating_ips_client._client.request.assert_called_with( url="/floating_ips/1", method="GET" ) 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, floating_ips_client, one_floating_ips_response): floating_ips_client._client.request.return_value = one_floating_ips_response bound_floating_ip = floating_ips_client.get_by_name("Web Frontend") floating_ips_client._client.request.assert_called_with( url="/floating_ips", method="GET", 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, floating_ips_client, two_floating_ips_response, params): floating_ips_client._client.request.return_value = two_floating_ips_response result = floating_ips_client.get_list(**params) floating_ips_client._client.request.assert_called_with( url="/floating_ips", method="GET", params=params ) bound_floating_ips = result.floating_ips assert result.meta is 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, floating_ips_client, two_floating_ips_response, params): floating_ips_client._client.request.return_value = two_floating_ips_response bound_floating_ips = floating_ips_client.get_all(**params) params.update({"page": 1, "per_page": 50}) floating_ips_client._client.request.assert_called_with( url="/floating_ips", method="GET", 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, floating_ips_client, floating_ip_response): floating_ips_client._client.request.return_value = floating_ip_response response = floating_ips_client.create( "ipv6", "Web Frontend", home_location=Location(name="location") ) floating_ips_client._client.request.assert_called_with( url="/floating_ips", method="POST", 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, floating_ips_client, server, floating_ip_create_response ): floating_ips_client._client.request.return_value = floating_ip_create_response response = floating_ips_client.create( type="ipv6", description="Web Frontend", server=server ) floating_ips_client._client.request.assert_called_with( url="/floating_ips", method="POST", 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 @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_create_with_name( self, floating_ips_client, server, floating_ip_create_response ): floating_ips_client._client.request.return_value = floating_ip_create_response response = floating_ips_client.create( type="ipv6", description="Web Frontend", name="Web Frontend" ) floating_ips_client._client.request.assert_called_with( url="/floating_ips", method="POST", 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_get_actions(self, floating_ips_client, floating_ip, response_get_actions): floating_ips_client._client.request.return_value = response_get_actions actions = floating_ips_client.get_actions(floating_ip) floating_ips_client._client.request.assert_called_with( url="/floating_ips/1/actions", method="GET", params={"page": 1, "per_page": 50}, ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == floating_ips_client._client.actions assert actions[0].id == 13 assert actions[0].command == "assign_floating_ip" @pytest.mark.parametrize( "floating_ip", [FloatingIP(id=1), BoundFloatingIP(mock.MagicMock(), dict(id=1))] ) def test_update( self, floating_ips_client, floating_ip, response_update_floating_ip ): floating_ips_client._client.request.return_value = response_update_floating_ip floating_ip = floating_ips_client.update( floating_ip, description="New description", name="New name" ) floating_ips_client._client.request.assert_called_with( url="/floating_ips/1", method="PUT", 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, floating_ips_client, floating_ip, generic_action): floating_ips_client._client.request.return_value = generic_action action = floating_ips_client.change_protection(floating_ip, True) floating_ips_client._client.request.assert_called_with( url="/floating_ips/1/actions/change_protection", method="POST", 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, floating_ips_client, floating_ip, generic_action): floating_ips_client._client.request.return_value = generic_action delete_success = floating_ips_client.delete(floating_ip) floating_ips_client._client.request.assert_called_with( url="/floating_ips/1", method="DELETE" ) 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, floating_ips_client, server, floating_ip, generic_action): floating_ips_client._client.request.return_value = generic_action action = floating_ips_client.assign(floating_ip, server) floating_ips_client._client.request.assert_called_with( url="/floating_ips/12/actions/assign", method="POST", 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, floating_ips_client, floating_ip, generic_action): floating_ips_client._client.request.return_value = generic_action action = floating_ips_client.unassign(floating_ip) floating_ips_client._client.request.assert_called_with( url="/floating_ips/12/actions/unassign", method="POST" ) 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, floating_ips_client, floating_ip, generic_action): floating_ips_client._client.request.return_value = generic_action action = floating_ips_client.change_dns_ptr( floating_ip, "1.2.3.4", "server02.example.com" ) floating_ips_client._client.request.assert_called_with( url="/floating_ips/12/actions/change_dns_ptr", method="POST", json={"ip": "1.2.3.4", "dns_ptr": "server02.example.com"}, ) assert action.id == 1 assert action.progress == 0 def test_actions_get_by_id(self, floating_ips_client, response_get_actions): floating_ips_client._client.request.return_value = { "action": response_get_actions["actions"][0] } action = floating_ips_client.actions.get_by_id(13) floating_ips_client._client.request.assert_called_with( url="/floating_ips/actions/13", method="GET" ) assert isinstance(action, BoundAction) assert action._client == floating_ips_client._client.actions assert action.id == 13 assert action.command == "assign_floating_ip" def test_actions_get_list(self, floating_ips_client, response_get_actions): floating_ips_client._client.request.return_value = response_get_actions result = floating_ips_client.actions.get_list() floating_ips_client._client.request.assert_called_with( url="/floating_ips/actions", method="GET", params={}, ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == floating_ips_client._client.actions assert actions[0].id == 13 assert actions[0].command == "assign_floating_ip" def test_actions_get_all(self, floating_ips_client, response_get_actions): floating_ips_client._client.request.return_value = response_get_actions actions = floating_ips_client.actions.get_all() floating_ips_client._client.request.assert_called_with( url="/floating_ips/actions", method="GET", params={"page": 1, "per_page": 50}, ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == floating_ips_client._client.actions assert actions[0].id == 13 assert actions[0].command == "assign_floating_ip" hcloud-python-2.3.0/tests/unit/floating_ips/test_domain.py000066400000000000000000000005751470147622500240070ustar00rootroot00000000000000from __future__ import annotations import datetime from datetime import timezone from hcloud.floating_ips import FloatingIP class TestFloatingIP: def test_created_is_datetime(self): floatingIP = FloatingIP(id=1, created="2016-01-30T23:50+00:00") assert floatingIP.created == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) hcloud-python-2.3.0/tests/unit/helpers/000077500000000000000000000000001470147622500201045ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/helpers/__init__.py000066400000000000000000000000001470147622500222030ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/helpers/test_labels.py000066400000000000000000000124141470147622500227610ustar00rootroot00000000000000from __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 hcloud-python-2.3.0/tests/unit/images/000077500000000000000000000000001470147622500177075ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/images/__init__.py000066400000000000000000000000001470147622500220060ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/images/conftest.py000066400000000000000000000105571470147622500221160ustar00rootroot00000000000000from __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"}, } ] } hcloud-python-2.3.0/tests/unit/images/test_client.py000066400000000000000000000310421470147622500225760ustar00rootroot00000000000000from __future__ import annotations import datetime from datetime import timezone from unittest import mock import pytest from hcloud.actions import BoundAction from hcloud.images import BoundImage, Image, ImagesClient from hcloud.servers import BoundServer class TestBoundImage: @pytest.fixture() def bound_image(self, hetzner_client): return BoundImage(client=hetzner_client.images, data=dict(id=14)) def test_bound_image_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 @pytest.mark.parametrize( "params", [{}, {"sort": ["status"], "page": 1, "per_page": 2}] ) def test_get_actions_list( self, hetzner_client, bound_image, response_get_actions, params ): hetzner_client.request.return_value = response_get_actions result = bound_image.get_actions_list(**params) hetzner_client.request.assert_called_with( url="/images/14/actions", method="GET", params=params ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" @pytest.mark.parametrize("params", [{}, {"sort": ["status"]}]) def test_get_actions( self, hetzner_client, bound_image, response_get_actions, params ): hetzner_client.request.return_value = response_get_actions actions = bound_image.get_actions(**params) params.update({"page": 1, "per_page": 50}) hetzner_client.request.assert_called_with( url="/images/14/actions", method="GET", params=params ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" def test_update(self, hetzner_client, bound_image, response_update_image): hetzner_client.request.return_value = response_update_image image = bound_image.update( description="My new Image description", type="snapshot", labels={} ) hetzner_client.request.assert_called_with( url="/images/14", method="PUT", json={ "description": "My new Image description", "type": "snapshot", "labels": {}, }, ) assert image.id == 4711 assert image.description == "My new Image description" def test_delete(self, hetzner_client, bound_image, generic_action): hetzner_client.request.return_value = generic_action delete_success = bound_image.delete() hetzner_client.request.assert_called_with(url="/images/14", method="DELETE") assert delete_success is True def test_change_protection(self, hetzner_client, bound_image, generic_action): hetzner_client.request.return_value = generic_action action = bound_image.change_protection(True) hetzner_client.request.assert_called_with( url="/images/14/actions/change_protection", method="POST", json={"delete": True}, ) assert action.id == 1 assert action.progress == 0 class TestImagesClient: @pytest.fixture() def images_client(self): return ImagesClient(client=mock.MagicMock()) def test_get_by_id(self, images_client, image_response): images_client._client.request.return_value = image_response image = images_client.get_by_id(1) images_client._client.request.assert_called_with(url="/images/1", method="GET") 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, images_client, two_images_response, params): images_client._client.request.return_value = two_images_response result = images_client.get_list(**params) images_client._client.request.assert_called_with( url="/images", method="GET", params=params ) images = result.images assert result.meta is 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, images_client, two_images_response, params): images_client._client.request.return_value = two_images_response images = images_client.get_all(**params) params.update({"page": 1, "per_page": 50}) images_client._client.request.assert_called_with( url="/images", method="GET", 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, images_client, one_images_response): images_client._client.request.return_value = one_images_response image = images_client.get_by_name("ubuntu-20.04") params = {"name": "ubuntu-20.04"} images_client._client.request.assert_called_with( url="/images", method="GET", params=params ) assert image._client is images_client assert image.id == 4711 assert image.name == "ubuntu-20.04" def test_get_by_name_and_architecture(self, images_client, one_images_response): images_client._client.request.return_value = one_images_response image = images_client.get_by_name_and_architecture("ubuntu-20.04", "x86") params = {"name": "ubuntu-20.04", "architecture": ["x86"]} images_client._client.request.assert_called_with( url="/images", method="GET", params=params ) assert image._client is images_client assert image.id == 4711 assert image.name == "ubuntu-20.04" assert image.architecture == "x86" @pytest.mark.parametrize( "image", [Image(id=1), BoundImage(mock.MagicMock(), dict(id=1))] ) def test_get_actions_list(self, images_client, image, response_get_actions): images_client._client.request.return_value = response_get_actions result = images_client.get_actions_list(image) images_client._client.request.assert_called_with( url="/images/1/actions", method="GET", params={} ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == images_client._client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" @pytest.mark.parametrize( "image", [Image(id=1), BoundImage(mock.MagicMock(), dict(id=1))] ) def test_update(self, images_client, image, response_update_image): images_client._client.request.return_value = response_update_image image = images_client.update( image, description="My new Image description", type="snapshot", labels={} ) images_client._client.request.assert_called_with( url="/images/1", method="PUT", 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, images_client, image, generic_action): images_client._client.request.return_value = generic_action action = images_client.change_protection(image, True) images_client._client.request.assert_called_with( url="/images/1/actions/change_protection", method="POST", 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, images_client, image, generic_action): images_client._client.request.return_value = generic_action delete_success = images_client.delete(image) images_client._client.request.assert_called_with( url="/images/1", method="DELETE" ) assert delete_success is True def test_actions_get_by_id(self, images_client, response_get_actions): images_client._client.request.return_value = { "action": response_get_actions["actions"][0] } action = images_client.actions.get_by_id(13) images_client._client.request.assert_called_with( url="/images/actions/13", method="GET" ) assert isinstance(action, BoundAction) assert action._client == images_client._client.actions assert action.id == 13 assert action.command == "change_protection" def test_actions_get_list(self, images_client, response_get_actions): images_client._client.request.return_value = response_get_actions result = images_client.actions.get_list() images_client._client.request.assert_called_with( url="/images/actions", method="GET", params={}, ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == images_client._client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" def test_actions_get_all(self, images_client, response_get_actions): images_client._client.request.return_value = response_get_actions actions = images_client.actions.get_all() images_client._client.request.assert_called_with( url="/images/actions", method="GET", params={"page": 1, "per_page": 50}, ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == images_client._client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" hcloud-python-2.3.0/tests/unit/images/test_domain.py000066400000000000000000000005361470147622500225730ustar00rootroot00000000000000from __future__ import annotations import datetime from datetime import timezone from hcloud.images import Image 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 ) hcloud-python-2.3.0/tests/unit/isos/000077500000000000000000000000001470147622500174175ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/isos/__init__.py000066400000000000000000000000001470147622500215160ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/isos/conftest.py000066400000000000000000000036671470147622500216320ustar00rootroot00000000000000from __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", }, } ] } hcloud-python-2.3.0/tests/unit/isos/test_client.py000066400000000000000000000075151470147622500223160ustar00rootroot00000000000000from __future__ import annotations import datetime from datetime import timezone from unittest import mock import pytest from hcloud.isos import BoundIso, IsosClient class TestBoundIso: @pytest.fixture() def bound_iso(self, hetzner_client): return BoundIso(client=hetzner_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): return IsosClient(client=mock.MagicMock()) def test_get_by_id(self, isos_client, iso_response): isos_client._client.request.return_value = iso_response iso = isos_client.get_by_id(1) isos_client._client.request.assert_called_with(url="/isos/1", method="GET") 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, isos_client, two_isos_response, params): isos_client._client.request.return_value = two_isos_response result = isos_client.get_list(**params) isos_client._client.request.assert_called_with( url="/isos", method="GET", params=params ) isos = result.isos assert result.meta is 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, isos_client, two_isos_response, params): isos_client._client.request.return_value = two_isos_response isos = isos_client.get_all(**params) params.update({"page": 1, "per_page": 50}) isos_client._client.request.assert_called_with( url="/isos", method="GET", 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, isos_client, one_isos_response): isos_client._client.request.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"} isos_client._client.request.assert_called_with( url="/isos", method="GET", params=params ) assert iso._client is isos_client assert iso.id == 4711 assert iso.name == "FreeBSD-11.0-RELEASE-amd64-dvd1" hcloud-python-2.3.0/tests/unit/isos/test_domain.py000066400000000000000000000023331470147622500223000ustar00rootroot00000000000000from __future__ import annotations from datetime import datetime, timezone import pytest from hcloud.isos import Iso 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 ) hcloud-python-2.3.0/tests/unit/load_balancer_types/000077500000000000000000000000001470147622500224345ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/load_balancer_types/__init__.py000066400000000000000000000000001470147622500245330ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/load_balancer_types/conftest.py000066400000000000000000000067231470147622500246430ustar00rootroot00000000000000from __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", }, } ], } ] } hcloud-python-2.3.0/tests/unit/load_balancer_types/test_client.py000066400000000000000000000072571470147622500253360ustar00rootroot00000000000000from __future__ import annotations from unittest import mock import pytest from hcloud.load_balancer_types import LoadBalancerTypesClient class TestLoadBalancerTypesClient: @pytest.fixture() def load_balancer_types_client(self): return LoadBalancerTypesClient(client=mock.MagicMock()) def test_get_by_id(self, load_balancer_types_client, load_balancer_type_response): load_balancer_types_client._client.request.return_value = ( load_balancer_type_response ) load_balancer_type = load_balancer_types_client.get_by_id(1) load_balancer_types_client._client.request.assert_called_with( url="/load_balancer_types/1", method="GET" ) 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, load_balancer_types_client, two_load_balancer_types_response, params ): load_balancer_types_client._client.request.return_value = ( two_load_balancer_types_response ) result = load_balancer_types_client.get_list(**params) load_balancer_types_client._client.request.assert_called_with( url="/load_balancer_types", method="GET", params=params ) load_balancer_types = result.load_balancer_types assert result.meta is 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, load_balancer_types_client, two_load_balancer_types_response, params ): load_balancer_types_client._client.request.return_value = ( two_load_balancer_types_response ) load_balancer_types = load_balancer_types_client.get_all(**params) params.update({"page": 1, "per_page": 50}) load_balancer_types_client._client.request.assert_called_with( url="/load_balancer_types", method="GET", 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, load_balancer_types_client, one_load_balancer_types_response ): load_balancer_types_client._client.request.return_value = ( one_load_balancer_types_response ) load_balancer_type = load_balancer_types_client.get_by_name("LB21") params = {"name": "LB21"} load_balancer_types_client._client.request.assert_called_with( url="/load_balancer_types", method="GET", params=params ) assert load_balancer_type._client is load_balancer_types_client assert load_balancer_type.id == 2 assert load_balancer_type.name == "LB21" hcloud-python-2.3.0/tests/unit/load_balancers/000077500000000000000000000000001470147622500213735ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/load_balancers/__init__.py000066400000000000000000000000001470147622500234720ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/load_balancers/conftest.py000066400000000000000000000552011470147622500235750ustar00rootroot00000000000000from __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, } ], "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"}, } ] } hcloud-python-2.3.0/tests/unit/load_balancers/test_client.py000066400000000000000000000530271470147622500242710ustar00rootroot00000000000000from __future__ import annotations from unittest import mock import pytest from hcloud.actions import BoundAction from hcloud.load_balancer_types import LoadBalancerType from hcloud.load_balancers import ( BoundLoadBalancer, LoadBalancer, LoadBalancerAlgorithm, LoadBalancerHealthCheck, LoadBalancersClient, LoadBalancerService, LoadBalancerTarget, LoadBalancerTargetIP, LoadBalancerTargetLabelSelector, ) from hcloud.locations import Location from hcloud.networks import Network from hcloud.servers import Server class TestBoundLoadBalancer: @pytest.fixture() def bound_load_balancer(self, hetzner_client): return BoundLoadBalancer(client=hetzner_client.load_balancers, data=dict(id=14)) def test_bound_load_balancer_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" @pytest.mark.parametrize("params", [{"page": 1, "per_page": 10}, {}]) def test_get_actions_list( self, hetzner_client, bound_load_balancer, response_get_actions, params ): hetzner_client.request.return_value = response_get_actions result = bound_load_balancer.get_actions_list(**params) hetzner_client.request.assert_called_with( url="/load_balancers/14/actions", method="GET", params=params ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" @pytest.mark.parametrize("params", [{}]) def test_get_actions( self, hetzner_client, bound_load_balancer, response_get_actions, params ): hetzner_client.request.return_value = response_get_actions actions = bound_load_balancer.get_actions(**params) params.update({"page": 1, "per_page": 50}) hetzner_client.request.assert_called_with( url="/load_balancers/14/actions", method="GET", params=params ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" def test_update( self, hetzner_client, bound_load_balancer, response_update_load_balancer ): hetzner_client.request.return_value = response_update_load_balancer load_balancer = bound_load_balancer.update(name="new-name", labels={}) hetzner_client.request.assert_called_with( url="/load_balancers/14", method="PUT", json={"name": "new-name", "labels": {}}, ) assert load_balancer.id == 4711 assert load_balancer.name == "new-name" def test_delete(self, hetzner_client, generic_action, bound_load_balancer): hetzner_client.request.return_value = generic_action delete_success = bound_load_balancer.delete() hetzner_client.request.assert_called_with( url="/load_balancers/14", method="DELETE" ) assert delete_success is True def test_get_metrics( self, hetzner_client, response_get_metrics, bound_load_balancer: BoundLoadBalancer, ): hetzner_client.request.return_value = response_get_metrics response = bound_load_balancer.get_metrics( type=["requests_per_second"], start="2023-12-14T16:55:32+01:00", end="2023-12-14T16:55:32+01:00", ) hetzner_client.request.assert_called_with( url="/load_balancers/14/metrics", method="GET", params={ "type": "requests_per_second", "start": "2023-12-14T16:55:32+01:00", "end": "2023-12-14T16:55:32+01:00", }, ) assert "requests_per_second" in response.metrics.time_series assert len(response.metrics.time_series["requests_per_second"]["values"]) == 3 def test_add_service( self, hetzner_client, response_add_service, bound_load_balancer ): hetzner_client.request.return_value = response_add_service service = LoadBalancerService(listen_port=80, protocol="http") action = bound_load_balancer.add_service(service) hetzner_client.request.assert_called_with( json={"protocol": "http", "listen_port": 80}, url="/load_balancers/14/actions/add_service", method="POST", ) assert action.id == 13 assert action.progress == 100 assert action.command == "add_service" def test_delete_service( self, hetzner_client, response_delete_service, bound_load_balancer ): hetzner_client.request.return_value = response_delete_service service = LoadBalancerService(listen_port=12) action = bound_load_balancer.delete_service(service) hetzner_client.request.assert_called_with( json={"listen_port": 12}, url="/load_balancers/14/actions/delete_service", method="POST", ) assert action.id == 13 assert action.progress == 100 assert action.command == "delete_service" @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, hetzner_client, response_add_target, bound_load_balancer, target, params ): hetzner_client.request.return_value = response_add_target action = bound_load_balancer.add_target(target) params.update({"type": target.type}) hetzner_client.request.assert_called_with( url="/load_balancers/14/actions/add_target", method="POST", json=params ) assert action.id == 13 assert action.progress == 100 assert action.command == "add_target" @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, hetzner_client, response_remove_target, bound_load_balancer, target, params, ): hetzner_client.request.return_value = response_remove_target action = bound_load_balancer.remove_target(target) params.update({"type": target.type}) hetzner_client.request.assert_called_with( url="/load_balancers/14/actions/remove_target", method="POST", json=params ) assert action.id == 13 assert action.progress == 100 assert action.command == "remove_target" def test_update_service( self, hetzner_client, response_update_service, bound_load_balancer ): hetzner_client.request.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 = bound_load_balancer.update_service(service) hetzner_client.request.assert_called_with( json={ "listen_port": 12, "health_check": { "protocol": "http", "port": 13, "interval": 1, "timeout": 1, "retries": 1, }, }, url="/load_balancers/14/actions/update_service", method="POST", ) assert action.id == 13 assert action.progress == 100 assert action.command == "update_service" def test_change_algorithm( self, hetzner_client, response_change_algorithm, bound_load_balancer ): hetzner_client.request.return_value = response_change_algorithm algorithm = LoadBalancerAlgorithm(type="round_robin") action = bound_load_balancer.change_algorithm(algorithm) hetzner_client.request.assert_called_with( json={"type": "round_robin"}, url="/load_balancers/14/actions/change_algorithm", method="POST", ) assert action.id == 13 assert action.progress == 100 assert action.command == "change_algorithm" def test_change_dns_ptr( self, hetzner_client, response_change_reverse_dns_entry, bound_load_balancer ): hetzner_client.request.return_value = response_change_reverse_dns_entry action = bound_load_balancer.change_dns_ptr( ip="1.2.3.4", dns_ptr="lb1.example.com" ) hetzner_client.request.assert_called_with( json={"dns_ptr": "lb1.example.com", "ip": "1.2.3.4"}, url="/load_balancers/14/actions/change_dns_ptr", method="POST", ) assert action.id == 13 assert action.progress == 100 assert action.command == "change_dns_ptr" def test_change_protection( self, hetzner_client, response_change_protection, bound_load_balancer ): hetzner_client.request.return_value = response_change_protection action = bound_load_balancer.change_protection(delete=True) hetzner_client.request.assert_called_with( json={"delete": True}, url="/load_balancers/14/actions/change_protection", method="POST", ) assert action.id == 13 assert action.progress == 100 assert action.command == "change_protection" def test_enable_public_interface( self, response_enable_public_interface, hetzner_client, bound_load_balancer ): hetzner_client.request.return_value = response_enable_public_interface action = bound_load_balancer.enable_public_interface() hetzner_client.request.assert_called_with( url="/load_balancers/14/actions/enable_public_interface", method="POST" ) assert action.id == 13 assert action.progress == 100 assert action.command == "enable_public_interface" def test_disable_public_interface( self, response_disable_public_interface, hetzner_client, bound_load_balancer ): hetzner_client.request.return_value = response_disable_public_interface action = bound_load_balancer.disable_public_interface() hetzner_client.request.assert_called_with( url="/load_balancers/14/actions/disable_public_interface", method="POST" ) assert action.id == 13 assert action.progress == 100 assert action.command == "disable_public_interface" def test_attach_to_network( self, response_attach_load_balancer_to_network, hetzner_client, bound_load_balancer, ): hetzner_client.request.return_value = response_attach_load_balancer_to_network action = bound_load_balancer.attach_to_network(Network(id=1)) hetzner_client.request.assert_called_with( json={"network": 1}, url="/load_balancers/14/actions/attach_to_network", method="POST", ) assert action.id == 13 assert action.progress == 100 assert action.command == "attach_to_network" def test_detach_from_network( self, response_detach_from_network, hetzner_client, bound_load_balancer ): hetzner_client.request.return_value = response_detach_from_network action = bound_load_balancer.detach_from_network(Network(id=1)) hetzner_client.request.assert_called_with( json={"network": 1}, url="/load_balancers/14/actions/detach_from_network", method="POST", ) assert action.id == 13 assert action.progress == 100 assert action.command == "detach_from_network" def test_change_type(self, hetzner_client, bound_load_balancer, generic_action): hetzner_client.request.return_value = generic_action action = bound_load_balancer.change_type(LoadBalancerType(name="lb21")) hetzner_client.request.assert_called_with( url="/load_balancers/14/actions/change_type", method="POST", json={"load_balancer_type": "lb21"}, ) assert action.id == 1 assert action.progress == 0 class TestLoadBalancerslient: @pytest.fixture() def load_balancers_client(self): return LoadBalancersClient(client=mock.MagicMock()) def test_get_by_id(self, load_balancers_client, response_load_balancer): load_balancers_client._client.request.return_value = response_load_balancer bound_load_balancer = load_balancers_client.get_by_id(1) load_balancers_client._client.request.assert_called_with( url="/load_balancers/1", method="GET" ) assert bound_load_balancer._client is load_balancers_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, load_balancers_client, response_simple_load_balancers, params ): load_balancers_client._client.request.return_value = ( response_simple_load_balancers ) result = load_balancers_client.get_list(**params) load_balancers_client._client.request.assert_called_with( url="/load_balancers", method="GET", params=params ) bound_load_balancers = result.load_balancers assert result.meta is 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 load_balancers_client assert bound_load_balancer1.id == 4711 assert bound_load_balancer1.name == "Web Frontend" assert bound_load_balancer2._client is load_balancers_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, load_balancers_client, response_simple_load_balancers, params ): load_balancers_client._client.request.return_value = ( response_simple_load_balancers ) bound_load_balancers = load_balancers_client.get_all(**params) params.update({"page": 1, "per_page": 50}) load_balancers_client._client.request.assert_called_with( url="/load_balancers", method="GET", 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 load_balancers_client assert bound_load_balancer1.id == 4711 assert bound_load_balancer1.name == "Web Frontend" assert bound_load_balancer2._client is load_balancers_client assert bound_load_balancer2.id == 4712 assert bound_load_balancer2.name == "Web Frontend2" def test_get_by_name(self, load_balancers_client, response_simple_load_balancers): load_balancers_client._client.request.return_value = ( response_simple_load_balancers ) bound_load_balancer = load_balancers_client.get_by_name("Web Frontend") params = {"name": "Web Frontend"} load_balancers_client._client.request.assert_called_with( url="/load_balancers", method="GET", params=params ) assert bound_load_balancer._client is load_balancers_client assert bound_load_balancer.id == 4711 assert bound_load_balancer.name == "Web Frontend" def test_create(self, load_balancers_client, response_create_load_balancer): load_balancers_client._client.request.return_value = ( response_create_load_balancer ) response = load_balancers_client.create( "my-balancer", load_balancer_type=LoadBalancerType(name="lb11"), location=Location(id=1), ) load_balancers_client._client.request.assert_called_with( url="/load_balancers", method="POST", json={"name": "my-balancer", "load_balancer_type": "lb11", "location": 1}, ) bound_load_balancer = response.load_balancer assert bound_load_balancer._client is load_balancers_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, load_balancers_client, load_balancer, generic_action ): load_balancers_client._client.request.return_value = generic_action action = load_balancers_client.change_type( load_balancer, LoadBalancerType(name="lb11") ) load_balancers_client._client.request.assert_called_with( url="/load_balancers/1/actions/change_type", method="POST", 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, load_balancers_client, load_balancer, generic_action ): load_balancers_client._client.request.return_value = generic_action action = load_balancers_client.change_type( load_balancer, LoadBalancerType(id=1) ) load_balancers_client._client.request.assert_called_with( url="/load_balancers/1/actions/change_type", method="POST", json={"load_balancer_type": 1}, ) assert action.id == 1 assert action.progress == 0 def test_actions_get_by_id(self, load_balancers_client, response_get_actions): load_balancers_client._client.request.return_value = { "action": response_get_actions["actions"][0] } action = load_balancers_client.actions.get_by_id(13) load_balancers_client._client.request.assert_called_with( url="/load_balancers/actions/13", method="GET" ) assert isinstance(action, BoundAction) assert action._client == load_balancers_client._client.actions assert action.id == 13 assert action.command == "change_protection" def test_actions_get_list(self, load_balancers_client, response_get_actions): load_balancers_client._client.request.return_value = response_get_actions result = load_balancers_client.actions.get_list() load_balancers_client._client.request.assert_called_with( url="/load_balancers/actions", method="GET", params={}, ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == load_balancers_client._client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" def test_actions_get_all(self, load_balancers_client, response_get_actions): load_balancers_client._client.request.return_value = response_get_actions actions = load_balancers_client.actions.get_all() load_balancers_client._client.request.assert_called_with( url="/load_balancers/actions", method="GET", params={"page": 1, "per_page": 50}, ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == load_balancers_client._client.actions assert actions[0].id == 13 assert actions[0].command == "change_protection" hcloud-python-2.3.0/tests/unit/load_balancers/test_domain.py000066400000000000000000000005401470147622500242520ustar00rootroot00000000000000from __future__ import annotations import datetime from datetime import timezone from hcloud.load_balancers import LoadBalancer 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) hcloud-python-2.3.0/tests/unit/locations/000077500000000000000000000000001470147622500204355ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/locations/__init__.py000066400000000000000000000000001470147622500225340ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/locations/conftest.py000066400000000000000000000031451470147622500226370ustar00rootroot00000000000000from __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", } ] } hcloud-python-2.3.0/tests/unit/locations/test_client.py000066400000000000000000000062551470147622500233340ustar00rootroot00000000000000from __future__ import annotations from unittest import mock # noqa: F401 import pytest # noqa: F401 from hcloud.locations import LocationsClient class TestLocationsClient: @pytest.fixture() def locations_client(self): return LocationsClient(client=mock.MagicMock()) def test_get_by_id(self, locations_client, location_response): locations_client._client.request.return_value = location_response location = locations_client.get_by_id(1) locations_client._client.request.assert_called_with( url="/locations/1", method="GET" ) 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, locations_client, two_locations_response, params): locations_client._client.request.return_value = two_locations_response result = locations_client.get_list(**params) locations_client._client.request.assert_called_with( url="/locations", method="GET", params=params ) locations = result.locations assert result.meta is 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, locations_client, two_locations_response, params): locations_client._client.request.return_value = two_locations_response locations = locations_client.get_all(**params) params.update({"page": 1, "per_page": 50}) locations_client._client.request.assert_called_with( url="/locations", method="GET", 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, locations_client, one_locations_response): locations_client._client.request.return_value = one_locations_response location = locations_client.get_by_name("fsn1") params = {"name": "fsn1"} locations_client._client.request.assert_called_with( url="/locations", method="GET", params=params ) assert location._client is locations_client assert location.id == 1 assert location.name == "fsn1" assert location.network_zone == "eu-central" hcloud-python-2.3.0/tests/unit/networks/000077500000000000000000000000001470147622500203165ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/networks/__init__.py000066400000000000000000000000001470147622500224150ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/networks/conftest.py000066400000000000000000000146331470147622500225240ustar00rootroot00000000000000from __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"}, } ] } hcloud-python-2.3.0/tests/unit/networks/test_client.py000066400000000000000000000561751470147622500232230ustar00rootroot00000000000000from __future__ import annotations from unittest import mock import pytest from dateutil.parser import isoparse from hcloud.actions import BoundAction from hcloud.networks import ( BoundNetwork, Network, NetworkRoute, NetworksClient, NetworkSubnet, ) from hcloud.servers import BoundServer class TestBoundNetwork: @pytest.fixture() def bound_network(self, hetzner_client): return BoundNetwork(client=hetzner_client.networks, data=dict(id=14)) def test_bound_network_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" def test_get_actions(self, hetzner_client, bound_network, response_get_actions): hetzner_client.request.return_value = response_get_actions actions = bound_network.get_actions(sort="id") hetzner_client.request.assert_called_with( url="/networks/14/actions", method="GET", params={"page": 1, "per_page": 50, "sort": "id"}, ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "add_subnet" def test_update(self, hetzner_client, bound_network, response_update_network): hetzner_client.request.return_value = response_update_network network = bound_network.update(name="new-name") hetzner_client.request.assert_called_with( url="/networks/14", method="PUT", json={"name": "new-name"} ) assert network.id == 4711 assert network.name == "new-name" def test_delete(self, hetzner_client, bound_network, generic_action): hetzner_client.request.return_value = generic_action delete_success = bound_network.delete() hetzner_client.request.assert_called_with(url="/networks/14", method="DELETE") assert delete_success is True def test_change_protection(self, hetzner_client, bound_network, generic_action): hetzner_client.request.return_value = generic_action action = bound_network.change_protection(True) hetzner_client.request.assert_called_with( url="/networks/14/actions/change_protection", method="POST", json={"delete": True}, ) assert action.id == 1 assert action.progress == 0 def test_add_subnet(self, hetzner_client, bound_network, generic_action): hetzner_client.request.return_value = generic_action subnet = NetworkSubnet( type=NetworkSubnet.TYPE_CLOUD, ip_range="10.0.1.0/24", network_zone="eu-central", ) action = bound_network.add_subnet(subnet) hetzner_client.request.assert_called_with( url="/networks/14/actions/add_subnet", method="POST", json={ "type": NetworkSubnet.TYPE_CLOUD, "ip_range": "10.0.1.0/24", "network_zone": "eu-central", }, ) assert action.id == 1 assert action.progress == 0 def test_delete_subnet(self, hetzner_client, bound_network, generic_action): hetzner_client.request.return_value = generic_action subnet = NetworkSubnet(ip_range="10.0.1.0/24") action = bound_network.delete_subnet(subnet) hetzner_client.request.assert_called_with( url="/networks/14/actions/delete_subnet", method="POST", json={"ip_range": "10.0.1.0/24"}, ) assert action.id == 1 assert action.progress == 0 def test_add_route(self, hetzner_client, bound_network, generic_action): hetzner_client.request.return_value = generic_action route = NetworkRoute(destination="10.100.1.0/24", gateway="10.0.1.1") action = bound_network.add_route(route) hetzner_client.request.assert_called_with( url="/networks/14/actions/add_route", method="POST", json={"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}, ) assert action.id == 1 assert action.progress == 0 def test_delete_route(self, hetzner_client, bound_network, generic_action): hetzner_client.request.return_value = generic_action route = NetworkRoute(destination="10.100.1.0/24", gateway="10.0.1.1") action = bound_network.delete_route(route) hetzner_client.request.assert_called_with( url="/networks/14/actions/delete_route", method="POST", json={"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}, ) assert action.id == 1 assert action.progress == 0 def test_change_ip(self, hetzner_client, bound_network, generic_action): hetzner_client.request.return_value = generic_action action = bound_network.change_ip_range("10.0.0.0/12") hetzner_client.request.assert_called_with( url="/networks/14/actions/change_ip_range", method="POST", json={"ip_range": "10.0.0.0/12"}, ) assert action.id == 1 assert action.progress == 0 class TestNetworksClient: @pytest.fixture() def networks_client(self): return NetworksClient(client=mock.MagicMock()) @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, networks_client, network_response): networks_client._client.request.return_value = network_response bound_network = networks_client.get_by_id(1) networks_client._client.request.assert_called_with( url="/networks/1", method="GET" ) 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, networks_client, two_networks_response, params): networks_client._client.request.return_value = two_networks_response result = networks_client.get_list(**params) networks_client._client.request.assert_called_with( url="/networks", method="GET", params=params ) bound_networks = result.networks assert result.meta is 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, networks_client, two_networks_response, params): networks_client._client.request.return_value = two_networks_response bound_networks = networks_client.get_all(**params) params.update({"page": 1, "per_page": 50}) networks_client._client.request.assert_called_with( url="/networks", method="GET", 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, networks_client, one_network_response): networks_client._client.request.return_value = one_network_response bound_network = networks_client.get_by_name("mynet") params = {"name": "mynet"} networks_client._client.request.assert_called_with( url="/networks", method="GET", params=params ) assert bound_network._client is networks_client assert bound_network.id == 1 assert bound_network.name == "mynet" def test_create(self, networks_client, network_create_response): networks_client._client.request.return_value = network_create_response networks_client.create(name="mynet", ip_range="10.0.0.0/8") networks_client._client.request.assert_called_with( url="/networks", method="POST", json={"name": "mynet", "ip_range": "10.0.0.0/8"}, ) def test_create_with_expose_routes_to_vswitch( self, networks_client, network_create_response_with_expose_routes_to_vswitch ): networks_client._client.request.return_value = ( network_create_response_with_expose_routes_to_vswitch ) networks_client.create( name="mynet", ip_range="10.0.0.0/8", expose_routes_to_vswitch=True ) networks_client._client.request.assert_called_with( url="/networks", method="POST", json={ "name": "mynet", "ip_range": "10.0.0.0/8", "expose_routes_to_vswitch": True, }, ) def test_create_with_subnet( self, networks_client, network_subnet, network_create_response ): networks_client._client.request.return_value = network_create_response networks_client.create( name="mynet", ip_range="10.0.0.0/8", subnets=[network_subnet] ) networks_client._client.request.assert_called_with( url="/networks", method="POST", json={ "name": "mynet", "ip_range": "10.0.0.0/8", "subnets": [ { "type": NetworkSubnet.TYPE_CLOUD, "ip_range": "10.0.1.0/24", "network_zone": "eu-central", } ], }, ) def test_create_with_subnet_vswitch( self, networks_client, network_subnet, network_create_response ): networks_client._client.request.return_value = network_create_response network_subnet.type = NetworkSubnet.TYPE_VSWITCH network_subnet.vswitch_id = 1000 networks_client.create( name="mynet", ip_range="10.0.0.0/8", subnets=[network_subnet] ) networks_client._client.request.assert_called_with( url="/networks", method="POST", json={ "name": "mynet", "ip_range": "10.0.0.0/8", "subnets": [ { "type": NetworkSubnet.TYPE_VSWITCH, "ip_range": "10.0.1.0/24", "network_zone": "eu-central", "vswitch_id": 1000, } ], }, ) def test_create_with_route( self, networks_client, network_route, network_create_response ): networks_client._client.request.return_value = network_create_response networks_client.create( name="mynet", ip_range="10.0.0.0/8", routes=[network_route] ) networks_client._client.request.assert_called_with( url="/networks", method="POST", json={ "name": "mynet", "ip_range": "10.0.0.0/8", "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], }, ) def test_create_with_route_and_expose_routes_to_vswitch( self, networks_client, network_route, network_create_response_with_expose_routes_to_vswitch, ): networks_client._client.request.return_value = ( network_create_response_with_expose_routes_to_vswitch ) networks_client.create( name="mynet", ip_range="10.0.0.0/8", routes=[network_route], expose_routes_to_vswitch=True, ) networks_client._client.request.assert_called_with( url="/networks", method="POST", json={ "name": "mynet", "ip_range": "10.0.0.0/8", "routes": [{"destination": "10.100.1.0/24", "gateway": "10.0.1.1"}], "expose_routes_to_vswitch": True, }, ) def test_create_with_route_and_subnet( self, networks_client, network_subnet, network_route, network_create_response ): networks_client._client.request.return_value = network_create_response networks_client.create( name="mynet", ip_range="10.0.0.0/8", subnets=[network_subnet], routes=[network_route], ) networks_client._client.request.assert_called_with( url="/networks", method="POST", json={ "name": "mynet", "ip_range": "10.0.0.0/8", "subnets": [ { "type": NetworkSubnet.TYPE_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_get_actions_list(self, networks_client, network, response_get_actions): networks_client._client.request.return_value = response_get_actions result = networks_client.get_actions_list(network, sort="id") networks_client._client.request.assert_called_with( url="/networks/1/actions", method="GET", params={"sort": "id"} ) actions = result.actions assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == networks_client._client.actions assert actions[0].id == 13 assert actions[0].command == "add_subnet" @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_update(self, networks_client, network, response_update_network): networks_client._client.request.return_value = response_update_network network = networks_client.update( network, name="new-name", expose_routes_to_vswitch=True ) networks_client._client.request.assert_called_with( url="/networks/1", method="PUT", json={"name": "new-name", "expose_routes_to_vswitch": True}, ) assert network.id == 4711 assert network.name == "new-name" assert network.expose_routes_to_vswitch is True @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_change_protection(self, networks_client, network, generic_action): networks_client._client.request.return_value = generic_action action = networks_client.change_protection(network, True) networks_client._client.request.assert_called_with( url="/networks/1/actions/change_protection", method="POST", 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, networks_client, network, generic_action): networks_client._client.request.return_value = generic_action delete_success = networks_client.delete(network) networks_client._client.request.assert_called_with( url="/networks/1", method="DELETE" ) assert delete_success is True @pytest.mark.parametrize( "network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))] ) def test_add_subnet(self, networks_client, network, generic_action, network_subnet): networks_client._client.request.return_value = generic_action action = networks_client.add_subnet(network, network_subnet) networks_client._client.request.assert_called_with( url="/networks/1/actions/add_subnet", method="POST", 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, networks_client, network, generic_action, network_vswitch_subnet ): networks_client._client.request.return_value = generic_action action = networks_client.add_subnet(network, network_vswitch_subnet) networks_client._client.request.assert_called_with( url="/networks/1/actions/add_subnet", method="POST", 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, networks_client, network, generic_action, network_subnet ): networks_client._client.request.return_value = generic_action action = networks_client.delete_subnet(network, network_subnet) networks_client._client.request.assert_called_with( url="/networks/1/actions/delete_subnet", method="POST", 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, networks_client, network, generic_action, network_route): networks_client._client.request.return_value = generic_action action = networks_client.add_route(network, network_route) networks_client._client.request.assert_called_with( url="/networks/1/actions/add_route", method="POST", 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, networks_client, network, generic_action, network_route ): networks_client._client.request.return_value = generic_action action = networks_client.delete_route(network, network_route) networks_client._client.request.assert_called_with( url="/networks/1/actions/delete_route", method="POST", 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, networks_client, network, generic_action): networks_client._client.request.return_value = generic_action action = networks_client.change_ip_range(network, "10.0.0.0/12") networks_client._client.request.assert_called_with( url="/networks/1/actions/change_ip_range", method="POST", json={"ip_range": "10.0.0.0/12"}, ) assert action.id == 1 assert action.progress == 0 def test_actions_get_by_id(self, networks_client, response_get_actions): networks_client._client.request.return_value = { "action": response_get_actions["actions"][0] } action = networks_client.actions.get_by_id(13) networks_client._client.request.assert_called_with( url="/networks/actions/13", method="GET" ) assert isinstance(action, BoundAction) assert action._client == networks_client._client.actions assert action.id == 13 assert action.command == "add_subnet" def test_actions_get_list(self, networks_client, response_get_actions): networks_client._client.request.return_value = response_get_actions result = networks_client.actions.get_list() networks_client._client.request.assert_called_with( url="/networks/actions", method="GET", params={}, ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == networks_client._client.actions assert actions[0].id == 13 assert actions[0].command == "add_subnet" def test_actions_get_all(self, networks_client, response_get_actions): networks_client._client.request.return_value = response_get_actions actions = networks_client.actions.get_all() networks_client._client.request.assert_called_with( url="/networks/actions", method="GET", params={"page": 1, "per_page": 50}, ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == networks_client._client.actions assert actions[0].id == 13 assert actions[0].command == "add_subnet" hcloud-python-2.3.0/tests/unit/networks/test_domain.py000066400000000000000000000005521470147622500232000ustar00rootroot00000000000000from __future__ import annotations import datetime from datetime import timezone from hcloud.networks import Network 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 ) hcloud-python-2.3.0/tests/unit/placement_groups/000077500000000000000000000000001470147622500220115ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/placement_groups/__init__.py000066400000000000000000000000001470147622500241100ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/placement_groups/conftest.py000066400000000000000000000033661470147622500242200ustar00rootroot00000000000000from __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", } } hcloud-python-2.3.0/tests/unit/placement_groups/test_client.py000066400000000000000000000162431470147622500247060ustar00rootroot00000000000000from __future__ import annotations from unittest import mock import pytest from hcloud.placement_groups import BoundPlacementGroup, PlacementGroupsClient def check_variables(placement_group, 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: @pytest.fixture() def bound_placement_group(self, hetzner_client): return BoundPlacementGroup( client=hetzner_client.placement_groups, data=dict(id=897) ) def test_bound_placement_group_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"] ) def test_update( self, hetzner_client, bound_placement_group, placement_group_response ): hetzner_client.request.return_value = placement_group_response placement_group = bound_placement_group.update( name=placement_group_response["placement_group"]["name"], labels=placement_group_response["placement_group"]["labels"], ) hetzner_client.request.assert_called_with( url="/placement_groups/{placement_group_id}".format( placement_group_id=placement_group_response["placement_group"]["id"] ), method="PUT", 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, hetzner_client, bound_placement_group): delete_success = bound_placement_group.delete() hetzner_client.request.assert_called_with( url="/placement_groups/897", method="DELETE" ) assert delete_success is True class TestPlacementGroupsClient: @pytest.fixture() def placement_groups_client(self): return PlacementGroupsClient(client=mock.MagicMock()) def test_get_by_id(self, placement_groups_client, placement_group_response): placement_groups_client._client.request.return_value = placement_group_response placement_group = placement_groups_client.get_by_id( placement_group_response["placement_group"]["id"] ) placement_groups_client._client.request.assert_called_with( url="/placement_groups/{placement_group_id}".format( placement_group_id=placement_group_response["placement_group"]["id"] ), method="GET", ) assert placement_group._client is placement_groups_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, placement_groups_client, two_placement_groups_response, params ): placement_groups_client._client.request.return_value = ( two_placement_groups_response ) result = placement_groups_client.get_list(**params) placement_groups_client._client.request.assert_called_with( url="/placement_groups", method="GET", params=params ) placement_groups = result.placement_groups assert result.meta is 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 placement_groups_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, placement_groups_client, two_placement_groups_response, params ): placement_groups_client._client.request.return_value = ( two_placement_groups_response ) placement_groups = placement_groups_client.get_all(**params) params.update({"page": 1, "per_page": 50}) placement_groups_client._client.request.assert_called_with( url="/placement_groups", method="GET", 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 placement_groups_client check_variables(placement_group, expected) def test_get_by_name(self, placement_groups_client, one_placement_group_response): placement_groups_client._client.request.return_value = ( one_placement_group_response ) placement_group = placement_groups_client.get_by_name( one_placement_group_response["placement_groups"][0]["name"] ) params = {"name": one_placement_group_response["placement_groups"][0]["name"]} placement_groups_client._client.request.assert_called_with( url="/placement_groups", method="GET", params=params ) check_variables( placement_group, one_placement_group_response["placement_groups"][0] ) def test_create(self, placement_groups_client, response_create_placement_group): placement_groups_client._client.request.return_value = ( response_create_placement_group ) response = placement_groups_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"], } placement_groups_client._client.request.assert_called_with( url="/placement_groups", method="POST", json=json ) bound_placement_group = response.placement_group assert bound_placement_group._client is placement_groups_client check_variables( bound_placement_group, response_create_placement_group["placement_group"] ) hcloud-python-2.3.0/tests/unit/placement_groups/test_domain.py000066400000000000000000000006271470147622500246760ustar00rootroot00000000000000from __future__ import annotations import datetime from datetime import timezone from hcloud.placement_groups import PlacementGroup 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 ) hcloud-python-2.3.0/tests/unit/primary_ips/000077500000000000000000000000001470147622500210005ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/primary_ips/__init__.py000066400000000000000000000000001470147622500230770ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/primary_ips/conftest.py000066400000000000000000000203121470147622500231750ustar00rootroot00000000000000from __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"}, } ] } hcloud-python-2.3.0/tests/unit/primary_ips/test_client.py000066400000000000000000000332151470147622500236730ustar00rootroot00000000000000from __future__ import annotations from unittest import mock import pytest from hcloud.actions import BoundAction from hcloud.datacenters import BoundDatacenter, Datacenter from hcloud.primary_ips import BoundPrimaryIP, PrimaryIP, PrimaryIPsClient class TestBoundPrimaryIP: @pytest.fixture() def bound_primary_ip(self, hetzner_client): return BoundPrimaryIP(client=hetzner_client.primary_ips, data=dict(id=14)) def test_bound_primary_ip_init(self, primary_ip_response): bound_primary_ip = BoundPrimaryIP( client=mock.MagicMock(), data=primary_ip_response["primary_ip"] ) assert bound_primary_ip.id == 42 assert bound_primary_ip.name == "my-resource" assert bound_primary_ip.ip == "131.232.99.1" assert bound_primary_ip.type == "ipv4" assert bound_primary_ip.protection == {"delete": False} assert bound_primary_ip.labels == {} assert bound_primary_ip.blocked is False assert bound_primary_ip.assignee_id == 17 assert bound_primary_ip.assignee_type == "server" assert isinstance(bound_primary_ip.datacenter, BoundDatacenter) assert bound_primary_ip.datacenter.id == 42 assert bound_primary_ip.datacenter.name == "fsn1-dc8" assert bound_primary_ip.datacenter.description == "Falkenstein DC Park 8" assert bound_primary_ip.datacenter.location.country == "DE" assert bound_primary_ip.datacenter.location.city == "Falkenstein" assert bound_primary_ip.datacenter.location.latitude == 50.47612 assert bound_primary_ip.datacenter.location.longitude == 12.370071 def test_update(self, hetzner_client, bound_primary_ip, response_update_primary_ip): hetzner_client.request.return_value = response_update_primary_ip primary_ip = bound_primary_ip.update(auto_delete=True, name="my-resource") hetzner_client.request.assert_called_with( url="/primary_ips/14", method="PUT", json={"auto_delete": True, "name": "my-resource"}, ) assert primary_ip.id == 42 assert primary_ip.auto_delete is True def test_delete(self, hetzner_client, bound_primary_ip, generic_action): hetzner_client.request.return_value = generic_action delete_success = bound_primary_ip.delete() hetzner_client.request.assert_called_with( url="/primary_ips/14", method="DELETE" ) assert delete_success is True def test_change_protection(self, hetzner_client, bound_primary_ip, generic_action): hetzner_client.request.return_value = generic_action action = bound_primary_ip.change_protection(True) hetzner_client.request.assert_called_with( url="/primary_ips/14/actions/change_protection", method="POST", json={"delete": True}, ) assert action.id == 1 assert action.progress == 0 def test_assign(self, hetzner_client, bound_primary_ip, generic_action): hetzner_client.request.return_value = generic_action action = bound_primary_ip.assign(assignee_id=12, assignee_type="server") hetzner_client.request.assert_called_with( url="/primary_ips/14/actions/assign", method="POST", json={"assignee_id": 12, "assignee_type": "server"}, ) assert action.id == 1 assert action.progress == 0 def test_unassign(self, hetzner_client, bound_primary_ip, generic_action): hetzner_client.request.return_value = generic_action action = bound_primary_ip.unassign() hetzner_client.request.assert_called_with( url="/primary_ips/14/actions/unassign", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_change_dns_ptr(self, hetzner_client, bound_primary_ip, generic_action): hetzner_client.request.return_value = generic_action action = bound_primary_ip.change_dns_ptr("1.2.3.4", "server02.example.com") hetzner_client.request.assert_called_with( url="/primary_ips/14/actions/change_dns_ptr", method="POST", json={"ip": "1.2.3.4", "dns_ptr": "server02.example.com"}, ) assert action.id == 1 assert action.progress == 0 class TestPrimaryIPsClient: @pytest.fixture() def primary_ips_client(self): return PrimaryIPsClient(client=mock.MagicMock()) def test_get_by_id(self, primary_ips_client, primary_ip_response): primary_ips_client._client.request.return_value = primary_ip_response bound_primary_ip = primary_ips_client.get_by_id(1) primary_ips_client._client.request.assert_called_with( url="/primary_ips/1", method="GET" ) assert bound_primary_ip._client is primary_ips_client assert bound_primary_ip.id == 42 def test_get_by_name(self, primary_ips_client, one_primary_ips_response): primary_ips_client._client.request.return_value = one_primary_ips_response bound_primary_ip = primary_ips_client.get_by_name("my-resource") primary_ips_client._client.request.assert_called_with( url="/primary_ips", method="GET", params={"name": "my-resource"} ) assert bound_primary_ip._client is primary_ips_client assert bound_primary_ip.id == 42 assert bound_primary_ip.name == "my-resource" @pytest.mark.parametrize("params", [{"label_selector": "label1"}]) def test_get_all(self, primary_ips_client, all_primary_ips_response, params): primary_ips_client._client.request.return_value = all_primary_ips_response bound_primary_ips = primary_ips_client.get_all(**params) params.update({"page": 1, "per_page": 50}) primary_ips_client._client.request.assert_called_with( url="/primary_ips", method="GET", params=params ) assert len(bound_primary_ips) == 1 bound_primary_ip1 = bound_primary_ips[0] assert bound_primary_ip1._client is primary_ips_client assert bound_primary_ip1.id == 42 assert bound_primary_ip1.name == "my-resource" def test_create_with_datacenter(self, primary_ips_client, primary_ip_response): primary_ips_client._client.request.return_value = primary_ip_response response = primary_ips_client.create( type="ipv6", name="my-resource", datacenter=Datacenter(name="datacenter") ) primary_ips_client._client.request.assert_called_with( url="/primary_ips", method="POST", json={ "name": "my-resource", "type": "ipv6", "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, primary_ips_client, primary_ip_create_response ): primary_ips_client._client.request.return_value = primary_ip_create_response response = primary_ips_client.create( type="ipv6", name="my-ip", assignee_id=17, assignee_type="server", ) primary_ips_client._client.request.assert_called_with( url="/primary_ips", method="POST", 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, primary_ips_client, primary_ip, response_update_primary_ip): primary_ips_client._client.request.return_value = response_update_primary_ip primary_ip = primary_ips_client.update( primary_ip, auto_delete=True, name="my-resource" ) primary_ips_client._client.request.assert_called_with( url="/primary_ips/1", method="PUT", json={"auto_delete": True, "name": "my-resource"}, ) assert primary_ip.id == 42 assert primary_ip.auto_delete is True assert primary_ip.name == "my-resource" @pytest.mark.parametrize( "primary_ip", [PrimaryIP(id=1), BoundPrimaryIP(mock.MagicMock(), dict(id=1))] ) def test_change_protection(self, primary_ips_client, primary_ip, generic_action): primary_ips_client._client.request.return_value = generic_action action = primary_ips_client.change_protection(primary_ip, True) primary_ips_client._client.request.assert_called_with( url="/primary_ips/1/actions/change_protection", method="POST", json={"delete": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "primary_ip", [PrimaryIP(id=1), BoundPrimaryIP(mock.MagicMock(), dict(id=1))] ) def test_delete(self, primary_ips_client, primary_ip, generic_action): primary_ips_client._client.request.return_value = generic_action delete_success = primary_ips_client.delete(primary_ip) primary_ips_client._client.request.assert_called_with( url="/primary_ips/1", method="DELETE" ) assert delete_success is True @pytest.mark.parametrize( "assignee_id,assignee_type,primary_ip", [ (1, "server", PrimaryIP(id=12)), (1, "server", BoundPrimaryIP(mock.MagicMock(), dict(id=12))), ], ) def test_assign( self, primary_ips_client, assignee_id, assignee_type, primary_ip, generic_action ): primary_ips_client._client.request.return_value = generic_action action = primary_ips_client.assign(primary_ip, assignee_id, assignee_type) primary_ips_client._client.request.assert_called_with( url="/primary_ips/12/actions/assign", method="POST", json={"assignee_id": 1, "assignee_type": "server"}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "primary_ip", [PrimaryIP(id=12), BoundPrimaryIP(mock.MagicMock(), dict(id=12))] ) def test_unassign(self, primary_ips_client, primary_ip, generic_action): primary_ips_client._client.request.return_value = generic_action action = primary_ips_client.unassign(primary_ip) primary_ips_client._client.request.assert_called_with( url="/primary_ips/12/actions/unassign", method="POST" ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "primary_ip", [PrimaryIP(id=12), BoundPrimaryIP(mock.MagicMock(), dict(id=12))] ) def test_change_dns_ptr(self, primary_ips_client, primary_ip, generic_action): primary_ips_client._client.request.return_value = generic_action action = primary_ips_client.change_dns_ptr( primary_ip, "1.2.3.4", "server02.example.com" ) primary_ips_client._client.request.assert_called_with( url="/primary_ips/12/actions/change_dns_ptr", method="POST", json={"ip": "1.2.3.4", "dns_ptr": "server02.example.com"}, ) assert action.id == 1 assert action.progress == 0 def test_actions_get_by_id(self, primary_ips_client, response_get_actions): primary_ips_client._client.request.return_value = { "action": response_get_actions["actions"][0] } action = primary_ips_client.actions.get_by_id(13) primary_ips_client._client.request.assert_called_with( url="/primary_ips/actions/13", method="GET" ) assert isinstance(action, BoundAction) assert action._client == primary_ips_client._client.actions assert action.id == 13 assert action.command == "assign_primary_ip" def test_actions_get_list(self, primary_ips_client, response_get_actions): primary_ips_client._client.request.return_value = response_get_actions result = primary_ips_client.actions.get_list() primary_ips_client._client.request.assert_called_with( url="/primary_ips/actions", method="GET", params={}, ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == primary_ips_client._client.actions assert actions[0].id == 13 assert actions[0].command == "assign_primary_ip" def test_actions_get_all(self, primary_ips_client, response_get_actions): primary_ips_client._client.request.return_value = response_get_actions actions = primary_ips_client.actions.get_all() primary_ips_client._client.request.assert_called_with( url="/primary_ips/actions", method="GET", params={"page": 1, "per_page": 50}, ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == primary_ips_client._client.actions assert actions[0].id == 13 assert actions[0].command == "assign_primary_ip" hcloud-python-2.3.0/tests/unit/primary_ips/test_domain.py000066400000000000000000000005671470147622500236700ustar00rootroot00000000000000from __future__ import annotations import datetime from datetime import timezone from hcloud.primary_ips import PrimaryIP class TestPrimaryIP: def test_created_is_datetime(self): primaryIP = PrimaryIP(id=1, created="2016-01-30T23:50+00:00") assert primaryIP.created == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) hcloud-python-2.3.0/tests/unit/server_types/000077500000000000000000000000001470147622500211745ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/server_types/__init__.py000066400000000000000000000000001470147622500232730ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/server_types/conftest.py000066400000000000000000000114531470147622500233770ustar00rootroot00000000000000from __future__ import annotations import pytest @pytest.fixture() def server_type_response(): return { "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", "architecture": "x86", "included_traffic": 21990232555520, "deprecated": True, "deprecation": { "announced": "2023-06-01T00:00:00+00:00", "unavailable_after": "2023-09-01T00:00:00+00:00", }, } } @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:00+00:00", "unavailable_after": "2023-09-01T00:00:00+00:00", }, }, { "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:00+00:00", "unavailable_after": "2023-09-01T00:00:00+00:00", }, } ] } hcloud-python-2.3.0/tests/unit/server_types/test_client.py000066400000000000000000000105221470147622500240630ustar00rootroot00000000000000from __future__ import annotations from datetime import datetime, timezone from unittest import mock import pytest from hcloud.server_types import BoundServerType, ServerTypesClient class TestBoundServerType: @pytest.fixture() def bound_server_type(self, hetzner_client): return BoundServerType(client=hetzner_client.server_types, data=dict(id=14)) def test_bound_server_type_init(self, server_type_response): bound_server_type = BoundServerType( client=mock.MagicMock(), data=server_type_response["server_type"] ) assert bound_server_type.id == 1 assert bound_server_type.name == "cx11" assert bound_server_type.description == "CX11" assert bound_server_type.cores == 1 assert bound_server_type.memory == 1 assert bound_server_type.disk == 25 assert bound_server_type.storage_type == "local" assert bound_server_type.cpu_type == "shared" assert bound_server_type.architecture == "x86" assert bound_server_type.deprecated is True assert bound_server_type.deprecation is not None assert bound_server_type.deprecation.announced == datetime( 2023, 6, 1, tzinfo=timezone.utc ) assert bound_server_type.deprecation.unavailable_after == datetime( 2023, 9, 1, tzinfo=timezone.utc ) assert bound_server_type.included_traffic == 21990232555520 class TestServerTypesClient: @pytest.fixture() def server_types_client(self): return ServerTypesClient(client=mock.MagicMock()) def test_get_by_id(self, server_types_client, server_type_response): server_types_client._client.request.return_value = server_type_response server_type = server_types_client.get_by_id(1) server_types_client._client.request.assert_called_with( url="/server_types/1", method="GET" ) 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, server_types_client, two_server_types_response, params): server_types_client._client.request.return_value = two_server_types_response result = server_types_client.get_list(**params) server_types_client._client.request.assert_called_with( url="/server_types", method="GET", params=params ) server_types = result.server_types assert result.meta is 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, server_types_client, two_server_types_response, params): server_types_client._client.request.return_value = two_server_types_response server_types = server_types_client.get_all(**params) params.update({"page": 1, "per_page": 50}) server_types_client._client.request.assert_called_with( url="/server_types", method="GET", 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, server_types_client, one_server_types_response): server_types_client._client.request.return_value = one_server_types_response server_type = server_types_client.get_by_name("cx11") params = {"name": "cx11"} server_types_client._client.request.assert_called_with( url="/server_types", method="GET", params=params ) assert server_type._client is server_types_client assert server_type.id == 1 assert server_type.name == "cx11" hcloud-python-2.3.0/tests/unit/servers/000077500000000000000000000000001470147622500201335ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/servers/__init__.py000066400000000000000000000000001470147622500222320ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/servers/conftest.py000066400000000000000000000756461470147622500223540ustar00rootroot00000000000000from __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", } } hcloud-python-2.3.0/tests/unit/servers/test_client.py000066400000000000000000001415031470147622500230260ustar00rootroot00000000000000from __future__ import annotations from unittest import mock import pytest 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 class TestBoundServer: @pytest.fixture() def bound_server(self, hetzner_client): return BoundServer(client=hetzner_client.servers, data=dict(id=14)) def test_bound_server_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 assert isinstance(bound_server.datacenter, BoundDatacenter) assert ( bound_server.datacenter._client == bound_server._client._client.datacenters ) assert bound_server.datacenter.id == 1 assert bound_server.datacenter.complete is True assert isinstance(bound_server.server_type, BoundServerType) assert ( bound_server.server_type._client == bound_server._client._client.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._client.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._client.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._client.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._client.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._client.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._client.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 @pytest.mark.parametrize( "params", [ { "status": [Server.STATUS_RUNNING], "sort": "status", "page": 1, "per_page": 10, }, {}, ], ) def test_get_actions_list( self, hetzner_client, bound_server, response_get_actions, params ): hetzner_client.request.return_value = response_get_actions result = bound_server.get_actions_list(**params) hetzner_client.request.assert_called_with( url="/servers/14/actions", method="GET", params=params ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "start_server" @pytest.mark.parametrize( "params", [{"status": [Server.STATUS_RUNNING], "sort": "status"}, {}] ) def test_get_actions( self, hetzner_client, bound_server, response_get_actions, params ): hetzner_client.request.return_value = response_get_actions actions = bound_server.get_actions(**params) params.update({"page": 1, "per_page": 50}) hetzner_client.request.assert_called_with( url="/servers/14/actions", method="GET", params=params ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "start_server" def test_update(self, hetzner_client, bound_server, response_update_server): hetzner_client.request.return_value = response_update_server server = bound_server.update(name="new-name", labels={}) hetzner_client.request.assert_called_with( url="/servers/14", method="PUT", json={"name": "new-name", "labels": {}} ) assert server.id == 14 assert server.name == "new-name" def test_delete(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.delete() hetzner_client.request.assert_called_with(url="/servers/14", method="DELETE") assert action.id == 1 assert action.progress == 0 def test_get_metrics( self, hetzner_client, bound_server: BoundServer, response_get_metrics, ): hetzner_client.request.return_value = response_get_metrics response = bound_server.get_metrics( type=["cpu", "disk"], start="2023-12-14T17:40:00+01:00", end="2023-12-14T17:50:00+01:00", ) hetzner_client.request.assert_called_with( url="/servers/14/metrics", method="GET", params={ "type": "cpu,disk", "start": "2023-12-14T17:40:00+01:00", "end": "2023-12-14T17:50:00+01:00", }, ) assert "cpu" in response.metrics.time_series assert "disk.0.iops.read" in response.metrics.time_series assert len(response.metrics.time_series["disk.0.iops.read"]["values"]) == 3 def test_power_off(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.power_off() hetzner_client.request.assert_called_with( url="/servers/14/actions/poweroff", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_power_on(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.power_on() hetzner_client.request.assert_called_with( url="/servers/14/actions/poweron", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_reboot(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.reboot() hetzner_client.request.assert_called_with( url="/servers/14/actions/reboot", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_reset(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.reset() hetzner_client.request.assert_called_with( url="/servers/14/actions/reset", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_shutdown(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.shutdown() hetzner_client.request.assert_called_with( url="/servers/14/actions/shutdown", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_reset_password( self, hetzner_client, bound_server, response_server_reset_password ): hetzner_client.request.return_value = response_server_reset_password response = bound_server.reset_password() hetzner_client.request.assert_called_with( url="/servers/14/actions/reset_password", method="POST" ) assert response.action.id == 1 assert response.action.progress == 0 assert response.root_password == "YItygq1v3GYjjMomLaKc" def test_change_type(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.change_type(ServerType(name="cx11"), upgrade_disk=True) hetzner_client.request.assert_called_with( url="/servers/14/actions/change_type", method="POST", json={"server_type": "cx11", "upgrade_disk": True}, ) assert action.id == 1 assert action.progress == 0 def test_enable_rescue( self, hetzner_client, bound_server, response_server_enable_rescue ): hetzner_client.request.return_value = response_server_enable_rescue response = bound_server.enable_rescue(type="linux64") hetzner_client.request.assert_called_with( url="/servers/14/actions/enable_rescue", method="POST", json={"type": "linux64"}, ) assert response.action.id == 1 assert response.action.progress == 0 assert response.root_password == "YItygq1v3GYjjMomLaKc" def test_disable_rescue(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.disable_rescue() hetzner_client.request.assert_called_with( url="/servers/14/actions/disable_rescue", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_create_image( self, hetzner_client, bound_server, response_server_create_image ): hetzner_client.request.return_value = response_server_create_image response = bound_server.create_image(description="my image", type="snapshot") hetzner_client.request.assert_called_with( url="/servers/14/actions/create_image", method="POST", json={"description": "my image", "type": "snapshot"}, ) assert response.action.id == 1 assert response.action.progress == 0 assert response.image.description == "my image" def test_rebuild(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action response = bound_server.rebuild( Image(name="ubuntu-20.04"), return_response=True, ) hetzner_client.request.assert_called_with( url="/servers/14/actions/rebuild", method="POST", json={"image": "ubuntu-20.04"}, ) assert response.action.id == 1 assert response.action.progress == 0 assert response.root_password is None or isinstance(response.root_password, str) def test_enable_backup(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.enable_backup() hetzner_client.request.assert_called_with( url="/servers/14/actions/enable_backup", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_disable_backup(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.disable_backup() hetzner_client.request.assert_called_with( url="/servers/14/actions/disable_backup", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_attach_iso(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.attach_iso(Iso(name="FreeBSD-11.0-RELEASE-amd64-dvd1")) hetzner_client.request.assert_called_with( url="/servers/14/actions/attach_iso", method="POST", json={"iso": "FreeBSD-11.0-RELEASE-amd64-dvd1"}, ) assert action.id == 1 assert action.progress == 0 def test_detach_iso(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.detach_iso() hetzner_client.request.assert_called_with( url="/servers/14/actions/detach_iso", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_change_dns_ptr(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.change_dns_ptr("1.2.3.4", "example.com") hetzner_client.request.assert_called_with( url="/servers/14/actions/change_dns_ptr", method="POST", json={"ip": "1.2.3.4", "dns_ptr": "example.com"}, ) assert action.id == 1 assert action.progress == 0 def test_change_protection(self, hetzner_client, bound_server, generic_action): hetzner_client.request.return_value = generic_action action = bound_server.change_protection(True, True) hetzner_client.request.assert_called_with( url="/servers/14/actions/change_protection", method="POST", json={"delete": True, "rebuild": True}, ) assert action.id == 1 assert action.progress == 0 def test_request_console( self, hetzner_client, bound_server, response_server_request_console ): hetzner_client.request.return_value = response_server_request_console response = bound_server.request_console() hetzner_client.request.assert_called_with( url="/servers/14/actions/request_console", method="POST" ) 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( "network", [Network(id=4711), BoundNetwork(mock.MagicMock(), dict(id=4711))] ) def test_attach_to_network( self, hetzner_client, bound_server, network, response_attach_to_network ): hetzner_client.request.return_value = response_attach_to_network action = bound_server.attach_to_network( network, "10.0.1.1", ["10.0.1.2", "10.0.1.3"] ) hetzner_client.request.assert_called_with( url="/servers/14/actions/attach_to_network", method="POST", 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( "network", [Network(id=4711), BoundNetwork(mock.MagicMock(), dict(id=4711))] ) def test_detach_from_network( self, hetzner_client, bound_server, network, response_detach_from_network ): hetzner_client.request.return_value = response_detach_from_network action = bound_server.detach_from_network(network) hetzner_client.request.assert_called_with( url="/servers/14/actions/detach_from_network", method="POST", json={"network": 4711}, ) assert action.id == 1 assert action.progress == 0 assert action.command == "detach_from_network" @pytest.mark.parametrize( "network", [Network(id=4711), BoundNetwork(mock.MagicMock(), dict(id=4711))] ) def test_change_alias_ips( self, hetzner_client, bound_server, network, response_change_alias_ips ): hetzner_client.request.return_value = response_change_alias_ips action = bound_server.change_alias_ips(network, ["10.0.1.2", "10.0.1.3"]) hetzner_client.request.assert_called_with( url="/servers/14/actions/change_alias_ips", method="POST", 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( "placement_group", [PlacementGroup(id=897), BoundPlacementGroup(mock.MagicMock, dict(id=897))], ) def test_add_to_placement_group( self, hetzner_client, bound_server, placement_group, response_add_to_placement_group, ): hetzner_client.request.return_value = response_add_to_placement_group action = bound_server.add_to_placement_group(placement_group) hetzner_client.request.assert_called_with( url="/servers/14/actions/add_to_placement_group", method="POST", json={"placement_group": "897"}, ) assert action.id == 13 assert action.progress == 0 assert action.command == "add_to_placement_group" def test_remove_from_placement_group( self, hetzner_client, bound_server, response_remove_from_placement_group ): hetzner_client.request.return_value = response_remove_from_placement_group action = bound_server.remove_from_placement_group() hetzner_client.request.assert_called_with( url="/servers/14/actions/remove_from_placement_group", method="POST" ) assert action.id == 13 assert action.progress == 100 assert action.command == "remove_from_placement_group" class TestServersClient: @pytest.fixture() def servers_client(self): return ServersClient(client=mock.MagicMock()) def test_get_by_id(self, servers_client, response_simple_server): servers_client._client.request.return_value = response_simple_server bound_server = servers_client.get_by_id(1) servers_client._client.request.assert_called_with( url="/servers/1", method="GET" ) 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, servers_client, response_simple_servers, params): servers_client._client.request.return_value = response_simple_servers result = servers_client.get_list(**params) servers_client._client.request.assert_called_with( url="/servers", method="GET", params=params ) bound_servers = result.servers assert result.meta is 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, servers_client, response_simple_servers, params): servers_client._client.request.return_value = response_simple_servers bound_servers = servers_client.get_all(**params) params.update({"page": 1, "per_page": 50}) servers_client._client.request.assert_called_with( url="/servers", method="GET", 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, servers_client, response_simple_servers): servers_client._client.request.return_value = response_simple_servers bound_server = servers_client.get_by_name("my-server") params = {"name": "my-server"} servers_client._client.request.assert_called_with( url="/servers", method="GET", 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, servers_client, response_create_simple_server ): servers_client._client.request.return_value = response_create_simple_server response = servers_client.create( "my-server", server_type=ServerType(name="cx11"), image=Image(id=4711), datacenter=Datacenter(id=1), ) servers_client._client.request.assert_called_with( url="/servers", method="POST", 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._client.actions assert bound_action.id == 1 assert bound_action.command == "create_server" def test_create_with_location(self, servers_client, response_create_simple_server): servers_client._client.request.return_value = response_create_simple_server response = servers_client.create( "my-server", server_type=ServerType(name="cx11"), image=Image(name="ubuntu-20.04"), location=Location(name="fsn1"), ) servers_client._client.request.assert_called_with( url="/servers", method="POST", 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._client.actions assert bound_action.id == 1 assert bound_action.command == "create_server" def test_create_with_volumes(self, servers_client, response_create_simple_server): servers_client._client.request.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, ) servers_client._client.request.assert_called_with( url="/servers", method="POST", 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._client.actions assert bound_action.id == 1 assert bound_action.command == "create_server" assert next_actions[0].id == 13 def test_create_with_networks(self, servers_client, response_create_simple_server): servers_client._client.request.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, ) servers_client._client.request.assert_called_with( url="/servers", method="POST", 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._client.actions assert bound_action.id == 1 assert bound_action.command == "create_server" assert next_actions[0].id == 13 def test_create_with_firewalls(self, servers_client, response_create_simple_server): servers_client._client.request.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, ) servers_client._client.request.assert_called_with( url="/servers", method="POST", 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._client.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, servers_client, response_create_simple_server ): servers_client._client.request.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, ) servers_client._client.request.assert_called_with( url="/servers", method="POST", 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._client.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_get_actions_list(self, servers_client, server, response_get_actions): servers_client._client.request.return_value = response_get_actions result = servers_client.get_actions_list(server) servers_client._client.request.assert_called_with( url="/servers/1/actions", method="GET", params={} ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == servers_client._client.actions assert actions[0].id == 13 assert actions[0].command == "start_server" @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_update(self, servers_client, server, response_update_server): servers_client._client.request.return_value = response_update_server server = servers_client.update(server, name="new-name", labels={}) servers_client._client.request.assert_called_with( url="/servers/1", method="PUT", 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, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.delete(server) servers_client._client.request.assert_called_with( url="/servers/1", method="DELETE" ) 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, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.power_off(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/poweroff", method="POST" ) 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, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.power_on(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/poweron", method="POST" ) 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, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.reboot(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/reboot", method="POST" ) 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, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.reset(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/reset", method="POST" ) 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, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.shutdown(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/shutdown", method="POST" ) 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, servers_client, server, response_server_reset_password ): servers_client._client.request.return_value = response_server_reset_password response = servers_client.reset_password(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/reset_password", method="POST" ) 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, servers_client, server, generic_action ): servers_client._client.request.return_value = generic_action action = servers_client.change_type( server, ServerType(name="cx11"), upgrade_disk=True ) servers_client._client.request.assert_called_with( url="/servers/1/actions/change_type", method="POST", 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, servers_client, server, generic_action ): servers_client._client.request.return_value = generic_action action = servers_client.change_type(server, ServerType(id=1), upgrade_disk=True) servers_client._client.request.assert_called_with( url="/servers/1/actions/change_type", method="POST", 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, servers_client, 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" servers_client._client.request.assert_not_called() @pytest.mark.parametrize( "server", [Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))] ) def test_enable_rescue(self, servers_client, server, response_server_enable_rescue): servers_client._client.request.return_value = response_server_enable_rescue response = servers_client.enable_rescue(server, "linux64", [2323]) servers_client._client.request.assert_called_with( url="/servers/1/actions/enable_rescue", method="POST", 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, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.disable_rescue(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/disable_rescue", method="POST" ) 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, servers_client, server, response_server_create_image): servers_client._client.request.return_value = response_server_create_image response = servers_client.create_image( server, description="my image", type="snapshot", labels={"key": "value"} ) servers_client._client.request.assert_called_with( url="/servers/1/actions/create_image", method="POST", 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, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action response = servers_client.rebuild( server, Image(name="ubuntu-20.04"), return_response=True, ) servers_client._client.request.assert_called_with( url="/servers/1/actions/rebuild", method="POST", json={"image": "ubuntu-20.04"}, ) assert 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, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.enable_backup(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/enable_backup", method="POST" ) 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, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.disable_backup(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/disable_backup", method="POST" ) 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, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.attach_iso( server, Iso(name="FreeBSD-11.0-RELEASE-amd64-dvd1") ) servers_client._client.request.assert_called_with( url="/servers/1/actions/attach_iso", method="POST", 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, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.detach_iso(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/detach_iso", method="POST" ) 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, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.change_dns_ptr(server, "1.2.3.4", "example.com") servers_client._client.request.assert_called_with( url="/servers/1/actions/change_dns_ptr", method="POST", 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, servers_client, server, generic_action): servers_client._client.request.return_value = generic_action action = servers_client.change_protection(server, True, True) servers_client._client.request.assert_called_with( url="/servers/1/actions/change_protection", method="POST", 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, servers_client, server, response_server_request_console ): servers_client._client.request.return_value = response_server_request_console response = servers_client.request_console(server) servers_client._client.request.assert_called_with( url="/servers/1/actions/request_console", method="POST" ) 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, servers_client, server, network, response_attach_to_network ): servers_client._client.request.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"] ) servers_client._client.request.assert_called_with( url="/servers/1/actions/attach_to_network", method="POST", 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, servers_client, server, network, response_detach_from_network ): servers_client._client.request.return_value = response_detach_from_network action = servers_client.detach_from_network(server, network) servers_client._client.request.assert_called_with( url="/servers/1/actions/detach_from_network", method="POST", 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, servers_client, server, network, response_change_alias_ips ): servers_client._client.request.return_value = response_change_alias_ips action = servers_client.change_alias_ips( server, network, ["10.0.1.2", "10.0.1.3"] ) servers_client._client.request.assert_called_with( url="/servers/1/actions/change_alias_ips", method="POST", 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" def test_actions_get_by_id(self, servers_client, response_get_actions): servers_client._client.request.return_value = { "action": response_get_actions["actions"][0] } action = servers_client.actions.get_by_id(13) servers_client._client.request.assert_called_with( url="/servers/actions/13", method="GET" ) assert isinstance(action, BoundAction) assert action._client == servers_client._client.actions assert action.id == 13 assert action.command == "start_server" def test_actions_get_list(self, servers_client, response_get_actions): servers_client._client.request.return_value = response_get_actions result = servers_client.actions.get_list() servers_client._client.request.assert_called_with( url="/servers/actions", method="GET", params={}, ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == servers_client._client.actions assert actions[0].id == 13 assert actions[0].command == "start_server" def test_actions_get_all(self, servers_client, response_get_actions): servers_client._client.request.return_value = response_get_actions actions = servers_client.actions.get_all() servers_client._client.request.assert_called_with( url="/servers/actions", method="GET", params={"page": 1, "per_page": 50}, ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == servers_client._client.actions assert actions[0].id == 13 assert actions[0].command == "start_server" hcloud-python-2.3.0/tests/unit/servers/test_domain.py000066400000000000000000000005441470147622500230160ustar00rootroot00000000000000from __future__ import annotations import datetime from datetime import timezone from hcloud.servers import Server 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 ) hcloud-python-2.3.0/tests/unit/ssh_keys/000077500000000000000000000000001470147622500202725ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/ssh_keys/__init__.py000066400000000000000000000000001470147622500223710ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/ssh_keys/conftest.py000066400000000000000000000035241470147622500224750ustar00rootroot00000000000000from __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", } } hcloud-python-2.3.0/tests/unit/ssh_keys/test_client.py000066400000000000000000000151011470147622500231570ustar00rootroot00000000000000from __future__ import annotations from unittest import mock import pytest from hcloud.ssh_keys import BoundSSHKey, SSHKey, SSHKeysClient class TestBoundSSHKey: @pytest.fixture() def bound_ssh_key(self, hetzner_client): return BoundSSHKey(client=hetzner_client.ssh_keys, data=dict(id=14)) def test_bound_ssh_key_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" def test_update(self, hetzner_client, bound_ssh_key, response_update_ssh_key): hetzner_client.request.return_value = response_update_ssh_key ssh_key = bound_ssh_key.update(name="New name") hetzner_client.request.assert_called_with( url="/ssh_keys/14", method="PUT", json={"name": "New name"} ) assert ssh_key.id == 2323 assert ssh_key.name == "New name" def test_delete(self, hetzner_client, bound_ssh_key, generic_action): hetzner_client.request.return_value = generic_action delete_success = bound_ssh_key.delete() hetzner_client.request.assert_called_with(url="/ssh_keys/14", method="DELETE") assert delete_success is True class TestSSHKeysClient: @pytest.fixture() def ssh_keys_client(self): return SSHKeysClient(client=mock.MagicMock()) def test_get_by_id(self, ssh_keys_client, ssh_key_response): ssh_keys_client._client.request.return_value = ssh_key_response ssh_key = ssh_keys_client.get_by_id(1) ssh_keys_client._client.request.assert_called_with( url="/ssh_keys/1", method="GET" ) 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, ssh_keys_client, two_ssh_keys_response, params): ssh_keys_client._client.request.return_value = two_ssh_keys_response result = ssh_keys_client.get_list(**params) ssh_keys_client._client.request.assert_called_with( url="/ssh_keys", method="GET", 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, ssh_keys_client, two_ssh_keys_response, params): ssh_keys_client._client.request.return_value = two_ssh_keys_response ssh_keys = ssh_keys_client.get_all(**params) params.update({"page": 1, "per_page": 50}) ssh_keys_client._client.request.assert_called_with( url="/ssh_keys", method="GET", 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, ssh_keys_client, one_ssh_keys_response): ssh_keys_client._client.request.return_value = one_ssh_keys_response ssh_keys = ssh_keys_client.get_by_name("SSH-Key") params = {"name": "SSH-Key"} ssh_keys_client._client.request.assert_called_with( url="/ssh_keys", method="GET", 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, ssh_keys_client, one_ssh_keys_response): ssh_keys_client._client.request.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"} ssh_keys_client._client.request.assert_called_with( url="/ssh_keys", method="GET", 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, ssh_keys_client, ssh_key_response): ssh_keys_client._client.request.return_value = ssh_key_response ssh_key = ssh_keys_client.create( name="My ssh key", public_key="ssh-rsa AAAjjk76kgf...Xt" ) ssh_keys_client._client.request.assert_called_with( url="/ssh_keys", method="POST", 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, ssh_keys_client, ssh_key, response_update_ssh_key): ssh_keys_client._client.request.return_value = response_update_ssh_key ssh_key = ssh_keys_client.update(ssh_key, name="New name") ssh_keys_client._client.request.assert_called_with( url="/ssh_keys/1", method="PUT", 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, ssh_keys_client, ssh_key, generic_action): ssh_keys_client._client.request.return_value = generic_action delete_success = ssh_keys_client.delete(ssh_key) ssh_keys_client._client.request.assert_called_with( url="/ssh_keys/1", method="DELETE" ) assert delete_success is True hcloud-python-2.3.0/tests/unit/ssh_keys/test_domain.py000066400000000000000000000005451470147622500231560ustar00rootroot00000000000000from __future__ import annotations import datetime from datetime import timezone from hcloud.ssh_keys import SSHKey class TestSSHKey: def test_created_is_datetime(self): sshKey = SSHKey(id=1, created="2016-01-30T23:50+00:00") assert sshKey.created == datetime.datetime( 2016, 1, 30, 23, 50, tzinfo=timezone.utc ) hcloud-python-2.3.0/tests/unit/test_client.py000066400000000000000000000227311470147622500213360ustar00rootroot00000000000000from __future__ import annotations import json from unittest.mock import MagicMock import pytest import requests from hcloud import ( APIException, Client, constant_backoff_function, exponential_backoff_function, ) class TestHetznerClient: @pytest.fixture() def client(self): Client._version = "0.0.0" client = Client(token="project_token") client._requests_session = MagicMock() return client @pytest.fixture() def response(self): response = requests.Response() response.status_code = 200 response._content = json.dumps({"result": "data"}).encode("utf-8") return response @pytest.fixture() def fail_response(self, response): response.status_code = 422 error = { "code": "invalid_input", "message": "invalid input in field 'broken_field': is too long", "details": { "fields": [{"name": "broken_field", "messages": ["is too long"]}] }, } response._content = json.dumps({"error": error}).encode("utf-8") return response @pytest.fixture() def rate_limit_response(self, response): response.status_code = 422 error = { "code": "rate_limit_exceeded", "message": "limit of 10 requests per hour reached", "details": {}, } response._content = json.dumps({"error": error}).encode("utf-8") return response def test__get_user_agent(self, client): user_agent = client._get_user_agent() assert user_agent == "hcloud-python/0.0.0" def test__get_user_agent_with_application_name(self, client): client = Client(token="project_token", application_name="my-app") user_agent = client._get_user_agent() assert user_agent == "my-app hcloud-python/0.0.0" def test__get_user_agent_with_application_name_and_version(self, client): client = Client( token="project_token", application_name="my-app", application_version="1.0.0", ) user_agent = client._get_user_agent() assert user_agent == "my-app/1.0.0 hcloud-python/0.0.0" def test__get_headers(self, client): headers = client._get_headers() assert headers == { "User-Agent": "hcloud-python/0.0.0", "Authorization": "Bearer project_token", } def test_request_ok(self, client, response): client._requests_session.request.return_value = response response = client.request( "POST", "/servers", params={"argument": "value"}, timeout=2 ) client._requests_session.request.assert_called_once_with( method="POST", url="https://api.hetzner.cloud/v1/servers", headers={ "User-Agent": "hcloud-python/0.0.0", "Authorization": "Bearer project_token", }, params={"argument": "value"}, timeout=2, ) assert response == {"result": "data"} def test_request_fails(self, client, fail_response): client._requests_session.request.return_value = fail_response with pytest.raises(APIException) as exception_info: client.request( "POST", "http://url.com", params={"argument": "value"}, timeout=2 ) error = exception_info.value assert error.code == "invalid_input" assert error.message == "invalid input in field 'broken_field': is too long" assert error.details["fields"][0]["name"] == "broken_field" def test_request_fails_correlation_id(self, client, response): response.headers["X-Correlation-Id"] = "67ed842dc8bc8673" response.status_code = 422 response._content = json.dumps( { "error": { "code": "service_error", "message": "Something crashed", } } ).encode("utf-8") client._requests_session.request.return_value = response with pytest.raises(APIException) as exception_info: client.request( "POST", "http://url.com", params={"argument": "value"}, timeout=2 ) error = exception_info.value assert error.code == "service_error" assert error.message == "Something crashed" assert error.details is None assert error.correlation_id == "67ed842dc8bc8673" assert str(error) == "Something crashed (service_error, 67ed842dc8bc8673)" def test_request_500(self, client, fail_response): fail_response.status_code = 500 fail_response.reason = "Internal Server Error" fail_response._content = "Internal Server Error" client._requests_session.request.return_value = fail_response with pytest.raises(APIException) as exception_info: client.request( "POST", "http://url.com", params={"argument": "value"}, timeout=2 ) error = exception_info.value assert error.code == 500 assert error.message == "Internal Server Error" assert error.details["content"] == "Internal Server Error" def test_request_broken_json_200(self, client, response): content = b"{'key': 'value'" response.reason = "OK" response._content = content client._requests_session.request.return_value = response with pytest.raises(APIException) as exception_info: client.request( "POST", "http://url.com", params={"argument": "value"}, timeout=2 ) error = exception_info.value assert error.code == 200 assert error.message == "OK" assert error.details["content"] == content def test_request_empty_content_200(self, client, response): content = "" response.reason = "OK" response._content = content client._requests_session.request.return_value = response response = client.request( "POST", "http://url.com", params={"argument": "value"}, timeout=2 ) assert response == {} def test_request_500_empty_content(self, client, fail_response): fail_response.status_code = 500 fail_response.reason = "Internal Server Error" fail_response._content = "" client._requests_session.request.return_value = fail_response with pytest.raises(APIException) as exception_info: client.request( "POST", "http://url.com", params={"argument": "value"}, timeout=2 ) error = exception_info.value assert error.code == 500 assert error.message == "Internal Server Error" assert error.details["content"] == "" assert str(error) == "Internal Server Error (500)" def test_request_limit(self, client, rate_limit_response): client._retry_interval = constant_backoff_function(0.0) client._requests_session.request.return_value = rate_limit_response with pytest.raises(APIException) as exception_info: client.request( "POST", "http://url.com", params={"argument": "value"}, timeout=2 ) error = exception_info.value assert client._requests_session.request.call_count == 6 assert error.code == "rate_limit_exceeded" assert error.message == "limit of 10 requests per hour reached" def test_request_limit_then_success(self, client, rate_limit_response): client._retry_interval = constant_backoff_function(0.0) response = requests.Response() response.status_code = 200 response._content = json.dumps({"result": "data"}).encode("utf-8") client._requests_session.request.side_effect = [rate_limit_response, response] client.request( "POST", "http://url.com", params={"argument": "value"}, timeout=2 ) assert client._requests_session.request.call_count == 2 @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, exception, expected): assert client._retry_policy(exception) == expected 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_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] hcloud-python-2.3.0/tests/unit/volumes/000077500000000000000000000000001470147622500201345ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/volumes/__init__.py000066400000000000000000000000001470147622500222330ustar00rootroot00000000000000hcloud-python-2.3.0/tests/unit/volumes/conftest.py000066400000000000000000000140451470147622500223370ustar00rootroot00000000000000from __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"}, } ] } hcloud-python-2.3.0/tests/unit/volumes/test_client.py000066400000000000000000000406301470147622500230260ustar00rootroot00000000000000from __future__ import annotations from unittest import mock import pytest from dateutil.parser import isoparse from hcloud.actions import BoundAction from hcloud.locations import BoundLocation, Location from hcloud.servers import BoundServer, Server from hcloud.volumes import BoundVolume, Volume, VolumesClient class TestBoundVolume: @pytest.fixture() def bound_volume(self, hetzner_client): return BoundVolume(client=hetzner_client.volumes, 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 def test_get_actions(self, hetzner_client, bound_volume, response_get_actions): hetzner_client.request.return_value = response_get_actions actions = bound_volume.get_actions(sort="id") hetzner_client.request.assert_called_with( url="/volumes/14/actions", method="GET", params={"page": 1, "per_page": 50, "sort": "id"}, ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == hetzner_client.actions assert actions[0].id == 13 assert actions[0].command == "attach_volume" def test_update(self, hetzner_client, bound_volume, response_update_volume): hetzner_client.request.return_value = response_update_volume volume = bound_volume.update(name="new-name") hetzner_client.request.assert_called_with( url="/volumes/14", method="PUT", json={"name": "new-name"} ) assert volume.id == 4711 assert volume.name == "new-name" def test_delete(self, hetzner_client, bound_volume, generic_action): hetzner_client.request.return_value = generic_action delete_success = bound_volume.delete() hetzner_client.request.assert_called_with(url="/volumes/14", method="DELETE") assert delete_success is True def test_change_protection(self, hetzner_client, bound_volume, generic_action): hetzner_client.request.return_value = generic_action action = bound_volume.change_protection(True) hetzner_client.request.assert_called_with( url="/volumes/14/actions/change_protection", method="POST", json={"delete": True}, ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", (Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))) ) def test_attach(self, hetzner_client, bound_volume, server, generic_action): hetzner_client.request.return_value = generic_action action = bound_volume.attach(server) hetzner_client.request.assert_called_with( url="/volumes/14/actions/attach", method="POST", json={"server": 1} ) assert action.id == 1 assert action.progress == 0 @pytest.mark.parametrize( "server", (Server(id=1), BoundServer(mock.MagicMock(), dict(id=1))) ) def test_attach_with_automount( self, hetzner_client, bound_volume, server, generic_action ): hetzner_client.request.return_value = generic_action action = bound_volume.attach(server, False) hetzner_client.request.assert_called_with( url="/volumes/14/actions/attach", method="POST", json={"server": 1, "automount": False}, ) assert action.id == 1 assert action.progress == 0 def test_detach(self, hetzner_client, bound_volume, generic_action): hetzner_client.request.return_value = generic_action action = bound_volume.detach() hetzner_client.request.assert_called_with( url="/volumes/14/actions/detach", method="POST" ) assert action.id == 1 assert action.progress == 0 def test_resize(self, hetzner_client, bound_volume, generic_action): hetzner_client.request.return_value = generic_action action = bound_volume.resize(50) hetzner_client.request.assert_called_with( url="/volumes/14/actions/resize", method="POST", json={"size": 50} ) assert action.id == 1 assert action.progress == 0 class TestVolumesClient: @pytest.fixture() def volumes_client(self): return VolumesClient(client=mock.MagicMock()) def test_get_by_id(self, volumes_client, volume_response): volumes_client._client.request.return_value = volume_response bound_volume = volumes_client.get_by_id(1) volumes_client._client.request.assert_called_with( url="/volumes/1", method="GET" ) 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, volumes_client, two_volumes_response, params): volumes_client._client.request.return_value = two_volumes_response result = volumes_client.get_list(**params) volumes_client._client.request.assert_called_with( url="/volumes", method="GET", params=params ) bound_volumes = result.volumes assert result.meta is 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, volumes_client, two_volumes_response, params): volumes_client._client.request.return_value = two_volumes_response bound_volumes = volumes_client.get_all(**params) params.update({"page": 1, "per_page": 50}) volumes_client._client.request.assert_called_with( url="/volumes", method="GET", 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, volumes_client, one_volumes_response): volumes_client._client.request.return_value = one_volumes_response bound_volume = volumes_client.get_by_name("database-storage") params = {"name": "database-storage"} volumes_client._client.request.assert_called_with( url="/volumes", method="GET", 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, volumes_client, volume_create_response): volumes_client._client.request.return_value = volume_create_response response = volumes_client.create( 100, "database-storage", location=Location(name="location"), automount=False, format="xfs", ) volumes_client._client.request.assert_called_with( url="/volumes", method="POST", 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, volumes_client, server, volume_create_response): volumes_client._client.request.return_value = volume_create_response volumes_client.create( 100, "database-storage", server=server, automount=False, format="xfs" ) volumes_client._client.request.assert_called_with( url="/volumes", method="POST", json={ "name": "database-storage", "size": 100, "server": 1, "automount": False, "format": "xfs", }, ) def test_create_negative_size(self, 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" volumes_client._client.request.assert_not_called() @pytest.mark.parametrize( "location,server", [(None, None), ("location", Server(id=1))] ) def test_create_wrong_location_server_combination( self, volumes_client, 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" volumes_client._client.request.assert_not_called() @pytest.mark.parametrize( "volume", [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=1))] ) def test_get_actions_list(self, volumes_client, volume, response_get_actions): volumes_client._client.request.return_value = response_get_actions result = volumes_client.get_actions_list(volume, sort="id") volumes_client._client.request.assert_called_with( url="/volumes/1/actions", method="GET", params={"sort": "id"} ) actions = result.actions assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == volumes_client._client.actions assert actions[0].id == 13 assert actions[0].command == "attach_volume" @pytest.mark.parametrize( "volume", [Volume(id=1), BoundVolume(mock.MagicMock(), dict(id=1))] ) def test_update(self, volumes_client, volume, response_update_volume): volumes_client._client.request.return_value = response_update_volume volume = volumes_client.update(volume, name="new-name") volumes_client._client.request.assert_called_with( url="/volumes/1", method="PUT", 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, volumes_client, volume, generic_action): volumes_client._client.request.return_value = generic_action action = volumes_client.change_protection(volume, True) volumes_client._client.request.assert_called_with( url="/volumes/1/actions/change_protection", method="POST", 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, volumes_client, volume, generic_action): volumes_client._client.request.return_value = generic_action delete_success = volumes_client.delete(volume) volumes_client._client.request.assert_called_with( url="/volumes/1", method="DELETE" ) 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, volumes_client, server, volume, generic_action): volumes_client._client.request.return_value = generic_action action = volumes_client.attach(volume, server) volumes_client._client.request.assert_called_with( url="/volumes/12/actions/attach", method="POST", json={"server": 1} ) 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, volumes_client, volume, generic_action): volumes_client._client.request.return_value = generic_action action = volumes_client.detach(volume) volumes_client._client.request.assert_called_with( url="/volumes/12/actions/detach", method="POST" ) 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, volumes_client, volume, generic_action): volumes_client._client.request.return_value = generic_action action = volumes_client.resize(volume, 50) volumes_client._client.request.assert_called_with( url="/volumes/12/actions/resize", method="POST", json={"size": 50} ) assert action.id == 1 assert action.progress == 0 def test_actions_get_by_id(self, volumes_client, response_get_actions): volumes_client._client.request.return_value = { "action": response_get_actions["actions"][0] } action = volumes_client.actions.get_by_id(13) volumes_client._client.request.assert_called_with( url="/volumes/actions/13", method="GET" ) assert isinstance(action, BoundAction) assert action._client == volumes_client._client.actions assert action.id == 13 assert action.command == "attach_volume" def test_actions_get_list(self, volumes_client, response_get_actions): volumes_client._client.request.return_value = response_get_actions result = volumes_client.actions.get_list() volumes_client._client.request.assert_called_with( url="/volumes/actions", method="GET", params={}, ) actions = result.actions assert result.meta is None assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == volumes_client._client.actions assert actions[0].id == 13 assert actions[0].command == "attach_volume" def test_actions_get_all(self, volumes_client, response_get_actions): volumes_client._client.request.return_value = response_get_actions actions = volumes_client.actions.get_all() volumes_client._client.request.assert_called_with( url="/volumes/actions", method="GET", params={"page": 1, "per_page": 50}, ) assert len(actions) == 1 assert isinstance(actions[0], BoundAction) assert actions[0]._client == volumes_client._client.actions assert actions[0].id == 13 assert actions[0].command == "attach_volume" hcloud-python-2.3.0/tests/unit/volumes/test_domain.py000066400000000000000000000005441470147622500230170ustar00rootroot00000000000000from __future__ import annotations import datetime from datetime import timezone from hcloud.volumes import Volume 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 ) hcloud-python-2.3.0/tox.ini000066400000000000000000000004121470147622500156310ustar00rootroot00000000000000[tox] envlist = py38, py39, py310, py311, py312 [testenv] passenv = FAKE_API_ENDPOINT deps = -e.[test] commands = pytest tests/unit {posargs} [gh-actions] python = 3.8: py38 3.9: py39 3.10: py310 3.11: py311 3.12: py312 3.13: py313