"""HTTP-клиент хаба: для MCP, тестов и скриптов. Ошибки API → HubError, недоступный хаб → HubUnavailable.""" import json import urllib.error import urllib.parse import urllib.request from . import db, paths, settings class HubError(Exception): def __init__(self, status, error, detail=None): super().__init__(f"HTTP {status} {error}: {detail or ''}".strip()) self.status, self.error, self.detail = status, error, detail class HubUnavailable(Exception): """Хаб не отвечает — обычно не запущен (jira-hub serve).""" class HubClient: def __init__(self, base_url: str, token: str, timeout: float = 60): self.base, self.token, self.timeout = base_url.rstrip("/"), token, timeout @classmethod def from_home(cls, timeout: float = 60) -> "HubClient": conn = db.connect(paths.db_path()) try: settings.ensure_defaults(conn) return cls(f"http://127.0.0.1:{settings.get(conn, 'port')}", settings.tokens(conn)["client"], timeout) finally: conn.close() def call(self, method: str, path: str, body=None, params=None): url = self.base + path if params: url += "?" + urllib.parse.urlencode({k: v for k, v in params.items() if v not in (None, "")}) data = json.dumps(body, ensure_ascii=False).encode("utf-8") if body is not None else None req = urllib.request.Request(url, data=data, method=method) req.add_header("Authorization", "Bearer " + self.token) req.add_header("Accept", "application/json") if data is not None: req.add_header("Content-Type", "application/json; charset=utf-8") try: with urllib.request.urlopen(req, timeout=self.timeout) as resp: raw = resp.read() return json.loads(raw.decode("utf-8")) if raw else {} except urllib.error.HTTPError as e: try: payload = json.loads(e.read().decode("utf-8")) except Exception: payload = {"error": "http", "detail": str(e)} raise HubError(e.code, payload.get("error", "http"), payload.get("detail")) from None except (urllib.error.URLError, ConnectionError, TimeoutError) as e: raise HubUnavailable(f"хаб не отвечает ({self.base}): {e}") from None def get(self, path, **params): return self.call("GET", path, params=params or None) def post(self, path, body=None): return self.call("POST", path, body if body is not None else {}) def put(self, path, body): return self.call("PUT", path, body)