compshare-cli 0.3.6__tar.gz → 0.4.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.
Files changed (58) hide show
  1. {compshare_cli-0.3.6/src/compshare_cli.egg-info → compshare_cli-0.4.0}/PKG-INFO +15 -3
  2. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/README.md +13 -1
  3. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/pyproject.toml +1 -1
  4. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/__init__.py +1 -1
  5. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/actions.py +17 -2
  6. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/cli.py +15 -9
  7. compshare_cli-0.4.0/src/compshare_cli/commands/bandwidth.py +452 -0
  8. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/commands/common.py +6 -1
  9. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/commands/instance.py +7 -19
  10. compshare_cli-0.4.0/src/compshare_cli/commands/minimax.py +428 -0
  11. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/i18n.py +118 -0
  12. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/location.py +29 -2
  13. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/sdk.py +1 -1
  14. {compshare_cli-0.3.6 → compshare_cli-0.4.0/src/compshare_cli.egg-info}/PKG-INFO +15 -3
  15. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli.egg-info/SOURCES.txt +4 -0
  16. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/tests/test_actions.py +5 -3
  17. compshare_cli-0.4.0/tests/test_bandwidth.py +273 -0
  18. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/tests/test_cli.py +4 -2
  19. compshare_cli-0.4.0/tests/test_minimax.py +287 -0
  20. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/tests/test_sdk.py +15 -0
  21. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/LICENSE +0 -0
  22. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/setup.cfg +0 -0
  23. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/__main__.py +0 -0
  24. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/api.py +0 -0
  25. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/commands/__init__.py +0 -0
  26. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/commands/ask.py +0 -0
  27. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/commands/doctor.py +0 -0
  28. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/commands/feedback.py +0 -0
  29. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/commands/image.py +0 -0
  30. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/commands/storage.py +0 -0
  31. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/commands/team.py +0 -0
  32. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/config.py +0 -0
  33. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/errors.py +0 -0
  34. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/insights.py +0 -0
  35. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/instance_templates.py +0 -0
  36. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/output.py +0 -0
  37. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/parsing.py +0 -0
  38. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/qa.py +0 -0
  39. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/remote_jobs.py +0 -0
  40. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/runtime.py +0 -0
  41. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/ssh.py +0 -0
  42. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli/ssh_cache.py +0 -0
  43. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli.egg-info/dependency_links.txt +0 -0
  44. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli.egg-info/entry_points.txt +0 -0
  45. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli.egg-info/requires.txt +0 -0
  46. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/src/compshare_cli.egg-info/top_level.txt +0 -0
  47. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/tests/test_api.py +0 -0
  48. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/tests/test_config.py +0 -0
  49. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/tests/test_feedback.py +0 -0
  50. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/tests/test_instance_templates.py +0 -0
  51. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/tests/test_location.py +0 -0
  52. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/tests/test_output.py +0 -0
  53. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/tests/test_parsing.py +0 -0
  54. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/tests/test_qa.py +0 -0
  55. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/tests/test_release_version.py +0 -0
  56. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/tests/test_remote_jobs.py +0 -0
  57. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/tests/test_ssh.py +0 -0
  58. {compshare_cli-0.3.6 → compshare_cli-0.4.0}/tests/test_ssh_cache.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compshare-cli
3
- Version: 0.3.6
4
- Summary: Command-line client for CompShare GPU compute
3
+ Version: 0.4.0
4
+ Summary: Command-line client for CompShare GPU compute and MiniMax H3
5
5
  Author: CompShare
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/BennielAllan/compshare-cli
@@ -25,12 +25,14 @@ Dynamic: license-file
25
25
 
26
26
  # CompShare CLI
27
27
 
28
- 在终端管理优云智算 GPU 实例、镜像、云盘、US3 挂载和团队资源。
28
+ 在终端管理优云智算 GPU 实例、镜像、云盘、US3 挂载、独享带宽、团队资源和 MiniMax H3 视频任务。
29
29
 
30
30
  - 交互式创建实例,实时查询规格、库存和价格
31
31
  - 支持批量生命周期操作、等待状态和跨地域查询
32
32
  - 支持可断线恢复的远程任务、状态查询和增量日志读取
33
33
  - 支持社区/自定义镜像、云盘、团队额度与账单
34
+ - 支持独享带宽询价、购买、改配、退款预估及实例 EIP 切换
35
+ - 支持创建、查询和取消 MiniMax H3 视频任务,并查看积分套餐包与余额
34
36
  - 默认中文帮助,可切换英文;所有命令支持 JSON 输出
35
37
  - 可通过 `compshare ask` 查询产品使用和计费知识
36
38
  - 可通过 `compshare feedback` 反馈 CLI 问题和建议
@@ -61,6 +63,9 @@ compshare --profile production instance list
61
63
  ```bash
62
64
  export COMPSHARE_PUBLIC_KEY='...'
63
65
  export COMPSHARE_PRIVATE_KEY='...'
66
+
67
+ # MiniMax H3 使用独立的模型 API Key
68
+ export COMPSHARE_MINIMAX_API_KEY='sk-ml-...'
64
69
  ```
65
70
 
66
71
  CLI 使用统计和反馈与业务 API 一样,通过当前 profile 的凭证签名后发送到
@@ -77,6 +82,11 @@ compshare instance list --all
77
82
  # 连接实例
78
83
  compshare instance ssh INSTANCE_ID
79
84
 
85
+ # MiniMax H3:先预演,再创建
86
+ compshare --json minimax create '一只猫在海边奔跑' --dry-run
87
+ compshare --json minimax create '一只猫在海边奔跑' --yes
88
+ compshare --json minimax points
89
+
80
90
  # 查看帮助
81
91
  compshare -h
82
92
  compshare instance -h
@@ -92,6 +102,8 @@ compshare ask 产品使用和计费问答
92
102
  compshare instance GPU 实例、规格、库存、价格和生命周期
93
103
  compshare image 平台、自定义、社区及共享镜像
94
104
  compshare storage 云盘和 US3 挂载
105
+ compshare bandwidth 独享带宽和实例 EIP 带宽归属
106
+ compshare minimax MiniMax H3 视频任务和积分
95
107
  compshare team 团队、邀请、成员额度、账单和审计
96
108
  ```
97
109
 
@@ -1,11 +1,13 @@
1
1
  # CompShare CLI
2
2
 
3
- 在终端管理优云智算 GPU 实例、镜像、云盘、US3 挂载和团队资源。
3
+ 在终端管理优云智算 GPU 实例、镜像、云盘、US3 挂载、独享带宽、团队资源和 MiniMax H3 视频任务。
4
4
 
5
5
  - 交互式创建实例,实时查询规格、库存和价格
6
6
  - 支持批量生命周期操作、等待状态和跨地域查询
7
7
  - 支持可断线恢复的远程任务、状态查询和增量日志读取
8
8
  - 支持社区/自定义镜像、云盘、团队额度与账单
9
+ - 支持独享带宽询价、购买、改配、退款预估及实例 EIP 切换
10
+ - 支持创建、查询和取消 MiniMax H3 视频任务,并查看积分套餐包与余额
9
11
  - 默认中文帮助,可切换英文;所有命令支持 JSON 输出
10
12
  - 可通过 `compshare ask` 查询产品使用和计费知识
11
13
  - 可通过 `compshare feedback` 反馈 CLI 问题和建议
@@ -36,6 +38,9 @@ compshare --profile production instance list
36
38
  ```bash
37
39
  export COMPSHARE_PUBLIC_KEY='...'
38
40
  export COMPSHARE_PRIVATE_KEY='...'
41
+
42
+ # MiniMax H3 使用独立的模型 API Key
43
+ export COMPSHARE_MINIMAX_API_KEY='sk-ml-...'
39
44
  ```
40
45
 
41
46
  CLI 使用统计和反馈与业务 API 一样,通过当前 profile 的凭证签名后发送到
@@ -52,6 +57,11 @@ compshare instance list --all
52
57
  # 连接实例
53
58
  compshare instance ssh INSTANCE_ID
54
59
 
60
+ # MiniMax H3:先预演,再创建
61
+ compshare --json minimax create '一只猫在海边奔跑' --dry-run
62
+ compshare --json minimax create '一只猫在海边奔跑' --yes
63
+ compshare --json minimax points
64
+
55
65
  # 查看帮助
56
66
  compshare -h
57
67
  compshare instance -h
@@ -67,6 +77,8 @@ compshare ask 产品使用和计费问答
67
77
  compshare instance GPU 实例、规格、库存、价格和生命周期
68
78
  compshare image 平台、自定义、社区及共享镜像
69
79
  compshare storage 云盘和 US3 挂载
80
+ compshare bandwidth 独享带宽和实例 EIP 带宽归属
81
+ compshare minimax MiniMax H3 视频任务和积分
70
82
  compshare team 团队、邀请、成员额度、账单和审计
71
83
  ```
72
84
 
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "compshare-cli"
7
7
  dynamic = ["version"]
8
- description = "Command-line client for CompShare GPU compute"
8
+ description = "Command-line client for CompShare GPU compute and MiniMax H3"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
11
11
  license = "Apache-2.0"
@@ -1,3 +1,3 @@
1
1
  """CompShare command-line client."""
2
2
 
3
- __version__ = "0.3.6"
3
+ __version__ = "0.4.0"
@@ -1,4 +1,4 @@
1
- """Public CompShare API actions covered by the first CLI release."""
1
+ """Public CompShare API actions covered by the CLI."""
2
2
 
3
3
  INSTANCE_ACTIONS = frozenset(
4
4
  {
@@ -88,7 +88,22 @@ TEAM_ACTIONS = frozenset(
88
88
  }
89
89
  )
90
90
 
91
- PUBLIC_ACTIONS = INSTANCE_ACTIONS | IMAGE_ACTIONS | STORAGE_ACTIONS | TEAM_ACTIONS
91
+ BANDWIDTH_ACTIONS = frozenset(
92
+ {
93
+ "CreateCompShareShareBandwidth",
94
+ "DeleteCompShareShareBandwidth",
95
+ "DescribeCompShareShareBandwidth",
96
+ "GetCompShareShareBandwidthPrice",
97
+ "GetCompShareShareBandwidthRefundPrice",
98
+ "GetCompShareShareBandwidthUpgradePrice",
99
+ "ModifyCompShareShareBandwidth",
100
+ "SwitchCompShareEIPShareBandwidth",
101
+ }
102
+ )
103
+
104
+ PUBLIC_ACTIONS = (
105
+ INSTANCE_ACTIONS | IMAGE_ACTIONS | STORAGE_ACTIONS | TEAM_ACTIONS | BANDWIDTH_ACTIONS
106
+ )
92
107
 
93
108
  # No unavailable API actions are currently exposed as placeholders.
94
109
  COMING_SOON_ACTIONS = frozenset()
@@ -11,9 +11,9 @@ from typer.main import get_command
11
11
 
12
12
  from compshare_cli import __version__
13
13
  from compshare_cli.commands import ask as ask_command
14
+ from compshare_cli.commands import bandwidth, image, instance, minimax, storage, team
14
15
  from compshare_cli.commands import doctor as doctor_command
15
16
  from compshare_cli.commands import feedback as feedback_command
16
- from compshare_cli.commands import image, instance, storage, team
17
17
  from compshare_cli.commands.common import confirm
18
18
  from compshare_cli.config import DEFAULT_PROFILE, ConfigStore, Profile
19
19
  from compshare_cli.errors import CLIError, ConfigError, UsageError
@@ -26,6 +26,10 @@ _TYPER_CLICK = getattr(typer_core, "_click", click)
26
26
  _TYPER_CLICK_EXCEPTIONS = getattr(_TYPER_CLICK, "exceptions", _TYPER_CLICK)
27
27
  _CLICK_EXCEPTIONS = (click.ClickException, _TYPER_CLICK_EXCEPTIONS.ClickException)
28
28
  _ABORT_EXCEPTIONS = (click.Abort, _TYPER_CLICK_EXCEPTIONS.Abort)
29
+ _GROUP_TYPES = (click.Group, typer_core.TyperGroup)
30
+ _OPTION_TYPES = (click.Option, typer_core.TyperOption)
31
+ _ARGUMENT_TYPES = (click.Argument, typer_core.TyperArgument)
32
+ _CLICK_CONTEXT = getattr(_TYPER_CLICK, "Context", click.Context)
29
33
 
30
34
  completion_init()
31
35
 
@@ -42,7 +46,7 @@ class RootGroup(typer_core.TyperGroup):
42
46
 
43
47
  app = typer.Typer(
44
48
  name="compshare",
45
- help="Manage CompShare GPU compute from the terminal.",
49
+ help="Manage CompShare GPU compute and MiniMax H3 from the terminal.",
46
50
  cls=RootGroup,
47
51
  no_args_is_help=True,
48
52
  add_completion=False,
@@ -51,6 +55,8 @@ app = typer.Typer(
51
55
  app.add_typer(instance.app, name="instance")
52
56
  app.add_typer(image.app, name="image")
53
57
  app.add_typer(storage.app, name="storage")
58
+ app.add_typer(bandwidth.app, name="bandwidth")
59
+ app.add_typer(minimax.app, name="minimax")
54
60
  app.add_typer(team.app, name="team")
55
61
  config_app = typer.Typer(
56
62
  help="Manage credential profiles.",
@@ -391,7 +397,7 @@ def _command_path(command: click.Command, argv: List[str]) -> Optional[str]:
391
397
 
392
398
  path: List[str] = []
393
399
  current = command
394
- while remaining and isinstance(current, click.Group):
400
+ while remaining and isinstance(current, _GROUP_TYPES):
395
401
  name = remaining.pop(0)
396
402
  child = current.commands.get(name)
397
403
  if child is None:
@@ -429,13 +435,13 @@ def _json_help_payload(command: click.Command, argv: List[str]) -> Dict[str, Any
429
435
  "--show-sensitive",
430
436
  }:
431
437
  continue
432
- if isinstance(current, click.Group) and token in current.commands:
438
+ if isinstance(current, _GROUP_TYPES) and token in current.commands:
433
439
  current = current.commands[token]
434
440
  path.append(token)
435
441
 
436
442
  parameters: List[Dict[str, Any]] = []
437
443
  for parameter in current.params:
438
- if isinstance(parameter, click.Option):
444
+ if isinstance(parameter, _OPTION_TYPES):
439
445
  parameters.append(
440
446
  {
441
447
  "kind": "option",
@@ -446,7 +452,7 @@ def _json_help_payload(command: click.Command, argv: List[str]) -> Dict[str, Any
446
452
  "help": parameter.help,
447
453
  }
448
454
  )
449
- elif isinstance(parameter, click.Argument):
455
+ elif isinstance(parameter, _ARGUMENT_TYPES):
450
456
  parameters.append(
451
457
  {
452
458
  "kind": "argument",
@@ -456,11 +462,11 @@ def _json_help_payload(command: click.Command, argv: List[str]) -> Dict[str, Any
456
462
  }
457
463
  )
458
464
  commands = []
459
- if isinstance(current, click.Group):
465
+ if isinstance(current, _GROUP_TYPES):
460
466
  commands = [
461
467
  {"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
468
+ for name in current.list_commands(_CLICK_CONTEXT(current))
469
+ if (child := current.get_command(_CLICK_CONTEXT(current), name)) is not None
464
470
  and not child.hidden
465
471
  ]
466
472
  command_name = " ".join(["compshare", *path])
@@ -0,0 +1,452 @@
1
+ from __future__ import annotations
2
+
3
+ from decimal import Decimal
4
+ from typing import Any, Dict, Iterable, List, Optional, Tuple
5
+
6
+ import typer
7
+
8
+ from compshare_cli.api import call, call_captured, collect_pages
9
+ from compshare_cli.commands.common import confirm_details, request, runtime
10
+ from compshare_cli.errors import UsageError
11
+ from compshare_cli.i18n import tr
12
+ from compshare_cli.location import locate_instances, supported_locations
13
+ from compshare_cli.output import Renderer
14
+ from compshare_cli.parsing import compact
15
+
16
+ app = typer.Typer(help="Manage dedicated bandwidth.", no_args_is_help=True)
17
+
18
+ _BANDWIDTH_COLUMNS = (
19
+ ("ShareBandwidthId", "BANDWIDTH ID"),
20
+ ("Region", "REGION"),
21
+ ("Zone", "ZONE"),
22
+ ("Bandwidth", "BANDWIDTH (Mbps)"),
23
+ ("Status", "STATUS"),
24
+ ("ChargeType", "CHARGE"),
25
+ ("BindEIPCount", "EIPS"),
26
+ ("PurchaseValue", "EXPIRES"),
27
+ )
28
+ _BANDWIDTH_JSON_FIELDS = (
29
+ "ShareBandwidthId",
30
+ "ResourceId",
31
+ "Region",
32
+ "Zone",
33
+ "Bandwidth",
34
+ "Status",
35
+ "ChargeType",
36
+ "Quantity",
37
+ "PurchaseValue",
38
+ "BindEIPCount",
39
+ "BindEIPInfo",
40
+ "CreateTime",
41
+ "UpdateTime",
42
+ )
43
+ _EIP_COLUMNS = (
44
+ ("UHostId", "INSTANCE"),
45
+ ("Name", "NAME"),
46
+ ("Zone", "ZONE"),
47
+ ("IP", "PUBLIC IP"),
48
+ ("EIPId", "EIP ID"),
49
+ ("Scope", "BANDWIDTH SCOPE"),
50
+ ("Bandwidth", "BANDWIDTH (Mbps)"),
51
+ ("CanSwitch", "CAN SWITCH"),
52
+ )
53
+ _EIP_JSON_FIELDS = tuple(key for key, _ in _EIP_COLUMNS) + (
54
+ "Region",
55
+ "ShareBandwidthId",
56
+ "TargetScope",
57
+ )
58
+
59
+
60
+ def _charge_type(value: str) -> str:
61
+ charge = {"month": "Month", "year": "Year"}.get(value.casefold())
62
+ if charge is None:
63
+ raise UsageError(tr("--charge must be Month or Year."))
64
+ return charge
65
+
66
+
67
+ def _target_scope(value: str) -> Tuple[str, str]:
68
+ target = {
69
+ "company": ("Company", "dedicated"),
70
+ "dedicated": ("Company", "dedicated"),
71
+ "public": ("Public", "shared"),
72
+ "shared": ("Public", "shared"),
73
+ }.get(value.casefold())
74
+ if target is None:
75
+ raise UsageError(tr("--to must be dedicated or shared."))
76
+ return target
77
+
78
+
79
+ def _cny(cents: Any, *, signed: bool = False) -> Optional[str]:
80
+ if isinstance(cents, bool) or not isinstance(cents, (int, float)):
81
+ return None
82
+ amount = Decimal(str(cents)) / 100
83
+ if signed and amount < 0:
84
+ return tr("Refund {amount} CNY", amount=f"{abs(amount):.2f}")
85
+ if signed and amount > 0:
86
+ return tr("Payment {amount} CNY", amount=f"{amount:.2f}")
87
+ return f"{amount:.2f} CNY"
88
+
89
+
90
+ def _request(
91
+ ctx: typer.Context,
92
+ region: str,
93
+ zone: str,
94
+ values: Optional[Dict[str, Any]] = None,
95
+ ) -> Dict[str, Any]:
96
+ params = request(ctx, zone=True, region_value=region, zone_value=zone)
97
+ params.update(values or {})
98
+ return params
99
+
100
+
101
+ def _bandwidth_rows(response: Dict[str, Any]) -> Iterable[Dict[str, Any]]:
102
+ return response.get("CompanyShareBandwidthSet") or []
103
+
104
+
105
+ def _eip_rows(response: Dict[str, Any]) -> Iterable[Dict[str, Any]]:
106
+ for host in response.get("UHostSet") or []:
107
+ for ip in host.get("IPSet") or []:
108
+ if not ip.get("IPId") or ip.get("Type") in {"Private", "Proxy"}:
109
+ continue
110
+ assignment = ip.get("ShareBandwidth") or {}
111
+ yield {
112
+ "UHostId": host.get("UHostId"),
113
+ "Name": host.get("Name"),
114
+ "Region": host.get("Region"),
115
+ "Zone": host.get("Zone"),
116
+ "IP": ip.get("IP"),
117
+ "EIPId": ip.get("IPId"),
118
+ "Scope": assignment.get("Scope", "Unknown"),
119
+ "ShareBandwidthId": assignment.get("ShareBandwidthId"),
120
+ "Bandwidth": assignment.get("Bandwidth"),
121
+ "CanSwitch": bool(assignment.get("CanSwitch")),
122
+ "TargetScope": assignment.get("TargetScope"),
123
+ }
124
+
125
+
126
+ @app.command("list", help="List dedicated bandwidth resources.")
127
+ def list_bandwidths(
128
+ ctx: typer.Context,
129
+ region: Optional[str] = typer.Option(None, "--region", help="Filter by region."),
130
+ zone: Optional[str] = typer.Option(None, "--zone", help="Filter by availability zone."),
131
+ ) -> None:
132
+ if zone is not None and region is None:
133
+ raise UsageError(tr("--zone requires --region."))
134
+ state = runtime(ctx)
135
+ locations = [
136
+ item
137
+ for item in supported_locations(state, request_region=region)
138
+ if not item.get("IsPod") and (zone is None or item.get("Zone") == zone)
139
+ ]
140
+ rows: List[Dict[str, Any]] = []
141
+ for location in locations:
142
+ location_region = str(location["Region"])
143
+ location_zone = str(location["Zone"])
144
+ response = call(
145
+ state,
146
+ "DescribeCompShareShareBandwidth",
147
+ _request(ctx, location_region, location_zone),
148
+ )
149
+ bandwidth = response.get("CompanyShareBandwidth")
150
+ if bandwidth:
151
+ rows.append(
152
+ {
153
+ **dict(bandwidth),
154
+ "Region": location_region,
155
+ "Zone": location_zone,
156
+ }
157
+ )
158
+ response = {
159
+ "CompanyShareBandwidthSet": rows,
160
+ "TotalCount": len(rows),
161
+ "ReturnedCount": len(rows),
162
+ }
163
+ Renderer(state.json_output, state.show_sensitive).data(
164
+ response,
165
+ rows=_bandwidth_rows(response),
166
+ columns=_BANDWIDTH_COLUMNS,
167
+ json_list=True,
168
+ json_fields=_BANDWIDTH_JSON_FIELDS,
169
+ )
170
+
171
+
172
+ @app.command("instances", help="List instance EIP bandwidth assignments.")
173
+ def instances(
174
+ ctx: typer.Context,
175
+ ids: Optional[List[str]] = typer.Option(None, "--id", help="Instance ID; repeatable."),
176
+ region: Optional[str] = typer.Option(None, "--region", help="Filter by region."),
177
+ zone: Optional[str] = typer.Option(None, "--zone", help="Filter by availability zone."),
178
+ ) -> None:
179
+ if (region is None) != (zone is None):
180
+ raise UsageError(tr("--region and --zone must be provided together."))
181
+ state = runtime(ctx)
182
+ params = request(ctx, region_value=region)
183
+ params.update(
184
+ compact(
185
+ {
186
+ "Zone": zone,
187
+ "UHostIds": ids,
188
+ "SkipBill": True,
189
+ "IncludeShareBandwidth": True,
190
+ }
191
+ )
192
+ )
193
+ response = collect_pages(state, "DescribeCompShareInstance", params, "UHostSet")
194
+ rows = list(_eip_rows(response))
195
+ result = {
196
+ "EIPSet": rows,
197
+ "TotalCount": len(rows),
198
+ "ReturnedCount": len(rows),
199
+ }
200
+ Renderer(state.json_output, state.show_sensitive).data(
201
+ result,
202
+ rows=rows,
203
+ columns=_EIP_COLUMNS,
204
+ json_list=True,
205
+ json_fields=_EIP_JSON_FIELDS,
206
+ )
207
+
208
+
209
+ @app.command("create", help="Purchase dedicated bandwidth after quoting the price.")
210
+ def create(
211
+ ctx: typer.Context,
212
+ bandwidth: int = typer.Option(
213
+ ...,
214
+ "--bandwidth",
215
+ min=20,
216
+ max=10000,
217
+ help="Bandwidth limit in Mbps (20-10000).",
218
+ ),
219
+ charge: str = typer.Option("Month", help="Billing type: Month or Year."),
220
+ quantity: int = typer.Option(1, min=1, help="Billing duration for prepaid modes."),
221
+ region: str = typer.Option(..., "--region", help="Region for this request."),
222
+ zone: str = typer.Option(..., "--zone", help="Availability zone."),
223
+ dry_run: bool = typer.Option(
224
+ False,
225
+ "--dry-run",
226
+ help="Quote and show the request without purchasing bandwidth.",
227
+ ),
228
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
229
+ ) -> None:
230
+ state = runtime(ctx)
231
+ params = _request(
232
+ ctx,
233
+ region,
234
+ zone,
235
+ {
236
+ "ShareBandwidth": bandwidth,
237
+ "ChargeType": _charge_type(charge),
238
+ "Quantity": quantity,
239
+ },
240
+ )
241
+ quote = call(state, "GetCompShareShareBandwidthPrice", params)
242
+ plan = {"dry_run": dry_run, "request": params, "price": quote}
243
+ fields = [
244
+ ("ZONE", zone),
245
+ ("BANDWIDTH", f"{bandwidth} Mbps"),
246
+ ("CHARGE", params["ChargeType"]),
247
+ ("QUANTITY", quantity),
248
+ ("PRICE", _cny(quote.get("TotalPrice"))),
249
+ ]
250
+ if dry_run:
251
+ Renderer(state.json_output, state.show_sensitive).details(
252
+ "Create plan", fields, response=plan
253
+ )
254
+ return
255
+ confirm_details(state, "Create plan", fields, "Confirm this operation?", yes)
256
+ created = call(state, "CreateCompShareShareBandwidth", params)
257
+ result = {"price": quote, "created": created}
258
+ resource = created.get("CompanyShareBandwidth") or {}
259
+ Renderer(state.json_output, state.show_sensitive).details(
260
+ "Operation completed",
261
+ [
262
+ ("BANDWIDTH ID", resource.get("ShareBandwidthId")),
263
+ ("BANDWIDTH", f"{resource.get('Bandwidth', bandwidth)} Mbps"),
264
+ ("ORDER", created.get("OrderNo")),
265
+ ],
266
+ response=result,
267
+ )
268
+
269
+
270
+ @app.command("resize", help="Change a dedicated bandwidth limit after quoting the price.")
271
+ def resize(
272
+ ctx: typer.Context,
273
+ bandwidth_id: str = typer.Argument(..., help="Dedicated bandwidth ID."),
274
+ bandwidth: int = typer.Option(
275
+ ...,
276
+ "--bandwidth",
277
+ min=20,
278
+ max=10000,
279
+ help="Target bandwidth limit in Mbps (20-10000).",
280
+ ),
281
+ region: str = typer.Option(..., "--region", help="Region for this request."),
282
+ zone: str = typer.Option(..., "--zone", help="Availability zone."),
283
+ dry_run: bool = typer.Option(
284
+ False,
285
+ "--dry-run",
286
+ help="Quote and show the request without changing bandwidth.",
287
+ ),
288
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
289
+ ) -> None:
290
+ state = runtime(ctx)
291
+ params = _request(
292
+ ctx,
293
+ region,
294
+ zone,
295
+ {"ShareBandwidthId": bandwidth_id, "ShareBandwidth": bandwidth},
296
+ )
297
+ quote = call(state, "GetCompShareShareBandwidthUpgradePrice", params)
298
+ plan = {"dry_run": dry_run, "request": params, "price": quote}
299
+ fields = [
300
+ ("BANDWIDTH ID", bandwidth_id),
301
+ ("ZONE", zone),
302
+ ("TARGET BANDWIDTH", f"{bandwidth} Mbps"),
303
+ ("PRICE", _cny(quote.get("TotalPrice"), signed=True)),
304
+ ]
305
+ if dry_run:
306
+ Renderer(state.json_output, state.show_sensitive).details(
307
+ "Operation plan", fields, response=plan
308
+ )
309
+ return
310
+ confirm_details(state, "Operation plan", fields, "Confirm this operation?", yes)
311
+ modified = call(state, "ModifyCompShareShareBandwidth", params)
312
+ Renderer(state.json_output, state.show_sensitive).details(
313
+ "Operation completed",
314
+ [("BANDWIDTH ID", bandwidth_id), ("BANDWIDTH", f"{bandwidth} Mbps")],
315
+ response={"price": quote, "modified": modified},
316
+ )
317
+
318
+
319
+ @app.command("delete", help="Delete dedicated bandwidth after quoting the refund.")
320
+ def delete(
321
+ ctx: typer.Context,
322
+ bandwidth_id: str = typer.Argument(..., help="Dedicated bandwidth ID."),
323
+ region: str = typer.Option(..., "--region", help="Region for this request."),
324
+ zone: str = typer.Option(..., "--zone", help="Availability zone."),
325
+ dry_run: bool = typer.Option(
326
+ False,
327
+ "--dry-run",
328
+ help="Quote and show the request without deleting bandwidth.",
329
+ ),
330
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
331
+ ) -> None:
332
+ state = runtime(ctx)
333
+ params = _request(ctx, region, zone, {"ShareBandwidthId": bandwidth_id})
334
+ quote = call(state, "GetCompShareShareBandwidthRefundPrice", params)
335
+ plan = {"dry_run": dry_run, "request": params, "refund": quote}
336
+ fields = [
337
+ ("BANDWIDTH ID", bandwidth_id),
338
+ ("ZONE", zone),
339
+ ("REFUND", _cny(quote.get("RefundPrice"), signed=True)),
340
+ ("ACTION", tr("Migrate bound EIPs to shared bandwidth, then delete")),
341
+ ]
342
+ if dry_run:
343
+ Renderer(state.json_output, state.show_sensitive).details(
344
+ "Operation plan", fields, response=plan
345
+ )
346
+ return
347
+ confirm_details(state, "Operation plan", fields, "Confirm this operation?", yes)
348
+ deleted = call(state, "DeleteCompShareShareBandwidth", params)
349
+ Renderer(state.json_output, state.show_sensitive).details(
350
+ "Operation completed",
351
+ [
352
+ ("BANDWIDTH ID", bandwidth_id),
353
+ ("MIGRATED EIPS", deleted.get("MigratedEIPCount", 0)),
354
+ ],
355
+ response={"refund": quote, "deleted": deleted},
356
+ )
357
+
358
+
359
+ @app.command("switch", help="Switch instance EIPs between dedicated and shared bandwidth.")
360
+ def switch(
361
+ ctx: typer.Context,
362
+ instances: List[str] = typer.Argument(..., help="Instance IDs."),
363
+ target: str = typer.Option(..., "--to", help="Target bandwidth: dedicated or shared."),
364
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
365
+ ) -> None:
366
+ state = runtime(ctx)
367
+ target_scope, target_name = _target_scope(target)
368
+ requested = list(dict.fromkeys(instances))
369
+ locations, missing = locate_instances(state, requested)
370
+ confirm_details(
371
+ state,
372
+ "Operation plan",
373
+ [("INSTANCE", requested), ("TARGET", tr(target_name))],
374
+ "Confirm this operation?",
375
+ yes,
376
+ )
377
+
378
+ results: List[Dict[str, Any]] = [
379
+ {
380
+ "UHostId": instance,
381
+ "Success": False,
382
+ "Message": tr("Instance {instance} was not found.", instance=instance),
383
+ }
384
+ for instance in missing
385
+ ]
386
+ groups: Dict[Tuple[str, str], List[str]] = {}
387
+ for instance in requested:
388
+ location = locations.get(instance)
389
+ if location is not None:
390
+ groups.setdefault((location[0], location[1]), []).append(instance)
391
+
392
+ for (region, zone), ids in groups.items():
393
+ response, error = call_captured(
394
+ state,
395
+ "SwitchCompShareEIPShareBandwidth",
396
+ _request(
397
+ ctx,
398
+ region,
399
+ zone,
400
+ {"UHostIds": ids, "TargetScope": target_scope},
401
+ ),
402
+ )
403
+ if error:
404
+ results.extend(
405
+ {
406
+ "UHostId": instance,
407
+ "Region": region,
408
+ "Zone": zone,
409
+ "Success": False,
410
+ "Message": error.get("message"),
411
+ }
412
+ for instance in ids
413
+ )
414
+ continue
415
+ returned = {
416
+ str(item.get("UHostId")): item for item in (response or {}).get("Results") or []
417
+ }
418
+ for instance in ids:
419
+ item = returned.get(instance) or {
420
+ "UHostId": instance,
421
+ "Success": False,
422
+ "Message": tr(
423
+ "The API returned no result for instance {instance}.", instance=instance
424
+ ),
425
+ }
426
+ results.append({**item, "Region": region, "Zone": zone})
427
+
428
+ failed = [item for item in results if not item.get("Success")]
429
+ payload: Dict[str, Any] = {"Results": results}
430
+ if failed:
431
+ payload.update(
432
+ {
433
+ "ok": False,
434
+ "error": {
435
+ "code": "partial_failure",
436
+ "message": tr("Some bandwidth switches failed."),
437
+ },
438
+ }
439
+ )
440
+ Renderer(state.json_output, state.show_sensitive).data(
441
+ payload,
442
+ rows=results,
443
+ columns=(
444
+ ("UHostId", "INSTANCE"),
445
+ ("Zone", "ZONE"),
446
+ ("Success", "SUCCESS"),
447
+ ("CurrentScope", "BANDWIDTH SCOPE"),
448
+ ("Message", "MESSAGE"),
449
+ ),
450
+ )
451
+ if failed:
452
+ raise typer.Exit(1)