"""Sync daemon client for the web UI. Communicates with vacuum-walld over a Unix socket using requests-unixsocket. """ import json import logging from typing import Any import requests import requests_unixsocket logger = logging.getLogger(__name__) class NotFound(Exception): """Raised when the daemon returns HTTP 404.""" pass class BadRequest(Exception): """Raised when the daemon returns HTTP 400.""" pass _DEFAULT_SOCKET = None def _get_socket_path() -> str: global _DEFAULT_SOCKET if _DEFAULT_SOCKET is None: import os from pathlib import Path project_dir = Path(__file__).resolve().parent.parent socket_env = os.environ.get( "VACUUM_WALLD_SOCKET", str(project_dir / "data" / "daemon.sock") ) _DEFAULT_SOCKET = socket_env return _DEFAULT_SOCKET def set_socket_path(path: str) -> None: global _DEFAULT_SOCKET _DEFAULT_SOCKET = path def request( method: str, path: str, json_body: dict[str, Any] | None = None, query_params: dict[str, Any] | None = None, socket_path: str | None = None, timeout: float = 30, ) -> dict[str, Any]: """Make a request to the daemon and return the parsed response body. For GET requests, query_params are sent as URL query parameters instead of a JSON body. For other methods, json_body is sent as JSON. Raises RuntimeError on non-2xx responses or connection errors. Raises NotFound on HTTP 404. Raises BadRequest on HTTP 400. """ sp = socket_path or _get_socket_path() url = f"http://localhost{path}" sess = requests_unixsocket.Session() try: kwargs: dict[str, Any] = { "timeout": timeout, "unix_socket": sp, } if method == "GET": if query_params: kwargs["params"] = query_params else: if json_body is not None: kwargs["json"] = json_body resp = sess.request( method, url, **kwargs, ) resp.raise_for_status() try: data = resp.json() except json.JSONDecodeError as exc: raise RuntimeError( f"Daemon returned non-JSON response ({resp.status_code}): {exc}" ) from exc except requests.ConnectionError as exc: raise RuntimeError(f"Cannot connect to daemon at {sp}: {exc}") from exc except requests.Timeout as exc: raise RuntimeError(f"Daemon request timed out: {method} {path}") from exc except requests.HTTPError as exc: try: data = resp.json() except Exception: data = {"ok": False, "error": resp.text} if not data.get("ok"): if resp.status_code == 404: raise NotFound(data.get("error", str(exc))) from exc if resp.status_code == 400: raise BadRequest(data.get("error", str(exc))) from exc raise RuntimeError(data.get("error", str(exc))) from exc if not data.get("ok"): raise RuntimeError(data.get("error", "Unknown error")) return data.get("data") def get(path: str, params: dict[str, Any] | None = None, **kwargs: Any) -> Any: """GET request to daemon. Params are sent as URL query parameters.""" return request("GET", path, query_params=params, **kwargs) def post(path: str, body: dict[str, Any] | None = None, **kwargs: Any) -> Any: """POST request to daemon.""" return request("POST", path, json_body=body, **kwargs) def patch(path: str, body: dict[str, Any] | None = None, **kwargs: Any) -> Any: """PATCH request to daemon.""" return request("PATCH", path, json_body=body, **kwargs) def delete(path: str, body: dict[str, Any] | None = None, **kwargs: Any) -> Any: """DELETE request to daemon.""" return request("DELETE", path, json_body=body, **kwargs) def batch(ops: list[dict[str, Any]], **kwargs: Any) -> dict[str, Any]: """Batch request to daemon. Each op is a dict with 'id', 'method', 'path', and optionally 'body'. Returns a dict mapping each id to its result. """ return post("/batch", {"ops": ops}, **kwargs)