优化 CLI 命令,增加获取所有节点和管道属性的功能
This commit is contained in:
@@ -97,14 +97,58 @@ def test_auth_stdin_can_be_reused_with_runtime_context_cache(monkeypatch):
|
||||
assert len(observed_runtime_ids) == 1
|
||||
|
||||
|
||||
def test_network_get_all_junction_properties_uses_network_context(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_request_json(ctx, **kwargs):
|
||||
captured["access_token"] = ctx.auth.access_token
|
||||
captured["params"] = kwargs["params"]
|
||||
return [{"id": "J1"}], 5
|
||||
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
|
||||
monkeypatch.setattr(common, "request_json", fake_request_json)
|
||||
|
||||
result = runner.invoke(app, ["network", "get-all-junction-properties"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert payload["ok"] is True
|
||||
assert payload["data"] == [{"id": "J1"}]
|
||||
assert captured == {"access_token": "abc", "params": {"network": "tjwater"}}
|
||||
|
||||
|
||||
def test_network_get_all_pipe_properties_uses_network_context(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_request_json(ctx, **kwargs):
|
||||
captured["access_token"] = ctx.auth.access_token
|
||||
captured["params"] = kwargs["params"]
|
||||
return [{"id": "P1"}], 5
|
||||
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
|
||||
monkeypatch.setattr(common, "request_json", fake_request_json)
|
||||
|
||||
result = runner.invoke(app, ["network", "get-all-pipe-properties"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert payload["ok"] is True
|
||||
assert payload["data"] == [{"id": "P1"}]
|
||||
assert captured == {"access_token": "abc", "params": {"network": "tjwater"}}
|
||||
|
||||
|
||||
def test_help_outputs_json_lists_commands():
|
||||
result = runner.invoke(app, ["help"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert payload["schema_version"] == "tjwater-cli/v1"
|
||||
assert any(command["command"] == "project" for command in payload["commands"])
|
||||
assert any(command["command"] == "analysis" for command in payload["commands"])
|
||||
assert all(command["command"] != "project" for command in payload["commands"])
|
||||
assert payload["menu_level"] == 1
|
||||
assert all(command["command"] != "project list" for command in payload["commands"])
|
||||
|
||||
@@ -137,7 +181,7 @@ def test_nested_group_help_lists_examples():
|
||||
assert payload["summary"] == "漏损分析相关命令。"
|
||||
commands = {command["command"]: command for command in payload["commands"]}
|
||||
assert commands["analysis leakage identify"]["summary"] == "执行漏损识别"
|
||||
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"
|
||||
assert commands["analysis leakage identify"]["example"] == "tjwater-cli analysis leakage identify --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --scheme leak_case_01"
|
||||
|
||||
|
||||
def test_analysis_help_uses_group_summaries_for_nested_groups():
|
||||
@@ -150,8 +194,8 @@ def test_analysis_help_uses_group_summaries_for_nested_groups():
|
||||
assert commands["analysis burst-detection"]["summary"] == "爆管检测相关命令。"
|
||||
assert "analysis burst-location" not in commands
|
||||
assert "analysis risk" not in commands
|
||||
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 analysis valve --mode close --start-time 2025-01-02T03:04:05+08:00 --valve V1 --duration 900"
|
||||
assert commands["analysis burst"]["example"] == "tjwater-cli analysis burst --start-time 2025-01-02T03:04:05+08:00 --duration 900 --burst-file ./burst.json --scheme burst_case_01"
|
||||
assert commands["analysis valve"]["example"] == "tjwater-cli analysis valve --mode close --start-time 2025-01-02T03:04:05+08:00 --valve V1 --valve V2 --duration 900 --scheme valve_case_01"
|
||||
|
||||
|
||||
def test_bare_analysis_uses_typer_help_with_descriptions():
|
||||
@@ -178,15 +222,6 @@ def test_leaf_help_outputs_json():
|
||||
assert "simulation run" in payload["examples"][0]
|
||||
|
||||
|
||||
def test_project_help_uses_legal_kind_example():
|
||||
result = runner.invoke(app, ["project", "help"])
|
||||
payload = json.loads(result.stdout)
|
||||
commands = {command["command"]: command for command in payload["commands"]}
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "project data" in commands["project data"]["example"]
|
||||
|
||||
|
||||
def test_root_help_flag_uses_typer_style_with_examples():
|
||||
result = runner.invoke(app, ["--help"], prog_name="tjwater-cli")
|
||||
|
||||
@@ -244,6 +279,214 @@ def test_analysis_burst_returns_next_step_to_fetch_scheme(monkeypatch, tmp_path:
|
||||
assert "tjwater-cli data scheme list" in result.stdout
|
||||
|
||||
|
||||
def test_analysis_contaminant_sends_required_scheme_name(monkeypatch):
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "demo")
|
||||
captured = {}
|
||||
|
||||
def fake_request(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return DummyResponse(text="success", headers={"content-type": "text/plain"})
|
||||
|
||||
monkeypatch.setattr(core.requests, "request", fake_request)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"analysis",
|
||||
"contaminant",
|
||||
"--start-time",
|
||||
"2025-01-02T03:04:05+08:00",
|
||||
"--duration",
|
||||
"900",
|
||||
"--source-node",
|
||||
"N1",
|
||||
"--concentration",
|
||||
"10.0",
|
||||
"--scheme",
|
||||
"contam_case_01",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert captured["params"] == {
|
||||
"network": "demo",
|
||||
"start_time": "2025-01-02T03:04:05+08:00",
|
||||
"source": "N1",
|
||||
"concentration": 10.0,
|
||||
"duration": 900,
|
||||
"scheme_name": "contam_case_01",
|
||||
}
|
||||
|
||||
|
||||
def test_analysis_flushing_sends_required_scheme_name(monkeypatch, tmp_path: Path):
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "demo")
|
||||
captured = {}
|
||||
valve_path = tmp_path / "valve.json"
|
||||
valve_path.write_text('[{"valve":"V1","opening":0.5}]', encoding="utf-8")
|
||||
|
||||
def fake_request(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return DummyResponse(text="success", headers={"content-type": "text/plain"})
|
||||
|
||||
monkeypatch.setattr(core.requests, "request", fake_request)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"analysis",
|
||||
"flushing",
|
||||
"--start-time",
|
||||
"2025-01-02T03:04:05+08:00",
|
||||
"--valve-setting-file",
|
||||
str(valve_path),
|
||||
"--drainage-node",
|
||||
"N1",
|
||||
"--flow",
|
||||
"100.0",
|
||||
"--duration",
|
||||
"900",
|
||||
"--scheme",
|
||||
"flush_case_01",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert captured["params"] == {
|
||||
"network": "demo",
|
||||
"start_time": "2025-01-02T03:04:05+08:00",
|
||||
"valves": ["V1"],
|
||||
"valves_k": [0.5],
|
||||
"drainage_node_ID": "N1",
|
||||
"flush_flow": 100.0,
|
||||
"duration": 900,
|
||||
"scheme_name": "flush_case_01",
|
||||
}
|
||||
|
||||
|
||||
def test_analysis_valve_close_sends_required_scheme_name(monkeypatch):
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "demo")
|
||||
captured = {}
|
||||
|
||||
def fake_request(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return DummyResponse(text="success", headers={"content-type": "text/plain"})
|
||||
|
||||
monkeypatch.setattr(core.requests, "request", fake_request)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"analysis",
|
||||
"valve",
|
||||
"--mode",
|
||||
"close",
|
||||
"--start-time",
|
||||
"2025-01-02T03:04:05+08:00",
|
||||
"--valve",
|
||||
"V1",
|
||||
"--duration",
|
||||
"900",
|
||||
"--scheme",
|
||||
"valve_case_01",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert captured["params"] == {
|
||||
"network": "demo",
|
||||
"start_time": "2025-01-02T03:04:05+08:00",
|
||||
"valves": ["V1"],
|
||||
"duration": 900,
|
||||
"scheme_name": "valve_case_01",
|
||||
}
|
||||
|
||||
|
||||
def test_analysis_contaminant_requires_scheme(monkeypatch, capsys):
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "demo")
|
||||
|
||||
exit_code = main(
|
||||
[
|
||||
"analysis",
|
||||
"contaminant",
|
||||
"--start-time",
|
||||
"2025-01-02T03:04:05+08:00",
|
||||
"--duration",
|
||||
"900",
|
||||
"--source-node",
|
||||
"N1",
|
||||
"--concentration",
|
||||
"10.0",
|
||||
],
|
||||
)
|
||||
|
||||
stdout = capsys.readouterr().out
|
||||
|
||||
assert exit_code == 2
|
||||
assert '"code": "SCHEME_REQUIRED"' in stdout
|
||||
|
||||
|
||||
def test_analysis_flushing_requires_scheme(monkeypatch, tmp_path: Path, capsys):
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "demo")
|
||||
valve_path = tmp_path / "valve.json"
|
||||
valve_path.write_text('[{"valve":"V1","opening":0.5}]', encoding="utf-8")
|
||||
|
||||
exit_code = main(
|
||||
[
|
||||
"analysis",
|
||||
"flushing",
|
||||
"--start-time",
|
||||
"2025-01-02T03:04:05+08:00",
|
||||
"--valve-setting-file",
|
||||
str(valve_path),
|
||||
"--drainage-node",
|
||||
"N1",
|
||||
"--flow",
|
||||
"100.0",
|
||||
],
|
||||
)
|
||||
|
||||
stdout = capsys.readouterr().out
|
||||
|
||||
assert exit_code == 2
|
||||
assert '"code": "SCHEME_REQUIRED"' in stdout
|
||||
|
||||
|
||||
def test_analysis_valve_close_requires_scheme(monkeypatch, capsys):
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "demo")
|
||||
|
||||
exit_code = main(
|
||||
[
|
||||
"analysis",
|
||||
"valve",
|
||||
"--mode",
|
||||
"close",
|
||||
"--start-time",
|
||||
"2025-01-02T03:04:05+08:00",
|
||||
"--valve",
|
||||
"V1",
|
||||
"--duration",
|
||||
"900",
|
||||
],
|
||||
)
|
||||
|
||||
stdout = capsys.readouterr().out
|
||||
|
||||
assert exit_code == 2
|
||||
assert '"code": "SCHEME_REQUIRED"' in stdout
|
||||
|
||||
|
||||
def test_main_missing_option_error_includes_usage_and_next_step(capsys):
|
||||
exit_code = main(["simulation", "run"])
|
||||
stdout = capsys.readouterr().out
|
||||
@@ -265,27 +508,6 @@ def test_main_bare_analysis_returns_typer_help_without_json_error(capsys):
|
||||
assert '"ok": false' not in stdout
|
||||
|
||||
|
||||
def test_project_list_uses_auth_stdin(monkeypatch):
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_PROJECT_ID", "pid")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "demo")
|
||||
captured = {}
|
||||
|
||||
def fake_request(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return DummyResponse(json_data=[{"project_id": "pid", "name": "Demo"}])
|
||||
|
||||
monkeypatch.setattr(core.requests, "request", fake_request)
|
||||
|
||||
result = runner.invoke(app, ["project", "list"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert '"ok": true' in result.stdout
|
||||
assert captured["headers"]["Authorization"] == "Bearer abc"
|
||||
assert captured["url"] == "http://server/api/v1/meta/projects"
|
||||
|
||||
|
||||
def test_simulation_run_translates_rfc3339(monkeypatch):
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
@@ -320,33 +542,9 @@ def test_simulation_run_translates_rfc3339(monkeypatch):
|
||||
assert "tjwater-cli data timeseries realtime nodes" in result.stdout
|
||||
|
||||
|
||||
def test_project_export_inp_downloads_file(monkeypatch, tmp_path: Path):
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "demo")
|
||||
output = tmp_path / "demo.inp"
|
||||
calls = []
|
||||
def test_removed_project_command_returns_not_found(capsys):
|
||||
exit_code = main(["project", "list"])
|
||||
stdout = capsys.readouterr().out
|
||||
|
||||
def fake_request(**kwargs):
|
||||
calls.append(kwargs["url"])
|
||||
if kwargs["url"].endswith("/dumpinp/"):
|
||||
return DummyResponse(json_data=True)
|
||||
return DummyResponse(
|
||||
headers={"content-type": "application/octet-stream"},
|
||||
content=b"inp-content",
|
||||
text="inp-content",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(core.requests, "request", fake_request)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["project", "export-inp", "--output", str(output)],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output.read_bytes() == b"inp-content"
|
||||
assert calls == [
|
||||
"http://server/api/v1/dumpinp/",
|
||||
"http://server/api/v1/downloadinp/",
|
||||
]
|
||||
assert exit_code == 2
|
||||
assert '"code": "COMMAND_NOT_FOUND"' in stdout or "No such command: project" in stdout
|
||||
|
||||
@@ -5,7 +5,6 @@ import typer
|
||||
from .formatters import TJWaterGroup
|
||||
|
||||
app = typer.Typer(help="TJWater agent CLI", add_completion=False, no_args_is_help=True, cls=TJWaterGroup)
|
||||
project_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
network_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
component_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
component_option_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
@@ -30,7 +29,6 @@ data_scheme_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
data_extension_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
data_misc_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
|
||||
app.add_typer(project_app, name="project")
|
||||
app.add_typer(network_app, name="network")
|
||||
app.add_typer(component_app, name="component")
|
||||
component_app.add_typer(component_option_app, name="option")
|
||||
@@ -56,7 +54,6 @@ data_app.add_typer(data_extension_app, name="extension")
|
||||
data_app.add_typer(data_misc_app, name="misc")
|
||||
|
||||
GROUP_HELP_APPS: list[tuple[typer.Typer, tuple[str, ...]]] = [
|
||||
(project_app, ("project",)),
|
||||
(network_app, ("network",)),
|
||||
(component_app, ("component",)),
|
||||
(component_option_app, ("component", "option")),
|
||||
@@ -82,4 +79,4 @@ GROUP_HELP_APPS: list[tuple[typer.Typer, tuple[str, ...]]] = [
|
||||
(data_misc_app, ("data", "misc")),
|
||||
]
|
||||
|
||||
TOP_LEVEL_COMMANDS = {"help", "project", "network", "component", "simulation", "analysis", "data"}
|
||||
TOP_LEVEL_COMMANDS = {"help", "network", "component", "simulation", "analysis", "data"}
|
||||
|
||||
@@ -106,6 +106,7 @@ def analysis_valve(
|
||||
element: Annotated[list[str] | None, typer.Option("--element", help="isolation 模式的事故元素,可重复")] = None,
|
||||
disabled_valve: Annotated[list[str] | None, typer.Option("--disabled-valve", help="故障阀门,可重复")] = None,
|
||||
duration: Annotated[int | None, typer.Option("--duration", help="close 模式持续秒数")] = None,
|
||||
scheme: Annotated[str | None, typer.Option("--scheme", help="close 模式的方案名称")] = None,
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
network = require_network(runtime)
|
||||
@@ -122,6 +123,7 @@ def analysis_valve(
|
||||
"start_time": parse_time_with_timezone(start_time, option_name="--start-time").isoformat(),
|
||||
"valves": valve,
|
||||
"duration": duration or 900,
|
||||
"scheme_name": resolve_scheme(runtime, scheme, required=True),
|
||||
}
|
||||
emit_api(
|
||||
ctx,
|
||||
@@ -182,10 +184,8 @@ def analysis_flushing(
|
||||
"drainage_node_ID": drainage_node,
|
||||
"flush_flow": flow,
|
||||
"duration": duration or 900,
|
||||
"scheme_name": resolve_scheme(runtime, scheme, required=True),
|
||||
}
|
||||
scheme_name = resolve_scheme(runtime, scheme)
|
||||
if scheme_name:
|
||||
params["scheme_name"] = scheme_name
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="冲洗分析执行成功",
|
||||
@@ -236,10 +236,8 @@ def analysis_contaminant(
|
||||
"source": source_node,
|
||||
"concentration": concentration,
|
||||
"duration": duration,
|
||||
"scheme_name": resolve_scheme(runtime, scheme, required=True),
|
||||
}
|
||||
scheme_name = resolve_scheme(runtime, scheme)
|
||||
if scheme_name:
|
||||
params["scheme_name"] = scheme_name
|
||||
if pattern:
|
||||
params["pattern"] = pattern
|
||||
emit_api(
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any
|
||||
|
||||
import typer
|
||||
|
||||
from .apps import component_option_app, network_app, project_app
|
||||
from .common import emit_api, runtime_context
|
||||
from .core import CLIError, emit_success, request_bytes, request_json, require_network
|
||||
|
||||
|
||||
@project_app.command("list")
|
||||
def project_list(ctx: typer.Context) -> None:
|
||||
emit_api(ctx, summary="读取项目列表成功", method="GET", path="/meta/projects", require_auth=True)
|
||||
|
||||
|
||||
@project_app.command("info")
|
||||
def project_info(ctx: typer.Context) -> None:
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取项目信息成功",
|
||||
method="GET",
|
||||
path="/meta/project",
|
||||
require_auth=True,
|
||||
require_project=True,
|
||||
)
|
||||
|
||||
|
||||
@project_app.command("db-health")
|
||||
def project_db_health(ctx: typer.Context) -> None:
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取数据库健康状态成功",
|
||||
method="GET",
|
||||
path="/meta/db/health",
|
||||
require_auth=True,
|
||||
require_project=True,
|
||||
)
|
||||
|
||||
|
||||
@project_app.command("data")
|
||||
def project_data(
|
||||
ctx: typer.Context,
|
||||
kind: Annotated[str, typer.Option("--kind", help="scada-info|scheme-list|burst-locate-result")],
|
||||
) -> None:
|
||||
kind_map = {
|
||||
"scada-info": "/scada-info",
|
||||
"scheme-list": "/scheme-list",
|
||||
"burst-locate-result": "/burst-locate-result",
|
||||
}
|
||||
path = kind_map.get(kind)
|
||||
if path is None:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="INVALID_KIND",
|
||||
message="kind must be one of: scada-info, scheme-list, burst-locate-result",
|
||||
exit_code=2,
|
||||
)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取项目数据成功",
|
||||
method="GET",
|
||||
path=path,
|
||||
require_auth=True,
|
||||
require_project=True,
|
||||
)
|
||||
|
||||
|
||||
@project_app.command("export-inp")
|
||||
def project_export_inp(
|
||||
ctx: typer.Context,
|
||||
output: Annotated[Path, typer.Option("--output", help="本地输出路径")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
network = require_network(runtime)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
temp_name = f"{output.stem}-{runtime.request_id}.inp"
|
||||
_, duration_dump = request_json(
|
||||
runtime,
|
||||
method="GET",
|
||||
path="/dumpinp/",
|
||||
params={"network": network, "inp": temp_name},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
content, duration_download = request_bytes(
|
||||
runtime,
|
||||
method="GET",
|
||||
path="/downloadinp/",
|
||||
params={"name": temp_name},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
output.write_bytes(content)
|
||||
emit_success(
|
||||
summary="导出 INP 成功",
|
||||
data={"output": str(output), "bytes": len(content)},
|
||||
ctx=runtime,
|
||||
duration_ms=duration_dump + duration_download,
|
||||
next_commands=["tjwater-cli project info"],
|
||||
)
|
||||
|
||||
|
||||
@network_app.command("get-node-properties")
|
||||
def network_get_node_properties(
|
||||
ctx: typer.Context,
|
||||
node: Annotated[str, typer.Option("--node", help="节点 ID")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取节点属性成功",
|
||||
method="GET",
|
||||
path="/getnodeproperties/",
|
||||
params={"network": require_network(runtime), "node": node},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@network_app.command("get-link-properties")
|
||||
def network_get_link_properties(
|
||||
ctx: typer.Context,
|
||||
link: Annotated[str, typer.Option("--link", help="管线 ID")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取管线属性成功",
|
||||
method="GET",
|
||||
path="/getlinkproperties/",
|
||||
params={"network": require_network(runtime), "link": link},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
def _component_option_mapping(kind: str, pump: str | None) -> tuple[str, dict[str, Any]]:
|
||||
if kind == "time":
|
||||
return "/gettimeschema", {}
|
||||
if kind == "energy":
|
||||
return "/getenergyschema/", {}
|
||||
if kind == "pump-energy":
|
||||
if not pump:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="PUMP_REQUIRED",
|
||||
message="--pump is required when --kind pump-energy",
|
||||
exit_code=2,
|
||||
)
|
||||
return "/getpumpenergyschema/", {"pump": pump}
|
||||
if kind == "network":
|
||||
return "/getoptionschema/", {}
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="INVALID_KIND",
|
||||
message="kind must be one of: time, energy, pump-energy, network",
|
||||
exit_code=2,
|
||||
)
|
||||
|
||||
|
||||
def _component_option_get_mapping(kind: str, pump: str | None) -> tuple[str, dict[str, Any]]:
|
||||
if kind == "time":
|
||||
return "/gettimeproperties/", {}
|
||||
if kind == "energy":
|
||||
return "/getenergyproperties/", {}
|
||||
if kind == "pump-energy":
|
||||
if not pump:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="PUMP_REQUIRED",
|
||||
message="--pump is required when --kind pump-energy",
|
||||
exit_code=2,
|
||||
)
|
||||
return "/getpumpenergyproperties/", {"pump": pump}
|
||||
if kind == "network":
|
||||
return "/getoptionproperties/", {}
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="INVALID_KIND",
|
||||
message="kind must be one of: time, energy, pump-energy, network",
|
||||
exit_code=2,
|
||||
)
|
||||
|
||||
|
||||
@component_option_app.command("schema")
|
||||
def component_option_schema(
|
||||
ctx: typer.Context,
|
||||
kind: Annotated[str, typer.Option("--kind", help="time|energy|pump-energy|network")],
|
||||
pump: Annotated[str | None, typer.Option("--pump", help="泵 ID")] = None,
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
path, extra = _component_option_mapping(kind, pump)
|
||||
params = {"network": require_network(runtime)} | extra
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取选项 schema 成功",
|
||||
method="GET",
|
||||
path=path,
|
||||
params=params,
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@component_option_app.command("get")
|
||||
def component_option_get(
|
||||
ctx: typer.Context,
|
||||
kind: Annotated[str, typer.Option("--kind", help="time|energy|pump-energy|network")],
|
||||
pump: Annotated[str | None, typer.Option("--pump", help="泵 ID")] = None,
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
path, extra = _component_option_get_mapping(kind, pump)
|
||||
params = {"network": require_network(runtime)} | extra
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取选项属性成功",
|
||||
method="GET",
|
||||
path=path,
|
||||
params=params,
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
@@ -0,0 +1,144 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
import typer
|
||||
|
||||
from .apps import component_option_app, network_app
|
||||
from .common import emit_api, runtime_context
|
||||
from .core import CLIError, require_network
|
||||
|
||||
|
||||
@network_app.command("get-node-properties")
|
||||
def network_get_node_properties(
|
||||
ctx: typer.Context,
|
||||
node: Annotated[str, typer.Option("--node", help="节点 ID")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取节点属性成功",
|
||||
method="GET",
|
||||
path="/getnodeproperties/",
|
||||
params={"network": require_network(runtime), "node": node},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@network_app.command("get-link-properties")
|
||||
def network_get_link_properties(
|
||||
ctx: typer.Context,
|
||||
link: Annotated[str, typer.Option("--link", help="管线 ID")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取管线属性成功",
|
||||
method="GET",
|
||||
path="/getlinkproperties/",
|
||||
params={"network": require_network(runtime), "link": link},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@network_app.command("get-all-junction-properties")
|
||||
def network_get_all_junction_properties(ctx: typer.Context) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取全部节点属性成功",
|
||||
method="GET",
|
||||
path="/getalljunctionproperties/",
|
||||
params={"network": require_network(runtime)},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@network_app.command("get-all-pipe-properties")
|
||||
def network_get_all_pipe_properties(ctx: typer.Context) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取全部管道属性成功",
|
||||
method="GET",
|
||||
path="/getallpipeproperties/",
|
||||
params={"network": require_network(runtime)},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@component_option_app.command("schema")
|
||||
def component_option_schema(
|
||||
ctx: typer.Context,
|
||||
kind: Annotated[str, typer.Option("--kind", help="time|energy|pump-energy|network")],
|
||||
pump: Annotated[str | None, typer.Option("--pump", help="pump-energy 时需要的泵 ID")] = None,
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
path = _component_option_path(kind, schema=True)
|
||||
params = {"network": require_network(runtime)}
|
||||
if kind == "pump-energy" and pump:
|
||||
params["pump"] = pump
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取选项 schema 成功",
|
||||
method="GET",
|
||||
path=path,
|
||||
params=params,
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@component_option_app.command("get")
|
||||
def component_option_get(
|
||||
ctx: typer.Context,
|
||||
kind: Annotated[str, typer.Option("--kind", help="time|energy|pump-energy|network")],
|
||||
pump: Annotated[str | None, typer.Option("--pump", help="pump-energy 时需要的泵 ID")] = None,
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
path = _component_option_path(kind, schema=False)
|
||||
params = {"network": require_network(runtime)}
|
||||
if kind == "pump-energy":
|
||||
if not pump:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="PUMP_REQUIRED",
|
||||
message="--pump is required when --kind pump-energy",
|
||||
exit_code=2,
|
||||
)
|
||||
params["pump"] = pump
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取选项属性成功",
|
||||
method="GET",
|
||||
path=path,
|
||||
params=params,
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
def _component_option_path(kind: str, *, schema: bool) -> str:
|
||||
routes = {
|
||||
("time", True): "/gettimeschema",
|
||||
("time", False): "/gettimeproperties/",
|
||||
("energy", True): "/getenergyschema/",
|
||||
("energy", False): "/getenergyproperties/",
|
||||
("pump-energy", True): "/getpumpenergyschema/",
|
||||
("pump-energy", False): "/getpumpenergyproperties//",
|
||||
("network", True): "/getoptionschema/",
|
||||
("network", False): "/getoptionproperties/",
|
||||
}
|
||||
path = routes.get((kind, schema))
|
||||
if path is None:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="INVALID_KIND",
|
||||
message="--kind must be one of time, energy, pump-energy, network",
|
||||
exit_code=2,
|
||||
)
|
||||
return path
|
||||
@@ -97,7 +97,6 @@ def _click_option_docs(path: tuple[str, ...]) -> list[dict[str, Any]]:
|
||||
|
||||
def _sample_option_value(path: tuple[str, ...], option_name: str) -> str:
|
||||
path_specific_samples: dict[tuple[tuple[str, ...], str], str] = {
|
||||
(("project", "data"), "kind"): "scada-info",
|
||||
(("component", "option", "schema"), "kind"): "time",
|
||||
(("component", "option", "get"), "kind"): "time",
|
||||
(("data", "timeseries", "composite"), "kind"): "scada-simulation",
|
||||
|
||||
@@ -8,7 +8,7 @@ import click
|
||||
import typer
|
||||
from click.exceptions import NoArgsIsHelpError
|
||||
|
||||
from . import commands_analysis, commands_data, commands_project # noqa: F401
|
||||
from . import commands_analysis, commands_data, commands_readonly # noqa: F401
|
||||
from .apps import app
|
||||
from .core import CLIError, DEFAULT_SERVER, DEFAULT_TIMEOUT, emit_failure
|
||||
from .helping import (
|
||||
|
||||
+228
-48
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
from .core import CommandDoc, CommandOptionDoc, SCHEMA_VERSION
|
||||
|
||||
GROUP_SUMMARIES: dict[tuple[str, ...], str] = {
|
||||
("project",): "项目与项目级元数据相关命令。",
|
||||
("network",): "管网节点、管线等基础属性查询命令。",
|
||||
("component",): "组件选项与配置读取命令。",
|
||||
("component", "option"): "组件选项查询命令。",
|
||||
@@ -35,51 +34,31 @@ HIDDEN_PATH_PREFIXES: tuple[tuple[str, ...], ...] = (
|
||||
)
|
||||
|
||||
COMMAND_DOCS: dict[tuple[str, ...], CommandDoc] = {
|
||||
("project", "list"): CommandDoc(
|
||||
path=("project", "list"),
|
||||
summary="列出当前用户可访问项目",
|
||||
description="调用 /meta/projects 返回项目列表。",
|
||||
examples=("tjwater-cli project list",),
|
||||
next_commands=("tjwater-cli project info",),
|
||||
),
|
||||
("project", "info"): CommandDoc(
|
||||
path=("project", "info"),
|
||||
summary="查看当前项目摘要信息。",
|
||||
description="查看当前项目的基础信息。",
|
||||
examples=("tjwater-cli project info",),
|
||||
output="项目元数据",
|
||||
),
|
||||
("project", "db-health"): CommandDoc(
|
||||
path=("project", "db-health"),
|
||||
summary="检查当前项目数据库健康状态",
|
||||
description="调用 /meta/db/health 返回 PostgreSQL 与 Timescale 健康状态。",
|
||||
),
|
||||
("project", "export-inp"): CommandDoc(
|
||||
path=("project", "export-inp"),
|
||||
summary="导出当前项目 INP 到本地文件",
|
||||
description="先调用 /dumpinp/ 在服务端生成 INP,再通过 /downloadinp/ 下载到本地。",
|
||||
options=(
|
||||
CommandOptionDoc("output", "本地输出路径", required=True),
|
||||
),
|
||||
output="本地文件路径和下载摘要",
|
||||
),
|
||||
("project", "data"): CommandDoc(
|
||||
path=("project", "data"),
|
||||
summary="读取当前项目业务数据",
|
||||
description="kind 支持 scada-info、scheme-list、burst-locate-result。",
|
||||
options=(CommandOptionDoc("kind", "数据类型", required=True),),
|
||||
),
|
||||
("network", "get-node-properties"): CommandDoc(
|
||||
path=("network", "get-node-properties"),
|
||||
summary="读取节点属性",
|
||||
description="调用 /getnodeproperties/。",
|
||||
options=(CommandOptionDoc("node", "节点 ID", required=True),),
|
||||
examples=("tjwater-cli network get-node-properties --node J1",),
|
||||
),
|
||||
("network", "get-link-properties"): CommandDoc(
|
||||
path=("network", "get-link-properties"),
|
||||
summary="读取管线属性",
|
||||
description="调用 /getlinkproperties/。",
|
||||
options=(CommandOptionDoc("link", "管线 ID", required=True),),
|
||||
examples=("tjwater-cli network get-link-properties --link P1",),
|
||||
),
|
||||
("network", "get-all-junction-properties"): CommandDoc(
|
||||
path=("network", "get-all-junction-properties"),
|
||||
summary="读取全部节点属性",
|
||||
description="调用 /getalljunctionproperties/。",
|
||||
examples=("tjwater-cli network get-all-junction-properties",),
|
||||
),
|
||||
("network", "get-all-pipe-properties"): CommandDoc(
|
||||
path=("network", "get-all-pipe-properties"),
|
||||
summary="读取全部管道属性",
|
||||
description="调用 /getallpipeproperties/。",
|
||||
examples=("tjwater-cli network get-all-pipe-properties",),
|
||||
),
|
||||
("component", "option", "schema"): CommandDoc(
|
||||
path=("component", "option", "schema"),
|
||||
@@ -89,6 +68,12 @@ COMMAND_DOCS: dict[tuple[str, ...], CommandDoc] = {
|
||||
CommandOptionDoc("kind", "选项类型", required=True),
|
||||
CommandOptionDoc("pump", "pump-energy 时需要的泵 ID"),
|
||||
),
|
||||
examples=(
|
||||
"tjwater-cli component option schema --kind time",
|
||||
"tjwater-cli component option schema --kind energy",
|
||||
"tjwater-cli component option schema --kind pump-energy --pump PUMP1",
|
||||
"tjwater-cli component option schema --kind network",
|
||||
),
|
||||
),
|
||||
("component", "option", "get"): CommandDoc(
|
||||
path=("component", "option", "get"),
|
||||
@@ -98,15 +83,22 @@ COMMAND_DOCS: dict[tuple[str, ...], CommandDoc] = {
|
||||
CommandOptionDoc("kind", "选项类型", required=True),
|
||||
CommandOptionDoc("pump", "pump-energy 时需要的泵 ID"),
|
||||
),
|
||||
examples=(
|
||||
"tjwater-cli component option get --kind time",
|
||||
"tjwater-cli component option get --kind energy",
|
||||
"tjwater-cli component option get --kind pump-energy --pump PUMP1",
|
||||
"tjwater-cli component option get --kind network",
|
||||
),
|
||||
),
|
||||
("simulation", "run"): CommandDoc(
|
||||
path=("simulation", "run"),
|
||||
summary="触发指定绝对时间的模拟运行",
|
||||
description="把显式带时区的 RFC3339 start-time 直接传给 /runsimulationmanuallybydate/;服务端按带时区时间处理并统一按 UTC 存储结果,实时数据需后续通过 data timeseries 在对应时间段查询。",
|
||||
description="把显式带时区的 RFC3339 start-time 直接传给 /runsimulationmanuallybydate/;服务端按带时区时间处理并统一按 UTC 存储结果,实时数据需后续通过 data timeseries 在对应时间段查询。duration 单位为分钟。",
|
||||
options=(
|
||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
||||
CommandOptionDoc("duration", "持续分钟数", required=True),
|
||||
),
|
||||
examples=("tjwater-cli simulation run --start-time 2025-01-02T03:04:05+08:00 --duration 30",),
|
||||
next_commands=(
|
||||
"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 data timeseries realtime nodes --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00",
|
||||
@@ -116,7 +108,7 @@ COMMAND_DOCS: dict[tuple[str, ...], CommandDoc] = {
|
||||
("analysis", "burst"): CommandDoc(
|
||||
path=("analysis", "burst"),
|
||||
summary="执行爆管分析",
|
||||
description="读取 burst-file 并转换为 burst_ID[] / burst_size[];接口本身只返回分析执行结果,方案数据需后续通过 data scheme 命令获取。",
|
||||
description="读取 burst-file 并转换为 burst_ID[] / burst_size[];接口本身只返回分析执行结果,方案数据需后续通过 data scheme 命令获取。duration 单位为秒。",
|
||||
options=(
|
||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
||||
CommandOptionDoc("duration", "持续秒数", required=True),
|
||||
@@ -124,7 +116,7 @@ COMMAND_DOCS: dict[tuple[str, ...], CommandDoc] = {
|
||||
CommandOptionDoc("scheme", "方案名称"),
|
||||
),
|
||||
examples=(
|
||||
"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 analysis burst --start-time 2025-01-02T03:04:05+08:00 --duration 900 --burst-file ./burst.json --scheme burst_case_01",
|
||||
"tjwater-cli data scheme get --name burst_case_01",
|
||||
"tjwater-cli data scheme list",
|
||||
),
|
||||
@@ -132,201 +124,389 @@ COMMAND_DOCS: dict[tuple[str, ...], CommandDoc] = {
|
||||
("analysis", "valve"): CommandDoc(
|
||||
path=("analysis", "valve"),
|
||||
summary="阀门工况分析。",
|
||||
description="指定阀门采取关闭/开启等操作逻辑,并执行定时长模拟。结果写入时序库。",
|
||||
description="close 模式按指定阀门关闭执行定时长模拟;isolation 模式按指定事故元素计算关阀隔离方案。duration 单位为秒。",
|
||||
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),
|
||||
CommandOptionDoc(name="mode", description="阀门操作模式:'close' 或 'isolation'", required=True),
|
||||
CommandOptionDoc(name="start-time", description="close 模式需要的起始绝对时间,必须显式带时区偏移"),
|
||||
CommandOptionDoc(name="valve", description="close 模式下需关闭的阀门 ID(可多次指定)", repeated=True),
|
||||
CommandOptionDoc(name="element", description="isolation 模式下的事故元素 ID(可多次指定)", repeated=True),
|
||||
CommandOptionDoc(name="disabled-valve", description="isolation 模式下需排除的故障阀门 ID(可多次指定)", repeated=True),
|
||||
CommandOptionDoc(name="duration", description="close 模式持续秒数,默认 900"),
|
||||
CommandOptionDoc(name="scheme", description="close 模式方案名称"),
|
||||
),
|
||||
examples=(
|
||||
"tjwater-cli 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 --valve V2 --duration 900 --scheme valve_case_01",
|
||||
"tjwater-cli analysis valve --mode isolation --element E1 --element E2",
|
||||
"tjwater-cli analysis valve --mode isolation --element E1 --disabled-valve V3",
|
||||
),
|
||||
),
|
||||
("analysis", "flushing"): CommandDoc(
|
||||
path=("analysis", "flushing"),
|
||||
summary="执行冲洗分析",
|
||||
description="读取 valve-setting-file 并转换为 valves[] / valves_k[]。",
|
||||
description="读取 valve-setting-file 并转换为 valves[] / valves_k[]。duration 单位为秒,默认 900。",
|
||||
options=(
|
||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
||||
CommandOptionDoc("valve-setting-file", "阀门开度 JSON 文件", required=True),
|
||||
CommandOptionDoc("drainage-node", "排污节点 ID", required=True),
|
||||
CommandOptionDoc("flow", "冲洗流量", required=True),
|
||||
CommandOptionDoc("duration", "持续秒数,默认 900"),
|
||||
CommandOptionDoc("scheme", "方案名称", required=True),
|
||||
),
|
||||
examples=("tjwater-cli analysis flushing --start-time 2025-01-02T03:04:05+08:00 --valve-setting-file ./valve.json --drainage-node N1 --flow 100.0 --duration 900 --scheme flush_case_01",),
|
||||
),
|
||||
("analysis", "age"): CommandDoc(
|
||||
path=("analysis", "age"),
|
||||
summary="执行水龄分析",
|
||||
description="调用 /age_analysis/。",
|
||||
description="调用 /age_analysis/。duration 单位为秒。",
|
||||
options=(
|
||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
||||
CommandOptionDoc("duration", "持续秒数", required=True),
|
||||
),
|
||||
examples=("tjwater-cli analysis age --start-time 2025-01-02T03:04:05+08:00 --duration 900",),
|
||||
),
|
||||
("analysis", "contaminant"): CommandDoc(
|
||||
path=("analysis", "contaminant"),
|
||||
summary="执行污染物模拟",
|
||||
description="调用 /contaminant_simulation/。",
|
||||
description="调用 /contaminant_simulation/。duration 单位为秒。",
|
||||
options=(
|
||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
||||
CommandOptionDoc("duration", "持续秒数", required=True),
|
||||
CommandOptionDoc("source-node", "污染源节点 ID", required=True),
|
||||
CommandOptionDoc("concentration", "浓度值", required=True),
|
||||
CommandOptionDoc("pattern", "模式 ID"),
|
||||
CommandOptionDoc("scheme", "方案名称", required=True),
|
||||
),
|
||||
examples=("tjwater-cli analysis contaminant --start-time 2025-01-02T03:04:05+08:00 --duration 900 --source-node N1 --concentration 10.0 --scheme contam_case_01",),
|
||||
),
|
||||
("analysis", "sensor-placement", "kmeans"): CommandDoc(
|
||||
path=("analysis", "sensor-placement", "kmeans"),
|
||||
summary="执行 KMeans 传感器选址",
|
||||
description="使用 POST /pressure_sensor_placement_kmeans/,补齐 username 和 min_diameter。",
|
||||
options=(
|
||||
CommandOptionDoc("count", "传感器数量", required=True),
|
||||
CommandOptionDoc("min-diameter", "最小管径,默认 0"),
|
||||
CommandOptionDoc("scheme", "方案名称"),
|
||||
),
|
||||
examples=("tjwater-cli analysis sensor-placement kmeans --count 5 --min-diameter 100 --scheme placement_case_01",),
|
||||
),
|
||||
("analysis", "leakage", "identify"): CommandDoc(
|
||||
path=("analysis", "leakage", "identify"),
|
||||
summary="执行漏损识别",
|
||||
description="把 CLI 时间映射到 scada_start / scada_end。",
|
||||
options=(
|
||||
CommandOptionDoc("start-time", "显式带时区的 SCADA 开始时间", required=True),
|
||||
CommandOptionDoc("end-time", "显式带时区的 SCADA 结束时间", required=True),
|
||||
CommandOptionDoc("scheme", "方案名称"),
|
||||
),
|
||||
examples=("tjwater-cli analysis leakage identify --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --scheme leak_case_01",),
|
||||
),
|
||||
("analysis", "leakage", "schemes", "list"): CommandDoc(
|
||||
path=("analysis", "leakage", "schemes", "list"),
|
||||
summary="列出漏损方案",
|
||||
description="调用 /leakage/schemes/。",
|
||||
examples=("tjwater-cli analysis leakage schemes list",),
|
||||
),
|
||||
("analysis", "leakage", "schemes", "get"): CommandDoc(
|
||||
path=("analysis", "leakage", "schemes", "get"),
|
||||
summary="读取漏损方案详情",
|
||||
description="调用 /leakage/schemes/{scheme_name}。",
|
||||
examples=("tjwater-cli analysis leakage schemes get my_scheme",),
|
||||
),
|
||||
("analysis", "burst-detection", "detect"): CommandDoc(
|
||||
path=("analysis", "burst-detection", "detect"),
|
||||
summary="执行爆管检测",
|
||||
description="调用 /burst-detection/detect/。",
|
||||
options=(
|
||||
CommandOptionDoc("start-time", "显式带时区的 SCADA 开始时间", required=True),
|
||||
CommandOptionDoc("end-time", "显式带时区的 SCADA 结束时间", required=True),
|
||||
CommandOptionDoc("scheme", "方案名称"),
|
||||
),
|
||||
examples=("tjwater-cli analysis burst-detection detect --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --scheme detect_case_01",),
|
||||
),
|
||||
("analysis", "burst-detection", "schemes", "list"): CommandDoc(
|
||||
path=("analysis", "burst-detection", "schemes", "list"),
|
||||
summary="列出爆管检测方案",
|
||||
description="调用 /burst-detection/schemes/。",
|
||||
examples=("tjwater-cli analysis burst-detection schemes list",),
|
||||
),
|
||||
("analysis", "burst-detection", "schemes", "get"): CommandDoc(
|
||||
path=("analysis", "burst-detection", "schemes", "get"),
|
||||
summary="读取爆管检测方案详情",
|
||||
description="调用 /burst-detection/schemes/{scheme_name}。",
|
||||
examples=("tjwater-cli analysis burst-detection schemes get my_scheme",),
|
||||
),
|
||||
("analysis", "burst-location", "locate"): CommandDoc(
|
||||
path=("analysis", "burst-location", "locate"),
|
||||
summary="执行爆管定位",
|
||||
description="调用 /burst-location/locate/;需要 burst-leakage。",
|
||||
description="调用 /burst-location/locate/;需要 burst-leakage。支持 monitoring 和 simulation 两种数据源。",
|
||||
options=(
|
||||
CommandOptionDoc("start-time", "显式带时区的 SCADA 开始时间", required=True),
|
||||
CommandOptionDoc("end-time", "显式带时区的 SCADA 结束时间", required=True),
|
||||
CommandOptionDoc("burst-leakage", "爆管漏水量", required=True),
|
||||
CommandOptionDoc("scheme", "方案名称"),
|
||||
CommandOptionDoc("data-source", "数据源:monitoring(默认)或 simulation"),
|
||||
CommandOptionDoc("pressure-scada-id", "压力 SCADA ID(可多次指定)", repeated=True),
|
||||
CommandOptionDoc("flow-scada-id", "流量 SCADA ID(可多次指定)", repeated=True),
|
||||
CommandOptionDoc("pressure-file", "包含 burst_pressure/normal_pressure 的 JSON 文件"),
|
||||
CommandOptionDoc("flow-file", "包含 burst_flow/normal_flow 的 JSON 文件"),
|
||||
CommandOptionDoc("use-scada-flow", "启用 SCADA 流量"),
|
||||
),
|
||||
examples=(
|
||||
"tjwater-cli analysis burst-location locate --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --burst-leakage 100.0 --scheme locate_case_01",
|
||||
"tjwater-cli analysis burst-location locate --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --burst-leakage 50.0 --scheme locate_case_01 --data-source simulation --pressure-file ./pressure.json --flow-file ./flow.json",
|
||||
),
|
||||
),
|
||||
("analysis", "burst-location", "schemes", "list"): CommandDoc(
|
||||
path=("analysis", "burst-location", "schemes", "list"),
|
||||
summary="列出爆管定位方案",
|
||||
description="调用 /burst-location/schemes/。",
|
||||
examples=("tjwater-cli analysis burst-location schemes list",),
|
||||
),
|
||||
("analysis", "burst-location", "schemes", "get"): CommandDoc(
|
||||
path=("analysis", "burst-location", "schemes", "get"),
|
||||
summary="读取爆管定位方案详情",
|
||||
description="调用 /burst-location/schemes/{scheme_name}。",
|
||||
examples=("tjwater-cli analysis burst-location schemes get my_scheme",),
|
||||
),
|
||||
("analysis", "risk", "pipe-now"): CommandDoc(
|
||||
path=("analysis", "risk", "pipe-now"),
|
||||
summary="读取单条管道当前风险",
|
||||
description="调用 /getpiperiskprobabilitynow/。",
|
||||
options=(CommandOptionDoc("pipe", "管道 ID", required=True),),
|
||||
examples=("tjwater-cli analysis risk pipe-now --pipe P1",),
|
||||
),
|
||||
("analysis", "risk", "pipe-history"): CommandDoc(
|
||||
path=("analysis", "risk", "pipe-history"),
|
||||
summary="读取单条管道历史风险",
|
||||
description="调用 /getpiperiskprobability/。",
|
||||
options=(CommandOptionDoc("pipe", "管道 ID", required=True),),
|
||||
examples=("tjwater-cli analysis risk pipe-history --pipe P1",),
|
||||
),
|
||||
("analysis", "risk", "network"): CommandDoc(
|
||||
path=("analysis", "risk", "network"),
|
||||
summary="读取全网风险",
|
||||
description="组合 /getnetworkpiperiskprobabilitynow/ 与 /getpiperiskprobabilitygeometries/。",
|
||||
examples=("tjwater-cli analysis risk network",),
|
||||
),
|
||||
("data", "timeseries", "realtime", "links"): CommandDoc(
|
||||
path=("data", "timeseries", "realtime", "links"),
|
||||
summary="查询实时管道时序",
|
||||
description="调用 /realtime/links。",
|
||||
options=(
|
||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
||||
CommandOptionDoc("end-time", "显式带时区的结束时间", required=True),
|
||||
),
|
||||
examples=("tjwater-cli data timeseries realtime links --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00",),
|
||||
),
|
||||
("data", "timeseries", "realtime", "nodes"): CommandDoc(
|
||||
path=("data", "timeseries", "realtime", "nodes"),
|
||||
summary="查询实时节点时序",
|
||||
description="调用 /realtime/nodes。",
|
||||
options=(
|
||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
||||
CommandOptionDoc("end-time", "显式带时区的结束时间", required=True),
|
||||
),
|
||||
examples=("tjwater-cli data timeseries realtime nodes --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00",),
|
||||
),
|
||||
("data", "timeseries", "realtime", "simulation-by-id-time"): CommandDoc(
|
||||
path=("data", "timeseries", "realtime", "simulation-by-id-time"),
|
||||
summary="按元素和时间查询实时模拟结果",
|
||||
description="调用 /realtime/query/by-id-time。",
|
||||
options=(
|
||||
CommandOptionDoc("id", "元素 ID", required=True),
|
||||
CommandOptionDoc("type", "元素类型:pipe 或 junction", required=True),
|
||||
CommandOptionDoc("time", "显式带时区的查询时间", required=True),
|
||||
),
|
||||
examples=(
|
||||
"tjwater-cli data timeseries realtime simulation-by-id-time --id J1 --type junction --time 2025-01-02T03:30:00+08:00",
|
||||
"tjwater-cli data timeseries realtime simulation-by-id-time --id P1 --type pipe --time 2025-01-02T03:30:00+08:00",
|
||||
),
|
||||
),
|
||||
("data", "timeseries", "realtime", "simulation-by-time-property"): CommandDoc(
|
||||
path=("data", "timeseries", "realtime", "simulation-by-time-property"),
|
||||
summary="按时间和属性查询实时模拟结果",
|
||||
description="调用 /realtime/query/by-time-property。",
|
||||
options=(
|
||||
CommandOptionDoc("type", "元素类型:pipe 或 junction", required=True),
|
||||
CommandOptionDoc("time", "显式带时区的查询时间", required=True),
|
||||
CommandOptionDoc("property", "属性名", required=True),
|
||||
),
|
||||
examples=("tjwater-cli data timeseries realtime simulation-by-time-property --type pipe --time 2025-01-02T03:30:00+08:00 --property flow",),
|
||||
),
|
||||
("data", "timeseries", "scheme", "links"): CommandDoc(
|
||||
path=("data", "timeseries", "scheme", "links"),
|
||||
summary="查询方案管道时序",
|
||||
description="调用 /scheme/links。",
|
||||
options=(
|
||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
||||
CommandOptionDoc("end-time", "显式带时区的结束时间", required=True),
|
||||
CommandOptionDoc("scheme", "方案名称"),
|
||||
CommandOptionDoc("scheme-type", "方案类型"),
|
||||
),
|
||||
examples=("tjwater-cli data timeseries scheme links --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --scheme my_scheme",),
|
||||
),
|
||||
("data", "timeseries", "scheme", "node-field"): CommandDoc(
|
||||
path=("data", "timeseries", "scheme", "node-field"),
|
||||
summary="查询方案节点字段时序",
|
||||
description="调用 /scheme/nodes/{node_id}/field。",
|
||||
options=(
|
||||
CommandOptionDoc("node", "节点 ID", required=True),
|
||||
CommandOptionDoc("field", "字段名", required=True),
|
||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
||||
CommandOptionDoc("end-time", "显式带时区的结束时间", required=True),
|
||||
CommandOptionDoc("scheme", "方案名称"),
|
||||
CommandOptionDoc("scheme-type", "方案类型"),
|
||||
),
|
||||
examples=("tjwater-cli data timeseries scheme node-field --node J1 --field pressure --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --scheme my_scheme",),
|
||||
),
|
||||
("data", "timeseries", "scheme", "simulation"): CommandDoc(
|
||||
path=("data", "timeseries", "scheme", "simulation"),
|
||||
summary="查询方案模拟数据",
|
||||
description="支持 by-id-time 与 by-scheme-time-property 两种查询。",
|
||||
options=(
|
||||
CommandOptionDoc("query", "查询模式:by-id-time 或 by-scheme-time-property", required=True),
|
||||
CommandOptionDoc("scheme", "方案名称"),
|
||||
CommandOptionDoc("scheme-type", "方案类型"),
|
||||
CommandOptionDoc("id", "元素 ID(by-id-time 时必需)"),
|
||||
CommandOptionDoc("time", "显式带时区的查询时间", required=True),
|
||||
CommandOptionDoc("type", "元素类型:pipe 或 junction"),
|
||||
CommandOptionDoc("property", "属性名(by-scheme-time-property 时必需)"),
|
||||
),
|
||||
examples=(
|
||||
"tjwater-cli data timeseries scheme simulation --query by-id-time --id J1 --time 2025-01-02T03:30:00+08:00 --type junction --scheme my_scheme",
|
||||
"tjwater-cli data timeseries scheme simulation --query by-scheme-time-property --time 2025-01-02T03:30:00+08:00 --type pipe --property flow --scheme my_scheme",
|
||||
),
|
||||
),
|
||||
("data", "timeseries", "scada", "query"): CommandDoc(
|
||||
path=("data", "timeseries", "scada", "query"),
|
||||
summary="查询 SCADA 时序",
|
||||
description="device-id 会被转换成后端逗号分隔参数。",
|
||||
options=(
|
||||
CommandOptionDoc("device-id", "设备 ID(可多次指定)", required=True, repeated=True),
|
||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
||||
CommandOptionDoc("end-time", "显式带时区的结束时间", required=True),
|
||||
CommandOptionDoc("field", "字段名"),
|
||||
),
|
||||
examples=(
|
||||
"tjwater-cli data timeseries scada query --device-id D1 --device-id D2 --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00",
|
||||
"tjwater-cli data timeseries scada query --device-id D1 --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --field flow",
|
||||
),
|
||||
),
|
||||
("data", "timeseries", "composite"): CommandDoc(
|
||||
path=("data", "timeseries", "composite"),
|
||||
summary="执行复合时序查询",
|
||||
description="kind 支持 scada-simulation、element-simulation、element-scada。",
|
||||
options=(
|
||||
CommandOptionDoc("kind", "复合查询类型", required=True),
|
||||
CommandOptionDoc("feature", "特征值(可多次指定,scada-simulation 为 device_id,element-simulation 为 element_id:property,element-scada 为 element_id)", repeated=True),
|
||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
||||
CommandOptionDoc("end-time", "显式带时区的结束时间", required=True),
|
||||
CommandOptionDoc("scheme", "方案名称"),
|
||||
CommandOptionDoc("scheme-type", "方案类型"),
|
||||
CommandOptionDoc("use-cleaned", "element-scada 使用清洗值"),
|
||||
),
|
||||
examples=(
|
||||
"tjwater-cli data timeseries composite --kind scada-simulation --feature D1 --feature D2 --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --scheme my_scheme",
|
||||
"tjwater-cli data timeseries composite --kind element-simulation --feature J1:pressure --feature P1:flow --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --scheme my_scheme",
|
||||
"tjwater-cli data timeseries composite --kind element-scada --feature J1 --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --use-cleaned",
|
||||
),
|
||||
),
|
||||
("data", "timeseries", "composite", "pipeline-health"): CommandDoc(
|
||||
path=("data", "timeseries", "composite", "pipeline-health"),
|
||||
summary="查询管道健康预测",
|
||||
description="调用 /composite/pipeline-health-prediction。",
|
||||
options=(
|
||||
CommandOptionDoc("pipe", "管道 ID", required=True),
|
||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
||||
CommandOptionDoc("end-time", "显式带时区的结束时间", required=True),
|
||||
),
|
||||
examples=("tjwater-cli data timeseries composite pipeline-health --pipe P1 --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00",),
|
||||
),
|
||||
("data", "scada", "schema"): CommandDoc(
|
||||
path=("data", "scada", "schema"),
|
||||
summary="读取 SCADA schema",
|
||||
description="kind 支持 device、device-data、element、info。",
|
||||
options=(CommandOptionDoc("kind", "SCADA 数据类型", required=True),),
|
||||
examples=(
|
||||
"tjwater-cli data scada schema --kind device",
|
||||
"tjwater-cli data scada schema --kind device-data",
|
||||
"tjwater-cli data scada schema --kind element",
|
||||
"tjwater-cli data scada schema --kind info",
|
||||
),
|
||||
),
|
||||
("data", "scada", "get"): CommandDoc(
|
||||
path=("data", "scada", "get"),
|
||||
summary="读取单条 SCADA 元数据",
|
||||
description="kind 支持 device、device-data、element、info。",
|
||||
options=(
|
||||
CommandOptionDoc("kind", "SCADA 数据类型", required=True),
|
||||
CommandOptionDoc("id", "记录 ID", required=True),
|
||||
),
|
||||
examples=(
|
||||
"tjwater-cli data scada get --kind device --id D1",
|
||||
"tjwater-cli data scada get --kind element --id E1",
|
||||
),
|
||||
),
|
||||
("data", "scada", "list"): CommandDoc(
|
||||
path=("data", "scada", "list"),
|
||||
summary="列出 SCADA 元数据",
|
||||
description="kind 支持 device、element、info;device-data 当前后端无 list 接口。",
|
||||
options=(CommandOptionDoc("kind", "SCADA 数据类型", required=True),),
|
||||
examples=(
|
||||
"tjwater-cli data scada list --kind device",
|
||||
"tjwater-cli data scada list --kind element",
|
||||
"tjwater-cli data scada list --kind info",
|
||||
),
|
||||
),
|
||||
("data", "scheme", "schema"): CommandDoc(
|
||||
path=("data", "scheme", "schema"),
|
||||
summary="读取方案 schema",
|
||||
description="调用 /getschemeschema/。",
|
||||
examples=("tjwater-cli data scheme schema",),
|
||||
),
|
||||
("data", "scheme", "get"): CommandDoc(
|
||||
path=("data", "scheme", "get"),
|
||||
summary="读取单条方案",
|
||||
description="调用 /getscheme/。",
|
||||
options=(CommandOptionDoc("name", "方案名称", required=True),),
|
||||
examples=("tjwater-cli data scheme get --name my_scheme",),
|
||||
),
|
||||
("data", "scheme", "list"): CommandDoc(
|
||||
path=("data", "scheme", "list"),
|
||||
summary="列出方案",
|
||||
description="调用 /getallschemes/。",
|
||||
examples=("tjwater-cli data scheme list",),
|
||||
),
|
||||
("data", "extension", "keys"): CommandDoc(
|
||||
path=("data", "extension", "keys"),
|
||||
summary="列出扩展数据键",
|
||||
description="调用 /getallextensiondatakeys/。",
|
||||
examples=("tjwater-cli data extension keys",),
|
||||
),
|
||||
("data", "extension", "get"): CommandDoc(
|
||||
path=("data", "extension", "get"),
|
||||
summary="读取扩展数据",
|
||||
description="调用 /getextensiondata/。",
|
||||
options=(CommandOptionDoc("key", "扩展键", required=True),),
|
||||
examples=("tjwater-cli data extension get --key my_key",),
|
||||
),
|
||||
("data", "extension", "list"): CommandDoc(
|
||||
path=("data", "extension", "list"),
|
||||
summary="列出扩展数据",
|
||||
description="调用 /getallextensiondata/。",
|
||||
examples=("tjwater-cli data extension list",),
|
||||
),
|
||||
("data", "misc", "sensor-placements"): CommandDoc(
|
||||
path=("data", "misc", "sensor-placements"),
|
||||
summary="列出传感器布置结果",
|
||||
description="调用 /getallsensorplacements/。",
|
||||
examples=("tjwater-cli data misc sensor-placements",),
|
||||
),
|
||||
("data", "misc", "burst-location-results"): CommandDoc(
|
||||
path=("data", "misc", "burst-location-results"),
|
||||
summary="列出爆管定位结果",
|
||||
description="调用 /getallburstlocateresults/。",
|
||||
examples=("tjwater-cli data misc burst-location-results",),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
首批 CLI 采用 **少量顶层入口 + 业务域二级分组 + 只读/分析优先** 的设计。
|
||||
|
||||
```text
|
||||
tjwater-cli project
|
||||
tjwater-cli network
|
||||
tjwater-cli component
|
||||
tjwater-cli simulation
|
||||
@@ -39,7 +38,6 @@ tjwater-cli help
|
||||
|
||||
| 顶层命令 | 二级范围 | 说明 |
|
||||
|---|---|---|
|
||||
| `project` | `list`、`info`、`db-health`、`export-inp`、`data` | 项目发现和只读项目数据 |
|
||||
| `network` | `get-node-properties`、`get-link-properties` | 管网节点/管线属性查询,只读 |
|
||||
| `component` | `option` | EPANET 选项设置,只读 |
|
||||
| `simulation` | `run` | 模拟运行 |
|
||||
@@ -92,85 +90,6 @@ tjwater-cli help
|
||||
|
||||
## 首批 CLI 范围
|
||||
|
||||
### Project
|
||||
|
||||
来源:
|
||||
|
||||
```text
|
||||
app/api/v1/endpoints/auth.py
|
||||
app/api/v1/endpoints/meta.py
|
||||
app/api/v1/endpoints/project.py
|
||||
app/api/v1/endpoints/project_data.py
|
||||
TJWaterFrontend_Refine/src/lib/requestHeaders.ts
|
||||
TJWaterFrontend_Refine/src/lib/api.ts
|
||||
TJWaterFrontend_Refine/src/lib/apiFetch.ts
|
||||
```
|
||||
|
||||
认证模式:
|
||||
|
||||
- **Non-interactive / Agent**
|
||||
- 面向 agent、脚本、多用户多 agent 并发调用。
|
||||
- 必须显式传入认证上下文。
|
||||
- 不得隐式回退到本机默认状态。
|
||||
|
||||
Agent 调用认证上下文:
|
||||
|
||||
- 当前前端调用链会自动附加以下请求头:
|
||||
- `Authorization: Bearer <access_token>`
|
||||
- `X-Project-Id: <projectId>`
|
||||
- `X-User-Id: <userId>`
|
||||
- 其中 `Authorization` 来自访问令牌,`X-Project-Id` 来自当前项目上下文,`X-User-Id` 来自当前登录用户。
|
||||
- 因此前端触发的 agent 调用,应默认支持直接消费这三个字段;不再设计额外的本地 `login` 流程。
|
||||
- CLI 侧建议提供两类显式注入方式:
|
||||
- `--auth-context PATH`
|
||||
- 环境变量 / 调用方 header 映射
|
||||
|
||||
认证解析优先级建议固定为:
|
||||
|
||||
1. 命令行显式参数(如 `--auth-context`)
|
||||
2. 调用方显式注入的环境变量 / header 映射
|
||||
|
||||
约束:
|
||||
|
||||
- Agent 模式下,若未显式提供认证上下文,应返回明确错误,而不是尝试复用默认登录态。
|
||||
- `X-Project-Id` 是当前 project scope 的默认来源;CLI 命令默认直接使用该上下文,不要求重复传参。
|
||||
- `X-User-Id` 主要用于审计、结果归属和多用户隔离,不应用来替代 access token 做认证。
|
||||
|
||||
| 命令 | 覆盖接口 | 说明 |
|
||||
|---|---|---|
|
||||
| `tjwater-cli project list` | `GET /meta/projects` | 项目列表 |
|
||||
| `tjwater-cli project info` | `GET /meta/project` | 当前 project 信息 |
|
||||
| `tjwater-cli project db-health` | `GET /meta/db/health` | 当前 project 数据库健康 |
|
||||
| `tjwater-cli project export-inp --output PATH` | `GET /exportinp/`、`GET /dumpinp/`、`GET /downloadinp/` | 导出当前 project 的 INP 到本地文件 |
|
||||
| `tjwater-cli project data --kind scada-info\|scheme-list\|burst-locate-result` | `GET /scada-info`、`GET /scheme-list`、`GET /burst-locate-result*` | 当前 project 的业务数据 |
|
||||
|
||||
暂不暴露:
|
||||
|
||||
```text
|
||||
POST /auth/register
|
||||
POST /auth/login
|
||||
POST /auth/login/simple
|
||||
GET /auth/me
|
||||
POST /auth/refresh
|
||||
GET /listprojects/
|
||||
GET /project_info/
|
||||
GET /haveproject/
|
||||
GET /isprojectopen/
|
||||
GET /isprojectlocked/
|
||||
GET /isprojectlockedbyme/
|
||||
POST /createproject/
|
||||
POST /deleteproject/
|
||||
POST /openproject/
|
||||
POST /closeproject/
|
||||
POST /copyproject/
|
||||
POST /importinp/
|
||||
POST /readinp/
|
||||
POST /lockproject/
|
||||
POST /unlockproject/
|
||||
POST /uploadinp/
|
||||
GET /convertv3tov2/
|
||||
```
|
||||
|
||||
### Network
|
||||
|
||||
来源:
|
||||
@@ -183,6 +102,8 @@ app/api/v1/endpoints/network/*.py
|
||||
|---|---|---|
|
||||
| `tjwater-cli network get-node-properties --node NODE` | `GET /getnodeproperties/` | 读取当前 project 中指定节点的属性 |
|
||||
| `tjwater-cli network get-link-properties --link LINK` | `GET /getlinkproperties/` | 读取当前 project 中指定管线的属性 |
|
||||
| `tjwater-cli network get-all-junction-properties` | `GET /getalljunctionproperties/` | 读取当前 project 中所有节点属性 |
|
||||
| `tjwater-cli network get-all-pipe-properties` | `GET /getallpipeproperties/` | 读取当前 project 中所有管道属性 |
|
||||
|
||||
暂不暴露:
|
||||
|
||||
@@ -275,10 +196,10 @@ app/api/v1/endpoints/risk.py
|
||||
|---|---|---|
|
||||
| `tjwater-cli simulation run --start-time RFC3339 --duration MINUTES` | `POST /runsimulationmanuallybydate/` | 按指定绝对开始时间触发当前 project 的实时模拟;`start-time` 必须显式带时区,结果写入服务端时序库,后续通过 `tjwater-cli data timeseries realtime *` 查询 |
|
||||
| `tjwater-cli analysis burst --start-time TIME --duration SEC --scheme SCHEME --burst-file FILE` | `GET /burst_analysis/` | 爆管分析;`FILE` 提供爆管点与流量列表,CLI 负责转换为 `burst_ID[]` / `burst_size[]` |
|
||||
| `tjwater-cli analysis valve --mode close\|isolation --start-time TIME --valve VALVE` | `GET /valve_close_analysis/`、`GET /valve_isolation_analysis/` | 阀门分析,`--valve` 可重复 |
|
||||
| `tjwater-cli analysis flushing --start-time TIME --valve-setting-file FILE --drainage-node NODE --flow FLOW [--duration SEC] [--scheme SCHEME]` | `GET /flushing_analysis/` | 冲洗分析;`FILE` 提供阀门与开度列表,CLI 负责转换为 `valves[]` / `valves_k[]` |
|
||||
| `tjwater-cli analysis valve --mode close\|isolation --start-time TIME --valve VALVE [--scheme SCHEME]` | `GET /valve_close_analysis/`、`GET /valve_isolation_analysis/` | 阀门分析;close 模式需要 `--scheme`,`--valve` 可重复 |
|
||||
| `tjwater-cli analysis flushing --start-time TIME --valve-setting-file FILE --drainage-node NODE --flow FLOW --scheme SCHEME [--duration SEC]` | `GET /flushing_analysis/` | 冲洗分析;`FILE` 提供阀门与开度列表,CLI 负责转换为 `valves[]` / `valves_k[]` |
|
||||
| `tjwater-cli analysis age --start-time TIME --duration SEC` | `GET /age_analysis/` | 水龄分析 |
|
||||
| `tjwater-cli analysis contaminant --start-time TIME --duration SEC --source-node NODE --concentration VALUE [--pattern PATTERN] [--scheme SCHEME]` | `GET /contaminant_simulation/` | 污染物模拟 |
|
||||
| `tjwater-cli analysis contaminant --start-time TIME --duration SEC --source-node NODE --concentration VALUE --scheme SCHEME [--pattern PATTERN]` | `GET /contaminant_simulation/` | 污染物模拟 |
|
||||
| `tjwater-cli analysis sensor-placement kmeans --count N` | `GET /pressuresensorplacementkmeans/` | 基于 kmeans 的传感器放置分析;不包含创建方案 |
|
||||
| `tjwater-cli analysis leakage identify --scheme SCHEME --start-time TIME --end-time TIME` | `POST /leakage/identify/` | 漏损识别 |
|
||||
| `tjwater-cli analysis leakage schemes list\|get` | `GET /leakage/schemes/`、`GET /leakage/schemes/{scheme_name}` | 漏损方案查询 |
|
||||
@@ -440,7 +361,7 @@ POST /users/{user_id}/deactivate
|
||||
输出补充约束:
|
||||
|
||||
- 首批 CLI 不再设计通用 `result_ref` / `--out-ref` 机制。
|
||||
- 若某业务命令确实需要落本地文件,应由所属命令显式提供 `--output PATH`,例如 `project export-inp --output PATH`。
|
||||
- 若某业务命令确实需要落本地文件,应由所属命令显式提供 `--output PATH`。
|
||||
- 若后续出现超大结果集、必须脱离 stdout 传输时,再单独设计结果引用机制,而不是在首批 CLI 中预埋未闭环能力。
|
||||
|
||||
## 输出规范
|
||||
|
||||
@@ -623,7 +623,7 @@ def age_analysis(
|
||||
new_name,
|
||||
"realtime",
|
||||
modify_pattern_start_time,
|
||||
modify_total_duration,
|
||||
duration=modify_total_duration,
|
||||
downloading_prohibition=True,
|
||||
)
|
||||
# step 2. restore the base model status
|
||||
|
||||
@@ -269,3 +269,92 @@ def test_runsimulationmanuallybydate_endpoint_rejects_naive_start_time(monkeypat
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_valve_close_endpoint_passes_scheme_name(monkeypatch):
|
||||
module = _load_simulation_module(monkeypatch)
|
||||
captured = {}
|
||||
|
||||
def fake_valve_close_analysis(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return "ok"
|
||||
|
||||
monkeypatch.setattr(module, "valve_close_analysis", fake_valve_close_analysis)
|
||||
client = TestClient(build_test_app(module.router, "/api/v1"))
|
||||
|
||||
response = client.get(
|
||||
"/api/v1/valve_close_analysis/",
|
||||
params={
|
||||
"network": "demo",
|
||||
"start_time": "2025-01-02T03:04:05+08:00",
|
||||
"valves": ["V1", "V2"],
|
||||
"duration": 900,
|
||||
"scheme_name": "valve_case_01",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.text == "ok"
|
||||
assert captured == {
|
||||
"name": "demo",
|
||||
"modify_pattern_start_time": "2025-01-02T03:04:05+08:00",
|
||||
"modify_total_duration": 900,
|
||||
"modify_valve_opening": {"V1": 0.0, "V2": 0.0},
|
||||
"scheme_name": "valve_case_01",
|
||||
}
|
||||
|
||||
|
||||
def test_flushing_endpoint_passes_required_scheme_name(monkeypatch):
|
||||
module = _load_simulation_module(monkeypatch)
|
||||
captured = {}
|
||||
|
||||
def fake_flushing_analysis(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return "ok"
|
||||
|
||||
monkeypatch.setattr(module, "flushing_analysis", fake_flushing_analysis)
|
||||
client = TestClient(build_test_app(module.router, "/api/v1"))
|
||||
|
||||
response = client.get(
|
||||
"/api/v1/flushing_analysis/",
|
||||
params={
|
||||
"network": "demo",
|
||||
"start_time": "2025-01-02T03:04:05+08:00",
|
||||
"valves": ["V1"],
|
||||
"valves_k": [0.5],
|
||||
"drainage_node_ID": "N1",
|
||||
"flush_flow": 100.0,
|
||||
"duration": 900,
|
||||
"scheme_name": "flush_case_01",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.text == "ok"
|
||||
assert captured == {
|
||||
"name": "demo",
|
||||
"modify_pattern_start_time": "2025-01-02T03:04:05+08:00",
|
||||
"modify_total_duration": 900,
|
||||
"modify_valve_opening": {"V1": 0.5},
|
||||
"drainage_node_ID": "N1",
|
||||
"flushing_flow": 100.0,
|
||||
"scheme_name": "flush_case_01",
|
||||
}
|
||||
|
||||
|
||||
def test_contaminant_endpoint_requires_scheme_name(monkeypatch):
|
||||
module = _load_simulation_module(monkeypatch)
|
||||
client = TestClient(build_test_app(module.router, "/api/v1"))
|
||||
|
||||
response = client.get(
|
||||
"/api/v1/contaminant_simulation/",
|
||||
params={
|
||||
"network": "demo",
|
||||
"start_time": "2025-01-02T03:04:05+08:00",
|
||||
"source": "N1",
|
||||
"concentration": 10.0,
|
||||
"duration": 900,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import json
|
||||
|
||||
from tests.conftest import install_stub, load_module_from_path
|
||||
|
||||
|
||||
def _load_scenarios_module(monkeypatch):
|
||||
install_stub(monkeypatch, "app.services", package=True)
|
||||
install_stub(monkeypatch, "app.algorithms", package=True)
|
||||
install_stub(monkeypatch, "app.algorithms.simulation", package=True)
|
||||
install_stub(monkeypatch, "app.services.simulation", {})
|
||||
install_stub(
|
||||
monkeypatch,
|
||||
"app.algorithms.simulation.runner",
|
||||
{
|
||||
"run_simulation_ex": lambda *args, **kwargs: json.dumps(
|
||||
{"output": {"node_results": [], "link_results": []}}
|
||||
),
|
||||
"from_clock_to_seconds_2": lambda value: value,
|
||||
},
|
||||
)
|
||||
install_stub(monkeypatch, "app.services.scheme_management", {"store_scheme_info": lambda *args, **kwargs: None})
|
||||
install_stub(
|
||||
monkeypatch,
|
||||
"app.services.tjnetwork",
|
||||
{
|
||||
"ChangeSet": type("ChangeSet", (), {}),
|
||||
"OPTION_DEMAND_MODEL_PDA": "OPTION_DEMAND_MODEL_PDA",
|
||||
"OPTION_QUALITY_CHEMICAL": "OPTION_QUALITY_CHEMICAL",
|
||||
"SOURCE_TYPE_SETPOINT": "SOURCE_TYPE_SETPOINT",
|
||||
"add_pattern": lambda *args, **kwargs: None,
|
||||
"add_source": lambda *args, **kwargs: None,
|
||||
"close_project": lambda *args, **kwargs: None,
|
||||
"copy_project": lambda *args, **kwargs: None,
|
||||
"delete_project": lambda *args, **kwargs: None,
|
||||
"get_demand": lambda *args, **kwargs: None,
|
||||
"get_emitter": lambda *args, **kwargs: None,
|
||||
"get_node_links": lambda *args, **kwargs: None,
|
||||
"get_option": lambda *args, **kwargs: None,
|
||||
"get_pattern": lambda *args, **kwargs: None,
|
||||
"get_pipe": lambda *args, **kwargs: None,
|
||||
"get_source": lambda *args, **kwargs: None,
|
||||
"get_time": lambda *args, **kwargs: None,
|
||||
"have_project": lambda *args, **kwargs: False,
|
||||
"is_junction": lambda *args, **kwargs: False,
|
||||
"is_project_open": lambda *args, **kwargs: False,
|
||||
"open_project": lambda *args, **kwargs: None,
|
||||
"set_demand": lambda *args, **kwargs: None,
|
||||
"set_emitter": lambda *args, **kwargs: None,
|
||||
"set_option": lambda *args, **kwargs: None,
|
||||
"set_source": lambda *args, **kwargs: None,
|
||||
"set_time": lambda *args, **kwargs: None,
|
||||
},
|
||||
)
|
||||
return load_module_from_path(
|
||||
"tests_age_analysis_scenarios_module",
|
||||
"app/algorithms/simulation/scenarios.py",
|
||||
)
|
||||
|
||||
|
||||
def test_age_analysis_passes_duration_by_keyword(monkeypatch):
|
||||
module = _load_scenarios_module(monkeypatch)
|
||||
captured = {}
|
||||
|
||||
monkeypatch.setattr(module, "copy_project", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(module, "open_project", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(module, "close_project", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(module, "delete_project", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(module, "have_project", lambda *args, **kwargs: False)
|
||||
monkeypatch.setattr(module, "is_project_open", lambda *args, **kwargs: False)
|
||||
|
||||
def fake_run_simulation_ex(*args, **kwargs):
|
||||
captured["args"] = args
|
||||
captured["kwargs"] = kwargs
|
||||
return json.dumps({"output": {"node_results": [], "link_results": []}})
|
||||
|
||||
monkeypatch.setattr(module, "run_simulation_ex", fake_run_simulation_ex)
|
||||
|
||||
module.age_analysis("demo", "2026-06-03T07:00:00+08:00", 300)
|
||||
|
||||
assert captured["args"] == (
|
||||
"age_Anal_demo",
|
||||
"realtime",
|
||||
"2026-06-03T07:00:00+08:00",
|
||||
)
|
||||
assert captured["kwargs"] == {
|
||||
"duration": 300,
|
||||
"downloading_prohibition": True,
|
||||
}
|
||||
Reference in New Issue
Block a user