compshare-cli 0.3.5__tar.gz → 0.3.6__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/src/compshare_cli.egg-info → compshare_cli-0.3.6}/PKG-INFO +4 -1
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/README.md +3 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/__init__.py +1 -1
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/cli.py +33 -1
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/commands/feedback.py +1 -1
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/i18n.py +1 -3
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/insights.py +63 -56
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/sdk.py +10 -2
- {compshare_cli-0.3.5 → compshare_cli-0.3.6/src/compshare_cli.egg-info}/PKG-INFO +4 -1
- compshare_cli-0.3.6/tests/test_feedback.py +202 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_sdk.py +9 -0
- compshare_cli-0.3.5/tests/test_feedback.py +0 -159
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/LICENSE +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/pyproject.toml +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/setup.cfg +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/__main__.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/actions.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/api.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/commands/__init__.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/commands/ask.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/commands/common.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/commands/doctor.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/commands/image.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/commands/instance.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/commands/storage.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/commands/team.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/config.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/errors.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/instance_templates.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/location.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/output.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/parsing.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/qa.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/remote_jobs.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/runtime.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/ssh.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/ssh_cache.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli.egg-info/SOURCES.txt +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli.egg-info/dependency_links.txt +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli.egg-info/entry_points.txt +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli.egg-info/requires.txt +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli.egg-info/top_level.txt +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_actions.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_api.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_cli.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_config.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_instance_templates.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_location.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_output.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_parsing.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_qa.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_release_version.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_remote_jobs.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_ssh.py +0 -0
- {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_ssh_cache.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compshare-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
4
4
|
Summary: Command-line client for CompShare GPU compute
|
|
5
5
|
Author: CompShare
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -63,6 +63,9 @@ export COMPSHARE_PUBLIC_KEY='...'
|
|
|
63
63
|
export COMPSHARE_PRIVATE_KEY='...'
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
CLI 使用统计和反馈与业务 API 一样,通过当前 profile 的凭证签名后发送到
|
|
67
|
+
`https://api.compshare.cn/`;开发环境可使用 `COMPSHARE_INSIGHTS_URL` 覆盖网关地址。
|
|
68
|
+
|
|
66
69
|
## 快速开始
|
|
67
70
|
|
|
68
71
|
```bash
|
|
@@ -506,16 +506,45 @@ def _requested_language(argv: List[str]) -> Optional[str]:
|
|
|
506
506
|
return requested
|
|
507
507
|
|
|
508
508
|
|
|
509
|
+
def _requested_profile(argv: List[str]) -> Optional[str]:
|
|
510
|
+
"""Return the root --profile value, if present."""
|
|
511
|
+
index = 0
|
|
512
|
+
while index < len(argv):
|
|
513
|
+
option = argv[index]
|
|
514
|
+
if option == "--profile":
|
|
515
|
+
return argv[index + 1] if index + 1 < len(argv) else None
|
|
516
|
+
if option.startswith("--profile="):
|
|
517
|
+
return option.split("=", 1)[1] or None
|
|
518
|
+
if option in {"--lang"}:
|
|
519
|
+
index += 2
|
|
520
|
+
continue
|
|
521
|
+
if option.startswith("--lang=") or option in {
|
|
522
|
+
"--json",
|
|
523
|
+
"--show-sensitive",
|
|
524
|
+
"--version",
|
|
525
|
+
"-h",
|
|
526
|
+
"--help",
|
|
527
|
+
}:
|
|
528
|
+
index += 1
|
|
529
|
+
continue
|
|
530
|
+
if not option.startswith("-"):
|
|
531
|
+
break
|
|
532
|
+
index += 1
|
|
533
|
+
return None
|
|
534
|
+
|
|
535
|
+
|
|
509
536
|
def main(args: Optional[List[str]] = None) -> None:
|
|
510
537
|
original_argv = list(sys.argv[1:] if args is None else args)
|
|
511
538
|
argv = list(original_argv)
|
|
512
539
|
if not argv:
|
|
513
540
|
argv = ["-h"]
|
|
514
541
|
telemetry_command: Optional[str] = None
|
|
542
|
+
telemetry_profile: Optional[str] = None
|
|
515
543
|
json_output = _global_flag_requested(argv, "--json")
|
|
516
544
|
show_sensitive = _global_flag_requested(argv, "--show-sensitive")
|
|
517
545
|
try:
|
|
518
546
|
argv = _normalize_global_options(argv)
|
|
547
|
+
telemetry_profile = _requested_profile(argv)
|
|
519
548
|
requested_language = _requested_language(argv)
|
|
520
549
|
if requested_language is None:
|
|
521
550
|
language = configured_language()
|
|
@@ -547,7 +576,10 @@ def main(args: Optional[List[str]] = None) -> None:
|
|
|
547
576
|
raise SystemExit(1) from error
|
|
548
577
|
finally:
|
|
549
578
|
if telemetry_command:
|
|
550
|
-
|
|
579
|
+
if telemetry_profile is None:
|
|
580
|
+
record_command(telemetry_command)
|
|
581
|
+
else:
|
|
582
|
+
record_command(telemetry_command, telemetry_profile)
|
|
551
583
|
if isinstance(result, int) and result:
|
|
552
584
|
raise SystemExit(result)
|
|
553
585
|
|
|
@@ -14,7 +14,7 @@ class FeedbackCategory(str, Enum):
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def run(state: Runtime, category: FeedbackCategory, message: str) -> None:
|
|
17
|
-
response = submit_feedback(category.value, message)
|
|
17
|
+
response = submit_feedback(state.profile, category.value, message)
|
|
18
18
|
Renderer(state.json_output, state.show_sensitive).success(
|
|
19
19
|
tr("Thank you for your feedback."), response
|
|
20
20
|
)
|
|
@@ -718,9 +718,7 @@ ZH_TRANSLATIONS: Dict[str, str] = {
|
|
|
718
718
|
"Feedback service is not configured; set {variable}.": (
|
|
719
719
|
"反馈服务尚未配置,请设置 {variable}。"
|
|
720
720
|
),
|
|
721
|
-
"Insights service rejected the request
|
|
722
|
-
"数据服务拒绝了请求,HTTP 状态码为 {status}。"
|
|
723
|
-
),
|
|
721
|
+
"Insights service rejected the request: {detail}": "数据服务拒绝了请求:{detail}",
|
|
724
722
|
"Unable to reach the insights service.": "无法连接数据服务。",
|
|
725
723
|
"Insights service returned an invalid response.": "数据服务返回了无效响应。",
|
|
726
724
|
"Thank you for your feedback.": "感谢反馈。",
|
|
@@ -7,16 +7,21 @@ import platform
|
|
|
7
7
|
import subprocess
|
|
8
8
|
import sys
|
|
9
9
|
from datetime import datetime, timezone
|
|
10
|
-
from typing import Any, Dict
|
|
11
|
-
from urllib import
|
|
12
|
-
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
from urllib import parse
|
|
12
|
+
|
|
13
|
+
from ucloud.core import exc as ucloud_exc
|
|
13
14
|
|
|
14
15
|
from compshare_cli import __version__
|
|
16
|
+
from compshare_cli.config import ConfigStore, Profile
|
|
15
17
|
from compshare_cli.errors import CLIError
|
|
16
18
|
from compshare_cli.i18n import tr
|
|
19
|
+
from compshare_cli.sdk import CompShareSDK
|
|
17
20
|
|
|
18
21
|
ENDPOINT_ENV = "COMPSHARE_INSIGHTS_URL"
|
|
19
|
-
DEFAULT_ENDPOINT = "
|
|
22
|
+
DEFAULT_ENDPOINT = "https://api.compshare.cn"
|
|
23
|
+
EVENT_ACTION = "CreateCSCLIEvent"
|
|
24
|
+
FEEDBACK_ACTION = "CreateCSCLIFeedback"
|
|
20
25
|
|
|
21
26
|
|
|
22
27
|
def now() -> str:
|
|
@@ -28,18 +33,21 @@ def os_name() -> str:
|
|
|
28
33
|
return value or sys.platform.lower()
|
|
29
34
|
|
|
30
35
|
|
|
31
|
-
def record_command(command: str) -> None:
|
|
36
|
+
def record_command(command: str, profile_name: Optional[str] = None) -> None:
|
|
32
37
|
"""Report a command in a detached process without delaying or affecting the CLI."""
|
|
33
38
|
if not _base_url():
|
|
34
39
|
return
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
document = {
|
|
41
|
+
"profile_name": profile_name,
|
|
42
|
+
"payload": {
|
|
43
|
+
"Command": command,
|
|
44
|
+
"CLIVersion": __version__,
|
|
45
|
+
"OS": os_name(),
|
|
46
|
+
"OccurredAt": now(),
|
|
47
|
+
},
|
|
40
48
|
}
|
|
41
49
|
encoded = base64.urlsafe_b64encode(
|
|
42
|
-
json.dumps(
|
|
50
|
+
json.dumps(document, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|
43
51
|
).decode("ascii")
|
|
44
52
|
kwargs: Dict[str, Any] = {
|
|
45
53
|
"stdin": subprocess.DEVNULL,
|
|
@@ -60,7 +68,11 @@ def record_command(command: str) -> None:
|
|
|
60
68
|
pass
|
|
61
69
|
|
|
62
70
|
|
|
63
|
-
def submit_feedback(
|
|
71
|
+
def submit_feedback(
|
|
72
|
+
profile: Profile,
|
|
73
|
+
category: str,
|
|
74
|
+
message: str,
|
|
75
|
+
) -> Dict[str, Any]:
|
|
64
76
|
if category not in {"bug", "suggest"}:
|
|
65
77
|
raise CLIError(tr("Feedback category must be bug or suggest."))
|
|
66
78
|
normalized = message.strip()
|
|
@@ -75,17 +87,33 @@ def submit_feedback(category: str, message: str) -> Dict[str, Any]:
|
|
|
75
87
|
variable=ENDPOINT_ENV,
|
|
76
88
|
)
|
|
77
89
|
)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
try:
|
|
91
|
+
response = _invoke(
|
|
92
|
+
profile,
|
|
93
|
+
FEEDBACK_ACTION,
|
|
94
|
+
{
|
|
95
|
+
"Category": category,
|
|
96
|
+
"Content": normalized,
|
|
97
|
+
"CLIVersion": __version__,
|
|
98
|
+
"OS": os_name(),
|
|
99
|
+
"OccurredAt": now(),
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
feedback_id = response.get("Id") if isinstance(response, dict) else None
|
|
103
|
+
if not feedback_id:
|
|
104
|
+
raise ValueError("missing feedback ID")
|
|
105
|
+
return {"ok": True, "id": feedback_id}
|
|
106
|
+
except ucloud_exc.RetCodeException as error:
|
|
107
|
+
raise CLIError(
|
|
108
|
+
tr(
|
|
109
|
+
"Insights service rejected the request: {detail}",
|
|
110
|
+
detail=error.message or str(error.code),
|
|
111
|
+
)
|
|
112
|
+
) from error
|
|
113
|
+
except (ucloud_exc.UCloudException, TimeoutError, OSError) as error:
|
|
114
|
+
raise CLIError(tr("Unable to reach the insights service.")) from error
|
|
115
|
+
except Exception as error:
|
|
116
|
+
raise CLIError(tr("Insights service returned an invalid response.")) from error
|
|
89
117
|
|
|
90
118
|
|
|
91
119
|
def _base_url() -> str:
|
|
@@ -96,43 +124,22 @@ def _base_url() -> str:
|
|
|
96
124
|
return value if parsed.scheme in {"http", "https"} and parsed.netloc else ""
|
|
97
125
|
|
|
98
126
|
|
|
99
|
-
def
|
|
100
|
-
|
|
101
|
-
req = request.Request(
|
|
102
|
-
f"{_base_url()}{path}",
|
|
103
|
-
data=body,
|
|
104
|
-
headers={
|
|
105
|
-
"Accept": "application/json",
|
|
106
|
-
"Content-Type": "application/json",
|
|
107
|
-
"User-Agent": f"compshare-cli/{__version__}",
|
|
108
|
-
},
|
|
109
|
-
method="POST",
|
|
110
|
-
)
|
|
111
|
-
try:
|
|
112
|
-
with request.urlopen(req, timeout=timeout) as response:
|
|
113
|
-
content = response.read()
|
|
114
|
-
except urlerror.HTTPError as error:
|
|
115
|
-
raise CLIError(
|
|
116
|
-
tr("Insights service rejected the request with HTTP {status}.", status=error.code)
|
|
117
|
-
) from error
|
|
118
|
-
except (urlerror.URLError, TimeoutError, OSError) as error:
|
|
119
|
-
raise CLIError(tr("Unable to reach the insights service.")) from error
|
|
120
|
-
if not content:
|
|
121
|
-
return {"ok": True}
|
|
122
|
-
try:
|
|
123
|
-
result = json.loads(content.decode("utf-8"))
|
|
124
|
-
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
125
|
-
raise CLIError(tr("Insights service returned an invalid response.")) from error
|
|
126
|
-
if not isinstance(result, dict):
|
|
127
|
-
raise CLIError(tr("Insights service returned an invalid response."))
|
|
128
|
-
return result
|
|
127
|
+
def _invoke(profile: Profile, action: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
128
|
+
return CompShareSDK(profile, base_url=_base_url()).invoke(action, payload)
|
|
129
129
|
|
|
130
130
|
|
|
131
131
|
def _send_event(encoded: str) -> None:
|
|
132
132
|
try:
|
|
133
|
-
|
|
134
|
-
if isinstance(
|
|
135
|
-
|
|
133
|
+
document = json.loads(base64.urlsafe_b64decode(encoded.encode("ascii")).decode("utf-8"))
|
|
134
|
+
if not isinstance(document, dict) or not _base_url():
|
|
135
|
+
return
|
|
136
|
+
payload = document.get("payload")
|
|
137
|
+
if not isinstance(payload, dict):
|
|
138
|
+
return
|
|
139
|
+
raw_profile_name = document.get("profile_name")
|
|
140
|
+
profile_name = str(raw_profile_name) if raw_profile_name else None
|
|
141
|
+
profile = ConfigStore().load_profile(profile_name)
|
|
142
|
+
_invoke(profile, EVENT_ACTION, payload)
|
|
136
143
|
except Exception:
|
|
137
144
|
pass
|
|
138
145
|
|
|
@@ -17,13 +17,21 @@ class CompShareSDK:
|
|
|
17
17
|
lag behind the public API and silently discard newer fields.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
def __init__(
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
profile: Profile,
|
|
23
|
+
region: Optional[str] = None,
|
|
24
|
+
base_url: Optional[str] = None,
|
|
25
|
+
) -> None:
|
|
21
26
|
logger = logging.getLogger("compshare_cli.ucloud")
|
|
22
27
|
logger.handlers.clear()
|
|
23
28
|
logger.addHandler(logging.NullHandler())
|
|
24
29
|
logger.setLevel(logging.CRITICAL)
|
|
25
30
|
logger.propagate = False
|
|
26
|
-
|
|
31
|
+
config = profile.sdk_config(region)
|
|
32
|
+
if base_url is not None:
|
|
33
|
+
config["base_url"] = base_url
|
|
34
|
+
self._service = Client(config, logger=logger).ucompshare()
|
|
27
35
|
|
|
28
36
|
def invoke(self, action: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
29
37
|
return self._service.invoke(action, params)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compshare-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
4
4
|
Summary: Command-line client for CompShare GPU compute
|
|
5
5
|
Author: CompShare
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -63,6 +63,9 @@ export COMPSHARE_PUBLIC_KEY='...'
|
|
|
63
63
|
export COMPSHARE_PRIVATE_KEY='...'
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
CLI 使用统计和反馈与业务 API 一样,通过当前 profile 的凭证签名后发送到
|
|
67
|
+
`https://api.compshare.cn/`;开发环境可使用 `COMPSHARE_INSIGHTS_URL` 覆盖网关地址。
|
|
68
|
+
|
|
66
69
|
## 快速开始
|
|
67
70
|
|
|
68
71
|
```bash
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from typer.main import get_command
|
|
6
|
+
from typer.testing import CliRunner
|
|
7
|
+
from ucloud.core import exc as ucloud_exc
|
|
8
|
+
|
|
9
|
+
from compshare_cli import __version__, cli, insights
|
|
10
|
+
from compshare_cli.config import Profile
|
|
11
|
+
from compshare_cli.errors import CLIError
|
|
12
|
+
|
|
13
|
+
runner = CliRunner()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_feedback_posts_category_and_message(monkeypatch) -> None:
|
|
17
|
+
captured = {}
|
|
18
|
+
|
|
19
|
+
def fake_invoke(profile, action, payload):
|
|
20
|
+
captured["profile"] = profile
|
|
21
|
+
captured["action"] = action
|
|
22
|
+
captured["payload"] = payload
|
|
23
|
+
return {"Id": "feedback-7"}
|
|
24
|
+
|
|
25
|
+
monkeypatch.setenv("COMPSHARE_PUBLIC_KEY", "public")
|
|
26
|
+
monkeypatch.setenv("COMPSHARE_PRIVATE_KEY", "private")
|
|
27
|
+
monkeypatch.setattr(insights, "_invoke", fake_invoke)
|
|
28
|
+
result = runner.invoke(
|
|
29
|
+
cli.app,
|
|
30
|
+
["--json", "feedback", "bug", "创建实例失败"],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
assert result.exit_code == 0, result.output
|
|
34
|
+
assert json.loads(result.stdout) == {
|
|
35
|
+
"ok": True,
|
|
36
|
+
"schema_version": "1",
|
|
37
|
+
"data": {"id": "feedback-7"},
|
|
38
|
+
}
|
|
39
|
+
assert captured["profile"] == Profile("public", "private")
|
|
40
|
+
assert captured["action"] == "CreateCSCLIFeedback"
|
|
41
|
+
assert captured["payload"]["Category"] == "bug"
|
|
42
|
+
assert captured["payload"]["Content"] == "创建实例失败"
|
|
43
|
+
assert set(captured["payload"]) == {
|
|
44
|
+
"Category",
|
|
45
|
+
"Content",
|
|
46
|
+
"CLIVersion",
|
|
47
|
+
"OS",
|
|
48
|
+
"OccurredAt",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_feedback_only_accepts_bug_or_suggest() -> None:
|
|
53
|
+
result = runner.invoke(cli.app, ["feedback", "other", "message"])
|
|
54
|
+
|
|
55
|
+
assert result.exit_code == 2
|
|
56
|
+
assert "bug" in result.output
|
|
57
|
+
assert "suggest" in result.output
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_insights_uses_external_gateway_by_default(monkeypatch) -> None:
|
|
61
|
+
captured = {}
|
|
62
|
+
|
|
63
|
+
class FakeSDK:
|
|
64
|
+
def __init__(self, profile, region=None, base_url=None):
|
|
65
|
+
captured["profile"] = profile
|
|
66
|
+
captured["region"] = region
|
|
67
|
+
captured["base_url"] = base_url
|
|
68
|
+
|
|
69
|
+
def invoke(self, action, payload):
|
|
70
|
+
captured["action"] = action
|
|
71
|
+
captured["payload"] = payload
|
|
72
|
+
return {"Id": "feedback-8"}
|
|
73
|
+
|
|
74
|
+
monkeypatch.delenv("COMPSHARE_INSIGHTS_URL", raising=False)
|
|
75
|
+
monkeypatch.setattr(insights, "CompShareSDK", FakeSDK)
|
|
76
|
+
response = insights.submit_feedback(Profile("public", "private"), "suggest", "建议内容")
|
|
77
|
+
|
|
78
|
+
assert response == {"ok": True, "id": "feedback-8"}
|
|
79
|
+
assert captured["base_url"] == "https://api.compshare.cn"
|
|
80
|
+
assert captured["action"] == "CreateCSCLIFeedback"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_feedback_formats_gateway_rejection(monkeypatch) -> None:
|
|
84
|
+
def reject(*_args):
|
|
85
|
+
raise ucloud_exc.RetCodeException(
|
|
86
|
+
action="CreateCSCLIFeedback",
|
|
87
|
+
code=1001,
|
|
88
|
+
message="invalid request body",
|
|
89
|
+
request_uuid="request-1",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
monkeypatch.setattr(insights, "_invoke", reject)
|
|
93
|
+
|
|
94
|
+
with pytest.raises(CLIError, match="invalid request body"):
|
|
95
|
+
insights.submit_feedback(Profile("public", "private"), "suggest", "建议内容")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_telemetry_contains_only_requested_fields(monkeypatch) -> None:
|
|
99
|
+
launched = {}
|
|
100
|
+
|
|
101
|
+
def fake_popen(args, **kwargs):
|
|
102
|
+
launched["args"] = args
|
|
103
|
+
launched["kwargs"] = kwargs
|
|
104
|
+
return object()
|
|
105
|
+
|
|
106
|
+
monkeypatch.setenv("COMPSHARE_INSIGHTS_URL", "https://insights.example.test")
|
|
107
|
+
monkeypatch.setattr(insights.subprocess, "Popen", fake_popen)
|
|
108
|
+
insights.record_command("instance.create", "work")
|
|
109
|
+
|
|
110
|
+
encoded = launched["args"][-1]
|
|
111
|
+
document = json.loads(base64.urlsafe_b64decode(encoded).decode("utf-8"))
|
|
112
|
+
assert document["profile_name"] == "work"
|
|
113
|
+
assert document["payload"]["Command"] == "instance.create"
|
|
114
|
+
assert set(document["payload"]) == {"Command", "CLIVersion", "OS", "OccurredAt"}
|
|
115
|
+
assert launched["kwargs"]["stdout"] is insights.subprocess.DEVNULL
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_event_worker_posts_to_event_endpoint(monkeypatch) -> None:
|
|
119
|
+
captured = {}
|
|
120
|
+
document = {
|
|
121
|
+
"profile_name": "work",
|
|
122
|
+
"payload": {
|
|
123
|
+
"Command": "image.list",
|
|
124
|
+
"CLIVersion": "0.2.1",
|
|
125
|
+
"OS": "linux",
|
|
126
|
+
"OccurredAt": "2026-07-17T10:00:00Z",
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
encoded = base64.urlsafe_b64encode(json.dumps(document).encode("utf-8")).decode("ascii")
|
|
130
|
+
|
|
131
|
+
def fake_load_profile(profile_name):
|
|
132
|
+
captured["profile_name"] = profile_name
|
|
133
|
+
return Profile("public", "private")
|
|
134
|
+
|
|
135
|
+
def fake_invoke(profile, action, payload):
|
|
136
|
+
captured["profile"] = profile
|
|
137
|
+
captured["action"] = action
|
|
138
|
+
captured["payload"] = payload
|
|
139
|
+
return {"Id": "event-1"}
|
|
140
|
+
|
|
141
|
+
monkeypatch.setattr(
|
|
142
|
+
insights.ConfigStore,
|
|
143
|
+
"load_profile",
|
|
144
|
+
lambda self, name: fake_load_profile(name),
|
|
145
|
+
)
|
|
146
|
+
monkeypatch.setattr(insights, "_invoke", fake_invoke)
|
|
147
|
+
insights._send_event(encoded)
|
|
148
|
+
|
|
149
|
+
assert captured["profile_name"] == "work"
|
|
150
|
+
assert captured["profile"] == Profile("public", "private")
|
|
151
|
+
assert captured["action"] == "CreateCSCLIEvent"
|
|
152
|
+
assert captured["payload"] == document["payload"]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@pytest.mark.parametrize(
|
|
156
|
+
("args", "expected"),
|
|
157
|
+
[
|
|
158
|
+
(["version"], "version"),
|
|
159
|
+
(["--json", "instance", "list", "--all"], "instance.list"),
|
|
160
|
+
(["--lang", "en", "instance", "list", "--all"], "instance.list"),
|
|
161
|
+
(
|
|
162
|
+
[
|
|
163
|
+
"--profile",
|
|
164
|
+
"work",
|
|
165
|
+
"team",
|
|
166
|
+
"billing",
|
|
167
|
+
"export",
|
|
168
|
+
"1",
|
|
169
|
+
"--output",
|
|
170
|
+
"orders.csv",
|
|
171
|
+
],
|
|
172
|
+
"team.billing.export",
|
|
173
|
+
),
|
|
174
|
+
(["--json", "--show-sensitive", "instance", "show", "uhost-1"], "instance.show"),
|
|
175
|
+
(["ask", "云硬盘还收费吗?"], "ask"),
|
|
176
|
+
(["feedback", "suggest", "message"], "feedback"),
|
|
177
|
+
(["instance", "--help"], None),
|
|
178
|
+
([], None),
|
|
179
|
+
],
|
|
180
|
+
)
|
|
181
|
+
def test_telemetry_resolves_only_command_names(args, expected) -> None:
|
|
182
|
+
assert cli._command_path(get_command(cli.app), args) == expected
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_main_records_resolved_command(monkeypatch, capsys) -> None:
|
|
186
|
+
commands = []
|
|
187
|
+
monkeypatch.setattr(cli, "record_command", commands.append)
|
|
188
|
+
|
|
189
|
+
cli.main(["version"])
|
|
190
|
+
|
|
191
|
+
assert capsys.readouterr().out.strip() == __version__
|
|
192
|
+
assert commands == ["version"]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_main_passes_selected_profile_to_telemetry(monkeypatch, capsys) -> None:
|
|
196
|
+
calls = []
|
|
197
|
+
monkeypatch.setattr(cli, "record_command", lambda *args: calls.append(args))
|
|
198
|
+
|
|
199
|
+
cli.main(["--profile", "work", "version"])
|
|
200
|
+
|
|
201
|
+
assert capsys.readouterr().out.strip() == __version__
|
|
202
|
+
assert calls == [("version", "work")]
|
|
@@ -15,6 +15,15 @@ def test_official_sdk_accepts_no_default_region() -> None:
|
|
|
15
15
|
assert sdk._service.config.region is None
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
def test_official_sdk_accepts_base_url_override() -> None:
|
|
19
|
+
sdk = CompShareSDK(
|
|
20
|
+
Profile("public", "private"),
|
|
21
|
+
base_url="https://insights.example.test",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
assert sdk._service.config.base_url == "https://insights.example.test"
|
|
25
|
+
|
|
26
|
+
|
|
18
27
|
def test_sdk_uses_generic_invoke_and_quiet_logger(monkeypatch) -> None:
|
|
19
28
|
captured = {}
|
|
20
29
|
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
import json
|
|
3
|
-
from urllib import parse
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
from typer.main import get_command
|
|
7
|
-
from typer.testing import CliRunner
|
|
8
|
-
|
|
9
|
-
from compshare_cli import __version__, cli, insights
|
|
10
|
-
|
|
11
|
-
runner = CliRunner()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class Response:
|
|
15
|
-
def __init__(self, payload):
|
|
16
|
-
self.payload = payload
|
|
17
|
-
|
|
18
|
-
def __enter__(self):
|
|
19
|
-
return self
|
|
20
|
-
|
|
21
|
-
def __exit__(self, *args):
|
|
22
|
-
return None
|
|
23
|
-
|
|
24
|
-
def read(self):
|
|
25
|
-
return json.dumps(self.payload).encode("utf-8")
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def test_feedback_posts_category_and_message(monkeypatch) -> None:
|
|
29
|
-
captured = {}
|
|
30
|
-
|
|
31
|
-
def fake_urlopen(req, timeout):
|
|
32
|
-
captured["url"] = req.full_url
|
|
33
|
-
captured["payload"] = json.loads(req.data)
|
|
34
|
-
captured["timeout"] = timeout
|
|
35
|
-
return Response({"ok": True, "id": 7})
|
|
36
|
-
|
|
37
|
-
monkeypatch.setenv("COMPSHARE_INSIGHTS_URL", "https://insights.example.test/")
|
|
38
|
-
monkeypatch.setattr(insights.request, "urlopen", fake_urlopen)
|
|
39
|
-
result = runner.invoke(
|
|
40
|
-
cli.app,
|
|
41
|
-
["--json", "feedback", "bug", "创建实例失败"],
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
assert result.exit_code == 0, result.output
|
|
45
|
-
assert json.loads(result.stdout) == {
|
|
46
|
-
"ok": True,
|
|
47
|
-
"schema_version": "1",
|
|
48
|
-
"data": {"id": 7},
|
|
49
|
-
}
|
|
50
|
-
assert captured["url"] == "https://insights.example.test/v1/feedback"
|
|
51
|
-
assert captured["timeout"] == 5.0
|
|
52
|
-
assert captured["payload"]["category"] == "bug"
|
|
53
|
-
assert captured["payload"]["message"] == "创建实例失败"
|
|
54
|
-
assert set(captured["payload"]) == {"category", "message", "cli_version", "os", "time"}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def test_feedback_only_accepts_bug_or_suggest() -> None:
|
|
58
|
-
result = runner.invoke(cli.app, ["feedback", "other", "message"])
|
|
59
|
-
|
|
60
|
-
assert result.exit_code == 2
|
|
61
|
-
assert "bug" in result.output
|
|
62
|
-
assert "suggest" in result.output
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def test_feedback_uses_default_service(monkeypatch) -> None:
|
|
66
|
-
captured = {}
|
|
67
|
-
|
|
68
|
-
def fake_urlopen(req, timeout):
|
|
69
|
-
captured["url"] = req.full_url
|
|
70
|
-
return Response({"ok": True})
|
|
71
|
-
|
|
72
|
-
monkeypatch.delenv("COMPSHARE_INSIGHTS_URL", raising=False)
|
|
73
|
-
monkeypatch.setattr(insights.request, "urlopen", fake_urlopen)
|
|
74
|
-
result = runner.invoke(cli.app, ["feedback", "suggest", "建议内容"])
|
|
75
|
-
|
|
76
|
-
assert result.exit_code == 0, result.output
|
|
77
|
-
assert captured["url"] == "http://117.50.180.139:27299/v1/feedback"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def test_telemetry_contains_only_requested_fields(monkeypatch) -> None:
|
|
81
|
-
launched = {}
|
|
82
|
-
|
|
83
|
-
def fake_popen(args, **kwargs):
|
|
84
|
-
launched["args"] = args
|
|
85
|
-
launched["kwargs"] = kwargs
|
|
86
|
-
return object()
|
|
87
|
-
|
|
88
|
-
monkeypatch.setenv("COMPSHARE_INSIGHTS_URL", "https://insights.example.test")
|
|
89
|
-
monkeypatch.setattr(insights.subprocess, "Popen", fake_popen)
|
|
90
|
-
insights.record_command("instance.create")
|
|
91
|
-
|
|
92
|
-
encoded = launched["args"][-1]
|
|
93
|
-
payload = json.loads(base64.urlsafe_b64decode(encoded).decode("utf-8"))
|
|
94
|
-
assert payload["command"] == "instance.create"
|
|
95
|
-
assert set(payload) == {"command", "cli_version", "os", "time"}
|
|
96
|
-
assert launched["kwargs"]["stdout"] is insights.subprocess.DEVNULL
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def test_event_worker_posts_to_event_endpoint(monkeypatch) -> None:
|
|
100
|
-
captured = {}
|
|
101
|
-
payload = {
|
|
102
|
-
"command": "image.list",
|
|
103
|
-
"cli_version": "0.2.1",
|
|
104
|
-
"os": "linux",
|
|
105
|
-
"time": "2026-07-17T10:00:00Z",
|
|
106
|
-
}
|
|
107
|
-
encoded = base64.urlsafe_b64encode(json.dumps(payload).encode("utf-8")).decode("ascii")
|
|
108
|
-
|
|
109
|
-
def fake_urlopen(req, timeout):
|
|
110
|
-
captured["url"] = req.full_url
|
|
111
|
-
captured["payload"] = json.loads(req.data)
|
|
112
|
-
return Response({"ok": True})
|
|
113
|
-
|
|
114
|
-
monkeypatch.setenv("COMPSHARE_INSIGHTS_URL", "https://insights.example.test")
|
|
115
|
-
monkeypatch.setattr(insights.request, "urlopen", fake_urlopen)
|
|
116
|
-
insights._send_event(encoded)
|
|
117
|
-
|
|
118
|
-
assert parse.urlparse(captured["url"]).path == "/v1/events"
|
|
119
|
-
assert captured["payload"] == payload
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
@pytest.mark.parametrize(
|
|
123
|
-
("args", "expected"),
|
|
124
|
-
[
|
|
125
|
-
(["version"], "version"),
|
|
126
|
-
(["--json", "instance", "list", "--all"], "instance.list"),
|
|
127
|
-
(["--lang", "en", "instance", "list", "--all"], "instance.list"),
|
|
128
|
-
(
|
|
129
|
-
[
|
|
130
|
-
"--profile",
|
|
131
|
-
"work",
|
|
132
|
-
"team",
|
|
133
|
-
"billing",
|
|
134
|
-
"export",
|
|
135
|
-
"1",
|
|
136
|
-
"--output",
|
|
137
|
-
"orders.csv",
|
|
138
|
-
],
|
|
139
|
-
"team.billing.export",
|
|
140
|
-
),
|
|
141
|
-
(["--json", "--show-sensitive", "instance", "show", "uhost-1"], "instance.show"),
|
|
142
|
-
(["ask", "云硬盘还收费吗?"], "ask"),
|
|
143
|
-
(["feedback", "suggest", "message"], "feedback"),
|
|
144
|
-
(["instance", "--help"], None),
|
|
145
|
-
([], None),
|
|
146
|
-
],
|
|
147
|
-
)
|
|
148
|
-
def test_telemetry_resolves_only_command_names(args, expected) -> None:
|
|
149
|
-
assert cli._command_path(get_command(cli.app), args) == expected
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def test_main_records_resolved_command(monkeypatch, capsys) -> None:
|
|
153
|
-
commands = []
|
|
154
|
-
monkeypatch.setattr(cli, "record_command", commands.append)
|
|
155
|
-
|
|
156
|
-
cli.main(["version"])
|
|
157
|
-
|
|
158
|
-
assert capsys.readouterr().out.strip() == __version__
|
|
159
|
-
assert commands == ["version"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|