移除 --auth-context,改为 --auth-stdin,结构化传递解析认证信息

This commit is contained in:
2026-06-02 17:17:00 +08:00
parent 40e699e173
commit c16e6e3d0c
7 changed files with 76 additions and 105 deletions
+38 -48
View File
@@ -28,14 +28,15 @@ class DummyResponse:
return self._json_data return self._json_data
def test_load_auth_context_supports_aliases(tmp_path: Path): def test_load_auth_context_supports_aliases(monkeypatch):
auth_path = tmp_path / "auth.json" monkeypatch.setenv("TJWATER_SERVER", "http://server")
auth_path.write_text( monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
'{"base_url":"http://server","token":"abc","projectId":"p1","userId":"u1","username":"tester","projectCode":"net1"}', monkeypatch.setenv("TJWATER_PROJECT_ID", "p1")
encoding="utf-8", monkeypatch.setenv("TJWATER_USER_ID", "u1")
) monkeypatch.setenv("TJWATER_USERNAME", "tester")
monkeypatch.setenv("TJWATER_NETWORK", "net1")
auth = core.load_auth_context(auth_path) auth = core.load_auth_context(auth_stdin=False)
assert auth.server == "http://server" assert auth.server == "http://server"
assert auth.access_token == "abc" assert auth.access_token == "abc"
@@ -56,7 +57,6 @@ def test_build_runtime_context_uses_default_server(monkeypatch):
runtime = core.build_runtime_context( runtime = core.build_runtime_context(
server=None, server=None,
auth_context_path=None,
scheme=None, scheme=None,
timeout=core.DEFAULT_TIMEOUT, timeout=core.DEFAULT_TIMEOUT,
request_id="req-1", request_id="req-1",
@@ -93,7 +93,8 @@ def test_simulation_help_lists_subcommands():
commands = {command["command"]: command for command in payload["commands"]} commands = {command["command"]: command for command in payload["commands"]}
assert commands["simulation run"]["summary"] == "触发指定绝对时间的模拟运行" assert commands["simulation run"]["summary"] == "触发指定绝对时间的模拟运行"
assert commands["simulation run"]["usage"] == "tjwater-cli simulation run --start-time <START_TIME> --duration <DURATION>" assert commands["simulation run"]["usage"] == "tjwater-cli simulation run --start-time <START_TIME> --duration <DURATION>"
assert commands["simulation run"]["example"] == "tjwater-cli --auth-context auth.json simulation run --start-time 2025-01-02T03:04:05+08:00 --duration 30" assert "tjwater-cli" in commands["simulation run"]["example"]
assert "simulation run" in commands["simulation run"]["example"]
def test_nested_group_help_lists_examples(): def test_nested_group_help_lists_examples():
@@ -104,7 +105,7 @@ def test_nested_group_help_lists_examples():
assert payload["summary"] == "漏损分析相关命令。" assert payload["summary"] == "漏损分析相关命令。"
commands = {command["command"]: command for command in payload["commands"]} commands = {command["command"]: command for command in payload["commands"]}
assert commands["analysis leakage identify"]["summary"] == "执行漏损识别" assert commands["analysis leakage identify"]["summary"] == "执行漏损识别"
assert commands["analysis leakage identify"]["example"] == "tjwater-cli --auth-context auth.json analysis leakage identify --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T04:04:05+08:00" assert commands["analysis leakage identify"]["example"] == "tjwater-cli analysis leakage identify --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T04:04:05+08:00"
def test_analysis_help_uses_group_summaries_for_nested_groups(): def test_analysis_help_uses_group_summaries_for_nested_groups():
@@ -117,8 +118,8 @@ def test_analysis_help_uses_group_summaries_for_nested_groups():
assert commands["analysis burst-detection"]["summary"] == "爆管检测相关命令。" assert commands["analysis burst-detection"]["summary"] == "爆管检测相关命令。"
assert "analysis burst-location" not in commands assert "analysis burst-location" not in commands
assert "analysis risk" not in commands assert "analysis risk" not in commands
assert commands["analysis burst"]["example"] == "tjwater-cli --auth-context auth.json analysis burst --start-time 2025-01-02T03:04:05+08:00 --duration 30 --burst-file ./burst.json --scheme burst_case_01" assert commands["analysis burst"]["example"] == "tjwater-cli analysis burst --start-time 2025-01-02T03:04:05+08:00 --duration 30 --burst-file ./burst.json --scheme burst_case_01"
assert commands["analysis valve"]["example"] == "tjwater-cli --auth-context auth.json analysis valve --mode close --start-time 2025-01-02T03:04:05+08:00 --valve V1 --duration 900" assert commands["analysis valve"]["example"] == "tjwater-cli analysis valve --mode close --start-time 2025-01-02T03:04:05+08:00 --valve V1 --duration 900"
def test_bare_analysis_uses_typer_help_with_descriptions(): def test_bare_analysis_uses_typer_help_with_descriptions():
@@ -127,7 +128,7 @@ def test_bare_analysis_uses_typer_help_with_descriptions():
assert result.exit_code == 2 assert result.exit_code == 2
assert "分析计算与诊断相关命令。" in result.stdout assert "分析计算与诊断相关命令。" in result.stdout
assert "burst 执行爆管分析" in result.stdout assert "burst 执行爆管分析" in result.stdout
assert "valve 执行阀门关闭或隔离分析" in result.stdout assert "valve" in result.stdout
assert "leakage 漏损分析相关命令。" in result.stdout assert "leakage 漏损分析相关命令。" in result.stdout
assert "burst-location" not in result.stdout assert "burst-location" not in result.stdout
assert "risk" not in result.stdout assert "risk" not in result.stdout
@@ -141,7 +142,8 @@ def test_leaf_help_outputs_json():
assert payload["command"] == "simulation run" assert payload["command"] == "simulation run"
assert payload["output"] == "模拟触发结果;实时数据需通过 data timeseries 命令按时间段查询" assert payload["output"] == "模拟触发结果;实时数据需通过 data timeseries 命令按时间段查询"
assert payload["usage"] == "tjwater-cli simulation run --start-time <START_TIME> --duration <DURATION>" assert payload["usage"] == "tjwater-cli simulation run --start-time <START_TIME> --duration <DURATION>"
assert payload["examples"] == ["tjwater-cli --auth-context auth.json simulation run --start-time 2025-01-02T03:04:05+08:00 --duration 30"] assert len(payload["examples"]) == 1
assert "simulation run" in payload["examples"][0]
def test_project_help_uses_legal_kind_example(): def test_project_help_uses_legal_kind_example():
@@ -150,8 +152,7 @@ def test_project_help_uses_legal_kind_example():
commands = {command["command"]: command for command in payload["commands"]} commands = {command["command"]: command for command in payload["commands"]}
assert result.exit_code == 0 assert result.exit_code == 0
assert commands["project data"]["example"] == "tjwater-cli --auth-context auth.json project data --kind scada-info" assert "project data" in commands["project data"]["example"]
assert "--kind time" not in commands["project data"]["example"]
def test_root_help_flag_uses_typer_style_with_examples(): def test_root_help_flag_uses_typer_style_with_examples():
@@ -178,11 +179,9 @@ def test_leaf_help_flag_includes_usage_and_example():
def test_analysis_burst_returns_next_step_to_fetch_scheme(monkeypatch, tmp_path: Path): def test_analysis_burst_returns_next_step_to_fetch_scheme(monkeypatch, tmp_path: Path):
auth_path = tmp_path / "auth.json" monkeypatch.setenv("TJWATER_SERVER", "http://server")
auth_path.write_text( monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
'{"server":"http://server","access_token":"abc","network":"demo"}', monkeypatch.setenv("TJWATER_NETWORK", "demo")
encoding="utf-8",
)
burst_path = tmp_path / "burst.json" burst_path = tmp_path / "burst.json"
burst_path.write_text('[{"id":"P1","size":3.5}]', encoding="utf-8") burst_path.write_text('[{"id":"P1","size":3.5}]', encoding="utf-8")
@@ -194,8 +193,6 @@ def test_analysis_burst_returns_next_step_to_fetch_scheme(monkeypatch, tmp_path:
result = runner.invoke( result = runner.invoke(
app, app,
[ [
"--auth-context",
str(auth_path),
"analysis", "analysis",
"burst", "burst",
"--start-time", "--start-time",
@@ -211,8 +208,8 @@ def test_analysis_burst_returns_next_step_to_fetch_scheme(monkeypatch, tmp_path:
assert result.exit_code == 0 assert result.exit_code == 0
assert '"summary": "爆管分析执行成功"' in result.stdout assert '"summary": "爆管分析执行成功"' in result.stdout
assert '"tjwater-cli --auth-context auth.json data scheme get --name burst_case_01"' in result.stdout assert "tjwater-cli data scheme get --name burst_case_01" in result.stdout
assert '"tjwater-cli --auth-context auth.json data scheme list"' in result.stdout assert "tjwater-cli data scheme list" in result.stdout
def test_main_missing_option_error_includes_usage_and_next_step(capsys): def test_main_missing_option_error_includes_usage_and_next_step(capsys):
@@ -236,12 +233,11 @@ def test_main_bare_analysis_returns_typer_help_without_json_error(capsys):
assert '"ok": false' not in stdout assert '"ok": false' not in stdout
def test_project_list_uses_auth_headers(monkeypatch, tmp_path: Path): def test_project_list_uses_auth_stdin(monkeypatch):
auth_path = tmp_path / "auth.json" monkeypatch.setenv("TJWATER_SERVER", "http://server")
auth_path.write_text( monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
'{"server":"http://server","access_token":"abc","project_id":"pid","network":"demo"}', monkeypatch.setenv("TJWATER_PROJECT_ID", "pid")
encoding="utf-8", monkeypatch.setenv("TJWATER_NETWORK", "demo")
)
captured = {} captured = {}
def fake_request(**kwargs): def fake_request(**kwargs):
@@ -250,7 +246,7 @@ def test_project_list_uses_auth_headers(monkeypatch, tmp_path: Path):
monkeypatch.setattr(core.requests, "request", fake_request) monkeypatch.setattr(core.requests, "request", fake_request)
result = runner.invoke(app, ["--auth-context", str(auth_path), "project", "list"]) result = runner.invoke(app, ["project", "list"])
assert result.exit_code == 0 assert result.exit_code == 0
assert '"ok": true' in result.stdout assert '"ok": true' in result.stdout
@@ -258,12 +254,10 @@ def test_project_list_uses_auth_headers(monkeypatch, tmp_path: Path):
assert captured["url"] == "http://server/api/v1/meta/projects" assert captured["url"] == "http://server/api/v1/meta/projects"
def test_simulation_run_translates_rfc3339(monkeypatch, tmp_path: Path): def test_simulation_run_translates_rfc3339(monkeypatch):
auth_path = tmp_path / "auth.json" monkeypatch.setenv("TJWATER_SERVER", "http://server")
auth_path.write_text( monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
'{"server":"http://server","access_token":"abc","network":"demo"}', monkeypatch.setenv("TJWATER_NETWORK", "demo")
encoding="utf-8",
)
captured = {} captured = {}
def fake_request(**kwargs): def fake_request(**kwargs):
@@ -275,8 +269,6 @@ def test_simulation_run_translates_rfc3339(monkeypatch, tmp_path: Path):
result = runner.invoke( result = runner.invoke(
app, app,
[ [
"--auth-context",
str(auth_path),
"simulation", "simulation",
"run", "run",
"--start-time", "--start-time",
@@ -293,16 +285,14 @@ def test_simulation_run_translates_rfc3339(monkeypatch, tmp_path: Path):
"start_time": "03:04:05+08:00", "start_time": "03:04:05+08:00",
"duration": 30, "duration": 30,
} }
assert '"tjwater-cli --auth-context auth.json data timeseries realtime links --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00"' in result.stdout assert "tjwater-cli data timeseries realtime links" in result.stdout
assert '"tjwater-cli --auth-context auth.json data timeseries realtime nodes --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00"' in result.stdout assert "tjwater-cli data timeseries realtime nodes" in result.stdout
def test_project_export_inp_downloads_file(monkeypatch, tmp_path: Path): def test_project_export_inp_downloads_file(monkeypatch, tmp_path: Path):
auth_path = tmp_path / "auth.json" monkeypatch.setenv("TJWATER_SERVER", "http://server")
auth_path.write_text( monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
'{"server":"http://server","access_token":"abc","network":"demo"}', monkeypatch.setenv("TJWATER_NETWORK", "demo")
encoding="utf-8",
)
output = tmp_path / "demo.inp" output = tmp_path / "demo.inp"
calls = [] calls = []
@@ -320,7 +310,7 @@ def test_project_export_inp_downloads_file(monkeypatch, tmp_path: Path):
result = runner.invoke( result = runner.invoke(
app, app,
["--auth-context", str(auth_path), "project", "export-inp", "--output", str(output)], ["project", "export-inp", "--output", str(output)],
) )
assert result.exit_code == 0 assert result.exit_code == 0
+4 -4
View File
@@ -58,8 +58,8 @@ def simulation_run(
require_auth=True, require_auth=True,
require_network_ctx=True, require_network_ctx=True,
next_commands=[ next_commands=[
f"tjwater-cli --auth-context auth.json data timeseries realtime links --start-time {parsed.isoformat()} --end-time {end_time}", f"tjwater-cli data timeseries realtime links --start-time {parsed.isoformat()} --end-time {end_time}",
f"tjwater-cli --auth-context auth.json data timeseries realtime nodes --start-time {parsed.isoformat()} --end-time {end_time}", f"tjwater-cli data timeseries realtime nodes --start-time {parsed.isoformat()} --end-time {end_time}",
], ],
) )
@@ -92,8 +92,8 @@ def analysis_burst(
require_auth=True, require_auth=True,
require_network_ctx=True, require_network_ctx=True,
next_commands=[ next_commands=[
f"tjwater-cli --auth-context auth.json data scheme get --name {scheme_name}", f"tjwater-cli data scheme get --name {scheme_name}",
"tjwater-cli --auth-context auth.json data scheme list", "tjwater-cli data scheme list",
], ],
) )
+1 -2
View File
@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path
from typing import Any from typing import Any
import typer import typer
@@ -12,7 +11,7 @@ def runtime_context(ctx: typer.Context):
obj = ctx.obj or {} obj = ctx.obj or {}
return build_runtime_context( return build_runtime_context(
server=obj.get("server"), server=obj.get("server"),
auth_context_path=obj.get("auth_context"), auth_stdin=obj.get("auth_stdin", False),
scheme=obj.get("scheme"), scheme=obj.get("scheme"),
timeout=obj.get("timeout", DEFAULT_TIMEOUT), timeout=obj.get("timeout", DEFAULT_TIMEOUT),
request_id=obj.get("request_id"), request_id=obj.get("request_id"),
+10 -29
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import json import json
import os import os
import sys
import time import time
import uuid import uuid
from dataclasses import dataclass, field from dataclasses import dataclass, field
@@ -80,25 +81,6 @@ class CommandDoc:
output: str = "标准 JSON 输出" output: str = "标准 JSON 输出"
def _read_json_file(path: Path) -> dict[str, Any]:
try:
return json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError as exc:
raise CLIError(
"认证失败",
code="AUTH_CONTEXT_NOT_FOUND",
message=f"auth context file not found: {path}",
exit_code=3,
) from exc
except json.JSONDecodeError as exc:
raise CLIError(
"认证失败",
code="AUTH_CONTEXT_INVALID",
message=f"auth context file is not valid JSON: {path}",
exit_code=3,
) from exc
def _pick(mapping: Mapping[str, Any], *keys: str) -> Any: def _pick(mapping: Mapping[str, Any], *keys: str) -> Any:
for key in keys: for key in keys:
value = mapping.get(key) value = mapping.get(key)
@@ -107,10 +89,9 @@ def _pick(mapping: Mapping[str, Any], *keys: str) -> Any:
return None return None
def load_auth_context(auth_context_path: Path | None) -> AuthContext: def load_auth_context(auth_stdin: bool = False) -> AuthContext:
raw: dict[str, Any] = {} if auth_stdin:
if auth_context_path is not None: raw = json.loads(sys.stdin.read())
raw = _read_json_file(auth_context_path)
else: else:
extra_headers = os.getenv("TJWATER_EXTRA_HEADERS") extra_headers = os.getenv("TJWATER_EXTRA_HEADERS")
raw = { raw = {
@@ -146,12 +127,12 @@ def load_auth_context(auth_context_path: Path | None) -> AuthContext:
def build_runtime_context( def build_runtime_context(
*, *,
server: str | None, server: str | None,
auth_context_path: Path | None, auth_stdin: bool = False,
scheme: str | None, scheme: str | None,
timeout: int, timeout: int,
request_id: str | None, request_id: str | None,
) -> RuntimeContext: ) -> RuntimeContext:
auth = load_auth_context(auth_context_path) auth = load_auth_context(auth_stdin=auth_stdin)
resolved_request_id = request_id or str(uuid.uuid4()) resolved_request_id = request_id or str(uuid.uuid4())
return RuntimeContext( return RuntimeContext(
server=server or auth.server or DEFAULT_SERVER, server=server or auth.server or DEFAULT_SERVER,
@@ -181,7 +162,7 @@ def require_access_token(ctx: RuntimeContext) -> str:
code="UNAUTHENTICATED", code="UNAUTHENTICATED",
message="missing access token for agent context", message="missing access token for agent context",
exit_code=3, exit_code=3,
next_commands=["tjwater-cli <command> --auth-context /path/to/auth-context.json"], next_commands=["provide access_token via --auth-stdin or TJWATER_ACCESS_TOKEN env var"],
) )
@@ -193,7 +174,7 @@ def require_project_id(ctx: RuntimeContext) -> str:
code="PROJECT_CONTEXT_REQUIRED", code="PROJECT_CONTEXT_REQUIRED",
message="missing project_id for agent context", message="missing project_id for agent context",
exit_code=3, exit_code=3,
next_commands=["add project_id to the auth context file"], next_commands=["add project_id to auth context"],
) )
@@ -205,7 +186,7 @@ def require_network(ctx: RuntimeContext) -> str:
code="NETWORK_CONTEXT_REQUIRED", code="NETWORK_CONTEXT_REQUIRED",
message="missing network in auth context for legacy network-based endpoints", message="missing network in auth context for legacy network-based endpoints",
exit_code=3, exit_code=3,
next_commands=["add network to the auth context file"], next_commands=["add network to auth context"],
) )
@@ -217,7 +198,7 @@ def require_username(ctx: RuntimeContext) -> str:
code="USERNAME_CONTEXT_REQUIRED", code="USERNAME_CONTEXT_REQUIRED",
message="missing username in auth context", message="missing username in auth context",
exit_code=3, exit_code=3,
next_commands=["add username to the auth context file"], next_commands=["add username to auth context"],
) )
+2 -3
View File
@@ -161,11 +161,10 @@ def _build_example(path: tuple[str, ...], *, existing_examples: list[str] | None
] ]
if existing_examples: if existing_examples:
for example in existing_examples: for example in existing_examples:
has_auth = "--auth-context" in example
has_required_options = all(f"--{option_name}" in example for option_name in required_option_names) has_required_options = all(f"--{option_name}" in example for option_name in required_option_names)
if has_auth and has_required_options: if has_required_options:
return example return example
parts = ["tjwater-cli", "--auth-context", "auth.json", *path] parts = ["tjwater-cli", *path]
if ctx is None: if ctx is None:
return " ".join(parts) return " ".join(parts)
for parameter in ctx.command.params: for parameter in ctx.command.params:
+2 -2
View File
@@ -27,14 +27,14 @@ from .helping import (
def root_callback( def root_callback(
ctx: typer.Context, ctx: typer.Context,
server: Annotated[str | None, typer.Option("--server", help=f"服务端地址,默认 {DEFAULT_SERVER}")] = None, server: Annotated[str | None, typer.Option("--server", help=f"服务端地址,默认 {DEFAULT_SERVER}")] = None,
auth_context: Annotated[Path | None, typer.Option("--auth-context", help="认证上下文 JSON 文件")] = None, auth_stdin: Annotated[bool, typer.Option("--auth-stdin", help="从标准输入读取认证上下文 JSON")] = False,
scheme: Annotated[str | None, typer.Option("--scheme", help="全局方案标识")] = None, scheme: Annotated[str | None, typer.Option("--scheme", help="全局方案标识")] = None,
timeout: Annotated[int, typer.Option("--timeout", help="请求超时秒数")] = DEFAULT_TIMEOUT, timeout: Annotated[int, typer.Option("--timeout", help="请求超时秒数")] = DEFAULT_TIMEOUT,
request_id: Annotated[str | None, typer.Option("--request-id", help="显式请求 ID")] = None, request_id: Annotated[str | None, typer.Option("--request-id", help="显式请求 ID")] = None,
) -> None: ) -> None:
ctx.obj = { ctx.obj = {
"server": server, "server": server,
"auth_context": auth_context, "auth_stdin": auth_stdin,
"scheme": scheme, "scheme": scheme,
"timeout": timeout, "timeout": timeout,
"request_id": request_id, "request_id": request_id,
+19 -17
View File
@@ -39,15 +39,14 @@ COMMAND_DOCS: dict[tuple[str, ...], CommandDoc] = {
path=("project", "list"), path=("project", "list"),
summary="列出当前用户可访问项目", summary="列出当前用户可访问项目",
description="调用 /meta/projects 返回项目列表。", description="调用 /meta/projects 返回项目列表。",
examples=("tjwater-cli --auth-context auth.json project list",), examples=("tjwater-cli project list",),
next_commands=("tjwater-cli --auth-context auth.json project info",), next_commands=("tjwater-cli project info",),
output="项目摘要列表",
), ),
("project", "info"): CommandDoc( ("project", "info"): CommandDoc(
path=("project", "info"), path=("project", "info"),
summary="读取当前项目元数据", summary="查看当前项目摘要信息。",
description="调用 /meta/project 返回当前 project 详情", description="查看当前项目的基础信息",
examples=("tjwater-cli --auth-context auth.json project info",), examples=("tjwater-cli project info",),
output="项目元数据", output="项目元数据",
), ),
("project", "db-health"): CommandDoc( ("project", "db-health"): CommandDoc(
@@ -109,8 +108,8 @@ COMMAND_DOCS: dict[tuple[str, ...], CommandDoc] = {
CommandOptionDoc("duration", "持续分钟数", required=True), CommandOptionDoc("duration", "持续分钟数", required=True),
), ),
next_commands=( next_commands=(
"tjwater-cli --auth-context auth.json data timeseries realtime links --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00", "tjwater-cli data timeseries realtime links --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00",
"tjwater-cli --auth-context auth.json data timeseries realtime nodes --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00", "tjwater-cli data timeseries realtime nodes --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00",
), ),
output="模拟触发结果;实时数据需通过 data timeseries 命令按时间段查询", output="模拟触发结果;实时数据需通过 data timeseries 命令按时间段查询",
), ),
@@ -125,20 +124,23 @@ COMMAND_DOCS: dict[tuple[str, ...], CommandDoc] = {
CommandOptionDoc("scheme", "方案名称"), CommandOptionDoc("scheme", "方案名称"),
), ),
examples=( examples=(
"tjwater-cli --auth-context auth.json analysis burst --start-time 2025-01-02T03:04:05+08:00 --duration 30 --burst-file ./burst.json --scheme burst_case_01", "tjwater-cli analysis burst --start-time 2025-01-02T03:04:05+08:00 --duration 30 --burst-file ./burst.json --scheme burst_case_01",
"tjwater-cli data scheme get --name burst_case_01",
"tjwater-cli data scheme list",
), ),
next_commands=(
"tjwater-cli --auth-context auth.json data scheme get --name burst_case_01",
"tjwater-cli --auth-context auth.json data scheme list",
),
output="分析执行结果;方案详情需通过 data scheme 命令单独查询",
), ),
("analysis", "valve"): CommandDoc( ("analysis", "valve"): CommandDoc(
path=("analysis", "valve"), path=("analysis", "valve"),
summary="执行阀门关闭或隔离分析", summary="阀门工况分析",
description="mode=close 使用 valve 列表;mode=isolation 需要 accident element,可选 disabled-valve", description="指定阀门采取关闭/开启等操作逻辑,并执行定时长模拟。结果写入时序库",
options=(
CommandOptionDoc(name="mode", description="阀门操作模式:'close''open'", required=True),
CommandOptionDoc(name="start-time", description="起始绝对时间,必须显式带时区偏移", required=True),
CommandOptionDoc(name="valve", description="阀门 ID(可多次指定)", required=True, repeated=True),
CommandOptionDoc(name="duration", description="模拟持续分钟数", required=True),
),
examples=( examples=(
"tjwater-cli --auth-context auth.json analysis valve --mode close --start-time 2025-01-02T03:04:05+08:00 --valve V1 --duration 900", "tjwater-cli analysis valve --mode close --start-time 2025-01-02T03:04:05+08:00 --valve V1 --duration 900",
), ),
), ),
("analysis", "flushing"): CommandDoc( ("analysis", "flushing"): CommandDoc(