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.
Files changed (55) hide show
  1. {compshare_cli-0.3.5/src/compshare_cli.egg-info → compshare_cli-0.3.6}/PKG-INFO +4 -1
  2. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/README.md +3 -0
  3. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/__init__.py +1 -1
  4. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/cli.py +33 -1
  5. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/commands/feedback.py +1 -1
  6. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/i18n.py +1 -3
  7. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/insights.py +63 -56
  8. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/sdk.py +10 -2
  9. {compshare_cli-0.3.5 → compshare_cli-0.3.6/src/compshare_cli.egg-info}/PKG-INFO +4 -1
  10. compshare_cli-0.3.6/tests/test_feedback.py +202 -0
  11. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_sdk.py +9 -0
  12. compshare_cli-0.3.5/tests/test_feedback.py +0 -159
  13. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/LICENSE +0 -0
  14. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/pyproject.toml +0 -0
  15. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/setup.cfg +0 -0
  16. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/__main__.py +0 -0
  17. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/actions.py +0 -0
  18. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/api.py +0 -0
  19. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/commands/__init__.py +0 -0
  20. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/commands/ask.py +0 -0
  21. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/commands/common.py +0 -0
  22. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/commands/doctor.py +0 -0
  23. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/commands/image.py +0 -0
  24. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/commands/instance.py +0 -0
  25. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/commands/storage.py +0 -0
  26. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/commands/team.py +0 -0
  27. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/config.py +0 -0
  28. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/errors.py +0 -0
  29. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/instance_templates.py +0 -0
  30. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/location.py +0 -0
  31. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/output.py +0 -0
  32. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/parsing.py +0 -0
  33. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/qa.py +0 -0
  34. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/remote_jobs.py +0 -0
  35. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/runtime.py +0 -0
  36. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/ssh.py +0 -0
  37. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli/ssh_cache.py +0 -0
  38. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli.egg-info/SOURCES.txt +0 -0
  39. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli.egg-info/dependency_links.txt +0 -0
  40. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli.egg-info/entry_points.txt +0 -0
  41. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli.egg-info/requires.txt +0 -0
  42. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/src/compshare_cli.egg-info/top_level.txt +0 -0
  43. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_actions.py +0 -0
  44. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_api.py +0 -0
  45. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_cli.py +0 -0
  46. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_config.py +0 -0
  47. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_instance_templates.py +0 -0
  48. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_location.py +0 -0
  49. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_output.py +0 -0
  50. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_parsing.py +0 -0
  51. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_qa.py +0 -0
  52. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_release_version.py +0 -0
  53. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_remote_jobs.py +0 -0
  54. {compshare_cli-0.3.5 → compshare_cli-0.3.6}/tests/test_ssh.py +0 -0
  55. {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.5
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
@@ -38,6 +38,9 @@ export COMPSHARE_PUBLIC_KEY='...'
38
38
  export COMPSHARE_PRIVATE_KEY='...'
39
39
  ```
40
40
 
41
+ CLI 使用统计和反馈与业务 API 一样,通过当前 profile 的凭证签名后发送到
42
+ `https://api.compshare.cn/`;开发环境可使用 `COMPSHARE_INSIGHTS_URL` 覆盖网关地址。
43
+
41
44
  ## 快速开始
42
45
 
43
46
  ```bash
@@ -1,3 +1,3 @@
1
1
  """CompShare command-line client."""
2
2
 
3
- __version__ = "0.3.5"
3
+ __version__ = "0.3.6"
@@ -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
- record_command(telemetry_command)
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 with HTTP {status}.": (
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 error as urlerror
12
- from urllib import parse, request
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 = "http://117.50.180.139:27299"
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
- payload = {
36
- "command": command,
37
- "cli_version": __version__,
38
- "os": os_name(),
39
- "time": now(),
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(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
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(category: str, message: str) -> Dict[str, Any]:
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
- return _post(
79
- "/v1/feedback",
80
- {
81
- "category": category,
82
- "message": normalized,
83
- "cli_version": __version__,
84
- "os": os_name(),
85
- "time": now(),
86
- },
87
- timeout=5.0,
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 _post(path: str, payload: Dict[str, Any], *, timeout: float) -> Dict[str, Any]:
100
- body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
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
- payload = json.loads(base64.urlsafe_b64decode(encoded.encode("ascii")).decode("utf-8"))
134
- if isinstance(payload, dict) and _base_url():
135
- _post("/v1/events", payload, timeout=3.0)
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__(self, profile: Profile, region: Optional[str] = None) -> None:
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
- self._service = Client(profile.sdk_config(region), logger=logger).ucompshare()
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.5
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