dts-dance 0.2.2__py3-none-any.whl → 0.2.4__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.
@@ -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
 
@@ -0,0 +1,14 @@
1
+ dtsdance/__init__.py,sha256=Yl_jEZ5weYfcrklnDvwB4wSgCOvMBLRRgWx0gHs3qfM,49
2
+ dtsdance/bytecloud.py,sha256=s1suo-YexMH6gUHYwC6P5k6AeyOm7Q_acYh_gHCDVjI,5654
3
+ dtsdance/dflow.py,sha256=ggwNB2x-LgnpI9nzNTTnYnGfYyGH5rYcHHBhC4pOlfg,7059
4
+ dtsdance/dsyncer.py,sha256=3Oj1Ko5FuB_sUm9Hj-hBy95wee3b6TH2FXKoPFGsakM,11193
5
+ dtsdance/feishu_base.py,sha256=Dsl8CZZ3wyZf3TEiiUAs3_WpX1pinCuWFwqpm29ROVc,10414
6
+ dtsdance/feishu_table.py,sha256=JleCjQJ-rcbC15v2UAc5BK-YioB2SMGsdGztQ9BaT4k,10779
7
+ dtsdance/metrics_fe.py,sha256=hzIl5BJmuCrkqJOHELVzXm3YAqrPttbyVkKBglS4mgQ,18978
8
+ dtsdance/s3.py,sha256=Bh-cwLksfO5PewNtIzE_Md3rRLDLI1DUVoOD7Pou5T8,1294
9
+ dtsdance/spacex_bytedts.py,sha256=oVnHXTj-cuq2vcsUJq33_aaq0TQp7A52kldvspKueI4,4799
10
+ dtsdance/tcc_inner.py,sha256=6Tuq_r2nNCK_HC4hqEGh7UVAa8kDtuIcU95WBu4zuFQ,1623
11
+ dtsdance/tcc_open.py,sha256=Qb_ue3xH0CSsoReItdFKuvW7JsDTfFkDNIB_4zjzSQI,6893
12
+ dts_dance-0.2.4.dist-info/METADATA,sha256=LY_7WWh06QNmhr_1vyuMTtKDrx88N0P1BjGEURYAl6U,833
13
+ dts_dance-0.2.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
+ dts_dance-0.2.4.dist-info/RECORD,,
dtsdance/bytecloud.py CHANGED
@@ -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 令牌
dtsdance/dflow.py CHANGED
@@ -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)}")
dtsdance/feishu_base.py CHANGED
@@ -1,7 +1,10 @@
1
1
  from datetime import datetime, timedelta
2
2
  import threading
3
- from typing import Optional
3
+ import json
4
4
  from loguru import logger
5
+ from typing import Any
6
+ from requests_toolbelt import MultipartEncoder
7
+
5
8
  import requests
6
9
 
7
10
  URL_FEISHU_OPEN_API: str = "https://fsopen.bytedance.net/open-apis"
@@ -14,8 +17,8 @@ class FeishuBase:
14
17
  """
15
18
  初始化飞书基础类
16
19
  """
17
- self.tenant_access_token: Optional[str] = None
18
- self.token_expire_time: Optional[datetime] = None
20
+ self.tenant_access_token: str | None = None
21
+ self.token_expire_time: datetime | None = None
19
22
  self._token_lock = threading.Lock()
20
23
 
21
24
  self.bot_app_id = bot_app_id
@@ -99,9 +102,195 @@ class FeishuBase:
99
102
 
100
103
  try:
101
104
  response = requests.request(method, url, **kwargs)
102
- # logger.debug(f"response_json: {response.json()}")
105
+ logger.debug(f"response_json: {response.json()}")
103
106
  response.raise_for_status()
104
107
  return response
105
108
  except requests.RequestException as e:
106
109
  logger.warning(f"API请求失败: {method} {url}, error: {e}")
107
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
dtsdance/feishu_table.py CHANGED
@@ -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
@@ -1,14 +0,0 @@
1
- dtsdance/__init__.py,sha256=Yl_jEZ5weYfcrklnDvwB4wSgCOvMBLRRgWx0gHs3qfM,49
2
- dtsdance/bytecloud.py,sha256=CQNebRy8UlILYmY8XhHhmEfW-WwW7SAlUh387X4OnY8,5646
3
- dtsdance/dflow.py,sha256=XB6RClJzs9B5fhLofYvb5JazBF-iDfbtv1ETH6ENNUo,7074
4
- dtsdance/dsyncer.py,sha256=3Oj1Ko5FuB_sUm9Hj-hBy95wee3b6TH2FXKoPFGsakM,11193
5
- dtsdance/feishu_base.py,sha256=2j4ZM4PFqJ-9EhC6DQ1OmAg--3VBGZyyRuxyjL0j6OU,3733
6
- dtsdance/feishu_table.py,sha256=ZUeoKrM4nmm5hFhc3vWOVYeLP390orm-284Od92G4iQ,8424
7
- dtsdance/metrics_fe.py,sha256=hzIl5BJmuCrkqJOHELVzXm3YAqrPttbyVkKBglS4mgQ,18978
8
- dtsdance/s3.py,sha256=Bh-cwLksfO5PewNtIzE_Md3rRLDLI1DUVoOD7Pou5T8,1294
9
- dtsdance/spacex_bytedts.py,sha256=83lUU9sFPsvmZ0zLCGG_V6vMYXePKbEUeG1d6qfkQhE,2575
10
- dtsdance/tcc_inner.py,sha256=6Tuq_r2nNCK_HC4hqEGh7UVAa8kDtuIcU95WBu4zuFQ,1623
11
- dtsdance/tcc_open.py,sha256=Qb_ue3xH0CSsoReItdFKuvW7JsDTfFkDNIB_4zjzSQI,6893
12
- dts_dance-0.2.2.dist-info/METADATA,sha256=ZRzPIxPE0Y4uJRTwBK2_PukFRUBuHb8xkuxQPGQnOjo,793
13
- dts_dance-0.2.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
- dts_dance-0.2.2.dist-info/RECORD,,