Metadata-Version: 2.4
Name: eqvps
Version: 0.2.0
Summary: EQVPS code sandboxes: isolated Firecracker microVMs for AI agents, billed per second from your prepaid balance
License: MIT
Project-URL: Homepage, https://eqvps.com
Project-URL: Documentation, https://eqvps.com/en/docs/sandboxes
Project-URL: API, https://eqvps.com/openapi.json
Keywords: sandbox,firecracker,microvm,ai-agents,code-execution,eqvps
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# eqvps — code sandboxes for AI agents (Python)

Isolated **Firecracker microVMs** in ~1 second: Linux with Python 3.12 + pip, Node.js 22 + npm, bash, git and curl (3–40 GB disk by tariff), internet egress (pip/npm install and git clone work), no inbound ports.
Billed per second from your prepaid [EQVPS](https://eqvps.com) balance — the same balance as VPS, no subscription. Zero dependencies, Python 3.8+.

```bash
pip install eqvps
```

Docs: [Sandboxes](https://eqvps.com/en/docs/sandboxes) · [Connecting your account (token, config, balance)](https://eqvps.com/en/docs/connect) · [OpenAPI](https://eqvps.com/openapi.json)

```python
from eqvps import Sandbox

with Sandbox.create(tariff="small") as sb:          # EQVPS_API_KEY from the environment
    r = sb.run("print(2 + 2)")
    print(r.stdout, r.exit_code)                     # "4\n" 0
    sb.upload("/root/data.csv", "a,b\n1,2\n")
    print(sb.exec("wc -l /root/data.csv").stdout)
    print(sb.download_text("/root/data.csv"))
# deleted when the block exits — also on exceptions
```

## API key
Any EQVPS account Bearer token (`POST https://api.eqvps.com/api/v1/eqvps/auth/register`, the MCP `register_account` tool, or your dashboard). Pass `api_key=` or set `EQVPS_API_KEY`. Fund the balance with crypto (`topup_balance`).

## Modes and prices
| | ephemeral (default) | persistent |
|---|---|---|
| billing | per second, min 60 s | per started hour; a month costs at most 730 h −25 % |
| lifetime | idle timeout (default 300 s, max 3600), `ttl` ≤ 24 h | `ttl` ≤ 30 days, survives host maintenance |
| empty balance | can't create (402) | paused (memory+disk snapshot, not billed), resumes after top-up; snapshot kept 14 days |

| tariff | vCPU | RAM | disk | $/hour | persistent month max |
|---|---|---|---|---|---|
| `micro` | 0.25 | 512 MB | 3 GB | $0.0165 | $9.03 |
| `small` | 0.5 | 1 GB | 5 GB | $0.033 | $18.07 |
| `standard` | 1 | 2 GB | 10 GB | $0.066 | $36.14 |
| `plus` | 2 | 4 GB | 15 GB | $0.132 | $72.27 |
| `pro` | 4 | 8 GB | 20 GB | $0.264 | $144.54 |
| `max` | 8 | 16 GB | 40 GB | $0.528 | $289.08 |

Rates: vCPU $0.040/h + RAM $0.013/GiB-h. Live: `tariffs()`. Heavy installs (torch) and builds: `plus` or bigger. If the host is busy, creating a big tariff raises `CapacityError` (503, `retry_after`).

```python
sb = Sandbox.create(mode="persistent", tariff="micro", ttl=7 * 86400)
sid = sb.id                       # keep it
sb = Sandbox.connect(sid)         # reattach later
sb.kill()
```

## API
- `Sandbox.create(mode, tariff, idle_timeout, ttl)` · `Sandbox.connect(id)` · `Sandbox.list()` · `eqvps.tariffs()`
- `sb.run(code, language="python"|"node"|"bash", timeout=30)` → `ExecResult(exit_code, stdout, stderr, timed_out, truncated, duration_ms, ok)`
- `sb.exec("cmd" | ["argv"], cwd=None, stdin=None, timeout=30)` → `ExecResult`
- `sb.upload(path, bytes|str, mode=None)` · `sb.download(path) -> bytes` · `sb.download_text(path)` (≤ 5 MB per file)
- `sb.usage()` → running seconds, CPU used, egress, `billed_usd`, `estimated_total_usd` · `sb.refresh()` · `sb.kill()`

Limits: command timeout ≤ 55 s per call (start long jobs with `nohup … &` and poll), output ≤ 1 MiB per stream, files ≤ 5 MB per transfer, 20 concurrent sandboxes and 2 concurrent commands per account.

## Long jobs — background tasks (no 55 s limit)
```python
task = sb.exec("python3 train.py", background=True)          # returns at once with task.id
result = task.wait(on_output=lambda out, err: print(out, end=""), poll_interval=5)
print(result.state, result.exit_code)                        # done 0  (also: failed | killed | timeout)

out, err = task.logs()      # or poll yourself: only the new output since the last call
task.kill()                 # stop it (and its child processes)
sb.task(task_id)            # reattach later, e.g. from another process
```
A task may run until the sandbox's maximum lifetime (`timeout=` to cap it). While it runs the sandbox is not idle-deleted, and it is billed like any running time. Up to 8 tasks per sandbox; 8 MiB of output per stream is retained (older output is dropped).

## Environment variables
```python
sb = Sandbox.create(env={"OPENAI_API_KEY": "sk-...", "MODE": "prod"})   # for every command in this sandbox
print(sb.env_keys)                                                     # ['OPENAI_API_KEY', 'MODE'] — values are never returned
sb.run("import os; print(os.environ['MODE'])")
sb.exec("echo $REGION", env={"REGION": "eu"})                          # this call only, overrides create()
```
Values are stored encrypted and never logged. Up to 64 variables, 64 KB in total.

## Spending limit
Set a daily/monthly cap for sandboxes in the dashboard (Sandboxes → Spending limit, UTC). When it is reached, `Sandbox.create()` raises `BudgetExceededError` (a `RateLimitError`), running ephemeral sandboxes are deleted and persistent ones are paused (disk kept) until the limit resets or you raise it. The limit can also be set via the REST API: `PUT /sandboxes/budget`.

## Errors
All inherit `EqvpsError(status, code, message, retry_after)`: `AuthenticationError` 401 · `InsufficientBalanceError` 402 · `NotFoundError` 404 · `SandboxPausedError` 409 · `SandboxDeletedError` 410 · `FileTooLargeError` 413 · `ValidationError` 422 · `RateLimitError` 429 (`BudgetExceededError` when the spending limit is reached) · `CapacityError` 503 · `SandboxTimeoutError` (HTTP timeout). 429/503 with `Retry-After` are retried automatically (`max_retries=3`). A non-zero exit code or a command timeout is **not** an exception — check `result.ok` / `result.timed_out`.

```python
from eqvps import Sandbox, InsufficientBalanceError, RateLimitError
try:
    sb = Sandbox.create()
except InsufficientBalanceError:
    print("top up your EQVPS balance")
except RateLimitError as e:
    print("retry in", e.retry_after, "s")
```

REST API reference: https://eqvps.com/openapi.json (tag `sandboxes`) · human guide: https://eqvps.com/en/docs/sandboxes

## Changelog
- **0.2.0** — environment variables: `env` in `create()`, `run()` and `exec()` (values stored encrypted, only names come back as `env_keys`); `BudgetExceededError` (HTTP 429 `budget_exceeded`) when your sandbox spending limit is reached.
- **0.1.0** — first release: create / run / exec / files / background tasks / kill.
