feat: prune SDK to API-key surface, add stats/usage/metrics/events
All checks were successful
ci/woodpecker/push/unit Pipeline was successful

Strip session-only routes (auth, me, teams, hosts, channels, admin,
audit-logs) from api/openapi.yaml via new scripts/prune_openapi.py,
which is wired into `make generate` and `make prune-spec`. Spec drops
from 4787 to 1717 lines; regenerated _generated.py drops from 906 to
415 lines with all obsolete models removed.

Add SDK methods on CapsulesResource / AsyncCapsulesResource for the
apiKey-usable routes that were previously unwrapped:
  - capsules.stats(range=...)         -> CapsuleStats
  - capsules.usage(from_=..., to=...) -> UsageResponse (accepts date)
  - capsules.metrics(id, range=...)   -> CapsuleMetrics

Add EventsResource / AsyncEventsResource exposing
WrennClient.events.stream() over /v1/events/stream (SSE). Parser
handles `:keepalive` comments and multi-line `data:` frames.

Re-export new models (CapsuleStats, CapsuleMetrics, MetricPoint,
UsageResponse, SSEEvent, Resource, Actor) from wrenn.models.

Drop email-validator runtime dep (no EmailStr remains). Bump version
to 0.3.0 (pyproject + __version__). Add pyyaml to dev deps and
types-PyYAML to the pre-commit mypy hook for the prune script.

Normalize docstrings on new methods/classes to Google style with
Example:: blocks where useful. Regenerate docs/reference.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-23 01:28:54 +06:00
parent a2d9aac78b
commit 03cd2227b5
12 changed files with 1915 additions and 3612 deletions

View File

@ -103,6 +103,73 @@ class TestCapsules:
client.capsules.ping("sb-1")
assert route.called
@respx.mock
def test_stats(self, client):
route = respx.get(f"{BASE}/v1/capsules/stats").respond(
200,
json={
"range": "6h",
"current": {"running_count": 2, "vcpus_reserved": 4},
"peaks": {"running_count": 9},
"series": {"running": [1, 2, 2]},
},
)
stats = client.capsules.stats(range="6h")
assert "range=6h" in str(route.calls[0].request.url)
assert stats.current and stats.current.running_count == 2
assert stats.peaks and stats.peaks.running_count == 9
@respx.mock
def test_usage_passes_date_params(self, client):
import datetime as _dt
route = respx.get(f"{BASE}/v1/capsules/usage").respond(
200, json={"from": "2026-06-01", "to": "2026-06-22", "points": []}
)
client.capsules.usage(from_=_dt.date(2026, 6, 1), to="2026-06-22")
url = str(route.calls[0].request.url)
assert "from=2026-06-01" in url
assert "to=2026-06-22" in url
@respx.mock
def test_metrics(self, client):
respx.get(f"{BASE}/v1/capsules/sb-1/metrics").respond(
200,
json={
"sandbox_id": "sb-1",
"range": "10m",
"points": [{"timestamp_unix": 1, "cpu_pct": 12.5, "mem_bytes": 4096}],
},
)
m = client.capsules.metrics("sb-1")
assert m.sandbox_id == "sb-1"
assert m.points and m.points[0].cpu_pct == 12.5
class TestEvents:
@respx.mock
def test_stream_parses_sse_frames(self, client):
body = (
": keepalive\n"
"event: capsule.create\n"
'data: {"event":"capsule.create","resource":{"id":"sb-1","type":"capsule"}}\n'
"\n"
"event: capsule.destroy\n"
'data: {"event":"capsule.destroy","resource":{"id":"sb-1","type":"capsule"}}\n'
"\n"
)
respx.get(f"{BASE}/v1/events/stream").respond(
200,
headers={"content-type": "text/event-stream"},
content=body,
)
events = list(client.events.stream())
assert [e.event.value for e in events] == [
"capsule.create",
"capsule.destroy",
]
assert events[0].resource and events[0].resource.id == "sb-1"
class TestSnapshots:
@respx.mock