chore: httpx retry transport + healthcheck stdlib + mypy config

- mcp_common/http.py: nuovo helper async_client() con
  AsyncHTTPTransport(retries=3) per gestire connection error transient
  + call_with_retry() generic async retry decorator. Sostituite 25
  occorrenze httpx.AsyncClient(...) in deribit/hyperliquid/sentiment/
  macro client. 5 nuovi test.

- Dockerfile healthcheck: passato da python+httpx subprocess a
  stdlib urllib.request.urlopen() su tutti i 6 servizi MCP. Zero
  dipendenze esterne nel runtime check, timeout esplicito 3s, image
  leggermente più snella.

- pyproject.toml: aggiunto [tool.mypy] python_version=3.13 con
  mypy_path multi-package + override ignore_missing_imports per i
  vendor SDK (pybit, alpaca, hyperliquid, pythonjsonlogger). mypy 1.20
  in dev deps; ruff pinned 0.5.x. mcp_common passa mypy clean; 44
  errori tipo pre-esistenti nei servizi affiorati ma non bloccanti —
  fix da pianificare separatamente.

- 455 test verdi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
AdrianoDev
2026-04-28 07:26:17 +02:00
parent 4d9db750be
commit 6b7b3f7658
15 changed files with 395 additions and 53 deletions
@@ -6,6 +6,7 @@ import xml.etree.ElementTree as ET
from typing import Any
import httpx
from mcp_common.http import async_client
CRYPTOPANIC_URL = "https://cryptopanic.com/api/v1/posts/"
ALTERNATIVE_ME_URL = "https://api.alternative.me/fng/"
@@ -18,7 +19,7 @@ MESSARI_NEWS_URL = "https://data.messari.io/api/v1/news"
async def _fetch_coindesk_headlines(limit: int = 20) -> list[dict[str, Any]]:
items: list[dict[str, Any]] = []
try:
async with httpx.AsyncClient(timeout=10, follow_redirects=True) as client:
async with async_client(timeout=10.0, follow_redirects=True) as client:
resp = await client.get(COINDESK_RSS)
if resp.status_code != 200:
return items
@@ -54,7 +55,7 @@ async def _fetch_cryptocompare_news(limit: int = 20) -> list[dict[str, Any]]:
"""CER-017: CryptoCompare news free (no key needed)."""
items: list[dict[str, Any]] = []
try:
async with httpx.AsyncClient(timeout=10) as client:
async with async_client(timeout=10.0) as client:
resp = await client.get(CRYPTOCOMPARE_NEWS_URL, params={"lang": "EN"})
if resp.status_code != 200:
return items
@@ -82,7 +83,7 @@ async def _fetch_messari_news(limit: int = 20) -> list[dict[str, Any]]:
"""CER-017: Messari news free (no key needed for basic feed)."""
items: list[dict[str, Any]] = []
try:
async with httpx.AsyncClient(timeout=10) as client:
async with async_client(timeout=10.0) as client:
resp = await client.get(MESSARI_NEWS_URL)
if resp.status_code != 200:
return items
@@ -164,7 +165,7 @@ async def fetch_crypto_news(api_key: str = "", limit: int = 20) -> dict[str, Any
async def _fetch_cryptopanic_news(api_key: str, limit: int) -> list[dict[str, Any]]:
"""Cryptopanic as one of the sources. Failure → []."""
try:
async with httpx.AsyncClient(timeout=10) as client:
async with async_client(timeout=10.0) as client:
resp = await client.get(
CRYPTOPANIC_URL,
params={"auth_token": api_key, "public": "true"},
@@ -189,7 +190,7 @@ async def _fetch_cryptopanic_news(api_key: str, limit: int) -> list[dict[str, An
async def _fetch_lunarcrush(symbol: str, api_key: str) -> dict | None:
"""CER-P2-005: LunarCrush v4 social metrics. Ritorna None se fail."""
try:
async with httpx.AsyncClient(timeout=10) as client:
async with async_client(timeout=10.0) as client:
resp = await client.get(
LUNARCRUSH_COIN_URL.format(symbol=symbol.upper()),
headers={"Authorization": f"Bearer {api_key}"},
@@ -220,7 +221,7 @@ async def fetch_social_sentiment(symbol: str = "BTC") -> dict[str, Any]:
Altrimenti deriva proxy da fear_greed per popolare twitter/reddit sentiment
(marcato come derived=True così l'agent sa che è proxy).
"""
async with httpx.AsyncClient(timeout=10) as client:
async with async_client(timeout=10.0) as client:
fng_resp = await client.get(ALTERNATIVE_ME_URL, params={"limit": 1})
fng_data = fng_resp.json()
fng_list = fng_data.get("data", [])
@@ -275,7 +276,7 @@ async def fetch_funding_rates(asset: str = "BTC") -> dict[str, Any]:
okx_inst = f"{asset}-USDT-SWAP"
rates: list[dict[str, Any]] = []
async with httpx.AsyncClient(timeout=10) as client:
async with async_client(timeout=10.0) as client:
# Binance
try:
resp = await client.get(BINANCE_FUNDING_URL, params={"symbol": usdt_symbol})
@@ -341,7 +342,7 @@ async def fetch_cross_exchange_funding(assets: list[str] | None = None) -> dict[
assets = [a.upper() for a in (assets or ["BTC", "ETH", "SOL"])]
snapshot: dict[str, dict[str, Any]] = {}
async with httpx.AsyncClient(timeout=10) as client:
async with async_client(timeout=10.0) as client:
for asset in assets:
rates: dict[str, float | None] = {
"binance": None,
@@ -530,7 +531,7 @@ async def fetch_cointegration_pairs(
interval = "1h"
limit = max(50, lookback_hours)
async with httpx.AsyncClient(timeout=15) as client:
async with async_client(timeout=15.0) as client:
for pair in pairs:
if len(pair) != 2:
continue
@@ -574,7 +575,7 @@ async def fetch_world_news() -> dict[str, Any]:
"""Fetch world financial news from free RSS feeds."""
articles: list[dict[str, Any]] = []
async with httpx.AsyncClient(timeout=10, follow_redirects=True) as client:
async with async_client(timeout=10.0, follow_redirects=True) as client:
for source_name, url in WORLD_NEWS_FEEDS:
try:
resp = await client.get(url)
@@ -614,7 +615,7 @@ async def fetch_oi_history(asset: str = "BTC", period: str = "5m", limit: int =
limit = max(1, min(int(limit), 500))
points: list[dict[str, Any]] = []
try:
async with httpx.AsyncClient(timeout=10) as client:
async with async_client(timeout=10.0) as client:
resp = await client.get(
BINANCE_OI_HIST_URL,
params={"symbol": symbol, "period": period, "limit": limit},