fix: update SDK for v0.2.0 API compatibility
Some checks failed
ci/woodpecker/pr/check Pipeline failed
Some checks failed
ci/woodpecker/pr/check Pipeline failed
Sync OpenAPI spec to v0.2.0, fix type annotation shadowing by using builtins.list in annotated signatures, guard poll interval lookup against None status, and reorder capsule ID assignment to validate before storing.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -181,3 +181,4 @@ CODE_EXECUTION.md
|
||||
.code-review-graph/
|
||||
.claude
|
||||
.mcp.json
|
||||
AGENTS.md
|
||||
|
||||
56
AGENTS.md
56
AGENTS.md
@ -1,56 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Project
|
||||
|
||||
Wrenn Python SDK — a client library for the Wrenn microVM platform. e2b drop-in replacement.
|
||||
Package name: `wrenn`. Python 3.13+, managed with [uv](https://docs.astral.sh/uv/).
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
uv sync # install deps
|
||||
make lint # ruff check + format check (no auto-fix)
|
||||
make test # unit tests only (tests/test_client.py)
|
||||
make test-integration # all tests including integration (needs live server)
|
||||
make generate # regenerate models from OpenAPI spec (fetches from remote)
|
||||
make check # lint + unit test
|
||||
```
|
||||
|
||||
- `make test` only runs `tests/test_client.py`, not all unit tests. To run a specific test file: `uv run pytest tests/test_capsule_features.py -v`
|
||||
- No typecheck step in Makefile or CI. `mypy` is a dev dependency but not wired up — do not assume it runs.
|
||||
|
||||
## Architecture
|
||||
|
||||
- `src/wrenn/` — the library package
|
||||
- `capsule.py` / `async_capsule.py` — high-level `Capsule` / `AsyncCapsule` (main user-facing classes)
|
||||
- `client.py` — low-level `WrennClient` / `AsyncWrennClient`
|
||||
- `commands.py` — command execution and streaming
|
||||
- `files.py` — filesystem operations
|
||||
- `pty.py` — interactive terminal (PTY) over WebSocket
|
||||
- `exceptions.py` — typed error hierarchy (`WrennError` base)
|
||||
- `models/_generated.py` — **auto-generated** from OpenAPI spec via `datamodel-codegen` (never edit directly; run `make generate`)
|
||||
- `sandbox.py` — deprecated `Sandbox` alias for `Capsule`
|
||||
- `code_interpreter/` — specialized capsule for stateful Jupyter kernel execution
|
||||
- `tests/` — unit tests use `respx` to mock `httpx`; integration tests are in `tests/integration/`
|
||||
- `api/openapi.yaml` — downloaded OpenAPI spec used for code generation
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- Generated code lives in `src/wrenn/models/_generated.py`. Never edit it. Run `make generate` to update.
|
||||
- `Sandbox` is a deprecated alias for `Capsule`. New code should use `Capsule` / `AsyncCapsule`.
|
||||
- Dual sync/async API: every major class has an `Async` counterpart.
|
||||
- Uses `httpx` for HTTP, `httpx-ws` for WebSockets, `pydantic` for models.
|
||||
- `__init__.py` uses `__getattr__` for lazy deprecated aliases (`Sandbox`, `WrennHostHasSandboxesError`).
|
||||
|
||||
## Testing
|
||||
|
||||
- Unit tests mock HTTP via `respx` (httpx mocking library).
|
||||
- Integration tests require env vars: `WRENN_API_KEY` (or `WRENN_TOKEN`), optionally `WRENN_BASE_URL`.
|
||||
- Integration test fixtures in `tests/integration/conftest.py` create real capsules and clean them up.
|
||||
- `pytest` marker: `@pytest.mark.integration` for tests needing a live server.
|
||||
|
||||
## CI
|
||||
|
||||
Woodpecker CI (`.woodpecker/check.yml`) runs on push to `main` and `dev`:
|
||||
1. `make lint`
|
||||
2. `make test` (unit tests only — integration tests are not in CI)
|
||||
115
api/openapi.yaml
115
api/openapi.yaml
@ -1,8 +1,8 @@
|
||||
openapi: "3.1.0"
|
||||
info:
|
||||
title: Wrenn API
|
||||
description: MicroVM-based code execution platform API.
|
||||
version: "0.1.3"
|
||||
description: AI agent execution platform API.
|
||||
version: "0.2.0"
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8080
|
||||
@ -866,8 +866,8 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CreateCapsuleRequest"
|
||||
responses:
|
||||
"201":
|
||||
description: Capsule created
|
||||
"202":
|
||||
description: Capsule creation initiated (status will be "starting")
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@ -988,8 +988,8 @@ paths:
|
||||
security:
|
||||
- apiKeyAuth: []
|
||||
responses:
|
||||
"204":
|
||||
description: Capsule destroyed
|
||||
"202":
|
||||
description: Capsule destruction initiated
|
||||
|
||||
/v1/capsules/{id}/exec:
|
||||
parameters:
|
||||
@ -1260,8 +1260,8 @@ paths:
|
||||
destroys all running resources. The capsule exists only as files on
|
||||
disk and can be resumed later.
|
||||
responses:
|
||||
"200":
|
||||
description: Capsule paused (snapshot taken, resources released)
|
||||
"202":
|
||||
description: Capsule pause initiated (status will be "pausing")
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@ -1292,8 +1292,8 @@ paths:
|
||||
memory loading. Boots a fresh Firecracker process, sets up a new
|
||||
network slot, and waits for envd to become ready.
|
||||
responses:
|
||||
"200":
|
||||
description: Capsule resumed (new VM booted from snapshot)
|
||||
"202":
|
||||
description: Capsule resume initiated (status will be "resuming")
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@ -2035,6 +2035,51 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/v1/hosts/sandbox-events:
|
||||
post:
|
||||
summary: Sandbox lifecycle event callback
|
||||
operationId: sandboxEventCallback
|
||||
tags: [hosts]
|
||||
security:
|
||||
- hostTokenAuth: []
|
||||
description: |
|
||||
Receives autonomous lifecycle events from host agents (e.g. auto-pause
|
||||
from the TTL reaper). The event is published to an internal Redis stream
|
||||
for the control plane's event consumer to process.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [event, sandbox_id, host_id]
|
||||
properties:
|
||||
event:
|
||||
type: string
|
||||
enum: [sandbox.auto_paused]
|
||||
sandbox_id:
|
||||
type: string
|
||||
host_id:
|
||||
type: string
|
||||
timestamp:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
"204":
|
||||
description: Event accepted
|
||||
"400":
|
||||
description: Invalid request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"403":
|
||||
description: Host ID mismatch
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/v1/hosts/auth/refresh:
|
||||
post:
|
||||
summary: Refresh host JWT
|
||||
@ -2346,6 +2391,54 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
/v1/admin/users/{id}/admin:
|
||||
put:
|
||||
summary: Grant or revoke platform admin
|
||||
operationId: setUserAdmin
|
||||
tags: [admin]
|
||||
description: |
|
||||
Sets the platform admin flag on a user. Cannot remove the last admin.
|
||||
Requires platform admin access (JWT + is_admin).
|
||||
The target user's JWT is not re-issued — their frontend will reflect the
|
||||
change on next login or team switch.
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: "usr-a1b2c3d4"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [admin]
|
||||
properties:
|
||||
admin:
|
||||
type: boolean
|
||||
description: true to grant admin, false to revoke.
|
||||
responses:
|
||||
"204":
|
||||
description: Admin status updated
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"403":
|
||||
description: Caller is not a platform admin
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
"404":
|
||||
description: User not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
apiKeyAuth:
|
||||
@ -2544,7 +2637,7 @@ components:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
enum: [pending, starting, running, paused, hibernated, stopped, missing, error]
|
||||
enum: [pending, starting, running, pausing, paused, resuming, stopping, hibernated, stopped, missing, error]
|
||||
template:
|
||||
type: string
|
||||
vcpus:
|
||||
|
||||
@ -1964,15 +1964,17 @@ inactivity TTL is set.
|
||||
#### wait\_ready
|
||||
|
||||
```python
|
||||
async def wait_ready(timeout: float = 30, interval: float = 0.5) -> None
|
||||
async def wait_ready(timeout: float = 30) -> None
|
||||
```
|
||||
|
||||
Await until the capsule status is ``running``.
|
||||
|
||||
Polling interval adapts to the current transient status:
|
||||
0.5 s for starting/resuming, 2 s for pausing, 1 s for stopping.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `timeout` _float_ - Maximum seconds to wait. Defaults to ``30``.
|
||||
- `interval` _float_ - Polling interval in seconds. Defaults to ``0.5``.
|
||||
|
||||
|
||||
**Raises**:
|
||||
@ -2534,15 +2536,17 @@ inactivity TTL is set.
|
||||
#### wait\_ready
|
||||
|
||||
```python
|
||||
def wait_ready(timeout: float = 30, interval: float = 0.5) -> None
|
||||
def wait_ready(timeout: float = 30) -> None
|
||||
```
|
||||
|
||||
Block until the capsule status is ``running``.
|
||||
|
||||
Polling interval adapts to the current transient status:
|
||||
0.5 s for starting/resuming, 2 s for pausing, 1 s for stopping.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `timeout` _float_ - Maximum seconds to wait. Defaults to ``30``.
|
||||
- `interval` _float_ - Polling interval in seconds. Defaults to ``0.5``.
|
||||
|
||||
|
||||
**Raises**:
|
||||
@ -2700,17 +2704,6 @@ Create a snapshot template from this capsule's current state.
|
||||
|
||||
# wrenn.\_config
|
||||
|
||||
<a id="wrenn._config.ConnectionConfig"></a>
|
||||
|
||||
## ConnectionConfig Objects
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class ConnectionConfig()
|
||||
```
|
||||
|
||||
Resolved credentials and base URL for Wrenn API calls.
|
||||
|
||||
<a id="wrenn._git._auth"></a>
|
||||
|
||||
# wrenn.\_git.\_auth
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import builtins
|
||||
import logging
|
||||
import time
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
@ -140,14 +140,19 @@ class AsyncCapsule:
|
||||
info = await client.capsules.get(capsule_id)
|
||||
|
||||
if info.status == Status.paused:
|
||||
info = await client.capsules.resume(capsule_id)
|
||||
await client.capsules.resume(capsule_id)
|
||||
|
||||
return cls(
|
||||
capsule = cls(
|
||||
_capsule_id=capsule_id,
|
||||
_client=client,
|
||||
_info=info,
|
||||
)
|
||||
|
||||
if info.status != Status.running:
|
||||
await capsule.wait_ready()
|
||||
|
||||
return capsule
|
||||
|
||||
# ── Dual instance/static lifecycle ──────────────────────────
|
||||
|
||||
destroy = _DualMethod("_instance_destroy", "_static_destroy")
|
||||
@ -224,12 +229,21 @@ class AsyncCapsule:
|
||||
"""
|
||||
await self._client.capsules.ping(self._id)
|
||||
|
||||
async def wait_ready(self, timeout: float = 30, interval: float = 0.5) -> None:
|
||||
_POLL_INTERVALS: dict[Status, float] = {
|
||||
Status.starting: 0.5,
|
||||
Status.resuming: 0.5,
|
||||
Status.pausing: 2.0,
|
||||
Status.stopping: 1.0,
|
||||
}
|
||||
|
||||
async def wait_ready(self, timeout: float = 30) -> None:
|
||||
"""Await until the capsule status is ``running``.
|
||||
|
||||
Polling interval adapts to the current transient status:
|
||||
0.5 s for starting/resuming, 2 s for pausing, 1 s for stopping.
|
||||
|
||||
Args:
|
||||
timeout (float): Maximum seconds to wait. Defaults to ``30``.
|
||||
interval (float): Polling interval in seconds. Defaults to ``0.5``.
|
||||
|
||||
Raises:
|
||||
TimeoutError: If the capsule does not reach ``running`` state
|
||||
@ -246,7 +260,10 @@ class AsyncCapsule:
|
||||
if info.status in (Status.error, Status.stopped):
|
||||
raise RuntimeError(f"Capsule entered {info.status} state while waiting")
|
||||
if info.status == Status.paused:
|
||||
info = await self._client.capsules.resume(self._id)
|
||||
await self._client.capsules.resume(self._id)
|
||||
interval = (
|
||||
self._POLL_INTERVALS.get(info.status, 0.5) if info.status else 0.5
|
||||
)
|
||||
await asyncio.sleep(interval)
|
||||
raise TimeoutError(f"Capsule {self._id} did not become ready within {timeout}s")
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import builtins
|
||||
import logging
|
||||
import time
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
@ -112,9 +112,9 @@ class Capsule:
|
||||
memory_mb=memory_mb,
|
||||
timeout_sec=timeout,
|
||||
)
|
||||
self._id = self._info.id
|
||||
if self._id is None:
|
||||
if self._info.id is None:
|
||||
raise RuntimeError("API returned a capsule without an ID")
|
||||
self._id = self._info.id
|
||||
except Exception:
|
||||
self._client.close()
|
||||
raise
|
||||
@ -214,14 +214,19 @@ class Capsule:
|
||||
info = client.capsules.get(capsule_id)
|
||||
|
||||
if info.status == Status.paused:
|
||||
info = client.capsules.resume(capsule_id)
|
||||
client.capsules.resume(capsule_id)
|
||||
|
||||
return cls(
|
||||
capsule = cls(
|
||||
_capsule_id=capsule_id,
|
||||
_client=client,
|
||||
_info=info,
|
||||
)
|
||||
|
||||
if info.status != Status.running:
|
||||
capsule.wait_ready()
|
||||
|
||||
return capsule
|
||||
|
||||
# ── Dual instance/static lifecycle ──────────────────────────
|
||||
|
||||
destroy = _DualMethod("_instance_destroy", "_static_destroy")
|
||||
@ -306,12 +311,21 @@ class Capsule:
|
||||
"""
|
||||
self._client.capsules.ping(self._id)
|
||||
|
||||
def wait_ready(self, timeout: float = 30, interval: float = 0.5) -> None:
|
||||
_POLL_INTERVALS: dict[Status, float] = {
|
||||
Status.starting: 0.5,
|
||||
Status.resuming: 0.5,
|
||||
Status.pausing: 2.0,
|
||||
Status.stopping: 1.0,
|
||||
}
|
||||
|
||||
def wait_ready(self, timeout: float = 30) -> None:
|
||||
"""Block until the capsule status is ``running``.
|
||||
|
||||
Polling interval adapts to the current transient status:
|
||||
0.5 s for starting/resuming, 2 s for pausing, 1 s for stopping.
|
||||
|
||||
Args:
|
||||
timeout (float): Maximum seconds to wait. Defaults to ``30``.
|
||||
interval (float): Polling interval in seconds. Defaults to ``0.5``.
|
||||
|
||||
Raises:
|
||||
TimeoutError: If the capsule does not reach ``running`` state
|
||||
@ -328,7 +342,10 @@ class Capsule:
|
||||
if info.status in (Status.error, Status.stopped):
|
||||
raise RuntimeError(f"Capsule entered {info.status} state while waiting")
|
||||
if info.status == Status.paused:
|
||||
info = self._client.capsules.resume(self._id)
|
||||
self._client.capsules.resume(self._id)
|
||||
interval = (
|
||||
self._POLL_INTERVALS.get(info.status, 0.5) if info.status else 0.5
|
||||
)
|
||||
time.sleep(interval)
|
||||
raise TimeoutError(f"Capsule {self._id} did not become ready within {timeout}s")
|
||||
|
||||
|
||||
@ -111,7 +111,7 @@ class CapsulesResource:
|
||||
Raises:
|
||||
WrennNotFoundError: If no capsule with the given ID exists.
|
||||
"""
|
||||
resp = self._http.post(f"/v1/capsules/{id}/pause", timeout=_LONG_TIMEOUT)
|
||||
resp = self._http.post(f"/v1/capsules/{id}/pause")
|
||||
return CapsuleModel.model_validate(handle_response(resp))
|
||||
|
||||
def resume(self, id: str) -> CapsuleModel:
|
||||
@ -227,7 +227,7 @@ class AsyncCapsulesResource:
|
||||
Raises:
|
||||
WrennNotFoundError: If no capsule with the given ID exists.
|
||||
"""
|
||||
resp = await self._http.post(f"/v1/capsules/{id}/pause", timeout=_LONG_TIMEOUT)
|
||||
resp = await self._http.post(f"/v1/capsules/{id}/pause")
|
||||
return CapsuleModel.model_validate(handle_response(resp))
|
||||
|
||||
async def resume(self, id: str) -> CapsuleModel:
|
||||
|
||||
@ -150,6 +150,9 @@ def handle_response(resp: httpx.Response) -> dict | list:
|
||||
if resp.status_code == 204:
|
||||
return {}
|
||||
|
||||
if not resp.content:
|
||||
return {}
|
||||
|
||||
return resp.json()
|
||||
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: openapi.yaml
|
||||
# timestamp: 2026-04-22T20:21:34+00:00
|
||||
# timestamp: 2026-05-15T07:57:28+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
from pydantic import AwareDatetime, BaseModel, EmailStr, Field
|
||||
@ -133,7 +133,10 @@ class Status(StrEnum):
|
||||
pending = "pending"
|
||||
starting = "starting"
|
||||
running = "running"
|
||||
pausing = "pausing"
|
||||
paused = "paused"
|
||||
resuming = "resuming"
|
||||
stopping = "stopping"
|
||||
hibernated = "hibernated"
|
||||
stopped = "stopped"
|
||||
missing = "missing"
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
import respx
|
||||
|
||||
from wrenn.capsule import Capsule, _build_proxy_url
|
||||
@ -30,9 +31,13 @@ class TestCapsuleCreate:
|
||||
@respx.mock
|
||||
def test_capsule_constructor_creates(self):
|
||||
respx.post(f"{BASE}/v1/capsules").respond(
|
||||
201, json={"id": "cl-1", "status": "pending", "template": "minimal"}
|
||||
202, json={"id": "cl-1", "status": "starting", "template": "minimal"}
|
||||
)
|
||||
cap = Capsule(
|
||||
template="minimal",
|
||||
api_key="wrn_test1234567890abcdef12345678",
|
||||
base_url=BASE,
|
||||
)
|
||||
cap = Capsule(template="minimal", api_key="wrn_test1234567890abcdef12345678", base_url=BASE)
|
||||
assert cap.capsule_id == "cl-1"
|
||||
assert hasattr(cap, "commands")
|
||||
assert hasattr(cap, "files")
|
||||
@ -40,7 +45,7 @@ class TestCapsuleCreate:
|
||||
@respx.mock
|
||||
def test_capsule_create_classmethod(self):
|
||||
respx.post(f"{BASE}/v1/capsules").respond(
|
||||
201, json={"id": "cl-2", "status": "pending"}
|
||||
202, json={"id": "cl-2", "status": "starting"}
|
||||
)
|
||||
cap = Capsule.create(api_key="wrn_test1234567890abcdef12345678", base_url=BASE)
|
||||
assert cap.capsule_id == "cl-2"
|
||||
@ -48,9 +53,9 @@ class TestCapsuleCreate:
|
||||
@respx.mock
|
||||
def test_capsule_context_manager_kills(self):
|
||||
respx.post(f"{BASE}/v1/capsules").respond(
|
||||
201, json={"id": "cl-1", "status": "pending"}
|
||||
202, json={"id": "cl-1", "status": "starting"}
|
||||
)
|
||||
kill_route = respx.delete(f"{BASE}/v1/capsules/cl-1").respond(204)
|
||||
kill_route = respx.delete(f"{BASE}/v1/capsules/cl-1").respond(202)
|
||||
with Capsule(api_key="wrn_test1234567890abcdef12345678", base_url=BASE) as cap:
|
||||
assert cap.capsule_id == "cl-1"
|
||||
assert kill_route.called
|
||||
@ -59,7 +64,7 @@ class TestCapsuleCreate:
|
||||
def test_capsule_env_var(self, monkeypatch):
|
||||
monkeypatch.setenv("WRENN_API_KEY", "wrn_from_env_key")
|
||||
respx.post(f"{BASE}/v1/capsules").respond(
|
||||
201, json={"id": "cl-3", "status": "pending"}
|
||||
202, json={"id": "cl-3", "status": "starting"}
|
||||
)
|
||||
cap = Capsule(base_url=BASE)
|
||||
assert cap.capsule_id == "cl-3"
|
||||
@ -68,17 +73,21 @@ class TestCapsuleCreate:
|
||||
class TestCapsuleStaticMethods:
|
||||
@respx.mock
|
||||
def test_static_destroy(self):
|
||||
route = respx.delete(f"{BASE}/v1/capsules/cl-1").respond(204)
|
||||
Capsule._static_destroy("cl-1", api_key="wrn_test1234567890abcdef12345678", base_url=BASE)
|
||||
route = respx.delete(f"{BASE}/v1/capsules/cl-1").respond(202)
|
||||
Capsule._static_destroy(
|
||||
"cl-1", api_key="wrn_test1234567890abcdef12345678", base_url=BASE
|
||||
)
|
||||
assert route.called
|
||||
|
||||
@respx.mock
|
||||
def test_static_pause(self):
|
||||
respx.post(f"{BASE}/v1/capsules/cl-1/pause").respond(
|
||||
200, json={"id": "cl-1", "status": "paused"}
|
||||
202, json={"id": "cl-1", "status": "pausing"}
|
||||
)
|
||||
info = Capsule._static_pause("cl-1", api_key="wrn_test1234567890abcdef12345678", base_url=BASE)
|
||||
assert info.status.value == "paused"
|
||||
info = Capsule._static_pause(
|
||||
"cl-1", api_key="wrn_test1234567890abcdef12345678", base_url=BASE
|
||||
)
|
||||
assert info.status.value == "pausing"
|
||||
|
||||
@respx.mock
|
||||
def test_static_list(self):
|
||||
@ -106,18 +115,24 @@ class TestCapsuleConnect:
|
||||
respx.get(f"{BASE}/v1/capsules/cl-1").respond(
|
||||
200, json={"id": "cl-1", "status": "running"}
|
||||
)
|
||||
cap = Capsule.connect("cl-1", api_key="wrn_test1234567890abcdef12345678", base_url=BASE)
|
||||
cap = Capsule.connect(
|
||||
"cl-1", api_key="wrn_test1234567890abcdef12345678", base_url=BASE
|
||||
)
|
||||
assert cap.capsule_id == "cl-1"
|
||||
|
||||
@respx.mock
|
||||
def test_connect_paused_resumes(self):
|
||||
respx.get(f"{BASE}/v1/capsules/cl-1").respond(
|
||||
200, json={"id": "cl-1", "status": "paused"}
|
||||
)
|
||||
get_route = respx.get(f"{BASE}/v1/capsules/cl-1")
|
||||
get_route.side_effect = [
|
||||
httpx.Response(200, json={"id": "cl-1", "status": "paused"}),
|
||||
httpx.Response(200, json={"id": "cl-1", "status": "running"}),
|
||||
]
|
||||
respx.post(f"{BASE}/v1/capsules/cl-1/resume").respond(
|
||||
200, json={"id": "cl-1", "status": "running"}
|
||||
202, json={"id": "cl-1", "status": "resuming"}
|
||||
)
|
||||
cap = Capsule.connect(
|
||||
"cl-1", api_key="wrn_test1234567890abcdef12345678", base_url=BASE
|
||||
)
|
||||
cap = Capsule.connect("cl-1", api_key="wrn_test1234567890abcdef12345678", base_url=BASE)
|
||||
assert cap.capsule_id == "cl-1"
|
||||
|
||||
|
||||
|
||||
@ -36,10 +36,10 @@ class TestCapsules:
|
||||
@respx.mock
|
||||
def test_create(self, client):
|
||||
respx.post(f"{BASE}/v1/capsules").respond(
|
||||
201,
|
||||
202,
|
||||
json={
|
||||
"id": "sb-1",
|
||||
"status": "pending",
|
||||
"status": "starting",
|
||||
"template": "base-python",
|
||||
"vcpus": 2,
|
||||
"memory_mb": 1024,
|
||||
@ -48,12 +48,12 @@ class TestCapsules:
|
||||
resp = client.capsules.create(template="base-python", vcpus=2, memory_mb=1024)
|
||||
assert isinstance(resp, Capsule)
|
||||
assert resp.id == "sb-1"
|
||||
assert resp.status == Status.pending
|
||||
assert resp.status == Status.starting
|
||||
|
||||
@respx.mock
|
||||
def test_create_defaults(self, client):
|
||||
respx.post(f"{BASE}/v1/capsules").respond(
|
||||
201, json={"id": "sb-2", "status": "pending"}
|
||||
202, json={"id": "sb-2", "status": "starting"}
|
||||
)
|
||||
resp = client.capsules.create()
|
||||
assert resp.id == "sb-2"
|
||||
@ -77,25 +77,25 @@ class TestCapsules:
|
||||
|
||||
@respx.mock
|
||||
def test_destroy(self, client):
|
||||
route = respx.delete(f"{BASE}/v1/capsules/sb-1").respond(204)
|
||||
route = respx.delete(f"{BASE}/v1/capsules/sb-1").respond(202)
|
||||
client.capsules.destroy("sb-1")
|
||||
assert route.called
|
||||
|
||||
@respx.mock
|
||||
def test_pause(self, client):
|
||||
respx.post(f"{BASE}/v1/capsules/sb-1/pause").respond(
|
||||
200, json={"id": "sb-1", "status": "paused"}
|
||||
202, json={"id": "sb-1", "status": "pausing"}
|
||||
)
|
||||
resp = client.capsules.pause("sb-1")
|
||||
assert resp.status == Status.paused
|
||||
assert resp.status == Status.pausing
|
||||
|
||||
@respx.mock
|
||||
def test_resume(self, client):
|
||||
respx.post(f"{BASE}/v1/capsules/sb-1/resume").respond(
|
||||
200, json={"id": "sb-1", "status": "running"}
|
||||
202, json={"id": "sb-1", "status": "resuming"}
|
||||
)
|
||||
resp = client.capsules.resume("sb-1")
|
||||
assert resp.status == Status.running
|
||||
assert resp.status == Status.resuming
|
||||
|
||||
@respx.mock
|
||||
def test_ping(self, client):
|
||||
@ -238,7 +238,7 @@ class TestAsyncClient:
|
||||
async def test_async_capsules_create(self, async_client):
|
||||
async with async_client:
|
||||
respx.post(f"{BASE}/v1/capsules").respond(
|
||||
201, json={"id": "sb-1", "status": "pending"}
|
||||
202, json={"id": "sb-1", "status": "starting"}
|
||||
)
|
||||
resp = await async_client.capsules.create(template="base-python")
|
||||
assert resp.id == "sb-1"
|
||||
|
||||
Reference in New Issue
Block a user