hw-cloudrobo-infer 0.2.0__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.
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: hw-cloudrobo-infer
3
+ Version: 0.2.0
4
+ Summary: CloudRobo Inference Service SDK and CLI
5
+ Author-email: Huawei Cloud CloudRobo Team <hwcloudrobo@huawei.com>
6
+ License: Apache-2.0
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: Apache Software License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.8
19
+ Requires-Dist: click>=8.0
20
+ Requires-Dist: hw-cloudrobo-core>=0.1.0
21
+ Requires-Dist: hw-cloudrobo-workspace>=0.1.0
@@ -0,0 +1,16 @@
1
+ # cloudrobo-infer
2
+
3
+ 推理服务模块,提供推理服务的创建、查询、启停和模型部署等待。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pip install -e packages/cloudrobo-infer
9
+ ```
10
+
11
+ ## 文档
12
+
13
+ - [模块概览](docs/index.md)
14
+ - [CLI 命令](docs/commands.md)
15
+ - [使用示例](docs/examples.md)
16
+ - [开发指南](docs/development.md)
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools==83.0.0", "packaging==26.2", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hw-cloudrobo-infer"
7
+ version = "0.2.0"
8
+ description = "CloudRobo Inference Service SDK and CLI"
9
+ requires-python = ">=3.8"
10
+ authors = [{name = "Huawei Cloud CloudRobo Team", email = "hwcloudrobo@huawei.com"}]
11
+ license = {text = "Apache-2.0"}
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: Apache Software License",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.8",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ ]
25
+ dependencies = [
26
+ "click>=8.0",
27
+ "hw-cloudrobo-core>=0.1.0",
28
+ "hw-cloudrobo-workspace>=0.1.0",
29
+ ]
30
+
31
+ [project.entry-points."cloudrobo.groups"]
32
+ infer = "cloudrobo_infer.cli:infer"
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from cloudrobo_infer.client import InferClient
2
+
3
+ __all__ = ["InferClient"]
@@ -0,0 +1,329 @@
1
+ import json
2
+
3
+ import click
4
+ from cloudrobo_core.cli.cli_utils import get_client, out
5
+
6
+ from .client import InferClient
7
+
8
+ # isort: off
9
+ # ===== VALIDATOR IMPORTS =====
10
+ from .validators.cli_callbacks import (
11
+ _validate_cmd,
12
+ _validate_deploy_timeout_minutes,
13
+ _validate_description,
14
+ _validate_end_time,
15
+ _validate_envs,
16
+ _validate_files,
17
+ _validate_flavor,
18
+ _validate_image_swr_url,
19
+ _validate_keywords,
20
+ _validate_limit,
21
+ _validate_line_num,
22
+ _validate_liveness_health,
23
+ _validate_model,
24
+ _validate_model_ext_metadata,
25
+ _validate_model_id,
26
+ _validate_model_name,
27
+ _validate_model_version_id,
28
+ _validate_model_version_name,
29
+ _validate_name,
30
+ _validate_offset,
31
+ _validate_pool_id,
32
+ _validate_pool_type,
33
+ _validate_readiness_health,
34
+ _validate_service_id,
35
+ _validate_service_invoke,
36
+ _validate_skill_config,
37
+ _validate_sort_dir,
38
+ _validate_sort_key,
39
+ _validate_start_time,
40
+ _validate_startup_health,
41
+ _validate_status,
42
+ _validate_stop_schedule,
43
+ _validate_user_id,
44
+ _validate_user_name,
45
+ _validate_workspace_id,
46
+ )
47
+ # ===== END VALIDATOR IMPORTS =====
48
+ # isort: on
49
+
50
+
51
+
52
+
53
+
54
+ @click.group()
55
+ def infer():
56
+ """推理服务命令组"""
57
+
58
+
59
+
60
+
61
+
62
+ def _parse_json_options(ctx, target, options):
63
+ for key, raw in options.items():
64
+ if raw:
65
+ try:
66
+ target[key] = json.loads(raw)
67
+ except json.JSONDecodeError as e:
68
+ raise click.BadParameter(f"Invalid JSON for '{key}': {e}")
69
+
70
+
71
+ def _parse_required_json(ctx, raw, key):
72
+ if not raw:
73
+ raise click.BadParameter(f"'{key}' is required and must be a JSON object")
74
+ try:
75
+ return json.loads(raw)
76
+ except json.JSONDecodeError as e:
77
+ raise click.BadParameter(f"Invalid JSON for '{key}': {e}")
78
+
79
+
80
+ @infer.command("create")
81
+ @click.option("--cmd", default=None, callback=_validate_cmd, help='启动命令')
82
+ @click.option("--deploy-timeout-minutes", type=click.IntRange(1, 300), callback=_validate_deploy_timeout_minutes, help='部署超时时间')
83
+ @click.option("--description", default=None, callback=_validate_description, help='描述')
84
+ @click.option("--envs-json", default=None, callback=_validate_envs, help='环境变量')
85
+ @click.option("--files-json", default=None, callback=_validate_files, help='文件挂载')
86
+ @click.option("--flavor", required=True, callback=_validate_flavor, help='资源规格')
87
+ @click.option("--image-swr-url", default=None, callback=_validate_image_swr_url, help='镜像地址')
88
+ @click.option("--internet-access-enable", is_flag=True, default=False, help='是否开启公网访问')
89
+ @click.option("--liveness-health-json", default=None, callback=_validate_liveness_health, help='模型服务健康检查')
90
+ @click.option("--model-json", required=True, callback=_validate_model, help='模型信息')
91
+ @click.option("--model-ext-metadata", default=None, callback=_validate_model_ext_metadata, help='模型扩展元数据,JSON/YAML 格式的 r2c 配置信息。')
92
+ @click.option("--name", required=True, callback=_validate_name, help='服务名称')
93
+ @click.option("--pool-id", required=True, callback=_validate_pool_id, help='资源池ID')
94
+ @click.option("--pool-type", type=click.Choice(['DEDICATED', 'SHARED'], case_sensitive=False), callback=_validate_pool_type, required=True, help='资源池类型')
95
+ @click.option("--readiness-health-json", default=None, callback=_validate_readiness_health, help='模型服务健康检查')
96
+ @click.option("--service-invoke-json", default=None, callback=_validate_service_invoke, help='服务调用配置')
97
+ @click.option("--skill-config-json", default=None, callback=_validate_skill_config, help='技能配置')
98
+ @click.option("--startup-health-json", default=None, callback=_validate_startup_health, help='模型服务健康检查')
99
+ @click.option("--stop-schedule-json", default=None, callback=_validate_stop_schedule, help='定时停止配置')
100
+ @click.option("--workspace-id", required=False, default=None, callback=_validate_workspace_id, help='工作空间ID')
101
+ @click.option("--dry-run", is_flag=True)
102
+ @click.pass_context
103
+ def create_infer_service(ctx, cmd, deploy_timeout_minutes, description, envs_json,
104
+ files_json, flavor, image_swr_url, internet_access_enable,
105
+ liveness_health_json, model_json, model_ext_metadata, name,
106
+ pool_id, pool_type, readiness_health_json,
107
+ service_invoke_json, skill_config_json, startup_health_json,
108
+ stop_schedule_json, workspace_id, dry_run):
109
+ """创建推理服务(部署模型)"""
110
+ req = {
111
+ "name": name,
112
+ "flavor": flavor,
113
+ "model": _parse_required_json(ctx, model_json, "model"),
114
+ "workspace_id": workspace_id,
115
+ "pool_id": pool_id,
116
+ "pool_type": pool_type.upper(),
117
+ }
118
+ if description is not None:
119
+ req["description"] = description
120
+ if image_swr_url is not None:
121
+ req["image_swr_url"] = image_swr_url
122
+ if cmd is not None:
123
+ req["cmd"] = cmd
124
+ if deploy_timeout_minutes is not None:
125
+ req["deploy_timeout_minutes"] = deploy_timeout_minutes
126
+ if model_ext_metadata is not None:
127
+ req["model_ext_metadata"] = model_ext_metadata
128
+ if internet_access_enable is not None:
129
+ req["internet_access_enable"] = str(internet_access_enable).lower() in ("1", "true", "yes")
130
+ _parse_json_options(ctx, req, {
131
+ "envs": envs_json,
132
+ "stop_schedule": stop_schedule_json,
133
+ "service_invoke": service_invoke_json,
134
+ "skill_config": skill_config_json,
135
+ "files": files_json,
136
+ "startup_health": startup_health_json,
137
+ "readiness_health": readiness_health_json,
138
+ "liveness_health": liveness_health_json,
139
+ })
140
+ if dry_run:
141
+ click.echo(f"[DRY-RUN] create_infer_service({json.dumps(req, ensure_ascii=False)})")
142
+ return
143
+ client = get_client(ctx, InferClient)
144
+ result = client.create_infer_service(req)
145
+ out(result)
146
+
147
+
148
+ @infer.command("list")
149
+ @click.option("--limit", type=click.IntRange(1, 50), default=10, callback=_validate_limit, help='每页数据条数')
150
+ @click.option("--offset", type=click.IntRange(0, 1000), default=0, callback=_validate_offset, help='分页页码偏移量')
151
+ @click.option("--sort-key", type=click.Choice(['create_at', 'created_at', 'update_at', 'updated_at'], case_sensitive=False), callback=_validate_sort_key, help='排序字段,支持 create_at / update_at / created_at / updated_at')
152
+ @click.option("--sort-dir", type=click.Choice(['ASC', 'DESC'], case_sensitive=False), callback=_validate_sort_dir, help='排序方向,ASC正序 / DESC倒序')
153
+ @click.option("--name", default=None, callback=_validate_name, help='推理服务名称模糊查询')
154
+ @click.option("--workspace-id", required=False, default=None, callback=_validate_workspace_id, help='工作空间ID')
155
+ @click.option("--status", default=None, callback=_validate_status, help='根据服务状态查询相关推理服务,支持多选')
156
+ @click.option("--model-id", default=None, callback=_validate_model_id, help='模型资产ID筛选')
157
+ @click.option("--model-name", default=None, callback=_validate_model_name, help='模型资产名称筛选')
158
+ @click.option("--model-version-id", default=None, callback=_validate_model_version_id, help='模型版本ID筛选')
159
+ @click.option("--model-version-name", default=None, callback=_validate_model_version_name, help='模型版本名称筛选')
160
+ @click.option("--user-name", default=None, callback=_validate_user_name, help='创建人名称筛选')
161
+ @click.option("--user-id", default=None, callback=_validate_user_id, help='创建人ID筛选')
162
+ @click.option("--contain-ext-metadata", is_flag=True, default=None, help='是否只返回包含 model_ext_metadata 的记录。省略=返回全部;true=只返回有 model_ext_metadata 的记录;false=只返回没有 model_ext_metadata 的记录')
163
+ @click.pass_context
164
+ def list_infer_services(ctx, limit, offset, sort_key, sort_dir, name, workspace_id,
165
+ status, model_id, model_name, model_version_id,
166
+ model_version_name, user_name, user_id, contain_ext_metadata):
167
+ """查询推理服务列表"""
168
+ client = get_client(ctx, InferClient)
169
+ params = {}
170
+ if limit is not None:
171
+ params["limit"] = limit
172
+ if offset is not None:
173
+ params["offset"] = offset
174
+ if sort_key:
175
+ params["sort_key"] = sort_key
176
+ if sort_dir:
177
+ params["sort_dir"] = sort_dir
178
+ if name:
179
+ params["name"] = name
180
+ if workspace_id:
181
+ params["workspace_id"] = workspace_id
182
+ if status:
183
+ params["status"] = status
184
+ if model_id:
185
+ params["model_id"] = model_id
186
+ if model_name:
187
+ params["model_name"] = model_name
188
+ if model_version_id:
189
+ params["model_version_id"] = model_version_id
190
+ if model_version_name:
191
+ params["model_version_name"] = model_version_name
192
+ if user_name:
193
+ params["user_name"] = user_name
194
+ if user_id:
195
+ params["user_id"] = user_id
196
+ if contain_ext_metadata is not None:
197
+ params["contain_ext_metadata"] = contain_ext_metadata
198
+ result = client.list_infer_services(**params)
199
+ out(result)
200
+
201
+
202
+ @infer.command("show")
203
+ @click.option("--service-id", required=True, callback=_validate_service_id, help='推理服务唯一标识ID')
204
+ @click.pass_context
205
+ def show_infer_service(ctx, service_id):
206
+ """查询推理服务详情"""
207
+ client = get_client(ctx, InferClient)
208
+ result = client.show_infer_service(service_id)
209
+ out(result)
210
+
211
+
212
+ @infer.command("update")
213
+ @click.option("--service-id", required=True, callback=_validate_service_id, help='推理服务唯一标识ID')
214
+ @click.option("--description", default=None, callback=_validate_description, help='描述')
215
+ @click.option("--model-ext-metadata", default=None, callback=_validate_model_ext_metadata, help='模型扩展元数据,JSON/YAML 格式的 r2c 配置信息。')
216
+ @click.option("--dry-run", is_flag=True)
217
+ @click.pass_context
218
+ def update_infer_service(ctx, service_id, description, model_ext_metadata, dry_run):
219
+ """更新推理服务配置"""
220
+ req = {}
221
+ if description is not None:
222
+ req["description"] = description
223
+ if model_ext_metadata is not None:
224
+ req["model_ext_metadata"] = model_ext_metadata
225
+ if dry_run:
226
+ click.echo(
227
+ f"[DRY-RUN] update_infer_service(service_id={service_id}, "
228
+ f"{json.dumps(req, ensure_ascii=False)})"
229
+ )
230
+ return
231
+ client = get_client(ctx, InferClient)
232
+ result = client.update_infer_service(service_id, req)
233
+ out(result)
234
+
235
+
236
+ @infer.command("delete")
237
+ @click.option("--service-id", required=True, callback=_validate_service_id, help='推理服务唯一标识ID')
238
+ @click.option("--dry-run", is_flag=True)
239
+ @click.pass_context
240
+ def delete_infer_service(ctx, service_id, dry_run):
241
+ """删除推理服务"""
242
+ if dry_run:
243
+ click.echo(f"[DRY-RUN] delete_infer_service(service_id={service_id})")
244
+ return
245
+ client = get_client(ctx, InferClient)
246
+ client.delete_infer_service(service_id)
247
+ out(f"deleted: service_id={service_id}")
248
+
249
+
250
+ @infer.command("start")
251
+ @click.option("--service-id", required=True, callback=_validate_service_id, help='推理服务唯一标识ID')
252
+ @click.option("--dry-run", is_flag=True)
253
+ @click.pass_context
254
+ def start_infer_service(ctx, service_id, dry_run):
255
+ """启动推理服务"""
256
+ if dry_run:
257
+ click.echo(f"[DRY-RUN] start_infer_service(service_id={service_id})")
258
+ return
259
+ client = get_client(ctx, InferClient)
260
+ result = client.start_infer_service(service_id)
261
+ out(result)
262
+
263
+
264
+ @infer.command("stop")
265
+ @click.option("--service-id", required=True, callback=_validate_service_id, help='推理服务唯一标识ID')
266
+ @click.option("--dry-run", is_flag=True)
267
+ @click.pass_context
268
+ def stop_infer_service(ctx, service_id, dry_run):
269
+ """停止推理服务"""
270
+ if dry_run:
271
+ click.echo(f"[DRY-RUN] stop_infer_service(service_id={service_id})")
272
+ return
273
+ client = get_client(ctx, InferClient)
274
+ result = client.stop_infer_service(service_id)
275
+ out(result)
276
+
277
+
278
+ @infer.command("list-logs")
279
+ @click.option("--service-id", required=True, callback=_validate_service_id, help='推理服务唯一标识ID')
280
+ @click.option("--end-time", type=click.IntRange(0, 32503680000000), required=True, callback=_validate_end_time, help='搜索日志的结束时间。')
281
+ @click.option("--highlight", is_flag=True, default=None, help='在查询结果中日志关键词是否高亮显示。')
282
+ @click.option("--is-count", is_flag=True, default=None, help='在查询结果中是否统计日志条数。')
283
+ @click.option("--is-desc", is_flag=True, default=None, help='表示日志查询的顺序,当前支持顺序(false)或倒序查询(true)。')
284
+ @click.option("--keywords", default=None, callback=_validate_keywords, help='支持关键词精确搜索。关键词指相邻两个分词之间的单词。')
285
+ @click.option("--limit", type=click.IntRange(1, 5000), callback=_validate_limit, help='每次查询的日志条数。最小值:1,最大值:5000。')
286
+ @click.option("--line-num", default=None, callback=_validate_line_num, help='日志单行序列号,标识日志上报顺序,通常用于分页查询和日志数据的有序处理。分页查询需要使用该参数,用于从上次查询结束的问题继续查询。该参数从上次查询的返回结果中获取。')
287
+ @click.option("--start-time", type=click.IntRange(0, 32503680000000), required=True, callback=_validate_start_time, help='搜索日志的起始时间。')
288
+ @click.option("--dry-run", is_flag=True)
289
+ @click.pass_context
290
+ def list_infer_service_logs(ctx, service_id, start_time, end_time, limit, is_desc,
291
+ line_num, is_count, keywords, highlight, dry_run):
292
+ """查询推理服务日志"""
293
+ req = {"start_time": start_time, "end_time": end_time}
294
+ if limit is not None:
295
+ req["limit"] = limit
296
+ if is_desc is not None:
297
+ req["is_desc"] = True
298
+ if line_num:
299
+ req["line_num"] = line_num
300
+ if is_count is not None:
301
+ req["is_count"] = True
302
+ if keywords:
303
+ req["keywords"] = keywords
304
+ if highlight is not None:
305
+ req["highlight"] = True
306
+ if dry_run:
307
+ click.echo(
308
+ f"[DRY-RUN] list_infer_service_logs(service_id={service_id}, "
309
+ f"{json.dumps(req, ensure_ascii=False)})"
310
+ )
311
+ return
312
+ client = get_client(ctx, InferClient)
313
+ result = client.list_infer_service_logs(service_id, req)
314
+ out(result)
315
+
316
+
317
+ @infer.command("wait-deploy")
318
+ @click.option("--service-id", required=True, callback=_validate_service_id, help='推理服务唯一标识ID')
319
+ @click.option("--timeout", type=click.IntRange(1, 3600), default=600, help="部署等待超时时间(秒),默认600")
320
+ @click.pass_context
321
+ def wait_deploy(ctx, service_id, timeout):
322
+ """等待推理服务部署完成"""
323
+ client = get_client(ctx, InferClient)
324
+ try:
325
+ result = client.wait_deploy(service_id, timeout=timeout)
326
+ except RuntimeError as e:
327
+ click.echo(json.dumps({"error": str(e)}, ensure_ascii=False))
328
+ raise click.ClickException(str(e))
329
+ out(result)
@@ -0,0 +1,59 @@
1
+ import time
2
+
3
+ from cloudrobo_core.sdk import BaseClient
4
+ from cloudrobo_core.sdk.exceptions import validate_safe_id
5
+
6
+ from .validators import validate_params
7
+
8
+ TERMINAL_STATES = {"FAILED", "RUNNING", "STOPPING", "STOPPED", "DELETING", "ERROR"}
9
+
10
+
11
+ class InferClient(BaseClient):
12
+ SERVICE = "cloudrobo-service"
13
+
14
+ @validate_params("create_infer_service")
15
+ def create_infer_service(self, req: dict) -> dict:
16
+ return self._client.post(self._url("/v1/infer-services"), json=req)
17
+
18
+ def list_infer_services(self, **params) -> dict:
19
+ return self._client.get(self._url("/v1/infer-services"), params=params)
20
+
21
+ def show_infer_service(self, service_id: str) -> dict:
22
+ validate_safe_id(service_id, "service_id")
23
+ return self._client.get(self._url(f"/v1/infer-services/{service_id}"))
24
+
25
+ @validate_params("update_infer_service")
26
+ def update_infer_service(self, service_id: str, req: dict) -> dict:
27
+ validate_safe_id(service_id, "service_id")
28
+ return self._client.put(self._url(f"/v1/infer-services/{service_id}"), json=req)
29
+
30
+ def delete_infer_service(self, service_id: str) -> dict:
31
+ validate_safe_id(service_id, "service_id")
32
+ return self._client.delete(self._url(f"/v1/infer-services/{service_id}"))
33
+
34
+ def start_infer_service(self, service_id: str) -> dict:
35
+ validate_safe_id(service_id, "service_id")
36
+ return self._client.post(self._url(f"/v1/infer-services/{service_id}/start"))
37
+
38
+ def stop_infer_service(self, service_id: str) -> dict:
39
+ validate_safe_id(service_id, "service_id")
40
+ return self._client.post(self._url(f"/v1/infer-services/{service_id}/stop"))
41
+
42
+ @validate_params("list_infer_service_logs")
43
+ def list_infer_service_logs(self, service_id: str, req: dict) -> dict:
44
+ validate_safe_id(service_id, "service_id")
45
+ return self._client.post(self._url(f"/v1/infer-services/{service_id}/logs"), json=req)
46
+
47
+ def wait_deploy(self, service_id: str, timeout: int = 600) -> dict:
48
+ validate_safe_id(service_id, "service_id")
49
+ elapsed = 0
50
+ while elapsed < timeout:
51
+ service = self.show_infer_service(service_id)
52
+ if service.get("status") != "DEPLOYING":
53
+ return service
54
+ time.sleep(5)
55
+ elapsed += 5
56
+ last = self.show_infer_service(service_id)
57
+ raise RuntimeError(
58
+ f"wait-deploy timeout after {timeout}s, last status: {last.get('status')}"
59
+ )
@@ -0,0 +1,3 @@
1
+ from .validator import InferValidator, validate_params
2
+
3
+ __all__ = ["InferValidator", "validate_params"]