Files
ros_control/app/ros/client.py
T

68 lines
2.7 KiB
Python
Raw Normal View History

import json as jsonlib
import httpx
class RosError(Exception):
"""Ошибка обращения к устройству RouterOS. status=None — сбой на уровне соединения."""
def __init__(self, message: str, status: int | None = None):
super().__init__(message)
self.status = status
class RosClient:
"""Тонкий клиент RouterOS REST API (https://<host>:<port>/rest/...)."""
def __init__(
self,
host: str,
port: int,
username: str,
password: str,
verify_tls: bool = False,
timeout: float = 30.0,
use_tls: bool = True,
transport: httpx.AsyncBaseTransport | None = None,
connect_timeout: float = 4.0,
):
self._connect = connect_timeout
self._http = httpx.AsyncClient(
base_url=f"{'https' if use_tls else 'http'}://{host}:{port}/rest",
auth=(username, password),
verify=verify_tls,
timeout=httpx.Timeout(timeout, connect=connect_timeout),
transport=transport,
)
async def __aenter__(self) -> "RosClient":
return self
async def __aexit__(self, *exc) -> None:
await self._http.aclose()
async def request(self, method: str, path: str, json: dict | None = None,
params: dict | None = None, timeout: float | None = None):
try:
kwargs = {"timeout": httpx.Timeout(timeout, connect=self._connect)} if timeout else {}
r = await self._http.request(method, path, json=json, params=params, **kwargs)
except httpx.HTTPError as e:
raise RosError(f"{self._http.base_url}: {type(e).__name__}: {e or repr(e.__cause__)}") from e
if r.status_code >= 400:
try:
body = jsonlib.loads(r.content.decode("utf-8", "replace"))
detail = body.get("detail") or body.get("message") or r.text
except ValueError:
detail = r.text
raise RosError(f"HTTP {r.status_code}: {detail}", status=r.status_code)
# RouterOS может отдавать имена файлов не в UTF-8 — декодируем с заменой, а не падаем
return jsonlib.loads(r.content.decode("utf-8", "replace")) if r.content else None
async def get(self, path: str, **params):
return await self.request("GET", path, params=params or None)
async def post(self, path: str, json: dict | None = None, timeout: float | None = None):
return await self.request("POST", path, json=json or {}, timeout=timeout)
async def delete(self, path: str):
return await self.request("DELETE", path)