sdlc-hub/src/jira_hub/client.py
Andrey Epifancev 2558248dc9 feat(hub): HTTP API — маршруты агента и интерфейсов, токены, долгий claim, клиент
Соединение SQLite на запрос, сравнение токенов в постоянное время,
400 на битый JSON и нечисловые параметры.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
2026-10-11 22:15:59 +04:00

64 lines
2.6 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):
"""Хаб не отвечает — обычно не запущен (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)