dts-dance 0.2.4__py3-none-any.whl → 0.2.5__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,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dts-dance
3
- Version: 0.2.4
3
+ Version: 0.2.5
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.30
7
+ Requires-Dist: boto3>=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
10
  Requires-Dist: requests-toolbelt==1.0.0
@@ -0,0 +1,15 @@
1
+ dtsdance/__init__.py,sha256=Yl_jEZ5weYfcrklnDvwB4wSgCOvMBLRRgWx0gHs3qfM,49
2
+ dtsdance/bytecloud.py,sha256=s1suo-YexMH6gUHYwC6P5k6AeyOm7Q_acYh_gHCDVjI,5654
3
+ dtsdance/ddutil.py,sha256=aZ-fG_e2ZkHs8sxZsudf6bVCnp_OI2Z4WjbxOfPoJRQ,1134
4
+ dtsdance/dflow.py,sha256=lEn_gd0pUOqEhuvs0hSQICR8LKOk3wP4DBbeyHZ-A44,7766
5
+ dtsdance/dsyncer.py,sha256=gK2rRiO3041P3efbmXgvLPeTgKlJowuPR7CCz_uyoSA,9926
6
+ dtsdance/feishu_base.py,sha256=dYYNnsCjGZYN2I6inR9FRQoUiDLB7j40rzydw50N38o,11825
7
+ dtsdance/feishu_table.py,sha256=JleCjQJ-rcbC15v2UAc5BK-YioB2SMGsdGztQ9BaT4k,10779
8
+ dtsdance/metrics_fe.py,sha256=hzIl5BJmuCrkqJOHELVzXm3YAqrPttbyVkKBglS4mgQ,18978
9
+ dtsdance/s3.py,sha256=Bh-cwLksfO5PewNtIzE_Md3rRLDLI1DUVoOD7Pou5T8,1294
10
+ dtsdance/spacex.py,sha256=oVnHXTj-cuq2vcsUJq33_aaq0TQp7A52kldvspKueI4,4799
11
+ dtsdance/tcc_inner.py,sha256=Z0Wr-z2YPwAti6BO_fsX0VPT5pCbfuCBLQ37UKMFL7U,1629
12
+ dtsdance/tcc_open.py,sha256=JIfVhL3yfm0ftS_vzONE2QNzfqcZuX1mPTvjovAtwmE,6893
13
+ dts_dance-0.2.5.dist-info/METADATA,sha256=-skiMz-CuBlzpt7BBDJpOEXrmsj8np_xKnG-6K_HPao,826
14
+ dts_dance-0.2.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
15
+ dts_dance-0.2.5.dist-info/RECORD,,
dtsdance/ddutil.py ADDED
@@ -0,0 +1,39 @@
1
+ from typing import Any, Optional
2
+ from loguru import logger
3
+ import requests
4
+
5
+
6
+ def make_request(method: str, url: str, headers: dict[str, str], payload: Optional[dict] = None) -> dict[str, Any]:
7
+ """
8
+ 发送 HTTP 请求的通用方法
9
+
10
+ Args:
11
+ method: HTTP 方法 (GET/POST)
12
+ url: 请求 URL
13
+ headers: 请求头
14
+ payload: POST 请求的 JSON 数据
15
+
16
+ Returns:
17
+ dict[str, Any]: 解析后的 JSON 响应
18
+ """
19
+ response = None
20
+ try:
21
+ if method.upper() == "GET":
22
+ response = requests.get(url, headers=headers)
23
+ elif method.upper() == "POST":
24
+ response = requests.post(url, json=payload, headers=headers)
25
+ else:
26
+ raise ValueError(f"不支持的 HTTP 方法: {method}")
27
+
28
+ # 检查响应状态码
29
+ # response.raise_for_status()
30
+
31
+ # 解析 JSON 响应
32
+ return response.json()
33
+
34
+ except Exception as e:
35
+ error_msg = f"make_request occur error, error: {e}"
36
+ if response is not None:
37
+ error_msg += f", response.text: {response.text}"
38
+ logger.warning(error_msg)
39
+ raise
dtsdance/dflow.py CHANGED
@@ -1,7 +1,9 @@
1
- from typing import Any, cast, Optional
1
+ import collections
2
+ from typing import Any, cast
2
3
  from loguru import logger
4
+
5
+ from dtsdance.ddutil import make_request
3
6
  from .bytecloud import ByteCloudClient
4
- import requests
5
7
 
6
8
 
7
9
  class TaskNotFound(Exception):
@@ -17,42 +19,13 @@ class DFlowClient:
17
19
  def __init__(self, bytecloud_client: ByteCloudClient) -> None:
18
20
  self.bytecloud_client = bytecloud_client
19
21
 
20
- def _make_request(self, method: str, url: str, headers: dict[str, str], json_data: Optional[dict] = None) -> dict[str, Any]:
21
- """
22
- 发送 HTTP 请求的通用方法
22
+ def build_request_headers(self, site: str, vregion: str | None) -> dict[str, str]:
23
+ headers = self.bytecloud_client.build_request_headers(site)
24
+ if vregion:
25
+ headers["x-bcgw-vregion"] = vregion
26
+ return headers
23
27
 
24
- Args:
25
- method: HTTP 方法 (GET/POST)
26
- url: 请求 URL
27
- headers: 请求头
28
- json_data: POST 请求的 JSON 数据
29
-
30
- Returns:
31
- dict[str, Any]: 解析后的 JSON 响应
32
- """
33
- response = None
34
- try:
35
- if method.upper() == "GET":
36
- response = requests.get(url, headers=headers)
37
- elif method.upper() == "POST":
38
- response = requests.post(url, json=json_data, headers=headers)
39
- else:
40
- raise ValueError(f"不支持的 HTTP 方法: {method}")
41
-
42
- # 检查响应状态码
43
- response.raise_for_status()
44
-
45
- # 解析 JSON 响应
46
- return response.json()
47
-
48
- except Exception as e:
49
- error_msg = f"_make_request occur error, error: {e}"
50
- if response is not None:
51
- error_msg += f", response.text: {response.text}"
52
- logger.warning(error_msg)
53
- raise
54
-
55
- def get_task_info(self, site: str, task_id: str) -> dict[str, Any]:
28
+ def get_task_info(self, site: str, task_id: str, vregion: str | None = None) -> dict[str, Any]:
56
29
  """
57
30
  获取 DFlow 任务信息
58
31
 
@@ -68,9 +41,9 @@ class DFlowClient:
68
41
  url = f"{site_info.endpoint}/api/v1/bytedts/api/bytedts/v3/DescribeTaskInfo"
69
42
 
70
43
  # 构建请求数据
71
- json_data = {"id": int(task_id)}
44
+ payload = {"id": int(task_id)}
72
45
 
73
- response_data = self._make_request("POST", url, self.bytecloud_client.build_request_headers(site), json_data)
46
+ response_data = make_request("POST", url, self.build_request_headers(site, vregion), payload)
74
47
 
75
48
  message = response_data.get("message")
76
49
  # logger.debug(f"get_task_info {site} {task_id}, message: {message}")
@@ -94,7 +67,7 @@ class DFlowClient:
94
67
  except (KeyError, AttributeError, Exception) as e:
95
68
  raise Exception(f"无法从响应中提取 DFlow 任务信息数据: {str(e)}")
96
69
 
97
- def get_dflow_info(self, site: str, dflow_id: str) -> dict[str, Any]:
70
+ def get_dflow_info(self, site: str, dflow_id: str, vregion: str | None = None) -> dict[str, Any]:
98
71
  """
99
72
  获取 DFlow 进程信息
100
73
 
@@ -110,9 +83,9 @@ class DFlowClient:
110
83
  url = f"{site_info.endpoint}/api/v1/bytedts/api/bytedts/v3/DescribeDFlowDetail"
111
84
 
112
85
  # 构建请求数据
113
- json_data = {"dflow_id": int(dflow_id)}
86
+ payload = {"dflow_id": int(dflow_id)}
114
87
 
115
- response_data = self._make_request("POST", url, self.bytecloud_client.build_request_headers(site), json_data)
88
+ response_data = make_request("POST", url, self.build_request_headers(site, vregion), payload)
116
89
 
117
90
  message = response_data.get("message", "")
118
91
  # logger.debug(f"get_dflow_info {site} {dflow_id}, message: {message}")
@@ -168,9 +141,9 @@ class DFlowClient:
168
141
  url = f"{site_info.endpoint}/api/v1/bytedts/api/bytedts/v3/InitSystemResource"
169
142
 
170
143
  # 构建请求数据
171
- json_data = {"ctrl_env": ctrl_env}
144
+ payload = {"ctrl_env": ctrl_env}
172
145
 
173
- response_data = self._make_request("POST", url, self.bytecloud_client.build_request_headers(site), json_data)
146
+ response_data = make_request("POST", url, self.build_request_headers(site, None), payload)
174
147
 
175
148
  message = response_data.get("message")
176
149
  logger.info(f"int_resources {site} {ctrl_env}, message: {message}")
@@ -193,9 +166,9 @@ class DFlowClient:
193
166
  url = f"{site_info.endpoint}/api/v1/bytedts/api/bytedts/v3/DescribeResources"
194
167
 
195
168
  # 构建请求数据
196
- json_data = {"offset": 0, "limit": 10, "ctrl_env": ctrl_env}
169
+ payload = {"offset": 0, "limit": 10, "ctrl_env": ctrl_env}
197
170
 
198
- response_data = self._make_request("POST", url, self.bytecloud_client.build_request_headers(site), json_data)
171
+ response_data = make_request("POST", url, self.build_request_headers(site, None), payload)
199
172
 
200
173
  try:
201
174
  data = response_data.get("data", {})
@@ -203,3 +176,31 @@ class DFlowClient:
203
176
  return [item["name"] for item in items]
204
177
  except (KeyError, AttributeError, Exception) as e:
205
178
  raise Exception(f"无法从响应中提取 CTRL 环境资源列表数据: {str(e)}")
179
+
180
+ def get_all_ctrl_envs(self, site: str) -> dict[str, list[str]]:
181
+ """获取所有可用的控制环境列表,返回以domain为键,ctrl_env列表为值的字典"""
182
+ site_info = self.bytecloud_client.get_site_config(site)
183
+ url = f"{site_info.endpoint}/api/v1/bytedts/api/bytedts/v3/DescribeRegions"
184
+ resp_json = make_request("POST", url, self.build_request_headers(site, None), {})
185
+ if resp_json["code"] != 0:
186
+ raise Exception(resp_json["message"])
187
+
188
+ # print(f"Fetched regions info: {json.dumps(resp_json["data"], ensure_ascii=False, indent=2)}")
189
+
190
+ # 提取所有控制环境列表,格式为以domain为键,ctrl_env列表为值的字典
191
+ all_ctrl_envs_by_domain = collections.defaultdict(list[str])
192
+
193
+ # 从ops_platform_region中提取ctrl_env_list
194
+ if "bytedts_env" in resp_json["data"]:
195
+ for bytedts_env in resp_json["data"]["bytedts_env"]:
196
+ if "ctrl_env_list" in bytedts_env and "domain" in bytedts_env:
197
+ env = bytedts_env["env"]
198
+ region = bytedts_env["include_region"][0] if bytedts_env["include_region"] else "Unknown"
199
+ env_region_key = env + "|" + region
200
+ for ctrl_env_item in bytedts_env["ctrl_env_list"]:
201
+ if "ctrl_env" in ctrl_env_item:
202
+ ctrl_env = ctrl_env_item["ctrl_env"]
203
+ # 将ctrl_env添加到domain对应的列表中
204
+ all_ctrl_envs_by_domain[env_region_key].append(ctrl_env)
205
+
206
+ return dict(all_ctrl_envs_by_domain)
dtsdance/dsyncer.py CHANGED
@@ -1,7 +1,7 @@
1
+ from dtsdance.ddutil import make_request
1
2
  from .bytecloud import ByteCloudClient
2
3
  from typing import Any, NamedTuple, Tuple, cast, Optional
3
4
  from loguru import logger
4
- import requests
5
5
  import re
6
6
  from datetime import datetime
7
7
 
@@ -41,42 +41,6 @@ class DSyncerClient:
41
41
 
42
42
  return headers
43
43
 
44
- def _make_request(self, method: str, url: str, headers: dict[str, str], data_raw: Optional[dict] = None) -> dict[str, Any]:
45
- """
46
- 发送 HTTP 请求的通用方法
47
-
48
- Args:
49
- method: HTTP 方法 (GET/POST)
50
- url: 请求 URL
51
- headers: 请求头
52
- json_data: POST 请求的 JSON 数据
53
- operation_name: 操作名称,用于日志记录
54
-
55
- Returns:
56
- dict[str, Any]: 解析后的 JSON 响应
57
- """
58
- response = None
59
- try:
60
- if method.upper() == "GET":
61
- response = requests.get(url, headers=headers)
62
- elif method.upper() == "POST":
63
- response = requests.post(url, json=data_raw, headers=headers)
64
- else:
65
- raise ValueError(f"不支持的 HTTP 方法: {method}")
66
-
67
- # 检查响应状态码
68
- # response.raise_for_status()
69
-
70
- # 解析 JSON 响应
71
- return response.json()
72
-
73
- except Exception as e:
74
- error_msg = f"_make_request occur error, error: {e}"
75
- if response is not None:
76
- error_msg += f", response.text: {response.text}"
77
- logger.warning(error_msg)
78
- raise
79
-
80
44
  def _acquire_task_info(self, site: str, task_id: str) -> dict[str, str]:
81
45
  """
82
46
  获取 DSyncer 任务信息
@@ -95,7 +59,7 @@ class DSyncerClient:
95
59
  # 准备请求头
96
60
  headers = self._build_headers(site)
97
61
 
98
- return self._make_request("GET", url, headers)
62
+ return make_request("GET", url, headers)
99
63
 
100
64
  def get_dflow_task_info(self, site: str, task_id: str) -> tuple[str, str]:
101
65
  """
@@ -108,7 +72,7 @@ class DSyncerClient:
108
72
  # 准备请求头
109
73
  headers = self._build_headers(site)
110
74
 
111
- json_data = self._make_request("GET", url, headers)
75
+ json_data = make_request("GET", url, headers)
112
76
 
113
77
  message = json_data.get("message", "")
114
78
  logger.debug(f"get task migrate info {site} {task_id}, message: {message}")
@@ -214,9 +178,9 @@ class DSyncerClient:
214
178
  # 准备请求头
215
179
  headers = self._build_headers(site, secret_api=True)
216
180
 
217
- data_raw = {"task_id": task_id}
181
+ payload = {"task_id": task_id}
218
182
 
219
- response_data = self._make_request("POST", url, headers, data_raw)
183
+ response_data = make_request("POST", url, headers, payload)
220
184
 
221
185
  message = response_data.get("message", "")
222
186
  logger.debug(f"get task migrate status {site} {task_id}, message: {message}")
@@ -236,9 +200,9 @@ class DSyncerClient:
236
200
  # 准备请求头
237
201
  headers = self._build_headers(site, secret_api=True)
238
202
 
239
- data_raw = {"task_id_list": [task_id]}
203
+ payload = {"task_id_list": [task_id]}
240
204
 
241
- response_data = self._make_request("POST", url, headers, data_raw)
205
+ response_data = make_request("POST", url, headers, payload)
242
206
 
243
207
  logger.debug(f"migration_mark_rollback return {site} {task_id}, json_data: {response_data}")
244
208
  success_task = response_data.get("data", {}).get("success_task", [])
@@ -255,9 +219,9 @@ class DSyncerClient:
255
219
  # 准备请求头
256
220
  headers = self._build_headers(site, secret_api=True)
257
221
 
258
- data_raw = {"task_id": task_id}
222
+ payload = {"task_id": task_id}
259
223
 
260
- response_data = self._make_request("POST", url, headers, data_raw)
224
+ response_data = make_request("POST", url, headers, payload)
261
225
 
262
226
  logger.debug(f"migration_mark_success return {site} {task_id}, json_data: {response_data}")
263
227
  success_task = response_data.get("data", {}).get("success_task", [])
@@ -277,7 +241,7 @@ class DSyncerClient:
277
241
  # 准备请求头
278
242
  headers = self._build_headers(site, secret_api=True)
279
243
 
280
- data_raw = {
244
+ payload = {
281
245
  "task_id": task_id,
282
246
  "delay_threshold": "20s",
283
247
  "dsyncer_task_pause_threshold": "180s",
@@ -289,7 +253,7 @@ class DSyncerClient:
289
253
  "app_parallel": app_parallel,
290
254
  }
291
255
 
292
- response_data = self._make_request("POST", url, headers, data_raw)
256
+ response_data = make_request("POST", url, headers, payload)
293
257
 
294
258
  logger.debug(f"migrate_task return {site} {task_id}, json_data: {response_data}")
295
259
  message = response_data.get("message")
dtsdance/feishu_base.py CHANGED
@@ -100,13 +100,15 @@ class FeishuBase:
100
100
  headers["Authorization"] = f"Bearer {token}"
101
101
  kwargs["headers"] = headers
102
102
 
103
+ response: requests.Response | None = None
103
104
  try:
104
105
  response = requests.request(method, url, **kwargs)
105
- logger.debug(f"response_json: {response.json()}")
106
+ # logger.debug(f"response_json: {response.json()}")
106
107
  response.raise_for_status()
107
108
  return response
108
109
  except requests.RequestException as e:
109
- logger.warning(f"API请求失败: {method} {url}, error: {e}")
110
+ error_detail = response.json() if response is not None else "无响应"
111
+ logger.warning(f"API请求失败: {method} {url}, error: {e}, response_json: {error_detail}")
110
112
  raise
111
113
 
112
114
  def upload_image(self, image_path: str) -> str | None:
@@ -265,7 +267,7 @@ class FeishuBase:
265
267
  logger.warning(f"回复消息失败: {e}")
266
268
  raise
267
269
 
268
- def batch_get_user_ids(self, emails: list[str]) -> list[str]:
270
+ def batch_get_user_ids(self, emails: list[str]) -> dict[str, None | str]:
269
271
  """
270
272
  批量获取用户ID
271
273
 
@@ -273,7 +275,7 @@ class FeishuBase:
273
275
  emails: 用户邮箱列表
274
276
 
275
277
  Returns:
276
- 批量获取结果(用户ID列表)
278
+ 批量获取结果(email -> user_id 的字典映射)
277
279
 
278
280
  Raises:
279
281
  Exception: 获取失败
@@ -281,16 +283,51 @@ class FeishuBase:
281
283
  payload = {"emails": emails, "include_resigned": False}
282
284
 
283
285
  try:
284
- response = self.make_request("POST", f"/contact/v3/users/batch_get_id?user_id_type=user_id", json=payload)
286
+ response = self.make_request("POST", "/contact/v3/users/batch_get_id?user_id_type=user_id", json=payload)
285
287
  result = response.json()
286
288
  if result.get("code") != 0:
287
289
  raise Exception(f"请求失败: {result.get('msg')}")
288
290
 
289
- user_ids = [user.get("user_id") for user in result.get("data", {}).get("user_list", [])]
291
+ user_dict = {user.get("email"): user.get("user_id", None) for user in result.get("data", {}).get("user_list", []) if user.get("email")}
290
292
 
291
293
  logger.debug(f"发送响应logid: {response.headers.get('X-Tt-Logid')}")
292
294
 
293
- return user_ids
295
+ return user_dict
294
296
  except Exception as e:
295
297
  logger.warning(f"批量获取用户ID失败: {e}")
296
298
  raise
299
+
300
+ def batch_get_all_user_ids(self, emails: list[str]) -> dict[str, None | str]:
301
+ """
302
+ 批量获取所有用户ID,自动分批处理(每批最多50个)
303
+
304
+ Args:
305
+ emails: 用户邮箱列表
306
+
307
+ Returns:
308
+ 所有用户ID字典(email -> user_id 的映射)
309
+
310
+ Raises:
311
+ Exception: 获取失败
312
+ """
313
+ if not emails:
314
+ return {}
315
+
316
+ all_user_dict = {}
317
+ batch_size = 50
318
+
319
+ # 分批处理
320
+ for i in range(0, len(emails), batch_size):
321
+ batch_emails = emails[i : i + batch_size]
322
+ logger.info(f"正在获取第 {i // batch_size + 1} 批用户ID,数量: {len(batch_emails)}")
323
+
324
+ try:
325
+ batch_user_dict = self.batch_get_user_ids(batch_emails)
326
+ all_user_dict.update(batch_user_dict)
327
+ except Exception as e:
328
+ logger.error(f"获取第 {i // batch_size + 1} 批用户ID失败: {e}")
329
+ # 继续处理下一批,不中断整个流程
330
+ continue
331
+
332
+ logger.info(f"共获取到 {len(all_user_dict)} 个用户ID")
333
+ return all_user_dict
dtsdance/tcc_inner.py CHANGED
@@ -20,7 +20,7 @@ class TCCInnerClient:
20
20
  region: str,
21
21
  dir: str,
22
22
  conf_name: str,
23
- ) -> dict[str, Any]:
23
+ ) -> list[dict[str, Any]]:
24
24
  """
25
25
  List TCC configurations.
26
26
  """
@@ -49,7 +49,7 @@ class TCCInnerClient:
49
49
  if base_resp.get("error_code", 0) != 0:
50
50
  raise TCCError(f"TCC API error: {base_resp.get('error_message', 'Unknown error')}")
51
51
 
52
- return result.get("data", {})
52
+ return result.get("data", [])
53
53
 
54
54
  except requests.RequestException as e:
55
55
  raise TCCError(f"Failed to get TCC config: {str(e)}") from e
dtsdance/tcc_open.py CHANGED
@@ -40,9 +40,9 @@ class TCCClient:
40
40
  self,
41
41
  ns_name: str,
42
42
  region: str,
43
+ dir: str,
43
44
  conf_name: str,
44
45
  value: str,
45
- dir: str,
46
46
  description: str,
47
47
  data_type: str,
48
48
  ) -> dict[str, Any]:
@@ -1,14 +0,0 @@
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,,
File without changes