Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
# Set update schedule for GitHub Actions
# Set update schedule for GitHub Actions and Python dependencies
---
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

- package-ecosystem: "uv"
directory: "/"
schedule:
interval: "weekly"
12 changes: 0 additions & 12 deletions .github/workflows/flake8.yml

This file was deleted.

25 changes: 10 additions & 15 deletions .github/workflows/pythonpublish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,14 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -U setuptools wheel twine
- name: Build and publish
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d

- name: Build
run: uv build

- name: Publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*
UV_PUBLISH_USERNAME: ${{ secrets.PYPI_USERNAME }}
UV_PUBLISH_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: uv publish
46 changes: 25 additions & 21 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,41 +1,45 @@
name: Python package
name: Test

on: [push]
on: [push, pull_request]

jobs:
build:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d
with:
enable-cache: true
- run: uv run ruff check .
- run: uv run ruff format --check .

test:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
fail-fast: false
matrix:
python-version:
- "3.6"
- "3.7"
- "3.8"
- "3.9"
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d
with:
enable-cache: true
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Test with tox
run: tox

- name: GitHub Action for Flake8
uses: cclauss/GitHub-Action-for-Flake8@v0.5.0
- name: Run tests
run: uv run --frozen pytest --cov=cloudscale --cov-report=xml

- name: Upload coverage to Codecov
if: matrix.python-version == '3.13'
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
files: ./coverage.xml
flags: unittests
name: codecov-umbrella
yml: ./codecov.yml
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ dist
__pycache__/
*.pyc
.coverage
.pytest_cache
.ruff_cache
.tox
.venv
coverage.xml
Expand Down
5 changes: 0 additions & 5 deletions MANIFEST.in

This file was deleted.

37 changes: 25 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
clean:
rm -rf *.egg-info
rm -rf *.dist-info
rm -rf dist
rm -rf build
.PHONY: install lint format test coverage build clean release test-release

install:
uv sync

lint:
uv run ruff check .
uv run ruff format --check .

format:
uv run ruff check --fix .
uv run ruff format .

test:
uv run pytest

coverage:
uv run pytest --cov=cloudscale

build: clean
python3 setup.py sdist bdist_wheel
uv build

test-release:
twine upload --repository testpypi dist/*
clean:
rm -rf dist build *.egg-info

release:
twine upload dist/*
test-release: build
uv publish --publish-url https://test.pypi.org/legacy/

docs:
mkdocs gh-deploy
release: build
uv publish
64 changes: 60 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,74 @@

# Python SDK for cloudscale.ch

A Python3 library for [cloudscale.ch](https://www.cloudscale.ch).
A Python library for [cloudscale.ch](https://www.cloudscale.ch), requires Python 3.10 or newer.

## Install / Update

~~~shell
pip3 install cloudscale-sdk
pip install -U cloudscale-sdk
~~~

## Usage

~~~python
from cloudscale import Cloudscale

with Cloudscale(api_token="...") as cloudscale:
server = cloudscale.server.create(
name="db-master", flavor="flex-4", image="debian-12"
)
cloudscale.server.stop(uuid=server["uuid"])
for volume in cloudscale.volume.get_all(filter_tag="project=apollo"):
print(volume["name"])
~~~

Resources available as attributes: `custom_image`, `flavor`, `floating_ip`, `image`,
`network`, `objects_user`, `region`, `server`, `server_group`, `subnet`, `volume`.
Depending on the resource they offer `get_all()`, `get_by_uuid()`, `create()`,
`update()` and `delete()`.

Errors are raised as `CloudscaleException` (configuration problems) and
`CloudscaleApiException` (API errors, carrying `status_code` and `response`).

### Authentication

The API token is taken from the `api_token` argument, or from a `cloudscale.ini`
looked up in this order, later files winning:

1. `$XDG_CONFIG_HOME/cloudscale/cloudscale.ini` (`~/.config/...` by default)
2. `~/.cloudscale.ini`
3. `./cloudscale.ini`

~~~ini
[default]
api_token = ...

[production]
api_token = ...
timeout = 30
~~~

Select a section with `Cloudscale(profile="production")` or `$CLOUDSCALE_PROFILE`.

### Environment variables

| Variable | Description |
| --- | --- |
| `CLOUDSCALE_API_URL` | API endpoint, defaults to `https://api.cloudscale.ch/v1` |
| `CLOUDSCALE_CONFIG` | Config file name, defaults to `cloudscale.ini` |
| `CLOUDSCALE_PROFILE` | Config section to use, defaults to `default` |
| `CLOUDSCALE_LOG_LEVEL` | Log level, defaults to `ERROR` |

## Development

### Run tests with coverage
This project uses [uv](https://docs.astral.sh/uv/) and
[ruff](https://docs.astral.sh/ruff/).

~~~shell
tox -e coverage
make install # create the virtualenv
make test # run the tests
make coverage # run the tests with a coverage report
make lint # lint and format check
make format # apply lint fixes and formatting
~~~
127 changes: 12 additions & 115 deletions cloudscale/__init__.py
Original file line number Diff line number Diff line change
@@ -1,118 +1,15 @@
import configparser
import logging
import os
"""A Python SDK for the cloudscale.ch API."""

from xdg import XDG_CONFIG_HOME

from .error import CloudscaleApiException, CloudscaleException # noqa F401
from .lib.custom_image import CustomImage
from .lib.flavor import Flavor
from .lib.floating_ip import FloatingIp
from .lib.image import Image
from .lib.network import Network
from .lib.objects_user import ObjectsUser
from .lib.region import Region
from .lib.server import Server
from .lib.server_group import ServerGroup
from .lib.subnet import Subnet
from .lib.volume import Volume
from .log import logger
from .client import CLOUDSCALE_API_URL, Cloudscale
from .config import CLOUDSCALE_CONFIG
from .error import CloudscaleApiException, CloudscaleException
from .version import __version__

CLOUDSCALE_API_URL = os.getenv("CLOUDSCALE_API_URL", "https://api.cloudscale.ch/v1")
CLOUDSCALE_CONFIG = "cloudscale.ini"


class Cloudscale:
api_url = CLOUDSCALE_API_URL
version = __version__

def __init__(self, api_token: str = "", profile: str = "", debug: bool = False):
"""Cloudscale

Args:
api_token (str, optional): API token. Defaults to None.
profile (str, optional): Section to use in cloudscale.ini. Defaults to None.
debug (bool, optional): Enable debug logging. Defaults to False.

Raises:
CloudscaleException: Exception related to API token handling.
"""
if debug:
logger.setLevel(logging.INFO)

logger.info(f"Started, version: {self.version}")

if api_token and profile:
raise CloudscaleException("API token and profile are mutually exclusive")

# Read ini configs
self.config = self._read_from_configfile(profile=profile)

if api_token:
self.api_token = api_token
else:
self.api_token = self.config.get("api_token")

if not self.api_token:
raise CloudscaleException("Missing API key")

logger.info(f"API Token used: {self.api_token[:4]}...")

# Configure requests timeout
self.timeout: int = int(self.config.get("timeout", 60))
logger.debug(f"Timeout is: {self.timeout}")

# Resource attributes
self.custom_image = CustomImage(self)
self.flavor = Flavor(self)
self.floating_ip = FloatingIp(self)
self.image = Image(self)
self.network = Network(self)
self.objects_user = ObjectsUser(self)
self.region = Region(self)
self.server = Server(self)
self.server_group = ServerGroup(self)
self.subnet = Subnet(self)
self.volume = Volume(self)

def _read_from_configfile(self, profile: str = "") -> dict:
"""Reads from config ini file.

Args:
profile (str, optional): Section to read. Defaults to "".

Raises:
CloudscaleException: Profile not found.

Returns:
dict: Read configs.
"""

config_file = os.getenv("CLOUDSCALE_CONFIG", CLOUDSCALE_CONFIG)

paths = (
os.path.join(XDG_CONFIG_HOME, "cloudscale", config_file),
os.path.join(os.path.expanduser("~"), ".{}".format(config_file)),
os.path.join(os.getcwd(), config_file),
)

conf = configparser.ConfigParser()
conf.read(paths)

if profile:
if not conf.has_section(profile):
raise CloudscaleException(
"Profile '{}' not found in config files: ({})".format(
profile, ", ".join(paths)
)
)
else:
profile = os.getenv("CLOUDSCALE_PROFILE", "default")

logger.info(f"Using profile {profile}")

if not conf.has_section(profile):
return dict()

return dict(conf[profile])
__all__ = [
"CLOUDSCALE_API_URL",
"CLOUDSCALE_CONFIG",
"Cloudscale",
"CloudscaleApiException",
"CloudscaleException",
"__version__",
]
Loading
Loading