diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..97d70ba --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,4 @@ +paths: + .github/workflows/**/*.{yml,yaml}: + ignore: + - '"paths" section must be sequence node but got alias node with "" tag' diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 0371331..681cad8 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -18,6 +18,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + contents: read + steps: - name: "Checkout" uses: actions/checkout@v6 @@ -52,17 +55,13 @@ jobs: python -m build - name: "List Artifacts" - env: - path: ${{ inputs.path }} + working-directory: ${{ inputs.path }} run: | - echo "::group::ls" - ls -lAhR "${path}" - echo "::endgroup::" - echo "::group::tree" - tree "${path}" + results="$(tree . || ls -lAhR .)" + echo "::group::results" + echo "${results}" echo "::endgroup::" - results="$(tree "${path}")" - markdown='Artifacts:\n```text\n'"${results}"'\n```' + markdown='Artifacts: `${{ inputs.path }}`\n```text\n'"${results}"'\n```' echo -e "${markdown}" >> $GITHUB_STEP_SUMMARY - name: "Upload to Actions" diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 21272c5..3ec6a5e 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -13,7 +13,7 @@ on: default: site env: - logo: "https://raw.githubusercontent.com/smashedr/repo-images/refs/heads/master/vultr-python/logo128.png" + logo: "https://raw.githubusercontent.com/cssnr/vultr-python/refs/heads/master/.github/assets/logo.svg" link: ${{ github.event.repository.html_url }} jobs: @@ -22,9 +22,12 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + contents: read + steps: - name: "Checkout" - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: "Debug CTX github" continue-on-error: true @@ -50,29 +53,29 @@ jobs: python -m pip install -U pdoc python -m pip install -e . python -m pdoc -t docs -o "${{ inputs.path }}" \ - --favicon "${{ env.logo }}" \ --logo "${{ env.logo }}" \ --logo-link "${{ env.link }}" \ vultr - - name: "Update Permissions" + - name: "Fix Docs" + working-directory: ${{ inputs.path }} run: | - chmod -c -R +rX "${{ inputs.path }}" | while read name; do - echo "::notice::Fixed invalid file permissions: ${name}" - done + mv -f vultr.html index.html + + #- name: "Update Permissions" + # run: | + # chmod -c -R +rX "${{ inputs.path }}" | while read name; do + # echo "::notice::Fixed invalid file permissions: ${name}" + # done - name: "List Artifacts" - env: - path: ${{ inputs.path }} + working-directory: ${{ inputs.path }} run: | - echo "::group::ls" - ls -lAhR "${path}" - echo "::endgroup::" - echo "::group::tree" - tree "${path}" + results="$(tree . || ls -lAhR .)" + echo "::group::results" + echo "${results}" echo "::endgroup::" - results="$(tree "${path}")" - markdown='Artifacts:\n```text\n'"${results}"'\n```' + markdown='Artifacts: `${{ inputs.path }}`\n```text\n'"${results}"'\n```' echo -e "${markdown}" >> $GITHUB_STEP_SUMMARY - name: "Upload Pages Artifact" diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 7dfc8cd..8268ad4 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -26,7 +26,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: "Debug event.json" continue-on-error: true @@ -137,19 +137,19 @@ jobs: echo "::endgroup::" "${RUNNER_TEMP}/actionlint" -color -verbose -shellcheck= -pyflakes= - #- name: "pytest" - # if: ${{ !cancelled() }} - # id: coverage - # run: | - # coverage run -m pytest - # coverage xml - # coverage report -m - - #- name: "codecov" - # if: ${{ !cancelled() && steps.coverage.outcome == 'success' }} - # uses: codecov/codecov-action@v5 - # with: - # token: ${{ secrets.CODECOV_TOKEN }} + - name: "pytest" + if: ${{ !cancelled() }} + id: coverage + run: | + coverage run -m pytest + coverage xml + coverage report -m + + - name: "codecov" + if: ${{ !cancelled() && steps.coverage.outcome == 'success' }} + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} #- name: "hadolint" # if: ${{ !cancelled() }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 6c917d7..4b6e6b9 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -16,6 +16,9 @@ jobs: with: name: "release" + permissions: + contents: read + publish: name: "Publish" runs-on: ubuntu-latest @@ -107,6 +110,9 @@ jobs: timeout-minutes: 5 needs: [publish] + permissions: + contents: write + steps: - name: "Debug" continue-on-error: true diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..e61026f --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,85 @@ +name: "Test" + +on: + workflow_dispatch: + push: + branches: [master] + paths: &paths + - ".github/workflows/test.yaml" + - "src/**" + - "tests/**" + - "pyproject.toml" + pull_request: + paths: *paths + +jobs: + build: + name: "Build" + if: ${{ !contains(github.event.head_commit.message, '#notest') }} + uses: ./.github/workflows/build.yaml + with: + name: test + + permissions: + contents: read + + test: + runs-on: ubuntu-22.04 + timeout-minutes: 5 + needs: [build] + strategy: + fail-fast: false + matrix: + version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + name: "Test ${{ matrix.version }}" + + permissions: + contents: read + + steps: + - name: "Checkout" + uses: actions/checkout@v6 + + - name: "Debug event.json" + if: ${{ !github.event.act }} + continue-on-error: true + run: cat "${GITHUB_EVENT_PATH}" + + - name: "Debug CTX github" + if: ${{ !github.event.act }} + continue-on-error: true + env: + GITHUB_CTX: ${{ toJSON(github) }} + run: echo "$GITHUB_CTX" + + - name: "Debug Environment" + if: ${{ !github.event.act }} + continue-on-error: true + run: env + + - name: "Download Artifact" + uses: actions/download-artifact@v6 + with: + name: test + path: dist + + - name: "Setup Python ${{ matrix.version }}" + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.version }} + cache: "pip" + + - name: "Install ${{ matrix.version }}" + run: | + python -V + python -m pip install -U pip pytest + python -m pip install dist/*.whl + + #- name: "Debug ${{ matrix.version }}" + # run: | + # ls -lAhR dist/ + + - name: "Test ${{ matrix.version }}" + #continue-on-error: ${{ contains('dev', matrix.version) }} + run: | + pytest -s diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..1eeef06 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +prune tests diff --git a/README.md b/README.md index 06c1747..48fd2e1 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ [![Codacy Badge](https://app.codacy.com/project/badge/Grade/9b356c4327df41e395c81de1c717ce11)](https://app.codacy.com/gh/cssnr/vultr-python/dashboard) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=cssnr_vultr-python&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=cssnr_vultr-python) [![Workflow Lint](https://img.shields.io/github/actions/workflow/status/cssnr/vultr-python/lint.yaml?logo=cachet&label=lint)](https://github.com/cssnr/vultr-python/actions/workflows/lint.yaml) -[![GitHub Deployments](https://img.shields.io/github/deployments/cssnr/vultr-python/github-pages?logo=materialformkdocs&logoColor=white&label=github-pages)](https://cssnr.github.io/vultr-python) +[![Workflow Test](https://img.shields.io/github/actions/workflow/status/cssnr/vultr-python/test.yaml?logo=cachet&label=test)](https://github.com/cssnr/vultr-python/actions/workflows/test.yaml) +[![Deployments PyPi](https://img.shields.io/github/deployments/cssnr/vultr-python/pypi?logo=pypi&logoColor=white&label=pypi)](https://pypi.org/project/vultr-python/) +[![Deployments Pages](https://img.shields.io/github/deployments/cssnr/vultr-python/github-pages?logo=materialformkdocs&logoColor=white&label=github-pages)](https://cssnr.github.io/vultr-python/) [![GitHub Last Commit](https://img.shields.io/github/last-commit/cssnr/vultr-python?logo=github&label=updated)](https://github.com/cssnr/vultr-python/graphs/commit-activity) [![GitHub Repo Size](https://img.shields.io/github/repo-size/cssnr/vultr-python?logo=bookstack&logoColor=white&label=repo%20size)](https://github.com/cssnr/vultr-python) [![GitHub Top Language](https://img.shields.io/github/languages/top/cssnr/vultr-python?logo=htmx&logoColor=white)](https://github.com/cssnr/vultr-python?tab=readme-ov-file#readme) @@ -19,8 +21,8 @@ # Vultr Python - -Vultr Python + +Vultr Python - [Install](#Install) - [Usage](#Usage) @@ -31,13 +33,12 @@ Python 3 wrapper for the Vultr API v2. [![GitHub](https://img.shields.io/badge/github-232925?style=for-the-badge&logo=github)](https://github.com/cssnr/vultr-python?tab=readme-ov-file#readme) [![PyPi](https://img.shields.io/badge/pypi-006dad?style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/vultr-python) -[![Docs](https://img.shields.io/badge/docs-198754?style=for-the-badge&logo=mdbook)](https://cssnr.github.io/vultr-python) +[![Docs](https://img.shields.io/badge/docs-198754?style=for-the-badge&logo=mdbook)](https://cssnr.github.io/vultr-python/) [![Vultr](https://img.shields.io/badge/vultr-007bfc?style=for-the-badge&logo=vultr)](https://www.vultr.com/api/?ref=6905748) Vultr API Reference: [https://www.vultr.com/api](https://www.vultr.com/api/?ref=6905748) > [!TIP] -> This project is not complete, but has many useful functions. > Please submit a [Feature Request](https://github.com/cssnr/vultr-python/discussions/categories/feature-requests) > or report any [Issues](https://github.com/cssnr/vultr-python/issues). @@ -60,57 +61,86 @@ python -m pip install vultr-python ## Usage -You will need to create an api key and whitelist your IP address. -Most functions do not work without an API Key. +You will need to create an api key and whitelist your IP address for most functions. - [https://my.vultr.com/settings/#settingsapi](https://my.vultr.com/settings/#settingsapi) -Initialize the class with your API Key or with the `VULTR_API_KEY` environment variable. +Initialize the `Vultr` class with your API Key or use the `VULTR_API_KEY` environment variable. ```python from vultr import Vultr -vultr = Vultr('VULTR_API_KEY') +vultr = Vultr("VULTR_API_KEY") ``` List plans and get available regions for that plan ```python -plans = vultr.list_plans() -plan = plans[0] # 0 seems to be the basic 5 dollar plan +plans = vultr.list_plans({"type": "vc2"}) # Filter by type +plan = plans[0] # 0 seems to be the base plan regions = vultr.list_regions() -available = vultr.filter_regions(regions, plan['locations']) +available = vultr.filter_regions(regions, plan["locations"]) ``` Get the OS list and filter by name ```python os_list = vultr.list_os() -ubuntu_lts = vultr.filter_os(os_list, 'Ubuntu 24.04 LTS x64') +ubuntu_lts = vultr.filter_os(os_list, "Ubuntu 24.04 LTS x64") ``` Create a new ssh key from key string ```python -sshkey = vultr.create_key('key-name', 'ssh-rsa AAAA...') +sshkey = vultr.create_key("key-name", "ssh-rsa AAAA...") +vultr.delete_key(sshkey['id']) ``` Create a new instance ```python -hostname = 'my-new-host' data = { - 'region': available[0]['id'], - 'plan': plan['id'], - 'os_id': ubuntu_lts['id'], - 'sshkey_id': [sshkey['id']], - 'hostname': hostname, - 'label': hostname, + "os_id": ubuntu_lts["id"], + "sshkey_id": [sshkey["id"]], + "hostname": "my-new-host", + "label": "my-new-host", } -instance = vultr.create_instance(**data) +instance = vultr.create_instance(available[0], plan, **data) ``` -Full Documentation: [https://cssnr.github.io/vultr-python](https://cssnr.github.io/vultr-python) +Arbitrary Methods `get`, `post`, `patch`, `put`, `delete` + +```python +plans = vultr.get("/plans", {"type": "vc2"}) +sshkey = vultr.post("/ssh-keys", name="key-name", ssh_key="ssh-rsa AAAA...") +instance = vultr.patch("/instances/{instance-id}", plan=plans[1]["id"]) +database = vultr.put("/databases/{database-id}", tag="new tag") +vultr.delete("/snapshots/{snapshot-id}") +``` + +Error Handling + +```python +>>> instance = vultr.create_instance("atl", "vc2-1c-0.5gb-v6", os_id=2284) +Traceback (most recent call last): +vultr.vultr.VultrException: Error 400: Server add failed: Ubuntu 24.04 LTS x64 requires a plan with at least 1000 MB memory. +``` + +Using the `VultrException` class + +```python +from vultr import VultrException + +try: + instance = vultr.create_instance("atl", "vc2-1c-0.5gb-v6", os_id=2284) +except VultrException as error: + print(error.error) + # 'Server add failed: Ubuntu 24.04 LTS x64 requires a plan with at least 1000 MB memory.' + print(error.status) + # 400 +``` + +Full Documentation: [https://cssnr.github.io/vultr-python](https://cssnr.github.io/vultr-python/) Vultr API Reference: [https://www.vultr.com/api](https://www.vultr.com/api/?ref=6905748) diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..5db65bf --- /dev/null +++ b/build.ps1 @@ -0,0 +1,48 @@ +param ( + [switch]$c, + [switch]$i, + [switch]$u +) + +$ErrorActionPreference = "Stop" + +write-output "Clean: $c" +write-output "Install: $i" +write-output "Uninstall: $u" + +if ($u) { + Write-Host -ForegroundColor Red "Uninstalling..." + python -m pip uninstall -y vultr-python +} + +$egg_dir = ".\src\*.egg-info" +if (Test-Path $egg_dir) { + Write-Host -ForegroundColor Cyan "Removing: $egg_dir" + Remove-Item -Force -Recurse $egg_dir +} +$cache_dir = ".\src\*\__pycache__" +if (Test-Path $cache_dir) { + Write-Host -ForegroundColor Cyan "Removing: $cache_dir" + Remove-Item -Force -Recurse $cache_dir +} +if (Test-Path ".\dist") { + Write-Host -ForegroundColor Cyan "Removing: .\dist" + Remove-Item -Force -Recurse ".\dist" +} +if (Test-Path ".\build") { + Write-Host -ForegroundColor Cyan "Removing: .\build" + Remove-Item -Force -Recurse ".\build" +} +if ($c) { + Write-Host -ForegroundColor Yellow "Clean Only. Not Building or Installing!" + exit +} + +python -m build + +if ($args[0] -eq "i") { + Write-Host -ForegroundColor Green "Installing..." + python -m pip install .\dist\vultr_python-0.0.1-py3-none-any.whl +} + +Write-Output "Success." diff --git a/docs.ps1 b/docs.ps1 new file mode 100644 index 0000000..7fed3bc --- /dev/null +++ b/docs.ps1 @@ -0,0 +1,40 @@ +param ( + [switch]$c, + [switch]$b +) + +$ErrorActionPreference = "Stop" + +write-output "Clean: $c" +write-output "Build: $b" + +if ($c) { + Write-Host -ForegroundColor Yellow "Cleaning Docs..." + $site_dir = ".\site" + if (Test-Path $site_dir) { + Write-Host -ForegroundColor Cyan "Removing: $site_dir" + Remove-Item -Force -Recurse $site_dir + } + $cache_dir = ".\.cache" + if (Test-Path $cache_dir) { + Write-Host -ForegroundColor Cyan "Removing: $cache_dir" + Remove-Item -Force -Recurse $cache_dir + } +} + +if ($b) { + Write-Host -ForegroundColor Yellow "Building Docs..." + python -m pdoc -t .\docs\ -o site ` + --logo "https://raw.githubusercontent.com/cssnr/vultr-python/refs/heads/master/.github/assets/logo.svg" ` + --logo-link "https://github.com/cssnr/vultr-python" ` + vultr +} else { + Write-Host -ForegroundColor Green "Serving Docs..." + python -m pdoc -t .\docs\ -p 8000 -h 0.0.0.0 ` + --logo "https://raw.githubusercontent.com/cssnr/vultr-python/refs/heads/master/.github/assets/logo.svg" ` + --logo-link "https://github.com/cssnr/vultr-python" ` + vultr +} + +#--favicon "https://df.cssnr.com/raw/logo128.png" ` +#-e "vultr=https://github.com/cssnr/vultr-python/blob/updates/src/vultr/" ` diff --git a/docs/index.md b/docs/index.md index 2cab29c..5c9ed91 100644 --- a/docs/index.md +++ b/docs/index.md @@ -39,53 +39,83 @@ python -m pip install vultr-python ## Usage -You will need to create an api key and whitelist your IP address. Most functions do not work without an API Key. +You will need to create an api key and whitelist your IP address for most functions. - [https://my.vultr.com/settings/#settingsapi](https://my.vultr.com/settings/#settingsapi) -Initialize the class with your API Key or with the `VULTR_API_KEY` environment variable. +Initialize the `Vultr` class with your API Key or use the `VULTR_API_KEY` environment variable. ```python from vultr import Vultr -vultr = Vultr('VULTR_API_KEY') +vultr = Vultr("VULTR_API_KEY") ``` List plans and get available regions for that plan ```python -plans = vultr.list_plans() -plan = plans[0] # 0 seems to be the basic 5 dollar plan +plans = vultr.list_plans({"type": "vc2"}) # Filter by type +plan = plans[0] # 0 seems to be the base plan regions = vultr.list_regions() -available = vultr.filter_regions(regions, plan['locations']) +available = vultr.filter_regions(regions, plan["locations"]) ``` Get the OS list and filter by name ```python os_list = vultr.list_os() -ubuntu_lts = vultr.filter_os(os_list, 'Ubuntu 24.04 LTS x64') +ubuntu_lts = vultr.filter_os(os_list, "Ubuntu 24.04 LTS x64") ``` Create a new ssh key from key string ```python -sshkey = vultr.create_key('key-name', 'ssh-rsa AAAA...') +sshkey = vultr.create_key("key-name", "ssh-rsa AAAA...") +vultr.delete_key(sshkey['id']) ``` Create a new instance ```python -hostname = 'my-new-host' data = { - 'region': available[0]['id'], - 'plan': plan['id'], - 'os_id': ubuntu_lts['id'], - 'sshkey_id': [sshkey['id']], - 'hostname': hostname, - 'label': hostname, + "os_id": ubuntu_lts["id"], + "sshkey_id": [sshkey["id"]], + "hostname": "my-new-host", + "label": "my-new-host", } -instance = vultr.create_instance(**data) +instance = vultr.create_instance(available[0], plan, **data) +``` + +Arbitrary Methods [get](#Vultr.get), [post](#Vultr.post), [patch](#Vultr.patch), [put](#Vultr.put), [delete](#Vultr.delete) + +```python +plans = vultr.get("/plans", {"type": "vc2"}) +sshkey = vultr.post("/ssh-keys", name="key-name", ssh_key="ssh-rsa AAAA...") +instance = vultr.patch("/instances/{instance-id}", plan=plans[1]["id"]) +database = vultr.put("/databases/{database-id}", tag="new tag") +vultr.delete("/snapshots/{snapshot-id}") +``` + +Error Handling + +```python +>>> instance = vultr.create_instance("atl", "vc2-1c-0.5gb-v6", os_id=2284) +Traceback (most recent call last): +vultr.vultr.VultrException: Error 400: Server add failed: Ubuntu 24.04 LTS x64 requires a plan with at least 1000 MB memory. +``` + +Using the `VultrException` class + +```python +from vultr import VultrException + +try: + instance = vultr.create_instance("atl", "vc2-1c-0.5gb-v6", os_id=2284) +except VultrException as error: + print(error.error) + # 'Server add failed: Ubuntu 24.04 LTS x64 requires a plan with at least 1000 MB memory.' + print(error.status) + # 400 ```   @@ -93,3 +123,5 @@ instance = vultr.create_instance(**data) Vultr API Reference: [https://www.vultr.com/api](https://www.vultr.com/api/?ref=6905748) --- + +

API Documentation

diff --git a/docs/module.html.jinja2 b/docs/module.html.jinja2 index 229f2a8..0948e60 100644 --- a/docs/module.html.jinja2 +++ b/docs/module.html.jinja2 @@ -1,2 +1,44 @@ {% extends "default/module.html.jinja2" %} {% block title %}Vultr Python{% endblock %} +{% block head %} + + + + + + + + + + + + + + + + + + + + + + + + + +{% endblock %} + +{#{% block nav_footer %}#} +{# #} +{#{% endblock %}#} + +{#{% block attribution %}{% endblock %}#} diff --git a/pyproject.toml b/pyproject.toml index 47d3620..6f3b1a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "Python 3 wrapper for the Vultr API v2.0" authors = [{ name="Shane" }] readme = "README.md" dynamic = ["version"] -requires-python = ">=3.6" +requires-python = ">=3.7" classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", @@ -36,6 +36,7 @@ dev = [ "coverage", "isort", "mypy", + "pytest", "requests", "ruff", "setuptools", diff --git a/src/vultr/__init__.py b/src/vultr/__init__.py index 6cc4cfb..ab69aad 100644 --- a/src/vultr/__init__.py +++ b/src/vultr/__init__.py @@ -2,7 +2,7 @@ .. include:: ../../docs/index.md """ -from .vultr import Vultr +from .vultr import Vultr, VultrException -__all__ = ["Vultr"] +__all__ = ["Vultr", "VultrException"] diff --git a/src/vultr/py.typed b/src/vultr/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/vultr/vultr.py b/src/vultr/vultr.py index 96f0b50..183035b 100644 --- a/src/vultr/vultr.py +++ b/src/vultr/vultr.py @@ -1,5 +1,6 @@ import os -from typing import Optional, Union +import warnings +from typing import Any, List, Optional, Union import requests @@ -8,118 +9,183 @@ class Vultr(object): url = "https://api.vultr.com/v2" def __init__(self, api_key: Optional[str] = None): - """ - :param str api_key: Vultr API Key or VULTR_API_KEY environment variable - """ + """:param api_key: Vultr API Key or `VULTR_API_KEY` Environment Variable""" self.api_key = api_key or os.getenv("VULTR_API_KEY") - self.s = requests.session() + """Provide the API key here or with the `VULTR_API_KEY` environment variable""" + self._session = requests.session() if self.api_key: - self.s.headers.update({"Authorization": f"Bearer {self.api_key}"}) + self._session.headers.update({"Authorization": f"Bearer {self.api_key}"}) + + def get(self, url: str, params: Optional[dict] = None) -> Any: + """ + GET Data + :param url: Request URL. Example `/instances` + :param params: Query Parameters Dictionary + :return: Response Data + :raises: `VultrException` + """ + return self._req("get", f"{self.url}/{url.lstrip('/')}", params=params) + + def post(self, url: str, **kwargs) -> Any: + """ + POST Data + :param url: Request URL. Example `/instances` + :param kwargs: Request Data Keyword Arguments + :return: Response Data + :raises: `VultrException` + """ + return self._req("post", f"{self.url}/{url.lstrip('/')}", kwargs) + + def patch(self, url: str, **kwargs) -> Any: + """ + PATCH Data + :param url: Request URL. Example `/instances/{instance-id}` + :param kwargs: Request Data Keyword Arguments + :return: Response Data + :raises: `VultrException` + """ + return self._req("patch", f"{self.url}/{url.lstrip('/')}", kwargs) + + def put(self, url: str, **kwargs) -> Any: + """ + PUT Data + :param url: Request URL. Example `/instances/{instance-id}` + :param kwargs: Request Data Keyword Arguments + :return: Response Data + :raises: `VultrException` + """ + return self._req("put", f"{self.url}/{url.lstrip('/')}", kwargs) + + def delete(self, url: str) -> None: + """ + DELETE a Resource + :param url: Request URL. Example `/instances/{instance-id}` + :return: None + :raises: `VultrException` + """ + return self._req("delete", f"{self.url}/{url.lstrip('/')}") - def list_os(self): + def list_os(self, params: Optional[dict] = None) -> list: url = f"{self.url}/os" - return self._get(url)["os"] + return self._req("get", url, params=params)["os"] - def list_plans(self): + def list_plans(self, params: Optional[dict] = None) -> list: url = f"{self.url}/plans" - return self._get(url)["plans"] + return self._req("get", url, params=params)["plans"] - def list_regions(self): + def list_regions(self, params: Optional[dict] = None) -> list: url = f"{self.url}/regions" - return self._get(url)["regions"] + return self._req("get", url, params=params)["regions"] - def list_instances(self): + def list_instances(self, params: Optional[dict] = None) -> list: url = f"{self.url}/instances" - return self._get(url)["instances"] + return self._req("get", url, params=params)["instances"] - def get_instance(self, instance: Union[str, dict]): + def get_instance(self, instance: Union[str, dict], params: Optional[dict] = None) -> dict: instance_id = self._get_obj_key(instance) url = f"{self.url}/instances/{instance_id}" - return self._get(url)["instance"] + return self._req("get", url, params=params)["instance"] - def create_instance(self, region: str, plan: str, **kwargs): - data = {"region": region, "plan": plan} + def create_instance(self, region: Union[str, dict], plan: Union[str, dict], **kwargs) -> dict: + data = {"region": self._get_obj_key(region), "plan": self._get_obj_key(plan)} data.update(kwargs) url = f"{self.url}/instances" - return self._post(url, data)["instance"] + return self._req("post", url, data)["instance"] - def update_instance(self, instance: Union[str, dict], **kwargs): + def update_instance(self, instance: Union[str, dict], **kwargs) -> dict: instance_id = self._get_obj_key(instance) url = f"{self.url}/instances/{instance_id}" - return self._patch(url, kwargs)["instance"] + return self._req("patch", url, kwargs)["instance"] - def delete_instance(self, instance: Union[str, dict]): + def delete_instance(self, instance: Union[str, dict]) -> None: instance_id = self._get_obj_key(instance) url = f"{self.url}/instances/{instance_id}" - return self._delete(url) + return self._req("delete", url) - def list_keys(self): + def list_keys(self, params: Optional[dict] = None) -> list: url = f"{self.url}/ssh-keys" - return self._get(url)["ssh_keys"] + return self._req("get", url, params=params)["ssh_keys"] - def get_key(self, key: Union[str, dict]): + def get_key(self, key: Union[str, dict], params: Optional[dict] = None) -> dict: key_id = self._get_obj_key(key) url = f"{self.url}/ssh-keys/{key_id}" - return self._get(url)["ssh_key"] + return self._req("get", url, params=params)["ssh_key"] - def create_key(self, name: str, key: str, **kwargs): + def create_key(self, name: str, key: str, **kwargs) -> dict: data = {"name": name, "ssh_key": key} data.update(kwargs) url = f"{self.url}/ssh-keys" - return self._post(url, data)["ssh_key"] + return self._req("post", url, data)["ssh_key"] - def update_key(self, key: Union[str, dict], **kwargs): + def update_key(self, key: Union[str, dict], **kwargs) -> None: key_id = self._get_obj_key(key) url = f"{self.url}/ssh-keys/{key_id}" - return self._patch(url, kwargs)["ssh_key"] + return self._req("patch", url, kwargs)["ssh_key"] - def delete_key(self, key: Union[str, dict]): + def delete_key(self, key: Union[str, dict]) -> None: key_id = self._get_obj_key(key) url = f"{self.url}/ssh-keys/{key_id}" - return self._delete(url) + return self._req("delete", url) - def list_scripts(self): + def list_scripts(self, params: Optional[dict] = None) -> list: url = f"{self.url}/startup-scripts" - return self._get(url)["startup_scripts"] + return self._req("get", url, params=params)["startup_scripts"] - def get_script(self, script: Union[str, dict]): + def get_script(self, script: Union[str, dict], params: Optional[dict] = None) -> dict: script_id = self._get_obj_key(script) url = f"{self.url}/startup-scripts/{script_id}" - return self._get(url)["startup_script"] + return self._req("get", url, params=params)["startup_script"] - def create_script(self, name: str, script: str, **kwargs): + def create_script(self, name: str, script: str, **kwargs) -> dict: data = {"name": name, "script": script} data.update(kwargs) url = f"{self.url}/startup-scripts" - return self._post(url, data)["startup_script"] + return self._req("post", url, data)["startup_script"] - def update_script(self, script: Union[str, dict], **kwargs): + def update_script(self, script: Union[str, dict], **kwargs) -> None: script_id = self._get_obj_key(script) url = f"{self.url}/startup-scripts/{script_id}" - return self._patch(url, kwargs)["startup_script"] + return self._req("patch", url, kwargs)["startup_script"] - def delete_script(self, script: Union[str, dict]): + def delete_script(self, script: Union[str, dict]) -> None: script_id = self._get_obj_key(script) url = f"{self.url}/startup-scripts/{script_id}" - return self._delete(url) + return self._req("delete", url) - def list_ipv4(self, instance: Union[str, dict]): + def list_ipv4(self, instance: Union[str, dict], params: Optional[dict] = None) -> list: instance_id = self._get_obj_key(instance) url = f"{self.url}/instances/{instance_id}/ipv4" - return self._get(url)["ipv4s"] + return self._req("get", url, params=params)["ipv4s"] - def create_ipv4(self, instance: Union[str, dict], **kwargs): + def create_ipv4(self, instance: Union[str, dict], **kwargs) -> dict: instance_id = self._get_obj_key(instance) url = f"{self.url}/instances/{instance_id}/ipv4" - return self._post(url, kwargs)["ipv4"] + return self._req("post", url, kwargs)["ipv4"] - def delete_ipv4(self, instance: Union[str, dict]): + def delete_ipv4(self, instance: Union[str, dict]) -> None: instance_id = self._get_obj_key(instance) url = f"{self.url}/instances/{instance_id}/ipv4" - return self._delete(url) + return self._req("delete", url) + + @staticmethod + def filter_list(item_list: List[dict], value: str, key: str = "name") -> dict: + """ + Helper Function to get an Item from a List of Dictionaries + :param item_list: List to filter + :param value: Value of the Key + :param key: Key to check for Value + :return: Item or {} + """ + return next((d for d in item_list if str(d.get(key, "")).lower() == value.lower()), {}) + + @staticmethod + def filter_regions(regions: list, locations: list) -> list: + return [d for d in regions if d["id"] in locations] @staticmethod def filter_keys(keys: list, name: str) -> dict: + """Soft Deprecated in 0.2.0. Use `Vultr.filter_list()`""" + warnings.warn("Soft Deprecated in 0.2.0. Use filter_list()", PendingDeprecationWarning, stacklevel=2) try: return next(d for d in keys if d["name"].lower() == name.lower()) except StopIteration: @@ -127,6 +193,8 @@ def filter_keys(keys: list, name: str) -> dict: @staticmethod def filter_os(os_list: list, name: str) -> dict: + """Soft Deprecated in 0.2.0. Use `Vultr.filter_list()`""" + warnings.warn("Soft Deprecated in 0.2.0. Use filter_list()", PendingDeprecationWarning, stacklevel=2) try: return next(d for d in os_list if d["name"].lower() == name.lower()) except StopIteration: @@ -134,38 +202,22 @@ def filter_os(os_list: list, name: str) -> dict: @staticmethod def filter_scripts(scripts: list, name: str) -> dict: + """Soft Deprecated in 0.2.0. Use `Vultr.filter_list()`""" + warnings.warn("Soft Deprecated in 0.2.0. Use filter_list()", PendingDeprecationWarning, stacklevel=2) try: return next(d for d in scripts if d["name"].lower() == name.lower()) except StopIteration: return {} - @staticmethod - def filter_regions(regions: list, locations: list) -> list: - return [d for d in regions if d["id"] in locations] - - def _get(self, url): - r = self.s.get(url, timeout=10) + def _req(self, method, url, data: Any = None, params: Optional[dict] = None) -> Any: + r = self._session.request(method, url, params=params, json=data, timeout=10) if not r.ok: - r.raise_for_status() - return r.json() - - def _post(self, url, data): - r = self.s.post(url, json=data, timeout=10) - if not r.ok: - r.raise_for_status() - return r.json() - - def _patch(self, url, data): - r = self.s.patch(url, json=data, timeout=10) - if not r.ok: - r.raise_for_status() - return r.json() - - def _delete(self, url): - r = self.s.delete(url, timeout=10) - if not r.ok: - r.raise_for_status() - return None + raise VultrException(r) + if r.status_code == 204: + return None + if r.headers.get("content-type") == "application/json": + return r.json() + return r.text @staticmethod def _get_obj_key(obj, key="id"): @@ -178,3 +230,19 @@ def _get_obj_key(obj, key="id"): return obj[key] else: raise ValueError(f"Unable to parse object: {key}") + + +class VultrException(Exception): + """Exception class for all Vultr error responses.""" + + def __init__(self, response: requests.Response): + self.status: int = response.status_code + """Response Status Code""" + try: + data = response.json() + error = data.get("error", response.text) + except requests.JSONDecodeError: + error = response.text + self.error: str = str(error) + """Error Message for 400 Codes""" + super().__init__(f"Error {self.status}: {self.error}") diff --git a/tests/test_all.py b/tests/test_all.py new file mode 100644 index 0000000..b10f871 --- /dev/null +++ b/tests/test_all.py @@ -0,0 +1,24 @@ +from vultr import Vultr + + +vultr = Vultr() + + +def test_basic(): + per_page = 100 + plans = vultr.get("plans", {"type": "vc2", "per_page": per_page}) + print(f"plans: {len(plans['plans'])}") + assert len(plans["plans"]) == min(plans["meta"]["total"], per_page) + + plan_id = "vc2-1c-1gb" + plan = vultr.filter_list(plans["plans"], plan_id, "id") + print(f"plan: {plan}") + assert plan.get("id") == plan_id + + # regions = vultr.list_regions({"per_page": per_page}) + # print(f"regions: {len(regions)}") + # assert 0 < len(regions) <= per_page + + # available = vultr.filter_regions(regions, plans["plans"][0]["locations"]) + # print(f"available: {len(available)}") + # assert 0 < len(available) <= per_page