Skip to content

Latest commit

 

History

History
318 lines (240 loc) · 8.14 KB

File metadata and controls

318 lines (240 loc) · 8.14 KB

Python SDK for the Convox API.

Installation

pip install convox

Quick Start

from convox import ConvoxClient

client = ConvoxClient(
    host="console.convox.com",
    api_key="your-deploy-key",
    rack="org/rack-name",          # required for console connections
)

# List apps
for app in client.apps.list():
    print(f"{app.name}: {app.status}")

# Get app details
app = client.apps.get("myapp")
print(f"Release: {app.release}, Locked: {app.locked}")

Authentication

For console-managed racks the api_key is a deploy key generated in the Convox console (Organization > Deploy Keys). Deploy keys are the recommended auth method for programmatic and SDK usage. The examples below all use deploy keys.

Three options, in order of precedence:

1. Explicit credentials (recommended)

client = ConvoxClient(
    host="console.convox.com",
    api_key="your-deploy-key",
    rack="org/rack-name",        # required for console-managed racks
)

2. Environment variables

Set CONVOX_HOST, CONVOX_PASSWORD (deploy key), and CONVOX_RACK:

client = ConvoxClient.from_env()

3. CLI config file

Reads the auth file written by convox login (~/.config/convox/auth for v3, ~/.convox/auth for v2). This is a convenience for local development when you have already authenticated via the CLI -- it reads whatever credential is stored for the given host, whether that is a CLI session token or a deploy key you placed there manually.

# Console-managed rack — specify both host and rack
client = ConvoxClient.from_cli_config(
    host="console.convox.com",       # auth file lookup key
    rack="org/rack-name",            # rack to target
)

# Direct rack connection (host and rack are the same)
client = ConvoxClient.from_cli_config(rack="rack.example.com")

API Reference

Apps

# List, get, create, delete
apps = client.apps.list()
app = client.apps.get("myapp")
app = client.apps.create("myapp")
client.apps.delete("myapp")

# Update (lock/unlock, set parameters)
client.apps.update("myapp", lock=True)
client.apps.update("myapp", parameters={"BuildCpu": "512"})

# Cancel in-progress operation
client.apps.cancel("myapp")

Builds

builds = client.builds.list("myapp")
build = client.builds.get("myapp", "BABCDEF")
build = client.builds.create("myapp", "https://example.com/source.tgz",
                              description="v2.0 release")

Releases

releases = client.releases.list("myapp")
release = client.releases.get("myapp", "RABCDEF")

# Promote a release to production
client.releases.promote("myapp", "RABCDEF")

# Rollback to a previous release
client.releases.rollback("myapp", "RPREVIOUS")

Environment Variables

# Get current env
env = client.environment.get("myapp")
print(env)  # {"DATABASE_URL": "postgres://...", "SECRET_KEY": "..."}

# Set env vars (creates a new release)
release = client.environment.set("myapp", {"KEY": "value", "OTHER": "val"})

# Unset env vars (creates a new release)
release = client.environment.unset("myapp", "KEY")

Services

services = client.services.list("myapp")
client.services.update("myapp", "web", count=3, memory=1024)
client.services.restart("myapp", "web")

Processes

processes = client.processes.list("myapp")
process = client.processes.get("myapp", "proc-abc123")
client.processes.stop("myapp", "proc-abc123")

Resources

# App-scoped resources
resources = client.resources.list("myapp")
resource = client.resources.get("myapp", "database")

# System-scoped resources
all_resources = client.resources.system_list()
client.resources.system_create("postgres", name="mydb")
client.resources.system_link("mydb", "myapp")

System

system = client.system.get()
print(f"{system.name} v{system.version} ({system.provider})")

capacity = client.system.capacity()

Low-Level API

Escape hatch for endpoints not covered by resource methods:

# Equivalent to `convox api get /apps`
data = client.api.get("/apps")

# POST with form data
data = client.api.post("/apps", data={"name": "newapp"})

Error Handling

All API errors raise typed exceptions:

from convox import (
    ConvoxAPIError,        # Base for all API errors
    ConvoxAuthError,       # 401 - bad credentials
    ConvoxForbiddenError,  # 401 - insufficient permissions
    ConvoxNotFoundError,   # 404 - resource not found
    ConvoxValidationError, # 400 - invalid input
    ConvoxConflictError,   # 409 - already exists / locked
    ConvoxServerError,     # 500 - server error
    ConvoxConnectionError, # Network failure
    ConvoxTimeoutError,    # Request timeout
)

try:
    app = client.apps.get("myapp")
except ConvoxNotFoundError:
    print("App does not exist")
except ConvoxConflictError:
    print("App is locked")
except ConvoxAPIError as e:
    print(f"API error {e.status_code}: {e.message}")
    print(f"Request ID: {e.request_id}")  # for support tickets

Retry Configuration

The SDK retries on 429 (rate limit) and 502/503/504 (gateway errors) with exponential backoff:

from convox import ConvoxClient, RetryConfig

client = ConvoxClient(
    host="rack.example.com",
    api_key="key",
    retry=RetryConfig(
        max_retries=5,           # default: 3
        backoff_base=2.0,        # default: 1.0
        backoff_max=60.0,        # default: 30.0
        retry_on_500=True,       # default: False (500s may be permanent)
    ),
)

Disable retries:

client = ConvoxClient(host="...", api_key="...", retry=RetryConfig(max_retries=0))

Timeouts

The default timeout is 30 seconds. Most API operations complete well within this limit, since build creation (builds.create) returns immediately after dispatching the build to a Kubernetes pod.

The one exception is builds.import_build(), which is synchronous — the server reads the entire gzip archive and pushes every service image to the container registry before responding. This method defaults to a 1800-second (30-minute) per-request timeout, independent of the client-wide default:

# Uses the 30-minute default automatically
build = client.builds.import_build("myapp", tarball_bytes)

# Override with a custom timeout if needed
build = client.builds.import_build("myapp", tarball_bytes, timeout=3600)

You can also pass a per-request timeout to the low-level API:

data = client.api.post("/apps/myapp/builds/import", content=tarball, timeout=600)

Streaming

The stream() method returns the raw httpx.Response without checking for error status codes. This is intentional: streaming endpoints (logs, build output) may send partial data before an error occurs.

response = client.stream("GET", "/apps/myapp/logs", params={"follow": "true"})
try:
    if response.status_code >= 400:
        print(f"Error: {response.status_code}")
    else:
        for line in response.iter_lines():
            print(line)
finally:
    response.close()

CLI Comparison

CLI Command SDK Equivalent
convox apps client.apps.list()
convox apps info -a myapp client.apps.get("myapp")
convox apps create myapp client.apps.create("myapp")
convox apps delete myapp client.apps.delete("myapp")
convox builds -a myapp client.builds.list("myapp")
convox releases -a myapp client.releases.list("myapp")
convox releases promote RABCDEF -a myapp client.releases.promote("myapp", "RABCDEF")
convox env -a myapp client.environment.get("myapp")
convox env set KEY=val -a myapp client.environment.set("myapp", {"KEY": "val"})
convox env unset KEY -a myapp client.environment.unset("myapp", "KEY")
convox api get /apps client.api.get("/apps")

Contributing

# Install dev dependencies
pip install -e ".[dev]"

# Run unit tests (mocked, no rack needed)
pytest tests/

# Run integration tests (requires a live rack)
CONVOX_HOST=console.convox.com \
CONVOX_PASSWORD=your-deploy-key \
CONVOX_RACK=org/rack-name \
pytest integration_tests/ -v

# Lint and format
ruff check .
ruff format .

# Type check
mypy src/

License

Apache 2.0