forked from wrenn/python-sdk
1024 lines
36 KiB
Python
1024 lines
36 KiB
Python
from __future__ import annotations
|
|
|
|
import builtins
|
|
import warnings
|
|
from typing import cast
|
|
|
|
import httpx
|
|
|
|
from wrenn.capsule import Capsule
|
|
from wrenn.exceptions import handle_response
|
|
from wrenn.models import (
|
|
APIKeyResponse,
|
|
AuthResponse,
|
|
CapsuleStats,
|
|
ChannelResponse,
|
|
CreateChannelRequest,
|
|
CreateHostResponse,
|
|
Host,
|
|
HostDeletePreview,
|
|
MeResponse,
|
|
RotateConfigRequest,
|
|
SignupResponse,
|
|
Template,
|
|
TeamDetail,
|
|
TeamMember,
|
|
TeamWithRole,
|
|
TestChannelRequest,
|
|
UpdateChannelRequest,
|
|
UsageResponse,
|
|
UserSearchResult,
|
|
)
|
|
from wrenn.models import (
|
|
Capsule as CapsuleModel,
|
|
)
|
|
|
|
DEFAULT_BASE_URL = "https://api.wrenn.dev"
|
|
|
|
_MGMT_AUTH_MSG = "This operation requires a JWT token. Pass token= to WrennClient."
|
|
_DATA_AUTH_MSG = "Capsule operations require an API key. Pass api_key= to WrennClient."
|
|
|
|
|
|
def _require(
|
|
client: httpx.Client | httpx.AsyncClient | None, message: str
|
|
) -> httpx.Client | httpx.AsyncClient:
|
|
if client is None:
|
|
raise ValueError(message)
|
|
return client
|
|
|
|
|
|
class AuthResource:
|
|
"""Sync auth operations."""
|
|
|
|
def __init__(
|
|
self,
|
|
public_http: httpx.Client,
|
|
mgmt_http: httpx.Client | None,
|
|
) -> None:
|
|
self._public_http = public_http
|
|
self._mgmt_http = mgmt_http
|
|
|
|
def signup(self, email: str, password: str, name: str) -> SignupResponse:
|
|
resp = self._public_http.post(
|
|
"/v1/auth/signup",
|
|
json={"email": email, "password": password, "name": name},
|
|
)
|
|
return SignupResponse.model_validate(handle_response(resp))
|
|
|
|
def login(self, email: str, password: str) -> AuthResponse:
|
|
resp = self._public_http.post(
|
|
"/v1/auth/login", json={"email": email, "password": password}
|
|
)
|
|
return AuthResponse.model_validate(handle_response(resp))
|
|
|
|
def activate(self, token: str) -> AuthResponse:
|
|
resp = self._public_http.post("/v1/auth/activate", json={"token": token})
|
|
return AuthResponse.model_validate(handle_response(resp))
|
|
|
|
def switch_team(self, team_id: str) -> AuthResponse:
|
|
http = _require(self._mgmt_http, _MGMT_AUTH_MSG)
|
|
resp = http.post("/v1/auth/switch-team", json={"team_id": team_id})
|
|
return AuthResponse.model_validate(handle_response(resp))
|
|
|
|
|
|
class AsyncAuthResource:
|
|
"""Async auth operations."""
|
|
|
|
def __init__(
|
|
self,
|
|
public_http: httpx.AsyncClient,
|
|
mgmt_http: httpx.AsyncClient | None,
|
|
) -> None:
|
|
self._public_http = public_http
|
|
self._mgmt_http = mgmt_http
|
|
|
|
async def signup(self, email: str, password: str, name: str) -> SignupResponse:
|
|
resp = await self._public_http.post(
|
|
"/v1/auth/signup",
|
|
json={"email": email, "password": password, "name": name},
|
|
)
|
|
return SignupResponse.model_validate(handle_response(resp))
|
|
|
|
async def login(self, email: str, password: str) -> AuthResponse:
|
|
resp = await self._public_http.post(
|
|
"/v1/auth/login", json={"email": email, "password": password}
|
|
)
|
|
return AuthResponse.model_validate(handle_response(resp))
|
|
|
|
async def activate(self, token: str) -> AuthResponse:
|
|
resp = await self._public_http.post("/v1/auth/activate", json={"token": token})
|
|
return AuthResponse.model_validate(handle_response(resp))
|
|
|
|
async def switch_team(self, team_id: str) -> AuthResponse:
|
|
http = _require(self._mgmt_http, _MGMT_AUTH_MSG)
|
|
resp = await http.post("/v1/auth/switch-team", json={"team_id": team_id})
|
|
return AuthResponse.model_validate(handle_response(resp))
|
|
|
|
|
|
class AccountResource:
|
|
"""Sync account operations."""
|
|
|
|
def __init__(
|
|
self,
|
|
public_http: httpx.Client,
|
|
mgmt_http: httpx.Client | None,
|
|
) -> None:
|
|
self._public_http = public_http
|
|
self._mgmt_http = mgmt_http
|
|
|
|
def _require_mgmt(self) -> httpx.Client:
|
|
return _require(self._mgmt_http, _MGMT_AUTH_MSG) # type: ignore[return-value]
|
|
|
|
def get(self) -> MeResponse:
|
|
resp = self._require_mgmt().get("/v1/me")
|
|
return MeResponse.model_validate(handle_response(resp))
|
|
|
|
def update_name(self, name: str) -> AuthResponse:
|
|
resp = self._require_mgmt().patch("/v1/me", json={"name": name})
|
|
return AuthResponse.model_validate(handle_response(resp))
|
|
|
|
def delete(self, confirmation: str) -> None:
|
|
resp = self._require_mgmt().delete(
|
|
"/v1/me", json={"confirmation": confirmation}
|
|
)
|
|
handle_response(resp)
|
|
|
|
def change_password(
|
|
self,
|
|
new_password: str,
|
|
current_password: str | None = None,
|
|
confirm_password: str | None = None,
|
|
) -> None:
|
|
payload: dict = {"new_password": new_password}
|
|
if current_password is not None:
|
|
payload["current_password"] = current_password
|
|
if confirm_password is not None:
|
|
payload["confirm_password"] = confirm_password
|
|
resp = self._require_mgmt().post("/v1/me/password", json=payload)
|
|
handle_response(resp)
|
|
|
|
def request_password_reset(self, email: str) -> None:
|
|
resp = self._public_http.post("/v1/me/password/reset", json={"email": email})
|
|
handle_response(resp)
|
|
|
|
def confirm_password_reset(self, token: str, new_password: str) -> None:
|
|
resp = self._public_http.post(
|
|
"/v1/me/password/reset/confirm",
|
|
json={"token": token, "new_password": new_password},
|
|
)
|
|
handle_response(resp)
|
|
|
|
def connect_provider(self, provider: str) -> dict:
|
|
resp = self._require_mgmt().get(f"/v1/me/providers/{provider}/connect")
|
|
return handle_response(resp)
|
|
|
|
def disconnect_provider(self, provider: str) -> None:
|
|
resp = self._require_mgmt().delete(f"/v1/me/providers/{provider}")
|
|
handle_response(resp)
|
|
|
|
|
|
class AsyncAccountResource:
|
|
"""Async account operations."""
|
|
|
|
def __init__(
|
|
self,
|
|
public_http: httpx.AsyncClient,
|
|
mgmt_http: httpx.AsyncClient | None,
|
|
) -> None:
|
|
self._public_http = public_http
|
|
self._mgmt_http = mgmt_http
|
|
|
|
def _require_mgmt(self) -> httpx.AsyncClient:
|
|
return _require(self._mgmt_http, _MGMT_AUTH_MSG) # type: ignore[return-value]
|
|
|
|
async def get(self) -> MeResponse:
|
|
resp = await self._require_mgmt().get("/v1/me")
|
|
return MeResponse.model_validate(handle_response(resp))
|
|
|
|
async def update_name(self, name: str) -> AuthResponse:
|
|
resp = await self._require_mgmt().patch("/v1/me", json={"name": name})
|
|
return AuthResponse.model_validate(handle_response(resp))
|
|
|
|
async def delete(self, confirmation: str) -> None:
|
|
resp = await self._require_mgmt().delete(
|
|
"/v1/me", json={"confirmation": confirmation}
|
|
)
|
|
handle_response(resp)
|
|
|
|
async def change_password(
|
|
self,
|
|
new_password: str,
|
|
current_password: str | None = None,
|
|
confirm_password: str | None = None,
|
|
) -> None:
|
|
payload: dict = {"new_password": new_password}
|
|
if current_password is not None:
|
|
payload["current_password"] = current_password
|
|
if confirm_password is not None:
|
|
payload["confirm_password"] = confirm_password
|
|
resp = await self._require_mgmt().post("/v1/me/password", json=payload)
|
|
handle_response(resp)
|
|
|
|
async def request_password_reset(self, email: str) -> None:
|
|
resp = await self._public_http.post(
|
|
"/v1/me/password/reset", json={"email": email}
|
|
)
|
|
handle_response(resp)
|
|
|
|
async def confirm_password_reset(self, token: str, new_password: str) -> None:
|
|
resp = await self._public_http.post(
|
|
"/v1/me/password/reset/confirm",
|
|
json={"token": token, "new_password": new_password},
|
|
)
|
|
handle_response(resp)
|
|
|
|
async def connect_provider(self, provider: str) -> dict:
|
|
resp = await self._require_mgmt().get(f"/v1/me/providers/{provider}/connect")
|
|
return handle_response(resp)
|
|
|
|
async def disconnect_provider(self, provider: str) -> None:
|
|
resp = await self._require_mgmt().delete(f"/v1/me/providers/{provider}")
|
|
handle_response(resp)
|
|
|
|
|
|
class APIKeysResource:
|
|
"""Sync API key operations."""
|
|
|
|
def __init__(self, http: httpx.Client | None) -> None:
|
|
self._http = http
|
|
|
|
def _require(self) -> httpx.Client:
|
|
return _require(self._http, _MGMT_AUTH_MSG) # type: ignore[return-value]
|
|
|
|
def create(self, name: str | None = None) -> APIKeyResponse:
|
|
payload: dict = {}
|
|
if name is not None:
|
|
payload["name"] = name
|
|
resp = self._require().post("/v1/api-keys", json=payload)
|
|
return APIKeyResponse.model_validate(handle_response(resp))
|
|
|
|
def list(self) -> list[APIKeyResponse]:
|
|
resp = self._require().get("/v1/api-keys")
|
|
return [APIKeyResponse.model_validate(item) for item in handle_response(resp)]
|
|
|
|
def delete(self, id: str) -> None:
|
|
resp = self._require().delete(f"/v1/api-keys/{id}")
|
|
handle_response(resp)
|
|
|
|
|
|
class AsyncAPIKeysResource:
|
|
"""Async API key operations."""
|
|
|
|
def __init__(self, http: httpx.AsyncClient | None) -> None:
|
|
self._http = http
|
|
|
|
def _require(self) -> httpx.AsyncClient:
|
|
return _require(self._http, _MGMT_AUTH_MSG) # type: ignore[return-value]
|
|
|
|
async def create(self, name: str | None = None) -> APIKeyResponse:
|
|
payload: dict = {}
|
|
if name is not None:
|
|
payload["name"] = name
|
|
resp = await self._require().post("/v1/api-keys", json=payload)
|
|
return APIKeyResponse.model_validate(handle_response(resp))
|
|
|
|
async def list(self) -> list[APIKeyResponse]:
|
|
resp = await self._require().get("/v1/api-keys")
|
|
return [APIKeyResponse.model_validate(item) for item in handle_response(resp)]
|
|
|
|
async def delete(self, id: str) -> None:
|
|
resp = await self._require().delete(f"/v1/api-keys/{id}")
|
|
handle_response(resp)
|
|
|
|
|
|
class UsersResource:
|
|
"""Sync user operations."""
|
|
|
|
def __init__(self, http: httpx.Client | None) -> None:
|
|
self._http = http
|
|
|
|
def _require(self) -> httpx.Client:
|
|
return _require(self._http, _MGMT_AUTH_MSG) # type: ignore[return-value]
|
|
|
|
def search(self, email: str) -> list[UserSearchResult]:
|
|
resp = self._require().get("/v1/users/search", params={"email": email})
|
|
return [UserSearchResult.model_validate(item) for item in handle_response(resp)]
|
|
|
|
|
|
class AsyncUsersResource:
|
|
"""Async user operations."""
|
|
|
|
def __init__(self, http: httpx.AsyncClient | None) -> None:
|
|
self._http = http
|
|
|
|
def _require(self) -> httpx.AsyncClient:
|
|
return _require(self._http, _MGMT_AUTH_MSG) # type: ignore[return-value]
|
|
|
|
async def search(self, email: str) -> list[UserSearchResult]:
|
|
resp = await self._require().get("/v1/users/search", params={"email": email})
|
|
return [UserSearchResult.model_validate(item) for item in handle_response(resp)]
|
|
|
|
|
|
class TeamsResource:
|
|
"""Sync team operations."""
|
|
|
|
def __init__(self, http: httpx.Client | None) -> None:
|
|
self._http = http
|
|
|
|
def _require(self) -> httpx.Client:
|
|
return _require(self._http, _MGMT_AUTH_MSG) # type: ignore[return-value]
|
|
|
|
def list(self) -> list[TeamWithRole]:
|
|
resp = self._require().get("/v1/teams")
|
|
return [TeamWithRole.model_validate(item) for item in handle_response(resp)]
|
|
|
|
def create(self, name: str) -> TeamWithRole:
|
|
resp = self._require().post("/v1/teams", json={"name": name})
|
|
return TeamWithRole.model_validate(handle_response(resp))
|
|
|
|
def get(self, id: str) -> TeamDetail:
|
|
resp = self._require().get(f"/v1/teams/{id}")
|
|
return TeamDetail.model_validate(handle_response(resp))
|
|
|
|
def rename(self, id: str, name: str) -> None:
|
|
resp = self._require().patch(f"/v1/teams/{id}", json={"name": name})
|
|
handle_response(resp)
|
|
|
|
def delete(self, id: str) -> None:
|
|
resp = self._require().delete(f"/v1/teams/{id}")
|
|
handle_response(resp)
|
|
|
|
def list_members(self, id: str) -> list[TeamMember]:
|
|
resp = self._require().get(f"/v1/teams/{id}/members")
|
|
return [TeamMember.model_validate(item) for item in handle_response(resp)]
|
|
|
|
def add_member(self, id: str, email: str) -> TeamMember:
|
|
resp = self._require().post(f"/v1/teams/{id}/members", json={"email": email})
|
|
return TeamMember.model_validate(handle_response(resp))
|
|
|
|
def update_member_role(self, id: str, uid: str, role: str) -> None:
|
|
resp = self._require().patch(
|
|
f"/v1/teams/{id}/members/{uid}", json={"role": role}
|
|
)
|
|
handle_response(resp)
|
|
|
|
def remove_member(self, id: str, uid: str) -> None:
|
|
resp = self._require().delete(f"/v1/teams/{id}/members/{uid}")
|
|
handle_response(resp)
|
|
|
|
def leave(self, id: str) -> None:
|
|
resp = self._require().post(f"/v1/teams/{id}/leave")
|
|
handle_response(resp)
|
|
|
|
|
|
class AsyncTeamsResource:
|
|
"""Async team operations."""
|
|
|
|
def __init__(self, http: httpx.AsyncClient | None) -> None:
|
|
self._http = http
|
|
|
|
def _require(self) -> httpx.AsyncClient:
|
|
return _require(self._http, _MGMT_AUTH_MSG) # type: ignore[return-value]
|
|
|
|
async def list(self) -> list[TeamWithRole]:
|
|
resp = await self._require().get("/v1/teams")
|
|
return [TeamWithRole.model_validate(item) for item in handle_response(resp)]
|
|
|
|
async def create(self, name: str) -> TeamWithRole:
|
|
resp = await self._require().post("/v1/teams", json={"name": name})
|
|
return TeamWithRole.model_validate(handle_response(resp))
|
|
|
|
async def get(self, id: str) -> TeamDetail:
|
|
resp = await self._require().get(f"/v1/teams/{id}")
|
|
return TeamDetail.model_validate(handle_response(resp))
|
|
|
|
async def rename(self, id: str, name: str) -> None:
|
|
resp = await self._require().patch(f"/v1/teams/{id}", json={"name": name})
|
|
handle_response(resp)
|
|
|
|
async def delete(self, id: str) -> None:
|
|
resp = await self._require().delete(f"/v1/teams/{id}")
|
|
handle_response(resp)
|
|
|
|
async def list_members(self, id: str) -> list[TeamMember]:
|
|
resp = await self._require().get(f"/v1/teams/{id}/members")
|
|
return [TeamMember.model_validate(item) for item in handle_response(resp)]
|
|
|
|
async def add_member(self, id: str, email: str) -> TeamMember:
|
|
resp = await self._require().post(
|
|
f"/v1/teams/{id}/members", json={"email": email}
|
|
)
|
|
return TeamMember.model_validate(handle_response(resp))
|
|
|
|
async def update_member_role(self, id: str, uid: str, role: str) -> None:
|
|
resp = await self._require().patch(
|
|
f"/v1/teams/{id}/members/{uid}", json={"role": role}
|
|
)
|
|
handle_response(resp)
|
|
|
|
async def remove_member(self, id: str, uid: str) -> None:
|
|
resp = await self._require().delete(f"/v1/teams/{id}/members/{uid}")
|
|
handle_response(resp)
|
|
|
|
async def leave(self, id: str) -> None:
|
|
resp = await self._require().post(f"/v1/teams/{id}/leave")
|
|
handle_response(resp)
|
|
|
|
|
|
class CapsulesResource:
|
|
"""Sync capsule control-plane operations."""
|
|
|
|
def __init__(
|
|
self,
|
|
http: httpx.Client | None,
|
|
base_url: str,
|
|
api_key: str | None = None,
|
|
token: str | None = None,
|
|
) -> None:
|
|
self._http = http
|
|
self._base_url = base_url
|
|
self._api_key = api_key
|
|
self._token = token
|
|
|
|
def _require(self) -> httpx.Client:
|
|
return _require(self._http, _DATA_AUTH_MSG) # type: ignore[return-value]
|
|
|
|
def create(
|
|
self,
|
|
template: str | None = None,
|
|
vcpus: int | None = None,
|
|
memory_mb: int | None = None,
|
|
timeout_sec: int | None = None,
|
|
) -> Capsule:
|
|
http = self._require()
|
|
payload: dict = {}
|
|
if template is not None:
|
|
payload["template"] = template
|
|
if vcpus is not None:
|
|
payload["vcpus"] = vcpus
|
|
if memory_mb is not None:
|
|
payload["memory_mb"] = memory_mb
|
|
if timeout_sec is not None:
|
|
payload["timeout_sec"] = timeout_sec
|
|
resp = http.post("/v1/capsules", json=payload)
|
|
model = CapsuleModel.model_validate(handle_response(resp))
|
|
cap = Capsule.model_validate(model.model_dump())
|
|
cap._bind(http, self._base_url, self._api_key, self._token)
|
|
return cap
|
|
|
|
def list(self) -> list[CapsuleModel]:
|
|
resp = self._require().get("/v1/capsules")
|
|
return [CapsuleModel.model_validate(item) for item in handle_response(resp)]
|
|
|
|
def get(self, id: str) -> CapsuleModel:
|
|
resp = self._require().get(f"/v1/capsules/{id}")
|
|
return CapsuleModel.model_validate(handle_response(resp))
|
|
|
|
def destroy(self, id: str) -> None:
|
|
resp = self._require().delete(f"/v1/capsules/{id}")
|
|
handle_response(resp)
|
|
|
|
def stats(self, range: str | None = None) -> CapsuleStats:
|
|
params: dict = {}
|
|
if range is not None:
|
|
params["range"] = range
|
|
resp = self._require().get("/v1/capsules/stats", params=params)
|
|
return CapsuleStats.model_validate(handle_response(resp))
|
|
|
|
def usage(
|
|
self,
|
|
from_date: str | None = None,
|
|
to_date: str | None = None,
|
|
) -> UsageResponse:
|
|
params: dict = {}
|
|
if from_date is not None:
|
|
params["from"] = from_date
|
|
if to_date is not None:
|
|
params["to"] = to_date
|
|
resp = self._require().get("/v1/capsules/usage", params=params)
|
|
return UsageResponse.model_validate(handle_response(resp))
|
|
|
|
|
|
class AsyncCapsulesResource:
|
|
"""Async capsule control-plane operations."""
|
|
|
|
def __init__(
|
|
self,
|
|
http: httpx.AsyncClient | None,
|
|
base_url: str,
|
|
api_key: str | None = None,
|
|
token: str | None = None,
|
|
) -> None:
|
|
self._http = http
|
|
self._base_url = base_url
|
|
self._api_key = api_key
|
|
self._token = token
|
|
|
|
def _require(self) -> httpx.AsyncClient:
|
|
return _require(self._http, _DATA_AUTH_MSG) # type: ignore[return-value]
|
|
|
|
async def create(
|
|
self,
|
|
template: str | None = None,
|
|
vcpus: int | None = None,
|
|
memory_mb: int | None = None,
|
|
timeout_sec: int | None = None,
|
|
) -> Capsule:
|
|
http = self._require()
|
|
payload: dict = {}
|
|
if template is not None:
|
|
payload["template"] = template
|
|
if vcpus is not None:
|
|
payload["vcpus"] = vcpus
|
|
if memory_mb is not None:
|
|
payload["memory_mb"] = memory_mb
|
|
if timeout_sec is not None:
|
|
payload["timeout_sec"] = timeout_sec
|
|
resp = await http.post("/v1/capsules", json=payload)
|
|
model = CapsuleModel.model_validate(handle_response(resp))
|
|
cap = Capsule.model_validate(model.model_dump())
|
|
cap._bind(http, self._base_url, self._api_key, self._token)
|
|
return cap
|
|
|
|
async def list(self) -> list[CapsuleModel]:
|
|
resp = await self._require().get("/v1/capsules")
|
|
return [CapsuleModel.model_validate(item) for item in handle_response(resp)]
|
|
|
|
async def get(self, id: str) -> CapsuleModel:
|
|
resp = await self._require().get(f"/v1/capsules/{id}")
|
|
return CapsuleModel.model_validate(handle_response(resp))
|
|
|
|
async def destroy(self, id: str) -> None:
|
|
resp = await self._require().delete(f"/v1/capsules/{id}")
|
|
handle_response(resp)
|
|
|
|
async def stats(self, range: str | None = None) -> CapsuleStats:
|
|
params: dict = {}
|
|
if range is not None:
|
|
params["range"] = range
|
|
resp = await self._require().get("/v1/capsules/stats", params=params)
|
|
return CapsuleStats.model_validate(handle_response(resp))
|
|
|
|
async def usage(
|
|
self,
|
|
from_date: str | None = None,
|
|
to_date: str | None = None,
|
|
) -> UsageResponse:
|
|
params: dict = {}
|
|
if from_date is not None:
|
|
params["from"] = from_date
|
|
if to_date is not None:
|
|
params["to"] = to_date
|
|
resp = await self._require().get("/v1/capsules/usage", params=params)
|
|
return UsageResponse.model_validate(handle_response(resp))
|
|
|
|
|
|
class SnapshotsResource:
|
|
"""Sync snapshot operations."""
|
|
|
|
def __init__(self, http: httpx.Client | None) -> None:
|
|
self._http = http
|
|
|
|
def _require(self) -> httpx.Client:
|
|
return _require(self._http, _DATA_AUTH_MSG) # type: ignore[return-value]
|
|
|
|
def create(
|
|
self,
|
|
capsule_id: str,
|
|
name: str | None = None,
|
|
overwrite: bool = False,
|
|
) -> Template:
|
|
payload: dict = {"sandbox_id": capsule_id}
|
|
if name is not None:
|
|
payload["name"] = name
|
|
params: dict = {}
|
|
if overwrite:
|
|
params["overwrite"] = "true"
|
|
resp = self._require().post("/v1/snapshots", json=payload, params=params)
|
|
return Template.model_validate(handle_response(resp))
|
|
|
|
def list(self, type: str | None = None) -> list[Template]:
|
|
params: dict = {}
|
|
if type is not None:
|
|
params["type"] = type
|
|
resp = self._require().get("/v1/snapshots", params=params)
|
|
return [Template.model_validate(item) for item in handle_response(resp)]
|
|
|
|
def delete(self, name: str) -> None:
|
|
resp = self._require().delete(f"/v1/snapshots/{name}")
|
|
handle_response(resp)
|
|
|
|
|
|
class AsyncSnapshotsResource:
|
|
"""Async snapshot operations."""
|
|
|
|
def __init__(self, http: httpx.AsyncClient | None) -> None:
|
|
self._http = http
|
|
|
|
def _require(self) -> httpx.AsyncClient:
|
|
return _require(self._http, _DATA_AUTH_MSG) # type: ignore[return-value]
|
|
|
|
async def create(
|
|
self,
|
|
capsule_id: str,
|
|
name: str | None = None,
|
|
overwrite: bool = False,
|
|
) -> Template:
|
|
payload: dict = {"sandbox_id": capsule_id}
|
|
if name is not None:
|
|
payload["name"] = name
|
|
params: dict = {}
|
|
if overwrite:
|
|
params["overwrite"] = "true"
|
|
resp = await self._require().post("/v1/snapshots", json=payload, params=params)
|
|
return Template.model_validate(handle_response(resp))
|
|
|
|
async def list(self, type: str | None = None) -> list[Template]:
|
|
params: dict = {}
|
|
if type is not None:
|
|
params["type"] = type
|
|
resp = await self._require().get("/v1/snapshots", params=params)
|
|
return [Template.model_validate(item) for item in handle_response(resp)]
|
|
|
|
async def delete(self, name: str) -> None:
|
|
resp = await self._require().delete(f"/v1/snapshots/{name}")
|
|
handle_response(resp)
|
|
|
|
|
|
class HostsResource:
|
|
"""Sync host operations."""
|
|
|
|
def __init__(self, http: httpx.Client | None) -> None:
|
|
self._http = http
|
|
|
|
def _require(self) -> httpx.Client:
|
|
return _require(self._http, _MGMT_AUTH_MSG) # type: ignore[return-value]
|
|
|
|
def create(
|
|
self,
|
|
type: str,
|
|
team_id: str | None = None,
|
|
provider: str | None = None,
|
|
availability_zone: str | None = None,
|
|
) -> CreateHostResponse:
|
|
payload: dict = {"type": type}
|
|
if team_id is not None:
|
|
payload["team_id"] = team_id
|
|
if provider is not None:
|
|
payload["provider"] = provider
|
|
if availability_zone is not None:
|
|
payload["availability_zone"] = availability_zone
|
|
resp = self._require().post("/v1/hosts", json=payload)
|
|
return CreateHostResponse.model_validate(handle_response(resp))
|
|
|
|
def list(self) -> list[Host]:
|
|
resp = self._require().get("/v1/hosts")
|
|
return [Host.model_validate(item) for item in handle_response(resp)]
|
|
|
|
def get(self, id: str) -> Host:
|
|
resp = self._require().get(f"/v1/hosts/{id}")
|
|
return Host.model_validate(handle_response(resp))
|
|
|
|
def delete(self, id: str, force: bool = False) -> None:
|
|
params: dict = {}
|
|
if force:
|
|
params["force"] = "true"
|
|
resp = self._require().delete(f"/v1/hosts/{id}", params=params)
|
|
handle_response(resp)
|
|
|
|
def regenerate_token(self, id: str) -> CreateHostResponse:
|
|
resp = self._require().post(f"/v1/hosts/{id}/token")
|
|
return CreateHostResponse.model_validate(handle_response(resp))
|
|
|
|
def delete_preview(self, id: str) -> HostDeletePreview:
|
|
resp = self._require().get(f"/v1/hosts/{id}/delete-preview")
|
|
return HostDeletePreview.model_validate(handle_response(resp))
|
|
|
|
def list_tags(self, id: str) -> builtins.list[str]:
|
|
resp = self._require().get(f"/v1/hosts/{id}/tags")
|
|
return cast(builtins.list[str], handle_response(resp))
|
|
|
|
def add_tag(self, id: str, tag: str) -> None:
|
|
resp = self._require().post(f"/v1/hosts/{id}/tags", json={"tag": tag})
|
|
handle_response(resp)
|
|
|
|
def remove_tag(self, id: str, tag: str) -> None:
|
|
resp = self._require().delete(f"/v1/hosts/{id}/tags/{tag}")
|
|
handle_response(resp)
|
|
|
|
|
|
class AsyncHostsResource:
|
|
"""Async host operations."""
|
|
|
|
def __init__(self, http: httpx.AsyncClient | None) -> None:
|
|
self._http = http
|
|
|
|
def _require(self) -> httpx.AsyncClient:
|
|
return _require(self._http, _MGMT_AUTH_MSG) # type: ignore[return-value]
|
|
|
|
async def create(
|
|
self,
|
|
type: str,
|
|
team_id: str | None = None,
|
|
provider: str | None = None,
|
|
availability_zone: str | None = None,
|
|
) -> CreateHostResponse:
|
|
payload: dict = {"type": type}
|
|
if team_id is not None:
|
|
payload["team_id"] = team_id
|
|
if provider is not None:
|
|
payload["provider"] = provider
|
|
if availability_zone is not None:
|
|
payload["availability_zone"] = availability_zone
|
|
resp = await self._require().post("/v1/hosts", json=payload)
|
|
return CreateHostResponse.model_validate(handle_response(resp))
|
|
|
|
async def list(self) -> list[Host]:
|
|
resp = await self._require().get("/v1/hosts")
|
|
return [Host.model_validate(item) for item in handle_response(resp)]
|
|
|
|
async def get(self, id: str) -> Host:
|
|
resp = await self._require().get(f"/v1/hosts/{id}")
|
|
return Host.model_validate(handle_response(resp))
|
|
|
|
async def delete(self, id: str, force: bool = False) -> None:
|
|
params: dict = {}
|
|
if force:
|
|
params["force"] = "true"
|
|
resp = await self._require().delete(f"/v1/hosts/{id}", params=params)
|
|
handle_response(resp)
|
|
|
|
async def regenerate_token(self, id: str) -> CreateHostResponse:
|
|
resp = await self._require().post(f"/v1/hosts/{id}/token")
|
|
return CreateHostResponse.model_validate(handle_response(resp))
|
|
|
|
async def delete_preview(self, id: str) -> HostDeletePreview:
|
|
resp = await self._require().get(f"/v1/hosts/{id}/delete-preview")
|
|
return HostDeletePreview.model_validate(handle_response(resp))
|
|
|
|
async def list_tags(self, id: str) -> builtins.list[str]:
|
|
resp = await self._require().get(f"/v1/hosts/{id}/tags")
|
|
return cast(builtins.list[str], handle_response(resp))
|
|
|
|
async def add_tag(self, id: str, tag: str) -> None:
|
|
resp = await self._require().post(f"/v1/hosts/{id}/tags", json={"tag": tag})
|
|
handle_response(resp)
|
|
|
|
async def remove_tag(self, id: str, tag: str) -> None:
|
|
resp = await self._require().delete(f"/v1/hosts/{id}/tags/{tag}")
|
|
handle_response(resp)
|
|
|
|
|
|
class ChannelsResource:
|
|
"""Sync notification channel operations."""
|
|
|
|
def __init__(self, http: httpx.Client | None) -> None:
|
|
self._http = http
|
|
|
|
def _require(self) -> httpx.Client:
|
|
return _require(self._http, _MGMT_AUTH_MSG) # type: ignore[return-value]
|
|
|
|
def create(self, request: CreateChannelRequest) -> ChannelResponse:
|
|
resp = self._require().post(
|
|
"/v1/channels", json=request.model_dump(mode="json", exclude_none=True)
|
|
)
|
|
return ChannelResponse.model_validate(handle_response(resp))
|
|
|
|
def list(self) -> list[ChannelResponse]:
|
|
resp = self._require().get("/v1/channels")
|
|
return [ChannelResponse.model_validate(item) for item in handle_response(resp)]
|
|
|
|
def test(self, request: TestChannelRequest) -> dict:
|
|
resp = self._require().post(
|
|
"/v1/channels/test", json=request.model_dump(mode="json", exclude_none=True)
|
|
)
|
|
return handle_response(resp)
|
|
|
|
def get(self, id: str) -> ChannelResponse:
|
|
resp = self._require().get(f"/v1/channels/{id}")
|
|
return ChannelResponse.model_validate(handle_response(resp))
|
|
|
|
def update(self, id: str, request: UpdateChannelRequest) -> ChannelResponse:
|
|
resp = self._require().patch(
|
|
f"/v1/channels/{id}",
|
|
json=request.model_dump(mode="json", exclude_none=True),
|
|
)
|
|
return ChannelResponse.model_validate(handle_response(resp))
|
|
|
|
def delete(self, id: str) -> None:
|
|
resp = self._require().delete(f"/v1/channels/{id}")
|
|
handle_response(resp)
|
|
|
|
def rotate_config(self, id: str, request: RotateConfigRequest) -> ChannelResponse:
|
|
resp = self._require().put(
|
|
f"/v1/channels/{id}/config",
|
|
json=request.model_dump(mode="json", exclude_none=True),
|
|
)
|
|
return ChannelResponse.model_validate(handle_response(resp))
|
|
|
|
|
|
class AsyncChannelsResource:
|
|
"""Async notification channel operations."""
|
|
|
|
def __init__(self, http: httpx.AsyncClient | None) -> None:
|
|
self._http = http
|
|
|
|
def _require(self) -> httpx.AsyncClient:
|
|
return _require(self._http, _MGMT_AUTH_MSG) # type: ignore[return-value]
|
|
|
|
async def create(self, request: CreateChannelRequest) -> ChannelResponse:
|
|
resp = await self._require().post(
|
|
"/v1/channels", json=request.model_dump(mode="json", exclude_none=True)
|
|
)
|
|
return ChannelResponse.model_validate(handle_response(resp))
|
|
|
|
async def list(self) -> list[ChannelResponse]:
|
|
resp = await self._require().get("/v1/channels")
|
|
return [ChannelResponse.model_validate(item) for item in handle_response(resp)]
|
|
|
|
async def test(self, request: TestChannelRequest) -> dict:
|
|
resp = await self._require().post(
|
|
"/v1/channels/test", json=request.model_dump(mode="json", exclude_none=True)
|
|
)
|
|
return handle_response(resp)
|
|
|
|
async def get(self, id: str) -> ChannelResponse:
|
|
resp = await self._require().get(f"/v1/channels/{id}")
|
|
return ChannelResponse.model_validate(handle_response(resp))
|
|
|
|
async def update(self, id: str, request: UpdateChannelRequest) -> ChannelResponse:
|
|
resp = await self._require().patch(
|
|
f"/v1/channels/{id}",
|
|
json=request.model_dump(mode="json", exclude_none=True),
|
|
)
|
|
return ChannelResponse.model_validate(handle_response(resp))
|
|
|
|
async def delete(self, id: str) -> None:
|
|
resp = await self._require().delete(f"/v1/channels/{id}")
|
|
handle_response(resp)
|
|
|
|
async def rotate_config(
|
|
self, id: str, request: RotateConfigRequest
|
|
) -> ChannelResponse:
|
|
resp = await self._require().put(
|
|
f"/v1/channels/{id}/config",
|
|
json=request.model_dump(mode="json", exclude_none=True),
|
|
)
|
|
return ChannelResponse.model_validate(handle_response(resp))
|
|
|
|
|
|
def _make_client(base_url: str, headers: dict[str, str]) -> httpx.Client:
|
|
return httpx.Client(base_url=base_url, headers=headers)
|
|
|
|
|
|
def _make_async_client(base_url: str, headers: dict[str, str]) -> httpx.AsyncClient:
|
|
return httpx.AsyncClient(base_url=base_url, headers=headers)
|
|
|
|
|
|
class WrennClient:
|
|
"""Synchronous client for the Wrenn API.
|
|
|
|
Authenticate with an API key, a JWT token, or both.
|
|
|
|
- ``api_key``: for capsule and snapshot operations (sent as ``X-API-Key``).
|
|
- ``token``: for management operations like account, teams, hosts
|
|
(sent as ``Authorization: Bearer``).
|
|
|
|
Args:
|
|
api_key: API key (``wrn_...``).
|
|
token: JWT token.
|
|
base_url: Wrenn Control Plane URL.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
api_key: str | None = None,
|
|
token: str | None = None,
|
|
base_url: str = DEFAULT_BASE_URL,
|
|
) -> None:
|
|
self._api_key = api_key
|
|
self._token = token
|
|
self._base_url = base_url
|
|
|
|
self._public_http = _make_client(base_url, {})
|
|
self._mgmt_http: httpx.Client | None = None
|
|
if token:
|
|
self._mgmt_http = _make_client(
|
|
base_url, {"Authorization": f"Bearer {token}"}
|
|
)
|
|
self._data_http: httpx.Client | None = None
|
|
if api_key:
|
|
self._data_http = _make_client(base_url, {"X-API-Key": api_key})
|
|
|
|
self.auth = AuthResource(self._public_http, self._mgmt_http)
|
|
self.account = AccountResource(self._public_http, self._mgmt_http)
|
|
self.api_keys = APIKeysResource(self._mgmt_http)
|
|
self.users = UsersResource(self._mgmt_http)
|
|
self.teams = TeamsResource(self._mgmt_http)
|
|
self.capsules = CapsulesResource(self._data_http, base_url, api_key, token)
|
|
self.snapshots = SnapshotsResource(self._data_http)
|
|
self.hosts = HostsResource(self._mgmt_http)
|
|
self.channels = ChannelsResource(self._mgmt_http)
|
|
|
|
@property
|
|
def sandboxes(self) -> CapsulesResource:
|
|
warnings.warn(
|
|
"'client.sandboxes' is deprecated, use 'client.capsules' instead",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
return self.capsules
|
|
|
|
def close(self) -> None:
|
|
"""Close the underlying HTTP connection pool(s)."""
|
|
self._public_http.close()
|
|
if self._mgmt_http is not None:
|
|
self._mgmt_http.close()
|
|
if self._data_http is not None:
|
|
self._data_http.close()
|
|
|
|
def __enter__(self) -> WrennClient:
|
|
return self
|
|
|
|
def __exit__(
|
|
self,
|
|
exc_type: type[BaseException] | None,
|
|
exc_val: BaseException | None,
|
|
exc_tb: object,
|
|
) -> None:
|
|
self.close()
|
|
|
|
|
|
class AsyncWrennClient:
|
|
"""Asynchronous client for the Wrenn API.
|
|
|
|
Authenticate with an API key, a JWT token, or both.
|
|
|
|
- ``api_key``: for capsule and snapshot operations (sent as ``X-API-Key``).
|
|
- ``token``: for management operations like account, teams, hosts
|
|
(sent as ``Authorization: Bearer``).
|
|
|
|
Args:
|
|
api_key: API key (``wrn_...``).
|
|
token: JWT token.
|
|
base_url: Wrenn Control Plane URL.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
api_key: str | None = None,
|
|
token: str | None = None,
|
|
base_url: str = DEFAULT_BASE_URL,
|
|
) -> None:
|
|
self._api_key = api_key
|
|
self._token = token
|
|
self._base_url = base_url
|
|
|
|
self._public_http = _make_async_client(base_url, {})
|
|
self._mgmt_http: httpx.AsyncClient | None = None
|
|
if token:
|
|
self._mgmt_http = _make_async_client(
|
|
base_url, {"Authorization": f"Bearer {token}"}
|
|
)
|
|
self._data_http: httpx.AsyncClient | None = None
|
|
if api_key:
|
|
self._data_http = _make_async_client(base_url, {"X-API-Key": api_key})
|
|
|
|
self.auth = AsyncAuthResource(self._public_http, self._mgmt_http)
|
|
self.account = AsyncAccountResource(self._public_http, self._mgmt_http)
|
|
self.api_keys = AsyncAPIKeysResource(self._mgmt_http)
|
|
self.users = AsyncUsersResource(self._mgmt_http)
|
|
self.teams = AsyncTeamsResource(self._mgmt_http)
|
|
self.capsules = AsyncCapsulesResource(self._data_http, base_url, api_key, token)
|
|
self.snapshots = AsyncSnapshotsResource(self._data_http)
|
|
self.hosts = AsyncHostsResource(self._mgmt_http)
|
|
self.channels = AsyncChannelsResource(self._mgmt_http)
|
|
|
|
@property
|
|
def sandboxes(self) -> AsyncCapsulesResource:
|
|
warnings.warn(
|
|
"'client.sandboxes' is deprecated, use 'client.capsules' instead",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
return self.capsules
|
|
|
|
async def aclose(self) -> None:
|
|
"""Close the underlying async HTTP connection pool(s)."""
|
|
await self._public_http.aclose()
|
|
if self._mgmt_http is not None:
|
|
await self._mgmt_http.aclose()
|
|
if self._data_http is not None:
|
|
await self._data_http.aclose()
|
|
|
|
async def __aenter__(self) -> AsyncWrennClient:
|
|
return self
|
|
|
|
async def __aexit__(
|
|
self,
|
|
exc_type: type[BaseException] | None,
|
|
exc_val: BaseException | None,
|
|
exc_tb: object,
|
|
) -> None:
|
|
await self.aclose()
|