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:
@@ -16,6 +16,7 @@ dependencies = [
|
|||||||
"anthropic>=0.39",
|
"anthropic>=0.39",
|
||||||
"openai>=1.55",
|
"openai>=1.55",
|
||||||
"httpx>=0.28",
|
"httpx>=0.28",
|
||||||
|
"requests>=2.32",
|
||||||
"tenacity>=9.0",
|
"tenacity>=9.0",
|
||||||
"pyyaml>=6.0",
|
"pyyaml>=6.0",
|
||||||
"streamlit>=1.40",
|
"streamlit>=1.40",
|
||||||
@@ -31,6 +32,7 @@ dev = [
|
|||||||
"responses>=0.25",
|
"responses>=0.25",
|
||||||
"ruff>=0.7",
|
"ruff>=0.7",
|
||||||
"mypy>=1.13",
|
"mypy>=1.13",
|
||||||
|
"types-requests>=2.32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import pytest
|
||||||
|
import responses
|
||||||
|
|
||||||
|
from multi_swarm.cerbero.client import CerberoClient
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_call_tool_passes_bearer_and_bot_tag() -> None:
|
||||||
|
responses.add(
|
||||||
|
responses.POST,
|
||||||
|
"http://test:9000/mcp-deribit/tools/get_iv_rank",
|
||||||
|
json={"iv_rank": 0.42},
|
||||||
|
status=200,
|
||||||
|
)
|
||||||
|
client = CerberoClient(
|
||||||
|
base_url="http://test:9000", token="tok-xyz", bot_tag="swarm-poc-phase1"
|
||||||
|
)
|
||||||
|
result = client.call_tool("deribit", "get_iv_rank", {"symbol": "BTC-PERPETUAL"})
|
||||||
|
assert result == {"iv_rank": 0.42}
|
||||||
|
req = responses.calls[0].request
|
||||||
|
assert req.headers["Authorization"] == "Bearer tok-xyz"
|
||||||
|
assert req.headers["X-Bot-Tag"] == "swarm-poc-phase1"
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_call_tool_raises_on_error() -> None:
|
||||||
|
responses.add(
|
||||||
|
responses.POST,
|
||||||
|
"http://test:9000/mcp-deribit/tools/get_iv_rank",
|
||||||
|
json={"error": "bad"},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
client = CerberoClient(
|
||||||
|
base_url="http://test:9000", token="tok-xyz", bot_tag="swarm-poc-phase1"
|
||||||
|
)
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
client.call_tool("deribit", "get_iv_rank", {})
|
||||||
@@ -898,6 +898,7 @@ dependencies = [
|
|||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
|
{ name = "requests" },
|
||||||
{ name = "scipy" },
|
{ name = "scipy" },
|
||||||
{ name = "sexpdata" },
|
{ name = "sexpdata" },
|
||||||
{ name = "sqlmodel" },
|
{ name = "sqlmodel" },
|
||||||
@@ -913,6 +914,7 @@ dev = [
|
|||||||
{ name = "pytest-mock" },
|
{ name = "pytest-mock" },
|
||||||
{ name = "responses" },
|
{ name = "responses" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
|
{ name = "types-requests" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
@@ -928,6 +930,7 @@ requires-dist = [
|
|||||||
{ name = "pydantic", specifier = ">=2.9" },
|
{ name = "pydantic", specifier = ">=2.9" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.6" },
|
{ name = "pydantic-settings", specifier = ">=2.6" },
|
||||||
{ name = "pyyaml", specifier = ">=6.0" },
|
{ name = "pyyaml", specifier = ">=6.0" },
|
||||||
|
{ name = "requests", specifier = ">=2.32" },
|
||||||
{ name = "scipy", specifier = ">=1.14" },
|
{ name = "scipy", specifier = ">=1.14" },
|
||||||
{ name = "sexpdata", specifier = ">=1.0.2" },
|
{ name = "sexpdata", specifier = ">=1.0.2" },
|
||||||
{ name = "sqlmodel", specifier = ">=0.0.22" },
|
{ name = "sqlmodel", specifier = ">=0.0.22" },
|
||||||
@@ -943,6 +946,7 @@ dev = [
|
|||||||
{ name = "pytest-mock", specifier = ">=3.14" },
|
{ name = "pytest-mock", specifier = ">=3.14" },
|
||||||
{ name = "responses", specifier = ">=0.25" },
|
{ name = "responses", specifier = ">=0.25" },
|
||||||
{ name = "ruff", specifier = ">=0.7" },
|
{ name = "ruff", specifier = ">=0.7" },
|
||||||
|
{ name = "types-requests", specifier = ">=2.32" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2048,6 +2052,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
|
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-requests"
|
||||||
|
version = "2.33.0.20260508"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c7/6b/eb226bdd61a982c9a03e02c657fb4ab001733506e6423906ac142331f2e3/types_requests-2.33.0.20260508.tar.gz", hash = "sha256:81b2ae5f0d20967714a6aa5ef9284c05570d7cb06b7de8f2a77b918b63ddd411", size = 23991, upload-time = "2026-05-08T04:50:56.818Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/96/080db0afdf2c5cc5fe512b41354e8d114fe8f65e9510c56ff8dfd40216ce/types_requests-2.33.0.20260508-py3-none-any.whl", hash = "sha256:fa01459cca184229713df03709db46a905325906d27e042cd4fd7ea3d15d3400", size = 20722, upload-time = "2026-05-08T04:50:55.548Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.15.0"
|
version = "4.15.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user