95 lines
3.5 KiB
Python
95 lines
3.5 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
|
||
|
|
class FeishuApiClient:
|
||
|
|
"""飞书 Open API HTTP 客户端,封装通用请求逻辑。"""
|
||
|
|
|
||
|
|
DEFAULT_TIMEOUT_S = 30.0
|
||
|
|
|
||
|
|
def __init__(self, lark_client, http_client: Any | None = None, timeout: float | None = None):
|
||
|
|
self._lark = lark_client
|
||
|
|
self._http = http_client
|
||
|
|
self._timeout = timeout or self.DEFAULT_TIMEOUT_S
|
||
|
|
|
||
|
|
@property
|
||
|
|
def domain(self) -> str:
|
||
|
|
return getattr(self._lark, "domain", "https://open.feishu.cn")
|
||
|
|
|
||
|
|
async def _get_token(self) -> str:
|
||
|
|
resp = self._lark.auth.tenant_access_token_internal()
|
||
|
|
if not resp.success():
|
||
|
|
raise RuntimeError(f"Token acquisition failed: {resp.msg}")
|
||
|
|
return resp.token
|
||
|
|
|
||
|
|
async def get(self, path: str, **params: Any) -> dict:
|
||
|
|
import httpx
|
||
|
|
|
||
|
|
token = await self._get_token()
|
||
|
|
client = self._http or httpx.AsyncClient(timeout=httpx.Timeout(self._timeout))
|
||
|
|
try:
|
||
|
|
headers = {"Authorization": f"Bearer {token}"}
|
||
|
|
url = f"{self.domain}{path}"
|
||
|
|
resp = await client.get(url, headers=headers, params=params)
|
||
|
|
if resp.status_code != 200:
|
||
|
|
raise RuntimeError(f"HTTP {resp.status_code}: {resp.text[:300]}")
|
||
|
|
return resp.json() if resp.text else {}
|
||
|
|
finally:
|
||
|
|
if self._http is None:
|
||
|
|
await client.aclose()
|
||
|
|
|
||
|
|
async def post(self, path: str, body: dict | None = None) -> dict:
|
||
|
|
import httpx
|
||
|
|
|
||
|
|
token = await self._get_token()
|
||
|
|
client = self._http or httpx.AsyncClient(timeout=httpx.Timeout(self._timeout))
|
||
|
|
try:
|
||
|
|
headers = {
|
||
|
|
"Authorization": f"Bearer {token}",
|
||
|
|
"Content-Type": "application/json",
|
||
|
|
}
|
||
|
|
url = f"{self.domain}{path}"
|
||
|
|
resp = await client.post(url, headers=headers, json=body)
|
||
|
|
if resp.status_code != 200:
|
||
|
|
raise RuntimeError(f"HTTP {resp.status_code}: {resp.text[:300]}")
|
||
|
|
return resp.json() if resp.text else {}
|
||
|
|
finally:
|
||
|
|
if self._http is None:
|
||
|
|
await client.aclose()
|
||
|
|
|
||
|
|
async def patch(self, path: str, body: dict) -> dict:
|
||
|
|
import httpx
|
||
|
|
|
||
|
|
token = await self._get_token()
|
||
|
|
client = self._http or httpx.AsyncClient(timeout=httpx.Timeout(self._timeout))
|
||
|
|
try:
|
||
|
|
headers = {
|
||
|
|
"Authorization": f"Bearer {token}",
|
||
|
|
"Content-Type": "application/json",
|
||
|
|
}
|
||
|
|
url = f"{self.domain}{path}"
|
||
|
|
resp = await client.patch(url, headers=headers, json=body)
|
||
|
|
if resp.status_code != 200:
|
||
|
|
raise RuntimeError(f"HTTP {resp.status_code}: {resp.text[:300]}")
|
||
|
|
return resp.json() if resp.text else {}
|
||
|
|
finally:
|
||
|
|
if self._http is None:
|
||
|
|
await client.aclose()
|
||
|
|
|
||
|
|
async def delete(self, path: str) -> dict:
|
||
|
|
import httpx
|
||
|
|
|
||
|
|
token = await self._get_token()
|
||
|
|
client = self._http or httpx.AsyncClient(timeout=httpx.Timeout(self._timeout))
|
||
|
|
try:
|
||
|
|
headers = {"Authorization": f"Bearer {token}"}
|
||
|
|
url = f"{self.domain}{path}"
|
||
|
|
resp = await client.delete(url, headers=headers)
|
||
|
|
if resp.status_code != 200:
|
||
|
|
raise RuntimeError(f"HTTP {resp.status_code}: {resp.text[:300]}")
|
||
|
|
return resp.json() if resp.text else {}
|
||
|
|
finally:
|
||
|
|
if self._http is None:
|
||
|
|
await client.aclose()
|