152 lines
4.3 KiB
Python
152 lines
4.3 KiB
Python
from __future__ import annotations
|
||
|
||
from collections.abc import Mapping
|
||
from typing import Any
|
||
|
||
import httpx
|
||
|
||
from yuxi.utils.logging_config import logger
|
||
|
||
# httpx 接受的 multipart 文件参数:字段名 -> 文件说明或 (文件名, 内容, MIME) 等
|
||
RequestFiles = list[tuple[str, Any]] | Mapping[str, Any]
|
||
|
||
|
||
class ChannelHttpClient:
|
||
"""渠道网关统一的异步 HTTP 客户端。
|
||
|
||
封装 ``httpx.AsyncClient``,自动完成响应状态检查、JSON 解析与错误日志,
|
||
简化各渠道插件中的 ``send_message``、``health_check`` 与媒体上传实现。
|
||
"""
|
||
|
||
def __init__(self, client: httpx.AsyncClient | None = None) -> None:
|
||
self._own_client = client is None
|
||
self._client = client or httpx.AsyncClient()
|
||
|
||
async def __aenter__(self) -> ChannelHttpClient:
|
||
return self
|
||
|
||
async def __aexit__(self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: Any) -> None:
|
||
if self._own_client:
|
||
await self._client.aclose()
|
||
|
||
async def request_json(
|
||
self,
|
||
method: str,
|
||
url: str,
|
||
*,
|
||
json: dict | None = None,
|
||
params: dict[str, Any] | None = None,
|
||
headers: dict[str, str] | None = None,
|
||
files: RequestFiles | None = None,
|
||
timeout: float = 60.0,
|
||
) -> dict[str, Any]:
|
||
try:
|
||
resp = await self._client.request(
|
||
method,
|
||
url,
|
||
json=json,
|
||
params=params,
|
||
headers=headers,
|
||
files=files,
|
||
timeout=timeout,
|
||
)
|
||
resp.raise_for_status()
|
||
return resp.json()
|
||
except httpx.HTTPStatusError as exc:
|
||
logger.error(
|
||
"Channel HTTP %s %s failed: %d %s",
|
||
method,
|
||
url,
|
||
exc.response.status_code,
|
||
exc.response.text[:200],
|
||
)
|
||
raise
|
||
except httpx.RequestError as exc:
|
||
logger.error(
|
||
"Channel HTTP %s %s request error: %s",
|
||
method,
|
||
url,
|
||
exc,
|
||
)
|
||
raise
|
||
except json.JSONDecodeError as exc:
|
||
logger.error(
|
||
"Channel HTTP %s %s returned invalid JSON: %s",
|
||
method,
|
||
url,
|
||
exc,
|
||
)
|
||
raise
|
||
|
||
async def post_json(
|
||
self,
|
||
url: str,
|
||
*,
|
||
json: dict | None = None,
|
||
params: dict[str, Any] | None = None,
|
||
headers: dict[str, str] | None = None,
|
||
files: RequestFiles | None = None,
|
||
timeout: float = 60.0,
|
||
) -> dict[str, Any]:
|
||
return await self.request_json(
|
||
"POST",
|
||
url,
|
||
json=json,
|
||
params=params,
|
||
headers=headers,
|
||
files=files,
|
||
timeout=timeout,
|
||
)
|
||
|
||
async def get_json(
|
||
self,
|
||
url: str,
|
||
*,
|
||
params: dict[str, Any] | None = None,
|
||
headers: dict[str, str] | None = None,
|
||
timeout: float = 60.0,
|
||
) -> dict[str, Any]:
|
||
return await self.request_json(
|
||
"GET",
|
||
url,
|
||
params=params,
|
||
headers=headers,
|
||
timeout=timeout,
|
||
)
|
||
|
||
async def get_bytes(
|
||
self,
|
||
url: str,
|
||
*,
|
||
params: dict[str, Any] | None = None,
|
||
headers: dict[str, str] | None = None,
|
||
timeout: float = 60.0,
|
||
follow_redirects: bool = True,
|
||
) -> bytes:
|
||
"""Execute a GET request and return the raw response body as bytes."""
|
||
try:
|
||
resp = await self._client.get(
|
||
url,
|
||
params=params,
|
||
headers=headers,
|
||
timeout=timeout,
|
||
follow_redirects=follow_redirects,
|
||
)
|
||
resp.raise_for_status()
|
||
return resp.content
|
||
except httpx.HTTPStatusError as exc:
|
||
logger.error(
|
||
"Channel HTTP GET %s failed: %d %s",
|
||
url,
|
||
exc.response.status_code,
|
||
exc.response.text[:200],
|
||
)
|
||
raise
|
||
except httpx.RequestError as exc:
|
||
logger.error(
|
||
"Channel HTTP GET %s request error: %s",
|
||
url,
|
||
exc,
|
||
)
|
||
raise
|