dts-dance 0.2.2__tar.gz → 0.2.4__tar.gz

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.
Files changed (25) hide show
  1. {dts_dance-0.2.2 → dts_dance-0.2.4}/PKG-INFO +3 -2
  2. {dts_dance-0.2.2 → dts_dance-0.2.4}/dtsdance/bytecloud.py +2 -2
  3. {dts_dance-0.2.2 → dts_dance-0.2.4}/dtsdance/dflow.py +4 -4
  4. dts_dance-0.2.4/dtsdance/feishu_base.py +296 -0
  5. {dts_dance-0.2.2 → dts_dance-0.2.4}/dtsdance/feishu_table.py +71 -9
  6. {dts_dance-0.2.2 → dts_dance-0.2.4}/dtsdance/spacex_bytedts.py +48 -0
  7. {dts_dance-0.2.2 → dts_dance-0.2.4}/pyproject.toml +3 -2
  8. {dts_dance-0.2.2 → dts_dance-0.2.4}/uv.lock +22 -8
  9. dts_dance-0.2.2/dtsdance/feishu_base.py +0 -107
  10. {dts_dance-0.2.2 → dts_dance-0.2.4}/.gitignore +0 -0
  11. {dts_dance-0.2.2 → dts_dance-0.2.4}/.python-version +0 -0
  12. {dts_dance-0.2.2 → dts_dance-0.2.4}/CLAUDE.md +0 -0
  13. {dts_dance-0.2.2 → dts_dance-0.2.4}/DEV.md +0 -0
  14. {dts_dance-0.2.2 → dts_dance-0.2.4}/README.md +0 -0
  15. {dts_dance-0.2.2 → dts_dance-0.2.4}/config.yaml +0 -0
  16. {dts_dance-0.2.2 → dts_dance-0.2.4}/dtsdance/__init__.py +0 -0
  17. {dts_dance-0.2.2 → dts_dance-0.2.4}/dtsdance/dsyncer.py +0 -0
  18. {dts_dance-0.2.2 → dts_dance-0.2.4}/dtsdance/metrics_fe.py +0 -0
  19. {dts_dance-0.2.2 → dts_dance-0.2.4}/dtsdance/s3.py +0 -0
  20. {dts_dance-0.2.2 → dts_dance-0.2.4}/dtsdance/tcc_inner.py +0 -0
  21. {dts_dance-0.2.2 → dts_dance-0.2.4}/dtsdance/tcc_open.py +0 -0
  22. {dts_dance-0.2.2 → dts_dance-0.2.4}/tests/config.py +0 -0
  23. {dts_dance-0.2.2 → dts_dance-0.2.4}/tests/test_dflow.py +0 -0
  24. {dts_dance-0.2.2 → dts_dance-0.2.4}/tests/test_spacex.py +0 -0
  25. {dts_dance-0.2.2 → dts_dance-0.2.4}/tests/test_tcc.py +0 -0
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dts-dance
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: dts dance lib
5
5
  Keywords: observation,tools
6
6
  Requires-Python: >=3.12
7
- Requires-Dist: boto3<2.0.0,>=1.42.29
7
+ Requires-Dist: boto3<2.0.0,>=1.42.30
8
8
  Requires-Dist: loguru<0.8.0,>=0.7.3
9
9
  Requires-Dist: pyyaml<7.0.0,>=6.0.3
10
+ Requires-Dist: requests-toolbelt==1.0.0
10
11
  Requires-Dist: requests<3.0.0,>=2.32.5
11
12
  Description-Content-Type: text/markdown
12
13
 
@@ -20,9 +20,9 @@ class ByteCloudClient:
20
20
  """
21
21
 
22
22
  # 每小时刷新一次,单位为秒
23
- _REFRESH_INTERVAL = 1 * 60 * 60
23
+ _REFRESH_INTERVAL = 2 * 60 * 60
24
24
 
25
- def __init__(self, enable_jwt_cache: bool, sites: dict[str, SiteConfig] | None = None) -> None:
25
+ def __init__(self, sites: dict[str, SiteConfig] | None = None, enable_jwt_cache: bool = False) -> None:
26
26
  """
27
27
  初始化 ByteCloud Client
28
28
  从配置文件加载所有环境的信息,并为每个环境初始化 JWT 令牌
@@ -150,7 +150,7 @@ class DFlowClient:
150
150
  """
151
151
  # 根据环境生成对应的 scope 参数
152
152
  site_info = self.bytecloud_client.get_site_config(site)
153
- return f"{site_info.endpoint}/bytedts/datasync/detail/{task_id}?scope={site}"
153
+ return f"{site_info.endpoint}/bytedts/datasync/detail/{task_id}"
154
154
 
155
155
  def init_resources(self, site: str, ctrl_env: str) -> bool:
156
156
  """
@@ -179,7 +179,7 @@ class DFlowClient:
179
179
 
180
180
  def list_resources(self, site: str, ctrl_env: str) -> list[str]:
181
181
  """
182
- 列举 CTRL 环境资源列表
182
+ 列举 CTRL 环境资源列表,连同 agent 列表
183
183
 
184
184
  Args:
185
185
  site: 站点名称
@@ -198,8 +198,8 @@ class DFlowClient:
198
198
  response_data = self._make_request("POST", url, self.bytecloud_client.build_request_headers(site), json_data)
199
199
 
200
200
  try:
201
- data = cast(dict, response_data.get("data", {}))
202
- items = cast(list, data.get("items", []))
201
+ data = response_data.get("data", {})
202
+ items = data.get("items", [])
203
203
  return [item["name"] for item in items]
204
204
  except (KeyError, AttributeError, Exception) as e:
205
205
  raise Exception(f"无法从响应中提取 CTRL 环境资源列表数据: {str(e)}")
@@ -0,0 +1,296 @@
1
+ from datetime import datetime, timedelta
2
+ import threading
3
+ import json
4
+ from loguru import logger
5
+ from typing import Any
6
+ from requests_toolbelt import MultipartEncoder
7
+
8
+ import requests
9
+
10
+ URL_FEISHU_OPEN_API: str = "https://fsopen.bytedance.net/open-apis"
11
+
12
+
13
+ class FeishuBase:
14
+ """飞书基础类,传入机器人认证信息,支持自动token续期管理"""
15
+
16
+ def __init__(self, bot_app_id: str, bot_app_secret: str):
17
+ """
18
+ 初始化飞书基础类
19
+ """
20
+ self.tenant_access_token: str | None = None
21
+ self.token_expire_time: datetime | None = None
22
+ self._token_lock = threading.Lock()
23
+
24
+ self.bot_app_id = bot_app_id
25
+ self.bot_app_secret = bot_app_secret
26
+
27
+ def _get_tenant_access_token(self) -> str:
28
+ """
29
+ 获取tenant_access_token
30
+
31
+ Returns:
32
+ tenant_access_token字符串
33
+
34
+ Raises:
35
+ Exception: 获取token失败时抛出异常
36
+ """
37
+ url = f"{URL_FEISHU_OPEN_API}/auth/v3/tenant_access_token/internal"
38
+ payload = {"app_id": self.bot_app_id, "app_secret": self.bot_app_secret}
39
+ headers = {"Content-Type": "application/json"}
40
+
41
+ try:
42
+ response = requests.post(url, json=payload, headers=headers)
43
+ response.raise_for_status()
44
+ result = response.json()
45
+ if result.get("code") != 0:
46
+ raise Exception(f"获取tenant_access_token失败: {result.get('msg')}")
47
+
48
+ token = result.get("tenant_access_token")
49
+ expire_in = result.get("expire", 7200) # 默认2小时过期
50
+
51
+ # 设置过期时间,提前5分钟刷新
52
+ self.token_expire_time = datetime.now() + timedelta(seconds=expire_in - 300)
53
+
54
+ logger.info(f"成功获取tenant_access_token,过期时间: {self.token_expire_time}")
55
+ return token
56
+
57
+ except requests.RequestException as e:
58
+ logger.warning(f"请求tenant_access_token失败: {e}")
59
+ raise Exception(f"网络请求失败: {e}")
60
+ except Exception as e:
61
+ logger.warning(f"获取tenant_access_token失败: {e}")
62
+ raise
63
+
64
+ def get_access_token(self) -> str:
65
+ """
66
+ 获取有效的access token,自动处理续期
67
+
68
+ Returns:
69
+ 有效的tenant_access_token
70
+ """
71
+ with self._token_lock:
72
+ # 检查token是否存在或已过期
73
+ if self.tenant_access_token is None or self.token_expire_time is None or datetime.now() >= self.token_expire_time:
74
+
75
+ logger.info("Token不存在或已过期,重新获取")
76
+ self.tenant_access_token = self._get_tenant_access_token()
77
+
78
+ return self.tenant_access_token
79
+
80
+ def make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
81
+ """
82
+ 发起API请求的通用方法
83
+
84
+ Args:
85
+ method: HTTP方法
86
+ endpoint: API端点
87
+ **kwargs: 其他请求参数
88
+
89
+ Returns:
90
+ Response对象
91
+ """
92
+ url = f"{URL_FEISHU_OPEN_API}{endpoint}"
93
+
94
+ # 获取有效token
95
+ token = self.get_access_token()
96
+ # logger.debug(f"使用token: {token} 发送请求: {method} {url}")
97
+
98
+ # 设置认证头
99
+ headers = kwargs.get("headers", {"Content-Type": "application/json"})
100
+ headers["Authorization"] = f"Bearer {token}"
101
+ kwargs["headers"] = headers
102
+
103
+ try:
104
+ response = requests.request(method, url, **kwargs)
105
+ logger.debug(f"response_json: {response.json()}")
106
+ response.raise_for_status()
107
+ return response
108
+ except requests.RequestException as e:
109
+ logger.warning(f"API请求失败: {method} {url}, error: {e}")
110
+ raise
111
+
112
+ def upload_image(self, image_path: str) -> str | None:
113
+ """
114
+ 上传图片到飞书
115
+
116
+ Args:
117
+ image_path: 图片文件路径
118
+ image_type: 图片类型,默认为'message'
119
+
120
+ Returns:
121
+ image_key
122
+
123
+ """
124
+ if not image_path:
125
+ raise ValueError("必须提供image_path")
126
+
127
+ file_obj = None
128
+ try:
129
+ # 准备文件
130
+ file_obj = open(image_path, "rb")
131
+ file_name = image_path.split("/")[-1]
132
+
133
+ # 构建表单数据
134
+ form = {"image_type": "message", "image": (file_name, file_obj, "image/png")}
135
+ multi_form = MultipartEncoder(form)
136
+ headers = {"Content-Type": multi_form.content_type}
137
+ response = self.make_request("POST", "/im/v1/images", headers=headers, data=multi_form)
138
+ result = response.json()
139
+ if result.get("code") != 0:
140
+ raise Exception(f"上传图片失败: {result.get('msg')}")
141
+
142
+ logger.info(f"图片上传成功,image_key: {result.get('data', {}).get('image_key')}")
143
+ logger.debug(f"上传响应logid: {response.headers.get('X-Tt-Logid')}")
144
+
145
+ return result.get("data", {}).get("image_key")
146
+
147
+ except Exception as e:
148
+ logger.warning(f"上传图片失败: {e}")
149
+ return None
150
+ finally:
151
+ # 如果是通过路径打开的文件,需要关闭
152
+ if image_path and file_obj and hasattr(file_obj, "close"):
153
+ file_obj.close()
154
+
155
+ def send_text_message(self, receive_id: str, text: str, receive_id_type: str) -> dict[str, Any]:
156
+ """
157
+ 发送文本消息
158
+
159
+ Args:
160
+ receive_id: 接收者ID(群聊ID、用户ID等)
161
+ text: 消息文本内容
162
+ receive_id_type: 接收者ID类型,可选值:open_id, user_id, union_id, email, chat_id
163
+
164
+ Returns:
165
+ 发送结果
166
+ """
167
+ content = json.dumps({"text": text}, ensure_ascii=False)
168
+ return self.send_message(receive_id, "text", content, receive_id_type)
169
+
170
+ def send_image_message(self, receive_id: str, image_key: str, receive_id_type: str) -> dict[str, Any]:
171
+ """
172
+ 发送图片消息
173
+
174
+ Args:
175
+ receive_id: 接收者ID
176
+ image_key: 图片key(通过upload_image获取)
177
+ receive_id_type: 接收者ID类型
178
+
179
+ Returns:
180
+ 发送结果
181
+ """
182
+ content = json.dumps({"image_key": image_key}, ensure_ascii=False)
183
+ return self.send_message(receive_id, "image", content, receive_id_type)
184
+
185
+ def send_card_message(self, receive_id: str, header: dict, elements: list, receive_id_type: str) -> dict[str, Any]:
186
+ """
187
+ 发送卡片消息
188
+
189
+ Args:
190
+ receive_id: 接收者ID
191
+ header: 卡片消息的头部内容(符合飞书卡片规范的JSON)
192
+ elements: 卡片消息的主体元素列表
193
+ receive_id_type: 接收者ID类型
194
+
195
+ Returns:
196
+ 发送结果
197
+ """
198
+ card_content = {"schema": "2.0", "config": {"update_multi": True}, "body": {"direction": "vertical", "elements": elements}, "header": header}
199
+ content = json.dumps(card_content, ensure_ascii=False)
200
+ return self.send_message(receive_id, "interactive", content, receive_id_type)
201
+
202
+ def send_message(self, receive_id: str, msg_type: str, content: str, receive_id_type: str = "chat_id") -> dict[str, Any]:
203
+ """
204
+ 发送消息的通用方法
205
+
206
+ Args:
207
+ receive_id: 接收者ID
208
+ msg_type: 消息类型(text, image, card等)
209
+ content: 消息内容
210
+ receive_id_type: 接收者ID类型
211
+
212
+ Returns:
213
+ 发送结果
214
+
215
+ Raises:
216
+ Exception: 发送失败
217
+ """
218
+ payload = {"receive_id": receive_id, "msg_type": msg_type, "content": content}
219
+ params = {"receive_id_type": receive_id_type}
220
+
221
+ try:
222
+ response = self.make_request("POST", "/im/v1/messages", json=payload, params=params)
223
+ result = response.json()
224
+ if result.get("code") != 0:
225
+ raise Exception(f"请求失败: {result.get('msg')}")
226
+
227
+ logger.info(f"消息发送成功,message_id: {result.get('data', {}).get('message_id')}")
228
+ logger.debug(f"发送响应logid: {response.headers.get('X-Tt-Logid')}")
229
+
230
+ return result.get("data", {})
231
+
232
+ except Exception as e:
233
+ logger.warning(f"发送消息失败: {e}")
234
+ raise
235
+
236
+ def reply_message(self, message_id: str, msg_type: str, content: str) -> dict[str, Any]:
237
+ """
238
+ 回复某条消息
239
+
240
+ Args:
241
+ message_id: 消息ID
242
+ msg_type: 消息类型(text, image, card等)
243
+ content: 消息内容
244
+
245
+ Returns:
246
+ 回复结果
247
+
248
+ Raises:
249
+ Exception: 回复失败
250
+ """
251
+ payload = {"reply_in_thread": False, "msg_type": msg_type, "content": content}
252
+
253
+ try:
254
+ response = self.make_request("POST", f"/im/v1/messages/{message_id}/reply", json=payload)
255
+ result = response.json()
256
+ if result.get("code") != 0:
257
+ raise Exception(f"请求失败: {result.get('msg')}")
258
+
259
+ logger.info(f"消息回复成功,message_id: {result.get('data', {}).get('message_id')}")
260
+ logger.debug(f"发送响应logid: {response.headers.get('X-Tt-Logid')}")
261
+
262
+ return result.get("data", {})
263
+
264
+ except Exception as e:
265
+ logger.warning(f"回复消息失败: {e}")
266
+ raise
267
+
268
+ def batch_get_user_ids(self, emails: list[str]) -> list[str]:
269
+ """
270
+ 批量获取用户ID
271
+
272
+ Args:
273
+ emails: 用户邮箱列表
274
+
275
+ Returns:
276
+ 批量获取结果(用户ID列表)
277
+
278
+ Raises:
279
+ Exception: 获取失败
280
+ """
281
+ payload = {"emails": emails, "include_resigned": False}
282
+
283
+ try:
284
+ response = self.make_request("POST", f"/contact/v3/users/batch_get_id?user_id_type=user_id", json=payload)
285
+ result = response.json()
286
+ if result.get("code") != 0:
287
+ raise Exception(f"请求失败: {result.get('msg')}")
288
+
289
+ user_ids = [user.get("user_id") for user in result.get("data", {}).get("user_list", [])]
290
+
291
+ logger.debug(f"发送响应logid: {response.headers.get('X-Tt-Logid')}")
292
+
293
+ return user_ids
294
+ except Exception as e:
295
+ logger.warning(f"批量获取用户ID失败: {e}")
296
+ raise
@@ -1,6 +1,6 @@
1
1
  from datetime import datetime
2
2
  import threading
3
- from typing import Callable, Optional, Dict, Any
3
+ from typing import Callable, Any
4
4
  from loguru import logger
5
5
  import requests
6
6
 
@@ -16,15 +16,15 @@ class FeishuTable:
16
16
  """
17
17
  初始化飞书表格
18
18
  """
19
- self.tenant_access_token: Optional[str] = None
20
- self.token_expire_time: Optional[datetime] = None
19
+ self.tenant_access_token: str | None = None
20
+ self.token_expire_time: datetime | None = None
21
21
  self._token_lock = threading.Lock()
22
22
 
23
23
  self.feishu_base = feishu_base
24
24
  self.table_app_token = table_app_token
25
25
  self.table_id = table_id
26
26
 
27
- def get_app_table_record_id(self, table_view_id: str, task_id: str) -> Optional[str]:
27
+ def get_app_table_record_id(self, table_view_id: str, task_id: str) -> str | None:
28
28
  """
29
29
  根据任务ID查询飞书多维表格记录,返回记录ID
30
30
  """
@@ -59,7 +59,7 @@ class FeishuTable:
59
59
  logger.warning(f"查询 {task_id} 表格记录失败: {e}")
60
60
  return None
61
61
 
62
- def update_app_table_record(self, record_id: str, fields: Dict[str, Any]) -> bool:
62
+ def update_app_table_record(self, record_id: str, fields: dict[str, Any]) -> bool:
63
63
  """
64
64
  更新飞书多维表格记录
65
65
 
@@ -81,14 +81,14 @@ class FeishuTable:
81
81
  logger.warning(f"更新表格记录失败: {result.get('msg')}")
82
82
  return False
83
83
 
84
- logger.info(f"成功更新记录ID {record_id},字段: {fields}")
84
+ # logger.debug(f"成功更新记录ID {record_id},字段: {fields}")
85
85
  return True
86
86
 
87
87
  except Exception as e:
88
88
  logger.warning(f"更新表格记录失败: {e}")
89
89
  return False
90
90
 
91
- def fetch_records(self, table_view_id: str, field_names: list[str], page_size: int = 100, page_token: Optional[str] = None) -> Dict[str, Any]:
91
+ def fetch_records(self, table_view_id: str, field_names: list[str], page_size: int = 100, page_token: str | None = None) -> dict[str, Any]:
92
92
  """
93
93
  获取表格记录
94
94
 
@@ -121,7 +121,7 @@ class FeishuTable:
121
121
  logger.error(f"获取记录失败: {e}")
122
122
  raise
123
123
 
124
- def parse_record(self, record: Dict[str, Any], field_names: Optional[list[str]] = None) -> Dict[str, str]:
124
+ def parse_record(self, record: dict[str, Any], field_names: list[str] | None = None) -> dict[str, str]:
125
125
  """
126
126
  解析单条记录,动态提取指定字段
127
127
 
@@ -157,7 +157,7 @@ class FeishuTable:
157
157
  return result
158
158
 
159
159
  def loop_all(
160
- self, table_view_id: str, field_names: list[str], callback: Optional[Callable[[Dict[str, str]], None]] = None, limit: Optional[int] = None
160
+ self, table_view_id: str, field_names: list[str], callback: Callable[[dict[str, str]], None] | None = None, limit: int | None = None
161
161
  ):
162
162
  """
163
163
  遍历视图中的所有记录
@@ -222,3 +222,65 @@ class FeishuTable:
222
222
 
223
223
  logger.info(f"总共获取到 {total_count} 条记录,成功处理 {processed_count} 条")
224
224
  logger.info("数据遍历完成")
225
+
226
+ def fetch_all(self, table_view_id: str, field_names: list[str], limit: int | None = None) -> list[dict[str, str]]:
227
+ """
228
+ 抓取视图中的所有记录
229
+
230
+ Args:
231
+ limit: 可选的记录处理限制数量
232
+ """
233
+ logger.info("开始获取飞书多维表格数据...")
234
+
235
+ page_count = 0
236
+ total_count = 0
237
+ processed_count = 0
238
+ page_token = None
239
+
240
+ records: list[dict[str, str]] = []
241
+ while True:
242
+ page_count += 1
243
+ logger.info(f"正在获取第 {page_count} 页数据...")
244
+
245
+ try:
246
+ data = self.fetch_records(table_view_id, field_names, page_size=200, page_token=page_token)
247
+ items = data.get("items", [])
248
+ size = len(items)
249
+ total_count += size
250
+ page_token = data.get("page_token")
251
+ logger.info(f"第 {page_count} 页获取到 {size} 条记录,next page_token: {page_token}")
252
+
253
+ # 处理每条记录
254
+ for item in items:
255
+ logger.info(f"正在处理第 {processed_count + 1} 条数据...")
256
+ # 检查是否达到处理限制
257
+ if limit and processed_count >= limit:
258
+ logger.info(f"已达到处理限制 {limit},停止处理")
259
+ break
260
+
261
+ try:
262
+ parsed_record = self.parse_record(item)
263
+ processed_count += 1
264
+
265
+ logger.info(f"任务ID: {parsed_record['task_id']}")
266
+ records.append(parsed_record)
267
+
268
+ except Exception as e:
269
+ logger.error(f"处理记录失败: {e}, 记录: {item}")
270
+ continue
271
+
272
+ # 如果达到处理限制,退出外层循环
273
+ if limit and processed_count >= limit:
274
+ break
275
+
276
+ # 检查是否还有更多数据
277
+ if not data.get("has_more", False) or not page_token:
278
+ break
279
+
280
+ except Exception as e:
281
+ logger.error(f"获取第 {page_count} 页数据失败: {e}")
282
+ break
283
+
284
+ logger.info(f"总共获取到 {total_count} 条记录,成功处理 {processed_count} 条")
285
+ logger.info("数据遍历完成")
286
+ return records
@@ -40,6 +40,41 @@ class SpaceXClient:
40
40
  logger.warning(f"do quest queryServerMeta exception: {e}")
41
41
  raise
42
42
 
43
+ def register_resource(self, site: str, data_raw: dict[str, Any]) -> bool:
44
+ site_info = self.bytecloud_client.get_site_config(site)
45
+ url = f"{site_info.endpoint_bytedts_spacex}/resource/v1/registerResource"
46
+ try:
47
+ response = requests.post(url, json=data_raw, headers=self.bytecloud_client.build_request_headers(site))
48
+ # print(f"response status code: {response.status_code}, response text: {response.text}")
49
+
50
+ response.raise_for_status()
51
+
52
+ result = response.json()
53
+ return result.get("message", "") == "ok"
54
+
55
+ except Exception as e:
56
+ logger.warning(f"do quest registerResource exception: {e}")
57
+ raise
58
+
59
+ def list_resources(self, site: str, data_raw: dict[str, Any]) -> list[str]:
60
+ site_info = self.bytecloud_client.get_site_config(site)
61
+ url = f"{site_info.endpoint_bytedts_spacex}/resource/v1/listResource"
62
+ try:
63
+ response = requests.post(url, json=data_raw, headers=self.bytecloud_client.build_request_headers(site))
64
+ # print(f"response status code: {response.status_code}, response text: {response.text}")
65
+
66
+ response.raise_for_status()
67
+
68
+ result = response.json()
69
+ if not result.get("message", "") == "ok":
70
+ return []
71
+
72
+ items = result.get("data", {}).get("items", [])
73
+ return [item["name"] for item in items]
74
+ except Exception as e:
75
+ logger.warning(f"do quest listResource exception: {e}")
76
+ raise
77
+
43
78
  def register_gateway(self, site: str, gateway_info: GatewayInfo) -> bool:
44
79
  site_info = self.bytecloud_client.get_site_config(site)
45
80
  url = f"{site_info.endpoint_bytedts_spacex}/bytedts/v1/registryGateway"
@@ -71,3 +106,16 @@ class SpaceXClient:
71
106
  except Exception as e:
72
107
  logger.warning(f"do quest registryGateway exception: {e}")
73
108
  raise
109
+
110
+ def gateway_operation(self, site: str, operation: str, data_raw: dict[str, Any]) -> dict[str, Any]:
111
+ site_info = self.bytecloud_client.get_site_config(site)
112
+ url = f"{site_info.endpoint_bytedts_spacex}/bytedts/v1/{operation}"
113
+ try:
114
+ response = requests.post(url, json=data_raw, headers=self.bytecloud_client.build_request_headers(site))
115
+ # print(f"response status code: {response.status_code}, response text: {response.text}")
116
+
117
+ response.raise_for_status()
118
+ return response.json()
119
+ except Exception as e:
120
+ logger.warning(f"do quest gateway_operation exception: {e}")
121
+ raise
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dts-dance"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "dts dance lib"
9
9
  readme = "README.md"
10
10
  keywords = ["tools", "observation"]
@@ -12,8 +12,9 @@ requires-python = ">=3.12"
12
12
  dependencies = [
13
13
  "requests>=2.32.5,<3.0.0",
14
14
  "loguru>=0.7.3,<0.8.0",
15
- "boto3>=1.42.29,<2.0.0",
15
+ "boto3>=1.42.30,<2.0.0",
16
16
  "pyyaml>=6.0.3,<7.0.0",
17
+ "requests_toolbelt==1.0.0",
17
18
  ]
18
19
 
19
20
  [tool.hatch.build.targets.wheel]
@@ -4,30 +4,30 @@ requires-python = ">=3.12"
4
4
 
5
5
  [[package]]
6
6
  name = "boto3"
7
- version = "1.42.29"
7
+ version = "1.42.30"
8
8
  source = { registry = "https://pypi.org/simple" }
9
9
  dependencies = [
10
10
  { name = "botocore" },
11
11
  { name = "jmespath" },
12
12
  { name = "s3transfer" },
13
13
  ]
14
- sdist = { url = "https://files.pythonhosted.org/packages/5c/24/1dd85b64004103c2e60476d0fa8d78435f5fed9db1129cd2cd332784037a/boto3-1.42.29.tar.gz", hash = "sha256:247e54f24116ad6792cfc14b274288383af3ec3433b0547da8a14a8bd6e81950", size = 112810, upload-time = "2026-01-15T20:36:39.404Z" }
14
+ sdist = { url = "https://files.pythonhosted.org/packages/42/79/2dac8b7cb075cfa43908ee9af3f8ee06880d84b86013854c5cca8945afac/boto3-1.42.30.tar.gz", hash = "sha256:ba9cd2f7819637d15bfbeb63af4c567fcc8a7dcd7b93dd12734ec58601169538", size = 112809, upload-time = "2026-01-16T20:37:23.636Z" }
15
15
  wheels = [
16
- { url = "https://files.pythonhosted.org/packages/51/30/2c25d7be8418e7f137ffece6097c68199dbd6996da645ec9b5a5a9647123/boto3-1.42.29-py3-none-any.whl", hash = "sha256:6c9c4dece67bf72d82ba7dff48e33a56a87cdf9b16c8887f88ca7789a95d3317", size = 140574, upload-time = "2026-01-15T20:36:37.206Z" },
16
+ { url = "https://files.pythonhosted.org/packages/52/b3/2c0d828c9f668292e277ca5232e6160dd5b4b660a3f076f20dd5378baa1e/boto3-1.42.30-py3-none-any.whl", hash = "sha256:d7e548bea65e0ae2c465c77de937bc686b591aee6a352d5a19a16bc751e591c1", size = 140573, upload-time = "2026-01-16T20:37:22.089Z" },
17
17
  ]
18
18
 
19
19
  [[package]]
20
20
  name = "botocore"
21
- version = "1.42.29"
21
+ version = "1.42.30"
22
22
  source = { registry = "https://pypi.org/simple" }
23
23
  dependencies = [
24
24
  { name = "jmespath" },
25
25
  { name = "python-dateutil" },
26
26
  { name = "urllib3" },
27
27
  ]
28
- sdist = { url = "https://files.pythonhosted.org/packages/70/08/8a8e0255949845f764c5126f97b1bc09a6484077f124c2177b979ecfbbff/botocore-1.42.29.tar.gz", hash = "sha256:0fe869227a1dfe818f691a31b8c1693e39be8056a6dff5d6d4b3fc5b3a5e7d42", size = 14890916, upload-time = "2026-01-15T20:36:28.241Z" }
28
+ sdist = { url = "https://files.pythonhosted.org/packages/44/38/23862628a0eb044c8b8b3d7a9ad1920b3bfd6bce6d746d5a871e8382c7e4/botocore-1.42.30.tar.gz", hash = "sha256:9bf1662b8273d5cc3828a49f71ca85abf4e021011c1f0a71f41a2ea5769a5116", size = 14891439, upload-time = "2026-01-16T20:37:13.77Z" }
29
29
  wheels = [
30
- { url = "https://files.pythonhosted.org/packages/94/76/cfa6a934ee5a8a87f626b38275193a046da894d2f9021e001587fc2e8c7d/botocore-1.42.29-py3-none-any.whl", hash = "sha256:b45f8dfc1de5106a9d040c5612f267582e68b2b2c5237477dff85c707c1c5d11", size = 14563947, upload-time = "2026-01-15T20:36:23.828Z" },
30
+ { url = "https://files.pythonhosted.org/packages/3d/8d/6d7b016383b1f74dd93611b1c5078bbaddaca901553ab886dcda87cae365/botocore-1.42.30-py3-none-any.whl", hash = "sha256:97070a438cac92430bb7b65f8ebd7075224f4a289719da4ee293d22d1e98db02", size = 14566340, upload-time = "2026-01-16T20:37:10.94Z" },
31
31
  ]
32
32
 
33
33
  [[package]]
@@ -107,21 +107,23 @@ wheels = [
107
107
 
108
108
  [[package]]
109
109
  name = "dts-dance"
110
- version = "0.2.2"
110
+ version = "0.2.4"
111
111
  source = { editable = "." }
112
112
  dependencies = [
113
113
  { name = "boto3" },
114
114
  { name = "loguru" },
115
115
  { name = "pyyaml" },
116
116
  { name = "requests" },
117
+ { name = "requests-toolbelt" },
117
118
  ]
118
119
 
119
120
  [package.metadata]
120
121
  requires-dist = [
121
- { name = "boto3", specifier = ">=1.42.29,<2.0.0" },
122
+ { name = "boto3", specifier = ">=1.42.30,<2.0.0" },
122
123
  { name = "loguru", specifier = ">=0.7.3,<0.8.0" },
123
124
  { name = "pyyaml", specifier = ">=6.0.3,<7.0.0" },
124
125
  { name = "requests", specifier = ">=2.32.5,<3.0.0" },
126
+ { name = "requests-toolbelt", specifier = "==1.0.0" },
125
127
  ]
126
128
 
127
129
  [[package]]
@@ -228,6 +230,18 @@ wheels = [
228
230
  { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
229
231
  ]
230
232
 
233
+ [[package]]
234
+ name = "requests-toolbelt"
235
+ version = "1.0.0"
236
+ source = { registry = "https://pypi.org/simple" }
237
+ dependencies = [
238
+ { name = "requests" },
239
+ ]
240
+ sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" }
241
+ wheels = [
242
+ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
243
+ ]
244
+
231
245
  [[package]]
232
246
  name = "s3transfer"
233
247
  version = "0.16.0"
@@ -1,107 +0,0 @@
1
- from datetime import datetime, timedelta
2
- import threading
3
- from typing import Optional
4
- from loguru import logger
5
- import requests
6
-
7
- URL_FEISHU_OPEN_API: str = "https://fsopen.bytedance.net/open-apis"
8
-
9
-
10
- class FeishuBase:
11
- """飞书基础类,传入机器人认证信息,支持自动token续期管理"""
12
-
13
- def __init__(self, bot_app_id: str, bot_app_secret: str):
14
- """
15
- 初始化飞书基础类
16
- """
17
- self.tenant_access_token: Optional[str] = None
18
- self.token_expire_time: Optional[datetime] = None
19
- self._token_lock = threading.Lock()
20
-
21
- self.bot_app_id = bot_app_id
22
- self.bot_app_secret = bot_app_secret
23
-
24
- def _get_tenant_access_token(self) -> str:
25
- """
26
- 获取tenant_access_token
27
-
28
- Returns:
29
- tenant_access_token字符串
30
-
31
- Raises:
32
- Exception: 获取token失败时抛出异常
33
- """
34
- url = f"{URL_FEISHU_OPEN_API}/auth/v3/tenant_access_token/internal"
35
- payload = {"app_id": self.bot_app_id, "app_secret": self.bot_app_secret}
36
- headers = {"Content-Type": "application/json"}
37
-
38
- try:
39
- response = requests.post(url, json=payload, headers=headers)
40
- response.raise_for_status()
41
- result = response.json()
42
- if result.get("code") != 0:
43
- raise Exception(f"获取tenant_access_token失败: {result.get('msg')}")
44
-
45
- token = result.get("tenant_access_token")
46
- expire_in = result.get("expire", 7200) # 默认2小时过期
47
-
48
- # 设置过期时间,提前5分钟刷新
49
- self.token_expire_time = datetime.now() + timedelta(seconds=expire_in - 300)
50
-
51
- logger.info(f"成功获取tenant_access_token,过期时间: {self.token_expire_time}")
52
- return token
53
-
54
- except requests.RequestException as e:
55
- logger.warning(f"请求tenant_access_token失败: {e}")
56
- raise Exception(f"网络请求失败: {e}")
57
- except Exception as e:
58
- logger.warning(f"获取tenant_access_token失败: {e}")
59
- raise
60
-
61
- def get_access_token(self) -> str:
62
- """
63
- 获取有效的access token,自动处理续期
64
-
65
- Returns:
66
- 有效的tenant_access_token
67
- """
68
- with self._token_lock:
69
- # 检查token是否存在或已过期
70
- if self.tenant_access_token is None or self.token_expire_time is None or datetime.now() >= self.token_expire_time:
71
-
72
- logger.info("Token不存在或已过期,重新获取")
73
- self.tenant_access_token = self._get_tenant_access_token()
74
-
75
- return self.tenant_access_token
76
-
77
- def make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
78
- """
79
- 发起API请求的通用方法
80
-
81
- Args:
82
- method: HTTP方法
83
- endpoint: API端点
84
- **kwargs: 其他请求参数
85
-
86
- Returns:
87
- Response对象
88
- """
89
- url = f"{URL_FEISHU_OPEN_API}{endpoint}"
90
-
91
- # 获取有效token
92
- token = self.get_access_token()
93
- # logger.debug(f"使用token: {token} 发送请求: {method} {url}")
94
-
95
- # 设置认证头
96
- headers = kwargs.get("headers", {"Content-Type": "application/json"})
97
- headers["Authorization"] = f"Bearer {token}"
98
- kwargs["headers"] = headers
99
-
100
- try:
101
- response = requests.request(method, url, **kwargs)
102
- # logger.debug(f"response_json: {response.json()}")
103
- response.raise_for_status()
104
- return response
105
- except requests.RequestException as e:
106
- logger.warning(f"API请求失败: {method} {url}, error: {e}")
107
- raise
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes