Соединение SQLite на запрос, сравнение токенов в постоянное время, 400 на битый JSON и нечисловые параметры. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
64 lines
2.6 KiB
Python
64 lines
2.6 KiB
Python
"""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)
|