feat: 微服务架构拆分和前后端优化
后端: - 拆分出 agent_service, runtime_service, trading_service, news_service - Gateway 模块化拆分 (gateway_*.py) - 添加 domains/ 领域层 - 新增 control_client, runtime_client - 更新 start-dev.sh 支持 split 服务模式 前端: - 完善 API 服务层 (newsApi, tradingApi) - 更新 vite.config.js - Explain 组件优化 测试: - 添加多个服务 app 测试 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Shared client package."""
|
||||
|
||||
from shared.client.control_client import ControlPlaneClient
|
||||
from shared.client.trading_client import TradingServiceClient
|
||||
from shared.client.news_client import NewsServiceClient
|
||||
from shared.client.agent_client import AgentServiceClient
|
||||
from shared.client.runtime_client import RuntimeServiceClient
|
||||
|
||||
__all__ = [
|
||||
"ControlPlaneClient",
|
||||
"RuntimeServiceClient",
|
||||
"TradingServiceClient",
|
||||
"NewsServiceClient",
|
||||
"AgentServiceClient",
|
||||
]
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Agent service client for agent orchestration and runtime operations."""
|
||||
|
||||
import json
|
||||
from typing import Any, AsyncIterator
|
||||
|
||||
import httpx
|
||||
import websockets
|
||||
|
||||
from shared.schema.signals import AgentStateData
|
||||
|
||||
|
||||
class AgentServiceClient:
|
||||
"""Async client for the Agent Service API."""
|
||||
|
||||
def __init__(self, base_url: str = "http://localhost:8000"):
|
||||
"""Initialize the client with a base URL.
|
||||
|
||||
Args:
|
||||
base_url: Base URL for the agent service API.
|
||||
"""
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self._client: httpx.AsyncClient | None = None
|
||||
|
||||
async def __aenter__(self) -> "AgentServiceClient":
|
||||
self._client = httpx.AsyncClient(base_url=self.base_url, timeout=30.0)
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
if self._client:
|
||||
await self._client.aclose()
|
||||
|
||||
async def get_agents(self) -> dict:
|
||||
"""Get list of all registered agents.
|
||||
|
||||
Returns:
|
||||
Dictionary with agent list.
|
||||
"""
|
||||
response = await self._client.get("/api/agents")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_agent_status(self, agent_id: str) -> dict:
|
||||
"""Get status of a specific agent.
|
||||
|
||||
Args:
|
||||
agent_id: The agent identifier.
|
||||
|
||||
Returns:
|
||||
Dictionary with agent status.
|
||||
"""
|
||||
response = await self._client.get(f"/api/agents/{agent_id}/status")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def post_run_daily(
|
||||
self,
|
||||
tickers: list[str],
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
runtime_config: dict[str, Any] | None = None,
|
||||
) -> dict:
|
||||
"""Trigger a daily analysis run.
|
||||
|
||||
Args:
|
||||
tickers: List of stock tickers to analyze.
|
||||
start_date: Start date (YYYY-MM-DD).
|
||||
end_date: End date (YYYY-MM-DD).
|
||||
runtime_config: Optional runtime configuration.
|
||||
|
||||
Returns:
|
||||
Dictionary with run initiation response.
|
||||
"""
|
||||
payload = {
|
||||
"tickers": tickers,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
}
|
||||
if runtime_config:
|
||||
payload["runtime_config"] = runtime_config
|
||||
response = await self._client.post("/api/run/daily", json=payload)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_run_status(self, run_id: str) -> dict:
|
||||
"""Get status of a run.
|
||||
|
||||
Args:
|
||||
run_id: The run identifier.
|
||||
|
||||
Returns:
|
||||
Dictionary with run status.
|
||||
"""
|
||||
response = await self._client.get(f"/api/runs/{run_id}/status")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_run_result(self, run_id: str) -> AgentStateData:
|
||||
"""Get the result of a completed run.
|
||||
|
||||
Args:
|
||||
run_id: The run identifier.
|
||||
|
||||
Returns:
|
||||
AgentStateData with run results.
|
||||
"""
|
||||
response = await self._client.get(f"/api/runs/{run_id}/result")
|
||||
response.raise_for_status()
|
||||
return AgentStateData.model_validate(response.json())
|
||||
|
||||
async def get_run_logs(self, run_id: str) -> dict:
|
||||
"""Get logs for a run.
|
||||
|
||||
Args:
|
||||
run_id: The run identifier.
|
||||
|
||||
Returns:
|
||||
Dictionary with run logs.
|
||||
"""
|
||||
response = await self._client.get(f"/api/runs/{run_id}/logs")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def cancel_run(self, run_id: str) -> dict:
|
||||
"""Cancel a running task.
|
||||
|
||||
Args:
|
||||
run_id: The run identifier.
|
||||
|
||||
Returns:
|
||||
Dictionary with cancellation confirmation.
|
||||
"""
|
||||
response = await self._client.post(f"/api/runs/{run_id}/cancel")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_runtime_config(self) -> dict:
|
||||
"""Get current runtime configuration.
|
||||
|
||||
Returns:
|
||||
Dictionary with runtime config.
|
||||
"""
|
||||
response = await self._client.get("/api/runtime/config")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def update_runtime_config(self, config: dict[str, Any]) -> dict:
|
||||
"""Update runtime configuration.
|
||||
|
||||
Args:
|
||||
config: New runtime configuration.
|
||||
|
||||
Returns:
|
||||
Dictionary with updated config.
|
||||
"""
|
||||
response = await self._client.put("/api/runtime/config", json=config)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def websocket_connect(
|
||||
self,
|
||||
run_id: str | None = None,
|
||||
) -> AsyncIterator[dict]:
|
||||
"""Connect to WebSocket for real-time updates.
|
||||
|
||||
Args:
|
||||
run_id: Optional run ID to subscribe to.
|
||||
|
||||
Yields:
|
||||
Dictionary with WebSocket messages.
|
||||
"""
|
||||
ws_url = self.base_url.replace("http", "ws") + "/ws"
|
||||
if run_id:
|
||||
ws_url += f"?run_id={run_id}"
|
||||
|
||||
async with websockets.connect(ws_url) as ws:
|
||||
async for message in ws:
|
||||
yield json.loads(message)
|
||||
|
||||
async def get_pipeline_status(self) -> dict:
|
||||
"""Get current pipeline execution status.
|
||||
|
||||
Returns:
|
||||
Dictionary with pipeline status.
|
||||
"""
|
||||
response = await self._client.get("/api/pipeline/status")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def trigger_pipeline(
|
||||
self,
|
||||
pipeline_type: str,
|
||||
tickers: list[str],
|
||||
config: dict[str, Any] | None = None,
|
||||
) -> dict:
|
||||
"""Trigger a pipeline execution.
|
||||
|
||||
Args:
|
||||
pipeline_type: Type of pipeline to run.
|
||||
tickers: List of tickers to process.
|
||||
config: Optional pipeline configuration.
|
||||
|
||||
Returns:
|
||||
Dictionary with pipeline trigger response.
|
||||
"""
|
||||
payload = {"pipeline_type": pipeline_type, "tickers": tickers}
|
||||
if config:
|
||||
payload["config"] = config
|
||||
response = await self._client.post("/api/pipeline/trigger", json=payload)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
82
shared/client/control_client.py
Normal file
82
shared/client/control_client.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Control-plane client for workspace, agent, and guard operations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
class ControlPlaneClient:
|
||||
"""Async client for the agent control-plane API surface."""
|
||||
|
||||
def __init__(self, base_url: str = "http://localhost:8000/api"):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self._client: httpx.AsyncClient | None = None
|
||||
|
||||
async def __aenter__(self) -> "ControlPlaneClient":
|
||||
self._client = httpx.AsyncClient(base_url=self.base_url, timeout=30.0)
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
if self._client:
|
||||
await self._client.aclose()
|
||||
|
||||
async def list_workspaces(self) -> dict:
|
||||
response = await self._client.get("/workspaces")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_workspace(self, workspace_id: str) -> dict:
|
||||
response = await self._client.get(f"/workspaces/{workspace_id}")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def list_agents(self, workspace_id: str) -> dict:
|
||||
response = await self._client.get(
|
||||
f"/workspaces/{workspace_id}/agents",
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_agent(self, workspace_id: str, agent_id: str) -> dict:
|
||||
response = await self._client.get(
|
||||
f"/workspaces/{workspace_id}/agents/{agent_id}",
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def fetch_pending_approvals(self) -> dict:
|
||||
response = await self._client.get("/guard/pending")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def approve_pending_approval(
|
||||
self,
|
||||
approval_id: str,
|
||||
*,
|
||||
one_time: bool = True,
|
||||
expires_in_minutes: int = 30,
|
||||
) -> dict:
|
||||
response = await self._client.post(
|
||||
"/guard/approve",
|
||||
json={
|
||||
"approval_id": approval_id,
|
||||
"one_time": one_time,
|
||||
"expires_in_minutes": expires_in_minutes,
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def deny_pending_approval(
|
||||
self,
|
||||
approval_id: str,
|
||||
*,
|
||||
reason: str = "Denied by client",
|
||||
) -> dict:
|
||||
response = await self._client.post(
|
||||
"/guard/deny",
|
||||
json={"approval_id": approval_id, "reason": reason},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
@@ -29,6 +29,7 @@ class NewsServiceClient:
|
||||
ticker: str,
|
||||
start_date: str | None = None,
|
||||
end_date: str | None = None,
|
||||
limit: int | None = None,
|
||||
) -> dict:
|
||||
"""Get enriched news for a ticker.
|
||||
|
||||
@@ -45,10 +46,44 @@ class NewsServiceClient:
|
||||
params["start_date"] = start_date
|
||||
if end_date:
|
||||
params["end_date"] = end_date
|
||||
if limit is not None:
|
||||
params["limit"] = limit
|
||||
response = await self._client.get("/api/enriched-news", params=params)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_news_for_date(
|
||||
self,
|
||||
ticker: str,
|
||||
date: str,
|
||||
limit: int = 20,
|
||||
) -> dict:
|
||||
"""Get enriched news rows for a specific trade date."""
|
||||
response = await self._client.get(
|
||||
"/api/news-for-date",
|
||||
params={"ticker": ticker, "date": date, "limit": limit},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_news_timeline(
|
||||
self,
|
||||
ticker: str,
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
) -> dict:
|
||||
"""Get aggregated news timeline for a ticker."""
|
||||
response = await self._client.get(
|
||||
"/api/news-timeline",
|
||||
params={
|
||||
"ticker": ticker,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_similar_days(
|
||||
self,
|
||||
ticker: str,
|
||||
@@ -70,61 +105,61 @@ class NewsServiceClient:
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_story(self, story_id: str) -> dict:
|
||||
"""Get a specific news story by ID.
|
||||
async def get_story(self, ticker: str, as_of_date: str) -> dict:
|
||||
"""Get or build a ticker story as of one date.
|
||||
|
||||
Args:
|
||||
story_id: The story identifier.
|
||||
ticker: Stock ticker symbol.
|
||||
as_of_date: Story date.
|
||||
|
||||
Returns:
|
||||
Dictionary with story data.
|
||||
"""
|
||||
response = await self._client.get(f"/api/stories/{story_id}")
|
||||
response = await self._client.get(
|
||||
f"/api/stories/{ticker}",
|
||||
params={"as_of_date": as_of_date},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def post_enrich(self, news_items: list[dict]) -> dict:
|
||||
"""Enrich news items with additional analysis.
|
||||
|
||||
Args:
|
||||
news_items: List of news items to enrich.
|
||||
|
||||
Returns:
|
||||
Dictionary with enriched news data.
|
||||
"""
|
||||
response = await self._client.post("/api/enrich", json=news_items)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_categories(self) -> dict:
|
||||
"""Get available news categories.
|
||||
async def get_categories(
|
||||
self,
|
||||
ticker: str,
|
||||
start_date: str | None = None,
|
||||
end_date: str | None = None,
|
||||
limit: int = 200,
|
||||
) -> dict:
|
||||
"""Get categories for a ticker window.
|
||||
|
||||
Returns:
|
||||
Dictionary with available categories.
|
||||
"""
|
||||
response = await self._client.get("/api/categories")
|
||||
params = {"ticker": ticker, "limit": limit}
|
||||
if start_date:
|
||||
params["start_date"] = start_date
|
||||
if end_date:
|
||||
params["end_date"] = end_date
|
||||
response = await self._client.get("/api/categories", params=params)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def search_news(
|
||||
async def get_range_explain(
|
||||
self,
|
||||
query: str,
|
||||
ticker: str | None = None,
|
||||
limit: int = 10,
|
||||
ticker: str,
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
article_ids: list[str] | None = None,
|
||||
limit: int = 100,
|
||||
) -> dict:
|
||||
"""Search news articles.
|
||||
|
||||
Args:
|
||||
query: Search query string.
|
||||
ticker: Optional ticker to filter by.
|
||||
limit: Maximum number of results.
|
||||
|
||||
Returns:
|
||||
Dictionary with search results.
|
||||
"""
|
||||
params = {"query": query, "limit": limit}
|
||||
if ticker:
|
||||
params["ticker"] = ticker
|
||||
response = await self._client.get("/api/search", params=params)
|
||||
"""Get a range explanation for a ticker window."""
|
||||
params: list[tuple[str, str | int]] = [
|
||||
("ticker", ticker),
|
||||
("start_date", start_date),
|
||||
("end_date", end_date),
|
||||
("limit", limit),
|
||||
]
|
||||
for article_id in article_ids or []:
|
||||
params.append(("article_ids", article_id))
|
||||
response = await self._client.get("/api/range-explain", params=params)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
72
shared/client/runtime_client.py
Normal file
72
shared/client/runtime_client.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Runtime service client for lifecycle and gateway operations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
class RuntimeServiceClient:
|
||||
"""Async client for the runtime-service API surface."""
|
||||
|
||||
def __init__(self, base_url: str = "http://localhost:8003/api/runtime"):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self._client: httpx.AsyncClient | None = None
|
||||
|
||||
async def __aenter__(self) -> "RuntimeServiceClient":
|
||||
self._client = httpx.AsyncClient(base_url=self.base_url, timeout=30.0)
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
if self._client:
|
||||
await self._client.aclose()
|
||||
|
||||
async def fetch_context(self) -> dict:
|
||||
response = await self._client.get("/context")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def fetch_agents(self) -> dict:
|
||||
response = await self._client.get("/agents")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def fetch_events(self) -> dict:
|
||||
response = await self._client.get("/events")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def fetch_gateway_port(self) -> dict:
|
||||
response = await self._client.get("/gateway/port")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def start_runtime(self, config: dict) -> dict:
|
||||
response = await self._client.post("/start", json=config)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def stop_runtime(self, *, force: bool = True) -> dict:
|
||||
response = await self._client.post(f"/stop?force={str(force).lower()}")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def restart_runtime(self, config: dict) -> dict:
|
||||
response = await self._client.post("/restart", json=config)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def fetch_current_runtime(self) -> dict:
|
||||
response = await self._client.get("/current")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_runtime_config(self) -> dict:
|
||||
response = await self._client.get("/config")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def update_runtime_config(self, config: dict) -> dict:
|
||||
response = await self._client.put("/config", json=config)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
@@ -5,8 +5,7 @@ import httpx
|
||||
|
||||
from shared.schema.price import PriceResponse
|
||||
from shared.schema.financial import FinancialMetricsResponse, LineItemResponse
|
||||
from shared.schema.market import InsiderTradeResponse, CompanyFactsResponse
|
||||
from shared.schema.portfolio import Portfolio
|
||||
from shared.schema.market import InsiderTradeResponse
|
||||
|
||||
|
||||
class TradingServiceClient:
|
||||
@@ -107,6 +106,8 @@ class TradingServiceClient:
|
||||
async def get_insider_trades(
|
||||
self,
|
||||
ticker: str,
|
||||
end_date: str | None = None,
|
||||
start_date: str | None = None,
|
||||
limit: int | None = None,
|
||||
) -> InsiderTradeResponse:
|
||||
"""Get insider trades for a ticker.
|
||||
@@ -119,45 +120,16 @@ class TradingServiceClient:
|
||||
InsiderTradeResponse with insider trade data.
|
||||
"""
|
||||
params = {"ticker": ticker}
|
||||
if start_date:
|
||||
params["start_date"] = start_date
|
||||
if end_date:
|
||||
params["end_date"] = end_date
|
||||
if limit:
|
||||
params["limit"] = limit
|
||||
response = await self._client.get("/api/insider-trades", params=params)
|
||||
response.raise_for_status()
|
||||
return InsiderTradeResponse.model_validate(response.json())
|
||||
|
||||
async def get_portfolio(self) -> Portfolio:
|
||||
"""Get the current portfolio.
|
||||
|
||||
Returns:
|
||||
Portfolio with current positions and cash.
|
||||
"""
|
||||
response = await self._client.get("/api/portfolio")
|
||||
response.raise_for_status()
|
||||
return Portfolio.model_validate(response.json())
|
||||
|
||||
async def post_trades(self, trades: list[dict]) -> dict:
|
||||
"""Submit trades for execution.
|
||||
|
||||
Args:
|
||||
trades: List of trade orders.
|
||||
|
||||
Returns:
|
||||
Dictionary with trade execution results.
|
||||
"""
|
||||
response = await self._client.post("/api/trades", json=trades)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def post_settle(self) -> dict:
|
||||
"""Settle all pending trades.
|
||||
|
||||
Returns:
|
||||
Dictionary with settlement results.
|
||||
"""
|
||||
response = await self._client.post("/api/settle")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_market_status(self) -> dict:
|
||||
"""Get current market status.
|
||||
|
||||
@@ -168,40 +140,32 @@ class TradingServiceClient:
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def get_company_facts(self, ticker: str) -> CompanyFactsResponse:
|
||||
"""Get company facts for a ticker.
|
||||
|
||||
Args:
|
||||
ticker: Stock ticker symbol.
|
||||
|
||||
Returns:
|
||||
CompanyFactsResponse with company information.
|
||||
"""
|
||||
response = await self._client.get(f"/api/company/{ticker}/facts")
|
||||
async def get_market_cap(self, ticker: str, end_date: str) -> dict:
|
||||
"""Get market cap for a ticker/date."""
|
||||
response = await self._client.get(
|
||||
"/api/market-cap",
|
||||
params={"ticker": ticker, "end_date": end_date},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return CompanyFactsResponse.model_validate(response.json())
|
||||
return response.json()
|
||||
|
||||
async def get_line_items(
|
||||
self,
|
||||
ticker: str,
|
||||
statement_type: str | None = None,
|
||||
period: str | None = None,
|
||||
line_items: list[str],
|
||||
end_date: str,
|
||||
period: str = "ttm",
|
||||
limit: int = 10,
|
||||
) -> LineItemResponse:
|
||||
"""Get line items (financial statement data) for a ticker.
|
||||
|
||||
Args:
|
||||
ticker: Stock ticker symbol.
|
||||
statement_type: Type of statement (income, balance, cash_flow).
|
||||
period: Reporting period.
|
||||
|
||||
Returns:
|
||||
LineItemResponse with financial statement data.
|
||||
"""
|
||||
params = {"ticker": ticker}
|
||||
if statement_type:
|
||||
params["statement_type"] = statement_type
|
||||
if period:
|
||||
params["period"] = period
|
||||
"""Get line-item search results for a ticker/date."""
|
||||
params: list[tuple[str, str | int]] = [
|
||||
("ticker", ticker),
|
||||
("end_date", end_date),
|
||||
("period", period),
|
||||
("limit", limit),
|
||||
]
|
||||
for item in line_items:
|
||||
params.append(("line_items", item))
|
||||
response = await self._client.get("/api/line-items", params=params)
|
||||
response.raise_for_status()
|
||||
return LineItemResponse.model_validate(response.json())
|
||||
|
||||
Reference in New Issue
Block a user