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 @@
[](https://app.codacy.com/gh/cssnr/vultr-python/dashboard)
[](https://sonarcloud.io/summary/new_code?id=cssnr_vultr-python)
[](https://github.com/cssnr/vultr-python/actions/workflows/lint.yaml)
-[](https://cssnr.github.io/vultr-python)
+[](https://github.com/cssnr/vultr-python/actions/workflows/test.yaml)
+[](https://pypi.org/project/vultr-python/)
+[](https://cssnr.github.io/vultr-python/)
[](https://github.com/cssnr/vultr-python/graphs/commit-activity)
[](https://github.com/cssnr/vultr-python)
[](https://github.com/cssnr/vultr-python?tab=readme-ov-file#readme)
@@ -19,8 +21,8 @@
# Vultr Python
-
-
+
+
- [Install](#Install)
- [Usage](#Usage)
@@ -31,13 +33,12 @@ Python 3 wrapper for the Vultr API v2.
[](https://github.com/cssnr/vultr-python?tab=readme-ov-file#readme)
[](https://pypi.org/project/vultr-python)
-[](https://cssnr.github.io/vultr-python)
+[](https://cssnr.github.io/vultr-python/)
[](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