intellif-aihub 0.1.0__py3-none-any.whl → 0.1.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.

Potentially problematic release.


This version of intellif-aihub might be problematic. Click here for more details.

@@ -0,0 +1,332 @@
1
+ # !/usr/bin/env python
2
+ # -*-coding:utf-8 -*-
3
+ """制品管理服务模块
4
+
5
+ 该模块提供了制品管理相关的功能,包括创建制品、上传制品、获取制品信息等。
6
+ 制品可以是数据集、模型、指标、日志、检查点、图像、预测结果等类型。
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from pathlib import Path
12
+ from typing import List, Optional
13
+
14
+ import httpx
15
+ import minio
16
+ from loguru import logger
17
+
18
+ from ..exceptions import APIError
19
+ from ..models.artifact import (
20
+ CreateArtifactsReq,
21
+ CreateArtifactsResponseData,
22
+ ArtifactResp,
23
+ InfinityPageSize,
24
+ ArtifactRespData,
25
+ ArtifactType,
26
+ StsResp,
27
+ )
28
+ from ..models.common import APIWrapper
29
+ from ..utils.s3 import S3_path_to_info, upload_dir_to_s3, download_dir_from_s3
30
+
31
+ # 制品管理API的基础路径
32
+ _Base = "/artifact-management/api/v1"
33
+
34
+
35
+ class ArtifactService:
36
+ """制品管理服务类,该类提供了制品管理相关的功能,包括创建制品、上传制品、获取制品信息等。
37
+
38
+
39
+ Methods:
40
+ get_by_run_id: 使用run_id获取制品
41
+ create_artifact: 创建一个制品文件
42
+ create_artifacts: 从目录创建一个制品
43
+ download_artifacts: 下载制品
44
+
45
+ """
46
+
47
+ def __init__(self, http: httpx.Client):
48
+ """初始化制品管理服务
49
+
50
+ Args:
51
+ http: HTTP客户端实例
52
+ """
53
+ self._http = http
54
+ self._Artifact = _Artifact(http)
55
+ self.sts = self._get_sts()
56
+ self.s3_client = minio.Minio(
57
+ self.sts.endpoint,
58
+ access_key=self.sts.access_key_id,
59
+ secret_key=self.sts.secret_access_key,
60
+ secure=False,
61
+ session_token=self.sts.session_token,
62
+ )
63
+
64
+ @property
65
+ def _artifact(self) -> _Artifact:
66
+ """获取内部制品管理实例
67
+
68
+ Returns:
69
+ _Artifact: 内部制品管理实例
70
+ """
71
+ return self._Artifact
72
+
73
+ def _create(self, payload: CreateArtifactsReq) -> CreateArtifactsResponseData:
74
+ """创建制品(内部方法)
75
+
76
+ Args:
77
+ payload: 创建制品请求参数
78
+
79
+ Returns:
80
+ CreateArtifactsResponseData: 创建制品响应数据
81
+ """
82
+ return self._artifact.create(payload)
83
+
84
+ def _upload_done(self, artifact_id: int) -> None:
85
+ """标记制品上传完成
86
+
87
+ Args:
88
+ artifact_id: 制品ID
89
+ """
90
+ return self._artifact.upload_done(artifact_id)
91
+
92
+ def _get_sts(self) -> StsResp:
93
+ """获取STS临时凭证
94
+
95
+ Returns:
96
+ StsResp: STS临时凭证信息
97
+ """
98
+ return self._artifact.get_sts()
99
+
100
+ def get_by_run_id(
101
+ self, run_id: str, artifact_path: Optional[str] = None
102
+ ) -> List[ArtifactResp]:
103
+ """根据运行ID获取制品列表
104
+
105
+ Args:
106
+ run_id: 运行ID
107
+ artifact_path: 制品路径,如果指定则只返回匹配的制品
108
+
109
+ Returns:
110
+ List[ArtifactResp]: 制品列表
111
+
112
+ Raises:
113
+ APIError: 当API调用失败时抛出
114
+ """
115
+ return self._artifact.get_by_run_id(run_id, artifact_path)
116
+
117
+ def create_artifact(
118
+ self,
119
+ local_path: str,
120
+ artifact_path: Optional[str] = None,
121
+ run_id: Optional[str] = None,
122
+ artifact_type: ArtifactType = ArtifactType.other,
123
+ ) -> None:
124
+ """创建单个文件制品并上传
125
+
126
+ 该方法用于将本地文件上传为制品。过程包括:
127
+ 1. 获取STS临时凭证
128
+ 2. 创建S3客户端
129
+ 3. 检查本地文件是否存在
130
+ 4. 创建制品记录
131
+ 5. 上传文件到S3
132
+ 6. 标记上传完成
133
+
134
+ Args:
135
+ local_path (str): 本地文件路径
136
+ artifact_path (str): 制品路径,如果为None则使用本地文件名
137
+ run_id (str): 运行ID,关联制品与特定运行
138
+ artifact_type (ArtifactType): 制品类型,默认为other
139
+
140
+ Raises:
141
+ ValueError: 当本地文件不存在时抛出
142
+ APIError: 当API调用失败时抛出
143
+ """
144
+ logger.info(f"log artifact: {artifact_path},local path: {local_path} ")
145
+
146
+ # 检查文件是否存在
147
+ if not os.path.exists(local_path):
148
+ raise ValueError(f"File {local_path} does not exist")
149
+ req = CreateArtifactsReq(
150
+ entity_id=run_id,
151
+ entity_type=artifact_type,
152
+ src_path=artifact_path,
153
+ is_dir=False,
154
+ )
155
+ resp = self._create(req)
156
+ bucket, object_name = S3_path_to_info(resp.s3_path)
157
+
158
+ self.s3_client.fput_object(bucket, object_name, local_path)
159
+ self._upload_done(resp.id)
160
+ logger.info(f"log artifact done: {artifact_path}")
161
+ return
162
+
163
+ def create_artifacts(
164
+ self,
165
+ local_dir: str,
166
+ artifact_path: Optional[str] = None,
167
+ run_id: Optional[str] = None,
168
+ artifact_type: ArtifactType = ArtifactType.other,
169
+ ) -> None:
170
+ """创建目录制品并上传
171
+
172
+ 该方法用于将本地目录上传为制品。过程包括:
173
+ 1. 获取STS临时凭证
174
+ 2. 创建S3客户端
175
+ 3. 检查本地目录是否存在
176
+ 4. 创建制品记录
177
+ 5. 上传目录内容到S3
178
+ 6. 标记上传完成
179
+
180
+ Args:
181
+ local_dir (str): 本地目录路径
182
+ artifact_path (str): 制品路径,如果为None则使用本地目录名
183
+ run_id (str): 运行ID,关联制品与特定运行
184
+ artifact_type (ArtifactType): 制品类型,默认为other
185
+
186
+ Raises:
187
+ ValueError: 当本地目录不存在时抛出
188
+ APIError: 当API调用失败时抛出
189
+ """
190
+
191
+ logger.info(f"log artifact: {artifact_path},local path: {local_dir} ")
192
+ if not os.path.exists(local_dir):
193
+ raise ValueError(f"File {local_dir} does not exist")
194
+ req = CreateArtifactsReq(
195
+ entity_id=run_id,
196
+ entity_type=artifact_type,
197
+ src_path=artifact_path,
198
+ is_dir=True,
199
+ )
200
+ resp = self._create(req)
201
+ bucket, object_name = S3_path_to_info(resp.s3_path)
202
+ upload_dir_to_s3(self.s3_client, local_dir, bucket, object_name)
203
+ self._upload_done(resp.id)
204
+ logger.info(f"log artifact done: {artifact_path}")
205
+ return
206
+
207
+ def download_artifacts(
208
+ self, run_id: str, artifact_path: Optional[str], local_dir: str
209
+ ) -> None:
210
+ """下载制品
211
+
212
+ Args:
213
+ run_id: 运行ID
214
+ artifact_path: 制品路径
215
+ local_dir: 本地目录路径
216
+
217
+ Raises:
218
+ APIError: 当API调用失败时抛出
219
+ """
220
+ artifacts = self.get_by_run_id(run_id, artifact_path)
221
+
222
+ for artifact_item in artifacts:
223
+ bucket, object_name = S3_path_to_info(artifact_item.s3_path)
224
+ if artifact_item.is_dir:
225
+ download_dir_from_s3(self.s3_client, bucket, object_name, local_dir)
226
+ else:
227
+ self.s3_client.fget_object(
228
+ bucket, object_name, str(Path(local_dir) / artifact_item.src_path)
229
+ )
230
+
231
+ logger.info(f"download artifact done: {artifact_path}")
232
+ return
233
+
234
+
235
+ class _Artifact:
236
+ """内部制品管理类
237
+
238
+ 该类提供了与制品管理API交互的底层方法。
239
+ 通常不直接使用该类,而是通过ArtifactService类访问其功能。
240
+ """
241
+
242
+ def __init__(self, http: httpx.Client):
243
+ """初始化内部制品管理类
244
+
245
+ Args:
246
+ http: HTTP客户端实例
247
+ """
248
+ self._http = http
249
+
250
+ def create(self, payload: CreateArtifactsReq) -> CreateArtifactsResponseData:
251
+ """创建制品记录
252
+
253
+ Args:
254
+ payload: 创建制品请求参数
255
+
256
+ Returns:
257
+ CreateArtifactsResponseData: 创建制品响应数据
258
+
259
+ Raises:
260
+ APIError: 当API调用失败时抛出
261
+ """
262
+ resp = self._http.post(f"{_Base}/artifacts", json=payload.model_dump())
263
+
264
+ wrapper = APIWrapper[CreateArtifactsResponseData].model_validate(resp.json())
265
+ if wrapper.code != 0:
266
+ raise APIError(f"backend code {wrapper.code}: {wrapper.msg}")
267
+ return wrapper.data
268
+
269
+ def upload_done(self, artifact_id: int) -> None:
270
+ """标记制品上传完成
271
+
272
+ 在制品文件上传到S3后,需要调用此方法标记上传完成。
273
+
274
+ Args:
275
+ artifact_id: 制品ID
276
+
277
+ Raises:
278
+ APIError: 当API调用失败时抛出
279
+ """
280
+ resp = self._http.post(f"{_Base}/artifacts/{artifact_id}/uploaded")
281
+ wrapper = APIWrapper.model_validate(resp.json())
282
+ if wrapper.code != 0:
283
+ raise APIError(f"backend code {wrapper.code}: {wrapper.msg}")
284
+ return
285
+
286
+ def get_by_run_id(
287
+ self, run_id: str, artifact_path: Optional[str]
288
+ ) -> List[ArtifactResp]:
289
+ """根据运行ID获取制品列表
290
+
291
+ Args:
292
+ run_id: 运行ID
293
+ artifact_path: 制品路径,如果指定则只返回匹配的制品
294
+
295
+ Returns:
296
+ List[ArtifactResp]: 制品列表
297
+
298
+ Raises:
299
+ APIError: 当API调用失败时抛出
300
+ """
301
+ resp = self._http.get(
302
+ f"{_Base}/artifacts?entity_id={run_id}&page_num=1&page_size={InfinityPageSize}"
303
+ )
304
+ wrapper = APIWrapper[ArtifactRespData].model_validate(resp.json())
305
+ if wrapper.code != 0:
306
+ raise APIError(f"backend code {wrapper.code}: {wrapper.msg}")
307
+ if artifact_path:
308
+ return [
309
+ artifact
310
+ for artifact in wrapper.data.data
311
+ if artifact.src_path == artifact_path
312
+ ]
313
+ else:
314
+ return wrapper.data.data
315
+
316
+ def get_sts(self) -> StsResp:
317
+ """获取STS临时凭证
318
+
319
+ 获取用于访问S3存储的临时凭证。
320
+
321
+ Returns:
322
+ StsResp: STS临时凭证信息
323
+
324
+ Raises:
325
+ APIError: 当API调用失败时抛出
326
+ """
327
+ resp = self._http.get(f"{_Base}/artifacts/get-sts")
328
+ logger.info(f"get sts: {resp.text}")
329
+ wrapper = APIWrapper[StsResp].model_validate(resp.json())
330
+ if wrapper.code != 0:
331
+ raise APIError(f"backend code {wrapper.code}: {wrapper.msg}")
332
+ return wrapper.data
@@ -0,0 +1,240 @@
1
+ from __future__ import annotations
2
+
3
+ import mimetypes
4
+ import os
5
+ import pathlib
6
+
7
+ import httpx
8
+
9
+ from ..exceptions import APIError
10
+ from ..models.common import APIWrapper
11
+ from ..models.dataset_management import *
12
+ from ..utils.download import dataset_download
13
+
14
+ _BASE = "/dataset-mng/api/v2"
15
+
16
+
17
+ class DatasetManagementService:
18
+ """数据集管理服务,用于数据集的上传、下载
19
+
20
+ Methods:
21
+ create_dataset_and_version: 创建数据集版本
22
+ run_download: 下载
23
+
24
+
25
+ """
26
+
27
+ def __init__(self, http: httpx.Client):
28
+ self._dataset = _Dataset(http)
29
+ self._dataset_version = _DatasetVersion(http)
30
+ self._upload = _Upload(http)
31
+
32
+ # 直接把常用方法抛到一级,调用体验简单
33
+ def create_dataset(self, payload: CreateDatasetRequest) -> int:
34
+ return self._dataset.create(payload)
35
+
36
+ def get_dataset(self, dataset_id: int) -> DatasetDetail:
37
+ return self._dataset.get(dataset_id)
38
+
39
+ def create_dataset_version(self, payload: CreateDatasetVersionRequest) -> int:
40
+ return self._dataset_version.create(payload)
41
+
42
+ def upload_dataset_version(self, payload: UploadDatasetVersionRequest) -> int:
43
+ return self._dataset_version.upload(payload)
44
+
45
+ def get_dataset_version(self, version_id: int) -> DatasetVersionDetail:
46
+ return self._dataset_version.get(version_id)
47
+
48
+ def get_dataset_version_by_name(self, version_name: str) -> DatasetVersionDetail:
49
+ return self._dataset_version.get_by_name(version_name)
50
+
51
+ def upload_file(self, file_path: str) -> FileUploadData:
52
+ return self._upload.upload_file(file_path)
53
+
54
+ # 如果想要访问子对象,也保留属性
55
+ @property
56
+ def dataset(self) -> _Dataset:
57
+ return self._dataset
58
+
59
+ @property
60
+ def dataset_version(self) -> _DatasetVersion:
61
+ return self._dataset_version
62
+
63
+ def create_dataset_and_version(
64
+ self,
65
+ *,
66
+ dataset_name: str,
67
+ dataset_description: str = "",
68
+ is_local_upload: bool,
69
+ local_file_path: str | None = None,
70
+ server_file_path: str | None = None,
71
+ version_description: str = "",
72
+ ) -> tuple[int, int, str]:
73
+ """创建数据集及其版本。
74
+
75
+ 根据参数创建数据集,并根据上传类型(本地或服务器路径)创建对应的数据集版本。
76
+
77
+ Args:
78
+ dataset_name: 数据集名称。
79
+ dataset_description: 数据集描述,默认为空。
80
+ is_local_upload: 是否为本地上传。若为 True,需提供 local_file_path;
81
+ 否则需提供 server_file_path。
82
+ local_file_path: 本地文件路径,当 is_local_upload=True 时必须提供。
83
+ server_file_path: 服务器已有文件路径,当 is_local_upload=False 时必须提供。
84
+ version_description: 版本描述,默认为空。
85
+
86
+ Returns:
87
+ tuple[int, int, str]: 一个三元组,包含:[数据集 ID,数据集版本 ID, 数据集版本标签(格式为 <dataset_name>/V<version_number>)]
88
+ """
89
+ if is_local_upload:
90
+ if not local_file_path:
91
+ raise ValueError("is_local_upload=True 时必须提供 local_file_path")
92
+ upload_type = 1
93
+ else:
94
+ if not server_file_path:
95
+ raise ValueError("is_local_upload=False 时必须提供 server_file_path")
96
+ upload_type = 3
97
+
98
+ dataset_id = self._dataset.create(
99
+ CreateDatasetRequest(
100
+ name=dataset_name,
101
+ description=dataset_description,
102
+ tags=[],
103
+ )
104
+ )
105
+
106
+ if is_local_upload:
107
+ upload_data = self._upload.upload_file(local_file_path)
108
+ upload_path = upload_data.path
109
+ else:
110
+ upload_path = server_file_path
111
+
112
+ version_id = self._dataset_version.upload(
113
+ UploadDatasetVersionRequest(
114
+ upload_path=upload_path,
115
+ upload_type=upload_type,
116
+ dataset_id=dataset_id,
117
+ description=version_description,
118
+ )
119
+ )
120
+
121
+ detail = self._dataset.get(dataset_id)
122
+ ver_num = next(
123
+ (v.version for v in detail.versions if v.id == version_id),
124
+ None,
125
+ )
126
+ if ver_num is None:
127
+ ver_num = 1
128
+
129
+ version_tag = f"{detail.name}/V{ver_num}"
130
+
131
+ return dataset_id, version_id, version_tag
132
+
133
+ def run_download(
134
+ self, *, dataset_version_name: str, local_dir: str, worker: int = 4
135
+ ) -> None:
136
+ """根据数据集版本名称下载对应的数据集文件。
137
+
138
+ Args:
139
+ dataset_version_name (str): 数据集版本名称。
140
+ local_dir (str): 下载文件保存的本地目录路径。
141
+ worker (int): 并发下载使用的线程数,默认为 4。
142
+
143
+ Raises:
144
+ APIError: 如果获取到的版本信息中没有 parquet_index_path,即无法进行下载时抛出异常。
145
+
146
+ Returns:
147
+ None
148
+ """
149
+ detail = self._dataset_version.get_by_name(dataset_version_name)
150
+ if not detail.parquet_index_path:
151
+ raise APIError("parquet_index_path 为空")
152
+ dataset_download(detail.parquet_index_path, local_dir, worker)
153
+
154
+
155
+ class _Dataset:
156
+ def __init__(self, http: httpx.Client):
157
+ self._http = http
158
+
159
+ def create(self, payload: CreateDatasetRequest) -> int:
160
+ resp = self._http.post(
161
+ f"{_BASE}/datasets",
162
+ json=payload.model_dump(by_alias=True, exclude_none=True),
163
+ )
164
+ wrapper = APIWrapper[CreateDatasetResponse].model_validate(resp.json())
165
+ if wrapper.code != 0:
166
+ raise APIError(f"backend code {wrapper.code}: {wrapper.msg}")
167
+ return wrapper.data.id
168
+
169
+ def get(self, dataset_id: int) -> DatasetDetail:
170
+ resp = self._http.get(f"{_BASE}/datasets/{dataset_id}")
171
+ wrapper = APIWrapper[DatasetDetail].model_validate(resp.json())
172
+ if wrapper.code != 0:
173
+ raise APIError(f"backend code {wrapper.code}: {wrapper.msg}")
174
+ return wrapper.data
175
+
176
+
177
+ class _DatasetVersion:
178
+ def __init__(self, http: httpx.Client):
179
+ self._http = http
180
+
181
+ def create(self, payload: CreateDatasetVersionRequest) -> int:
182
+ resp = self._http.post(
183
+ f"{_BASE}/dataset-versions",
184
+ json=payload.model_dump(by_alias=True, exclude_none=True),
185
+ )
186
+ wrapper = APIWrapper[CreateDatasetVersionResponse].model_validate(resp.json())
187
+ if wrapper.code != 0:
188
+ raise APIError(f"backend code {wrapper.code}: {wrapper.msg}")
189
+ return wrapper.data.id
190
+
191
+ def upload(self, payload: UploadDatasetVersionRequest) -> int:
192
+ resp = self._http.post(
193
+ f"{_BASE}/dataset-versions-upload",
194
+ json=payload.model_dump(by_alias=True, exclude_none=True),
195
+ )
196
+ wrapper = APIWrapper[UploadDatasetVersionResponse].model_validate(resp.json())
197
+ if wrapper.code != 0:
198
+ raise APIError(f"backend code {wrapper.code}: {wrapper.msg}")
199
+ return wrapper.data.id
200
+
201
+ def get(self, version_id: int) -> DatasetVersionDetail:
202
+ resp = self._http.get(f"{_BASE}/dataset-versions/{version_id}")
203
+ wrapper = APIWrapper[DatasetVersionDetail].model_validate(resp.json())
204
+ if wrapper.code != 0:
205
+ raise APIError(f"backend code {wrapper.code}: {wrapper.msg}")
206
+ return wrapper.data
207
+
208
+ def get_by_name(self, version_name: str) -> DatasetVersionDetail:
209
+ resp = self._http.get(
210
+ f"{_BASE}/dataset-versions-detail", params={"name": version_name}
211
+ )
212
+ wrapper = APIWrapper[DatasetVersionDetail].model_validate(resp.json())
213
+ if wrapper.code != 0:
214
+ raise APIError(f"backend code {wrapper.code}: {wrapper.msg}")
215
+ return wrapper.data
216
+
217
+
218
+ class _Upload:
219
+ def __init__(self, http: httpx.Client):
220
+ self._http = http
221
+
222
+ def upload_file(self, file_path: str) -> FileUploadData:
223
+ if not os.path.isfile(file_path):
224
+ raise FileNotFoundError(file_path)
225
+
226
+ file_name = pathlib.Path(file_path).name
227
+ mime_type = mimetypes.guess_type(file_name)[0] or "application/octet-stream"
228
+
229
+ with open(file_path, "rb") as fp:
230
+ resp = self._http.post(
231
+ f"/dataset-mng/api/v1/uploads",
232
+ files={"file": (file_name, fp, mime_type)},
233
+ timeout=None,
234
+ )
235
+
236
+ wrapper = APIWrapper[FileUploadData].model_validate(resp.json())
237
+ if wrapper.code != 0:
238
+ raise APIError(f"backend code {wrapper.code}: {wrapper.msg}")
239
+
240
+ return wrapper.data
@@ -0,0 +1,43 @@
1
+ # !/usr/bin/env python
2
+ # -*-coding:utf-8 -*-
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+
7
+ from ..exceptions import APIError
8
+ from ..models.common import APIWrapper
9
+ from ..models.document_center import *
10
+
11
+ _BASE = "/document-center/api/v1"
12
+
13
+
14
+ class DocumentCenterService:
15
+ def __init__(self, http: httpx.Client):
16
+ self._document = _Document(http)
17
+
18
+ def get_documents(
19
+ self, page_size: int = 9999, page_num: int = 1, name: str = ""
20
+ ) -> List[Document]:
21
+ return self._document.get_documents(page_size, page_num, name)
22
+
23
+ @property
24
+ def document(self) -> _Document:
25
+ return self._document
26
+
27
+
28
+ class _Document:
29
+ def __init__(self, http: httpx.Client):
30
+ self._http = http
31
+
32
+ def get_documents(
33
+ self, page_size: int = 9999, page_num: int = 1, name: str = ""
34
+ ) -> List[Document]:
35
+ params = {"page_size": page_size, "page_num": page_num, "name": name}
36
+ resp = self._http.get(f"{_BASE}/documents", params=params)
37
+ if resp.status_code != 200:
38
+ raise APIError(f"backend code {resp.status_code}: {resp.text}")
39
+ res = resp.json()
40
+ wrapper = APIWrapper[GetDocumentsResponse].model_validate(res)
41
+ if wrapper.code != 0:
42
+ raise APIError(f"backend code {wrapper.code}: {wrapper.msg}")
43
+ return wrapper.data.data
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+
5
+ from ..exceptions import APIError
6
+ from ..models.common import APIWrapper
7
+ from ..models.labelfree import *
8
+
9
+ _BASE = "/labelfree/api/v2"
10
+
11
+
12
+ class LabelfreeService:
13
+ def __init__(self, http: httpx.Client):
14
+ self._project = _Project(http)
15
+
16
+ def get_project_global_stats(self, project_name: str) -> GetGlobalStatsResponse:
17
+ """获取标注项目的进展信息
18
+
19
+ Args:
20
+ project_name: 标注项目名称
21
+
22
+ Returns:
23
+ GetGlobalStatsResponse
24
+
25
+ """
26
+ return self._project.get_global_stats(project_name)
27
+
28
+ @property
29
+ def project(self) -> _Project:
30
+ return self._project
31
+
32
+
33
+ class _Project:
34
+ def __init__(self, http: httpx.Client):
35
+ self._http = http
36
+
37
+ def get_global_stats(self, project_name: str) -> GetGlobalStatsResponse:
38
+ resp = self._http.get(
39
+ f"{_BASE}/projects/global_stats", params={"project_name": project_name}
40
+ )
41
+ wrapper = APIWrapper[GetGlobalStatsResponse].model_validate(resp.json())
42
+ if wrapper.code != 0:
43
+ raise APIError(f"backend code {wrapper.code}: {wrapper.msg}")
44
+ return wrapper.data
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ _ENV_KEY = "PRE_STOP_SENTINEL_FILE"
7
+ _DEFAULT_SENTINEL = "/tmp/pre_stop_sentinel_file"
8
+ _SENTINEL_PATH = Path(os.getenv(_ENV_KEY, _DEFAULT_SENTINEL))
9
+
10
+
11
+ def is_pre_stopped() -> bool:
12
+ return _SENTINEL_PATH.exists()
13
+
14
+
15
+ class PreStopService:
16
+ @staticmethod
17
+ def is_pre_stopped() -> bool:
18
+ return is_pre_stopped()
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+
6
+
7
+ class ReportService:
8
+ @staticmethod
9
+ def report_envs():
10
+ pod_namespace = os.environ.get('POD_NAMESPACE', '')
11
+ pod_name = os.environ.get('POD_NAME', '')
12
+ if not pod_namespace or not pod_name:
13
+ return
14
+ env_file_path = f'/var/mtp/log/{pod_namespace}/{pod_name}/env'
15
+ try:
16
+ os.makedirs(os.path.dirname(env_file_path), exist_ok=True)
17
+ with open(env_file_path, 'w', encoding='utf-8') as f:
18
+ f.write(json.dumps(dict(os.environ)))
19
+ except Exception as e:
20
+ print(f"[report_envs] 写入失败: {e}", flush=True)