Andrey Epifancev a88bb1e6e8 refactor(sdlc-rename): пакет sdlc_hub, CLI sdlc-hub, каталог данных sdlc-hub
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 11:27:17 +04:00

74 lines
3.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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):
"""Хаб не отвечает — обычно не запущен (sdlc-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):
raw = self._raw(method, path, body, params, "application/json")
return json.loads(raw.decode("utf-8")) if raw else {}
def get_bytes(self, path: str) -> bytes:
"""Тело ответа как есть (содержимое вложения); ошибки — как у call."""
return self._raw("GET", path, None, None, "*/*")
def _raw(self, method, path, body, params, accept) -> bytes:
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", accept)
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:
return resp.read()
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)
def patch(self, path, body):
return self.call("PATCH", path, body)