Initial commit: Ignis Client Python
- Sync and async HTTP clients for Ignis Core WiZ server - 23 endpoints: auth, devices, groups, control, schedules, stats, API keys - Pydantic models with client-side validation - 108 unit tests - README with role table and usage examples
This commit is contained in:
190
ignis_client/async_client.py
Normal file
190
ignis_client/async_client.py
Normal file
@@ -0,0 +1,190 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from .models import (
|
||||
SCENES,
|
||||
BlinkResponse,
|
||||
CommandRequest,
|
||||
DeleteStatusResponse,
|
||||
DeviceControlResponse,
|
||||
DeviceStatusResponse,
|
||||
GroupControlResponse,
|
||||
GroupStatusResponse,
|
||||
IgnisError,
|
||||
KeyActionRequest,
|
||||
RescanResponse,
|
||||
ScheduleCreateResponse,
|
||||
ScheduleCronRequest,
|
||||
ScheduleOnceRequest,
|
||||
ScheduleTasksResponse,
|
||||
ServerInfoResponse,
|
||||
)
|
||||
|
||||
|
||||
class AsyncIgnisClient:
|
||||
def __init__(self, base_url: str, api_key: str, *, timeout: float = 10.0):
|
||||
self._base = base_url.rstrip("/")
|
||||
self._api_key = api_key
|
||||
self._client = httpx.AsyncClient(
|
||||
base_url=self._base,
|
||||
headers={"X-API-Key": self._api_key},
|
||||
timeout=httpx.Timeout(timeout),
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
async def close(self):
|
||||
await self._client.aclose()
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *args):
|
||||
await self.close()
|
||||
|
||||
async def _request(self, method: str, path: str, **kwargs) -> Any:
|
||||
resp = await self._client.request(method, path, **kwargs)
|
||||
if resp.status_code >= 400:
|
||||
try:
|
||||
detail = resp.json().get("detail", resp.text)
|
||||
except Exception:
|
||||
detail = resp.text or f"HTTP {resp.status_code}"
|
||||
raise IgnisError(resp.status_code, detail)
|
||||
return resp.json()
|
||||
|
||||
async def _get(self, path: str, **kwargs) -> Any:
|
||||
return await self._request("GET", path, **kwargs)
|
||||
|
||||
async def _post(self, path: str, **kwargs) -> Any:
|
||||
return await self._request("POST", path, **kwargs)
|
||||
|
||||
async def _delete(self, path: str, **kwargs) -> Any:
|
||||
return await self._request("DELETE", path, **kwargs)
|
||||
|
||||
# ------------------------------------------------------------------ Auth
|
||||
|
||||
async def auth_me(self) -> dict[str, Any]:
|
||||
return await self._get("/auth/me")
|
||||
|
||||
# ------------------------------------------------------------------ System
|
||||
|
||||
async def system_info(self) -> ServerInfoResponse:
|
||||
data = await self._get("/system/info")
|
||||
return ServerInfoResponse(**data)
|
||||
|
||||
# ------------------------------------------------------------------ Devices
|
||||
|
||||
async def list_devices(self) -> dict[str, dict[str, str]]:
|
||||
return await self._get("/devices")
|
||||
|
||||
async def list_groups(self) -> dict[str, dict[str, Any]]:
|
||||
return await self._get("/devices/groups")
|
||||
|
||||
async def list_scenes(self) -> dict[str, int]:
|
||||
return dict(SCENES)
|
||||
|
||||
async def create_group(
|
||||
self, group_id: str, name: str, macs: list[str]
|
||||
) -> dict[str, Any]:
|
||||
return await self._post(
|
||||
"/devices/groups", json={"id": group_id, "name": name, "macs": macs}
|
||||
)
|
||||
|
||||
async def delete_group(self, group_id: str) -> dict[str, Any]:
|
||||
return await self._delete(f"/devices/groups/{group_id}")
|
||||
|
||||
async def rescan(self) -> RescanResponse:
|
||||
data = await self._post("/devices/rescan")
|
||||
return RescanResponse(**data)
|
||||
|
||||
# ------------------------------------------------------------------ Control
|
||||
|
||||
async def control_device(
|
||||
self, device_id: str, payload: CommandRequest
|
||||
) -> DeviceControlResponse:
|
||||
data = await self._post(
|
||||
f"/control/device/{device_id}",
|
||||
json=payload.model_dump(exclude_none=True),
|
||||
)
|
||||
return DeviceControlResponse(**data)
|
||||
|
||||
async def control_group(
|
||||
self, group_id: str, payload: CommandRequest
|
||||
) -> GroupControlResponse:
|
||||
data = await self._post(
|
||||
f"/control/group/{group_id}",
|
||||
json=payload.model_dump(exclude_none=True),
|
||||
)
|
||||
return GroupControlResponse(**data)
|
||||
|
||||
async def blink_device(self, device_id: str) -> BlinkResponse:
|
||||
data = await self._post(f"/control/device/{device_id}/blink")
|
||||
return BlinkResponse(**data)
|
||||
|
||||
async def device_status(self, device_id: str) -> DeviceStatusResponse:
|
||||
data = await self._get(f"/control/device/{device_id}/status")
|
||||
return DeviceStatusResponse(**data)
|
||||
|
||||
async def group_status(self, group_id: str) -> GroupStatusResponse:
|
||||
data = await self._get(f"/control/group/{group_id}/status")
|
||||
return GroupStatusResponse(**data)
|
||||
|
||||
# --------------------------------------------------------------- Schedules
|
||||
|
||||
async def create_once_schedule(
|
||||
self, payload: ScheduleOnceRequest
|
||||
) -> ScheduleCreateResponse:
|
||||
data = await self._post(
|
||||
"/schedules/once",
|
||||
json=payload.model_dump(exclude_none=True),
|
||||
)
|
||||
return ScheduleCreateResponse(**data)
|
||||
|
||||
async def create_cron_schedule(
|
||||
self, payload: ScheduleCronRequest
|
||||
) -> ScheduleCreateResponse:
|
||||
data = await self._post(
|
||||
"/schedules/cron",
|
||||
json=payload.model_dump(exclude_none=True),
|
||||
)
|
||||
return ScheduleCreateResponse(**data)
|
||||
|
||||
async def list_schedules(self) -> ScheduleTasksResponse:
|
||||
data = await self._get("/schedules/tasks")
|
||||
return ScheduleTasksResponse(**data)
|
||||
|
||||
async def delete_schedule(self, job_id: str) -> DeleteStatusResponse:
|
||||
data = await self._delete(f"/schedules/{job_id}")
|
||||
return DeleteStatusResponse(**data)
|
||||
|
||||
# ------------------------------------------------------------------- Stats
|
||||
|
||||
async def stats_summary(self, days: int = 7) -> dict[str, Any]:
|
||||
return await self._get("/stats/summary", params={"days": days})
|
||||
|
||||
async def stats_log(self, limit: int = 50) -> list[dict[str, Any]]:
|
||||
return await self._get("/stats/log", params={"limit": limit})
|
||||
|
||||
# ---------------------------------------------------------------- API Keys
|
||||
|
||||
async def list_api_keys(self) -> list[dict[str, Any]]:
|
||||
return await self._get("/api-keys")
|
||||
|
||||
async def create_api_key(
|
||||
self, name: str, *, is_admin: bool = False
|
||||
) -> dict[str, Any]:
|
||||
return await self._post(
|
||||
"/api-keys", params={"name": name, "is_admin": is_admin}
|
||||
)
|
||||
|
||||
async def revoke_api_key(self, key_or_id: str) -> dict[str, Any]:
|
||||
return await self._post(
|
||||
"/api-keys/revoke", json=KeyActionRequest(key=key_or_id).model_dump()
|
||||
)
|
||||
|
||||
async def activate_api_key(self, key_or_id: str) -> dict[str, Any]:
|
||||
return await self._post(
|
||||
"/api-keys/activate", json=KeyActionRequest(key=key_or_id).model_dump()
|
||||
)
|
||||
Reference in New Issue
Block a user