first commit
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import ssl
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
@@ -12,6 +13,17 @@ from .specs import FIELD_SPECS, PRIMARY_FIELD_NAME
|
|||||||
|
|
||||||
|
|
||||||
FEISHU_API_BASE = "https://open.feishu.cn/open-apis"
|
FEISHU_API_BASE = "https://open.feishu.cn/open-apis"
|
||||||
|
FEISHU_HTTP_RETRY_COUNT = 3
|
||||||
|
FEISHU_HTTP_RETRY_DELAY = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def create_feishu_tls12_context() -> ssl.SSLContext:
|
||||||
|
"""Create a verified TLS 1.2 context for the Feishu network path."""
|
||||||
|
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||||
|
context.maximum_version = ssl.TLSVersion.TLSv1_2
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class FeishuApiError(RuntimeError):
|
class FeishuApiError(RuntimeError):
|
||||||
@@ -25,6 +37,7 @@ class FeishuFieldValidationError(FeishuApiError):
|
|||||||
class FeishuClient:
|
class FeishuClient:
|
||||||
def __init__(self, config: AppConfig) -> None:
|
def __init__(self, config: AppConfig) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
|
self._opener = request.build_opener(request.HTTPSHandler(context=create_feishu_tls12_context()))
|
||||||
|
|
||||||
def request_json(
|
def request_json(
|
||||||
self,
|
self,
|
||||||
@@ -33,6 +46,7 @@ class FeishuClient:
|
|||||||
method: str = "GET",
|
method: str = "GET",
|
||||||
headers: dict[str, str] | None = None,
|
headers: dict[str, str] | None = None,
|
||||||
payload: dict[str, Any] | None = None,
|
payload: dict[str, Any] | None = None,
|
||||||
|
retry_transport: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
body = None
|
body = None
|
||||||
request_headers = {"Content-Type": "application/json; charset=utf-8"}
|
request_headers = {"Content-Type": "application/json; charset=utf-8"}
|
||||||
@@ -41,21 +55,32 @@ class FeishuClient:
|
|||||||
if payload is not None:
|
if payload is not None:
|
||||||
body = json.dumps(payload).encode("utf-8")
|
body = json.dumps(payload).encode("utf-8")
|
||||||
|
|
||||||
|
# Reads and token acquisition are idempotent. Do not automatically retry
|
||||||
|
# record-writing POST requests because a connection can fail after send.
|
||||||
|
attempts = FEISHU_HTTP_RETRY_COUNT if retry_transport or method.upper() == "GET" else 1
|
||||||
|
for attempt in range(1, attempts + 1):
|
||||||
req = request.Request(url, data=body, headers=request_headers, method=method)
|
req = request.Request(url, data=body, headers=request_headers, method=method)
|
||||||
try:
|
try:
|
||||||
with request.urlopen(req, timeout=30) as response:
|
with self._opener.open(req, timeout=30) as response:
|
||||||
return json.loads(response.read().decode("utf-8"))
|
return json.loads(response.read().decode("utf-8"))
|
||||||
except error.HTTPError as exc:
|
except error.HTTPError as exc:
|
||||||
detail = exc.read().decode("utf-8", errors="replace")
|
detail = exc.read().decode("utf-8", errors="replace")
|
||||||
raise RuntimeError(f"请求失败: {exc.code} {detail}") from exc
|
raise RuntimeError(f"请求失败: {exc.code} {detail}") from exc
|
||||||
except error.URLError as exc:
|
except (error.URLError, ssl.SSLError) as exc:
|
||||||
raise RuntimeError(f"无法连接飞书接口: {exc.reason}") from exc
|
if attempt == attempts:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"无法连接飞书接口(TLS 1.2,已尝试 {attempts} 次): {exc}"
|
||||||
|
) from exc
|
||||||
|
time.sleep(FEISHU_HTTP_RETRY_DELAY * attempt)
|
||||||
|
|
||||||
|
raise AssertionError("unreachable")
|
||||||
|
|
||||||
def get_tenant_access_token(self) -> str:
|
def get_tenant_access_token(self) -> str:
|
||||||
data = self.request_json(
|
data = self.request_json(
|
||||||
f"{FEISHU_API_BASE}/auth/v3/tenant_access_token/internal",
|
f"{FEISHU_API_BASE}/auth/v3/tenant_access_token/internal",
|
||||||
method="POST",
|
method="POST",
|
||||||
payload={"app_id": self.config.feishu_app_id, "app_secret": self.config.feishu_app_secret},
|
payload={"app_id": self.config.feishu_app_id, "app_secret": self.config.feishu_app_secret},
|
||||||
|
retry_transport=True,
|
||||||
)
|
)
|
||||||
if data.get("code") != 0:
|
if data.get("code") != 0:
|
||||||
raise FeishuApiError(f"获取 tenant_access_token 失败: {data}")
|
raise FeishuApiError(f"获取 tenant_access_token 失败: {data}")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ version = "0.1.0"
|
|||||||
description = "Internal inventory analytics workflows for batch upload, channel inventory sync, and batch inventory reporting."
|
description = "Internal inventory analytics workflows for batch upload, channel inventory sync, and batch inventory reporting."
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"importlib-metadata>=8.0.0",
|
||||||
"openpyxl>=3.1.2",
|
"openpyxl>=3.1.2",
|
||||||
"pandas",
|
"pandas",
|
||||||
"prefect==3.6.8",
|
"prefect==3.6.8",
|
||||||
|
|||||||
Reference in New Issue
Block a user