fix: align portfolio-hub read adapter to live API contract
whoami now hits GET /v1/user_info (the live endpoint; /v1/auth/whoami
does not exist), get_portfolios parses the {portfolios:[...],total} envelope
and filters by managed_by_daemon_id client-side (the list endpoint is
token-scoped, no such query param), and a missing URL scheme now defaults
to http:// so a bare host can't silently degrade the daemon.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
"""portfolio-hub read HTTP adapter (REQ-027 AC-1/AC-4).
|
||||
|
||||
Mirrors portfolio-hub `list_portfolios_v1`:
|
||||
GET /v1/portfolios?managed_by_daemon_id=<id>
|
||||
GET /v1/auth/whoami (returns {"daemon_id": "..."})
|
||||
Matches the live portfolio-hub HTTP contract:
|
||||
GET /v1/user_info -> UserInfoResponse {"daemon_id": "...", ...}
|
||||
GET /v1/portfolios -> PortfolioListResponse {"portfolios": [PortfolioItem], "total": N}
|
||||
|
||||
The list endpoint is token-scoped (no `managed_by_daemon_id` query param);
|
||||
results are filtered client-side by `managed_by_daemon_id == daemon_id`.
|
||||
`PortfolioItem` carries no `active_strategy_id`/`strategy_params`, so those
|
||||
default to None/{} (opaque pass-through stays empty until sourced elsewhere).
|
||||
|
||||
Credentials reused from BAT-006 (TREX_PORTFOLIO_HUB_*). All failures lift
|
||||
to PortfolioHubUnavailableError so the snapshot store can set the degraded
|
||||
@@ -29,7 +34,10 @@ class PortfolioHubReadAdapter:
|
||||
timeout: float = 5.0,
|
||||
client: Optional[httpx.Client] = None,
|
||||
) -> None:
|
||||
self._base_url = base_url.rstrip("/")
|
||||
normalized = base_url.strip()
|
||||
if "://" not in normalized:
|
||||
normalized = "http://" + normalized
|
||||
self._base_url = normalized.rstrip("/")
|
||||
self._token = token
|
||||
self._timeout = timeout
|
||||
self._client = client
|
||||
@@ -48,30 +56,30 @@ class PortfolioHubReadAdapter:
|
||||
timeout=self._timeout)
|
||||
|
||||
def whoami(self) -> str:
|
||||
url = f"{self._base_url}/v1/auth/whoami"
|
||||
url = f"{self._base_url}/v1/user_info"
|
||||
try:
|
||||
resp = self._get(url)
|
||||
except httpx.HTTPError as exc:
|
||||
raise PortfolioHubUnavailableError(
|
||||
f"whoami network error: {exc.__class__.__name__}"
|
||||
f"user_info network error: {exc.__class__.__name__}"
|
||||
) from exc
|
||||
if not (200 <= resp.status_code < 300):
|
||||
raise PortfolioHubUnavailableError(
|
||||
f"whoami {resp.status_code}: {resp.text[:120]}"
|
||||
f"user_info {resp.status_code}: {resp.text[:120]}"
|
||||
)
|
||||
try:
|
||||
body = resp.json()
|
||||
except ValueError as exc:
|
||||
raise PortfolioHubUnavailableError("whoami response not JSON") from exc
|
||||
raise PortfolioHubUnavailableError("user_info response not JSON") from exc
|
||||
did = body.get("daemon_id") if isinstance(body, dict) else None
|
||||
if not did:
|
||||
raise PortfolioHubUnavailableError("whoami missing daemon_id")
|
||||
raise PortfolioHubUnavailableError("user_info missing daemon_id")
|
||||
return str(did)
|
||||
|
||||
def get_portfolios(self, daemon_id: str) -> list[PortfolioItem]:
|
||||
url = f"{self._base_url}/v1/portfolios"
|
||||
try:
|
||||
resp = self._get(url, params={"managed_by_daemon_id": daemon_id})
|
||||
resp = self._get(url)
|
||||
except httpx.HTTPError as exc:
|
||||
raise PortfolioHubUnavailableError(
|
||||
f"get_portfolios network error: {exc.__class__.__name__}"
|
||||
@@ -85,7 +93,10 @@ class PortfolioHubReadAdapter:
|
||||
except ValueError as exc:
|
||||
raise PortfolioHubUnavailableError("get_portfolios not JSON") from exc
|
||||
|
||||
items_raw = body.get("items", body) if isinstance(body, dict) else body
|
||||
if isinstance(body, dict):
|
||||
items_raw = body.get("portfolios", body.get("items"))
|
||||
else:
|
||||
items_raw = body
|
||||
if not isinstance(items_raw, list):
|
||||
raise PortfolioHubUnavailableError("get_portfolios shape unexpected")
|
||||
|
||||
@@ -93,6 +104,9 @@ class PortfolioHubReadAdapter:
|
||||
for raw in items_raw:
|
||||
if not isinstance(raw, dict):
|
||||
continue
|
||||
mbd = raw.get("managed_by_daemon_id")
|
||||
if mbd is not None and str(mbd) != daemon_id:
|
||||
continue
|
||||
pid = str(raw.get("portfolio_id") or raw.get("asset_id") or "")
|
||||
if not pid:
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user