451 lines
19 KiB
Python
451 lines
19 KiB
Python
from __future__ import annotations
|
||
|
||
from .core import CommandDoc, CommandOptionDoc, SCHEMA_VERSION
|
||
|
||
GROUP_SUMMARIES: dict[tuple[str, ...], str] = {
|
||
("project",): "项目与项目级元数据相关命令。",
|
||
("network",): "管网节点、管线等基础属性查询命令。",
|
||
("component",): "组件选项与配置读取命令。",
|
||
("component", "option"): "组件选项查询命令。",
|
||
("simulation",): "模拟运行与调度相关命令。",
|
||
("analysis",): "分析计算与诊断相关命令。",
|
||
("analysis", "leakage"): "漏损分析相关命令。",
|
||
("analysis", "leakage", "schemes"): "漏损方案查询命令。",
|
||
("analysis", "burst-detection"): "爆管检测相关命令。",
|
||
("analysis", "burst-detection", "schemes"): "爆管检测方案查询命令。",
|
||
("analysis", "burst-location"): "爆管定位相关命令。",
|
||
("analysis", "burst-location", "schemes"): "爆管定位方案查询命令。",
|
||
("analysis", "risk"): "风险分析相关命令。",
|
||
("analysis", "sensor-placement"): "传感器选址相关命令。",
|
||
("data",): "时序、SCADA、方案和扩展数据查询命令。",
|
||
("data", "timeseries"): "时序数据查询命令。",
|
||
("data", "timeseries", "realtime"): "实时模拟时序查询命令。",
|
||
("data", "timeseries", "scheme"): "方案时序查询命令。",
|
||
("data", "timeseries", "scada"): "SCADA 时序查询命令。",
|
||
("data", "timeseries", "composite"): "复合时序查询命令。",
|
||
("data", "scada"): "SCADA 元数据查询命令。",
|
||
("data", "scheme"): "方案数据查询命令。",
|
||
("data", "extension"): "扩展数据查询命令。",
|
||
("data", "misc"): "其他结果数据查询命令。",
|
||
}
|
||
|
||
HIDDEN_PATH_PREFIXES: tuple[tuple[str, ...], ...] = (
|
||
("analysis", "burst-location"),
|
||
("analysis", "risk"),
|
||
)
|
||
|
||
COMMAND_DOCS: dict[tuple[str, ...], CommandDoc] = {
|
||
("project", "list"): CommandDoc(
|
||
path=("project", "list"),
|
||
summary="列出当前用户可访问项目",
|
||
description="调用 /meta/projects 返回项目列表。",
|
||
examples=("tjwater --auth-context auth.json project list",),
|
||
next_commands=("tjwater --auth-context auth.json project info",),
|
||
output="项目摘要列表",
|
||
),
|
||
("project", "info"): CommandDoc(
|
||
path=("project", "info"),
|
||
summary="读取当前项目元数据",
|
||
description="调用 /meta/project 返回当前 project 详情。",
|
||
examples=("tjwater --auth-context auth.json 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),),
|
||
),
|
||
("network", "get-link-properties"): CommandDoc(
|
||
path=("network", "get-link-properties"),
|
||
summary="读取管线属性",
|
||
description="调用 /getlinkproperties/。",
|
||
options=(CommandOptionDoc("link", "管线 ID", required=True),),
|
||
),
|
||
("component", "option", "schema"): CommandDoc(
|
||
path=("component", "option", "schema"),
|
||
summary="读取选项 schema",
|
||
description="kind 支持 time、energy、pump-energy、network。",
|
||
options=(
|
||
CommandOptionDoc("kind", "选项类型", required=True),
|
||
CommandOptionDoc("pump", "pump-energy 时需要的泵 ID"),
|
||
),
|
||
),
|
||
("component", "option", "get"): CommandDoc(
|
||
path=("component", "option", "get"),
|
||
summary="读取选项属性",
|
||
description="kind 支持 time、energy、pump-energy、network。",
|
||
options=(
|
||
CommandOptionDoc("kind", "选项类型", required=True),
|
||
CommandOptionDoc("pump", "pump-energy 时需要的泵 ID"),
|
||
),
|
||
),
|
||
("simulation", "run"): CommandDoc(
|
||
path=("simulation", "run"),
|
||
summary="触发指定绝对时间的模拟运行",
|
||
description="把 RFC3339 start-time 拆成 simulation_date 与 start_time 后调用 /runsimulationmanuallybydate/;接口本身只负责触发运行,结果需后续通过 data timeseries 在对应时间段查询。",
|
||
options=(
|
||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
||
CommandOptionDoc("duration", "持续分钟数", required=True),
|
||
),
|
||
next_commands=(
|
||
"tjwater --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 --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",
|
||
),
|
||
output="模拟触发结果;实时数据需通过 data timeseries 命令按时间段查询",
|
||
),
|
||
("analysis", "burst"): CommandDoc(
|
||
path=("analysis", "burst"),
|
||
summary="执行爆管分析",
|
||
description="读取 burst-file 并转换为 burst_ID[] / burst_size[];接口本身只返回分析执行结果,方案数据需后续通过 data scheme 命令获取。",
|
||
options=(
|
||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
||
CommandOptionDoc("duration", "持续秒数", required=True),
|
||
CommandOptionDoc("burst-file", "爆管输入 JSON 文件", required=True),
|
||
CommandOptionDoc("scheme", "方案名称"),
|
||
),
|
||
examples=(
|
||
"tjwater --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",
|
||
),
|
||
next_commands=(
|
||
"tjwater --auth-context auth.json data scheme get --name burst_case_01",
|
||
"tjwater --auth-context auth.json data scheme list",
|
||
),
|
||
output="分析执行结果;方案详情需通过 data scheme 命令单独查询",
|
||
),
|
||
("analysis", "valve"): CommandDoc(
|
||
path=("analysis", "valve"),
|
||
summary="执行阀门关闭或隔离分析",
|
||
description="mode=close 使用 valve 列表;mode=isolation 需要 accident element,可选 disabled-valve。",
|
||
examples=(
|
||
"tjwater --auth-context auth.json analysis valve --mode close --start-time 2025-01-02T03:04:05+08:00 --valve V1 --duration 900",
|
||
),
|
||
),
|
||
("analysis", "flushing"): CommandDoc(
|
||
path=("analysis", "flushing"),
|
||
summary="执行冲洗分析",
|
||
description="读取 valve-setting-file 并转换为 valves[] / valves_k[]。",
|
||
),
|
||
("analysis", "age"): CommandDoc(
|
||
path=("analysis", "age"),
|
||
summary="执行水龄分析",
|
||
description="调用 /age_analysis/。",
|
||
),
|
||
("analysis", "contaminant"): CommandDoc(
|
||
path=("analysis", "contaminant"),
|
||
summary="执行污染物模拟",
|
||
description="调用 /contaminant_simulation/。",
|
||
),
|
||
("analysis", "sensor-placement", "kmeans"): CommandDoc(
|
||
path=("analysis", "sensor-placement", "kmeans"),
|
||
summary="执行 KMeans 传感器选址",
|
||
description="使用 POST /pressure_sensor_placement_kmeans/,补齐 username 和 min_diameter。",
|
||
),
|
||
("analysis", "leakage", "identify"): CommandDoc(
|
||
path=("analysis", "leakage", "identify"),
|
||
summary="执行漏损识别",
|
||
description="把 CLI 时间映射到 scada_start / scada_end。",
|
||
),
|
||
("analysis", "leakage", "schemes", "list"): CommandDoc(
|
||
path=("analysis", "leakage", "schemes", "list"),
|
||
summary="列出漏损方案",
|
||
description="调用 /leakage/schemes/。",
|
||
),
|
||
("analysis", "leakage", "schemes", "get"): CommandDoc(
|
||
path=("analysis", "leakage", "schemes", "get"),
|
||
summary="读取漏损方案详情",
|
||
description="调用 /leakage/schemes/{scheme_name}。",
|
||
),
|
||
("analysis", "burst-detection", "detect"): CommandDoc(
|
||
path=("analysis", "burst-detection", "detect"),
|
||
summary="执行爆管检测",
|
||
description="调用 /burst-detection/detect/。",
|
||
),
|
||
("analysis", "burst-detection", "schemes", "list"): CommandDoc(
|
||
path=("analysis", "burst-detection", "schemes", "list"),
|
||
summary="列出爆管检测方案",
|
||
description="调用 /burst-detection/schemes/。",
|
||
),
|
||
("analysis", "burst-detection", "schemes", "get"): CommandDoc(
|
||
path=("analysis", "burst-detection", "schemes", "get"),
|
||
summary="读取爆管检测方案详情",
|
||
description="调用 /burst-detection/schemes/{scheme_name}。",
|
||
),
|
||
("analysis", "burst-location", "locate"): CommandDoc(
|
||
path=("analysis", "burst-location", "locate"),
|
||
summary="执行爆管定位",
|
||
description="调用 /burst-location/locate/;需要 burst-leakage。",
|
||
),
|
||
("analysis", "burst-location", "schemes", "list"): CommandDoc(
|
||
path=("analysis", "burst-location", "schemes", "list"),
|
||
summary="列出爆管定位方案",
|
||
description="调用 /burst-location/schemes/。",
|
||
),
|
||
("analysis", "burst-location", "schemes", "get"): CommandDoc(
|
||
path=("analysis", "burst-location", "schemes", "get"),
|
||
summary="读取爆管定位方案详情",
|
||
description="调用 /burst-location/schemes/{scheme_name}。",
|
||
),
|
||
("analysis", "risk", "pipe-now"): CommandDoc(
|
||
path=("analysis", "risk", "pipe-now"),
|
||
summary="读取单条管道当前风险",
|
||
description="调用 /getpiperiskprobabilitynow/。",
|
||
),
|
||
("analysis", "risk", "pipe-history"): CommandDoc(
|
||
path=("analysis", "risk", "pipe-history"),
|
||
summary="读取单条管道历史风险",
|
||
description="调用 /getpiperiskprobability/。",
|
||
),
|
||
("analysis", "risk", "network"): CommandDoc(
|
||
path=("analysis", "risk", "network"),
|
||
summary="读取全网风险",
|
||
description="组合 /getnetworkpiperiskprobabilitynow/ 与 /getpiperiskprobabilitygeometries/。",
|
||
),
|
||
("data", "timeseries", "realtime", "links"): CommandDoc(
|
||
path=("data", "timeseries", "realtime", "links"),
|
||
summary="查询实时管道时序",
|
||
description="调用 /realtime/links。",
|
||
),
|
||
("data", "timeseries", "realtime", "nodes"): CommandDoc(
|
||
path=("data", "timeseries", "realtime", "nodes"),
|
||
summary="查询实时节点时序",
|
||
description="调用 /realtime/nodes。",
|
||
),
|
||
("data", "timeseries", "realtime", "simulation-by-id-time"): CommandDoc(
|
||
path=("data", "timeseries", "realtime", "simulation-by-id-time"),
|
||
summary="按元素和时间查询实时模拟结果",
|
||
description="调用 /realtime/query/by-id-time。",
|
||
),
|
||
("data", "timeseries", "realtime", "simulation-by-time-property"): CommandDoc(
|
||
path=("data", "timeseries", "realtime", "simulation-by-time-property"),
|
||
summary="按时间和属性查询实时模拟结果",
|
||
description="调用 /realtime/query/by-time-property。",
|
||
),
|
||
("data", "timeseries", "scheme", "links"): CommandDoc(
|
||
path=("data", "timeseries", "scheme", "links"),
|
||
summary="查询方案管道时序",
|
||
description="调用 /scheme/links。",
|
||
),
|
||
("data", "timeseries", "scheme", "node-field"): CommandDoc(
|
||
path=("data", "timeseries", "scheme", "node-field"),
|
||
summary="查询方案节点字段时序",
|
||
description="调用 /scheme/nodes/{node_id}/field。",
|
||
),
|
||
("data", "timeseries", "scheme", "simulation"): CommandDoc(
|
||
path=("data", "timeseries", "scheme", "simulation"),
|
||
summary="查询方案模拟数据",
|
||
description="支持 by-id-time 与 by-scheme-time-property 两种查询。",
|
||
),
|
||
("data", "timeseries", "scada", "query"): CommandDoc(
|
||
path=("data", "timeseries", "scada", "query"),
|
||
summary="查询 SCADA 时序",
|
||
description="device-id 会被转换成后端逗号分隔参数。",
|
||
),
|
||
("data", "timeseries", "composite"): CommandDoc(
|
||
path=("data", "timeseries", "composite"),
|
||
summary="执行复合时序查询",
|
||
description="kind 支持 scada-simulation、element-simulation、element-scada。",
|
||
),
|
||
("data", "timeseries", "composite", "pipeline-health"): CommandDoc(
|
||
path=("data", "timeseries", "composite", "pipeline-health"),
|
||
summary="查询管道健康预测",
|
||
description="调用 /composite/pipeline-health-prediction。",
|
||
),
|
||
("data", "scada", "schema"): CommandDoc(
|
||
path=("data", "scada", "schema"),
|
||
summary="读取 SCADA schema",
|
||
description="kind 支持 device、device-data、element、info。",
|
||
),
|
||
("data", "scada", "get"): CommandDoc(
|
||
path=("data", "scada", "get"),
|
||
summary="读取单条 SCADA 元数据",
|
||
description="kind 支持 device、device-data、element、info。",
|
||
),
|
||
("data", "scada", "list"): CommandDoc(
|
||
path=("data", "scada", "list"),
|
||
summary="列出 SCADA 元数据",
|
||
description="kind 支持 device、element、info;device-data 当前后端无 list 接口。",
|
||
),
|
||
("data", "scheme", "schema"): CommandDoc(
|
||
path=("data", "scheme", "schema"),
|
||
summary="读取方案 schema",
|
||
description="调用 /getschemeschema/。",
|
||
),
|
||
("data", "scheme", "get"): CommandDoc(
|
||
path=("data", "scheme", "get"),
|
||
summary="读取单条方案",
|
||
description="调用 /getscheme/。",
|
||
),
|
||
("data", "scheme", "list"): CommandDoc(
|
||
path=("data", "scheme", "list"),
|
||
summary="列出方案",
|
||
description="调用 /getallschemes/。",
|
||
),
|
||
("data", "extension", "keys"): CommandDoc(
|
||
path=("data", "extension", "keys"),
|
||
summary="列出扩展数据键",
|
||
description="调用 /getallextensiondatakeys/。",
|
||
),
|
||
("data", "extension", "get"): CommandDoc(
|
||
path=("data", "extension", "get"),
|
||
summary="读取扩展数据",
|
||
description="调用 /getextensiondata/。",
|
||
),
|
||
("data", "extension", "list"): CommandDoc(
|
||
path=("data", "extension", "list"),
|
||
summary="列出扩展数据",
|
||
description="调用 /getallextensiondata/。",
|
||
),
|
||
("data", "misc", "sensor-placements"): CommandDoc(
|
||
path=("data", "misc", "sensor-placements"),
|
||
summary="列出传感器布置结果",
|
||
description="调用 /getallsensorplacements/。",
|
||
),
|
||
("data", "misc", "burst-location-results"): CommandDoc(
|
||
path=("data", "misc", "burst-location-results"),
|
||
summary="列出爆管定位结果",
|
||
description="调用 /getallburstlocateresults/。",
|
||
),
|
||
}
|
||
|
||
|
||
def _build_examples(doc: CommandDoc) -> list[str]:
|
||
return list(doc.examples) if doc.examples else [_build_usage(doc)]
|
||
|
||
|
||
def _is_hidden_path(path: tuple[str, ...]) -> bool:
|
||
return any(path[: len(prefix)] == prefix for prefix in HIDDEN_PATH_PREFIXES)
|
||
|
||
|
||
def is_hidden_path(path: tuple[str, ...]) -> bool:
|
||
return _is_hidden_path(path)
|
||
|
||
|
||
def has_subcommands(path_prefix: tuple[str, ...]) -> bool:
|
||
return any(
|
||
not _is_hidden_path(doc.path)
|
||
and doc.path[: len(path_prefix)] == path_prefix
|
||
and len(doc.path) > len(path_prefix)
|
||
for doc in COMMAND_DOCS.values()
|
||
)
|
||
|
||
|
||
def get_group_summary(path_prefix: tuple[str, ...]) -> str:
|
||
return GROUP_SUMMARIES.get(path_prefix, f"{' '.join(path_prefix)} 可用子命令")
|
||
|
||
|
||
def list_capabilities() -> dict[str, object]:
|
||
seen: set[tuple[str, ...]] = set()
|
||
commands: list[dict[str, str]] = []
|
||
for doc in sorted(COMMAND_DOCS.values(), key=lambda item: item.path):
|
||
if _is_hidden_path(doc.path):
|
||
continue
|
||
prefix = doc.path[:1]
|
||
if prefix in seen:
|
||
continue
|
||
seen.add(prefix)
|
||
commands.append(
|
||
{
|
||
"command": " ".join(prefix),
|
||
"summary": get_group_summary(prefix),
|
||
}
|
||
)
|
||
return {
|
||
"ok": True,
|
||
"schema_version": SCHEMA_VERSION,
|
||
"summary": "可用一级菜单",
|
||
"menu_level": 1,
|
||
"commands": commands,
|
||
}
|
||
|
||
|
||
def get_command_doc(path: tuple[str, ...]) -> dict[str, object] | None:
|
||
if _is_hidden_path(path):
|
||
return None
|
||
doc = COMMAND_DOCS.get(path)
|
||
if doc is None:
|
||
return None
|
||
return {
|
||
"ok": True,
|
||
"schema_version": SCHEMA_VERSION,
|
||
"summary": doc.summary,
|
||
"command": " ".join(doc.path),
|
||
"description": doc.description,
|
||
"usage": _build_usage(doc),
|
||
"options": [
|
||
{
|
||
"name": option.name,
|
||
"description": option.description,
|
||
"required": option.required,
|
||
"repeated": option.repeated,
|
||
"default": option.default,
|
||
}
|
||
for option in doc.options
|
||
],
|
||
"examples": _build_examples(doc),
|
||
"next_commands": list(doc.next_commands),
|
||
"output": doc.output,
|
||
}
|
||
|
||
|
||
def list_subcommands(path_prefix: tuple[str, ...], summary: str | None = None) -> dict[str, object]:
|
||
seen: set[str] = set()
|
||
commands: list[dict[str, str]] = []
|
||
for doc in sorted(COMMAND_DOCS.values(), key=lambda item: item.path):
|
||
if _is_hidden_path(doc.path):
|
||
continue
|
||
if doc.path[: len(path_prefix)] != path_prefix or len(doc.path) <= len(path_prefix):
|
||
continue
|
||
subcommand = doc.path[len(path_prefix)]
|
||
if subcommand in seen:
|
||
continue
|
||
seen.add(subcommand)
|
||
current_path = (*path_prefix, subcommand)
|
||
is_group = has_subcommands(current_path)
|
||
usage = f"tjwater {' '.join(current_path)} help" if is_group else (doc.examples[0] if doc.examples else _build_usage(doc))
|
||
commands.append(
|
||
{
|
||
"command": " ".join(current_path),
|
||
"summary": get_group_summary(current_path) if is_group else doc.summary,
|
||
"usage": usage,
|
||
"example": f"tjwater {' '.join(current_path)} help" if is_group else _build_examples(doc)[0],
|
||
}
|
||
)
|
||
return {
|
||
"ok": True,
|
||
"schema_version": SCHEMA_VERSION,
|
||
"summary": summary or get_group_summary(path_prefix),
|
||
"commands": commands,
|
||
}
|
||
|
||
|
||
def _build_usage(doc: CommandDoc) -> str:
|
||
parts = ["tjwater", *doc.path]
|
||
for option in doc.options:
|
||
placeholder = option.name.upper().replace("-", "_")
|
||
if option.required:
|
||
parts.extend([f"--{option.name}", f"<{placeholder}>"])
|
||
else:
|
||
parts.append(f"[--{option.name} <{placeholder}>]")
|
||
return " ".join(parts)
|