Files
Artem Kokos b934600380 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
2026-05-27 22:26:51 +07:00

185 lines
5.9 KiB
Python

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 IgnisClient:
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.Client(
base_url=self._base,
headers={"X-API-Key": self._api_key},
timeout=httpx.Timeout(timeout),
follow_redirects=True,
)
def close(self):
self._client.close()
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
def _request(self, method: str, path: str, **kwargs) -> Any:
resp = 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()
def _get(self, path: str, **kwargs) -> Any:
return self._request("GET", path, **kwargs)
def _post(self, path: str, **kwargs) -> Any:
return self._request("POST", path, **kwargs)
def _delete(self, path: str, **kwargs) -> Any:
return self._request("DELETE", path, **kwargs)
# ------------------------------------------------------------------ Auth
def auth_me(self) -> dict[str, Any]:
return self._get("/auth/me")
# ------------------------------------------------------------------ System
def system_info(self) -> ServerInfoResponse:
data = self._get("/system/info")
return ServerInfoResponse(**data)
# ------------------------------------------------------------------ Devices
def list_devices(self) -> dict[str, dict[str, str]]:
return self._get("/devices")
def list_groups(self) -> dict[str, dict[str, Any]]:
return self._get("/devices/groups")
def list_scenes(self) -> dict[str, int]:
return dict(SCENES)
def create_group(self, group_id: str, name: str, macs: list[str]) -> dict[str, Any]:
return self._post(
"/devices/groups", json={"id": group_id, "name": name, "macs": macs}
)
def delete_group(self, group_id: str) -> dict[str, Any]:
return self._delete(f"/devices/groups/{group_id}")
def rescan(self) -> RescanResponse:
data = self._post("/devices/rescan")
return RescanResponse(**data)
# ------------------------------------------------------------------ Control
def control_device(
self, device_id: str, payload: CommandRequest
) -> DeviceControlResponse:
data = self._post(
f"/control/device/{device_id}",
json=payload.model_dump(exclude_none=True),
)
return DeviceControlResponse(**data)
def control_group(
self, group_id: str, payload: CommandRequest
) -> GroupControlResponse:
data = self._post(
f"/control/group/{group_id}",
json=payload.model_dump(exclude_none=True),
)
return GroupControlResponse(**data)
def blink_device(self, device_id: str) -> BlinkResponse:
data = self._post(f"/control/device/{device_id}/blink")
return BlinkResponse(**data)
def device_status(self, device_id: str) -> DeviceStatusResponse:
data = self._get(f"/control/device/{device_id}/status")
return DeviceStatusResponse(**data)
def group_status(self, group_id: str) -> GroupStatusResponse:
data = self._get(f"/control/group/{group_id}/status")
return GroupStatusResponse(**data)
# --------------------------------------------------------------- Schedules
def create_once_schedule(
self, payload: ScheduleOnceRequest
) -> ScheduleCreateResponse:
data = self._post(
"/schedules/once",
json=payload.model_dump(exclude_none=True),
)
return ScheduleCreateResponse(**data)
def create_cron_schedule(
self, payload: ScheduleCronRequest
) -> ScheduleCreateResponse:
data = self._post(
"/schedules/cron",
json=payload.model_dump(exclude_none=True),
)
return ScheduleCreateResponse(**data)
def list_schedules(self) -> ScheduleTasksResponse:
data = self._get("/schedules/tasks")
return ScheduleTasksResponse(**data)
def delete_schedule(self, job_id: str) -> DeleteStatusResponse:
data = self._delete(f"/schedules/{job_id}")
return DeleteStatusResponse(**data)
# ------------------------------------------------------------------- Stats
def stats_summary(self, days: int = 7) -> dict[str, Any]:
return self._get("/stats/summary", params={"days": days})
def stats_log(self, limit: int = 50) -> list[dict[str, Any]]:
return self._get("/stats/log", params={"limit": limit})
# ---------------------------------------------------------------- API Keys
def list_api_keys(self) -> list[dict[str, Any]]:
return self._get("/api-keys")
def create_api_key(self, name: str, *, is_admin: bool = False) -> dict[str, Any]:
return self._post("/api-keys", params={"name": name, "is_admin": is_admin})
def revoke_api_key(self, key_or_id: str) -> dict[str, Any]:
return self._post(
"/api-keys/revoke", json=KeyActionRequest(key=key_or_id).model_dump()
)
def activate_api_key(self, key_or_id: str) -> dict[str, Any]:
return self._post(
"/api-keys/activate", json=KeyActionRequest(key=key_or_id).model_dump()
)