Metadata-Version: 2.4
Name: convox
Version: 0.1.0
Summary: Python SDK for the Convox API
Project-URL: Homepage, https://convox.com
Project-URL: Repository, https://github.com/convox/convox-python
Project-URL: Documentation, https://github.com/convox/convox-python#readme
Author-email: Convox <support@convox.com>
License-Expression: Apache-2.0
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Requires-Dist: eval-type-backport>=0.2.0; python_version < '3.10'
Requires-Dist: httpx<1,>=0.27
Requires-Dist: pydantic<3,>=2.0
Provides-Extra: dev
Requires-Dist: mypy>=1.0; extra == 'dev'
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# Python SDK for the [Convox](https://convox.com) API.

## Installation

```bash
pip install convox
```

## Quick Start

```python
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)

```python
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`:

```python
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.

```python
# 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

```python
# 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

```python
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

```python
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

```python
# 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

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

### Processes

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

### Resources

```python
# 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

```python
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:

```python
# 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:

```python
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:

```python
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:

```python
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:

```python
# 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:

```python
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.

```python
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

```bash
# 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
