redfox-python-sdk 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- redfox/__init__.py +9 -3
- redfox/client.py +322 -67
- redfox/endpoints/bilibili.py +52 -24
- redfox/endpoints/doubao_video.py +42 -27
- redfox/endpoints/gpt_image.py +16 -10
- redfox/endpoints/wechat.py +27 -17
- redfox/endpoints/xiaohongshu.py +57 -34
- redfox_python_sdk-0.2.2.dist-info/METADATA +371 -0
- redfox_python_sdk-0.2.2.dist-info/RECORD +18 -0
- {redfox_python_sdk-0.2.0.dist-info → redfox_python_sdk-0.2.2.dist-info}/WHEEL +1 -1
- redfox_python_sdk-0.2.0.dist-info/METADATA +0 -49
- redfox_python_sdk-0.2.0.dist-info/RECORD +0 -18
- {redfox_python_sdk-0.2.0.dist-info → redfox_python_sdk-0.2.2.dist-info}/top_level.txt +0 -0
redfox/__init__.py
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
"""RedFox Python SDK - 红狐数据平台 Python 客户端"""
|
|
2
2
|
|
|
3
|
-
from .client import RedFoxClient
|
|
3
|
+
from .client import RedFoxClient, AsyncRedFoxClient
|
|
4
4
|
from .exceptions import RedFoxAPIError, RedFoxAuthError, RedFoxRateLimitError
|
|
5
5
|
|
|
6
|
-
__version__ = "0.2.
|
|
7
|
-
__all__ = [
|
|
6
|
+
__version__ = "0.2.2"
|
|
7
|
+
__all__ = [
|
|
8
|
+
"RedFoxClient",
|
|
9
|
+
"AsyncRedFoxClient",
|
|
10
|
+
"RedFoxAPIError",
|
|
11
|
+
"RedFoxAuthError",
|
|
12
|
+
"RedFoxRateLimitError",
|
|
13
|
+
]
|
redfox/client.py
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
"""RedFox SDK 核心客户端"""
|
|
1
|
+
"""RedFox SDK 核心客户端 — 同步 + 异步双客户端,自动重试,零配置"""
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
import random
|
|
6
|
+
import logging
|
|
4
7
|
from typing import Optional, Dict, Any
|
|
5
8
|
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
6
11
|
from .exceptions import RedFoxAPIError, RedFoxAuthError, RedFoxRateLimitError
|
|
12
|
+
|
|
13
|
+
# 导入端点(同步和异步共享同一套端点类)
|
|
7
14
|
from .endpoints.douyin import DouyinAPI
|
|
8
15
|
from .endpoints.xiaohongshu import XiaohongshuAPI
|
|
9
16
|
from .endpoints.wechat import WechatAPI
|
|
@@ -15,16 +22,102 @@ from .endpoints.doubao_image import DoubaoImageAPI
|
|
|
15
22
|
from .endpoints.doubao_video import DoubaoVideoAPI
|
|
16
23
|
from .endpoints.ai_search import AISearchAPI
|
|
17
24
|
|
|
25
|
+
logger = logging.getLogger("redfox")
|
|
26
|
+
|
|
27
|
+
# 可重试的 HTTP 状态码
|
|
28
|
+
RETRYABLE_STATUSES = {429, 500, 502, 503, 504}
|
|
29
|
+
|
|
30
|
+
# 可重试的网络异常
|
|
31
|
+
RETRYABLE_EXCEPTIONS = (
|
|
32
|
+
httpx.TimeoutException,
|
|
33
|
+
httpx.ConnectError,
|
|
34
|
+
httpx.RemoteProtocolError,
|
|
35
|
+
httpx.NetworkError,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _compute_delay(attempt: int, backoff_factor: float = 0.5) -> float:
|
|
40
|
+
"""计算指数退避延迟(含随机抖动)"""
|
|
41
|
+
base = backoff_factor * (2 ** attempt)
|
|
42
|
+
jitter = base * random.uniform(0.5, 1.5)
|
|
43
|
+
return min(jitter, 30.0) # 上限 30 秒
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _should_retry(status_code: int, exception: Optional[Exception] = None) -> bool:
|
|
47
|
+
"""判断是否应该重试"""
|
|
48
|
+
if exception is not None:
|
|
49
|
+
return isinstance(exception, RETRYABLE_EXCEPTIONS)
|
|
50
|
+
return status_code in RETRYABLE_STATUSES
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _RequestMixin:
|
|
54
|
+
"""HTTP 请求 + 重试 + 响应处理的共享逻辑"""
|
|
55
|
+
|
|
56
|
+
def _prepare_request(
|
|
57
|
+
self, method: str, path: str,
|
|
58
|
+
params: Optional[Dict[str, Any]] = None,
|
|
59
|
+
data: Optional[Dict[str, Any]] = None,
|
|
60
|
+
) -> tuple:
|
|
61
|
+
"""准备请求 URL 和清洗后的参数"""
|
|
62
|
+
url = f"{self.base_url}{path}"
|
|
63
|
+
if data:
|
|
64
|
+
data = {k: v for k, v in data.items() if v is not None}
|
|
65
|
+
if params:
|
|
66
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
67
|
+
return url, params, data
|
|
68
|
+
|
|
69
|
+
def _handle_response(self, response) -> dict:
|
|
70
|
+
"""处理 API 响应,提取 data 或抛出结构化异常"""
|
|
71
|
+
if response.status_code == 401:
|
|
72
|
+
raise RedFoxAuthError(
|
|
73
|
+
"API Key 无效或缺失,请在 https://redfox.hk/settings/api-keys?source=github 获取",
|
|
74
|
+
code=401,
|
|
75
|
+
)
|
|
76
|
+
if response.status_code == 429:
|
|
77
|
+
raise RedFoxRateLimitError("请求频率超限,请稍后重试", code=429)
|
|
78
|
+
if response.status_code >= 500:
|
|
79
|
+
raise RedFoxAPIError("服务端错误", code=response.status_code)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
result = response.json()
|
|
83
|
+
except ValueError:
|
|
84
|
+
raise RedFoxAPIError(f"响应解析失败: {response.text[:200]}")
|
|
85
|
+
|
|
86
|
+
code = result.get("code")
|
|
87
|
+
if code and code != 2000:
|
|
88
|
+
msg = result.get("msg", "未知错误")
|
|
89
|
+
if code == 401 or code == 4001:
|
|
90
|
+
raise RedFoxAuthError(
|
|
91
|
+
f"{msg},请在 https://redfox.hk/settings/api-keys?source=github 获取有效的 API Key",
|
|
92
|
+
code=code,
|
|
93
|
+
response=result,
|
|
94
|
+
)
|
|
95
|
+
raise RedFoxAPIError(msg, code=code, response=result)
|
|
96
|
+
|
|
97
|
+
return result.get("data", result)
|
|
98
|
+
|
|
99
|
+
def _map_exception(self, exc: Exception) -> RedFoxAPIError:
|
|
100
|
+
"""将网络异常映射为 RedFoxAPIError"""
|
|
101
|
+
if isinstance(exc, httpx.TimeoutException):
|
|
102
|
+
return RedFoxAPIError("请求超时,请稍后重试")
|
|
103
|
+
if isinstance(exc, (httpx.ConnectError, httpx.NetworkError)):
|
|
104
|
+
return RedFoxAPIError("网络连接失败,请检查网络")
|
|
105
|
+
return RedFoxAPIError(f"请求异常: {str(exc)}")
|
|
106
|
+
|
|
18
107
|
|
|
19
|
-
class RedFoxClient:
|
|
108
|
+
class RedFoxClient(_RequestMixin):
|
|
20
109
|
"""
|
|
21
|
-
RedFox
|
|
110
|
+
RedFox 红狐数据平台客户端(同步)
|
|
22
111
|
|
|
23
112
|
使用示例::
|
|
24
113
|
|
|
25
114
|
from redfox import RedFoxClient
|
|
26
115
|
|
|
27
|
-
|
|
116
|
+
# 零配置:自动从环境变量 REDFOX_API_KEY 读取
|
|
117
|
+
client = RedFoxClient()
|
|
118
|
+
|
|
119
|
+
# 或显式传入
|
|
120
|
+
client = RedFoxClient(api_key="ak_xxx")
|
|
28
121
|
|
|
29
122
|
# 搜索抖音作品
|
|
30
123
|
result = client.douyin.search_articles(keyword="AI")
|
|
@@ -32,42 +125,55 @@ class RedFoxClient:
|
|
|
32
125
|
# 搜索小红书作品
|
|
33
126
|
result = client.xiaohongshu.search_articles(keyword="AI")
|
|
34
127
|
|
|
35
|
-
# 搜索公众号文章
|
|
36
|
-
result = client.wechat.search_articles(keyword="AI")
|
|
37
|
-
|
|
38
128
|
# AI 搜索
|
|
39
129
|
task = client.ai_search.kimi_submit(inquiry_text="夏日茶饮推荐")
|
|
40
130
|
|
|
41
|
-
:param api_key: RedFox API Key
|
|
131
|
+
:param api_key: RedFox API Key。不传则从环境变量 ``REDFOX_API_KEY`` 读取
|
|
42
132
|
:param base_url: API 基础地址,默认 https://redfox.hk
|
|
43
|
-
:param timeout:
|
|
133
|
+
:param timeout: 请求超时(秒),默认 30
|
|
134
|
+
:param max_retries: 最大重试次数(指数退避),默认 3
|
|
135
|
+
:param backoff_factor: 退避基础因子,默认 0.5
|
|
44
136
|
"""
|
|
45
137
|
|
|
46
138
|
DEFAULT_BASE_URL = "https://redfox.hk"
|
|
47
139
|
DEFAULT_TIMEOUT = 30
|
|
140
|
+
DEFAULT_MAX_RETRIES = 3
|
|
141
|
+
DEFAULT_BACKOFF_FACTOR = 0.5
|
|
48
142
|
|
|
49
143
|
def __init__(
|
|
50
144
|
self,
|
|
51
|
-
api_key: str,
|
|
145
|
+
api_key: Optional[str] = None,
|
|
52
146
|
base_url: str = DEFAULT_BASE_URL,
|
|
53
147
|
timeout: int = DEFAULT_TIMEOUT,
|
|
148
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
149
|
+
backoff_factor: float = DEFAULT_BACKOFF_FACTOR,
|
|
54
150
|
):
|
|
151
|
+
# 零配置:未传 api_key 时从环境变量读取
|
|
152
|
+
if api_key is None:
|
|
153
|
+
api_key = os.getenv("REDFOX_API_KEY", "")
|
|
154
|
+
|
|
55
155
|
if not api_key:
|
|
56
156
|
raise ValueError(
|
|
57
|
-
"api_key
|
|
157
|
+
"api_key 不能为空。请设置环境变量 REDFOX_API_KEY,"
|
|
158
|
+
"或传入 api_key 参数。"
|
|
159
|
+
"获取地址: https://redfox.hk/settings/api-keys?source=github"
|
|
58
160
|
)
|
|
59
161
|
|
|
60
162
|
self.api_key = api_key
|
|
61
163
|
self.base_url = base_url.rstrip("/")
|
|
62
164
|
self.timeout = timeout
|
|
165
|
+
self.max_retries = max_retries
|
|
166
|
+
self.backoff_factor = backoff_factor
|
|
63
167
|
|
|
64
|
-
self.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
168
|
+
self._client = httpx.Client(
|
|
169
|
+
timeout=httpx.Timeout(timeout),
|
|
170
|
+
headers={
|
|
171
|
+
"Content-Type": "application/json",
|
|
172
|
+
"REDFOX_API_KEY": self.api_key,
|
|
173
|
+
},
|
|
174
|
+
)
|
|
69
175
|
|
|
70
|
-
# 初始化各平台 API
|
|
176
|
+
# 初始化各平台 API 模块(端点类与异步客户端共享)
|
|
71
177
|
self.douyin = DouyinAPI(self)
|
|
72
178
|
self.xiaohongshu = XiaohongshuAPI(self)
|
|
73
179
|
self.wechat = WechatAPI(self)
|
|
@@ -87,39 +193,57 @@ class RedFoxClient:
|
|
|
87
193
|
data: Optional[Dict[str, Any]] = None,
|
|
88
194
|
) -> dict:
|
|
89
195
|
"""
|
|
90
|
-
发送 API
|
|
196
|
+
发送 API 请求(含自动重试 + 指数退避)
|
|
91
197
|
|
|
92
198
|
:param method: HTTP 方法(GET/POST)
|
|
93
199
|
:param path: API 路径
|
|
94
200
|
:param params: URL 查询参数
|
|
95
201
|
:param data: 请求体 JSON 数据
|
|
96
202
|
:return: API 响应 data 字段内容
|
|
97
|
-
:raises RedFoxAPIError:
|
|
203
|
+
:raises RedFoxAPIError: 重试耗尽后仍失败时抛出
|
|
98
204
|
"""
|
|
99
|
-
url =
|
|
205
|
+
url, params, data = self._prepare_request(method, path, params, data)
|
|
206
|
+
last_exception = None
|
|
100
207
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
208
|
+
for attempt in range(self.max_retries + 1):
|
|
209
|
+
try:
|
|
210
|
+
response = self._client.request(
|
|
211
|
+
method=method,
|
|
212
|
+
url=url,
|
|
213
|
+
params=params,
|
|
214
|
+
json=data,
|
|
215
|
+
)
|
|
106
216
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
217
|
+
# 需要重试的 HTTP 状态码
|
|
218
|
+
if response.status_code in RETRYABLE_STATUSES and attempt < self.max_retries:
|
|
219
|
+
delay = _compute_delay(attempt, self.backoff_factor)
|
|
220
|
+
logger.debug(
|
|
221
|
+
"收到 %d,第 %d/%d 次重试,等待 %.1fs",
|
|
222
|
+
response.status_code, attempt + 1, self.max_retries, delay,
|
|
223
|
+
)
|
|
224
|
+
time.sleep(delay)
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
return self._handle_response(response)
|
|
228
|
+
|
|
229
|
+
except RETRYABLE_EXCEPTIONS as exc:
|
|
230
|
+
last_exception = exc
|
|
231
|
+
if attempt < self.max_retries:
|
|
232
|
+
delay = _compute_delay(attempt, self.backoff_factor)
|
|
233
|
+
logger.debug(
|
|
234
|
+
"网络异常 %s,第 %d/%d 次重试,等待 %.1fs",
|
|
235
|
+
type(exc).__name__, attempt + 1, self.max_retries, delay,
|
|
236
|
+
)
|
|
237
|
+
time.sleep(delay)
|
|
238
|
+
else:
|
|
239
|
+
raise self._map_exception(exc)
|
|
240
|
+
|
|
241
|
+
except (RedFoxAPIError, RedFoxAuthError, RedFoxRateLimitError):
|
|
242
|
+
# 业务异常不重试,直接抛出
|
|
243
|
+
raise
|
|
121
244
|
|
|
122
|
-
|
|
245
|
+
# 重试耗尽
|
|
246
|
+
raise self._map_exception(last_exception) if last_exception else RedFoxAPIError("请求失败")
|
|
123
247
|
|
|
124
248
|
def post(self, path: str, data: Optional[Dict[str, Any]] = None) -> dict:
|
|
125
249
|
"""发送 POST 请求"""
|
|
@@ -129,34 +253,165 @@ class RedFoxClient:
|
|
|
129
253
|
"""发送 GET 请求"""
|
|
130
254
|
return self.request("GET", path, params=params)
|
|
131
255
|
|
|
132
|
-
def
|
|
133
|
-
"""
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
256
|
+
def close(self):
|
|
257
|
+
"""关闭 HTTP 连接"""
|
|
258
|
+
self._client.close()
|
|
259
|
+
|
|
260
|
+
def __enter__(self):
|
|
261
|
+
return self
|
|
262
|
+
|
|
263
|
+
def __exit__(self, *args):
|
|
264
|
+
self.close()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class AsyncRedFoxClient(_RequestMixin):
|
|
268
|
+
"""
|
|
269
|
+
RedFox 红狐数据平台客户端(异步)
|
|
270
|
+
|
|
271
|
+
使用示例::
|
|
272
|
+
|
|
273
|
+
import asyncio
|
|
274
|
+
from redfox import AsyncRedFoxClient
|
|
275
|
+
|
|
276
|
+
async def main():
|
|
277
|
+
async with AsyncRedFoxClient() as client:
|
|
278
|
+
# 所有 API 调用与同步客户端完全一致,只需 await
|
|
279
|
+
result = await client.douyin.search_articles(keyword="AI")
|
|
280
|
+
print(result)
|
|
281
|
+
|
|
282
|
+
asyncio.run(main())
|
|
283
|
+
|
|
284
|
+
:param api_key: RedFox API Key。不传则从环境变量 ``REDFOX_API_KEY`` 读取
|
|
285
|
+
:param base_url: API 基础地址,默认 https://redfox.hk
|
|
286
|
+
:param timeout: 请求超时(秒),默认 30
|
|
287
|
+
:param max_retries: 最大重试次数(指数退避),默认 3
|
|
288
|
+
:param backoff_factor: 退避基础因子,默认 0.5
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
DEFAULT_BASE_URL = "https://redfox.hk"
|
|
292
|
+
DEFAULT_TIMEOUT = 30
|
|
293
|
+
DEFAULT_MAX_RETRIES = 3
|
|
294
|
+
DEFAULT_BACKOFF_FACTOR = 0.5
|
|
295
|
+
|
|
296
|
+
def __init__(
|
|
297
|
+
self,
|
|
298
|
+
api_key: Optional[str] = None,
|
|
299
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
300
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
301
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
302
|
+
backoff_factor: float = DEFAULT_BACKOFF_FACTOR,
|
|
303
|
+
):
|
|
304
|
+
if api_key is None:
|
|
305
|
+
api_key = os.getenv("REDFOX_API_KEY", "")
|
|
306
|
+
|
|
307
|
+
if not api_key:
|
|
308
|
+
raise ValueError(
|
|
309
|
+
"api_key 不能为空。请设置环境变量 REDFOX_API_KEY,"
|
|
310
|
+
"或传入 api_key 参数。"
|
|
311
|
+
"获取地址: https://redfox.hk/settings/api-keys?source=github"
|
|
139
312
|
)
|
|
140
|
-
if response.status_code == 429:
|
|
141
|
-
raise RedFoxRateLimitError("请求频率超限,请稍后重试", code=429)
|
|
142
|
-
if response.status_code >= 500:
|
|
143
|
-
raise RedFoxAPIError(f"服务端错误", code=response.status_code)
|
|
144
313
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
314
|
+
self.api_key = api_key
|
|
315
|
+
self.base_url = base_url.rstrip("/")
|
|
316
|
+
self.timeout = timeout
|
|
317
|
+
self.max_retries = max_retries
|
|
318
|
+
self.backoff_factor = backoff_factor
|
|
149
319
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
320
|
+
self._client = httpx.AsyncClient(
|
|
321
|
+
timeout=httpx.Timeout(timeout),
|
|
322
|
+
headers={
|
|
323
|
+
"Content-Type": "application/json",
|
|
324
|
+
"REDFOX_API_KEY": self.api_key,
|
|
325
|
+
},
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# 初始化各平台 API 模块(与同步客户端共享同一套端点类)
|
|
329
|
+
self.douyin = DouyinAPI(self)
|
|
330
|
+
self.xiaohongshu = XiaohongshuAPI(self)
|
|
331
|
+
self.wechat = WechatAPI(self)
|
|
332
|
+
self.bilibili = BilibiliAPI(self)
|
|
333
|
+
self.toutiao = ToutiaoAPI(self)
|
|
334
|
+
self.tiktok = TikTokAPI(self)
|
|
335
|
+
self.gpt_image = GPTImageAPI(self)
|
|
336
|
+
self.doubao_image = DoubaoImageAPI(self)
|
|
337
|
+
self.doubao_video = DoubaoVideoAPI(self)
|
|
338
|
+
self.ai_search = AISearchAPI(self)
|
|
339
|
+
|
|
340
|
+
async def request(
|
|
341
|
+
self,
|
|
342
|
+
method: str,
|
|
343
|
+
path: str,
|
|
344
|
+
params: Optional[Dict[str, Any]] = None,
|
|
345
|
+
data: Optional[Dict[str, Any]] = None,
|
|
346
|
+
) -> dict:
|
|
347
|
+
"""
|
|
348
|
+
异步发送 API 请求(含自动重试 + 指数退避)
|
|
349
|
+
|
|
350
|
+
:param method: HTTP 方法(GET/POST)
|
|
351
|
+
:param path: API 路径
|
|
352
|
+
:param params: URL 查询参数
|
|
353
|
+
:param data: 请求体 JSON 数据
|
|
354
|
+
:return: API 响应 data 字段内容
|
|
355
|
+
"""
|
|
356
|
+
url, params, data = self._prepare_request(method, path, params, data)
|
|
357
|
+
last_exception = None
|
|
358
|
+
|
|
359
|
+
for attempt in range(self.max_retries + 1):
|
|
360
|
+
try:
|
|
361
|
+
response = await self._client.request(
|
|
362
|
+
method=method,
|
|
363
|
+
url=url,
|
|
364
|
+
params=params,
|
|
365
|
+
json=data,
|
|
159
366
|
)
|
|
160
|
-
raise RedFoxAPIError(msg, code=code, response=result)
|
|
161
367
|
|
|
162
|
-
|
|
368
|
+
if response.status_code in RETRYABLE_STATUSES and attempt < self.max_retries:
|
|
369
|
+
delay = _compute_delay(attempt, self.backoff_factor)
|
|
370
|
+
logger.debug(
|
|
371
|
+
"收到 %d,第 %d/%d 次重试,等待 %.1fs",
|
|
372
|
+
response.status_code, attempt + 1, self.max_retries, delay,
|
|
373
|
+
)
|
|
374
|
+
await self._async_sleep(delay)
|
|
375
|
+
continue
|
|
376
|
+
|
|
377
|
+
return self._handle_response(response)
|
|
378
|
+
|
|
379
|
+
except RETRYABLE_EXCEPTIONS as exc:
|
|
380
|
+
last_exception = exc
|
|
381
|
+
if attempt < self.max_retries:
|
|
382
|
+
delay = _compute_delay(attempt, self.backoff_factor)
|
|
383
|
+
logger.debug(
|
|
384
|
+
"网络异常 %s,第 %d/%d 次重试,等待 %.1fs",
|
|
385
|
+
type(exc).__name__, attempt + 1, self.max_retries, delay,
|
|
386
|
+
)
|
|
387
|
+
await self._async_sleep(delay)
|
|
388
|
+
else:
|
|
389
|
+
raise self._map_exception(exc)
|
|
390
|
+
|
|
391
|
+
except (RedFoxAPIError, RedFoxAuthError, RedFoxRateLimitError):
|
|
392
|
+
raise
|
|
393
|
+
|
|
394
|
+
raise self._map_exception(last_exception) if last_exception else RedFoxAPIError("请求失败")
|
|
395
|
+
|
|
396
|
+
async def post(self, path: str, data: Optional[Dict[str, Any]] = None) -> dict:
|
|
397
|
+
"""异步发送 POST 请求"""
|
|
398
|
+
return await self.request("POST", path, data=data)
|
|
399
|
+
|
|
400
|
+
async def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> dict:
|
|
401
|
+
"""异步发送 GET 请求"""
|
|
402
|
+
return await self.request("GET", path, params=params)
|
|
403
|
+
|
|
404
|
+
async def _async_sleep(self, seconds: float):
|
|
405
|
+
"""异步延时"""
|
|
406
|
+
import asyncio
|
|
407
|
+
await asyncio.sleep(seconds)
|
|
408
|
+
|
|
409
|
+
async def close(self):
|
|
410
|
+
"""关闭 HTTP 连接"""
|
|
411
|
+
await self._client.aclose()
|
|
412
|
+
|
|
413
|
+
async def __aenter__(self):
|
|
414
|
+
return self
|
|
415
|
+
|
|
416
|
+
async def __aexit__(self, *args):
|
|
417
|
+
await self.close()
|
redfox/endpoints/bilibili.py
CHANGED
|
@@ -24,32 +24,46 @@ class BilibiliAPI:
|
|
|
24
24
|
"/story/api/bili/data/accountDetail", data={"mid": mid}
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
-
def get_work(
|
|
27
|
+
def get_work(
|
|
28
|
+
self,
|
|
29
|
+
bvid: str = None,
|
|
30
|
+
work_url: str = None,
|
|
31
|
+
) -> dict:
|
|
28
32
|
"""
|
|
29
33
|
获取哔哩哔哩作品内容详情(优质库)
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
bvid 和 work_url 至少传一个。
|
|
36
|
+
|
|
37
|
+
:param bvid: 作品 BV 号
|
|
38
|
+
:param work_url: 作品链接,支持 bilibili.com/video/BV1xxx 或 b23.tv 格式
|
|
32
39
|
:return: 作品详情字典
|
|
33
40
|
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
data: Dict[str, Any] = {}
|
|
42
|
+
if bvid:
|
|
43
|
+
data["bvId"] = bvid
|
|
44
|
+
if work_url:
|
|
45
|
+
data["workUrl"] = work_url
|
|
46
|
+
return self._client.post("/story/api/bili/data/workDetail", data=data)
|
|
37
47
|
|
|
38
48
|
def search_users(
|
|
39
49
|
self,
|
|
40
50
|
keyword: str,
|
|
41
|
-
|
|
51
|
+
page: int = 1,
|
|
52
|
+
page_size: int = None,
|
|
42
53
|
order: str = None,
|
|
43
54
|
) -> dict:
|
|
44
55
|
"""
|
|
45
56
|
搜索关键词获取哔哩哔哩账号(优质库)
|
|
46
57
|
|
|
47
58
|
:param keyword: 搜索关键词(必填)
|
|
48
|
-
:param
|
|
49
|
-
:param
|
|
59
|
+
:param page: 页码,从 1 开始(必填)
|
|
60
|
+
:param page_size: 每页条数,默认 10,最大 50
|
|
61
|
+
:param order: 排序:follower=粉丝数 / like=获赞数,默认相关性
|
|
50
62
|
:return: 搜索结果字典
|
|
51
63
|
"""
|
|
52
|
-
data: Dict[str, Any] = {"keyword": keyword, "page": str(
|
|
64
|
+
data: Dict[str, Any] = {"keyword": keyword, "page": str(page)}
|
|
65
|
+
if page_size is not None:
|
|
66
|
+
data["pageSize"] = page_size
|
|
53
67
|
if order is not None:
|
|
54
68
|
data["order"] = order
|
|
55
69
|
return self._client.post("/story/api/bili/data/accountSearch", data=data)
|
|
@@ -57,39 +71,53 @@ class BilibiliAPI:
|
|
|
57
71
|
def search_articles(
|
|
58
72
|
self,
|
|
59
73
|
keyword: str,
|
|
60
|
-
|
|
74
|
+
page: int = 1,
|
|
75
|
+
page_size: int = None,
|
|
61
76
|
order: str = None,
|
|
62
77
|
) -> dict:
|
|
63
78
|
"""
|
|
64
79
|
搜索关键词获取哔哩哔哩作品(优质库)
|
|
65
80
|
|
|
66
81
|
:param keyword: 搜索关键词(必填)
|
|
67
|
-
:param
|
|
68
|
-
:param
|
|
82
|
+
:param page: 页码,从 1 开始(必填)
|
|
83
|
+
:param page_size: 每页条数,默认 10,最大 50
|
|
84
|
+
:param order: 排序:time=发布时间 / play=播放数 / like=点赞数 / comment=评论数 / favorite=收藏数,默认 time
|
|
69
85
|
:return: 搜索结果字典
|
|
70
86
|
"""
|
|
71
|
-
data: Dict[str, Any] = {"keyword": keyword, "page": str(
|
|
87
|
+
data: Dict[str, Any] = {"keyword": keyword, "page": str(page)}
|
|
88
|
+
if page_size is not None:
|
|
89
|
+
data["pageSize"] = page_size
|
|
72
90
|
if order is not None:
|
|
73
91
|
data["order"] = order
|
|
74
92
|
return self._client.post("/story/api/bili/data/workSearch", data=data)
|
|
75
93
|
|
|
76
94
|
def get_user_works(
|
|
77
95
|
self,
|
|
78
|
-
mid: str,
|
|
79
|
-
|
|
80
|
-
|
|
96
|
+
mid: str = None,
|
|
97
|
+
account_url: str = None,
|
|
98
|
+
page: int = 1,
|
|
99
|
+
page_size: int = None,
|
|
100
|
+
order: str = None,
|
|
81
101
|
) -> dict:
|
|
82
102
|
"""
|
|
83
103
|
获取哔哩哔哩账号作品列表(优质库)
|
|
84
104
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
:param
|
|
105
|
+
mid 和 account_url 至少传一个。
|
|
106
|
+
|
|
107
|
+
:param mid: 账号 MID
|
|
108
|
+
:param account_url: 主页链接,如 https://space.bilibili.com/946974
|
|
109
|
+
:param page: 页码,默认 1
|
|
110
|
+
:param page_size: 每页条数,默认 10,最大 50
|
|
111
|
+
:param order: 排序:time=发布时间 / play=播放数 / like=点赞数,默认 time
|
|
88
112
|
:return: 作品列表字典
|
|
89
113
|
"""
|
|
90
|
-
data: Dict[str, Any] = {
|
|
91
|
-
|
|
92
|
-
"
|
|
93
|
-
|
|
94
|
-
|
|
114
|
+
data: Dict[str, Any] = {"page": str(page)}
|
|
115
|
+
if mid:
|
|
116
|
+
data["mid"] = mid
|
|
117
|
+
if account_url:
|
|
118
|
+
data["accountUrl"] = account_url
|
|
119
|
+
if page_size is not None:
|
|
120
|
+
data["pageSize"] = page_size
|
|
121
|
+
if order is not None:
|
|
122
|
+
data["order"] = order
|
|
95
123
|
return self._client.post("/story/api/bili/data/accountWorkList", data=data)
|