compshare-cli 0.3.3__tar.gz → 0.3.5__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.
- compshare_cli-0.3.5/PKG-INFO +109 -0
- compshare_cli-0.3.5/README.md +84 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/pyproject.toml +4 -1
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/__init__.py +1 -1
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/api.py +28 -13
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/cli.py +202 -22
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/commands/doctor.py +5 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/commands/image.py +9 -1
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/commands/instance.py +630 -2
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/commands/storage.py +1 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/commands/team.py +25 -3
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/config.py +46 -2
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/i18n.py +119 -7
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/location.py +2 -8
- compshare_cli-0.3.5/src/compshare_cli/output.py +362 -0
- compshare_cli-0.3.5/src/compshare_cli/remote_jobs.py +670 -0
- compshare_cli-0.3.5/src/compshare_cli.egg-info/PKG-INFO +109 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli.egg-info/SOURCES.txt +3 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_api.py +42 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_cli.py +789 -94
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_config.py +29 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_feedback.py +5 -1
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_location.py +1 -3
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_output.py +50 -2
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_qa.py +7 -2
- compshare_cli-0.3.5/tests/test_release_version.py +19 -0
- compshare_cli-0.3.5/tests/test_remote_jobs.py +230 -0
- compshare_cli-0.3.3/PKG-INFO +0 -312
- compshare_cli-0.3.3/README.md +0 -287
- compshare_cli-0.3.3/src/compshare_cli/output.py +0 -190
- compshare_cli-0.3.3/src/compshare_cli.egg-info/PKG-INFO +0 -312
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/LICENSE +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/setup.cfg +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/__main__.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/actions.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/commands/__init__.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/commands/ask.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/commands/common.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/commands/feedback.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/errors.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/insights.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/instance_templates.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/parsing.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/qa.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/runtime.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/sdk.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/ssh.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli/ssh_cache.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli.egg-info/dependency_links.txt +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli.egg-info/entry_points.txt +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli.egg-info/requires.txt +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/src/compshare_cli.egg-info/top_level.txt +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_actions.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_instance_templates.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_parsing.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_sdk.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_ssh.py +0 -0
- {compshare_cli-0.3.3 → compshare_cli-0.3.5}/tests/test_ssh_cache.py +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: compshare-cli
|
|
3
|
+
Version: 0.3.5
|
|
4
|
+
Summary: Command-line client for CompShare GPU compute
|
|
5
|
+
Author: CompShare
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/BennielAllan/compshare-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/BennielAllan/compshare-cli
|
|
9
|
+
Project-URL: Changelog, https://github.com/BennielAllan/compshare-cli/blob/master/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/BennielAllan/compshare-cli/issues
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: click<9,>=8.1
|
|
15
|
+
Requires-Dist: pexpect<5,>=4.9; platform_system != "Windows"
|
|
16
|
+
Requires-Dist: rich<15,>=13
|
|
17
|
+
Requires-Dist: typer<1,>=0.12
|
|
18
|
+
Requires-Dist: ucloud-sdk-python3<0.12,>=0.11.103
|
|
19
|
+
Requires-Dist: urllib3<2,>=1.26.18; python_version < "3.10" and platform_system == "Darwin"
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest<10,>=8; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-cov<8,>=5; extra == "dev"
|
|
23
|
+
Requires-Dist: ruff<1,>=0.6; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# CompShare CLI
|
|
27
|
+
|
|
28
|
+
在终端管理优云智算 GPU 实例、镜像、云盘、US3 挂载和团队资源。
|
|
29
|
+
|
|
30
|
+
- 交互式创建实例,实时查询规格、库存和价格
|
|
31
|
+
- 支持批量生命周期操作、等待状态和跨地域查询
|
|
32
|
+
- 支持可断线恢复的远程任务、状态查询和增量日志读取
|
|
33
|
+
- 支持社区/自定义镜像、云盘、团队额度与账单
|
|
34
|
+
- 默认中文帮助,可切换英文;所有命令支持 JSON 输出
|
|
35
|
+
- 可通过 `compshare ask` 查询产品使用和计费知识
|
|
36
|
+
- 可通过 `compshare feedback` 反馈 CLI 问题和建议
|
|
37
|
+
|
|
38
|
+
## 安装
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install compshare-cli
|
|
42
|
+
pip install --upgrade compshare-cli
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 配置
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
compshare config --name default
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
凭证保存在 `~/.config/compshare/config.json`,支持多个配置:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
compshare config list
|
|
55
|
+
compshare config use production
|
|
56
|
+
compshare --profile production instance list
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
也可以使用环境变量:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
export COMPSHARE_PUBLIC_KEY='...'
|
|
63
|
+
export COMPSHARE_PRIVATE_KEY='...'
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 快速开始
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# 查询、创建和查看实例
|
|
70
|
+
compshare instance search --region cn-sh2 --zone cn-sh2-02
|
|
71
|
+
compshare instance create
|
|
72
|
+
compshare instance list --all
|
|
73
|
+
|
|
74
|
+
# 连接实例
|
|
75
|
+
compshare instance ssh INSTANCE_ID
|
|
76
|
+
|
|
77
|
+
# 查看帮助
|
|
78
|
+
compshare -h
|
|
79
|
+
compshare instance -h
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 命令概览
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
compshare config API 凭证配置
|
|
86
|
+
compshare feedback 反馈 CLI 问题或建议
|
|
87
|
+
compshare doctor 配置、鉴权、网络与 SSH 环境诊断
|
|
88
|
+
compshare ask 产品使用和计费问答
|
|
89
|
+
compshare instance GPU 实例、规格、库存、价格和生命周期
|
|
90
|
+
compshare image 平台、自定义、社区及共享镜像
|
|
91
|
+
compshare storage 云盘和 US3 挂载
|
|
92
|
+
compshare team 团队、邀请、成员额度、账单和审计
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Skill
|
|
96
|
+
|
|
97
|
+
为 Codex、Claude Code、Cursor 等 AI Agent 安装 CompShare CLI Skill:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npx skills add compshare-cn/compshare-cli --skill compshare-cli
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
完整说明见 [CompShare CLI Skill](skills/compshare-cli/SKILL.md)。
|
|
104
|
+
|
|
105
|
+
## SDK
|
|
106
|
+
|
|
107
|
+
如需通过 SDK 集成 CompShare API,可参考独立的
|
|
108
|
+
[CompShare Developer Examples](https://github.com/ucloud/compshare-developer-examples) 仓库。
|
|
109
|
+
其中包含 Python、Go、Java、JavaScript 和 PHP 示例,覆盖 API 鉴权、GPU 实例创建、规格配置和状态查询。
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# CompShare CLI
|
|
2
|
+
|
|
3
|
+
在终端管理优云智算 GPU 实例、镜像、云盘、US3 挂载和团队资源。
|
|
4
|
+
|
|
5
|
+
- 交互式创建实例,实时查询规格、库存和价格
|
|
6
|
+
- 支持批量生命周期操作、等待状态和跨地域查询
|
|
7
|
+
- 支持可断线恢复的远程任务、状态查询和增量日志读取
|
|
8
|
+
- 支持社区/自定义镜像、云盘、团队额度与账单
|
|
9
|
+
- 默认中文帮助,可切换英文;所有命令支持 JSON 输出
|
|
10
|
+
- 可通过 `compshare ask` 查询产品使用和计费知识
|
|
11
|
+
- 可通过 `compshare feedback` 反馈 CLI 问题和建议
|
|
12
|
+
|
|
13
|
+
## 安装
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install compshare-cli
|
|
17
|
+
pip install --upgrade compshare-cli
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 配置
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
compshare config --name default
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
凭证保存在 `~/.config/compshare/config.json`,支持多个配置:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
compshare config list
|
|
30
|
+
compshare config use production
|
|
31
|
+
compshare --profile production instance list
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
也可以使用环境变量:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
export COMPSHARE_PUBLIC_KEY='...'
|
|
38
|
+
export COMPSHARE_PRIVATE_KEY='...'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 快速开始
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# 查询、创建和查看实例
|
|
45
|
+
compshare instance search --region cn-sh2 --zone cn-sh2-02
|
|
46
|
+
compshare instance create
|
|
47
|
+
compshare instance list --all
|
|
48
|
+
|
|
49
|
+
# 连接实例
|
|
50
|
+
compshare instance ssh INSTANCE_ID
|
|
51
|
+
|
|
52
|
+
# 查看帮助
|
|
53
|
+
compshare -h
|
|
54
|
+
compshare instance -h
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 命令概览
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
compshare config API 凭证配置
|
|
61
|
+
compshare feedback 反馈 CLI 问题或建议
|
|
62
|
+
compshare doctor 配置、鉴权、网络与 SSH 环境诊断
|
|
63
|
+
compshare ask 产品使用和计费问答
|
|
64
|
+
compshare instance GPU 实例、规格、库存、价格和生命周期
|
|
65
|
+
compshare image 平台、自定义、社区及共享镜像
|
|
66
|
+
compshare storage 云盘和 US3 挂载
|
|
67
|
+
compshare team 团队、邀请、成员额度、账单和审计
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Skill
|
|
71
|
+
|
|
72
|
+
为 Codex、Claude Code、Cursor 等 AI Agent 安装 CompShare CLI Skill:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npx skills add compshare-cn/compshare-cli --skill compshare-cli
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
完整说明见 [CompShare CLI Skill](skills/compshare-cli/SKILL.md)。
|
|
79
|
+
|
|
80
|
+
## SDK
|
|
81
|
+
|
|
82
|
+
如需通过 SDK 集成 CompShare API,可参考独立的
|
|
83
|
+
[CompShare Developer Examples](https://github.com/ucloud/compshare-developer-examples) 仓库。
|
|
84
|
+
其中包含 Python、Go、Java、JavaScript 和 PHP 示例,覆盖 API 鉴权、GPU 实例创建、规格配置和状态查询。
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "compshare-cli"
|
|
7
|
-
|
|
7
|
+
dynamic = ["version"]
|
|
8
8
|
description = "Command-line client for CompShare GPU compute"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -39,6 +39,9 @@ dev = [
|
|
|
39
39
|
compshare = "compshare_cli.cli:main"
|
|
40
40
|
compshare-ssh-askpass = "compshare_cli.ssh:askpass"
|
|
41
41
|
|
|
42
|
+
[tool.setuptools.dynamic]
|
|
43
|
+
version = { attr = "compshare_cli.__version__" }
|
|
44
|
+
|
|
42
45
|
[tool.setuptools.packages.find]
|
|
43
46
|
where = ["src"]
|
|
44
47
|
|
|
@@ -52,11 +52,12 @@ def invoke(
|
|
|
52
52
|
continue
|
|
53
53
|
raise
|
|
54
54
|
except CLIError as error:
|
|
55
|
-
renderer.
|
|
55
|
+
renderer.coded_error("configuration_error", str(error))
|
|
56
56
|
raise typer.Exit(1) from error
|
|
57
57
|
except ucloud_exc.RetCodeException as error:
|
|
58
58
|
hint = error_hint(error.action or action, error.code)
|
|
59
|
-
renderer.
|
|
59
|
+
renderer.coded_error(
|
|
60
|
+
"api_error",
|
|
60
61
|
_with_hint(error.message or str(error), hint),
|
|
61
62
|
details={
|
|
62
63
|
"action": error.action,
|
|
@@ -67,10 +68,10 @@ def invoke(
|
|
|
67
68
|
)
|
|
68
69
|
raise typer.Exit(1) from error
|
|
69
70
|
except ucloud_exc.UCloudException as error:
|
|
70
|
-
renderer.
|
|
71
|
+
renderer.coded_error("transport_error", str(error))
|
|
71
72
|
raise typer.Exit(1) from error
|
|
72
73
|
except Exception as error: # SDK transport exceptions are not all UCloudException subclasses.
|
|
73
|
-
renderer.
|
|
74
|
+
renderer.coded_error("transport_error", str(error))
|
|
74
75
|
raise typer.Exit(1) from error
|
|
75
76
|
|
|
76
77
|
if success:
|
|
@@ -79,7 +80,7 @@ def invoke(
|
|
|
79
80
|
renderer.data(response)
|
|
80
81
|
else:
|
|
81
82
|
rows = row_builder(response) if row_builder else response.get(list_key or "") or []
|
|
82
|
-
renderer.data(response, rows=rows, columns=columns)
|
|
83
|
+
renderer.data(response, rows=rows, columns=columns, json_list=True)
|
|
83
84
|
return response
|
|
84
85
|
|
|
85
86
|
|
|
@@ -88,11 +89,14 @@ def call(runtime: Runtime, action: str, params: Dict[str, Any]) -> Dict[str, Any
|
|
|
88
89
|
try:
|
|
89
90
|
return _sdk(runtime, params).invoke(action, params)
|
|
90
91
|
except CLIError as error:
|
|
91
|
-
Renderer(runtime.json_output, runtime.show_sensitive).
|
|
92
|
+
Renderer(runtime.json_output, runtime.show_sensitive).coded_error(
|
|
93
|
+
"configuration_error", str(error)
|
|
94
|
+
)
|
|
92
95
|
raise typer.Exit(1) from error
|
|
93
96
|
except ucloud_exc.RetCodeException as error:
|
|
94
97
|
hint = error_hint(error.action or action, error.code)
|
|
95
|
-
Renderer(runtime.json_output, runtime.show_sensitive).
|
|
98
|
+
Renderer(runtime.json_output, runtime.show_sensitive).coded_error(
|
|
99
|
+
"api_error",
|
|
96
100
|
_with_hint(error.message or str(error), hint),
|
|
97
101
|
details={
|
|
98
102
|
"action": error.action,
|
|
@@ -103,10 +107,14 @@ def call(runtime: Runtime, action: str, params: Dict[str, Any]) -> Dict[str, Any
|
|
|
103
107
|
)
|
|
104
108
|
raise typer.Exit(1) from error
|
|
105
109
|
except ucloud_exc.UCloudException as error:
|
|
106
|
-
Renderer(runtime.json_output, runtime.show_sensitive).
|
|
110
|
+
Renderer(runtime.json_output, runtime.show_sensitive).coded_error(
|
|
111
|
+
"transport_error", str(error)
|
|
112
|
+
)
|
|
107
113
|
raise typer.Exit(1) from error
|
|
108
114
|
except Exception as error:
|
|
109
|
-
Renderer(runtime.json_output, runtime.show_sensitive).
|
|
115
|
+
Renderer(runtime.json_output, runtime.show_sensitive).coded_error(
|
|
116
|
+
"transport_error", str(error)
|
|
117
|
+
)
|
|
110
118
|
raise typer.Exit(1) from error
|
|
111
119
|
|
|
112
120
|
|
|
@@ -195,11 +203,14 @@ def download_file(
|
|
|
195
203
|
try:
|
|
196
204
|
return _sdk(runtime, params).download(action, params)
|
|
197
205
|
except CLIError as error:
|
|
198
|
-
Renderer(runtime.json_output, runtime.show_sensitive).
|
|
206
|
+
Renderer(runtime.json_output, runtime.show_sensitive).coded_error(
|
|
207
|
+
"configuration_error", str(error)
|
|
208
|
+
)
|
|
199
209
|
raise typer.Exit(1) from error
|
|
200
210
|
except ucloud_exc.RetCodeException as error:
|
|
201
211
|
hint = error_hint(error.action or action, error.code)
|
|
202
|
-
Renderer(runtime.json_output, runtime.show_sensitive).
|
|
212
|
+
Renderer(runtime.json_output, runtime.show_sensitive).coded_error(
|
|
213
|
+
"api_error",
|
|
203
214
|
_with_hint(error.message or str(error), hint),
|
|
204
215
|
details={
|
|
205
216
|
"action": error.action or action,
|
|
@@ -210,10 +221,14 @@ def download_file(
|
|
|
210
221
|
)
|
|
211
222
|
raise typer.Exit(1) from error
|
|
212
223
|
except ucloud_exc.UCloudException as error:
|
|
213
|
-
Renderer(runtime.json_output, runtime.show_sensitive).
|
|
224
|
+
Renderer(runtime.json_output, runtime.show_sensitive).coded_error(
|
|
225
|
+
"transport_error", str(error)
|
|
226
|
+
)
|
|
214
227
|
raise typer.Exit(1) from error
|
|
215
228
|
except Exception as error:
|
|
216
|
-
Renderer(runtime.json_output, runtime.show_sensitive).
|
|
229
|
+
Renderer(runtime.json_output, runtime.show_sensitive).coded_error(
|
|
230
|
+
"transport_error", str(error)
|
|
231
|
+
)
|
|
217
232
|
raise typer.Exit(1) from error
|
|
218
233
|
|
|
219
234
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
|
-
from typing import List, Optional
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
5
|
|
|
6
6
|
import click
|
|
7
7
|
import typer
|
|
8
8
|
from typer import core as typer_core
|
|
9
|
+
from typer.completion import completion_init, install_callback
|
|
9
10
|
from typer.main import get_command
|
|
10
11
|
|
|
11
12
|
from compshare_cli import __version__
|
|
@@ -15,7 +16,7 @@ from compshare_cli.commands import feedback as feedback_command
|
|
|
15
16
|
from compshare_cli.commands import image, instance, storage, team
|
|
16
17
|
from compshare_cli.commands.common import confirm
|
|
17
18
|
from compshare_cli.config import DEFAULT_PROFILE, ConfigStore, Profile
|
|
18
|
-
from compshare_cli.errors import CLIError, UsageError
|
|
19
|
+
from compshare_cli.errors import CLIError, ConfigError, UsageError
|
|
19
20
|
from compshare_cli.i18n import configured_language, localize_command, normalize_language, tr
|
|
20
21
|
from compshare_cli.insights import record_command
|
|
21
22
|
from compshare_cli.output import Renderer
|
|
@@ -26,6 +27,8 @@ _TYPER_CLICK_EXCEPTIONS = getattr(_TYPER_CLICK, "exceptions", _TYPER_CLICK)
|
|
|
26
27
|
_CLICK_EXCEPTIONS = (click.ClickException, _TYPER_CLICK_EXCEPTIONS.ClickException)
|
|
27
28
|
_ABORT_EXCEPTIONS = (click.Abort, _TYPER_CLICK_EXCEPTIONS.Abort)
|
|
28
29
|
|
|
30
|
+
completion_init()
|
|
31
|
+
|
|
29
32
|
|
|
30
33
|
class RootGroup(typer_core.TyperGroup):
|
|
31
34
|
def list_commands(self, ctx):
|
|
@@ -42,7 +45,7 @@ app = typer.Typer(
|
|
|
42
45
|
help="Manage CompShare GPU compute from the terminal.",
|
|
43
46
|
cls=RootGroup,
|
|
44
47
|
no_args_is_help=True,
|
|
45
|
-
add_completion=
|
|
48
|
+
add_completion=False,
|
|
46
49
|
context_settings={"help_option_names": ["-h", "--help"]},
|
|
47
50
|
)
|
|
48
51
|
app.add_typer(instance.app, name="instance")
|
|
@@ -57,9 +60,12 @@ config_app = typer.Typer(
|
|
|
57
60
|
app.add_typer(config_app, name="config")
|
|
58
61
|
|
|
59
62
|
|
|
60
|
-
def _version_callback(value: bool) -> None:
|
|
63
|
+
def _version_callback(ctx: typer.Context, value: bool) -> None:
|
|
61
64
|
if value:
|
|
62
|
-
|
|
65
|
+
if bool(ctx.params.get("json_output")):
|
|
66
|
+
Renderer(True, bool(ctx.params.get("show_sensitive"))).data({"version": __version__})
|
|
67
|
+
else:
|
|
68
|
+
typer.echo(__version__)
|
|
63
69
|
raise typer.Exit()
|
|
64
70
|
|
|
65
71
|
|
|
@@ -89,11 +95,13 @@ def root(
|
|
|
89
95
|
json_output: bool = typer.Option(
|
|
90
96
|
False,
|
|
91
97
|
"--json",
|
|
98
|
+
is_eager=True,
|
|
92
99
|
help="Emit machine-readable JSON.",
|
|
93
100
|
),
|
|
94
101
|
show_sensitive: bool = typer.Option(
|
|
95
102
|
False,
|
|
96
103
|
"--show-sensitive",
|
|
104
|
+
is_eager=True,
|
|
97
105
|
help="Show passwords, IP addresses, access URLs, and login commands.",
|
|
98
106
|
),
|
|
99
107
|
version_option: bool = typer.Option(
|
|
@@ -103,6 +111,13 @@ def root(
|
|
|
103
111
|
is_eager=True,
|
|
104
112
|
help="Print the CLI version and exit.",
|
|
105
113
|
),
|
|
114
|
+
install_completion: bool = typer.Option(
|
|
115
|
+
None,
|
|
116
|
+
"--install-completion",
|
|
117
|
+
callback=install_callback,
|
|
118
|
+
expose_value=False,
|
|
119
|
+
help="Install completion for the current shell.",
|
|
120
|
+
),
|
|
106
121
|
) -> None:
|
|
107
122
|
"""CompShare CLI."""
|
|
108
123
|
ctx.obj = Runtime(
|
|
@@ -182,13 +197,54 @@ def config_set(
|
|
|
182
197
|
def config_list(ctx: typer.Context) -> None:
|
|
183
198
|
"""List credential profiles."""
|
|
184
199
|
store = ConfigStore()
|
|
185
|
-
current = store.current_profile()
|
|
186
|
-
profiles = [{"Profile": name, "Active": name == current} for name in store.list_profiles()]
|
|
187
200
|
state = ctx.find_root().obj
|
|
201
|
+
status = store.credential_status(state.profile_name)
|
|
202
|
+
source = status["credential_source"]
|
|
203
|
+
selected = status["selected_profile"]
|
|
204
|
+
active_profile = selected if source in {"profile", "mixed"} else None
|
|
205
|
+
profiles = [
|
|
206
|
+
{
|
|
207
|
+
"Profile": name,
|
|
208
|
+
"Active": name == active_profile,
|
|
209
|
+
"Source": "mixed" if name == active_profile and source == "mixed" else "profile",
|
|
210
|
+
}
|
|
211
|
+
for name in store.list_profiles()
|
|
212
|
+
]
|
|
213
|
+
rows = [
|
|
214
|
+
{
|
|
215
|
+
**profile,
|
|
216
|
+
"Source": tr("Profile and environment")
|
|
217
|
+
if profile["Source"] == "mixed"
|
|
218
|
+
else tr("Profile file"),
|
|
219
|
+
}
|
|
220
|
+
for profile in profiles
|
|
221
|
+
]
|
|
222
|
+
if source == "environment":
|
|
223
|
+
rows.insert(
|
|
224
|
+
0,
|
|
225
|
+
{"Profile": "-", "Active": True, "Source": tr("Environment variables")},
|
|
226
|
+
)
|
|
227
|
+
elif source in {"unconfigured", "incomplete"} and not any(
|
|
228
|
+
profile["Active"] for profile in profiles
|
|
229
|
+
):
|
|
230
|
+
rows.insert(
|
|
231
|
+
0,
|
|
232
|
+
{
|
|
233
|
+
"Profile": selected,
|
|
234
|
+
"Active": False,
|
|
235
|
+
"Source": tr("Incomplete credentials")
|
|
236
|
+
if source == "incomplete"
|
|
237
|
+
else tr("Not configured"),
|
|
238
|
+
},
|
|
239
|
+
)
|
|
188
240
|
Renderer(state.json_output, state.show_sensitive).data(
|
|
189
|
-
{
|
|
190
|
-
|
|
191
|
-
|
|
241
|
+
{
|
|
242
|
+
**status,
|
|
243
|
+
"current_profile": active_profile,
|
|
244
|
+
"profiles": profiles,
|
|
245
|
+
},
|
|
246
|
+
rows=rows,
|
|
247
|
+
columns=(("Profile", "PROFILE"), ("Active", "ACTIVE"), ("Source", "SOURCE")),
|
|
192
248
|
)
|
|
193
249
|
|
|
194
250
|
|
|
@@ -270,6 +326,53 @@ def feedback(
|
|
|
270
326
|
feedback_command.run(ctx.find_root().obj, category, message)
|
|
271
327
|
|
|
272
328
|
|
|
329
|
+
def _global_flag_requested(argv: List[str], option: str) -> bool:
|
|
330
|
+
for token in argv:
|
|
331
|
+
if token == "--":
|
|
332
|
+
break
|
|
333
|
+
if token == option:
|
|
334
|
+
return True
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _normalize_global_options(argv: List[str]) -> List[str]:
|
|
339
|
+
"""Move unambiguous root options before the command without crossing --."""
|
|
340
|
+
global_options: List[str] = []
|
|
341
|
+
remaining: List[str] = []
|
|
342
|
+
profile_seen = False
|
|
343
|
+
index = 0
|
|
344
|
+
while index < len(argv):
|
|
345
|
+
token = argv[index]
|
|
346
|
+
if token == "--":
|
|
347
|
+
remaining.extend(argv[index:])
|
|
348
|
+
break
|
|
349
|
+
if token in {"--json", "--show-sensitive"}:
|
|
350
|
+
global_options.append(token)
|
|
351
|
+
index += 1
|
|
352
|
+
continue
|
|
353
|
+
if token == "--profile":
|
|
354
|
+
if profile_seen:
|
|
355
|
+
raise UsageError(tr("Option --profile may only be specified once."))
|
|
356
|
+
profile_seen = True
|
|
357
|
+
if index + 1 >= len(argv) or argv[index + 1].startswith("-"):
|
|
358
|
+
raise UsageError(tr("Option --profile requires a value."))
|
|
359
|
+
global_options.extend((token, argv[index + 1]))
|
|
360
|
+
index += 2
|
|
361
|
+
continue
|
|
362
|
+
if token.startswith("--profile="):
|
|
363
|
+
if profile_seen:
|
|
364
|
+
raise UsageError(tr("Option --profile may only be specified once."))
|
|
365
|
+
profile_seen = True
|
|
366
|
+
if not token.partition("=")[2]:
|
|
367
|
+
raise UsageError(tr("Option --profile requires a value."))
|
|
368
|
+
global_options.append(token)
|
|
369
|
+
index += 1
|
|
370
|
+
continue
|
|
371
|
+
remaining.append(token)
|
|
372
|
+
index += 1
|
|
373
|
+
return [*global_options, *remaining]
|
|
374
|
+
|
|
375
|
+
|
|
273
376
|
def _command_path(command: click.Command, argv: List[str]) -> Optional[str]:
|
|
274
377
|
if not argv or "-h" in argv or "--help" in argv:
|
|
275
378
|
return None
|
|
@@ -298,6 +401,77 @@ def _command_path(command: click.Command, argv: List[str]) -> Optional[str]:
|
|
|
298
401
|
return ".".join(path) or None
|
|
299
402
|
|
|
300
403
|
|
|
404
|
+
def _json_help_requested(argv: List[str]) -> bool:
|
|
405
|
+
for token in argv:
|
|
406
|
+
if token == "--":
|
|
407
|
+
return False
|
|
408
|
+
if token in {"-h", "--help"}:
|
|
409
|
+
return True
|
|
410
|
+
return False
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _json_help_payload(command: click.Command, argv: List[str]) -> Dict[str, Any]:
|
|
414
|
+
"""Build machine-readable help without invoking Rich's terminal renderer."""
|
|
415
|
+
current = command
|
|
416
|
+
path: List[str] = []
|
|
417
|
+
skip_next = False
|
|
418
|
+
for token in argv:
|
|
419
|
+
if skip_next:
|
|
420
|
+
skip_next = False
|
|
421
|
+
continue
|
|
422
|
+
if token in {"-h", "--help", "--"}:
|
|
423
|
+
break
|
|
424
|
+
if token in {"--profile", "--lang"}:
|
|
425
|
+
skip_next = True
|
|
426
|
+
continue
|
|
427
|
+
if token.startswith(("--profile=", "--lang=")) or token in {
|
|
428
|
+
"--json",
|
|
429
|
+
"--show-sensitive",
|
|
430
|
+
}:
|
|
431
|
+
continue
|
|
432
|
+
if isinstance(current, click.Group) and token in current.commands:
|
|
433
|
+
current = current.commands[token]
|
|
434
|
+
path.append(token)
|
|
435
|
+
|
|
436
|
+
parameters: List[Dict[str, Any]] = []
|
|
437
|
+
for parameter in current.params:
|
|
438
|
+
if isinstance(parameter, click.Option):
|
|
439
|
+
parameters.append(
|
|
440
|
+
{
|
|
441
|
+
"kind": "option",
|
|
442
|
+
"name": parameter.name,
|
|
443
|
+
"flags": [*parameter.opts, *parameter.secondary_opts],
|
|
444
|
+
"required": parameter.required,
|
|
445
|
+
"multiple": parameter.multiple,
|
|
446
|
+
"help": parameter.help,
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
elif isinstance(parameter, click.Argument):
|
|
450
|
+
parameters.append(
|
|
451
|
+
{
|
|
452
|
+
"kind": "argument",
|
|
453
|
+
"name": parameter.name,
|
|
454
|
+
"required": parameter.required,
|
|
455
|
+
"multiple": parameter.nargs != 1,
|
|
456
|
+
}
|
|
457
|
+
)
|
|
458
|
+
commands = []
|
|
459
|
+
if isinstance(current, click.Group):
|
|
460
|
+
commands = [
|
|
461
|
+
{"name": name, "help": child.get_short_help_str()}
|
|
462
|
+
for name in current.list_commands(click.Context(current))
|
|
463
|
+
if (child := current.get_command(click.Context(current), name)) is not None
|
|
464
|
+
and not child.hidden
|
|
465
|
+
]
|
|
466
|
+
command_name = " ".join(["compshare", *path])
|
|
467
|
+
return {
|
|
468
|
+
"command_path": command_name,
|
|
469
|
+
"help": current.help,
|
|
470
|
+
"parameters": parameters,
|
|
471
|
+
"commands": commands,
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
|
|
301
475
|
def _requested_language(argv: List[str]) -> Optional[str]:
|
|
302
476
|
"""Return the last root --lang value before the command, if present."""
|
|
303
477
|
requested: Optional[str] = None
|
|
@@ -338,8 +512,10 @@ def main(args: Optional[List[str]] = None) -> None:
|
|
|
338
512
|
if not argv:
|
|
339
513
|
argv = ["-h"]
|
|
340
514
|
telemetry_command: Optional[str] = None
|
|
341
|
-
|
|
515
|
+
json_output = _global_flag_requested(argv, "--json")
|
|
516
|
+
show_sensitive = _global_flag_requested(argv, "--show-sensitive")
|
|
342
517
|
try:
|
|
518
|
+
argv = _normalize_global_options(argv)
|
|
343
519
|
requested_language = _requested_language(argv)
|
|
344
520
|
if requested_language is None:
|
|
345
521
|
language = configured_language()
|
|
@@ -347,23 +523,27 @@ def main(args: Optional[List[str]] = None) -> None:
|
|
|
347
523
|
language = normalize_language(requested_language)
|
|
348
524
|
ConfigStore().save_language(language)
|
|
349
525
|
command = localize_command(get_command(app), language)
|
|
350
|
-
telemetry_command = _command_path(command,
|
|
351
|
-
|
|
526
|
+
telemetry_command = _command_path(command, argv)
|
|
527
|
+
if json_output and _json_help_requested(argv):
|
|
528
|
+
Renderer(True, show_sensitive).data(_json_help_payload(command, argv))
|
|
529
|
+
result = None
|
|
530
|
+
else:
|
|
531
|
+
result = command.main(args=argv, prog_name="compshare", standalone_mode=False)
|
|
532
|
+
except ConfigError as error:
|
|
533
|
+
Renderer(json_output, show_sensitive).coded_error("configuration_error", str(error))
|
|
534
|
+
raise SystemExit(2) from error
|
|
535
|
+
except UsageError as error:
|
|
536
|
+
Renderer(json_output, show_sensitive).coded_error("invalid_usage", str(error))
|
|
537
|
+
raise SystemExit(2) from error
|
|
352
538
|
except CLIError as error:
|
|
353
|
-
Renderer(
|
|
539
|
+
Renderer(json_output, show_sensitive).coded_error("cli_error", str(error))
|
|
354
540
|
raise SystemExit(2) from error
|
|
355
541
|
except _CLICK_EXCEPTIONS as error:
|
|
356
542
|
message = error.format_message()
|
|
357
|
-
|
|
358
|
-
hint = tr(
|
|
359
|
-
"Global option --json must appear before the command, for example: "
|
|
360
|
-
"compshare --json instance list."
|
|
361
|
-
)
|
|
362
|
-
message = f"{message} {hint}"
|
|
363
|
-
Renderer("--json" in argv, show_sensitive).error(message)
|
|
543
|
+
Renderer(json_output, show_sensitive).coded_error("invalid_usage", message)
|
|
364
544
|
raise SystemExit(error.exit_code) from error
|
|
365
545
|
except _ABORT_EXCEPTIONS as error:
|
|
366
|
-
Renderer(
|
|
546
|
+
Renderer(json_output, show_sensitive).coded_error("aborted", tr("Aborted"))
|
|
367
547
|
raise SystemExit(1) from error
|
|
368
548
|
finally:
|
|
369
549
|
if telemetry_command:
|
|
@@ -127,6 +127,11 @@ def run(state: Runtime) -> None:
|
|
|
127
127
|
|
|
128
128
|
ok = not any(item["Status"] == "Failed" for item in checks)
|
|
129
129
|
payload = {"ok": ok, "checks": checks}
|
|
130
|
+
if not ok:
|
|
131
|
+
payload["error"] = {
|
|
132
|
+
"code": "diagnostic_failed",
|
|
133
|
+
"message": tr("One or more diagnostic checks failed."),
|
|
134
|
+
}
|
|
130
135
|
Renderer(state.json_output, state.show_sensitive).data(
|
|
131
136
|
payload,
|
|
132
137
|
rows=checks,
|
|
@@ -250,6 +250,8 @@ def list_images(
|
|
|
250
250
|
response,
|
|
251
251
|
rows=rows,
|
|
252
252
|
columns=IMAGE_COLUMNS,
|
|
253
|
+
json_list=True,
|
|
254
|
+
metadata={"source": source, "all": all_results},
|
|
253
255
|
)
|
|
254
256
|
|
|
255
257
|
|
|
@@ -293,7 +295,13 @@ def show(
|
|
|
293
295
|
("PRICE/H", item.get("Price")),
|
|
294
296
|
("DESCRIPTION", item.get("Description")),
|
|
295
297
|
],
|
|
296
|
-
response=
|
|
298
|
+
response={
|
|
299
|
+
"Action": response.get("Action"),
|
|
300
|
+
"RetCode": response.get("RetCode"),
|
|
301
|
+
"request_uuid": response.get("request_uuid"),
|
|
302
|
+
"source": source,
|
|
303
|
+
"image": item,
|
|
304
|
+
},
|
|
297
305
|
)
|
|
298
306
|
|
|
299
307
|
|