feat(cerbero): HTTP client with bearer + bot-tag + retry

Client minimale verso Cerbero MCP unified server con header
Authorization Bearer e X-Bot-Tag su ogni richiesta. Retry esponenziale
solo su ConnectionError (non su 4xx). 4xx/5xx -> RuntimeError.

Switch da httpx a requests per allineamento con libreria mock
`responses` (httpx richiederebbe respx). httpx resta in deps per usi
futuri. Aggiunto types-requests ai dev deps per mypy strict.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 19:25:00 +02:00
parent b61bbaf13c
commit 14b1b84a47
5 changed files with 115 additions and 0 deletions
View File
+60
View File
@@ -0,0 +1,60 @@
from __future__ import annotations
from types import TracebackType
from typing import Any
import requests
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential
class CerberoClient:
"""Client HTTP minimale verso Cerbero MCP unified server."""
def __init__(
self,
base_url: str,
token: str,
bot_tag: str,
timeout_seconds: float = 10.0,
) -> None:
self.base_url = base_url.rstrip("/")
self.token = token
self.bot_tag = bot_tag
self.timeout_seconds = timeout_seconds
self._session = requests.Session()
self._session.headers.update(
{
"Authorization": f"Bearer {token}",
"X-Bot-Tag": bot_tag,
"Content-Type": "application/json",
}
)
def close(self) -> None:
self._session.close()
def __enter__(self) -> CerberoClient:
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
self.close()
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=0.5, min=0.5, max=4.0),
retry=retry_if_exception_type(requests.ConnectionError),
reraise=True,
)
def call_tool(self, exchange: str, tool: str, args: dict[str, Any]) -> Any:
url = f"{self.base_url}/mcp-{exchange}/tools/{tool}"
resp = self._session.post(url, json=args, timeout=self.timeout_seconds)
if resp.status_code >= 400:
raise RuntimeError(
f"Cerbero {exchange}/{tool} returned {resp.status_code}: {resp.text}"
)
return resp.json()