6 Commits
Author SHA1 Message Date
jiang bd857ea5e1 fix(ci): test backend in container environment
Generic Container CI/CD / test-build-publish (push) Successful in 1m11s
Server CI/CD v2 / build-test-publish-and-deploy (push) Successful in 1m11s
2026-09-08 18:26:20 +08:00
jiang 8942541759 feat(scada): serve project devices through pooled API 2026-09-08 18:18:30 +08:00
jiang 5966d039de refactor(backend)!: separate algorithm and data layers
Reorganize algorithm packages by business responsibility, move orchestration into services, and keep database access behind pooled repositories.

Harden analysis API validation, remove unsafe legacy simulation endpoints, and add regression and architecture boundary coverage.

BREAKING CHANGE: legacy algorithm module paths and obsolete simulation endpoints are removed.
2026-09-04 17:30:55 +08:00
jiang 9b095c7439 refactor(db)!: clean up business SQL access
- make realtime replacement and analysis result writes transactional\n- consolidate SCADA repositories and remove process-global project state\n- validate SCADA batches and use indexed GIS-backed business queries\n\nBREAKING CHANGE: remove the public analysis result writer and the pipeline-health network_name query parameter.
2026-08-28 11:37:36 +08:00
jiang b74799a39d refactor(db)!: finalize pooled WNDB v2 migration 2026-08-27 17:26:22 +08:00
jiang fa188af0b1 refactor(db)!: adopt project-routed pooled databases
Reorganize WNDB by responsibility and remove legacy scheme endpoints.\n\nRoute analysis and time-series access through project pools, preserve transactional realtime replacement, and refresh GIS materialized views after writes.\n\nAdd database architecture documentation, live pooling coverage, API contract updates, and executable container verification.\n\nBREAKING CHANGE: legacy scheme APIs and flat app.native.wndb module imports are removed.
2026-08-25 18:35:05 +08:00
277 changed files with 15199 additions and 43418 deletions
+1 -1
View File
@@ -19,4 +19,4 @@ logs/
coverage/ coverage/
*.pyc *.pyc
*.dump *.dump
app/algorithms/health/model/my_survival_forest_model_quxi.joblib app/algorithms/pipe_health_prediction/model/my_survival_forest_model_quxi.joblib
+11
View File
@@ -39,6 +39,17 @@ METADATA_DB_PORT="5432"
METADATA_DB_USER="tjwater" METADATA_DB_USER="tjwater"
METADATA_DB_PASSWORD="password" METADATA_DB_PASSWORD="password"
# Per-project synchronous connection pools
PROJECT_PG_CACHE_SIZE="16"
PROJECT_TS_CACHE_SIZE="16"
PROJECT_PG_POOL_MIN_SIZE="0"
PROJECT_PG_POOL_SIZE="4"
PROJECT_PG_MAX_OVERFLOW="2"
PROJECT_TS_POOL_MIN_SIZE="0"
PROJECT_TS_POOL_MAX_SIZE="4"
WNDB_TEMPLATE_DB_NAME="tjwater_v2_template"
WNDB_TEMP_DB_MAX_COUNT="8"
# ============================================ # ============================================
# Keycloak JWT (可选) # Keycloak JWT (可选)
# ============================================ # ============================================
+2 -7
View File
@@ -8,17 +8,12 @@ on:
jobs: jobs:
build-test-publish-and-deploy: build-test-publish-and-deploy:
uses: OrgTJWater/ci-templates/.gitea/workflows/container-cd.yml@main uses: OrgTJWater/ci-templates/.gitea/workflows/container-cd.yml@68c8a9855391baa31d31523674f5812cd24ec604
with: with:
image_name: gitea.waternetwork.cn/orgtjwater/tjwater-backend image_name: gitea.waternetwork.cn/orgtjwater/tjwater-backend
dockerfile: Dockerfile dockerfile: Dockerfile
build_context: . build_context: .
test_command: | test_target: test
test -f app/api/v1/endpoints/access.py
grep -Fq 'api_router.include_router(access.router' app/api/v1/router.py
grep -Fq '@router.get("/projects"' app/api/v1/endpoints/meta.py
grep -Fq '@router.get("/projects/current"' app/api/v1/endpoints/project.py
grep -Fq '@router.post("/audit-events"' app/api/v1/endpoints/audit.py
deploy_service: backend deploy_service: backend
deploy_host: 192.168.1.114 deploy_host: 192.168.1.114
secrets: secrets:
+2 -2
View File
@@ -6,5 +6,5 @@ build/
.env .env
*.dump *.dump
.vscode/ .vscode/
app/algorithms/health/model/my_survival_forest_model_quxi.joblib app/algorithms/pipe_health_prediction/model/my_survival_forest_model_quxi.joblib
inp/ /inp/
+16 -3
View File
@@ -1,4 +1,4 @@
FROM condaforge/miniforge3:latest FROM condaforge/miniforge3:latest AS base
WORKDIR /app WORKDIR /app
@@ -17,13 +17,26 @@ RUN uv pip install --system --no-cache-dir -r requirements.txt
# 本地数据目录和环境变量在运行时通过 Compose 挂载或注入, # 本地数据目录和环境变量在运行时通过 Compose 挂载或注入,
# 不应进入镜像构建上下文。 # 不应进入镜像构建上下文。
COPY app ./app COPY app ./app
RUN python -c "from pathlib import Path; from zipfile import ZipFile; model_dir = Path('app/algorithms/health/model'); zip_path = model_dir / 'my_survival_forest_model_quxi.zip'; joblib_name = 'my_survival_forest_model_quxi.joblib'; joblib_path = model_dir / joblib_name; assert zip_path.exists(), f'Model archive not found: {zip_path}'; archive = ZipFile(zip_path); archive.extract(joblib_name, model_dir); archive.close(); assert joblib_path.exists(), f'Model file not extracted: {joblib_path}'" && \ COPY contracts ./contracts
rm -f app/algorithms/health/model/my_survival_forest_model_quxi.zip COPY infra ./infra
COPY resources ./resources
RUN python -c "from pathlib import Path; from zipfile import ZipFile; model_dir = Path('app/algorithms/pipe_health_prediction/model'); zip_path = model_dir / 'my_survival_forest_model_quxi.zip'; joblib_name = 'my_survival_forest_model_quxi.joblib'; joblib_path = model_dir / joblib_name; assert zip_path.exists(), f'Model archive not found: {zip_path}'; archive = ZipFile(zip_path); archive.extract(joblib_name, model_dir); archive.close(); assert joblib_path.exists(), f'Model file not extracted: {joblib_path}'" && \
rm -f app/algorithms/pipe_health_prediction/model/my_survival_forest_model_quxi.zip
RUN mkdir -p db_inp temp data inp RUN mkdir -p db_inp temp data inp
# 设置 PYTHONPATH 以便 uvicorn 找到 app 模块 # 设置 PYTHONPATH 以便 uvicorn 找到 app 模块
ENV PYTHONPATH=/app ENV PYTHONPATH=/app
FROM base AS test
COPY scripts ./scripts
COPY tests ./tests
RUN python -m compileall -q app && \
python -c "import app.main" && \
python scripts/check_openapi.py && \
pytest -q tests
FROM base AS runner
EXPOSE 8000 EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"] CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
+2 -2
View File
@@ -69,11 +69,11 @@ docker compose -f infra/docker/docker-compose.yml config
项目级 REST 请求通过 `X-Project-Id` 解析元数据中的数据库配置: 项目级 REST 请求通过 `X-Project-Id` 解析元数据中的数据库配置:
- `biz_data` DSN 用于管网业务数据;`{project_code}_template`模拟临时库沿用该 DSN 的主机、端口与凭据,仅替换数据库名。 - `biz_data` DSN 用于管网业务数据;版本模板固定由 `WNDB_TEMPLATE_DB_NAME` 配置(当前为 `tjwater_v2_template`),模拟临时库沿用该 DSN 的主机、端口与凭据,仅替换数据库名。
- `iot_data` DSN 用于 TimescaleDB,始终使用元数据配置的完整 DSN,不再从项目代码推导数据库名。 - `iot_data` DSN 用于 TimescaleDB,始终使用元数据配置的完整 DSN,不再从项目代码推导数据库名。
- 元数据、业务库和 TimescaleDB 可以部署在同一主机,也可以分别部署。 - 元数据、业务库和 TimescaleDB 可以部署在同一主机,也可以分别部署。
使用模板复制或临时方案库的模拟功能时,`biz_data` 账号必须具备现有数据库创建、删除和连接终止操作所需的 PostgreSQL 权限 使用模板复制或临时方案库的模拟功能时,`biz_data` 账号必须具备数据库创建和删除权限;只有显式删除项目时才会终止该项目的现有数据库会话,普通复制不会主动中断复制源会话
## 测试与发布 ## 测试与发布
+4 -44
View File
@@ -1,45 +1,5 @@
"""Algorithm package with side-effect-free, lazy compatibility exports.""" """Pure water-network calculation packages.
from importlib import import_module Application workflows belong in :mod:`app.services`; database and external
from typing import Any system access belongs in :mod:`app.infra` or :mod:`app.native`.
"""
_EXPORT_MODULES = {
"flow_data_clean": "app.algorithms.cleaning",
"pressure_data_clean": "app.algorithms.cleaning",
"pressure_sensor_placement_sensitivity": "app.algorithms.sensor",
"pressure_sensor_placement_kmeans": "app.algorithms.sensor",
"valve_isolation_analysis": "app.algorithms.isolation.valve",
"LeakageIdentifier": "app.algorithms.leakage",
"PipelineHealthAnalyzer": "app.algorithms.health",
"run_burst_location": "app.algorithms.burst_location",
**{
name: "app.algorithms.simulation.scenarios"
for name in (
"convert_to_local_unit",
"burst_analysis",
"valve_close_analysis",
"flushing_analysis",
"contaminant_simulation",
"age_analysis",
"pressure_regulation",
)
},
}
__all__ = list(_EXPORT_MODULES)
def __getattr__(name: str) -> Any:
try:
module_name = _EXPORT_MODULES[name]
except KeyError as exc:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from exc
value = getattr(import_module(module_name), name)
globals()[name] = value
return value
def __dir__() -> list[str]:
return sorted({*globals(), *__all__})
+2 -2
View File
@@ -1,3 +1,3 @@
from app.algorithms.burst_detection.burst_detector import BurstDetector from app.algorithms.burst_detection.pressure_anomaly import PressureAnomalyDetector
__all__ = ["BurstDetector"] __all__ = ["PressureAnomalyDetector"]
@@ -17,7 +17,7 @@ PressureDataInput = (
IGNORED_OBSERVATION_COLUMNS = {"time", "timestamp", "datetime", "date"} IGNORED_OBSERVATION_COLUMNS = {"time", "timestamp", "datetime", "date"}
class BurstDetector: class PressureAnomalyDetector:
"""FFT + IsolationForest based burst detection for daily aligned pressure data.""" """FFT + IsolationForest based burst detection for daily aligned pressure data."""
def __init__( def __init__(
@@ -0,0 +1,3 @@
from .pipeline import run_burst_location
__all__ = ["run_burst_location"]
@@ -11,13 +11,13 @@ import networkx as nx
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from .leak_simulator import cal_signature_pipe_multi_pf from .leak_signature import cal_signature_pipe_multi_pf
from .network_partitioner import ( from .topology_partitioning import (
cal_group_num, cal_group_num,
metis_grouping_pipe_weight, metis_grouping_pipe_weight,
visualize_metis_partition, visualize_metis_partition,
) )
from .similarity_calculator import ( from .similarity_metrics import (
adjust_ratio, adjust_ratio,
cal_similarity_all_multi_new_sq_improve_double_lzr, cal_similarity_all_multi_new_sq_improve_double_lzr,
decode_mode, decode_mode,
@@ -769,4 +769,3 @@ def DN_search_multi_simple_add_flow_count_new(
final_candidates_csv, final_candidates_csv,
) )
@@ -1,5 +1,3 @@
import argparse
import json
import logging import logging
from multiprocessing import cpu_count from multiprocessing import cpu_count
from pathlib import Path from pathlib import Path
@@ -7,12 +5,12 @@ from typing import Any, Iterable
import pandas as pd import pandas as pd
from app.algorithms.burst_location import leak_simulator from app.algorithms.burst_localization import leak_signature
from .burst_locator import ( from .candidate_ranking import (
DN_search_multi_simple_add_flow_count_new, DN_search_multi_simple_add_flow_count_new,
) )
from .network_model import ( from .topology_model import (
_build_node_pipe_maps, _build_node_pipe_maps,
cal_node_coordinate, cal_node_coordinate,
construct_graph, construct_graph,
@@ -26,35 +24,6 @@ DEFAULT_N_WORKERS = max(1, min(cpu_count() - 1, 4))
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _read_id_list_json(path):
if path is None:
return None
data = json.loads(Path(path).read_text(encoding="utf-8"))
if isinstance(data, list):
return [str(item) for item in data]
if isinstance(data, dict):
if "ids" in data and isinstance(data["ids"], list):
return [str(item) for item in data["ids"]]
raise ValueError(f"ID JSON must be list or dict with key 'ids': {path}")
raise ValueError(f"Unsupported ID JSON format: {path}")
def _read_series_csv(path):
if path is None:
return None
df = pd.read_csv(path)
if df.shape[1] < 2:
raise ValueError(f"CSV must contain at least two columns (id,value): {path}")
if {"id", "value"}.issubset(df.columns):
id_col, value_col = "id", "value"
else:
id_col, value_col = df.columns[0], df.columns[1]
series = pd.Series(
df[value_col].values, index=df[id_col].astype(str).values, dtype=float
)
return series
def _align_scada_series( def _align_scada_series(
series: pd.Series, ids: Iterable[str], series_name: str series: pd.Series, ids: Iterable[str], series_name: str
) -> pd.Series: ) -> pd.Series:
@@ -159,7 +128,7 @@ def run_burst_location(
pipe_diameter, pipe_diameter,
) = read_inf_inp(wn) ) = read_inf_inp(wn)
candidate_pipe, _ = leak_simulator.cal_possible_pipe( candidate_pipe, _ = leak_signature.cal_possible_pipe(
burst_leakage, all_pipe, pipe_diameter burst_leakage, all_pipe, pipe_diameter
) )
@@ -267,76 +236,3 @@ def run_burst_location(
"final_candidates_csv": final_candidates_csv, "final_candidates_csv": final_candidates_csv,
"stage_timing_seconds": stage_timing, "stage_timing_seconds": stage_timing,
} }
def _parse_args():
parser = argparse.ArgumentParser(description="爆管定位主函数入口")
parser.add_argument("--wn-inp", required=True, help="EPANET inp 文件路径")
parser.add_argument(
"--pressure-ids-json", required=True, help="压力SCADA ID列表 JSON 文件"
)
parser.add_argument(
"--flow-ids-json", default=None, help="(可选)流量SCADA ID列表 JSON 文件"
)
parser.add_argument(
"--burst-pressure-csv", required=True, help="爆管时压力 CSVid,value"
)
parser.add_argument(
"--normal-pressure-csv", required=True, help="正常时压力 CSVid,value"
)
parser.add_argument(
"--burst-flow-csv", default=None, help="(可选)爆管时流量 CSV(id,value"
)
parser.add_argument(
"--normal-flow-csv", default=None, help="(可选)正常时流量 CSV(id,value"
)
parser.add_argument(
"--burst-leakage", type=float, required=True, help="爆管漏损流量"
)
parser.add_argument(
"--min-dpressure",
type=float,
default=2.0,
help="(可选)最小压降阈值,默认 2.0",
)
parser.add_argument(
"--basic-pressure",
type=float,
default=10.0,
help="(可选)基础服务压力,默认 10.0",
)
parser.add_argument(
"--n-workers",
type=int,
default=DEFAULT_N_WORKERS,
help="(可选)特征中心模拟进程数,默认 max(1, min(cpu_count()-1, 4))",
)
parser.add_argument(
"--final-candidates-csv-path",
default="temp/burst_location/final_round_candidates.csv",
help="(可选)最后一轮候选管道明细 CSV 输出路径",
)
return parser.parse_args()
def main():
args = _parse_args()
result = run_burst_location(
wn_inp_path=args.wn_inp,
pressure_scada_ids=_read_id_list_json(args.pressure_ids_json),
burst_pressure=_read_series_csv(args.burst_pressure_csv),
normal_pressure=_read_series_csv(args.normal_pressure_csv),
burst_leakage=args.burst_leakage,
flow_scada_ids=_read_id_list_json(args.flow_ids_json),
burst_flow=_read_series_csv(args.burst_flow_csv),
normal_flow=_read_series_csv(args.normal_flow_csv),
min_dpressure=args.min_dpressure,
basic_pressure=args.basic_pressure,
n_workers=args.n_workers,
final_candidates_csv_path=args.final_candidates_csv_path,
)
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()
@@ -6,7 +6,7 @@ import random
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from .leak_simulator import simple_add_leak, simple_recover_wn, simple_simulation_pf from .leak_signature import simple_add_leak, simple_recover_wn, simple_simulation_pf
def add_noise_pd(data, noise_type, noise_para): def add_noise_pd(data, noise_type, noise_para):
@@ -195,4 +195,3 @@ def change_para_of_wn(wn, pipe_roughness_change):
pipe.roughness = pipe_roughness_change[pipe_name] pipe.roughness = pipe_roughness_change[pipe_name]
return wn return wn
@@ -1,3 +0,0 @@
from .burst_location import run_burst_location
__all__ = ["run_burst_location"]
-59
View File
@@ -1,59 +0,0 @@
import os
from app.algorithms.cleaning import flow as _flow_module
from app.algorithms.cleaning import pressure as _pressure_module
############################################################
# 流量监测数据清洗 ***卡尔曼滤波法***
############################################################
# 2025/08/21 hxyan
def flow_data_clean(input_csv_file: str) -> str:
"""
读取 input_csv_path 中的每列时间序列,使用一维 Kalman 滤波平滑并用预测值替换基于 3σ 检测出的异常点。
保存输出为:<input_filename>_cleaned.xlsx(与输入同目录),并返回输出文件的绝对路径。如有同名文件存在,则覆盖。
:param: input_csv_file: 输入的 CSV 文件明或路径
:return: 输出文件的绝对路径
"""
# 提供的 input_csv_path 绝对路径,以下为 默认脚本目录下同名 CSV 文件,构建绝对路径,可根据情况修改
# 使用 algorithms 根目录保持与原 data_cleaning.py 一致的行为
script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
input_csv_path = os.path.join(script_dir, input_csv_file)
# 检查文件是否存在
if not os.path.exists(input_csv_path):
raise FileNotFoundError(f"指定的文件不存在: {input_csv_path}")
# 调用 clean_flow_data_kf 函数进行数据清洗
out_xlsx_path = _flow_module.clean_flow_data_kf(input_csv_path)
print("清洗后的数据已保存到:", out_xlsx_path)
############################################################
# 压力监测数据清洗 ***kmean++法***
############################################################
# 2025/08/21 hxyan
def pressure_data_clean(input_csv_file: str) -> str:
"""
读取 input_csv_path 中的每列时间序列,使用Kmean++清洗数据。
保存输出为:<input_filename>_cleaned.xlsx(与输入同目录),并返回输出文件的绝对路径。如有同名文件存在,则覆盖。
原始数据在 sheet 'raw_pressure_data',处理后数据在 sheet 'cleaned_pressusre_data'
:param input_csv_path: 输入的 CSV 文件路径
:return: 输出文件的绝对路径
"""
# 提供的 input_csv_path 绝对路径,以下为 默认脚本目录下同名 CSV 文件,构建绝对路径,可根据情况修改
# 使用 algorithms 根目录保持与原 data_cleaning.py 一致的行为
script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
input_csv_path = os.path.join(script_dir, input_csv_file)
# 检查文件是否存在
if not os.path.exists(input_csv_path):
raise FileNotFoundError(f"指定的文件不存在: {input_csv_path}")
# 调用 clean_pressure_data_km 函数进行数据清洗
out_xlsx_path = _pressure_module.clean_pressure_data_km(input_csv_path)
print("清洗后的数据已保存到:", out_xlsx_path)
@@ -0,0 +1,5 @@
"""Demand allocation calculations."""
from .pipe_length_weighted import allocate_demand_by_pipe_length
__all__ = ["allocate_demand_by_pipe_length"]
@@ -0,0 +1,36 @@
"""Pipe-length-weighted demand allocation.
This module deliberately accepts plain topology data and performs no database
or file access. Application services are responsible for loading topology.
"""
from typing import Any, Mapping
def allocate_demand_by_pipe_length(
demand: float,
topology_nodes: Mapping[str, Mapping[str, Any]],
topology_links: Mapping[str, Mapping[str, Any]],
) -> dict[str, float]:
"""Allocate total demand to junctions by half of each incident link length."""
if not topology_nodes or not topology_links or demand == 0.0:
return {}
total_link_length = sum(
abs(float(link["length"])) for link in topology_links.values()
)
if total_link_length <= 0.0:
return {}
demand_per_length = demand / total_link_length
result: dict[str, float] = {}
for node_id, node in topology_nodes.items():
if node["type"] != "junction":
continue
incident_length = sum(
abs(float(topology_links[link_id]["length"]))
for link_id in node["links"]
)
result[node_id] = incident_length * demand_per_length * 0.5
return result
@@ -0,0 +1,3 @@
from app.algorithms.dma_leakage_estimation.genetic_optimizer import DmaLeakageOptimizer
__all__ = ["DmaLeakageOptimizer"]
@@ -3,7 +3,6 @@ import numpy as np
import pandas as pd import pandas as pd
import os import os
import time import time
import argparse
from multiprocessing import Pool, cpu_count from multiprocessing import Pool, cpu_count
from typing import Any, List, Dict, Union from typing import Any, List, Dict, Union
@@ -70,7 +69,7 @@ def _worker_init(
def _worker_evaluate(raw_ratios: np.ndarray) -> float: def _worker_evaluate(raw_ratios: np.ndarray) -> float:
d = _worker_data d = _worker_data
effective_ratio_map = LeakageIdentifier._effective_area_ratios( effective_ratio_map = DmaLeakageOptimizer._effective_area_ratios(
raw_ratios, raw_ratios,
d["area_ids"], d["area_ids"],
d["nodes_by_area"], d["nodes_by_area"],
@@ -121,7 +120,7 @@ def _worker_evaluate(raw_ratios: np.ndarray) -> float:
_cleanup_temp_files(prefix) _cleanup_temp_files(prefix)
class LeakageIdentifier: class DmaLeakageOptimizer:
FLOW_UNIT_TO_M3S = { FLOW_UNIT_TO_M3S = {
"m3/s": 1.0, "m3/s": 1.0,
"m³/s": 1.0, "m³/s": 1.0,
@@ -543,7 +542,7 @@ class LeakageProblem(Problem):
leak_ratios = x leak_ratios = x
# 将漏损分布归一化 # 将漏损分布归一化
effective_ratio_map = LeakageIdentifier._effective_area_ratios( effective_ratio_map = DmaLeakageOptimizer._effective_area_ratios(
leak_ratios, leak_ratios,
self.area_ids, self.area_ids,
self.nodes_by_area, self.nodes_by_area,
@@ -608,48 +607,3 @@ class LeakageProblem(Problem):
self._pool.close() self._pool.close()
self._pool.join() self._pool.join()
self._pool = None self._pool = None
def main() -> int:
parser = argparse.ArgumentParser(description="漏损区域识别")
parser.add_argument("--inp", required=True, help=".inp 文件路径")
parser.add_argument("--map", help="节点-区域映射 CSV 路径")
parser.add_argument("--scada", help="SCADA 压力 CSV 路径 (观测数据)")
parser.add_argument("--sensors", help="传感器节点 ID 列表 (逗号分隔)")
parser.add_argument("--output", default="Results", help="输出目录")
parser.add_argument("--pop_size", type=int, default=50, help="种群大小")
parser.add_argument("--max_gen", type=int, default=100, help="最大代数")
parser.add_argument("--duration", type=float, default=24, help="模拟时长(小时)")
parser.add_argument("--q_sum", type=float, default=0.241, help="总漏损流量")
parser.add_argument(
"--q_sum_unit",
default="m3/s",
choices=list(LeakageIdentifier.FLOW_UNIT_TO_M3S.keys()),
help="q_sum 输入单位(建议与现场习惯一致,内部统一换算为 m3/s)",
)
args = parser.parse_args()
if not args.map or not args.scada or not args.sensors:
parser.error("--map、--scada、--sensors 为必填")
q_sum_m3s = LeakageIdentifier._flow_to_m3s(args.q_sum, args.q_sum_unit)
sensors = [sensor.strip() for sensor in args.sensors.split(",") if sensor.strip()]
identifier = LeakageIdentifier(
args.inp, sensors, args.map, duration=args.duration, q_sum=q_sum_m3s
)
identifier.run_identification(
args.scada,
args.output,
pop_size=args.pop_size,
max_gen=args.max_gen,
output_flow_unit=args.q_sum_unit,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,206 @@
"""Pure topology partitioning used by DMA leakage estimation."""
import math
from collections import deque
from typing import Any, Iterable, Mapping
import numpy as np
def build_dma_partitions(
sensor_nodes: list[str],
node_coords: Mapping[str, Mapping[str, Any]],
link_entries: Iterable[str],
dma_count: int | None,
) -> tuple[dict[str, str], list[dict[str, Any]]]:
"""Assign every topology node to a sensor-seeded virtual DMA."""
all_nodes = list(node_coords)
if not all_nodes:
raise ValueError("管网中未获取到可分区节点。")
available_sensors = [node for node in sensor_nodes if node in node_coords]
if not available_sensors:
raise ValueError("无可用压力传感器,无法生成虚拟分区。")
area_count = _resolve_dma_count(dma_count, available_sensors, all_nodes)
sensor_area_map = _cluster_sensors_to_areas(
available_sensors, node_coords, area_count
)
adjacency = _build_adjacency(link_entries, all_nodes)
distance_by_sensor = {
sensor: _bfs_distances(adjacency, sensor) for sensor in available_sensors
}
assignment_count = {sensor: 0 for sensor in available_sensors}
area_map: dict[str, str] = {}
for node_id in sorted(all_nodes):
sensor = _choose_sensor_for_node(
node_id,
available_sensors,
node_coords,
distance_by_sensor,
assignment_count,
)
assignment_count[sensor] += 1
area_map[node_id] = sensor_area_map[sensor]
return area_map, _build_area_meta(area_map, sensor_area_map)
def _resolve_dma_count(
dma_count: int | None, sensor_nodes: list[str], all_nodes: list[str]
) -> int:
if dma_count is None:
return min(len(sensor_nodes), len(all_nodes))
if dma_count <= 0:
raise ValueError("dma_count 必须大于 0。")
if dma_count > len(all_nodes):
raise ValueError("dma_count 不能大于可分区节点数量。")
if dma_count > len(sensor_nodes):
raise ValueError("dma_count 不能大于可用传感器数量。")
return dma_count
def _cluster_sensors_to_areas(
sensor_nodes: list[str],
node_coords: Mapping[str, Mapping[str, Any]],
area_count: int,
) -> dict[str, str]:
if area_count >= len(sensor_nodes):
return {sensor: str(index + 1) for index, sensor in enumerate(sensor_nodes)}
points = np.array(
[
[float(node_coords[sensor]["x"]), float(node_coords[sensor]["y"])]
for sensor in sensor_nodes
],
dtype=float,
)
centers = points[:area_count].copy()
labels = np.full(points.shape[0], -1, dtype=int)
for _ in range(20):
distances_squared = (
(points[:, None, :] - centers[None, :, :]) ** 2
).sum(axis=2)
next_labels = distances_squared.argmin(axis=1)
if np.array_equal(labels, next_labels):
break
labels = next_labels
for index in range(area_count):
cluster_points = points[labels == index]
if cluster_points.size > 0:
centers[index] = cluster_points.mean(axis=0)
labels = _restore_empty_area_labels(labels, points, centers, area_count)
return {
sensor: str(int(labels[index]) + 1)
for index, sensor in enumerate(sensor_nodes)
}
def _restore_empty_area_labels(
labels: np.ndarray,
points: np.ndarray,
centers: np.ndarray,
area_count: int,
) -> np.ndarray:
"""Keep every requested area represented when coordinates are degenerate."""
labels = labels.copy()
for missing_area in sorted(set(range(area_count)) - set(labels.tolist())):
area_sizes = {
area: int(np.count_nonzero(labels == area)) for area in range(area_count)
}
donor_area = max(
(area for area, size in area_sizes.items() if size > 1),
key=lambda area: (area_sizes[area], -area),
)
donor_indices = np.flatnonzero(labels == donor_area)
replacement_index = max(
(int(index) for index in donor_indices),
key=lambda index: (
float(np.sum((points[index] - centers[donor_area]) ** 2)),
index,
),
)
labels[replacement_index] = missing_area
centers[missing_area] = points[replacement_index]
return labels
def _build_adjacency(
link_entries: Iterable[str], all_nodes: list[str]
) -> dict[str, set[str]]:
adjacency: dict[str, set[str]] = {node: set() for node in all_nodes}
for link in link_entries:
parts = str(link).split(":")
if len(parts) < 4:
continue
node1, node2 = parts[-2], parts[-1]
if node1 in adjacency and node2 in adjacency:
adjacency[node1].add(node2)
adjacency[node2].add(node1)
return adjacency
def _bfs_distances(adjacency: Mapping[str, set[str]], start: str) -> dict[str, int]:
distances = {start: 0}
queue: deque[str] = deque([start])
while queue:
node = queue.popleft()
for neighbor in adjacency.get(node, set()):
if neighbor in distances:
continue
distances[neighbor] = distances[node] + 1
queue.append(neighbor)
return distances
def _choose_sensor_for_node(
node_id: str,
sensors: list[str],
node_coords: Mapping[str, Mapping[str, Any]],
distance_by_sensor: Mapping[str, Mapping[str, int]],
assignment_count: Mapping[str, int],
) -> str:
min_distance: int | None = None
candidates: list[str] = []
for sensor in sensors:
distance = distance_by_sensor.get(sensor, {}).get(node_id)
if distance is None:
continue
if min_distance is None or distance < min_distance:
min_distance = distance
candidates = [sensor]
elif distance == min_distance:
candidates.append(sensor)
if not candidates:
node_coord = node_coords[node_id]
return min(
sensors,
key=lambda sensor: math.hypot(
float(node_coord["x"]) - float(node_coords[sensor]["x"]),
float(node_coord["y"]) - float(node_coords[sensor]["y"]),
),
)
return min(candidates, key=lambda sensor: (assignment_count[sensor], sensor))
def _build_area_meta(
area_map: Mapping[str, str], sensor_area_map: Mapping[str, str]
) -> list[dict[str, Any]]:
nodes_by_area: dict[str, list[str]] = {}
for node_id, area_id in area_map.items():
nodes_by_area.setdefault(area_id, []).append(node_id)
sensors_by_area: dict[str, list[str]] = {}
for sensor, area_id in sensor_area_map.items():
sensors_by_area.setdefault(area_id, []).append(sensor)
return [
{
"area_id": area_id,
"sensor_nodes": sorted(sensors_by_area.get(area_id, [])),
"node_ids": sorted(nodes_by_area[area_id]),
"node_count": len(nodes_by_area[area_id]),
}
for area_id in sorted(nodes_by_area, key=int)
]
-3
View File
@@ -1,3 +0,0 @@
from app.algorithms.health.analyzer import PipelineHealthAnalyzer
__all__ = ["PipelineHealthAnalyzer"]
-3
View File
@@ -1,3 +0,0 @@
from app.algorithms.isolation.valve import valve_isolation_analysis
__all__ = ["valve_isolation_analysis"]
-167
View File
@@ -1,167 +0,0 @@
from collections import defaultdict, deque
from functools import lru_cache
from typing import Any
from app.services.tjnetwork import (
get_network_link_nodes,
is_node,
get_link_properties,
)
VALVE_LINK_TYPE = "valve"
def _parse_link_entry(link_entry: str) -> tuple[str, str, str, str]:
parts = link_entry.split(":", 3)
if len(parts) != 4:
raise ValueError(f"Invalid link entry format: {link_entry}")
return parts[0], parts[1], parts[2], parts[3]
@lru_cache(maxsize=16)
def _get_network_topology(network: str):
"""
解析并缓存网络拓扑,大幅减少重复的 API 调用和字符串解析开销。
返回:
- pipe_adj: 永久连通的管道/泵邻接表 (dict[str, set])
- all_valves: 所有阀门字典 {id: (n1, n2)}
- link_lookup: 链路快速查表 {id: (n1, n2, type)} 用于快速定位事故点
- node_set: 所有已知节点集合
"""
pipe_adj = defaultdict(set)
all_valves = {}
link_lookup = {}
node_set = set()
# 此处假设 get_network_link_nodes 获取全网数据
for link_entry in get_network_link_nodes(network):
link_id, link_type, node1, node2 = _parse_link_entry(link_entry)
link_type_name = str(link_type).lower()
link_lookup[link_id] = (node1, node2, link_type_name)
node_set.add(node1)
node_set.add(node2)
if link_type_name == VALVE_LINK_TYPE:
all_valves[link_id] = (node1, node2)
else:
# 只有非阀门(管道/泵)才进入永久连通图
pipe_adj[node1].add(node2)
pipe_adj[node2].add(node1)
return pipe_adj, all_valves, link_lookup, node_set
def valve_isolation_analysis(
network: str, accident_elements: str | list[str], disabled_valves: list[str] = None
) -> dict[str, Any]:
"""
关阀搜索/分析:基于拓扑结构确定事故隔离所需关阀。
:param network: 模型名称
:param accident_elements: 事故点(节点或管道/泵/阀门ID),可以是单个ID字符串或ID列表
:param disabled_valves: 故障/无法关闭的阀门ID列表
:return: dict,包含受影响节点、必须关闭阀门、可选阀门等信息
"""
if disabled_valves is None:
disabled_valves_set = set()
else:
disabled_valves_set = set(disabled_valves)
if isinstance(accident_elements, str):
target_elements = [accident_elements]
else:
target_elements = accident_elements
# 1. 获取缓存拓扑 (极快,无 IO)
pipe_adj, all_valves, link_lookup, node_set = _get_network_topology(network)
# 2. 确定起点,优先查表避免 API 调用
start_nodes = set()
for element in target_elements:
if element in node_set:
start_nodes.add(element)
elif element in link_lookup:
n1, n2, _ = link_lookup[element]
start_nodes.add(n1)
start_nodes.add(n2)
else:
# 仅当缓存中没找到时(极少见),才回退到慢速 API
if is_node(network, element):
start_nodes.add(element)
else:
props = get_link_properties(network, element)
n1, n2 = props.get("node1"), props.get("node2")
if n1 and n2:
start_nodes.add(n1)
start_nodes.add(n2)
else:
raise ValueError(
f"Accident element {element} invalid or missing endpoints"
)
# 3. 处理故障阀门 (构建临时增量图)
# 我们不修改 cached pipe_adj,而是建立一个 extra_adj
extra_adj = defaultdict(list)
boundary_valves = {} # 当前有效的边界阀门
for vid, (n1, n2) in all_valves.items():
if vid in disabled_valves_set:
# 故障阀门:视为连通管道
extra_adj[n1].append(n2)
extra_adj[n2].append(n1)
else:
# 正常阀门:视为潜在边界
boundary_valves[vid] = (n1, n2)
# 4. BFS 搜索 (叠加 pipe_adj 和 extra_adj)
affected_nodes: set[str] = set()
queue = deque(start_nodes)
while queue:
node = queue.popleft()
if node in affected_nodes:
continue
affected_nodes.add(node)
# 遍历永久管道邻居
if node in pipe_adj:
for neighbor in pipe_adj[node]:
if neighbor not in affected_nodes:
queue.append(neighbor)
# 遍历故障阀门带来的额外邻居
if node in extra_adj:
for neighbor in extra_adj[node]:
if neighbor not in affected_nodes:
queue.append(neighbor)
# 5. 结果聚合
must_close_valves: list[str] = []
optional_valves: list[str] = []
for valve_id, (n1, n2) in boundary_valves.items():
in_n1 = n1 in affected_nodes
in_n2 = n2 in affected_nodes
if in_n1 and in_n2:
optional_valves.append(valve_id)
elif in_n1 or in_n2:
must_close_valves.append(valve_id)
must_close_valves.sort()
optional_valves.sort()
isolatable = bool(must_close_valves)
result = {
"accident_elements": target_elements,
"disabled_valves": disabled_valves,
"affected_nodes": sorted(affected_nodes) if isolatable else [],
"affected_node_count": len(affected_nodes),
"must_close_valves": must_close_valves,
"optional_valves": optional_valves,
"isolatable": isolatable,
}
if len(target_elements) == 1:
result["accident_element"] = target_elements[0]
return result
-3
View File
@@ -1,3 +0,0 @@
from app.algorithms.leakage.identifier import LeakageIdentifier
__all__ = ["LeakageIdentifier"]
@@ -0,0 +1,5 @@
from app.algorithms.pipe_health_prediction.survival_predictor import (
PipeHealthSurvivalPredictor,
)
__all__ = ["PipeHealthSurvivalPredictor"]
@@ -4,7 +4,7 @@ import pandas as pd
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
class PipelineHealthAnalyzer: class PipeHealthSurvivalPredictor:
""" """
管道健康分析器类使用随机生存森林模型预测管道的生存概率 管道健康分析器类使用随机生存森林模型预测管道的生存概率
@@ -28,11 +28,6 @@ class PipelineHealthAnalyzer:
"model", "model",
"my_survival_forest_model_quxi.joblib", "my_survival_forest_model_quxi.joblib",
) )
# 确保 model 目录存在
model_dir = os.path.dirname(model_path)
if model_dir and not os.path.exists(model_dir):
os.makedirs(model_dir, exist_ok=True)
if not os.path.exists(model_path): if not os.path.exists(model_path):
raise FileNotFoundError(f"模型文件未找到: {model_path}") raise FileNotFoundError(f"模型文件未找到: {model_path}")
@@ -102,7 +97,7 @@ class PipelineHealthAnalyzer:
# 调用说明示例 # 调用说明示例
""" """
在其他项目中使用PipelineHealthAnalyzer类的步骤 在其他项目中使用 PipeHealthSurvivalPredictor 类的步骤
1. 安装依赖在requirements.txt中添加 1. 安装依赖在requirements.txt中添加
joblib==1.5.0 joblib==1.5.0
@@ -112,34 +107,29 @@ class PipelineHealthAnalyzer:
matplotlib==3.9.4 matplotlib==3.9.4
2. 导入类 2. 导入类
from pipeline_health_analyzer import PipelineHealthAnalyzer from survival_predictor import PipeHealthSurvivalPredictor
3. 初始化分析器替换为实际模型路径 3. 初始化分析器替换为实际模型路径
analyzer = PipelineHealthAnalyzer(model_path='path/to/my_survival_forest_model3-10.joblib') predictor = PipeHealthSurvivalPredictor(model_path='path/to/model.joblib')
4. 准备数据pandas DataFrame包含9个特征列 4. 准备数据pandas DataFrame包含4个特征列
import pandas as pd import pandas as pd
data = pd.DataFrame({ data = pd.DataFrame({
'Material': [1, 2], # 示例数据 'Material': [1, 2], # 示例数据
'Diameter': [100, 150], 'Diameter': [100, 150],
'Flow Velocity': [1.5, 2.0], 'Flow Velocity': [1.5, 2.0],
'Pressure': [50, 60], 'Pressure': [50, 60]
'Temperature': [20, 25],
'Precipitation': [0.1, 0.2],
'Location': [1, 2],
'Structural Defects': [0, 1],
'Functional Defects': [0, 0]
}) })
5. 进行预测 5. 进行预测
survival_funcs = analyzer.predict_survival(data) survival_funcs = predictor.predict_survival(data)
6. 查看结果每个样本的生存概率随时间变化 6. 查看结果每个样本的生存概率随时间变化
for i, sf in enumerate(survival_funcs): for i, sf in enumerate(survival_funcs):
print(f"样本 {i+1}: 时间点: {sf.x[:5]}..., 生存概率: {sf.y[:5]}...") print(f"样本 {i+1}: 时间点: {sf.x[:5]}..., 生存概率: {sf.y[:5]}...")
7. 可视化可选 7. 可视化可选
analyzer.plot_survival(survival_funcs, save_path='survival_plot.png') predictor.plot_survival(survival_funcs, save_path='survival_plot.png')
注意 注意
- 数据格式必须匹配特征列表特征值为数值型 - 数据格式必须匹配特征列表特征值为数值型
@@ -0,0 +1 @@
"""Pressure sensor placement calculation implementations."""
@@ -0,0 +1,96 @@
import matplotlib.pyplot as plt
import numpy as np
import sklearn.cluster
import wntr
class KMeansPlacement:
def __init__(self, wn, num_monitors: int, min_diameter_mm: float):
self.cluster_num = num_monitors
self.wn = wn
self.monitor_nodes: list[str] = []
self.coords: list[tuple[float, float]] = []
self.candidate_nodes: list[str] = []
self.min_diameter_mm = min_diameter_mm
def get_junctions_coordinates(self) -> None:
eligible_nodes: set[str] = set()
junction_names = set(self.wn.junction_name_list)
for pipe_name in self.wn.pipe_name_list:
pipe = self.wn.get_link(pipe_name)
if float(pipe.diameter) * 1000 < self.min_diameter_mm:
continue
eligible_nodes.update(
node_id
for node_id in (pipe.start_node_name, pipe.end_node_name)
if node_id in junction_names
)
for junction_name in self.wn.junction_name_list:
if junction_name not in eligible_nodes:
continue
junction = self.wn.get_node(junction_name)
self.candidate_nodes.append(junction_name)
self.coords.append(junction.coordinates)
def select_monitoring_points(self) -> list[str]:
if not self.coords:
self.get_junctions_coordinates()
if self.cluster_num <= 0:
raise ValueError("sensor_count must be greater than zero")
if self.cluster_num > len(self.candidate_nodes):
raise ValueError("符合最小管径条件的候选节点数量少于请求的监测点数量")
coords = np.array(self.coords)
coordinate_span = coords.max(axis=0) - coords.min(axis=0)
coordinate_span[coordinate_span == 0] = 1.0
coords_normalized = (coords - coords.min(axis=0)) / coordinate_span
kmeans = sklearn.cluster.KMeans(n_clusters=self.cluster_num, random_state=42)
kmeans.fit(coords_normalized)
selected_indices: set[int] = set()
for cluster_index, center in enumerate(kmeans.cluster_centers_):
cluster_indices = np.flatnonzero(kmeans.labels_ == cluster_index)
available_indices = [
int(index)
for index in cluster_indices
if int(index) not in selected_indices
]
if not available_indices:
available_indices = [
index
for index in range(len(self.candidate_nodes))
if index not in selected_indices
]
nearest_index = min(
available_indices,
key=lambda index: (
float(np.sum((coords_normalized[index] - center) ** 2)),
index,
),
)
selected_indices.add(nearest_index)
nearest_node = self.candidate_nodes[nearest_index]
self.monitor_nodes.append(nearest_node)
return self.monitor_nodes
def visualize_network(self) -> None:
"""Visualize network with monitoring points."""
wntr.graphics.plot_network(
self.wn,
node_attribute=self.monitor_nodes,
node_size=30,
title="Optimal sensor",
)
plt.show()
def optimize_sensor_placement(
network_model: wntr.network.WaterNetworkModel,
sensor_count: int,
min_diameter_mm: float,
) -> list[str]:
"""Select sensor nodes from an already loaded network model."""
placement = KMeansPlacement(network_model, sensor_count, min_diameter_mm)
return placement.select_monitoring_points()
@@ -903,14 +903,3 @@ def optimize_sensor_placement_from_inp(
sensor_num=sensor_num, sensor_num=sensor_num,
min_diameter=min_diameter, min_diameter=min_diameter,
) )
def get_ID(name: str, sensor_num: int, min_diameter: int) -> list[str]:
"""Compatibility entry point used by the sensor placement service."""
inp_path = Path("db_inp") / f"{name}.db.inp"
return optimize_sensor_placement_from_inp(
inp_path,
sensor_num=sensor_num,
min_diameter=min_diameter,
)
@@ -0,0 +1,6 @@
"""SCADA time-series cleaning algorithms."""
from .flow_series import clean_flow_data_df_kf
from .pressure_series import clean_pressure_data_df_km
__all__ = ["clean_flow_data_df_kf", "clean_pressure_data_df_km"]
@@ -142,11 +142,13 @@ def clean_flow_data_kf(
return os.path.abspath(output_path) return os.path.abspath(output_path)
def clean_flow_data_df_kf(data: pd.DataFrame, show_plot: bool = False) -> dict: def clean_flow_data_df_kf(
data: pd.DataFrame, show_plot: bool = False
) -> pd.DataFrame:
""" """
接收一个 DataFrame 数据结构使用一维 Kalman 滤波平滑并用预测值替换基于 IQR 检测出的异常点 接收一个 DataFrame 数据结构使用一维 Kalman 滤波平滑并用预测值替换基于 IQR 检测出的异常点
区分合理的0值流量转换和异常的0值连续多个0或孤立0 区分合理的0值流量转换和异常的0值连续多个0或孤立0
返回完整的清洗后的字典数据结构 返回完整的清洗后 DataFrame
Args: Args:
data: 输入 DataFrame可包含 time data: 输入 DataFrame可包含 time
@@ -305,42 +307,3 @@ def clean_flow_data_df_kf(data: pd.DataFrame, show_plot: bool = False) -> dict:
# 返回完整的修复后字典 # 返回完整的修复后字典
return cleaned_data return cleaned_data
# # 测试
# if __name__ == "__main__":
# # 默认:脚本目录下同名 CSV 文件
# script_dir = os.path.dirname(os.path.abspath(__file__))
# default_csv = os.path.join(script_dir, "pipe_flow_data_to_clean2.0.csv")
# out = clean_flow_data_kf(default_csv)
# print("清洗后的数据已保存到:", out)
# 测试 clean_flow_data_dict 函数
if __name__ == "__main__":
import random
# 读取 szh_flow_scada.csv 文件
script_dir = os.path.dirname(os.path.abspath(__file__))
csv_path = os.path.join(script_dir, "szh_flow_scada.csv")
data = pd.read_csv(csv_path, header=0, index_col=None, encoding="utf-8")
# 排除 Time 列,随机选择 5 列
columns_to_exclude = ["Time"]
available_columns = [col for col in data.columns if col not in columns_to_exclude]
selected_columns = random.sample(available_columns, 1)
# 将选中的列转换为字典
data_dict = {col: data[col].tolist() for col in selected_columns}
print("选中的列:", selected_columns)
print("原始数据长度:", len(data_dict[selected_columns[0]]))
# 调用函数进行清洗
cleaned_dict = clean_flow_data_df_kf(data_dict, show_plot=True)
# 将清洗后的字典写回 CSV
out_csv = os.path.join(script_dir, f"{selected_columns[0]}_clean.csv")
pd.DataFrame(cleaned_dict).to_csv(out_csv, index=False, encoding="utf-8-sig")
print("已保存清洗结果到:", out_csv)
print("清洗后的字典键:", list(cleaned_dict.keys()))
print("清洗后的数据长度:", len(cleaned_dict[selected_columns[0]]))
print("测试完成:函数运行正常")
@@ -541,39 +541,3 @@ def clean_pressure_data_df_km(data: pd.DataFrame, show_plot: bool = False) -> pd
plt.show() plt.show()
return data_repaired return data_repaired
# 测试
# if __name__ == "__main__":
# # 默认使用脚本目录下的 pressure_raw_data.csv
# script_dir = os.path.dirname(os.path.abspath(__file__))
# default_csv = os.path.join(script_dir, "pressure_raw_data.csv")
# out_path = clean_pressure_data_km(default_csv, show_plot=False)
# print("保存路径:", out_path)
# 测试 clean_pressure_data_dict_km 函数
if __name__ == "__main__":
import random
# 读取 szh_pressure_scada.csv 文件
script_dir = os.path.dirname(os.path.abspath(__file__))
csv_path = os.path.join(script_dir, "szh_pressure_scada.csv")
data = pd.read_csv(csv_path, header=0, index_col=None, encoding="utf-8")
# 排除 Time 列,随机选择 5 列
columns_to_exclude = ["Time"]
available_columns = [col for col in data.columns if col not in columns_to_exclude]
selected_columns = random.sample(available_columns, 5)
# 将选中的列转换为字典
data_dict = {col: data[col].tolist() for col in selected_columns}
print("选中的列:", selected_columns)
print("原始数据长度:", len(data_dict[selected_columns[0]]))
# 调用函数进行清洗
cleaned_dict = clean_pressure_data_df_km(data_dict, show_plot=True)
print("清洗后的字典键:", list(cleaned_dict.keys()))
print("清洗后的数据长度:", len(cleaned_dict[selected_columns[0]]))
print("测试完成:函数运行正常")
-131
View File
@@ -1,131 +0,0 @@
from contextlib import contextmanager
import fcntl
from pathlib import Path
from typing import Any
from app.algorithms.sensor import kmeans as kmeans_sensor
from app.algorithms.sensor import sensitivity
from app.native.wndb.s42_sensor_placement import create_sensor_placement
from app.services.sensor_placement import (
SensorPlacementConflictError,
SensorPlacementValidationError,
validate_sensor_placement_nodes,
)
from app.services.tjnetwork import dump_inp
def _sensor_inp_path(name: str) -> Path:
if (
not name
or name in {".", ".."}
or "/" in name
or "\\" in name
or "\x00" in name
):
raise SensorPlacementValidationError("管网名称不是有效的项目标识")
return Path("db_inp") / f"{name}.db.inp"
@contextmanager
def _sensor_inp_lock(name: str):
inp_path = _sensor_inp_path(name)
inp_path.parent.mkdir(parents=True, exist_ok=True)
lock_path = inp_path.with_suffix(".sensor.lock")
with lock_path.open("w", encoding="utf-8") as lock_file:
try:
fcntl.flock(
lock_file.fileno(),
fcntl.LOCK_EX | fcntl.LOCK_NB,
)
except BlockingIOError as exc:
raise SensorPlacementConflictError(
"当前项目已有监测点优化任务正在运行,请稍后重试"
) from exc
try:
yield inp_path
finally:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
def _create_validated_placement(
name: str,
*,
scheme_name: str,
min_diameter: int,
username: str,
sensor_location: list[str],
) -> dict[str, Any]:
validate_sensor_placement_nodes(name, sensor_location)
return create_sensor_placement(
name,
scheme_name=scheme_name,
min_diameter=min_diameter,
username=username,
sensor_location=sensor_location,
)
def pressure_sensor_placement_sensitivity(
name: str,
scheme_name: str,
sensor_number: int,
min_diameter: int,
username: str,
) -> dict[str, Any]:
"""
基于改进灵敏度法进行压力监测点优化布置
:param name: 数据库名称
:param scheme_name: 监测优化布置方案名称
:param sensor_number: 传感器数目
:param min_diameter: 最小管径
:param username: 用户名
:return: 新建的监测点方案
"""
with _sensor_inp_lock(name):
sensor_location = sensitivity.get_ID(
name=name,
sensor_num=sensor_number,
min_diameter=min_diameter,
)
return _create_validated_placement(
name,
scheme_name=scheme_name,
min_diameter=min_diameter,
username=username,
sensor_location=sensor_location,
)
# 2025/08/21
# 基于kmeans聚类法进行压力监测点优化布置
def pressure_sensor_placement_kmeans(
name: str,
scheme_name: str,
sensor_number: int,
min_diameter: int,
username: str,
) -> dict[str, Any]:
"""
基于聚类法进行压力监测点优化布置
:param name: 数据库名称(注意,此处数据库名称也是inp文件名称,inp文件与pg库名要一样)
:param scheme_name: 监测优化布置方案名称
:param sensor_number: 传感器数目
:param min_diameter: 最小管径
:param username: 用户名
:return: 新建的监测点方案
"""
# dump_inp
with _sensor_inp_lock(name) as inp_path:
dump_inp(name, str(inp_path), "2")
sensor_location = kmeans_sensor.kmeans_sensor_placement(
name=name,
sensor_num=sensor_number,
min_diameter=min_diameter,
)
return _create_validated_placement(
name,
scheme_name=scheme_name,
min_diameter=min_diameter,
username=username,
sensor_location=sensor_location,
)
-71
View File
@@ -1,71 +0,0 @@
import wntr
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import sklearn.cluster
import os
class QD_KMeans(object):
def __init__(self, wn, num_monitors):
# self.inp = inp
self.cluster_num = num_monitors # 聚类中心个数,也即测压点个数
self.wn = wn
self.monitor_nodes = []
self.coords = []
self.junction_nodes = {} # Added missing initialization
def get_junctions_coordinates(self):
for junction_name in self.wn.junction_name_list:
junction = self.wn.get_node(junction_name)
self.junction_nodes[junction_name] = junction.coordinates
self.coords.append(junction.coordinates)
# print(f"Total junctions: {self.junction_coordinates}")
def select_monitoring_points(self):
if not self.coords: # Add check if coordinates are collected
self.get_junctions_coordinates()
coords = np.array(self.coords)
coords_normalized = (coords - coords.min(axis=0)) / (
coords.max(axis=0) - coords.min(axis=0)
)
kmeans = sklearn.cluster.KMeans(n_clusters=self.cluster_num, random_state=42)
kmeans.fit(coords_normalized)
for center in kmeans.cluster_centers_:
distances = np.sum((coords_normalized - center) ** 2, axis=1)
nearest_node = self.wn.junction_name_list[np.argmin(distances)]
self.monitor_nodes.append(nearest_node)
return self.monitor_nodes
def visualize_network(self):
"""Visualize network with monitoring points"""
ax = wntr.graphics.plot_network(
self.wn,
node_attribute=self.monitor_nodes,
node_size=30,
title="Optimal sensor",
)
plt.show()
def kmeans_sensor_placement(name: str, sensor_num: int, min_diameter: int) -> list:
inp_name = f"./db_inp/{name}.db.inp"
wn = wntr.network.WaterNetworkModel(inp_name)
wn_cluster = QD_KMeans(wn, sensor_num)
# Select monitoring pointse
sensor_ids = wn_cluster.select_monitoring_points()
# wn_cluster.visualize_network()
return sensor_ids
if __name__ == "__main__":
# sensorindex = get_ID(name='suzhouhe_2024_cloud_0817', sensor_num=30, min_diameter=500)
sensorindex = kmeans_sensor_placement(name="szh", sensor_num=50, min_diameter=300)
print(sensorindex)
-19
View File
@@ -1,19 +0,0 @@
from app.algorithms.simulation.scenarios import (
convert_to_local_unit,
burst_analysis,
valve_close_analysis,
flushing_analysis,
contaminant_simulation,
age_analysis,
pressure_regulation,
)
__all__ = [
"convert_to_local_unit",
"burst_analysis",
"valve_close_analysis",
"flushing_analysis",
"contaminant_simulation",
"age_analysis",
"pressure_regulation",
]
-867
View File
@@ -1,867 +0,0 @@
import numpy as np
from app.services.tjnetwork import (
ChangeSet,
close_project,
copy_project,
delete_project,
get_pattern,
get_patterns,
get_pump,
get_reservoir,
get_status,
get_tank,
get_time,
have_project,
is_project_open,
open_project,
read_all,
run_project,
set_pattern,
set_status,
set_tank,
set_time,
)
# from get_real_status import *
from datetime import datetime,timedelta
from math import modf
import json
import pytz
import requests
import time
import app.services.project_info as project_info
from app.services.time_api import parse_clock_duration_seconds
url_path = 'http://10.101.15.16:9000/loong' # 内网
# url_path = 'http://183.64.62.100:9057/loong' # 外网
url_real = url_path + '/api/mpoints/realValue'
url_hist = url_path + '/api/curves/data'
PATTERN_TIME_STEP=15.0
DN_900_ID='2498'
DN_500_ID='3854'
DN_1000_ID='3853'
H_RESSURE='2510'
L_PRESURE='2514'
H_TANK='4780'
L_TANK='4854'
H_REGION_1='SA_ZBBDJSCP000002'
H_REGION_2='' #to do
L_REGION_1='SA_ZBBDTJSC000001'
L_REGION_2='SA_R00003'
# reservoir basic height
RESERVOIR_BASIC_HEIGHT = float(250.35)
# regions
regions = ['hp', 'lp']
regions_demand_patterns = {'hp': ['DN900', 'DN500'], 'lp': ['DN1000']} # 出厂水量近似表示用水量
regions_patterns = {'hp': ['ChuanYiJiXiao', 'BeiQuanHuaYuan', 'ZhuangYuanFuDi', 'JingNingJiaYuan',
'308', 'JiaYinYuan', 'XinChengGuoJi', 'YiJingBeiChen', 'ZhongYangXinDu',
'XinHaiJiaYuan', 'DongFengJie', 'DingYaXinYu', 'ZiYunTai', 'XieMaGuangChang',
'YongJinFu', 'BianDianZhan', 'BeiNanDaDao', 'TianShengLiJie', 'XueYuanXiaoQu',
'YunHuaLu', 'GaoJiaQiao', 'LuZuoFuLuXiaDuan', 'TianRunCheng', 'CaoJiaBa',
'PuLingChang', 'QiLongXiaoQu', 'TuanXiao',
'TuanShanBaoZhongShiHua', 'XieMa', 'BeiWenQuanJiuHaoErQi', 'LaiYinHuSiQi',
'DN500', 'DN900'],
'lp': ['PanXiMingDu', 'WanKeJinYuHuaFuGaoCeng', 'KeJiXiao',
'LuGouQiao', 'LongJiangHuaYuan', 'LaoQiZhongDui', 'ShiYanCun', 'TianQiDaSha',
'TianShengPaiChuSuo', 'TianShengShangPin', 'JiaoTang', 'RenMinHuaYuan',
'TaiJiBinJiangYiQi', 'TianQiHuaYuan', 'TaiJiBinJiangErQi', '122Zhong',
'WanKeJinYuHuaFuYangFang', 'ChengBeiCaiShiKou', 'WenXingShe', 'YueLiangTianBBGJCZ',
'YueLiangTian', 'YueLiangTian200', 'ChengTaoChang', 'HuoCheZhan', 'LiangKu', 'QunXingLu',
'JiuYuanErTongYiYuan', 'TangDouHua', 'TaiJiBinJiangErQi(SanJi)',
'ZhangDouHua', 'JinYunXiaoQuDN400',
'DN1000']}
# nodes
monitor_single_patterns = ['ChuanYiJiXiao', 'BeiQuanHuaYuan', 'ZhuangYuanFuDi', 'JingNingJiaYuan',
'308', 'JiaYinYuan', 'XinChengGuoJi', 'YiJingBeiChen', 'ZhongYangXinDu',
'XinHaiJiaYuan', 'DongFengJie', 'DingYaXinYu', 'ZiYunTai', 'XieMaGuangChang',
'YongJinFu', 'PanXiMingDu', 'WanKeJinYuHuaFuGaoCeng', 'KeJiXiao',
'LuGouQiao', 'LongJiangHuaYuan', 'LaoQiZhongDui', 'ShiYanCun', 'TianQiDaSha',
'TianShengPaiChuSuo', 'TianShengShangPin', 'JiaoTang', 'RenMinHuaYuan',
'TaiJiBinJiangYiQi', 'TianQiHuaYuan', 'TaiJiBinJiangErQi', '122Zhong',
'WanKeJinYuHuaFuYangFang']
monitor_single_patterns_id = {'ChuanYiJiXiao': '7338', 'BeiQuanHuaYuan': '7315', 'ZhuangYuanFuDi': '7316',
'JingNingJiaYuan': '7528', '308': '8272', 'JiaYinYuan': '7304',
'XinChengGuoJi': '7325', 'YiJingBeiChen': '7328', 'ZhongYangXinDu': '7329',
'XinHaiJiaYuan': '9138', 'DongFengJie': '7302', 'DingYaXinYu': '7331',
'ZiYunTai': '7420,9059', 'XieMaGuangChang': '7326', 'YongJinFu': '9059',
'PanXiMingDu': '7320', 'WanKeJinYuHuaFuGaoCeng': '7419',
'KeJiXiao': '7305', 'LuGouQiao': '7306', 'LongJiangHuaYuan': '7318',
'LaoQiZhongDui': '9075', 'ShiYanCun': '7309', 'TianQiDaSha': '7323',
'TianShengPaiChuSuo': '7335', 'TianShengShangPin': '7324', 'JiaoTang': '7332',
'RenMinHuaYuan': '7322', 'TaiJiBinJiangYiQi': '7333', 'TianQiHuaYuan': '8235',
'TaiJiBinJiangErQi': '7334', '122Zhong': '7314', 'WanKeJinYuHuaFuYangFang': '7418'}
monitor_unity_patterns = ['BianDianZhan', 'BeiNanDaDao', 'TianShengLiJie', 'XueYuanXiaoQu',
'YunHuaLu', 'GaoJiaQiao', 'LuZuoFuLuXiaDuan', 'TianRunCheng',
'CaoJiaBa', 'PuLingChang', 'QiLongXiaoQu', 'TuanXiao',
'ChengBeiCaiShiKou', 'WenXingShe', 'YueLiangTianBBGJCZ',
'YueLiangTian', 'YueLiangTian200',
'ChengTaoChang', 'HuoCheZhan', 'LiangKu', 'QunXingLu',
'TuanShanBaoZhongShiHua', 'XieMa', 'BeiWenQuanJiuHaoErQi', 'LaiYinHuSiQi',
'JiuYuanErTongYiYuan', 'TangDouHua', 'TaiJiBinJiangErQi(SanJi)',
'ZhangDouHua', 'JinYunXiaoQuDN400',
'DN500', 'DN900', 'DN1000']
monitor_unity_patterns_id = {'BianDianZhan': '7339', 'BeiNanDaDao': '7319', 'TianShengLiJie': '8242',
'XueYuanXiaoQu': '7327', 'YunHuaLu': '7312', 'GaoJiaQiao': '7340',
'LuZuoFuLuXiaDuan': '7343', 'TianRunCheng': '7310', 'CaoJiaBa': '7300',
'PuLingChang': '7307', 'QiLongXiaoQu': '7321', 'TuanXiao': '8963',
'ChengBeiCaiShiKou': '7330', 'WenXingShe': '7311',
'YueLiangTianBBGJCZ': '7313', 'YueLiangTian': '7313', 'YueLiangTian200': '7313',
'ChengTaoChang': '7301', 'HuoCheZhan': '7303',
'LiangKu': '7296', 'QunXingLu': '7308',
'DN500': '3854', 'DN900': '2498', 'DN1000': '3853'}
monitor_patterns = monitor_single_patterns + monitor_unity_patterns
monitor_patterns_id = {**monitor_single_patterns_id, **monitor_unity_patterns_id}
# pumps
pumps_name = ['1#', '2#', '3#', '4#', '5#', '6#', '7#']
pumps = ['PU00000', 'PU00001', 'PU00002', 'PU00003', 'PU00004', 'PU00005', 'PU00006']
variable_frequency_pumps = ['PU00004', 'PU00005', 'PU00006']
pumps_id = {'PU00000': '2747', 'PU00001': '2776', 'PU00002': '2730', 'PU00003': '2787',
'PU00004': '2500', 'PU00005': '2502', 'PU00006': '2504'}
# reservoirs
reservoirs = ['ZBBDJSCP000002', 'R00003']
reservoirs_id = {'ZBBDJSCP000002': '2497', 'R00003': '2571'}
# tanks
tanks = ['ZBBDTJSC000002', 'ZBBDTJSC000001']
tanks_id = {'ZBBDTJSC000002': '4780', 'ZBBDTJSC000001': '9774'}
class DataLoader:
"""数据加载器"""
def __init__(self, project_name, start_time: datetime, end_time: datetime,
pumps_control: dict = None, tank_initial_level_control: dict = None,
region_demand_control: dict = None, downloading_prohibition: bool = False):
self.project_name = project_name # 数据库名
self.current_time = self.round_time(datetime.now(pytz.timezone('Asia/Shanghai')), 1) # 圆整至整分钟
self.current_round_time = self.round_time(self.current_time, int(PATTERN_TIME_STEP))
self.updating_data_flag = True \
if self.current_round_time == self.round_time(start_time, int(PATTERN_TIME_STEP)) \
else False # 判断是否从当前时刻开始模拟(是否更新最新监测数据)
self.downloading_prohibition = downloading_prohibition # 是否禁止下载数据(默认False: 允许下载)
self.updating_data_flag = False if self.downloading_prohibition else self.updating_data_flag
self.pattern_start_index = get_pattern_index(
self.round_time(start_time, int(PATTERN_TIME_STEP)).strftime("%Y-%m-%d %H:%M:%S")) # pattern起始索引
self.pattern_end_index = get_pattern_index(
self.round_time(end_time, int(PATTERN_TIME_STEP)).strftime("%Y-%m-%d %H:%M:%S")) # pattern结束索引
self.pattern_index_list = list(range(self.pattern_start_index, self.pattern_end_index + 1)) # pattern索引列表
self.download_id = self.get_download_id() # 数据下载接口id '7338,7315,7316,...'
self.current_time_download_data = dict(
zip(self.download_id.split(','),
[np.nan]*len(list(self.download_id.split(','))))
) # {id(str): value(float)}
self.current_time_download_data_flag = dict(
zip(self.download_id.split(','),
[False]*len(list(self.download_id.split(','))))
) # 下载数据是否具备实时性, {id(str): flag(bool)}
self.old_flow_data = self.init_dict_of_list(dict(
zip(monitor_patterns,
[[np.nan]] * (len(monitor_patterns)))
)) # {pattern_name(str): flow(float)}
self.old_pattern_factor = self.init_dict_of_list(dict(
zip(monitor_patterns,
[[np.nan]] * (len(monitor_patterns)))
)) # {pattern_name(str): [pattern_factor(float)]}
self.new_flow_data = self.init_dict_of_list(dict(
zip(monitor_patterns,
[[np.nan]] * (len(monitor_patterns)))
)) # {pattern_name(str): flow(float)}
self.new_pattern_factor = self.init_dict_of_list(dict(
zip(monitor_patterns,
[[np.nan]] * (len(monitor_patterns)))
)) # {pattern_name(str): [pattern_factor(float)]}
self.reservoir_data = dict(zip(reservoirs, [np.nan]*len(reservoirs))) # {reservoir_name(str): level(float)}
self.tank_data = dict(zip(tanks, [np.nan] * len(tanks))) # {tank_name(str): level(float)}
self.pump_data = self.init_dict_of_list(
dict(zip(pumps, [[np.nan]]*len(pumps)))) # {pump_name(str): [frequency(float)]}
self.pump_control = pumps_control # {pump_name(str): [frequency(float)]}
self.tank_initial_level_control = tank_initial_level_control # {tank_name(str): level(float)}
self.region_demand_current = dict(zip(regions, [0]*len(regions))) # {region_name(str): total_demand(float)}
self.region_demand_control = region_demand_control # {region_name(str): total_demand(float)}
self.region_demand_control_factor = dict(
zip(regions, [1]*len(regions))) # 区域流量控制系数(用于调整用水量), {region_name(str): factor(float)}
def load_data(self):
"""生成数据集"""
self.download_data() # 下载实时数据
self.get_old_pattern_and_flow() # 读取历史记录pattern信息
self.cal_demand_convert_factor() # 计算用水量转换系数(设定用水量时)
self.set_new_flow() # 设置'更新'流量
self.set_new_pattern_factor() # 设置'更新'pattern factors
self.set_reservoirs() # 设置清水池
self.set_tanks() # 设置调节池
self.set_pumps() # 设置水泵
return self.pattern_start_index
def download_data(self):
"""下载数据"""
if self.updating_data_flag is True:
print('{} -- Start downloading data.'.format(
datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
data_wait_flag = True
while data_wait_flag:
try:
newest_data_time = self.download_real_data(self.download_id) # 获取实时数据
except Exception as e:
print('{}\nWaiting for real data.'.format(e))
time.sleep(1)
else:
print('{} -- Downloading data ok. Newest timestamp: {}.'.format(
datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
newest_data_time.strftime('%Y-%m-%d %H:%M:%S')))
data_wait_flag = False
def cal_current_region_demand(self):
"""计算区域当前用水量"""
if self.updating_data_flag is True:
for region in self.region_demand_current.keys():
total_demand = 0
for pipe in regions_demand_patterns[region]:
total_demand += self.current_time_download_data[monitor_patterns_id[pipe]] # 出厂流量
self.region_demand_current[region] = total_demand
def cal_history_region_demand(self, pattern_index_list):
"""计算区域历史用水量(对应记录的pattern)"""
old_demand = {}
for region in regions:
total_demand = 0
for pipe_pattern_name in regions_demand_patterns[region]:
old_flows, old_patterns = self.get_history_pattern_info(self.project_name, pipe_pattern_name)
for idx in pattern_index_list:
total_demand += old_flows[idx] / 4 # 15分钟水量
old_demand[region] = total_demand
return old_demand
def cal_demand_convert_factor(self):
"""计算用水量转换系数(设定用水量时)"""
self.cal_current_region_demand() # 计算区域当前时刻用水量
old_demand_moment = self.cal_history_region_demand([self.pattern_start_index]) # 计算区域目标时刻总用水量
old_demand_period = self.cal_history_region_demand(self.pattern_index_list) # 计算区域目标时段总用水量
for region in regions:
self.region_demand_control_factor[region] \
= (self.region_demand_current[region] / 4) / old_demand_moment[region] \
if self.updating_data_flag is True else 1
self.region_demand_control_factor[region] = self.region_demand_control[region] / old_demand_period[region] \
if (self.region_demand_control is not None) and (region in self.region_demand_control.keys()) \
else self.region_demand_control_factor[region]
def get_old_pattern_and_flow(self):
"""获取所有pattern的选定时段的历史记录的pattern和flow"""
for idx in monitor_patterns: # 遍历patterns
old_flows, old_patterns = self.get_history_pattern_info(self.project_name, idx)
for pattern_idx in self.pattern_index_list:
old_flow_data = old_flows[pattern_idx]
old_pattern_factor = old_patterns[pattern_idx]
if pattern_idx == self.pattern_start_index: # 起始时刻
self.old_flow_data[idx][0] = old_flow_data
self.old_pattern_factor[idx][0] = old_pattern_factor
else:
self.old_flow_data[idx].append(old_flow_data)
self.old_pattern_factor[idx].append(old_pattern_factor)
def set_new_flow(self):
"""计算模拟时段新流量(相较于历史记录)"""
for idx in self.new_flow_data.keys(): # 遍历patterns
region_name = None
for region in regions_patterns.keys():
if idx in regions_patterns[region]:
region_name = region # pattern所属分区
break
# 实时流量
if self.updating_data_flag is True:
if idx in monitor_unity_patterns[-3:]: # 出水管流量
self.new_flow_data[idx][0] = self.current_time_download_data[monitor_patterns_id[idx]]
else: # 其余流量
self.new_flow_data[idx][0] \
= self.region_demand_control_factor[region_name] * self.old_flow_data[idx][0]
# if idx == 'ZiYunTai':
# idx_a, idx_b = monitor_patterns_id[idx].split(',')
# self.new_flow_data[idx][0] \
# = self.current_time_download_data[idx_a] - self.current_time_download_data[idx_b]
# else:
# self.new_flow_data[idx][0] = self.current_time_download_data[monitor_patterns_id[idx]]
# for data_id in monitor_patterns_id[idx].split(','):
# if (self.current_time_download_data_flag[data_id] is False) \
# and (idx not in [pipe for pipe_list in regions_demand_patterns.values()
# for pipe in pipe_list]): # 无法获取实时数据
# self.new_flow_data[idx][0] \
# = self.region_demand_control_factor[region_name] * self.old_flow_data[idx][0]
# break
# 根据设定用水量修改新流量
if (self.region_demand_control is not None) \
and (region_name in self.region_demand_control.keys()):
for pattern_idx in self.pattern_index_list:
if pattern_idx == self.pattern_start_index: # 起始时刻
self.new_flow_data[idx][0] \
= self.region_demand_control_factor[region_name] * self.old_flow_data[idx][0]
else:
self.new_flow_data[idx].append(
self.region_demand_control_factor[region_name]
* self.old_flow_data[idx][self.pattern_index_list.index(pattern_idx)]
)
def set_new_pattern_factor(self):
"""更新计算选定时段(设定用水量)/时刻的pattern factor"""
pattern_index_list = self.pattern_index_list \
if self.region_demand_control is not None \
else [self.pattern_start_index]
for idx in monitor_patterns: # 遍历patterns
for pattern_idx in pattern_index_list: # 遍历需要修改的pattern(index)
pattern_idx_cls = pattern_index_list.index(pattern_idx) # 转换index(类表存储结构)
old_flow_data = self.old_flow_data[idx][pattern_idx_cls]
old_pattern_factor = self.old_pattern_factor[idx][pattern_idx_cls]
if pattern_idx_cls == 0: # 起始时刻
if idx in monitor_single_patterns:
if not np.isnan(self.new_flow_data[idx][0]):
self.new_pattern_factor[idx][0] = (self.new_flow_data[idx][0] * 1000 / 3600) # m3/h to L/s
if idx in monitor_unity_patterns:
if not np.isnan(self.new_flow_data[idx][0]):
self.new_pattern_factor[idx][0] \
= old_pattern_factor * self.new_flow_data[idx][0] / old_flow_data
else:
if idx in monitor_single_patterns:
if len(self.new_flow_data[idx]) > pattern_idx_cls:
self.new_pattern_factor[idx].append(
(self.new_flow_data[idx][pattern_idx_cls] * 1000 / 3600)) # m3/h to L/s
if idx in monitor_unity_patterns:
if len(self.new_flow_data[idx]) > pattern_idx_cls:
self.new_pattern_factor[idx].append(
old_pattern_factor
* self.new_flow_data[idx][pattern_idx_cls]
/ old_flow_data)
def set_reservoirs(self):
"""设置清水池"""
if self.updating_data_flag is True:
for idx in self.reservoir_data.keys():
if self.current_time_download_data_flag[reservoirs_id[idx]] is False: # 无法获取实时数据
print('There is no current data of reservoir: {}.'.format(idx))
else:
self.reservoir_data[idx] \
= self.current_time_download_data[reservoirs_id[idx]] + RESERVOIR_BASIC_HEIGHT
def set_tanks(self):
"""设置调节池"""
for idx in self.tank_data.keys():
if self.updating_data_flag is True:
if self.current_time_download_data_flag[tanks_id[idx]] is False: # 无法获取实时数据
print('There is no current data of tank: {}.'.format(idx))
else:
self.tank_data[idx] = self.current_time_download_data[tanks_id[idx]]
self.tank_data[idx] = self.tank_initial_level_control[idx] \
if (self.tank_initial_level_control is not None) and (idx in self.tank_initial_level_control) \
else self.tank_data[idx]
def set_pumps(self):
"""设置水泵"""
for idx in self.pump_data.keys():
if self.updating_data_flag is True:
if self.current_time_download_data_flag[pumps_id[idx]] is False: # 无法获取实时数据
print('There is no current data of pump: {}.'.format(idx))
if (self.pump_control is not None) and (idx in self.pump_control.keys()):
self.pump_data[idx] = self.pump_control[idx]
else:
self.pump_data[idx] = [self.current_time_download_data[pumps_id[idx]]]
if (self.pump_control is not None) and (idx in self.pump_control.keys()):
self.pump_data[idx] = self.pump_data[idx] + self.pump_control[idx] \
if len(self.pump_control[idx]) < len(self.pattern_index_list) \
else self.pump_control[idx] # 水泵设定
else:
if (self.pump_control is not None) and (idx in self.pump_control.keys()):
self.pump_data[idx] = self.pump_control[idx]
self.pump_data[idx] \
= list(np.array(self.pump_data[idx]) / 50) \
if idx in variable_frequency_pumps else self.pump_data[idx]
def set_valves(self):
"""设置阀门"""
pass
def download_real_data(self, ids: str):
"""加载实时数据"""
# 数据接口的地址
global url_real
# 设置GET请求的参数
params = {'ids': ids}
# 发送GET请求获取数据
response = requests.get(url_real, params=params)
# 检查响应状态码,200表示请求成功
if response.status_code == 200:
newest_data_time = None # 下载记录数据的最新时间
# 解析响应的JSON数据
data = response.json()
for realValue in data: # 取出逐个id的数据
data_time = convert_utc_to_bj(realValue['datadt']) # datetime
self.current_time_download_data[str(realValue['id'])] \
= float(realValue['realValue']) # {id(str): value(float)}
if data_time > self.current_round_time.replace(tzinfo=None) - timedelta(minutes=5): # 下载数据为实时数据
self.current_time_download_data_flag[str(realValue['id'])] = True
if newest_data_time is None:
newest_data_time = data_time
else:
newest_data_time = data_time if data_time > newest_data_time else newest_data_time # 更新最新时间
if newest_data_time <= self.current_round_time.replace(tzinfo=None) - timedelta(minutes=5): # 最新记录时间早于当前时间
warning_text = 'There is no current data with newest timestamp: {}.'.format(
newest_data_time.strftime('%Y-%m-%d %H:%M:%S'))
delta_time = self.current_round_time.replace(tzinfo=None) - newest_data_time
if delta_time < timedelta(minutes=PATTERN_TIME_STEP): # 时间接近(可等待再次下载)
raise Exception(warning_text)
else:
print(warning_text)
self.updating_data_flag = False
else:
for idx in monitor_unity_patterns[-3:]: # 出水管流量
if self.current_time_download_data_flag[monitor_patterns_id[idx]] is False: # 无法获取出水管流量的实时数据
print('There is no current data of outflow: {}.'.format(idx))
self.updating_data_flag = False
if self.updating_data_flag is False:
print('Abandon updating data with downloaded data.')
return newest_data_time
else:
# 如果请求不成功,打印错误信息
print("请求失败,状态码:", response.status_code)
raise ConnectionError('Cannot download data.')
@ staticmethod
def init_dict_of_list(dict_of_list):
"""初始化值为列表的字典(重新生成列表地址, 防止指向同一列表)"""
for idx in dict_of_list.keys():
dict_of_list[idx] = dict_of_list[idx].copy()
return dict_of_list
@ staticmethod
def get_download_id():
"""生成下载数据项的id"""
# id_list = (list(monitor_single_patterns_id.values())
# + list(monitor_unity_patterns_id.values())
# + list(tanks_id.values())
# + list(reservoirs_id.values())
# + list(pumps_id.values()))
id_list = (list(monitor_unity_patterns_id.values())[-3:]
+ list(tanks_id.values())
+ list(reservoirs_id.values())
+ list(pumps_id.values()))
id_list = sorted(set(id_list), key=id_list.index)
if None in id_list:
id_list.remove(None)
return ','.join(id_list)
@ staticmethod
def get_history_pattern_info(project_name, pattern_name):
"""读取选定pattern的保存的历史pattern信息(flow, factor)"""
factors_list = []
flow_list = []
patterns_info = read_all(project_name,
f"select * from history_patterns_flows where id = '{pattern_name}' order by _order")
for item in patterns_info:
flow_list.append(float(item['flow']))
factors_list.append(float(item['factor']))
return flow_list, factors_list
@ staticmethod
def judge_time(current_time, time_index_list):
"""时间判断"""
current_index \
= time_index_list.index(current_time) if (current_time in time_index_list) else None
return current_index
@staticmethod
def get_time_index_list(start_time: datetime, end_time: datetime, step: int):
"""生成时间索引"""
time_index_list = [] # 时间索引[str]
time_index = start_time
while time_index <= end_time:
time_index_list.append(time_index)
time_index += timedelta(minutes=step)
return time_index_list
@ staticmethod
def round_time(time_: datetime, interval=5):
"""时间向下取整到整n分钟(北京时间): 四舍六入五留双/向下取整"""
# return datetime.fromtimestamp(round(time_.timestamp() / (60 * interval)) * (60 * interval))
return datetime.fromtimestamp(int((time_.timestamp()) // (60 * interval)) * (60 * interval))
def convert_utc_to_bj(utc_time_str):
"""将utc时间(str)转换成北京时间(datetime)"""
# 解析UTC时间字符串为datetime对象
utc_time = datetime.strptime(utc_time_str, '%Y-%m-%dT%H:%M:%SZ')
# 设定UTC时区
utc_timezone = pytz.timezone('UTC')
# 转换为北京时间
beijing_timezone = pytz.timezone('Asia/Shanghai')
beijing_time = utc_time.replace(tzinfo=utc_timezone).astimezone(beijing_timezone).replace(tzinfo=None)
return beijing_time
def get_datetime(cur_datetime:str):
str_format = "%Y-%m-%d %H:%M:%S"
return datetime.strptime(cur_datetime, str_format)
def get_strftime(cur_datetime: datetime):
str_format = "%Y-%m-%d %H:%M:%S"
return cur_datetime.strftime(str_format)
def step_time(cur_datetime:str, step=5):
str_format="%Y-%m-%d %H:%M:%S"
dt=datetime.strptime(cur_datetime,str_format)
dt=dt+timedelta(minutes=step)
return datetime.strftime(dt,str_format)
def get_pattern_index(cur_datetime:str)->int:
str_format="%Y-%m-%d %H:%M:%S"
dt=datetime.strptime(cur_datetime,str_format)
hr=dt.hour
mnt=dt.minute
i=int((hr*60+mnt)/PATTERN_TIME_STEP)
return i
def get_pattern_index_str(cur_datetime:str)->str:
i=get_pattern_index(cur_datetime)
[minN,hrN]=modf(i*PATTERN_TIME_STEP/60)
minN_str=str(int(minN*60))
minN_str=minN_str.zfill(2)
hrN_str=str(int(hrN))
hrN_str=hrN_str.zfill(2)
str_i='{}:{}:00'.format(hrN_str,minN_str)
return str_i
def from_seconds_to_clock (secs: int)->str:
hrs=int(secs/3600)
minutes=int((secs-hrs*3600)/60)
seconds=(secs-hrs*3600-minutes*60)
hrs_str=str(hrs).zfill(2)
minutes_str=str(minutes).zfill(2)
seconds_str=str(seconds).zfill(2)
str_clock='{}:{}:{}'.format(hrs_str,minutes_str,seconds_str)
return str_clock
def from_clock_to_seconds (clock: str)->int:
str_format="%Y-%m-%d %H:%M:%S"
dt=datetime.strptime(clock,str_format)
hr=dt.hour
mnt=dt.minute
seconds=dt.second
return hr*3600+mnt*60+seconds
def from_clock_to_seconds_2 (clock: str)->int:
return parse_clock_duration_seconds(clock)
def from_clock_to_seconds_3 (clock: str)->int:
return parse_clock_duration_seconds(clock)
###convert datetimestring
##"XXXX-XX-XXT00:00:00Z" ->"XXXX-XX-XX 00:00:00"
def trim_time_flag(url_date_time:str)->str:
str_datetime=str.replace(url_date_time,'T',' ')
str_datetime=str.replace(str_datetime,'Z','')
return str_datetime
# 单时间步长模拟
def run_simulation(name:str,start_datetime:str,end_datetime:str=None, duration:int=900)->str:
if(is_project_open(name)):
close_project(name)
open_project(name)
#get_current_data(cur_datetime)
#extract the patternindex from datetime
#e.g. 0: the first time step for 00:00-00:14; 1: the second step for 00:15-00:30
start_datetime=trim_time_flag(start_datetime)
if(end_datetime!=None):
end_datetime=trim_time_flag(end_datetime)
## redistribute the basedemand according to the currentTotalQ and the base_totalQ
# step 1. get_real _data
if end_datetime==None or start_datetime==end_datetime:
end_datetime=step_time(start_datetime)
# # ids=['2498','3854','3853','2510','2514','4780','4854']
# # real_data=get_real_data(ids,start_datetime,end_datetime)
# print(datetime.now(pytz.timezone('Asia/Shanghai')).strftime("%Y-%m-%d %H:%M:%S")+"--获取实时数据完毕\n")
# #step 2. re-distribute the real q to base demand of the node region_sa by region_sa
# regions=get_all_service_area_ids(name)
# total_demands={}
# for region in regions:
# total_demands[region]=get_total_base_demand(name,region)
# region_demand_factor={}
# #Region_ID:SA_ZBBDJSCP000002 高区;SA_R00003+SA_ZBBDTJSC000001 低区
# H_region_real_demands=real_data[DN_900_ID][start_datetime]+real_data[DN_500_ID][start_datetime]
# L_region_real_demands=real_data[DN_1000_ID][start_datetime]
# factor_H_zone=H_region_real_demands/total_demands[H_REGION_1]/3.6 #3.6: m3/h->L/s
# factor_L_zone=L_region_real_demands/(total_demands[L_REGION_1]+total_demands[L_REGION_2])/3.6
# print(datetime.now(pytz.timezone('Asia/Shanghai')).strftime("%Y-%m-%d %H:%M:%S")+"--流量因子计算完毕完毕\n")
# for region in regions:
# region_nodes=get_nodes_in_region(name,region)
# factor=1
# if region==H_REGION_1 or H_REGION_2:
# factor=factor_H_zone
# else:
# factor=factor_L_zone
#
# for node in region_nodes:
# d=get_demand(name,node)
# for r in d['demands']:
# r['demand']=factor*r['demand']
# cs=ChangeSet()
# cs.append(d)
# set_demand(name,cs)
#
# #
# #
# print(datetime.now(pytz.timezone('Asia/Shanghai')).strftime("%Y-%m-%d %H:%M:%S")+"--节点流量重分配完毕\n")
#step 3. set pattern index to the current time,and set duration to 300 secs
#
str_pattern_start=get_pattern_index_str(start_datetime)
dic_time=get_time(name)
dic_time['PATTERN START']=str_pattern_start
if duration !=None:
dic_time['DURATION']=from_seconds_to_clock(duration)
else:
dic_time['DURATION']=dic_time['HYDRAULIC TIMESTEP']
cs=ChangeSet()
cs.operations.append(dic_time)
set_time(name,cs)
# step4. run simulation and save the result to name-time.out for download
#inp_file = 'inp\\'+name+'.inp'
#db_name=name
#dump_inp(db_name,inp_file,'2')
# result=run_inp(db_name)
result=run_project(name)
#json string format
# simulation_result, output, report
result_data=json.loads(result)
#print(result_data['simulation_result'])
print(datetime.now(pytz.timezone('Asia/Shanghai')).strftime("%Y-%m-%d %H:%M:%S")+'run finished successfully\n')
#print(result_data['report'])
return result
# 在线模拟
def run_simulation_ex(name: str, simulation_type: str, start_datetime: str,
end_datetime: str = None, duration: int = 0,
pump_control: dict[str, list] = None, tank_initial_level_control: dict[str, float] = None,
region_demand_control: dict[str, float] = None, valve_control: dict[str, dict] = None,
downloading_prohibition: bool = False) -> str:
time_cost_start = time.perf_counter()
print('{} -- Hydraulic simulation started.'.format(
datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d %H:%M:%S')))
if is_project_open(name):
close_project(name)
if simulation_type.upper() == 'REALTIME': # 实时模拟(修改原数据库)
name_c = name
elif simulation_type.upper() == 'EXTENDED': # 扩展模拟(复制数据库)
name_c = '_'.join([name, 'c'])
if have_project(name_c):
if is_project_open(name_c):
close_project(name_c)
delete_project(name_c)
copy_project(name, name_c) # 备份项目
else:
raise Exception('Incorrect simulation type, choose in (realtime, extended)')
open_project(name_c)
# 时间处理
# extract the pattern index from datetime
# e.g. 0: the first time step for 00:00-00:14; 1: the second step for 00:15-00:30
# start_datetime = get_strftime(convert_utc_to_bj(start_datetime))
start_datetime = trim_time_flag(start_datetime)
if end_datetime is not None:
# end_datetime = get_strftime(convert_utc_to_bj(end_datetime))
end_datetime = trim_time_flag(end_datetime)
# pump name转化/输入值规范化
if pump_control is not None:
for key in list(pump_control.keys()):
pump_control[key] = [pump_control[key]] if type(pump_control[key]) is not list else pump_control[key]
pump_control[pumps[pumps_name.index(key)]] = pump_control.pop(key)
# 重新分配节点(nodes)水量
# 1) (single)base_demand_new=1, pattern_new=real_data
# 2) (unity)base_demand_new=base_demand_old, pattern_new=factor*pattern_old(factor=flow_new/flow_old)
# 获取需水量数据
# a) 历史pattern对应水量(读取保存数据库)
# b) 实时水量(数据接口下载)
# 修改node demand = 1, pattern factor *= demand(monitor single patterns对应node)
# nodes = get_nodes(name_c) # nodes
# for node_name in nodes: # 遍历nodes
# demands_dict = get_demand(name_c, node_name) # {'demands':[{'demand':, 'pattern':}]}
# for demands in demands_dict['demands']:
# if (demands['pattern'] in monitor_single_patterns) and (demands['demand'] != 1): # 1)
# pattern = get_pattern(name_c, demands['pattern'])
# pattern['factors'] = list(demands['demand'] * np.array(pattern['factors'])) # 修改pattern
# cs = ChangeSet()
# cs.append(pattern)
# set_pattern(name_c, cs)
# demands_dict['demands'][
# demands_dict['demands'].index(demands)
# ]['demand'] = 1 # 修改demand
# cs = ChangeSet()
# cs.append(demands_dict)
# set_demand(name_c, cs)
start_time = get_datetime(start_datetime) # datetime
end_time = get_datetime(end_datetime) \
if end_datetime is not None \
else get_datetime(start_datetime) + timedelta(seconds=duration) # datetime
# modify_pattern_start_index = get_pattern_index(start_datetime) # 待修改pattern的起始索引(int)
dataset_loader = DataLoader(project_name=name_c,
start_time=start_time, end_time=end_time,
pumps_control=pump_control, tank_initial_level_control=tank_initial_level_control,
region_demand_control=region_demand_control,
downloading_prohibition=downloading_prohibition) # 实例化数据加载器
modify_index \
= dataset_loader.load_data() # 加载数据(index: 需要修改pattern的factor index, None: 无需修改除水泵和调节池外pattern)
new_patterns \
= dataset_loader.new_pattern_factor # {name: float,} pattern factor(实时: 更新, 其他: 保持/更新(设定用水量时))
tank_init_level = dataset_loader.tank_data # {name: float,} 调节池初始液位(实时: 更新, 其他: 保持/更新(设定液位时))
reservoir_level = dataset_loader.reservoir_data # {name: float,} 水库液位(实时: 更新, 其他: 保持)
pump_freq = dataset_loader.pump_data # {name: [float,]} 水泵频率(实时: 更新, 其他: 保持/更新(设定状态时))
print(datetime.now(pytz.timezone('Asia/Shanghai')).strftime("%Y-%m-%d %H:%M:%S") + " -- Loading data ok.\n")
pattern_name_list = get_patterns(name_c) # 所有pattern
# 修改node pattern/demand
# nodes = get_nodes(name_c) # nodes
# for node_name in nodes: # 遍历nodes
# demands_dict = get_demand(name_c, node_name) # {'demands':[{'demand':, 'pattern':}]}
# for demands in demands_dict['demands']:
# if demands['pattern'] in monitor_single_patterns: # 1)
# demands_dict['demands'][
# demands_dict['demands'].index(demands)
# ]['demand'] = 1 # 修改demand
# pattern = get_pattern(name_c, demands['pattern'])
# pattern['factors'][modify_index] = flow_new[demands['pattern']] # 修改pattern
# cs = ChangeSet()
# cs.append(pattern)
# set_pattern(name_c, cs)
# if demands['pattern'] in pattern_name_list:
# pattern_name_list.remove(demands['pattern']) # 移出待修改pattern列表
# else: # 2)
# continue
# cs = ChangeSet()
# cs.append(demands_dict)
# set_demand(name_c, cs)
for pattern_name in monitor_patterns: # 遍历patterns
if not np.isnan(new_patterns[pattern_name][0]):
pattern = get_pattern(name_c, pattern_name)
pattern['factors'][modify_index:
modify_index + len(new_patterns[pattern_name])] \
= new_patterns[pattern_name]
cs = ChangeSet()
cs.append(pattern)
set_pattern(name_c, cs)
if pattern_name in pattern_name_list:
pattern_name_list.remove(pattern_name) # 移出待修改pattern列表
# 修改清水池(reservoir)液位pattern
for reservoir_name in reservoirs: # 遍历reservoirs
if (not np.isnan(reservoir_level[reservoir_name])) and (reservoir_level[reservoir_name] != 0):
reservoir_pattern = get_pattern(name_c, get_reservoir(name_c, reservoir_name)['pattern'])
reservoir_pattern['factors'][modify_index] = reservoir_level[reservoir_name]
cs = ChangeSet()
cs.append(reservoir_pattern)
set_pattern(name_c, cs)
if reservoir_pattern['id'] in pattern_name_list:
pattern_name_list.remove(reservoir_pattern['id']) # 移出待修改pattern列表
# 修改调节池(tank)初始液位
for tank_name in tanks: # 遍历tanks
if (not np.isnan(tank_init_level[tank_name])) and (tank_init_level[tank_name] != 0):
tank = get_tank(name_c, tank_name)
tank['init_level'] = tank_init_level[tank_name]
cs = ChangeSet()
cs.append(tank)
set_tank(name_c, cs)
# 修改水泵(pump)pattern
for pump_name in pumps: # 遍历pumps
if not np.isnan(pump_freq[pump_name][0]):
pump_pattern = get_pattern(name_c, get_pump(name_c, pump_name)['pattern'])
pump_pattern['factors'][modify_index
:modify_index + len(pump_freq[pump_name])] \
= pump_freq[pump_name]
cs = ChangeSet()
cs.append(pump_pattern)
set_pattern(name_c, cs)
if pump_pattern['id'] in pattern_name_list:
pattern_name_list.remove(pump_pattern['id']) # 移出待修改pattern列表
# 修改阀门(valve)status和setting
if valve_control is not None:
for valve in valve_control.keys():
status = get_status(name_c, valve)
if 'status' in valve_control[valve].keys():
status['status'] = valve_control[valve]['status']
if 'setting' in valve_control[valve].keys():
status['setting'] = valve_control[valve]['setting']
if 'k' in valve_control[valve].keys():
valve_k = valve_control[valve]['k']
if valve_k == 0:
status['status'] = 'CLOSED'
else:
status['setting'] = 0.1036 * pow(valve_k, -3.105)
cs = ChangeSet()
cs.append(status)
set_status(name_c, cs)
print('Finish demands amending, unmodified patterns: {}.'.format(pattern_name_list))
# 修改时间信息
str_pattern_start = get_pattern_index_str(
DataLoader.round_time(start_time, int(PATTERN_TIME_STEP)).strftime("%Y-%m-%d %H:%M:%S"))
dic_time = get_time(name_c)
dic_time['PATTERN START'] = str_pattern_start
if duration is not None:
dic_time['DURATION'] = from_seconds_to_clock(duration)
else:
dic_time['DURATION'] = dic_time['HYDRAULIC TIMESTEP']
cs = ChangeSet()
cs.operations.append(dic_time)
set_time(name_c, cs)
# 运行并返回结果
result = run_project(name_c)
time_cost_end = time.perf_counter()
print('{} -- Hydraulic simulation finished, cost time: {:.2f} s.'.format(
datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d %H:%M:%S'),
time_cost_end - time_cost_start))
close_project(name_c)
return result
if __name__ == '__main__':
# if get_current_data()==True:
# tQ=get_current_total_Q()
# print(f"the current tQ is {tQ}\n")
# data=get_hist_data(ids,conver_beingtime_to_ucttime('2024-04-10 15:05:00'),conver_beingtime_to_ucttime('2024-04-10 15:10:00'))
# open_project("beibeizone")
# read_inp("beibeizone","beibeizone-export_nochinese.inp")
# run_simulation("beibeizone","2024-04-01T08:00:00Z")
# read_inp('bb_server', 'model20_en.inp')
run_simulation_ex(
name=project_info.name, simulation_type='extended', start_datetime='2024-11-09T02:30:00Z',
# end_datetime='2024-05-30T16:00:00Z',
# duration=0,
# pump_control={'PU00006': [45, 40]}
# region_demand_control={'hp': 6000, 'lp': 2000}
)
@@ -0,0 +1,3 @@
from app.algorithms.valve_isolation.topology_search import valve_isolation_analysis
__all__ = ["valve_isolation_analysis"]
@@ -0,0 +1,103 @@
"""Topology-only valve isolation search."""
from collections import defaultdict, deque
from typing import Any, Iterable
VALVE_LINK_TYPE = "valve"
def _parse_link_entry(link_entry: str) -> tuple[str, str, str, str]:
parts = link_entry.split(":", 3)
if len(parts) != 4:
raise ValueError(f"Invalid link entry format: {link_entry}")
return parts[0], parts[1], parts[2], parts[3]
def valve_isolation_analysis(
link_entries: Iterable[str],
accident_elements: str | list[str],
disabled_valves: list[str] | None = None,
) -> dict[str, Any]:
"""Determine boundary valves and affected nodes from a topology snapshot."""
disabled_valves_set = set(disabled_valves or [])
target_elements = (
[accident_elements]
if isinstance(accident_elements, str)
else accident_elements
)
pipe_adj: dict[str, set[str]] = defaultdict(set)
all_valves: dict[str, tuple[str, str]] = {}
link_lookup: dict[str, tuple[str, str, str]] = {}
node_set: set[str] = set()
for link_entry in link_entries:
link_id, link_type, node1, node2 = _parse_link_entry(link_entry)
link_type_name = str(link_type).lower()
link_lookup[link_id] = (node1, node2, link_type_name)
node_set.update((node1, node2))
if link_type_name == VALVE_LINK_TYPE:
all_valves[link_id] = (node1, node2)
else:
pipe_adj[node1].add(node2)
pipe_adj[node2].add(node1)
start_nodes: set[str] = set()
for element in target_elements:
if element in node_set:
start_nodes.add(element)
elif element in link_lookup:
node1, node2, _ = link_lookup[element]
start_nodes.update((node1, node2))
else:
raise ValueError(f"Accident element {element} was not found in topology")
extra_adj: dict[str, list[str]] = defaultdict(list)
boundary_valves: dict[str, tuple[str, str]] = {}
for valve_id, (node1, node2) in all_valves.items():
if valve_id in disabled_valves_set:
extra_adj[node1].append(node2)
extra_adj[node2].append(node1)
else:
boundary_valves[valve_id] = (node1, node2)
affected_nodes: set[str] = set()
queue = deque(start_nodes)
while queue:
node = queue.popleft()
if node in affected_nodes:
continue
affected_nodes.add(node)
queue.extend(pipe_adj.get(node, set()) - affected_nodes)
queue.extend(
neighbor
for neighbor in extra_adj.get(node, ())
if neighbor not in affected_nodes
)
must_close_valves: list[str] = []
optional_valves: list[str] = []
for valve_id, (node1, node2) in boundary_valves.items():
node1_affected = node1 in affected_nodes
node2_affected = node2 in affected_nodes
if node1_affected and node2_affected:
optional_valves.append(valve_id)
elif node1_affected or node2_affected:
must_close_valves.append(valve_id)
must_close_valves.sort()
optional_valves.sort()
isolatable = bool(must_close_valves)
result: dict[str, Any] = {
"accident_elements": target_elements,
"disabled_valves": disabled_valves,
"affected_nodes": sorted(affected_nodes) if isolatable else [],
"affected_node_count": len(affected_nodes),
"must_close_valves": must_close_valves,
"optional_valves": optional_valves,
"isolatable": isolatable,
}
if len(target_elements) == 1:
result["accident_element"] = target_elements[0]
return result
+20
View File
@@ -8,6 +8,8 @@ from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.native.wndb.core.database import MaterializedViewRefreshAfterCommitError
class ProblemDetails(BaseModel): class ProblemDetails(BaseModel):
"""RFC 9457 compatible error response used by the REST contract.""" """RFC 9457 compatible error response used by the REST contract."""
@@ -53,6 +55,24 @@ def _problem_response(
def install_problem_details_handlers(app: FastAPI) -> None: def install_problem_details_handlers(app: FastAPI) -> None:
@app.exception_handler(MaterializedViewRefreshAfterCommitError)
async def materialized_view_refresh_error_handler(
request: Request,
exc: MaterializedViewRefreshAfterCommitError,
) -> JSONResponse:
response = _problem_response(
request,
status_code=503,
title="Materialized view refresh failed",
detail=(
f"Project {exc.project!r} changes were committed, but GIS query "
"views could not be refreshed. Do not repeat the write blindly."
),
code="materialized_view_refresh_failed_after_commit",
)
response.headers["X-TJWater-Changes-Committed"] = "true"
return response
@app.exception_handler(RequestValidationError) @app.exception_handler(RequestValidationError)
async def validation_error_handler( async def validation_error_handler(
request: Request, request: Request,
+8 -3
View File
@@ -1,8 +1,10 @@
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Body from fastapi import APIRouter, Depends, HTTPException, Body
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from starlette.concurrency import run_in_threadpool
from app.auth.keycloak_dependencies import get_current_keycloak_username from app.auth.keycloak_dependencies import get_current_keycloak_username
from app.services.burst_detection import ( from app.services.burst_detection import (
@@ -43,8 +45,7 @@ class BurstDetectionRequest(BaseModel):
sensor_nodes: list[str] | None = Field(None, description="传感器节点列表") sensor_nodes: list[str] | None = Field(None, description="传感器节点列表")
scheme_name: str | None = Field(None, description="方案名称") scheme_name: str | None = Field(None, description="方案名称")
data_source: str = Field("monitoring", description="数据来源:monitoring(监测)或simulation(模拟)") data_source: str = Field("monitoring", description="数据来源:monitoring(监测)或simulation(模拟)")
simulation_scheme_name: str | None = Field(None, description="模拟方案名称") simulation_run_id: UUID | None = Field(None, description="分析模拟运行 ID")
simulation_scheme_type: str | None = Field(None, description="模拟方案类型")
@router.post( @router.post(
@@ -73,6 +74,10 @@ async def detect_burst(
HTTPException: 当处理过程中发生错误时 HTTPException: 当处理过程中发生错误时
""" """
try: try:
return run_burst_detection(**data.model_dump(), username=username) return await run_in_threadpool(
run_burst_detection,
**data.model_dump(),
username=username,
)
except Exception as exc: except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc)) raise HTTPException(status_code=400, detail=str(exc))
+9 -4
View File
@@ -2,9 +2,11 @@ from typing import Any
from datetime import datetime from datetime import datetime
from typing import Literal from typing import Literal
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Body from fastapi import APIRouter, Depends, HTTPException, Body
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from starlette.concurrency import run_in_threadpool
from app.auth.keycloak_dependencies import get_current_keycloak_username from app.auth.keycloak_dependencies import get_current_keycloak_username
from app.services.burst_location import ( from app.services.burst_location import (
@@ -32,9 +34,8 @@ class BurstLocationRequest(BaseModel):
scada_normal_start: datetime | None = Field(None, description="监测数据正常工况开始时间") scada_normal_start: datetime | None = Field(None, description="监测数据正常工况开始时间")
scada_normal_end: datetime | None = Field(None, description="监测数据正常工况结束时间") scada_normal_end: datetime | None = Field(None, description="监测数据正常工况结束时间")
use_scada_flow: bool = Field(False, description="是否使用SCADA流量数据") use_scada_flow: bool = Field(False, description="是否使用SCADA流量数据")
scheme_name: str | None = Field(None, description="方案名称") scheme_name: str | None = Field(None, description="爆管定位运行名称")
simulation_scheme_name: str | None = Field(None, description="模拟方案名称") simulation_run_id: UUID | None = Field(None, description="分析模拟运行 ID")
simulation_scheme_type: str | None = Field(None, description="模拟方案类型")
@router.post( @router.post(
@@ -63,6 +64,10 @@ async def locate_burst(
HTTPException: 当数据类型或值不正确时 HTTPException: 当数据类型或值不正确时
""" """
try: try:
return run_burst_location_by_network(**data.model_dump(), username=username) return await run_in_threadpool(
run_burst_location_by_network,
**data.model_dump(),
username=username,
)
except (TypeError, ValueError) as exc: except (TypeError, ValueError) as exc:
raise HTTPException(status_code=400, detail=str(exc)) raise HTTPException(status_code=400, detail=str(exc))
+13 -13
View File
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Request, Query, Path, Body from typing import Any
from typing import Any, List, Dict, Union
from fastapi import APIRouter, Body, Query
from app.services.tjnetwork import ( from app.services.tjnetwork import (
Any,
ChangeSet, ChangeSet,
get_control, get_control,
get_control_schema, get_control_schema,
@@ -14,7 +14,7 @@ from app.services.tjnetwork import (
router = APIRouter() router = APIRouter()
@router.get("/network-schemas/control", summary="获取控制架构", description="获取网络中控制对象的架构定义") @router.get("/network-schemas/control", summary="获取控制架构", description="获取网络中控制对象的架构定义")
async def fastapi_get_control_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fastapi_get_control_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
"""获取控制架构。 """获取控制架构。
返回指定网络中控制对象的属性架构定义。 返回指定网络中控制对象的属性架构定义。
@@ -22,7 +22,7 @@ async def fastapi_get_control_schema(network: str = Query(..., description="管
return get_control_schema(network) return get_control_schema(network)
@router.get("/controls/properties", summary="获取控制属性", description="获取指定网络中的控制属性信息") @router.get("/controls/properties", summary="获取控制属性", description="获取指定网络中的控制属性信息")
async def fastapi_get_control_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]: def fastapi_get_control_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
"""获取控制属性。 """获取控制属性。
返回指定网络中的控制对象属性信息。 返回指定网络中的控制对象属性信息。
@@ -30,19 +30,19 @@ async def fastapi_get_control_properties(network: str = Query(..., description="
return get_control(network) return get_control(network)
@router.patch("/controls/properties", response_model=None, summary="设置控制属性", description="更新指定网络中的控制属性") @router.patch("/controls/properties", response_model=None, summary="设置控制属性", description="更新指定网络中的控制属性")
async def fastapi_set_control_properties( def fastapi_set_control_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置控制属性。 """设置控制属性。
更新指定网络中的控制属性值。 更新指定网络中的控制属性值。
""" """
props = await req.json() props = payload
return set_control(network, ChangeSet(props)) return set_control(network, ChangeSet(props))
@router.get("/rule-schemas", summary="获取规则架构", description="获取网络中规则对象的架构定义") @router.get("/rule-schemas", summary="获取规则架构", description="获取网络中规则对象的架构定义")
async def fastapi_get_rule_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fastapi_get_rule_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
"""获取规则架构。 """获取规则架构。
返回指定网络中规则对象的属性架构定义。 返回指定网络中规则对象的属性架构定义。
@@ -50,7 +50,7 @@ async def fastapi_get_rule_schema(network: str = Query(..., description="管网
return get_rule_schema(network) return get_rule_schema(network)
@router.get("/rule-properties", summary="获取规则属性", description="获取指定网络中的规则属性信息") @router.get("/rule-properties", summary="获取规则属性", description="获取指定网络中的规则属性信息")
async def fastapi_get_rule_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]: def fastapi_get_rule_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
"""获取规则属性。 """获取规则属性。
返回指定网络中的规则对象属性信息。 返回指定网络中的规则对象属性信息。
@@ -58,13 +58,13 @@ async def fastapi_get_rule_properties(network: str = Query(..., description="管
return get_rule(network) return get_rule(network)
@router.patch("/rule-properties", response_model=None, summary="设置规则属性", description="更新指定网络中的规则属性") @router.patch("/rule-properties", response_model=None, summary="设置规则属性", description="更新指定网络中的规则属性")
async def fastapi_set_rule_properties( def fastapi_set_rule_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置规则属性。 """设置规则属性。
更新指定网络中的规则属性值。 更新指定网络中的规则属性值。
""" """
props = await req.json() props = payload
return set_rule(network, ChangeSet(props)) return set_rule(network, ChangeSet(props))
+14 -14
View File
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Request, Query, Path, Body from typing import Any
from typing import Any, List, Dict, Union
from fastapi import APIRouter, Body, Query
from app.services.tjnetwork import ( from app.services.tjnetwork import (
Any,
ChangeSet, ChangeSet,
add_curve, add_curve,
delete_curve, delete_curve,
@@ -15,7 +15,7 @@ from app.services.tjnetwork import (
router = APIRouter() router = APIRouter()
@router.get("/network-schemas/curve", summary="获取曲线架构", description="获取网络中曲线对象的架构定义") @router.get("/network-schemas/curve", summary="获取曲线架构", description="获取网络中曲线对象的架构定义")
async def fastapi_get_curve_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fastapi_get_curve_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
"""获取曲线架构。 """获取曲线架构。
返回指定网络中曲线对象的属性架构定义。 返回指定网络中曲线对象的属性架构定义。
@@ -23,23 +23,23 @@ async def fastapi_get_curve_schema(network: str = Query(..., description="管网
return get_curve_schema(network) return get_curve_schema(network)
@router.post("/curves", response_model=None, summary="添加曲线", description="在网络中添加一条新的曲线") @router.post("/curves", response_model=None, summary="添加曲线", description="在网络中添加一条新的曲线")
async def fastapi_add_curve( def fastapi_add_curve(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
curve: str = Query(..., description="曲线ID"), curve: str = Query(..., description="曲线ID"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""添加曲线。 """添加曲线。
在指定网络中创建一条新的曲线,并设置其初始属性。 在指定网络中创建一条新的曲线,并设置其初始属性。
""" """
props = await req.json() props = payload
ps = { ps = {
"id": curve, "id": curve,
} | props } | props
return add_curve(network, ChangeSet(ps)) return add_curve(network, ChangeSet(ps))
@router.delete("/curves", response_model=None, summary="删除曲线", description="从网络中删除指定的曲线") @router.delete("/curves", response_model=None, summary="删除曲线", description="从网络中删除指定的曲线")
async def fastapi_delete_curve( def fastapi_delete_curve(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
curve: str = Query(..., description="曲线ID") curve: str = Query(..., description="曲线ID")
) -> ChangeSet: ) -> ChangeSet:
@@ -51,7 +51,7 @@ async def fastapi_delete_curve(
return delete_curve(network, ChangeSet(ps)) return delete_curve(network, ChangeSet(ps))
@router.get("/curves/properties", summary="获取曲线属性", description="获取指定曲线的属性信息") @router.get("/curves/properties", summary="获取曲线属性", description="获取指定曲线的属性信息")
async def fastapi_get_curve_properties( def fastapi_get_curve_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
curve: str = Query(..., description="曲线ID") curve: str = Query(..., description="曲线ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -62,21 +62,21 @@ async def fastapi_get_curve_properties(
return get_curve(network, curve) return get_curve(network, curve)
@router.patch("/curves/properties", response_model=None, summary="设置曲线属性", description="更新指定曲线的属性") @router.patch("/curves/properties", response_model=None, summary="设置曲线属性", description="更新指定曲线的属性")
async def fastapi_set_curve_properties( def fastapi_set_curve_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
curve: str = Query(..., description="曲线ID"), curve: str = Query(..., description="曲线ID"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置曲线属性。 """设置曲线属性。
更新指定曲线的属性值。 更新指定曲线的属性值。
""" """
props = await req.json() props = payload
ps = {"id": curve} | props ps = {"id": curve} | props
return set_curve(network, ChangeSet(ps)) return set_curve(network, ChangeSet(ps))
@router.get("/curves", summary="获取所有曲线", description="获取网络中的所有曲线列表") @router.get("/curves", summary="获取所有曲线", description="获取网络中的所有曲线列表")
async def fastapi_get_curves(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]: def fastapi_get_curves(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
"""获取所有曲线。 """获取所有曲线。
返回指定网络中的所有曲线ID列表。 返回指定网络中的所有曲线ID列表。
@@ -84,7 +84,7 @@ async def fastapi_get_curves(network: str = Query(..., description="管网名称
return get_curves(network) return get_curves(network)
@router.get("/curves/existence", summary="检查曲线存在性", description="检查指定的曲线是否存在") @router.get("/curves/existence", summary="检查曲线存在性", description="检查指定的曲线是否存在")
async def fastapi_is_curve( def fastapi_is_curve(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
curve: str = Query(..., description="曲线ID") curve: str = Query(..., description="曲线ID")
) -> bool: ) -> bool:
+23 -23
View File
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Request, Query, Path, Body from typing import Any
from typing import Any, List, Dict, Union
from fastapi import APIRouter, Body, Query
from app.services.tjnetwork import ( from app.services.tjnetwork import (
Any,
ChangeSet, ChangeSet,
get_energy, get_energy,
get_energy_schema, get_energy_schema,
@@ -20,7 +20,7 @@ from app.services.tjnetwork import (
router = APIRouter() router = APIRouter()
@router.get("/network-schemas/time", summary="获取时间选项架构", description="获取网络中时间选项的架构定义") @router.get("/network-schemas/time", summary="获取时间选项架构", description="获取网络中时间选项的架构定义")
async def fastapi_get_time_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fastapi_get_time_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
"""获取时间选项架构。 """获取时间选项架构。
返回指定网络中时间相关选项的属性架构定义。 返回指定网络中时间相关选项的属性架构定义。
@@ -28,7 +28,7 @@ async def fastapi_get_time_schema(network: str = Query(..., description="管网
return get_time_schema(network) return get_time_schema(network)
@router.get("/network-options/time", summary="获取时间选项属性", description="获取指定网络中的时间选项属性信息") @router.get("/network-options/time", summary="获取时间选项属性", description="获取指定网络中的时间选项属性信息")
async def fastapi_get_time_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]: def fastapi_get_time_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
"""获取时间选项属性。 """获取时间选项属性。
返回指定网络中的时间相关选项属性。 返回指定网络中的时间相关选项属性。
@@ -36,19 +36,19 @@ async def fastapi_get_time_properties(network: str = Query(..., description="管
return get_time(network) return get_time(network)
@router.patch("/time-properties", response_model=None, summary="设置时间选项属性", description="更新指定网络中的时间选项属性") @router.patch("/time-properties", response_model=None, summary="设置时间选项属性", description="更新指定网络中的时间选项属性")
async def fastapi_set_time_properties( def fastapi_set_time_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置时间选项属性。 """设置时间选项属性。
更新指定网络中的时间相关选项属性值。 更新指定网络中的时间相关选项属性值。
""" """
props = await req.json() props = payload
return set_time(network, ChangeSet(props)) return set_time(network, ChangeSet(props))
@router.get("/network-schemas/energy", summary="获取能耗选项架构", description="获取网络中能耗选项的架构定义") @router.get("/network-schemas/energy", summary="获取能耗选项架构", description="获取网络中能耗选项的架构定义")
async def fastapi_get_energy_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fastapi_get_energy_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
"""获取能耗选项架构。 """获取能耗选项架构。
返回指定网络中能耗相关选项的属性架构定义。 返回指定网络中能耗相关选项的属性架构定义。
@@ -56,7 +56,7 @@ async def fastapi_get_energy_schema(network: str = Query(..., description="管
return get_energy_schema(network) return get_energy_schema(network)
@router.get("/network-options/energy", summary="获取能耗选项属性", description="获取指定网络中的能耗选项属性信息") @router.get("/network-options/energy", summary="获取能耗选项属性", description="获取指定网络中的能耗选项属性信息")
async def fastapi_get_energy_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]: def fastapi_get_energy_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
"""获取能耗选项属性。 """获取能耗选项属性。
返回指定网络中的能耗相关选项属性。 返回指定网络中的能耗相关选项属性。
@@ -64,19 +64,19 @@ async def fastapi_get_energy_properties(network: str = Query(..., description="
return get_energy(network) return get_energy(network)
@router.patch("/energy-properties", response_model=None, summary="设置能耗选项属性", description="更新指定网络中的能耗选项属性") @router.patch("/energy-properties", response_model=None, summary="设置能耗选项属性", description="更新指定网络中的能耗选项属性")
async def fastapi_set_energy_properties( def fastapi_set_energy_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置能耗选项属性。 """设置能耗选项属性。
更新指定网络中的能耗相关选项属性值。 更新指定网络中的能耗相关选项属性值。
""" """
props = await req.json() props = payload
return set_energy(network, ChangeSet(props)) return set_energy(network, ChangeSet(props))
@router.get("/network-schemas/pump-energy", summary="获取泵能耗选项架构", description="获取网络中泵能耗选项的架构定义") @router.get("/network-schemas/pump-energy", summary="获取泵能耗选项架构", description="获取网络中泵能耗选项的架构定义")
async def fastapi_get_pump_energy_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fastapi_get_pump_energy_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
"""获取泵能耗选项架构。 """获取泵能耗选项架构。
返回指定网络中泵能耗相关选项的属性架构定义。 返回指定网络中泵能耗相关选项的属性架构定义。
@@ -84,7 +84,7 @@ async def fastapi_get_pump_energy_schema(network: str = Query(..., description="
return get_pump_energy_schema(network) return get_pump_energy_schema(network)
@router.get("/network-options/pump-energy", summary="获取泵能耗属性", description="获取指定泵的能耗属性信息") @router.get("/network-options/pump-energy", summary="获取泵能耗属性", description="获取指定泵的能耗属性信息")
async def fastapi_get_pump_energy_proeprties( def fastapi_get_pump_energy_proeprties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pump: str = Query(..., description="泵ID") pump: str = Query(..., description="泵ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -95,21 +95,21 @@ async def fastapi_get_pump_energy_proeprties(
return get_pump_energy(network, pump) return get_pump_energy(network, pump)
@router.patch("/network-options/pump-energy", response_model=None, summary="设置泵能耗属性", description="更新指定泵的能耗属性") @router.patch("/network-options/pump-energy", response_model=None, summary="设置泵能耗属性", description="更新指定泵的能耗属性")
async def fastapi_set_pump_energy_properties( def fastapi_set_pump_energy_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pump: str = Query(..., description="泵ID"), pump: str = Query(..., description="泵ID"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置泵能耗属性。 """设置泵能耗属性。
更新指定泵的能耗相关属性值。 更新指定泵的能耗相关属性值。
""" """
props = await req.json() props = payload
ps = {"id": pump} | props ps = {"id": pump} | props
return set_pump_energy(network, ChangeSet(ps)) return set_pump_energy(network, ChangeSet(ps))
@router.get("/network-schemas/option", summary="获取选项架构", description="获取网络中选项对象的架构定义") @router.get("/network-schemas/option", summary="获取选项架构", description="获取网络中选项对象的架构定义")
async def fastapi_get_option_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fastapi_get_option_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
"""获取选项架构。 """获取选项架构。
返回指定网络中选项对象的属性架构定义。 返回指定网络中选项对象的属性架构定义。
@@ -117,7 +117,7 @@ async def fastapi_get_option_schema(network: str = Query(..., description="管
return get_option_v3_schema(network) return get_option_v3_schema(network)
@router.get("/network-options", summary="获取选项属性", description="获取指定网络中的选项属性信息") @router.get("/network-options", summary="获取选项属性", description="获取指定网络中的选项属性信息")
async def fastapi_get_option_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]: def fastapi_get_option_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
"""获取选项属性。 """获取选项属性。
返回指定网络中的选项对象属性信息。 返回指定网络中的选项对象属性信息。
@@ -125,13 +125,13 @@ async def fastapi_get_option_properties(network: str = Query(..., description="
return get_option_v3(network) return get_option_v3(network)
@router.patch("/network-options", response_model=None, summary="设置选项属性", description="更新指定网络中的选项属性") @router.patch("/network-options", response_model=None, summary="设置选项属性", description="更新指定网络中的选项属性")
async def fastapi_set_option_properties( def fastapi_set_option_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置选项属性。 """设置选项属性。
更新指定网络中的选项属性值。 更新指定网络中的选项属性值。
""" """
props = await req.json() props = payload
return set_option_v3(network, ChangeSet(props)) return set_option_v3(network, ChangeSet(props))
+14 -14
View File
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Request, Query, Path, Body from typing import Any
from typing import Any, List, Dict, Union
from fastapi import APIRouter, Body, Query
from app.services.tjnetwork import ( from app.services.tjnetwork import (
Any,
ChangeSet, ChangeSet,
add_pattern, add_pattern,
delete_pattern, delete_pattern,
@@ -15,7 +15,7 @@ from app.services.tjnetwork import (
router = APIRouter() router = APIRouter()
@router.get("/network-schemas/pattern", summary="获取模式架构", description="获取网络中模式对象的架构定义") @router.get("/network-schemas/pattern", summary="获取模式架构", description="获取网络中模式对象的架构定义")
async def fastapi_get_pattern_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fastapi_get_pattern_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
"""获取模式架构。 """获取模式架构。
返回指定网络中模式对象的属性架构定义。 返回指定网络中模式对象的属性架构定义。
@@ -23,23 +23,23 @@ async def fastapi_get_pattern_schema(network: str = Query(..., description="管
return get_pattern_schema(network) return get_pattern_schema(network)
@router.post("/patterns", response_model=None, summary="添加模式", description="在网络中添加一个新的模式") @router.post("/patterns", response_model=None, summary="添加模式", description="在网络中添加一个新的模式")
async def fastapi_add_pattern( def fastapi_add_pattern(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pattern: str = Query(..., description="模式ID"), pattern: str = Query(..., description="模式ID"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""添加模式。 """添加模式。
在指定网络中创建一个新的模式,并设置其初始属性。 在指定网络中创建一个新的模式,并设置其初始属性。
""" """
props = await req.json() props = payload
ps = { ps = {
"id": pattern, "id": pattern,
} | props } | props
return add_pattern(network, ChangeSet(ps)) return add_pattern(network, ChangeSet(ps))
@router.delete("/patterns", response_model=None, summary="删除模式", description="从网络中删除指定的模式") @router.delete("/patterns", response_model=None, summary="删除模式", description="从网络中删除指定的模式")
async def fastapi_delete_pattern( def fastapi_delete_pattern(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pattern: str = Query(..., description="模式ID") pattern: str = Query(..., description="模式ID")
) -> ChangeSet: ) -> ChangeSet:
@@ -51,7 +51,7 @@ async def fastapi_delete_pattern(
return delete_pattern(network, ChangeSet(ps)) return delete_pattern(network, ChangeSet(ps))
@router.get("/patterns/properties", summary="获取模式属性", description="获取指定模式的属性信息") @router.get("/patterns/properties", summary="获取模式属性", description="获取指定模式的属性信息")
async def fastapi_get_pattern_properties( def fastapi_get_pattern_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pattern: str = Query(..., description="模式ID") pattern: str = Query(..., description="模式ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -62,21 +62,21 @@ async def fastapi_get_pattern_properties(
return get_pattern(network, pattern) return get_pattern(network, pattern)
@router.patch("/patterns/properties", response_model=None, summary="设置模式属性", description="更新指定模式的属性") @router.patch("/patterns/properties", response_model=None, summary="设置模式属性", description="更新指定模式的属性")
async def fastapi_set_pattern_properties( def fastapi_set_pattern_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pattern: str = Query(..., description="模式ID"), pattern: str = Query(..., description="模式ID"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置模式属性。 """设置模式属性。
更新指定模式的属性值。 更新指定模式的属性值。
""" """
props = await req.json() props = payload
ps = {"id": pattern} | props ps = {"id": pattern} | props
return set_pattern(network, ChangeSet(ps)) return set_pattern(network, ChangeSet(ps))
@router.get("/patterns/existence", summary="检查模式存在性", description="检查指定的模式是否存在") @router.get("/patterns/existence", summary="检查模式存在性", description="检查指定的模式是否存在")
async def fastapi_is_pattern( def fastapi_is_pattern(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pattern: str = Query(..., description="模式ID") pattern: str = Query(..., description="模式ID")
) -> bool: ) -> bool:
@@ -87,7 +87,7 @@ async def fastapi_is_pattern(
return is_pattern(network, pattern) return is_pattern(network, pattern)
@router.get("/patterns", summary="获取所有模式", description="获取网络中的所有模式列表") @router.get("/patterns", summary="获取所有模式", description="获取网络中的所有模式列表")
async def fastapi_get_patterns(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]: def fastapi_get_patterns(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
"""获取所有模式。 """获取所有模式。
返回指定网络中的所有模式ID列表。 返回指定网络中的所有模式ID列表。
+50 -50
View File
@@ -1,11 +1,10 @@
from fastapi import APIRouter, Request, Query, Path, Body from typing import Any
from typing import Any, List, Dict, Union
from fastapi import APIRouter, Body, Query
from app.services.tjnetwork import ( from app.services.tjnetwork import (
Any,
ChangeSet, ChangeSet,
add_mixing, add_mixing,
add_source, add_source,
api,
delete_mixing, delete_mixing,
delete_source, delete_source,
get_emitter, get_emitter,
@@ -23,6 +22,7 @@ from app.services.tjnetwork import (
get_tank_reaction, get_tank_reaction,
get_tank_reaction_schema, get_tank_reaction_schema,
set_emitter, set_emitter,
set_mixing,
set_pipe_reaction, set_pipe_reaction,
set_quality, set_quality,
set_reaction, set_reaction,
@@ -33,7 +33,7 @@ from app.services.tjnetwork import (
router = APIRouter() router = APIRouter()
@router.get("/network-schemas/quality", summary="获取水质架构", description="获取网络中水质对象的架构定义") @router.get("/network-schemas/quality", summary="获取水质架构", description="获取网络中水质对象的架构定义")
async def fastapi_get_quality_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fastapi_get_quality_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
"""获取水质架构。 """获取水质架构。
返回指定网络中水质对象的属性架构定义。 返回指定网络中水质对象的属性架构定义。
@@ -41,7 +41,7 @@ async def fastapi_get_quality_schema(network: str = Query(..., description="管
return get_quality_schema(network) return get_quality_schema(network)
@router.get("/quality-configurations/properties", summary="获取水质属性", description="获取指定节点的水质属性信息") @router.get("/quality-configurations/properties", summary="获取水质属性", description="获取指定节点的水质属性信息")
async def fastapi_get_quality_properties( def fastapi_get_quality_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
node: str = Query(..., description="节点ID") node: str = Query(..., description="节点ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -52,19 +52,19 @@ async def fastapi_get_quality_properties(
return get_quality(network, node) return get_quality(network, node)
@router.patch("/quality-configurations/properties", response_model=None, summary="设置水质属性", description="更新指定节点的水质属性") @router.patch("/quality-configurations/properties", response_model=None, summary="设置水质属性", description="更新指定节点的水质属性")
async def fastapi_set_quality_properties( def fastapi_set_quality_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置水质属性。 """设置水质属性。
更新指定节点的水质属性值。 更新指定节点的水质属性值。
""" """
props = await req.json() props = payload
return set_quality(network, ChangeSet(props)) return set_quality(network, ChangeSet(props))
@router.get("/network-schemas/emitter", summary="获取发射器架构", description="获取网络中发射器对象的架构定义") @router.get("/network-schemas/emitter", summary="获取发射器架构", description="获取网络中发射器对象的架构定义")
async def fastapi_get_emitter_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fastapi_get_emitter_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
"""获取发射器架构。 """获取发射器架构。
返回指定网络中发射器对象的属性架构定义。 返回指定网络中发射器对象的属性架构定义。
@@ -72,7 +72,7 @@ async def fastapi_get_emitter_schema(network: str = Query(..., description="管
return get_emitter_schema(network) return get_emitter_schema(network)
@router.get("/emitters/properties", summary="获取发射器属性", description="获取指定连接点的发射器属性信息") @router.get("/emitters/properties", summary="获取发射器属性", description="获取指定连接点的发射器属性信息")
async def fastapi_get_emitter_properties( def fastapi_get_emitter_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="连接点ID") junction: str = Query(..., description="连接点ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -83,21 +83,21 @@ async def fastapi_get_emitter_properties(
return get_emitter(network, junction) return get_emitter(network, junction)
@router.patch("/emitters/properties", response_model=None, summary="设置发射器属性", description="更新指定连接点的发射器属性") @router.patch("/emitters/properties", response_model=None, summary="设置发射器属性", description="更新指定连接点的发射器属性")
async def fastapi_set_emitter_properties( def fastapi_set_emitter_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="连接点ID"), junction: str = Query(..., description="连接点ID"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置发射器属性。 """设置发射器属性。
更新指定连接点的发射器属性值。 更新指定连接点的发射器属性值。
""" """
props = await req.json() props = payload
ps = {"junction": junction} | props ps = {"junction": junction} | props
return set_emitter(network, ChangeSet(ps)) return set_emitter(network, ChangeSet(ps))
@router.get("/network-schemas/source", summary="获取水源架构", description="获取网络中水源对象的架构定义") @router.get("/network-schemas/source", summary="获取水源架构", description="获取网络中水源对象的架构定义")
async def fastapi_get_source_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fastapi_get_source_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
"""获取水源架构。 """获取水源架构。
返回指定网络中水源对象的属性架构定义。 返回指定网络中水源对象的属性架构定义。
@@ -105,7 +105,7 @@ async def fastapi_get_source_schema(network: str = Query(..., description="管
return get_source_schema(network) return get_source_schema(network)
@router.get("/sources/detail", summary="获取水源属性", description="获取指定节点的水源属性信息") @router.get("/sources/detail", summary="获取水源属性", description="获取指定节点的水源属性信息")
async def fastapi_get_source( def fastapi_get_source(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
node: str = Query(..., description="节点ID") node: str = Query(..., description="节点ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -116,31 +116,31 @@ async def fastapi_get_source(
return get_source(network, node) return get_source(network, node)
@router.patch("/sources", response_model=None, summary="设置水源属性", description="更新指定节点的水源属性") @router.patch("/sources", response_model=None, summary="设置水源属性", description="更新指定节点的水源属性")
async def fastapi_set_source( def fastapi_set_source(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置水源属性。 """设置水源属性。
更新指定节点的水源属性值。 更新指定节点的水源属性值。
""" """
props = await req.json() props = payload
return set_source(network, ChangeSet(props)) return set_source(network, ChangeSet(props))
@router.post("/sources", response_model=None, summary="添加水源", description="在网络中添加一个新的水源") @router.post("/sources", response_model=None, summary="添加水源", description="在网络中添加一个新的水源")
async def fastapi_add_source( def fastapi_add_source(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""添加水源。 """添加水源。
在指定网络中创建一个新的水源,并设置其初始属性。 在指定网络中创建一个新的水源,并设置其初始属性。
""" """
props = await req.json() props = payload
return add_source(network, ChangeSet(props)) return add_source(network, ChangeSet(props))
@router.delete("/sources", response_model=None, summary="删除水源", description="从网络中删除指定节点的水源") @router.delete("/sources", response_model=None, summary="删除水源", description="从网络中删除指定节点的水源")
async def fastapi_delete_source( def fastapi_delete_source(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
node: str = Query(..., description="节点ID") node: str = Query(..., description="节点ID")
) -> ChangeSet: ) -> ChangeSet:
@@ -152,7 +152,7 @@ async def fastapi_delete_source(
return delete_source(network, ChangeSet(props)) return delete_source(network, ChangeSet(props))
@router.get("/network-schemas/reaction", summary="获取反应架构", description="获取网络中反应对象的架构定义") @router.get("/network-schemas/reaction", summary="获取反应架构", description="获取网络中反应对象的架构定义")
async def fastapi_get_reaction_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fastapi_get_reaction_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
"""获取反应架构。 """获取反应架构。
返回指定网络中反应对象的属性架构定义。 返回指定网络中反应对象的属性架构定义。
@@ -160,7 +160,7 @@ async def fastapi_get_reaction_schema(network: str = Query(..., description="管
return get_reaction_schema(network) return get_reaction_schema(network)
@router.get("/reactions/detail", summary="获取反应属性", description="获取指定网络中的反应属性信息") @router.get("/reactions/detail", summary="获取反应属性", description="获取指定网络中的反应属性信息")
async def fastapi_get_reaction(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]: def fastapi_get_reaction(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
"""获取反应属性。 """获取反应属性。
返回指定网络中的反应属性信息。 返回指定网络中的反应属性信息。
@@ -168,19 +168,19 @@ async def fastapi_get_reaction(network: str = Query(..., description="管网名
return get_reaction(network) return get_reaction(network)
@router.patch("/reactions", response_model=None, summary="设置反应属性", description="更新指定网络中的反应属性") @router.patch("/reactions", response_model=None, summary="设置反应属性", description="更新指定网络中的反应属性")
async def fastapi_set_reaction( def fastapi_set_reaction(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置反应属性。 """设置反应属性。
更新指定网络中的反应属性值。 更新指定网络中的反应属性值。
""" """
props = await req.json() props = payload
return set_reaction(network, ChangeSet(props)) return set_reaction(network, ChangeSet(props))
@router.get("/network-schemas/pipe-reaction", summary="获取管道反应架构", description="获取网络中管道反应对象的架构定义") @router.get("/network-schemas/pipe-reaction", summary="获取管道反应架构", description="获取网络中管道反应对象的架构定义")
async def fastapi_get_pipe_reaction_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fastapi_get_pipe_reaction_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
"""获取管道反应架构。 """获取管道反应架构。
返回指定网络中管道反应对象的属性架构定义。 返回指定网络中管道反应对象的属性架构定义。
@@ -188,7 +188,7 @@ async def fastapi_get_pipe_reaction_schema(network: str = Query(..., description
return get_pipe_reaction_schema(network) return get_pipe_reaction_schema(network)
@router.get("/pipe-reactions/detail", summary="获取管道反应属性", description="获取指定管道的反应属性信息") @router.get("/pipe-reactions/detail", summary="获取管道反应属性", description="获取指定管道的反应属性信息")
async def fastapi_get_pipe_reaction( def fastapi_get_pipe_reaction(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pipe: str = Query(..., description="管道ID") pipe: str = Query(..., description="管道ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -199,19 +199,19 @@ async def fastapi_get_pipe_reaction(
return get_pipe_reaction(network, pipe) return get_pipe_reaction(network, pipe)
@router.patch("/pipe-reactions", response_model=None, summary="设置管道反应属性", description="更新指定管道的反应属性") @router.patch("/pipe-reactions", response_model=None, summary="设置管道反应属性", description="更新指定管道的反应属性")
async def fastapi_set_pipe_reaction( def fastapi_set_pipe_reaction(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置管道反应属性。 """设置管道反应属性。
更新指定管道的反应属性值。 更新指定管道的反应属性值。
""" """
props = await req.json() props = payload
return set_pipe_reaction(network, ChangeSet(props)) return set_pipe_reaction(network, ChangeSet(props))
@router.get("/network-schemas/tank-reaction", summary="获取水池反应架构", description="获取网络中水池反应对象的架构定义") @router.get("/network-schemas/tank-reaction", summary="获取水池反应架构", description="获取网络中水池反应对象的架构定义")
async def fastapi_get_tank_reaction_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fastapi_get_tank_reaction_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
"""获取水池反应架构。 """获取水池反应架构。
返回指定网络中水池反应对象的属性架构定义。 返回指定网络中水池反应对象的属性架构定义。
@@ -219,7 +219,7 @@ async def fastapi_get_tank_reaction_schema(network: str = Query(..., description
return get_tank_reaction_schema(network) return get_tank_reaction_schema(network)
@router.get("/tank-reactions/detail", summary="获取水池反应属性", description="获取指定水池的反应属性信息") @router.get("/tank-reactions/detail", summary="获取水池反应属性", description="获取指定水池的反应属性信息")
async def fastapi_get_tank_reaction( def fastapi_get_tank_reaction(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水池ID") tank: str = Query(..., description="水池ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -230,19 +230,19 @@ async def fastapi_get_tank_reaction(
return get_tank_reaction(network, tank) return get_tank_reaction(network, tank)
@router.patch("/tank-reactions", response_model=None, summary="设置水池反应属性", description="更新指定水池的反应属性") @router.patch("/tank-reactions", response_model=None, summary="设置水池反应属性", description="更新指定水池的反应属性")
async def fastapi_set_tank_reaction( def fastapi_set_tank_reaction(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置水池反应属性。 """设置水池反应属性。
更新指定水池的反应属性值。 更新指定水池的反应属性值。
""" """
props = await req.json() props = payload
return set_tank_reaction(network, ChangeSet(props)) return set_tank_reaction(network, ChangeSet(props))
@router.get("/network-schemas/mixing", summary="获取混合架构", description="获取网络中混合对象的架构定义") @router.get("/network-schemas/mixing", summary="获取混合架构", description="获取网络中混合对象的架构定义")
async def fastapi_get_mixing_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fastapi_get_mixing_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
"""获取混合架构。 """获取混合架构。
返回指定网络中混合对象的属性架构定义。 返回指定网络中混合对象的属性架构定义。
@@ -250,7 +250,7 @@ async def fastapi_get_mixing_schema(network: str = Query(..., description="管
return get_mixing_schema(network) return get_mixing_schema(network)
@router.get("/mixing-configurations/detail", summary="获取混合属性", description="获取指定水池的混合属性信息") @router.get("/mixing-configurations/detail", summary="获取混合属性", description="获取指定水池的混合属性信息")
async def fastapi_get_mixing( def fastapi_get_mixing(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水池ID") tank: str = Query(..., description="水池ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -261,37 +261,37 @@ async def fastapi_get_mixing(
return get_mixing(network, tank) return get_mixing(network, tank)
@router.patch("/mixing-configurations", response_model=None, summary="设置混合属性", description="更新指定水池的混合属性") @router.patch("/mixing-configurations", response_model=None, summary="设置混合属性", description="更新指定水池的混合属性")
async def fastapi_set_mixing( def fastapi_set_mixing(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置混合属性。 """设置混合属性。
更新指定水池的混合属性值。 更新指定水池的混合属性值。
""" """
props = await req.json() props = payload
return api.set_mixing(network, ChangeSet(props)) return set_mixing(network, ChangeSet(props))
@router.post("/mixing-configurations", response_model=None, summary="添加混合", description="在网络中添加一个新的混合") @router.post("/mixing-configurations", response_model=None, summary="添加混合", description="在网络中添加一个新的混合")
async def fastapi_add_mixing( def fastapi_add_mixing(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""添加混合。 """添加混合。
在指定网络中创建一个新的混合,并设置其初始属性。 在指定网络中创建一个新的混合,并设置其初始属性。
""" """
props = await req.json() props = payload
return add_mixing(network, ChangeSet(props)) return add_mixing(network, ChangeSet(props))
@router.delete("/mixing-configurations", response_model=None, summary="删除混合", description="从网络中删除指定的混合") @router.delete("/mixing-configurations", response_model=None, summary="删除混合", description="从网络中删除指定的混合")
async def fastapi_delete_mixing( def fastapi_delete_mixing(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""删除混合。 """删除混合。
从指定网络中删除指定的混合及其相关数据。 从指定网络中删除指定的混合及其相关数据。
""" """
props = await req.json() props = payload
return delete_mixing(network, ChangeSet(props)) return delete_mixing(network, ChangeSet(props))
+32 -32
View File
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Request, Query, Path, Body, Response from typing import Any
from typing import Any, List, Dict, Union
from fastapi import APIRouter, Body, Query
from app.services.tjnetwork import ( from app.services.tjnetwork import (
Any,
ChangeSet, ChangeSet,
add_label, add_label,
add_vertex, add_vertex,
@@ -25,7 +25,7 @@ import json
router = APIRouter() router = APIRouter()
@router.get("/network-schemas/vertex", summary="获取图形元素架构", description="获取网络中图形元素对象的架构定义") @router.get("/network-schemas/vertex", summary="获取图形元素架构", description="获取网络中图形元素对象的架构定义")
async def fastapi_get_vertex_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fastapi_get_vertex_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
"""获取图形元素架构。 """获取图形元素架构。
返回指定网络中图形元素对象的属性架构定义。 返回指定网络中图形元素对象的属性架构定义。
@@ -33,7 +33,7 @@ async def fastapi_get_vertex_schema(network: str = Query(..., description="管
return get_vertex_schema(network) return get_vertex_schema(network)
@router.get("/visual-elements/properties", summary="获取图形元素属性", description="获取指定图形元素的属性信息") @router.get("/visual-elements/properties", summary="获取图形元素属性", description="获取指定图形元素的属性信息")
async def fastapi_get_vertex_properties( def fastapi_get_vertex_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
link: str = Query(..., description="图形元素链接") link: str = Query(..., description="图形元素链接")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -44,43 +44,43 @@ async def fastapi_get_vertex_properties(
return get_vertex(network, link) return get_vertex(network, link)
@router.patch("/visual-elements/properties", response_model=None, summary="设置图形元素属性", description="更新指定图形元素的属性") @router.patch("/visual-elements/properties", response_model=None, summary="设置图形元素属性", description="更新指定图形元素的属性")
async def fastapi_set_vertex_properties( def fastapi_set_vertex_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置图形元素属性。 """设置图形元素属性。
更新指定图形元素的属性值。 更新指定图形元素的属性值。
""" """
props = await req.json() props = payload
return set_vertex(network, ChangeSet(props)) return set_vertex(network, ChangeSet(props))
@router.post("/visual-elements", response_model=None, summary="添加图形元素", description="在网络中添加一个新的图形元素") @router.post("/visual-elements", response_model=None, summary="添加图形元素", description="在网络中添加一个新的图形元素")
async def fastapi_add_vertex( def fastapi_add_vertex(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""添加图形元素。 """添加图形元素。
在指定网络中创建一个新的图形元素,并设置其初始属性。 在指定网络中创建一个新的图形元素,并设置其初始属性。
""" """
props = await req.json() props = payload
return add_vertex(network, ChangeSet(props)) return add_vertex(network, ChangeSet(props))
@router.delete("/visual-elements", response_model=None, summary="删除图形元素", description="从网络中删除指定的图形元素") @router.delete("/visual-elements", response_model=None, summary="删除图形元素", description="从网络中删除指定的图形元素")
async def fastapi_delete_vertex( def fastapi_delete_vertex(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""删除图形元素。 """删除图形元素。
从指定网络中删除指定的图形元素及其相关数据。 从指定网络中删除指定的图形元素及其相关数据。
""" """
props = await req.json() props = payload
return delete_vertex(network, ChangeSet(props)) return delete_vertex(network, ChangeSet(props))
@router.get("/visual-elements/links", response_class=PlainTextResponse, summary="获取所有图形元素链接", description="获取网络中的所有图形元素链接列表") @router.get("/visual-elements/links", response_class=PlainTextResponse, summary="获取所有图形元素链接", description="获取网络中的所有图形元素链接列表")
async def fastapi_get_all_vertex_links(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]: def fastapi_get_all_vertex_links(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
"""获取所有图形元素链接。 """获取所有图形元素链接。
返回指定网络中的所有图形元素链接列表。 返回指定网络中的所有图形元素链接列表。
@@ -88,7 +88,7 @@ async def fastapi_get_all_vertex_links(network: str = Query(..., description="
return json.dumps(get_all_vertex_links(network)) return json.dumps(get_all_vertex_links(network))
@router.get("/all-vertices", response_class=PlainTextResponse, summary="获取所有图形元素", description="获取网络中的所有图形元素详细信息") @router.get("/all-vertices", response_class=PlainTextResponse, summary="获取所有图形元素", description="获取网络中的所有图形元素详细信息")
async def fastapi_get_all_vertices(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[str, Any]]: def fastapi_get_all_vertices(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[str, Any]]:
"""获取所有图形元素。 """获取所有图形元素。
返回指定网络中的所有图形元素详细信息。 返回指定网络中的所有图形元素详细信息。
@@ -96,7 +96,7 @@ async def fastapi_get_all_vertices(network: str = Query(..., description="管网
return json.dumps(get_all_vertices(network)) return json.dumps(get_all_vertices(network))
@router.get("/network-schemas/label", summary="获取标签架构", description="获取网络中标签对象的架构定义") @router.get("/network-schemas/label", summary="获取标签架构", description="获取网络中标签对象的架构定义")
async def fastapi_get_label_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fastapi_get_label_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
"""获取标签架构。 """获取标签架构。
返回指定网络中标签对象的属性架构定义。 返回指定网络中标签对象的属性架构定义。
@@ -104,7 +104,7 @@ async def fastapi_get_label_schema(network: str = Query(..., description="管网
return get_label_schema(network) return get_label_schema(network)
@router.get("/labels/properties", summary="获取标签属性", description="获取指定坐标处的标签属性信息") @router.get("/labels/properties", summary="获取标签属性", description="获取指定坐标处的标签属性信息")
async def fastapi_get_label_properties( def fastapi_get_label_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
x: float = Query(..., description="X坐标"), x: float = Query(..., description="X坐标"),
y: float = Query(..., description="Y坐标") y: float = Query(..., description="Y坐标")
@@ -116,43 +116,43 @@ async def fastapi_get_label_properties(
return get_label(network, x, y) return get_label(network, x, y)
@router.patch("/labels/properties", response_model=None, summary="设置标签属性", description="更新指定标签的属性") @router.patch("/labels/properties", response_model=None, summary="设置标签属性", description="更新指定标签的属性")
async def fastapi_set_label_properties( def fastapi_set_label_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置标签属性。 """设置标签属性。
更新指定标签的属性值。 更新指定标签的属性值。
""" """
props = await req.json() props = payload
return set_label(network, ChangeSet(props)) return set_label(network, ChangeSet(props))
@router.post("/labels", response_model=None, summary="添加标签", description="在网络中添加一个新的标签") @router.post("/labels", response_model=None, summary="添加标签", description="在网络中添加一个新的标签")
async def fastapi_add_label( def fastapi_add_label(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""添加标签。 """添加标签。
在指定网络中创建一个新的标签,并设置其初始属性。 在指定网络中创建一个新的标签,并设置其初始属性。
""" """
props = await req.json() props = payload
return add_label(network, ChangeSet(props)) return add_label(network, ChangeSet(props))
@router.delete("/labels", response_model=None, summary="删除标签", description="从网络中删除指定的标签") @router.delete("/labels", response_model=None, summary="删除标签", description="从网络中删除指定的标签")
async def fastapi_delete_label( def fastapi_delete_label(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""删除标签。 """删除标签。
从指定网络中删除指定的标签及其相关数据。 从指定网络中删除指定的标签及其相关数据。
""" """
props = await req.json() props = payload
return delete_label(network, ChangeSet(props)) return delete_label(network, ChangeSet(props))
@router.get("/network-schemas/backdrop", summary="获取背景架构", description="获取网络中背景对象的架构定义") @router.get("/network-schemas/backdrop", summary="获取背景架构", description="获取网络中背景对象的架构定义")
async def fastapi_get_backdrop_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fastapi_get_backdrop_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
"""获取背景架构。 """获取背景架构。
返回指定网络中背景对象的属性架构定义。 返回指定网络中背景对象的属性架构定义。
@@ -160,7 +160,7 @@ async def fastapi_get_backdrop_schema(network: str = Query(..., description="管
return get_backdrop_schema(network) return get_backdrop_schema(network)
@router.get("/backdrops/properties", summary="获取背景属性", description="获取指定网络的背景属性信息") @router.get("/backdrops/properties", summary="获取背景属性", description="获取指定网络的背景属性信息")
async def fastapi_get_backdrop_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]: def fastapi_get_backdrop_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
"""获取背景属性。 """获取背景属性。
返回指定网络的背景属性信息。 返回指定网络的背景属性信息。
@@ -168,13 +168,13 @@ async def fastapi_get_backdrop_properties(network: str = Query(..., description=
return get_backdrop(network) return get_backdrop(network)
@router.patch("/backdrops/properties", response_model=None, summary="设置背景属性", description="更新指定网络的背景属性") @router.patch("/backdrops/properties", response_model=None, summary="设置背景属性", description="更新指定网络的背景属性")
async def fastapi_set_backdrop_properties( def fastapi_set_backdrop_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置背景属性。 """设置背景属性。
更新指定网络的背景属性值。 更新指定网络的背景属性值。
""" """
props = await req.json() props = payload
return set_backdrop(network, ChangeSet(props)) return set_backdrop(network, ChangeSet(props))
-104
View File
@@ -1,104 +0,0 @@
from typing import List, Any
from fastapi import APIRouter, Request, HTTPException, Query, Body
from app.services.tjnetwork import (
ChangeSet,
get_all_extension_data_keys,
get_all_extension_data,
get_extension_data,
set_extension_data
)
router = APIRouter()
@router.get(
"/all-extension-data-keys",
summary="获取所有扩展数据键",
description="获取指定网络的所有扩展数据的键列表"
)
async def get_all_extension_data_keys_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> list[str]:
"""
获取所有扩展数据键。
返回指定网络中所有可用的扩展数据键。
Args:
network: 管网名称(或数据库名称)
Returns:
扩展数据键列表
"""
return get_all_extension_data_keys(network)
@router.get(
"/all-extension-datas",
summary="获取所有扩展数据",
description="获取指定网络的所有扩展数据"
)
async def get_all_extension_data_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> dict[str, Any]:
"""
获取所有扩展数据。
返回指定网络的所有扩展数据及其值。
Args:
network: 管网名称(或数据库名称)
Returns:
扩展数据字典
"""
return get_all_extension_data(network)
@router.get(
"/extension-datas",
summary="获取指定扩展数据",
description="获取指定网络中指定键的扩展数据值"
)
async def get_extension_data_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
key: str = Query(..., description="扩展数据键")
) -> str | None:
"""
获取指定扩展数据。
返回指定网络中指定键对应的扩展数据值。
Args:
network: 管网名称(或数据库名称)
key: 扩展数据键
Returns:
扩展数据值,如果不存在返回None
"""
return get_extension_data(network, key)
@router.patch(
"/extension-datas",
response_model=None,
summary="设置扩展数据",
description="设置指定网络中的扩展数据"
)
async def set_extension_data_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""
设置扩展数据。
在指定网络中设置扩展数据,并返回变更集信息。
Args:
network: 管网名称(或数据库名称)
req: 包含扩展数据的请求体
Returns:
变更集信息
"""
props = await req.json()
print(props)
cs = set_extension_data(network, ChangeSet(props))
print(cs.operations[0])
return cs
+33 -14
View File
@@ -3,34 +3,49 @@ from typing import Any
from datetime import datetime from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Body from fastapi import APIRouter, Depends, HTTPException, Body
from pydantic import BaseModel, Field from pydantic import BaseModel, ConfigDict, Field
from starlette.concurrency import run_in_threadpool
from app.auth.keycloak_dependencies import get_current_keycloak_username from app.auth.keycloak_dependencies import get_current_keycloak_username
from app.services.leakage_identifier import ( from app.services.dma_leakage_estimation import (
run_leakage_identification, run_leakage_identification,
) )
router = APIRouter() router = APIRouter()
DEFAULT_N_WORKERS = max(1, min((os.cpu_count() or 1) - 1, 4)) DEFAULT_N_WORKERS = max(1, min((os.cpu_count() or 1) - 1, 4))
MAX_POPULATION_SIZE = 1_000
MAX_GENERATIONS = 1_000
MAX_DURATION_HOURS = 168
class LeakageIdentifyRequest(BaseModel): class LeakageIdentifyRequest(BaseModel):
"""漏损识别请求模型""" """漏损识别请求模型"""
model_config = ConfigDict(extra="forbid")
network: str = Field(..., description="管网名称(或数据库名称)") network: str = Field(..., description="管网名称(或数据库名称)")
observed_pressure_data: str | dict[str, list[Any]] | list[dict[str, Any]] | None = Field( observed_pressure_data: dict[str, list[Any]] | list[dict[str, Any]] | None = (
None, description="观测的压力数据" Field(None, description="观测的压力数据;文件路径不属于公共 API 输入")
) )
start_time: float = Field(0, description="起始时间(小时)") start_time: float = Field(0, ge=0, description="起始时间(小时)")
duration: float = Field(24, description="持续时间(小时)") duration: float = Field(
timestep: float = Field(5, description="时间步长(分钟") 24, gt=0, le=MAX_DURATION_HOURS, description="持续时间(小时"
q_sum: float = Field(0.2, description="总流量(m3/s") )
timestep: float = Field(5, gt=0, le=1440, description="时间步长(分钟)")
q_sum: float = Field(0.2, ge=0, description="总流量(m3/s")
q_sum_unit: str = Field("m3/s", description="流量单位") q_sum_unit: str = Field("m3/s", description="流量单位")
output_dir: str = Field("db_inp", description="输出目录") pop_size: int = Field(
pop_size: int = Field(50, description="种群大小") 50, ge=2, le=MAX_POPULATION_SIZE, description="种群大小"
max_gen: int = Field(100, description="最大代数") )
n_workers: int = Field(DEFAULT_N_WORKERS, description="工作线程") max_gen: int = Field(100, ge=1, le=MAX_GENERATIONS, description="最大代")
n_workers: int = Field(
DEFAULT_N_WORKERS,
ge=1,
le=DEFAULT_N_WORKERS,
description="工作进程数",
)
output_flow_unit: str = Field("m3/s", description="输出流量单位") output_flow_unit: str = Field("m3/s", description="输出流量单位")
dma_count: int | None = Field(None, description="DMA区域数量") dma_count: int | None = Field(None, ge=1, description="DMA区域数量")
scada_start: datetime | None = Field(None, description="SCADA数据起始时间") scada_start: datetime | None = Field(None, description="SCADA数据起始时间")
scada_end: datetime | None = Field(None, description="SCADA数据结束时间") scada_end: datetime | None = Field(None, description="SCADA数据结束时间")
sensor_nodes: list[str] | None = Field(None, description="传感器节点列表") sensor_nodes: list[str] | None = Field(None, description="传感器节点列表")
@@ -63,6 +78,10 @@ async def identify_leakage(
HTTPException: 当处理过程中发生错误时 HTTPException: 当处理过程中发生错误时
""" """
try: try:
return run_leakage_identification(**data.model_dump(), username=username) return await run_in_threadpool(
run_leakage_identification,
**data.model_dump(),
username=username,
)
except Exception as exc: except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc)) raise HTTPException(status_code=400, detail=str(exc))
+6 -7
View File
@@ -2,14 +2,11 @@ import logging
from fastapi import APIRouter, Depends, HTTPException, status, Query, Path from fastapi import APIRouter, Depends, HTTPException, status, Query, Path
import psycopg import psycopg
from psycopg import AsyncConnection from psycopg import AsyncConnection
from sqlalchemy import text
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth.project_dependencies import ( from app.auth.project_dependencies import (
ProjectContext, ProjectContext,
get_project_context, get_project_context,
get_project_pg_session, get_project_pg_connection,
get_project_timescale_connection, get_project_timescale_connection,
get_metadata_repository, get_metadata_repository,
) )
@@ -90,7 +87,7 @@ async def list_user_projects(
@router.get("/projects/current/database-health", summary="检查数据库健康状态", description="检查项目数据库连接的健康状况") @router.get("/projects/current/database-health", summary="检查数据库健康状态", description="检查项目数据库连接的健康状况")
async def project_db_health( async def project_db_health(
pg_session: AsyncSession = Depends(get_project_pg_session), pg_conn: AsyncConnection = Depends(get_project_pg_connection),
ts_conn: AsyncConnection = Depends(get_project_timescale_connection), ts_conn: AsyncConnection = Depends(get_project_timescale_connection),
): ):
""" """
@@ -99,8 +96,10 @@ async def project_db_health(
检查PostgreSQL和TimescaleDB数据库的连接状态 检查PostgreSQL和TimescaleDB数据库的连接状态
""" """
try: try:
await pg_session.execute(text("SELECT 1")) async with pg_conn.cursor() as cur:
except SQLAlchemyError as exc: await cur.execute("SELECT 1")
await cur.fetchone()
except psycopg.Error as exc:
logger.error("Project PostgreSQL health check failed", exc_info=True) logger.error("Project PostgreSQL health check failed", exc_info=True)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
-62
View File
@@ -1,62 +0,0 @@
from typing import Any
from fastapi import APIRouter, Query
from fastapi.responses import JSONResponse
from fastapi import status
from pydantic import BaseModel
from app.services.tjnetwork import (
get_all_sensor_placements,
get_all_burst_locate_results,
)
router = APIRouter()
async def fastapi_get_json():
"""
获取JSON示例
返回示例JSON格式的响应
"""
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={
"code": 400,
"message": "this is message",
"data": 123,
},
)
@router.get("/sensor-placement-schemes", summary="获取所有传感器位置", description="获取网络中所有传感器的放置位置信息")
async def fastapi_get_all_sensor_placements(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[Any, Any]]:
"""
获取所有传感器位置
返回网络中所有传感器的放置位置及其配置信息
"""
return get_all_sensor_placements(network)
@router.get("/burst-locations", summary="获取所有爆管定位结果", description="获取网络中所有爆管定位的分析结果")
async def fastapi_get_all_burst_locate_results(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[Any, Any]]:
"""
获取所有爆管定位结果
返回网络中所有的爆管定位分析结果
"""
return get_all_burst_locate_results(network)
class Item(BaseModel):
"""测试数据模型"""
str_info: str
async def fastapi_test_dict(data: Item) -> dict[str, str]:
"""
测试字典处理
接收Item模型,返回其字典格式
"""
item = data.dict()
return item
+16 -4
View File
@@ -12,6 +12,7 @@ from fastapi import (
UploadFile, UploadFile,
status, status,
) )
from starlette.concurrency import run_in_threadpool
from app.auth.metadata_dependencies import ( from app.auth.metadata_dependencies import (
get_current_metadata_admin, get_current_metadata_admin,
@@ -24,6 +25,7 @@ from app.auth.project_dependencies import (
from app.core.audit import AuditAction, log_audit_event from app.core.audit import AuditAction, log_audit_event
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
from app.infra.db.project_routing import activate_project_routing from app.infra.db.project_routing import activate_project_routing
from app.native.wndb.core.database import MaterializedViewRefreshAfterCommitError
from app.services.network_import import network_update from app.services.network_import import network_update
from app.services.tjnetwork import run_inp from app.services.tjnetwork import run_inp
@@ -87,8 +89,8 @@ def _validate_inp_bytes(content: bytes, filename: str) -> str:
async def _read_upload(file: UploadFile) -> tuple[bytes, str]: async def _read_upload(file: UploadFile) -> tuple[bytes, str]:
filename = Path(file.filename or "").name filename = Path(file.filename or "").name
content = await file.read(MAX_INP_FILE_BYTES + 1) content = await file.read(MAX_INP_FILE_BYTES + 1)
_validate_inp_bytes(content, filename) normalized = _validate_inp_bytes(content, filename).encode("utf-8")
return content, filename return normalized, filename
async def _audit_model_change( async def _audit_model_change(
@@ -114,7 +116,7 @@ async def _audit_model_change(
) )
async def _run_uploaded_inp(content: bytes) -> str: def _run_uploaded_inp_sync(content: bytes) -> str:
target_dir = Path("inp") target_dir = Path("inp")
target_dir.mkdir(parents=True, exist_ok=True) target_dir.mkdir(parents=True, exist_ok=True)
model_name = f"admin_model_{uuid4().hex}" model_name = f"admin_model_{uuid4().hex}"
@@ -123,7 +125,11 @@ async def _run_uploaded_inp(content: bytes) -> str:
return run_inp(model_name) return run_inp(model_name)
async def _update_from_inp(content: bytes, project_code: str) -> None: async def _run_uploaded_inp(content: bytes) -> str:
return await run_in_threadpool(_run_uploaded_inp_sync, content)
def _update_from_inp_sync(content: bytes, project_code: str) -> None:
temp_path: Path | None = None temp_path: Path | None = None
try: try:
with NamedTemporaryFile(suffix=".inp", delete=False) as temp_file: with NamedTemporaryFile(suffix=".inp", delete=False) as temp_file:
@@ -135,9 +141,15 @@ async def _update_from_inp(content: bytes, project_code: str) -> None:
temp_path.unlink(missing_ok=True) temp_path.unlink(missing_ok=True)
async def _update_from_inp(content: bytes, project_code: str) -> None:
await run_in_threadpool(_update_from_inp_sync, content, project_code)
async def _apply_model_update(content: bytes, project_code: str) -> None: async def _apply_model_update(content: bytes, project_code: str) -> None:
try: try:
await _update_from_inp(content, project_code) await _update_from_inp(content, project_code)
except MaterializedViewRefreshAfterCommitError:
raise
except Exception as exc: except Exception as exc:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+15 -15
View File
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Request, Query, Path, Body from typing import Any
from typing import Any, List, Dict, Union
from fastapi import APIRouter, Body, Query
from app.services.tjnetwork import ( from app.services.tjnetwork import (
Any,
ChangeSet, ChangeSet,
calculate_demand_to_network, calculate_demand_to_network,
calculate_demand_to_nodes, calculate_demand_to_nodes,
@@ -22,7 +22,7 @@ router = APIRouter()
summary="获取需水量属性架构", summary="获取需水量属性架构",
description="获取指定水网中需水量(Demand)的属性架构定义" description="获取指定水网中需水量(Demand)的属性架构定义"
) )
async def fastapi_get_demand_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fastapi_get_demand_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
""" """
获取需水量属性架构。 获取需水量属性架构。
@@ -36,7 +36,7 @@ async def fastapi_get_demand_schema(network: str = Query(..., description="管
summary="获取需水量属性", summary="获取需水量属性",
description="获取指定水网中节点的需水量属性信息" description="获取指定水网中节点的需水量属性信息"
) )
async def fastapi_get_demand_properties( def fastapi_get_demand_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="节点ID") junction: str = Query(..., description="节点ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -55,17 +55,17 @@ async def fastapi_get_demand_properties(
summary="设置需水量属性", summary="设置需水量属性",
description="设置指定水网中节点的需水量属性信息" description="设置指定水网中节点的需水量属性信息"
) )
async def fastapi_set_demand_properties( def fastapi_set_demand_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="节点ID"), junction: str = Query(..., description="节点ID"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
""" """
设置节点的需水量属性。 设置节点的需水量属性。
修改指定节点的需水量信息。请求体应包含需水量值、水压等级等属性。 修改指定节点的需水量信息。请求体应包含需水量值、水压等级等属性。
""" """
props = await req.json() props = payload
ps = {"junction": junction} | props ps = {"junction": junction} | props
return set_demand(network, ChangeSet(ps)) return set_demand(network, ChangeSet(ps))
@@ -77,9 +77,9 @@ async def fastapi_set_demand_properties(
summary="计算需水量到节点分配", summary="计算需水量到节点分配",
description="将总需水量按指定方式分配到多个节点" description="将总需水量按指定方式分配到多个节点"
) )
async def fastapi_calculate_demand_to_nodes( def fastapi_calculate_demand_to_nodes(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> dict[str, float]: ) -> dict[str, float]:
""" """
计算需水量到节点分配。 计算需水量到节点分配。
@@ -92,7 +92,7 @@ async def fastapi_calculate_demand_to_nodes(
"nodes": 节点ID列表(list[str]) "nodes": 节点ID列表(list[str])
} }
""" """
props = await req.json() props = payload
demand = props["demand"] demand = props["demand"]
nodes = props["nodes"] nodes = props["nodes"]
return calculate_demand_to_nodes(network, demand, nodes) return calculate_demand_to_nodes(network, demand, nodes)
@@ -102,9 +102,9 @@ async def fastapi_calculate_demand_to_nodes(
summary="计算需水量到区域分配", summary="计算需水量到区域分配",
description="将总需水量按区域特征分配到该区域内的节点" description="将总需水量按区域特征分配到该区域内的节点"
) )
async def fastapi_calculate_demand_to_region( def fastapi_calculate_demand_to_region(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> dict[str, float]: ) -> dict[str, float]:
""" """
计算需水量到区域分配。 计算需水量到区域分配。
@@ -117,7 +117,7 @@ async def fastapi_calculate_demand_to_region(
"region": 区域ID(str) "region": 区域ID(str)
} }
""" """
props = await req.json() props = payload
demand = props["demand"] demand = props["demand"]
region = props["region"] region = props["region"]
return calculate_demand_to_region(network, demand, region) return calculate_demand_to_region(network, demand, region)
@@ -127,7 +127,7 @@ async def fastapi_calculate_demand_to_region(
summary="计算需水量到整网分配", summary="计算需水量到整网分配",
description="将需水量均匀分配到整个水网的所有需水节点" description="将需水量均匀分配到整个水网的所有需水节点"
) )
async def fastapi_calculate_demand_to_network( def fastapi_calculate_demand_to_network(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
demand: float = Query(..., description="总需水量(m³/h)", gt=0) demand: float = Query(..., description="总需水量(m³/h)", gt=0)
) -> dict[str, float]: ) -> dict[str, float]:
+35 -35
View File
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Request, Query, Path, Body from typing import Any
from typing import Any, List, Dict, Union
from fastapi import APIRouter, Body, Query
from app.services.tjnetwork import ( from app.services.tjnetwork import (
Any,
ChangeSet, ChangeSet,
delete_junction, delete_junction,
delete_pipe, delete_pipe,
@@ -49,7 +49,7 @@ router = APIRouter()
summary="检查节点有效性", summary="检查节点有效性",
description="检查指定ID是否为水网中的有效节点" description="检查指定ID是否为水网中的有效节点"
) )
async def fastapi_is_node( def fastapi_is_node(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
node: str = Query(..., description="节点ID") node: str = Query(..., description="节点ID")
) -> bool: ) -> bool:
@@ -61,7 +61,7 @@ async def fastapi_is_node(
summary="检查是否为接点", summary="检查是否为接点",
description="检查指定ID是否为水网中的接点(需求点)" description="检查指定ID是否为水网中的接点(需求点)"
) )
async def fastapi_is_junction( def fastapi_is_junction(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
node: str = Query(..., description="节点ID") node: str = Query(..., description="节点ID")
) -> bool: ) -> bool:
@@ -73,7 +73,7 @@ async def fastapi_is_junction(
summary="检查是否为水源", summary="检查是否为水源",
description="检查指定ID是否为水网中的水源(水库/河流)" description="检查指定ID是否为水网中的水源(水库/河流)"
) )
async def fastapi_is_reservoir( def fastapi_is_reservoir(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
node: str = Query(..., description="节点ID") node: str = Query(..., description="节点ID")
) -> bool: ) -> bool:
@@ -85,7 +85,7 @@ async def fastapi_is_reservoir(
summary="检查是否为蓄水池", summary="检查是否为蓄水池",
description="检查指定ID是否为水网中的蓄水池" description="检查指定ID是否为水网中的蓄水池"
) )
async def fastapi_is_tank( def fastapi_is_tank(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
node: str = Query(..., description="节点ID") node: str = Query(..., description="节点ID")
) -> bool: ) -> bool:
@@ -97,7 +97,7 @@ async def fastapi_is_tank(
summary="检查管线有效性", summary="检查管线有效性",
description="检查指定ID是否为水网中的有效管线" description="检查指定ID是否为水网中的有效管线"
) )
async def fastapi_is_link( def fastapi_is_link(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
link: str = Query(..., description="管线ID") link: str = Query(..., description="管线ID")
) -> bool: ) -> bool:
@@ -109,7 +109,7 @@ async def fastapi_is_link(
summary="检查是否为管道", summary="检查是否为管道",
description="检查指定ID是否为水网中的管道" description="检查指定ID是否为水网中的管道"
) )
async def fastapi_is_pipe( def fastapi_is_pipe(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
link: str = Query(..., description="管线ID") link: str = Query(..., description="管线ID")
) -> bool: ) -> bool:
@@ -121,7 +121,7 @@ async def fastapi_is_pipe(
summary="检查是否为泵", summary="检查是否为泵",
description="检查指定ID是否为水网中的泵" description="检查指定ID是否为水网中的泵"
) )
async def fastapi_is_pump( def fastapi_is_pump(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
link: str = Query(..., description="管线ID") link: str = Query(..., description="管线ID")
) -> bool: ) -> bool:
@@ -133,7 +133,7 @@ async def fastapi_is_pump(
summary="检查是否为阀门", summary="检查是否为阀门",
description="检查指定ID是否为水网中的阀门" description="检查指定ID是否为水网中的阀门"
) )
async def fastapi_is_valve( def fastapi_is_valve(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
link: str = Query(..., description="管线ID") link: str = Query(..., description="管线ID")
) -> bool: ) -> bool:
@@ -145,7 +145,7 @@ async def fastapi_is_valve(
summary="获取节点类型", summary="获取节点类型",
description="获取指定节点的类型(接点/水源/蓄水池)" description="获取指定节点的类型(接点/水源/蓄水池)"
) )
async def fastapi_get_node_type( def fastapi_get_node_type(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
node: str = Query(..., description="节点ID") node: str = Query(..., description="节点ID")
) -> str: ) -> str:
@@ -157,7 +157,7 @@ async def fastapi_get_node_type(
summary="获取管线类型", summary="获取管线类型",
description="获取指定管线的类型(管道/泵/阀门)" description="获取指定管线的类型(管道/泵/阀门)"
) )
async def fastapi_get_link_type( def fastapi_get_link_type(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
link: str = Query(..., description="管线ID") link: str = Query(..., description="管线ID")
) -> str: ) -> str:
@@ -169,7 +169,7 @@ async def fastapi_get_link_type(
summary="获取元素类型", summary="获取元素类型",
description="获取指定元素的类型(节点或管线)" description="获取指定元素的类型(节点或管线)"
) )
async def fastapi_get_element_type( def fastapi_get_element_type(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
element: str = Query(..., description="元素ID") element: str = Query(..., description="元素ID")
) -> str: ) -> str:
@@ -181,7 +181,7 @@ async def fastapi_get_element_type(
summary="获取元素类型值", summary="获取元素类型值",
description="获取指定元素的类型数值标识" description="获取指定元素的类型数值标识"
) )
async def fastapi_get_element_type_value( def fastapi_get_element_type_value(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
element: str = Query(..., description="元素ID") element: str = Query(..., description="元素ID")
) -> int: ) -> int:
@@ -193,7 +193,7 @@ async def fastapi_get_element_type_value(
summary="获取所有节点", summary="获取所有节点",
description="获取指定水网中的所有节点ID列表" description="获取指定水网中的所有节点ID列表"
) )
async def fastapi_get_nodes(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]: def fastapi_get_nodes(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
"""获取水网中所有节点的ID列表。""" """获取水网中所有节点的ID列表。"""
return get_nodes(network) return get_nodes(network)
@@ -202,7 +202,7 @@ async def fastapi_get_nodes(network: str = Query(..., description="管网名称
summary="获取所有管线", summary="获取所有管线",
description="获取指定水网中的所有管线ID列表" description="获取指定水网中的所有管线ID列表"
) )
async def fastapi_get_links(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]: def fastapi_get_links(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
"""获取水网中所有管线的ID列表。""" """获取水网中所有管线的ID列表。"""
return get_links(network) return get_links(network)
@@ -227,7 +227,7 @@ def get_node_links_endpoint(
summary="获取节点属性", summary="获取节点属性",
description="获取指定节点的所有属性信息" description="获取指定节点的所有属性信息"
) )
async def fast_get_node_properties( def fast_get_node_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
node: str = Query(..., description="节点ID") node: str = Query(..., description="节点ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -239,7 +239,7 @@ async def fast_get_node_properties(
summary="获取管线属性", summary="获取管线属性",
description="获取指定管线的所有属性信息" description="获取指定管线的所有属性信息"
) )
async def fast_get_link_properties( def fast_get_link_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
link: str = Query(..., description="管线ID") link: str = Query(..., description="管线ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -251,7 +251,7 @@ async def fast_get_link_properties(
summary="获取SCADA点属性", summary="获取SCADA点属性",
description="获取指定SCADA点的属性信息" description="获取指定SCADA点的属性信息"
) )
async def fast_get_scada_properties( def fast_get_scada_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
scada: str = Query(..., description="SCADA点ID") scada: str = Query(..., description="SCADA点ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -263,7 +263,7 @@ async def fast_get_scada_properties(
summary="获取所有SCADA点属性", summary="获取所有SCADA点属性",
description="获取指定水网中所有SCADA点的属性信息" description="获取指定水网中所有SCADA点的属性信息"
) )
async def fast_get_all_scada_properties( def fast_get_all_scada_properties(
network: str = Query(..., description="管网名称(或数据库名称)") network: str = Query(..., description="管网名称(或数据库名称)")
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
"""获取水网中所有SCADA点的属性列表。""" """获取水网中所有SCADA点的属性列表。"""
@@ -274,7 +274,7 @@ async def fast_get_all_scada_properties(
summary="获取指定类型元素属性", summary="获取指定类型元素属性",
description="获取指定类型的元素属性信息" description="获取指定类型的元素属性信息"
) )
async def fast_get_element_properties_with_type( def fast_get_element_properties_with_type(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
elementtype: str = Query(..., description="元素类型"), elementtype: str = Query(..., description="元素类型"),
element: str = Query(..., description="元素ID") element: str = Query(..., description="元素ID")
@@ -287,7 +287,7 @@ async def fast_get_element_properties_with_type(
summary="获取元素属性", summary="获取元素属性",
description="获取指定元素的属性信息" description="获取指定元素的属性信息"
) )
async def fast_get_element_properties( def fast_get_element_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
element: str = Query(..., description="元素ID") element: str = Query(..., description="元素ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -303,7 +303,7 @@ async def fast_get_element_properties(
summary="获取标题属性架构", summary="获取标题属性架构",
description="获取指定水网的标题(标题)属性架构定义" description="获取指定水网的标题(标题)属性架构定义"
) )
async def fast_get_title_schema( def fast_get_title_schema(
network: str = Query(..., description="管网名称(或数据库名称)") network: str = Query(..., description="管网名称(或数据库名称)")
) -> dict[str, dict[str, Any]]: ) -> dict[str, dict[str, Any]]:
"""获取水网标题的属性架构。""" """获取水网标题的属性架构。"""
@@ -314,7 +314,7 @@ async def fast_get_title_schema(
summary="获取水网标题属性", summary="获取水网标题属性",
description="获取指定水网的标题(Title)信息" description="获取指定水网的标题(Title)信息"
) )
async def fast_get_title(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]: def fast_get_title(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
"""获取水网的标题属性。""" """获取水网的标题属性。"""
return get_title(network) return get_title(network)
@@ -324,12 +324,12 @@ async def fast_get_title(network: str = Query(..., description="管网名称(
summary="设置水网标题属性", summary="设置水网标题属性",
description="设置指定水网的标题(Title)信息" description="设置指定水网的标题(Title)信息"
) )
async def fastapi_set_title( def fastapi_set_title(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置水网的标题属性。""" """设置水网的标题属性。"""
props = await req.json() props = payload
return set_title(network, ChangeSet(props)) return set_title(network, ChangeSet(props))
############################################################ ############################################################
@@ -341,7 +341,7 @@ async def fastapi_set_title(
summary="获取状态属性架构", summary="获取状态属性架构",
description="获取指定水网的状态(Status)属性架构定义" description="获取指定水网的状态(Status)属性架构定义"
) )
async def fastapi_get_status_schema( def fastapi_get_status_schema(
network: str = Query(..., description="管网名称(或数据库名称)") network: str = Query(..., description="管网名称(或数据库名称)")
) -> dict[str, dict[str, Any]]: ) -> dict[str, dict[str, Any]]:
"""获取水网状态的属性架构。""" """获取水网状态的属性架构。"""
@@ -352,7 +352,7 @@ async def fastapi_get_status_schema(
summary="获取管线状态", summary="获取管线状态",
description="获取指定管线的状态信息" description="获取指定管线的状态信息"
) )
async def fastapi_get_status( def fastapi_get_status(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
link: str = Query(..., description="管线ID") link: str = Query(..., description="管线ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -365,13 +365,13 @@ async def fastapi_get_status(
summary="设置管线状态", summary="设置管线状态",
description="设置指定管线的状态信息" description="设置指定管线的状态信息"
) )
async def fastapi_set_status_properties( def fastapi_set_status_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
link: str = Query(..., description="管线ID"), link: str = Query(..., description="管线ID"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置管线的状态属性。""" """设置管线的状态属性。"""
props = await req.json() props = payload
ps = {"link": link} | props ps = {"link": link} | props
return set_status(network, ChangeSet(ps)) return set_status(network, ChangeSet(ps))
@@ -385,7 +385,7 @@ async def fastapi_set_status_properties(
summary="删除节点", summary="删除节点",
description="删除指定的节点(接点/水源/蓄水池)" description="删除指定的节点(接点/水源/蓄水池)"
) )
async def fastapi_delete_node( def fastapi_delete_node(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
node: str = Query(..., description="节点ID") node: str = Query(..., description="节点ID")
) -> ChangeSet: ) -> ChangeSet:
@@ -405,7 +405,7 @@ async def fastapi_delete_node(
summary="删除管线", summary="删除管线",
description="删除指定的管线(管道/泵/阀门)" description="删除指定的管线(管道/泵/阀门)"
) )
async def fastapi_delete_link( def fastapi_delete_link(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
link: str = Query(..., description="管线ID") link: str = Query(..., description="管线ID")
) -> ChangeSet: ) -> ChangeSet:
+6 -6
View File
@@ -27,7 +27,7 @@ router = APIRouter()
# # example: set_coord(p, ChangeSet({'node': 'j1', 'x': 1.0, 'y': 2.0})) # # example: set_coord(p, ChangeSet({'node': 'j1', 'x': 1.0, 'y': 2.0}))
# @router.post("/setcoord/", response_model=None) # @router.post("/setcoord/", response_model=None)
# async def fastapi_set_coord(network: str, req: Request) -> ChangeSet: # async def fastapi_set_coord(network: str, req: Request) -> ChangeSet:
# props = await req.json() # props = payload
# return set_coord(network, ChangeSet(props)) # return set_coord(network, ChangeSet(props))
@router.get( @router.get(
@@ -35,7 +35,7 @@ router = APIRouter()
summary="获取节点坐标", summary="获取节点坐标",
description="获取指定节点的地理坐标(X, Y)" description="获取指定节点的地理坐标(X, Y)"
) )
async def fastapi_get_node_coord( def fastapi_get_node_coord(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
node: str = Query(..., description="节点ID") node: str = Query(..., description="节点ID")
) -> dict[str, float] | None: ) -> dict[str, float] | None:
@@ -48,7 +48,7 @@ async def fastapi_get_node_coord(
summary="获取范围内的网络元素", summary="获取范围内的网络元素",
description="获取指定地理范围内的网络节点和管线" description="获取指定地理范围内的网络节点和管线"
) )
async def fastapi_get_network_in_extent( def fastapi_get_network_in_extent(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
x1: float = Query(..., description="范围左下角X坐标", alias="x1"), x1: float = Query(..., description="范围左下角X坐标", alias="x1"),
y1: float = Query(..., description="范围左下角Y坐标", alias="y1"), y1: float = Query(..., description="范围左下角Y坐标", alias="y1"),
@@ -63,7 +63,7 @@ async def fastapi_get_network_in_extent(
summary="获取主要节点坐标", summary="获取主要节点坐标",
description="获取直径大于等于指定值的节点坐标" description="获取直径大于等于指定值的节点坐标"
) )
async def fastapi_get_majornode_coords( def fastapi_get_majornode_coords(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
diameter: int = Query(..., description="最小直径(mm)", gt=0) diameter: int = Query(..., description="最小直径(mm)", gt=0)
) -> dict[str, dict[str, float]]: ) -> dict[str, dict[str, float]]:
@@ -75,7 +75,7 @@ async def fastapi_get_majornode_coords(
summary="获取主要管道节点", summary="获取主要管道节点",
description="获取直径大于等于指定值的管道的节点ID" description="获取直径大于等于指定值的管道的节点ID"
) )
async def fastapi_get_major_pipe_nodes( def fastapi_get_major_pipe_nodes(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
diameter: int = Query(..., description="最小直径(mm)", gt=0) diameter: int = Query(..., description="最小直径(mm)", gt=0)
) -> list[str] | None: ) -> list[str] | None:
@@ -87,7 +87,7 @@ async def fastapi_get_major_pipe_nodes(
summary="获取网络管线节点", summary="获取网络管线节点",
description="获取指定水网所有管线的起点和终点节点" description="获取指定水网所有管线的起点和终点节点"
) )
async def fastapi_get_network_link_nodes( def fastapi_get_network_link_nodes(
network: str = Query(..., description="管网名称(或数据库名称)") network: str = Query(..., description="管网名称(或数据库名称)")
) -> list[str] | None: ) -> list[str] | None:
"""获取网络中所有管线的连接节点。""" """获取网络中所有管线的连接节点。"""
+23 -23
View File
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Request, Query, Path, Body from typing import Any
from typing import Any, List, Dict, Union
from fastapi import APIRouter, Body, Query
from app.services.tjnetwork import ( from app.services.tjnetwork import (
Any,
ChangeSet, ChangeSet,
add_junction, add_junction,
delete_junction, delete_junction,
@@ -14,7 +14,7 @@ from app.services.tjnetwork import (
router = APIRouter() router = APIRouter()
@router.get("/network-schemas/junction", summary="获取节点架构", description="获取指定项目的节点属性架构和数据类型定义。") @router.get("/network-schemas/junction", summary="获取节点架构", description="获取指定项目的节点属性架构和数据类型定义。")
async def fast_get_junction_schema( def fast_get_junction_schema(
network: str = Query(..., description="管网名称(或数据库名称)") network: str = Query(..., description="管网名称(或数据库名称)")
) -> dict[str, dict[str, Any]]: ) -> dict[str, dict[str, Any]]:
""" """
@@ -28,7 +28,7 @@ async def fast_get_junction_schema(
return get_junction_schema(network) return get_junction_schema(network)
@router.post("/junctions", response_model=None, summary="添加节点", description="在供水网络中添加新的节点,指定节点ID和空间坐标。") @router.post("/junctions", response_model=None, summary="添加节点", description="在供水网络中添加新的节点,指定节点ID和空间坐标。")
async def fastapi_add_junction( def fastapi_add_junction(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="节点 ID"), junction: str = Query(..., description="节点 ID"),
x: float = Query(..., description="X 坐标"), x: float = Query(..., description="X 坐标"),
@@ -52,7 +52,7 @@ async def fastapi_add_junction(
return add_junction(network, ChangeSet(ps)) return add_junction(network, ChangeSet(ps))
@router.delete("/junctions", response_model=None, summary="删除节点", description="从供水网络中删除指定的节点。") @router.delete("/junctions", response_model=None, summary="删除节点", description="从供水网络中删除指定的节点。")
async def fastapi_delete_junction( def fastapi_delete_junction(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="节点 ID") junction: str = Query(..., description="节点 ID")
) -> ChangeSet: ) -> ChangeSet:
@@ -70,7 +70,7 @@ async def fastapi_delete_junction(
return delete_junction(network, ChangeSet(ps)) return delete_junction(network, ChangeSet(ps))
@router.get("/junctions/elevation", summary="获取节点标高", description="获取指定节点的标高(海拔高度)。") @router.get("/junctions/elevation", summary="获取节点标高", description="获取指定节点的标高(海拔高度)。")
async def fastapi_get_junction_elevation( def fastapi_get_junction_elevation(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="节点 ID") junction: str = Query(..., description="节点 ID")
) -> float: ) -> float:
@@ -88,7 +88,7 @@ async def fastapi_get_junction_elevation(
return ps["elevation"] return ps["elevation"]
@router.get("/junctions/x", summary="获取节点 X 坐标", description="获取指定节点的 X 坐标值。") @router.get("/junctions/x", summary="获取节点 X 坐标", description="获取指定节点的 X 坐标值。")
async def fastapi_get_junction_x( def fastapi_get_junction_x(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="节点 ID") junction: str = Query(..., description="节点 ID")
) -> float: ) -> float:
@@ -106,7 +106,7 @@ async def fastapi_get_junction_x(
return ps["x"] return ps["x"]
@router.get("/junctions/y", summary="获取节点 Y 坐标", description="获取指定节点的 Y 坐标值。") @router.get("/junctions/y", summary="获取节点 Y 坐标", description="获取指定节点的 Y 坐标值。")
async def fastapi_get_junction_y( def fastapi_get_junction_y(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="节点 ID") junction: str = Query(..., description="节点 ID")
) -> float: ) -> float:
@@ -124,7 +124,7 @@ async def fastapi_get_junction_y(
return ps["y"] return ps["y"]
@router.get("/junctions/coord", summary="获取节点坐标", description="获取指定节点的 X 和 Y 坐标。") @router.get("/junctions/coord", summary="获取节点坐标", description="获取指定节点的 X 和 Y 坐标。")
async def fastapi_get_junction_coord( def fastapi_get_junction_coord(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="节点 ID") junction: str = Query(..., description="节点 ID")
) -> dict[str, float]: ) -> dict[str, float]:
@@ -143,7 +143,7 @@ async def fastapi_get_junction_coord(
return coord return coord
@router.get("/junctions/demand", summary="获取节点需水量", description="获取指定节点的需水量。") @router.get("/junctions/demand", summary="获取节点需水量", description="获取指定节点的需水量。")
async def fastapi_get_junction_demand( def fastapi_get_junction_demand(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="节点 ID") junction: str = Query(..., description="节点 ID")
) -> float: ) -> float:
@@ -161,7 +161,7 @@ async def fastapi_get_junction_demand(
return ps["demand"] return ps["demand"]
@router.get("/junctions/pattern", summary="获取节点需水模式", description="获取指定节点的需水模式标识。") @router.get("/junctions/pattern", summary="获取节点需水模式", description="获取指定节点的需水模式标识。")
async def fastapi_get_junction_pattern( def fastapi_get_junction_pattern(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="节点 ID") junction: str = Query(..., description="节点 ID")
) -> str: ) -> str:
@@ -179,7 +179,7 @@ async def fastapi_get_junction_pattern(
return ps["pattern"] return ps["pattern"]
@router.patch("/junctions/elevation", response_model=None, summary="设置节点标高", description="设置指定节点的标高值。") @router.patch("/junctions/elevation", response_model=None, summary="设置节点标高", description="设置指定节点的标高值。")
async def fastapi_set_junction_elevation( def fastapi_set_junction_elevation(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="节点 ID"), junction: str = Query(..., description="节点 ID"),
elevation: float = Query(..., description="标高(海拔高度)") elevation: float = Query(..., description="标高(海拔高度)")
@@ -199,7 +199,7 @@ async def fastapi_set_junction_elevation(
return set_junction(network, ChangeSet(ps)) return set_junction(network, ChangeSet(ps))
@router.patch("/junctions/x", response_model=None, summary="设置节点 X 坐标", description="设置指定节点的 X 坐标值。") @router.patch("/junctions/x", response_model=None, summary="设置节点 X 坐标", description="设置指定节点的 X 坐标值。")
async def fastapi_set_junction_x( def fastapi_set_junction_x(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="节点 ID"), junction: str = Query(..., description="节点 ID"),
x: float = Query(..., description="X 坐标值") x: float = Query(..., description="X 坐标值")
@@ -219,7 +219,7 @@ async def fastapi_set_junction_x(
return set_junction(network, ChangeSet(ps)) return set_junction(network, ChangeSet(ps))
@router.patch("/junctions/y", response_model=None, summary="设置节点 Y 坐标", description="设置指定节点的 Y 坐标值。") @router.patch("/junctions/y", response_model=None, summary="设置节点 Y 坐标", description="设置指定节点的 Y 坐标值。")
async def fastapi_set_junction_y( def fastapi_set_junction_y(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="节点 ID"), junction: str = Query(..., description="节点 ID"),
y: float = Query(..., description="Y 坐标值") y: float = Query(..., description="Y 坐标值")
@@ -239,7 +239,7 @@ async def fastapi_set_junction_y(
return set_junction(network, ChangeSet(ps)) return set_junction(network, ChangeSet(ps))
@router.patch("/junctions/coord", response_model=None, summary="设置节点坐标", description="设置指定节点的 X 和 Y 坐标。") @router.patch("/junctions/coord", response_model=None, summary="设置节点坐标", description="设置指定节点的 X 和 Y 坐标。")
async def fastapi_set_junction_coord( def fastapi_set_junction_coord(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="节点 ID"), junction: str = Query(..., description="节点 ID"),
x: float = Query(..., description="X 坐标值"), x: float = Query(..., description="X 坐标值"),
@@ -261,7 +261,7 @@ async def fastapi_set_junction_coord(
return set_junction(network, ChangeSet(ps)) return set_junction(network, ChangeSet(ps))
@router.patch("/junctions/demand", response_model=None, summary="设置节点需水量", description="设置指定节点的需水量。") @router.patch("/junctions/demand", response_model=None, summary="设置节点需水量", description="设置指定节点的需水量。")
async def fastapi_set_junction_demand( def fastapi_set_junction_demand(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="节点 ID"), junction: str = Query(..., description="节点 ID"),
demand: float = Query(..., description="需水量值") demand: float = Query(..., description="需水量值")
@@ -281,7 +281,7 @@ async def fastapi_set_junction_demand(
return set_junction(network, ChangeSet(ps)) return set_junction(network, ChangeSet(ps))
@router.patch("/junctions/pattern", response_model=None, summary="设置节点需水模式", description="设置指定节点的需水模式标识。") @router.patch("/junctions/pattern", response_model=None, summary="设置节点需水模式", description="设置指定节点的需水模式标识。")
async def fastapi_set_junction_pattern( def fastapi_set_junction_pattern(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="节点 ID"), junction: str = Query(..., description="节点 ID"),
pattern: str = Query(..., description="需水模式标识") pattern: str = Query(..., description="需水模式标识")
@@ -301,7 +301,7 @@ async def fastapi_set_junction_pattern(
return set_junction(network, ChangeSet(ps)) return set_junction(network, ChangeSet(ps))
@router.get("/junctions/properties", summary="获取节点属性", description="获取指定节点的所有属性信息。") @router.get("/junctions/properties", summary="获取节点属性", description="获取指定节点的所有属性信息。")
async def fastapi_get_junction_properties( def fastapi_get_junction_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="节点 ID") junction: str = Query(..., description="节点 ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -318,7 +318,7 @@ async def fastapi_get_junction_properties(
return get_junction(network, junction) return get_junction(network, junction)
@router.get("/junctions", summary="获取所有节点属性", description="获取指定项目中所有节点的属性信息。") @router.get("/junctions", summary="获取所有节点属性", description="获取指定项目中所有节点的属性信息。")
async def fastapi_get_all_junction_properties( def fastapi_get_all_junction_properties(
network: str = Query(..., description="管网名称(或数据库名称)") network: str = Query(..., description="管网名称(或数据库名称)")
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
""" """
@@ -337,10 +337,10 @@ async def fastapi_get_all_junction_properties(
return results return results
@router.patch("/junctions/properties", response_model=None, summary="批量设置节点属性", description="批量设置指定节点的多个属性。") @router.patch("/junctions/properties", response_model=None, summary="批量设置节点属性", description="批量设置指定节点的多个属性。")
async def fastapi_set_junction_properties( def fastapi_set_junction_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
junction: str = Query(..., description="节点 ID"), junction: str = Query(..., description="节点 ID"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
""" """
批量设置节点属性。 批量设置节点属性。
@@ -355,6 +355,6 @@ async def fastapi_set_junction_properties(
Returns: Returns:
ChangeSet: 包含变更信息的结果 ChangeSet: 包含变更信息的结果
""" """
props = await req.json() props = payload
ps = {"id": junction} | props ps = {"id": junction} | props
return set_junction(network, ChangeSet(ps)) return set_junction(network, ChangeSet(ps))
+25 -25
View File
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Request, Query, Path, Body from typing import Any
from typing import Any, List, Dict, Union
from fastapi import APIRouter, Body, Query
from app.services.tjnetwork import ( from app.services.tjnetwork import (
Any,
ChangeSet, ChangeSet,
PIPE_STATUS_OPEN, PIPE_STATUS_OPEN,
add_pipe, add_pipe,
@@ -15,7 +15,7 @@ from app.services.tjnetwork import (
router = APIRouter() router = APIRouter()
@router.get("/network-schemas/pipe", summary="获取管道模式", description="获取管道对象的模式定义,包含所有可用字段及其类型") @router.get("/network-schemas/pipe", summary="获取管道模式", description="获取管道对象的模式定义,包含所有可用字段及其类型")
async def fastapi_get_pipe_schema( def fastapi_get_pipe_schema(
network: str = Query(..., description="管网名称(或数据库名称)") network: str = Query(..., description="管网名称(或数据库名称)")
) -> dict[str, dict[str, Any]]: ) -> dict[str, dict[str, Any]]:
""" """
@@ -30,7 +30,7 @@ async def fastapi_get_pipe_schema(
return get_pipe_schema(network) return get_pipe_schema(network)
@router.post("/pipes", response_model=None, summary="添加管道", description="向网络中添加新的管道,需要提供管道的基本参数如长度、管径、粗糙度等") @router.post("/pipes", response_model=None, summary="添加管道", description="向网络中添加新的管道,需要提供管道的基本参数如长度、管径、粗糙度等")
async def fastapi_add_pipe( def fastapi_add_pipe(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pipe: str = Query(..., description="管道标识符"), pipe: str = Query(..., description="管道标识符"),
node1: str = Query(..., description="管道起始节点ID"), node1: str = Query(..., description="管道起始节点ID"),
@@ -71,7 +71,7 @@ async def fastapi_add_pipe(
return add_pipe(network, ChangeSet(ps)) return add_pipe(network, ChangeSet(ps))
@router.delete("/pipes", response_model=None, summary="删除管道", description="从网络中删除指定的管道") @router.delete("/pipes", response_model=None, summary="删除管道", description="从网络中删除指定的管道")
async def fastapi_delete_pipe( def fastapi_delete_pipe(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pipe: str = Query(..., description="要删除的管道ID") pipe: str = Query(..., description="要删除的管道ID")
) -> ChangeSet: ) -> ChangeSet:
@@ -89,7 +89,7 @@ async def fastapi_delete_pipe(
return delete_pipe(network, ChangeSet(ps)) return delete_pipe(network, ChangeSet(ps))
@router.get("/pipes/node1", summary="获取管道起始节点", description="获取指定管道的起始节点ID") @router.get("/pipes/node1", summary="获取管道起始节点", description="获取指定管道的起始节点ID")
async def fastapi_get_pipe_node1( def fastapi_get_pipe_node1(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pipe: str = Query(..., description="管道ID") pipe: str = Query(..., description="管道ID")
) -> str | None: ) -> str | None:
@@ -107,7 +107,7 @@ async def fastapi_get_pipe_node1(
return ps["node1"] return ps["node1"]
@router.get("/pipes/node2", summary="获取管道终止节点", description="获取指定管道的终止节点ID") @router.get("/pipes/node2", summary="获取管道终止节点", description="获取指定管道的终止节点ID")
async def fastapi_get_pipe_node2( def fastapi_get_pipe_node2(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pipe: str = Query(..., description="管道ID") pipe: str = Query(..., description="管道ID")
) -> str | None: ) -> str | None:
@@ -125,7 +125,7 @@ async def fastapi_get_pipe_node2(
return ps["node2"] return ps["node2"]
@router.get("/pipes/length", summary="获取管道长度", description="获取指定管道的长度") @router.get("/pipes/length", summary="获取管道长度", description="获取指定管道的长度")
async def fastapi_get_pipe_length( def fastapi_get_pipe_length(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pipe: str = Query(..., description="管道ID") pipe: str = Query(..., description="管道ID")
) -> float | None: ) -> float | None:
@@ -143,7 +143,7 @@ async def fastapi_get_pipe_length(
return ps["length"] return ps["length"]
@router.get("/pipes/diameter", summary="获取管道管径", description="获取指定管道的管径") @router.get("/pipes/diameter", summary="获取管道管径", description="获取指定管道的管径")
async def fastapi_get_pipe_diameter( def fastapi_get_pipe_diameter(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pipe: str = Query(..., description="管道ID") pipe: str = Query(..., description="管道ID")
) -> float | None: ) -> float | None:
@@ -161,7 +161,7 @@ async def fastapi_get_pipe_diameter(
return ps["diameter"] return ps["diameter"]
@router.get("/pipes/roughness", summary="获取管道粗糙度", description="获取指定管道的粗糙度") @router.get("/pipes/roughness", summary="获取管道粗糙度", description="获取指定管道的粗糙度")
async def fastapi_get_pipe_roughness( def fastapi_get_pipe_roughness(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pipe: str = Query(..., description="管道ID") pipe: str = Query(..., description="管道ID")
) -> float | None: ) -> float | None:
@@ -179,7 +179,7 @@ async def fastapi_get_pipe_roughness(
return ps["roughness"] return ps["roughness"]
@router.get("/pipes/minor-loss", summary="获取管道局部阻力系数", description="获取指定管道的局部阻力系数") @router.get("/pipes/minor-loss", summary="获取管道局部阻力系数", description="获取指定管道的局部阻力系数")
async def fastapi_get_pipe_minor_loss( def fastapi_get_pipe_minor_loss(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pipe: str = Query(..., description="管道ID") pipe: str = Query(..., description="管道ID")
) -> float | None: ) -> float | None:
@@ -197,7 +197,7 @@ async def fastapi_get_pipe_minor_loss(
return ps["minor_loss"] return ps["minor_loss"]
@router.get("/pipes/status", summary="获取管道状态", description="获取指定管道的状态(开启或关闭)") @router.get("/pipes/status", summary="获取管道状态", description="获取指定管道的状态(开启或关闭)")
async def fastapi_get_pipe_status( def fastapi_get_pipe_status(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pipe: str = Query(..., description="管道ID") pipe: str = Query(..., description="管道ID")
) -> str | None: ) -> str | None:
@@ -215,7 +215,7 @@ async def fastapi_get_pipe_status(
return ps["status"] return ps["status"]
@router.patch("/pipes/node1", response_model=None, summary="设置管道起始节点", description="设置指定管道的起始节点") @router.patch("/pipes/node1", response_model=None, summary="设置管道起始节点", description="设置指定管道的起始节点")
async def fastapi_set_pipe_node1( def fastapi_set_pipe_node1(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pipe: str = Query(..., description="管道ID"), pipe: str = Query(..., description="管道ID"),
node1: str = Query(..., description="新的起始节点ID") node1: str = Query(..., description="新的起始节点ID")
@@ -235,7 +235,7 @@ async def fastapi_set_pipe_node1(
return set_pipe(network, ChangeSet(ps)) return set_pipe(network, ChangeSet(ps))
@router.patch("/pipes/node2", response_model=None, summary="设置管道终止节点", description="设置指定管道的终止节点") @router.patch("/pipes/node2", response_model=None, summary="设置管道终止节点", description="设置指定管道的终止节点")
async def fastapi_set_pipe_node2( def fastapi_set_pipe_node2(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pipe: str = Query(..., description="管道ID"), pipe: str = Query(..., description="管道ID"),
node2: str = Query(..., description="新的终止节点ID") node2: str = Query(..., description="新的终止节点ID")
@@ -255,7 +255,7 @@ async def fastapi_set_pipe_node2(
return set_pipe(network, ChangeSet(ps)) return set_pipe(network, ChangeSet(ps))
@router.patch("/pipes/length", response_model=None, summary="设置管道长度", description="设置指定管道的长度") @router.patch("/pipes/length", response_model=None, summary="设置管道长度", description="设置指定管道的长度")
async def fastapi_set_pipe_length( def fastapi_set_pipe_length(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pipe: str = Query(..., description="管道ID"), pipe: str = Query(..., description="管道ID"),
length: float = Query(..., description="新的管道长度(单位:米)") length: float = Query(..., description="新的管道长度(单位:米)")
@@ -275,7 +275,7 @@ async def fastapi_set_pipe_length(
return set_pipe(network, ChangeSet(ps)) return set_pipe(network, ChangeSet(ps))
@router.patch("/pipes/diameter", response_model=None, summary="设置管道管径", description="设置指定管道的管径") @router.patch("/pipes/diameter", response_model=None, summary="设置管道管径", description="设置指定管道的管径")
async def fastapi_set_pipe_diameter( def fastapi_set_pipe_diameter(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pipe: str = Query(..., description="管道ID"), pipe: str = Query(..., description="管道ID"),
diameter: float = Query(..., description="新的管道管径(单位:毫米)") diameter: float = Query(..., description="新的管道管径(单位:毫米)")
@@ -295,7 +295,7 @@ async def fastapi_set_pipe_diameter(
return set_pipe(network, ChangeSet(ps)) return set_pipe(network, ChangeSet(ps))
@router.patch("/pipes/roughness", response_model=None, summary="设置管道粗糙度", description="设置指定管道的粗糙度") @router.patch("/pipes/roughness", response_model=None, summary="设置管道粗糙度", description="设置指定管道的粗糙度")
async def fastapi_set_pipe_roughness( def fastapi_set_pipe_roughness(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pipe: str = Query(..., description="管道ID"), pipe: str = Query(..., description="管道ID"),
roughness: float = Query(..., description="新的管道粗糙度值") roughness: float = Query(..., description="新的管道粗糙度值")
@@ -315,7 +315,7 @@ async def fastapi_set_pipe_roughness(
return set_pipe(network, ChangeSet(ps)) return set_pipe(network, ChangeSet(ps))
@router.patch("/pipes/minor-loss", response_model=None, summary="设置管道局部阻力系数", description="设置指定管道的局部阻力系数") @router.patch("/pipes/minor-loss", response_model=None, summary="设置管道局部阻力系数", description="设置指定管道的局部阻力系数")
async def fastapi_set_pipe_minor_loss( def fastapi_set_pipe_minor_loss(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pipe: str = Query(..., description="管道ID"), pipe: str = Query(..., description="管道ID"),
minor_loss: float = Query(..., description="新的局部阻力系数值") minor_loss: float = Query(..., description="新的局部阻力系数值")
@@ -335,7 +335,7 @@ async def fastapi_set_pipe_minor_loss(
return set_pipe(network, ChangeSet(ps)) return set_pipe(network, ChangeSet(ps))
@router.patch("/pipes/status", response_model=None, summary="设置管道状态", description="设置指定管道的状态(开启或关闭)") @router.patch("/pipes/status", response_model=None, summary="设置管道状态", description="设置指定管道的状态(开启或关闭)")
async def fastapi_set_pipe_status( def fastapi_set_pipe_status(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pipe: str = Query(..., description="管道ID"), pipe: str = Query(..., description="管道ID"),
status: str = Query(..., description="新的管道状态(开启/关闭)") status: str = Query(..., description="新的管道状态(开启/关闭)")
@@ -355,7 +355,7 @@ async def fastapi_set_pipe_status(
return set_pipe(network, ChangeSet(ps)) return set_pipe(network, ChangeSet(ps))
@router.get("/pipes/properties", summary="获取管道属性", description="获取指定管道的所有属性信息") @router.get("/pipes/properties", summary="获取管道属性", description="获取指定管道的所有属性信息")
async def fastapi_get_pipe_properties( def fastapi_get_pipe_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pipe: str = Query(..., description="管道ID") pipe: str = Query(..., description="管道ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -372,7 +372,7 @@ async def fastapi_get_pipe_properties(
return get_pipe(network, pipe) return get_pipe(network, pipe)
@router.get("/pipes", summary="获取所有管道属性", description="获取网络中所有管道的属性信息列表") @router.get("/pipes", summary="获取所有管道属性", description="获取网络中所有管道的属性信息列表")
async def fastapi_get_all_pipe_properties( def fastapi_get_all_pipe_properties(
network: str = Query(..., description="管网名称(或数据库名称)") network: str = Query(..., description="管网名称(或数据库名称)")
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
""" """
@@ -389,10 +389,10 @@ async def fastapi_get_all_pipe_properties(
return results return results
@router.patch("/pipes/properties", response_model=None, summary="设置管道属性", description="批量设置指定管道的多个属性") @router.patch("/pipes/properties", response_model=None, summary="设置管道属性", description="批量设置指定管道的多个属性")
async def fastapi_set_pipe_properties( def fastapi_set_pipe_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pipe: str = Query(..., description="管道ID"), pipe: str = Query(..., description="管道ID"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
""" """
批量设置管道属性。 批量设置管道属性。
@@ -405,6 +405,6 @@ async def fastapi_set_pipe_properties(
Returns: Returns:
ChangeSet对象,包含本次修改的变更信息 ChangeSet对象,包含本次修改的变更信息
""" """
props = await req.json() props = payload
ps = {"id": pipe} | props ps = {"id": pipe} | props
return set_pipe(network, ChangeSet(ps)) return set_pipe(network, ChangeSet(ps))
+15 -15
View File
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Request, Query, Path, Body from typing import Any
from typing import Any, List, Dict, Union
from fastapi import APIRouter, Body, Query
from app.services.tjnetwork import ( from app.services.tjnetwork import (
Any,
ChangeSet, ChangeSet,
add_pump, add_pump,
delete_pump, delete_pump,
@@ -14,7 +14,7 @@ from app.services.tjnetwork import (
router = APIRouter() router = APIRouter()
@router.get("/network-schemas/pump", summary="获取水泵模式", description="获取水泵对象的模式定义,包含所有可用字段及其类型") @router.get("/network-schemas/pump", summary="获取水泵模式", description="获取水泵对象的模式定义,包含所有可用字段及其类型")
async def fastapi_get_pump_schema( def fastapi_get_pump_schema(
network: str = Query(..., description="管网名称(或数据库名称)") network: str = Query(..., description="管网名称(或数据库名称)")
) -> dict[str, dict[str, Any]]: ) -> dict[str, dict[str, Any]]:
""" """
@@ -29,7 +29,7 @@ async def fastapi_get_pump_schema(
return get_pump_schema(network) return get_pump_schema(network)
@router.post("/pumps", response_model=None, summary="添加水泵", description="向网络中添加新的水泵,需要提供水泵的基本参数如功率等") @router.post("/pumps", response_model=None, summary="添加水泵", description="向网络中添加新的水泵,需要提供水泵的基本参数如功率等")
async def fastapi_add_pump( def fastapi_add_pump(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pump: str = Query(..., description="水泵标识符"), pump: str = Query(..., description="水泵标识符"),
node1: str = Query(..., description="水泵起始节点ID"), node1: str = Query(..., description="水泵起始节点ID"),
@@ -53,7 +53,7 @@ async def fastapi_add_pump(
return add_pump(network, ChangeSet(ps)) return add_pump(network, ChangeSet(ps))
@router.delete("/pumps", response_model=None, summary="删除水泵", description="从网络中删除指定的水泵") @router.delete("/pumps", response_model=None, summary="删除水泵", description="从网络中删除指定的水泵")
async def fastapi_delete_pump( def fastapi_delete_pump(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pump: str = Query(..., description="要删除的水泵ID") pump: str = Query(..., description="要删除的水泵ID")
) -> ChangeSet: ) -> ChangeSet:
@@ -71,7 +71,7 @@ async def fastapi_delete_pump(
return delete_pump(network, ChangeSet(ps)) return delete_pump(network, ChangeSet(ps))
@router.get("/pumps/node1", summary="获取水泵起始节点", description="获取指定水泵的起始节点ID") @router.get("/pumps/node1", summary="获取水泵起始节点", description="获取指定水泵的起始节点ID")
async def fastapi_get_pump_node1( def fastapi_get_pump_node1(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pump: str = Query(..., description="水泵ID") pump: str = Query(..., description="水泵ID")
) -> str | None: ) -> str | None:
@@ -89,7 +89,7 @@ async def fastapi_get_pump_node1(
return ps["node1"] return ps["node1"]
@router.get("/pumps/node2", summary="获取水泵终止节点", description="获取指定水泵的终止节点ID") @router.get("/pumps/node2", summary="获取水泵终止节点", description="获取指定水泵的终止节点ID")
async def fastapi_get_pump_node2( def fastapi_get_pump_node2(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pump: str = Query(..., description="水泵ID") pump: str = Query(..., description="水泵ID")
) -> str | None: ) -> str | None:
@@ -107,7 +107,7 @@ async def fastapi_get_pump_node2(
return ps["node2"] return ps["node2"]
@router.patch("/pumps/node1", response_model=None, summary="设置水泵起始节点", description="设置指定水泵的起始节点") @router.patch("/pumps/node1", response_model=None, summary="设置水泵起始节点", description="设置指定水泵的起始节点")
async def fastapi_set_pump_node1( def fastapi_set_pump_node1(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pump: str = Query(..., description="水泵ID"), pump: str = Query(..., description="水泵ID"),
node1: str = Query(..., description="新的起始节点ID") node1: str = Query(..., description="新的起始节点ID")
@@ -127,7 +127,7 @@ async def fastapi_set_pump_node1(
return set_pump(network, ChangeSet(ps)) return set_pump(network, ChangeSet(ps))
@router.patch("/pumps/node2", response_model=None, summary="设置水泵终止节点", description="设置指定水泵的终止节点") @router.patch("/pumps/node2", response_model=None, summary="设置水泵终止节点", description="设置指定水泵的终止节点")
async def fastapi_set_pump_node2( def fastapi_set_pump_node2(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pump: str = Query(..., description="水泵ID"), pump: str = Query(..., description="水泵ID"),
node2: str = Query(..., description="新的终止节点ID") node2: str = Query(..., description="新的终止节点ID")
@@ -147,7 +147,7 @@ async def fastapi_set_pump_node2(
return set_pump(network, ChangeSet(ps)) return set_pump(network, ChangeSet(ps))
@router.get("/pumps/properties", summary="获取水泵属性", description="获取指定水泵的所有属性信息") @router.get("/pumps/properties", summary="获取水泵属性", description="获取指定水泵的所有属性信息")
async def fastapi_get_pump_properties( def fastapi_get_pump_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pump: str = Query(..., description="水泵ID") pump: str = Query(..., description="水泵ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -164,7 +164,7 @@ async def fastapi_get_pump_properties(
return get_pump(network, pump) return get_pump(network, pump)
@router.get("/pumps", summary="获取所有水泵属性", description="获取网络中所有水泵的属性信息列表") @router.get("/pumps", summary="获取所有水泵属性", description="获取网络中所有水泵的属性信息列表")
async def fastapi_get_all_pump_properties( def fastapi_get_all_pump_properties(
network: str = Query(..., description="管网名称(或数据库名称)") network: str = Query(..., description="管网名称(或数据库名称)")
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
""" """
@@ -181,10 +181,10 @@ async def fastapi_get_all_pump_properties(
return results return results
@router.patch("/pumps/properties", response_model=None, summary="设置水泵属性", description="批量设置指定水泵的多个属性") @router.patch("/pumps/properties", response_model=None, summary="设置水泵属性", description="批量设置指定水泵的多个属性")
async def fastapi_set_pump_properties( def fastapi_set_pump_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
pump: str = Query(..., description="水泵ID"), pump: str = Query(..., description="水泵ID"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
""" """
批量设置水泵属性。 批量设置水泵属性。
@@ -197,6 +197,6 @@ async def fastapi_set_pump_properties(
Returns: Returns:
ChangeSet对象,包含本次修改的变更信息 ChangeSet对象,包含本次修改的变更信息
""" """
props = await req.json() props = payload
ps = {"id": pump} | props ps = {"id": pump} | props
return set_pump(network, ChangeSet(ps)) return set_pump(network, ChangeSet(ps))
+40 -502
View File
@@ -1,534 +1,72 @@
from fastapi import APIRouter, Request, Query, Path, Body from typing import Any
from typing import Any, List, Dict, Union
from fastapi import APIRouter, Body, Query
from app.services.tjnetwork import ( from app.services.tjnetwork import (
Any,
ChangeSet, ChangeSet,
add_district_metering_area,
add_region, add_region,
add_service_area,
add_virtual_district,
calculate_district_metering_area_for_network,
calculate_district_metering_area_for_nodes,
calculate_district_metering_area_for_region,
calculate_service_area,
calculate_virtual_district,
delete_district_metering_area,
delete_region, delete_region,
delete_service_area, get_nodes_in_region,
delete_virtual_district,
generate_district_metering_area,
generate_service_area,
generate_sub_district_metering_area,
generate_virtual_district,
get_all_district_metering_area_ids,
get_all_district_metering_areas,
get_all_service_areas,
get_all_virtual_districts,
get_district_metering_area,
get_district_metering_area_schema,
get_region, get_region,
get_region_schema, get_region_schema,
get_service_area, get_regions,
get_service_area_schema,
get_virtual_district,
get_virtual_district_schema,
set_district_metering_area,
set_region, set_region,
set_service_area,
set_virtual_district,
) )
router = APIRouter() router = APIRouter()
############################################################
# region 32
############################################################
@router.get( @router.get("/network-schemas/region", summary="获取区域属性架构")
"/network-schemas/region", def get_region_schema_endpoint(
summary="获取区域属性架构", network: str = Query(..., description="管网名称(或数据库名称)"),
description="获取指定水网的区域属性架构定义"
)
async def fastapi_get_region_schema(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> dict[str, dict[str, Any]]: ) -> dict[str, dict[str, Any]]:
"""获取区域的属性架构。"""
return get_region_schema(network) return get_region_schema(network)
@router.get(
"/regions/detail", @router.get("/regions", summary="获取区域列表")
summary="获取区域信息", def get_regions_endpoint(
description="获取指定ID的区域详细信息"
)
async def fastapi_get_region(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
id: str = Query(..., description="区域ID") ) -> list[dict[str, Any]]:
return [get_region(network, region_id) for region_id in get_regions(network)]
@router.get("/regions/detail", summary="获取区域信息")
def get_region_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
id: str = Query(..., description="区域 ID"),
) -> dict[str, Any]: ) -> dict[str, Any]:
"""获取区域的详细信息。"""
return get_region(network, id) return get_region(network, id)
@router.patch(
"/regions", @router.get("/regions/nodes", summary="获取区域节点")
response_model=None, def get_region_nodes_endpoint(
summary="设置区域属性",
description="修改指定区域的属性信息"
)
async def fastapi_set_region(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None id: str = Query(..., description="区域 ID"),
) -> ChangeSet:
"""设置区域属性。"""
props = await req.json()
return set_region(network, ChangeSet(props))
@router.post(
"/regions",
response_model=None,
summary="添加新区域",
description="向水网添加一个新的区域"
)
async def fastapi_add_region(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""添加新的区域。"""
props = await req.json()
return add_region(network, ChangeSet(props))
@router.delete(
"/regions",
response_model=None,
summary="删除区域",
description="删除指定的区域"
)
async def fastapi_delete_region(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""删除区域。"""
props = await req.json()
return delete_region(network, ChangeSet(props))
############################################################
# district_metering_area 33
############################################################
@router.post(
"/district-metering-areas/for-region",
summary="计算区域内DMA分区",
description="为指定区域计算区域计量(DMA)分区方案"
)
async def fastapi_calculate_district_metering_area_for_region(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> list[list[str]]:
"""
计算区域内DMA分区。
请求体格式:
{
"region": 区域ID(str),
"part_count": 分区数量(int),
"part_type": 分区类型(int)
}
"""
props = await req.json()
region = props["region"]
part_count = props["part_count"]
part_type = props["part_type"]
return calculate_district_metering_area_for_region(
network, region, part_count, part_type
)
@router.post(
"/district-metering-areas/for-network",
summary="计算整网DMA分区",
description="为整个水网计算区域计量(DMA)分区方案"
)
async def fastapi_calculate_district_metering_area_for_network(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> list[list[str]]:
"""
计算整网DMA分区。
请求体格式:
{
"part_count": 分区数量(int),
"part_type": 分区类型(int)
}
"""
props = await req.json()
part_count = props["part_count"]
part_type = props["part_type"]
return calculate_district_metering_area_for_network(network, part_count, part_type)
@router.get(
"/network-schemas/district-metering-area",
summary="获取DMA属性架构",
description="获取指定水网的区域计量(DMA)属性架构定义"
)
async def fastapi_get_district_metering_area_schema(
network: str = Query(..., description="管网名称(或数据库名称)"),
) -> dict[str, dict[str, Any]]:
"""获取DMA的属性架构。"""
return get_district_metering_area_schema(network)
@router.get(
"/district-metering-areas/detail",
summary="获取DMA信息",
description="获取指定ID的区域计量(DMA)详细信息"
)
async def fastapi_get_district_metering_area(
network: str = Query(..., description="管网名称(或数据库名称)"),
id: str = Query(..., description="DMA ID")
) -> dict[str, Any]:
"""获取DMA的详细信息。"""
return get_district_metering_area(network, id)
@router.patch(
"/district-metering-areas",
response_model=None,
summary="设置DMA属性",
description="修改指定DMA的属性信息"
)
async def fastapi_set_district_metering_area(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""设置DMA属性。"""
props = await req.json()
return set_district_metering_area(network, ChangeSet(props))
@router.post(
"/district-metering-areas",
response_model=None,
summary="添加新DMA",
description="向水网添加一个新的区域计量(DMA)"
)
async def fastapi_add_district_metering_area(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""添加新的DMA。"""
props = await req.json()
# boundary should be [(x,y), (x,y)]
boundary = props.get("boundary", [])
newBoundary = []
for pt in boundary:
if len(pt) >= 2:
newBoundary.append((pt[0], pt[1]))
props["boundary"] = newBoundary
return add_district_metering_area(network, ChangeSet(props))
@router.delete(
"/district-metering-areas",
response_model=None,
summary="删除DMA",
description="删除指定的区域计量(DMA)"
)
async def fastapi_delete_district_metering_area(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""删除DMA。"""
props = await req.json()
return delete_district_metering_area(network, ChangeSet(props))
@router.get(
"/district-metering-areas/ids",
summary="获取所有DMA ID",
description="获取指定水网中所有DMA的ID列表"
)
async def fastapi_get_all_district_metering_area_ids(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> list[str]: ) -> list[str]:
"""获取所有DMA的ID列表。""" return get_nodes_in_region(network, id)
return get_all_district_metering_area_ids(network)
@router.get(
"/district-metering-areas",
summary="获取所有DMA",
description="获取指定水网中所有DMA的详细信息"
)
async def getalldistrictmeteringareas(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> list[dict[str, Any]]:
"""获取所有DMA的详细信息列表。"""
return get_all_district_metering_areas(network)
@router.post( @router.patch("/regions", summary="修改区域", response_model=None)
"/district-metering-area-generation-runs", def set_region_endpoint(
response_model=None,
summary="生成DMA分区",
description="根据参数自动生成水网的DMA分区方案"
)
async def fastapi_generate_district_metering_area(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
part_count: int = Query(..., description="分区数量", gt=0), payload: dict[str, Any] = Body(...),
part_type: int = Query(..., description="分区类型"),
inflate_delta: float = Query(..., description="膨胀参数")
) -> ChangeSet: ) -> ChangeSet:
"""生成DMA分区。""" return set_region(network, ChangeSet(payload))
return generate_district_metering_area(
network, part_count, part_type, inflate_delta
)
@router.post(
"/sub-district-metering-areas", @router.post("/regions", summary="添加区域", response_model=None)
response_model=None, def add_region_endpoint(
summary="生成DMA子分区",
description="为指定DMA生成子DMA分区"
)
async def fastapi_generate_sub_district_metering_area(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
dma: str = Query(..., description="DMA ID"), payload: dict[str, Any] = Body(...),
part_count: int = Query(..., description="分区数量", gt=0),
part_type: int = Query(..., description="分区类型"),
inflate_delta: float = Query(..., description="膨胀参数")
) -> ChangeSet: ) -> ChangeSet:
"""生成DMA子分区。""" payload["boundary"] = [tuple(point[:2]) for point in payload.get("boundary", [])]
return generate_sub_district_metering_area( return add_region(network, ChangeSet(payload))
network, dma, part_count, part_type, inflate_delta
)
############################################################ @router.delete("/regions", summary="删除区域", response_model=None)
# service_area 34 def delete_region_endpoint(
############################################################
@router.post(
"/service-area-calculations",
summary="计算服务区",
description="计算指定水网的服务区分区,返回全部时间步结果"
)
async def fastapi_calculate_service_area(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
) -> list[dict[str, list[str]]]: payload: dict[str, Any] = Body(...),
"""计算服务区分区,返回全部时间步结果。"""
return calculate_service_area(network)
@router.get(
"/network-schemas/service-area",
summary="获取服务区属性架构",
description="获取指定水网的服务区属性架构定义"
)
async def fastapi_get_service_area_schema(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> dict[str, dict[str, Any]]:
"""获取服务区的属性架构。"""
return get_service_area_schema(network)
@router.get(
"/service-areas/detail",
summary="获取服务区信息",
description="获取指定ID的服务区详细信息"
)
async def fastapi_get_service_area(
network: str = Query(..., description="管网名称(或数据库名称)"),
id: str = Query(..., description="服务区ID")
) -> dict[str, Any]:
"""获取服务区的详细信息。"""
return get_service_area(network, id)
@router.patch(
"/service-areas",
response_model=None,
summary="设置服务区属性",
description="修改指定服务区的属性信息"
)
async def fastapi_set_service_area(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet: ) -> ChangeSet:
"""设置服务区属性。""" return delete_region(network, ChangeSet(payload))
props = await req.json()
return set_service_area(network, ChangeSet(props))
@router.post(
"/service-areas",
response_model=None,
summary="添加新服务区",
description="向水网添加一个新的服务区"
)
async def fastapi_add_service_area(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""添加新的服务区。"""
props = await req.json()
return add_service_area(network, ChangeSet(props))
@router.delete(
"/service-areas",
response_model=None,
summary="删除服务区",
description="删除指定的服务区"
)
async def fastapi_delete_service_area(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""删除服务区。"""
props = await req.json()
return delete_service_area(network, ChangeSet(props))
@router.get(
"/service-areas",
summary="获取所有服务区",
description="获取指定水网中的所有服务区信息"
)
async def fastapi_get_all_service_areas(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> list[dict[str, Any]]:
"""获取所有服务区的信息列表。"""
return get_all_service_areas(network)
@router.post(
"/service-area-generation-runs",
response_model=None,
summary="生成服务区分区",
description="根据参数自动生成水网的服务区分区"
)
async def fastapi_generate_service_area(
network: str = Query(..., description="管网名称(或数据库名称)"),
inflate_delta: float = Query(..., description="膨胀参数")
) -> ChangeSet:
"""生成服务区分区。"""
return generate_service_area(network, inflate_delta)
############################################################
# virtual_district 35
############################################################
@router.post(
"/virtual-district-calculations",
summary="计算虚拟分区",
description="根据指定的压力监测节点作为中心节点计算虚拟分区方案"
)
async def fastapi_calculate_virtual_district(
network: str = Query(..., description="管网名称(或数据库名称)"),
centers: list[str] = Query(..., description="压力监测节点ID列表")
) -> dict[str, list[Any]]:
"""计算虚拟分区。"""
return calculate_virtual_district(network, centers)
@router.get(
"/network-schemas/virtual-district",
summary="获取虚拟分区属性架构",
description="获取指定水网的虚拟分区属性架构定义"
)
async def fastapi_get_virtual_district_schema(
network: str = Query(..., description="管网名称(或数据库名称)"),
) -> dict[str, dict[str, Any]]:
"""获取虚拟分区的属性架构。"""
return get_virtual_district_schema(network)
@router.get(
"/virtual-districts/detail",
summary="获取虚拟分区信息",
description="获取指定ID的虚拟分区详细信息"
)
async def fastapi_get_virtual_district(
network: str = Query(..., description="管网名称(或数据库名称)"),
id: str = Query(..., description="虚拟分区ID")
) -> dict[str, Any]:
"""获取虚拟分区的详细信息。"""
return get_virtual_district(network, id)
@router.patch(
"/virtual-districts",
response_model=None,
summary="设置虚拟分区属性",
description="修改指定虚拟分区的属性信息"
)
async def fastapi_set_virtual_district(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""设置虚拟分区属性。"""
props = await req.json()
return set_virtual_district(network, ChangeSet(props))
@router.post(
"/virtual-districts",
response_model=None,
summary="添加新虚拟分区",
description="向水网添加一个新的虚拟分区"
)
async def fastapi_add_virtual_district(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""添加新的虚拟分区。"""
props = await req.json()
return add_virtual_district(network, ChangeSet(props))
@router.delete(
"/virtual-districts",
response_model=None,
summary="删除虚拟分区",
description="删除指定的虚拟分区"
)
async def fastapi_delete_virtual_district(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""删除虚拟分区。"""
props = await req.json()
return delete_virtual_district(network, ChangeSet(props))
@router.get(
"/virtual-districts",
summary="获取所有虚拟分区",
description="获取指定水网中的所有虚拟分区信息"
)
async def fastapi_get_all_virtual_district(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> list[dict[str, Any]]:
"""获取所有虚拟分区的信息列表。"""
return get_all_virtual_districts(network)
@router.post(
"/virtual-district-generation-runs",
response_model=None,
summary="生成虚拟分区",
description="根据参数自动生成虚拟分区方案"
)
async def fastapi_generate_virtual_district(
network: str = Query(..., description="管网名称(或数据库名称)"),
inflate_delta: float = Query(..., description="膨胀参数"),
req: Request = None
) -> ChangeSet:
"""生成虚拟分区。"""
props = await req.json()
return generate_virtual_district(network, props["centers"], inflate_delta)
@router.post(
"/district-metering-areas/for-nodes",
summary="计算节点DMA分区",
description="为指定节点集计算区域计量(DMA)分区方案"
)
async def fastapi_calculate_district_metering_area_for_nodes(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> list[list[str]]:
"""
计算节点DMA分区。
请求体格式:
{
"nodes": 节点ID列表(list[str]),
"part_count": 分区数量(int),
"part_type": 分区类型(int)
}
"""
props = await req.json()
nodes = props["nodes"]
part_count = props["part_count"]
part_type = props["part_type"]
return calculate_district_metering_area_for_nodes(
network, nodes, part_count, part_type
)
+21 -21
View File
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Request, Query, Path, Body from typing import Any
from typing import Any, List, Dict, Union
from fastapi import APIRouter, Body, Query
from app.services.tjnetwork import ( from app.services.tjnetwork import (
Any,
ChangeSet, ChangeSet,
add_reservoir, add_reservoir,
delete_reservoir, delete_reservoir,
@@ -18,7 +18,7 @@ router = APIRouter()
summary="获取水库模式", summary="获取水库模式",
description="获取指定供水网络中所有水库的模式/属性字段定义" description="获取指定供水网络中所有水库的模式/属性字段定义"
) )
async def fast_get_reservoir_schema( def fast_get_reservoir_schema(
network: str = Query(..., description="管网名称(或数据库名称)") network: str = Query(..., description="管网名称(或数据库名称)")
) -> dict[str, dict[str, Any]]: ) -> dict[str, dict[str, Any]]:
""" """
@@ -40,7 +40,7 @@ async def fast_get_reservoir_schema(
summary="添加水库", summary="添加水库",
description="在指定供水网络中添加新的水库/水源节点" description="在指定供水网络中添加新的水库/水源节点"
) )
async def fastapi_add_reservoir( def fastapi_add_reservoir(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
reservoir: str = Query(..., description="水库的唯一标识符"), reservoir: str = Query(..., description="水库的唯一标识符"),
x: float = Query(..., description="水库的X坐标"), x: float = Query(..., description="水库的X坐标"),
@@ -71,7 +71,7 @@ async def fastapi_add_reservoir(
summary="删除水库", summary="删除水库",
description="从指定供水网络中删除指定的水库/水源节点" description="从指定供水网络中删除指定的水库/水源节点"
) )
async def fastapi_delete_reservoir( def fastapi_delete_reservoir(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
reservoir: str = Query(..., description="要删除的水库的唯一标识符") reservoir: str = Query(..., description="要删除的水库的唯一标识符")
) -> ChangeSet: ) -> ChangeSet:
@@ -95,7 +95,7 @@ async def fastapi_delete_reservoir(
summary="获取水库水头", summary="获取水库水头",
description="获取指定水库的供水水头/总水头值" description="获取指定水库的供水水头/总水头值"
) )
async def fastapi_get_reservoir_head( def fastapi_get_reservoir_head(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
reservoir: str = Query(..., description="水库的唯一标识符") reservoir: str = Query(..., description="水库的唯一标识符")
) -> float | None: ) -> float | None:
@@ -119,7 +119,7 @@ async def fastapi_get_reservoir_head(
summary="获取水库模式", summary="获取水库模式",
description="获取指定水库的运行模式/供水模式" description="获取指定水库的运行模式/供水模式"
) )
async def fastapi_get_reservoir_pattern( def fastapi_get_reservoir_pattern(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
reservoir: str = Query(..., description="水库的唯一标识符") reservoir: str = Query(..., description="水库的唯一标识符")
) -> str | None: ) -> str | None:
@@ -143,7 +143,7 @@ async def fastapi_get_reservoir_pattern(
summary="获取水库X坐标", summary="获取水库X坐标",
description="获取指定水库的X坐标位置" description="获取指定水库的X坐标位置"
) )
async def fastapi_get_reservoir_x( def fastapi_get_reservoir_x(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
reservoir: str = Query(..., description="水库的唯一标识符") reservoir: str = Query(..., description="水库的唯一标识符")
) -> dict[str, float] | None: ) -> dict[str, float] | None:
@@ -167,7 +167,7 @@ async def fastapi_get_reservoir_x(
summary="获取水库Y坐标", summary="获取水库Y坐标",
description="获取指定水库的Y坐标位置" description="获取指定水库的Y坐标位置"
) )
async def fastapi_get_reservoir_y( def fastapi_get_reservoir_y(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
reservoir: str = Query(..., description="水库的唯一标识符") reservoir: str = Query(..., description="水库的唯一标识符")
) -> dict[str, float] | None: ) -> dict[str, float] | None:
@@ -191,7 +191,7 @@ async def fastapi_get_reservoir_y(
summary="获取水库坐标", summary="获取水库坐标",
description="获取指定水库的平面坐标(X和Y坐标)" description="获取指定水库的平面坐标(X和Y坐标)"
) )
async def fastapi_get_reservoir_coord( def fastapi_get_reservoir_coord(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
reservoir: str = Query(..., description="水库的唯一标识符") reservoir: str = Query(..., description="水库的唯一标识符")
) -> dict[str, float] | None: ) -> dict[str, float] | None:
@@ -217,7 +217,7 @@ async def fastapi_get_reservoir_coord(
summary="设置水库水头", summary="设置水库水头",
description="更新指定水库的供水水头/总水头值" description="更新指定水库的供水水头/总水头值"
) )
async def fastapi_set_reservoir_head( def fastapi_set_reservoir_head(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
reservoir: str = Query(..., description="水库的唯一标识符"), reservoir: str = Query(..., description="水库的唯一标识符"),
head: float = Query(..., description="新的水头值(米)") head: float = Query(..., description="新的水头值(米)")
@@ -244,7 +244,7 @@ async def fastapi_set_reservoir_head(
summary="设置水库模式", summary="设置水库模式",
description="更新指定水库的运行模式/供水模式" description="更新指定水库的运行模式/供水模式"
) )
async def fastapi_set_reservoir_pattern( def fastapi_set_reservoir_pattern(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
reservoir: str = Query(..., description="水库的唯一标识符"), reservoir: str = Query(..., description="水库的唯一标识符"),
pattern: str = Query(..., description="新的运行模式") pattern: str = Query(..., description="新的运行模式")
@@ -271,7 +271,7 @@ async def fastapi_set_reservoir_pattern(
summary="设置水库X坐标", summary="设置水库X坐标",
description="更新指定水库的X坐标位置" description="更新指定水库的X坐标位置"
) )
async def fastapi_set_reservoir_x( def fastapi_set_reservoir_x(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
reservoir: str = Query(..., description="水库的唯一标识符"), reservoir: str = Query(..., description="水库的唯一标识符"),
x: float = Query(..., description="新的X坐标值") x: float = Query(..., description="新的X坐标值")
@@ -298,7 +298,7 @@ async def fastapi_set_reservoir_x(
summary="设置水库Y坐标", summary="设置水库Y坐标",
description="更新指定水库的Y坐标位置" description="更新指定水库的Y坐标位置"
) )
async def fastapi_set_reservoir_y( def fastapi_set_reservoir_y(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
reservoir: str = Query(..., description="水库的唯一标识符"), reservoir: str = Query(..., description="水库的唯一标识符"),
y: float = Query(..., description="新的Y坐标值") y: float = Query(..., description="新的Y坐标值")
@@ -325,7 +325,7 @@ async def fastapi_set_reservoir_y(
summary="设置水库坐标", summary="设置水库坐标",
description="更新指定水库的平面坐标(X和Y坐标)" description="更新指定水库的平面坐标(X和Y坐标)"
) )
async def fastapi_set_reservoir_coord( def fastapi_set_reservoir_coord(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
reservoir: str = Query(..., description="水库的唯一标识符"), reservoir: str = Query(..., description="水库的唯一标识符"),
x: float = Query(..., description="新的X坐标值"), x: float = Query(..., description="新的X坐标值"),
@@ -353,7 +353,7 @@ async def fastapi_set_reservoir_coord(
summary="获取水库属性", summary="获取水库属性",
description="获取指定水库的所有属性" description="获取指定水库的所有属性"
) )
async def fastapi_get_reservoir_properties( def fastapi_get_reservoir_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
reservoir: str = Query(..., description="水库的唯一标识符") reservoir: str = Query(..., description="水库的唯一标识符")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -376,7 +376,7 @@ async def fastapi_get_reservoir_properties(
summary="获取所有水库属性", summary="获取所有水库属性",
description="获取指定供水网络中所有水库的属性" description="获取指定供水网络中所有水库的属性"
) )
async def fastapi_get_all_reservoir_properties( def fastapi_get_all_reservoir_properties(
network: str = Query(..., description="管网名称(或数据库名称)") network: str = Query(..., description="管网名称(或数据库名称)")
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
""" """
@@ -399,10 +399,10 @@ async def fastapi_get_all_reservoir_properties(
summary="设置水库属性", summary="设置水库属性",
description="批量更新指定水库的多个属性" description="批量更新指定水库的多个属性"
) )
async def fastapi_set_reservoir_properties( def fastapi_set_reservoir_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
reservoir: str = Query(..., description="水库的唯一标识符"), reservoir: str = Query(..., description="水库的唯一标识符"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
""" """
设置水库的多个属性。 设置水库的多个属性。
@@ -417,6 +417,6 @@ async def fastapi_set_reservoir_properties(
Returns: Returns:
包含操作变更集的ChangeSet对象 包含操作变更集的ChangeSet对象
""" """
props = await req.json() props = payload
ps = {"id": reservoir} | props ps = {"id": reservoir} | props
return set_reservoir(network, ChangeSet(ps)) return set_reservoir(network, ChangeSet(ps))
+9 -9
View File
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Request, Query, Path, Body from typing import Any
from typing import Any, List, Dict, Union
from fastapi import APIRouter, Body, Query
from app.services.tjnetwork import ( from app.services.tjnetwork import (
Any,
ChangeSet, ChangeSet,
get_tag, get_tag,
get_tag_schema, get_tag_schema,
@@ -20,7 +20,7 @@ router = APIRouter()
summary="获取标签属性架构", summary="获取标签属性架构",
description="获取指定水网的标签(Tag)属性架构定义" description="获取指定水网的标签(Tag)属性架构定义"
) )
async def fastapi_get_tag_schema( def fastapi_get_tag_schema(
network: str = Query(..., description="管网名称(或数据库名称)") network: str = Query(..., description="管网名称(或数据库名称)")
) -> dict[str, dict[str, Any]]: ) -> dict[str, dict[str, Any]]:
"""获取标签的属性架构。""" """获取标签的属性架构。"""
@@ -31,7 +31,7 @@ async def fastapi_get_tag_schema(
summary="获取标签信息", summary="获取标签信息",
description="获取指定类型和ID的标签信息" description="获取指定类型和ID的标签信息"
) )
async def fastapi_get_tag( def fastapi_get_tag(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
t_type: str = Query(..., description="标签类型"), t_type: str = Query(..., description="标签类型"),
id: str = Query(..., description="元素ID") id: str = Query(..., description="元素ID")
@@ -44,7 +44,7 @@ async def fastapi_get_tag(
summary="获取所有标签", summary="获取所有标签",
description="获取指定水网中的所有标签信息" description="获取指定水网中的所有标签信息"
) )
async def fastapi_get_tags( def fastapi_get_tags(
network: str = Query(..., description="管网名称(或数据库名称)") network: str = Query(..., description="管网名称(或数据库名称)")
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
"""获取水网中所有标签的列表。""" """获取水网中所有标签的列表。"""
@@ -57,10 +57,10 @@ async def fastapi_get_tags(
summary="设置标签", summary="设置标签",
description="为指定元素设置或修改标签信息" description="为指定元素设置或修改标签信息"
) )
async def fastapi_set_tag( def fastapi_set_tag(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
"""设置标签信息。""" """设置标签信息。"""
props = await req.json() props = payload
return set_tag(network, ChangeSet(props)) return set_tag(network, ChangeSet(props))
+33 -33
View File
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Request, Query, Path, Body from typing import Any
from typing import Any, List, Dict, Union
from fastapi import APIRouter, Body, Query
from app.services.tjnetwork import ( from app.services.tjnetwork import (
Any,
ChangeSet, ChangeSet,
add_tank, add_tank,
delete_tank, delete_tank,
@@ -14,7 +14,7 @@ from app.services.tjnetwork import (
router = APIRouter() router = APIRouter()
@router.get("/network-schemas/tank", summary="获取水箱模式", description="获取指定网络的水箱数据结构模式定义") @router.get("/network-schemas/tank", summary="获取水箱模式", description="获取指定网络的水箱数据结构模式定义")
async def fast_get_tank_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]: def fast_get_tank_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
""" """
获取水箱的数据结构模式。 获取水箱的数据结构模式。
@@ -27,7 +27,7 @@ async def fast_get_tank_schema(network: str = Query(..., description="管网名
return get_tank_schema(network) return get_tank_schema(network)
@router.post("/tanks", summary="新增水箱", description="向指定网络中新增一个水箱", response_model=None) @router.post("/tanks", summary="新增水箱", description="向指定网络中新增一个水箱", response_model=None)
async def fastapi_add_tank( def fastapi_add_tank(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID"), tank: str = Query(..., description="水箱ID"),
x: float = Query(..., description="X坐标"), x: float = Query(..., description="X坐标"),
@@ -71,7 +71,7 @@ async def fastapi_add_tank(
return add_tank(network, ChangeSet(ps)) return add_tank(network, ChangeSet(ps))
@router.delete("/tanks", summary="删除水箱", description="删除指定网络中的水箱", response_model=None) @router.delete("/tanks", summary="删除水箱", description="删除指定网络中的水箱", response_model=None)
async def fastapi_delete_tank( def fastapi_delete_tank(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID") tank: str = Query(..., description="水箱ID")
) -> ChangeSet: ) -> ChangeSet:
@@ -89,7 +89,7 @@ async def fastapi_delete_tank(
return delete_tank(network, ChangeSet(ps)) return delete_tank(network, ChangeSet(ps))
@router.get("/tanks/elevation", summary="获取水箱标高", description="获取指定水箱的标高值") @router.get("/tanks/elevation", summary="获取水箱标高", description="获取指定水箱的标高值")
async def fastapi_get_tank_elevation( def fastapi_get_tank_elevation(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID") tank: str = Query(..., description="水箱ID")
) -> float | None: ) -> float | None:
@@ -107,7 +107,7 @@ async def fastapi_get_tank_elevation(
return ps["elevation"] return ps["elevation"]
@router.get("/tanks/init-level", summary="获取水箱初始水位", description="获取指定水箱的初始水位值") @router.get("/tanks/init-level", summary="获取水箱初始水位", description="获取指定水箱的初始水位值")
async def fastapi_get_tank_init_level( def fastapi_get_tank_init_level(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID") tank: str = Query(..., description="水箱ID")
) -> float | None: ) -> float | None:
@@ -125,7 +125,7 @@ async def fastapi_get_tank_init_level(
return ps["init_level"] return ps["init_level"]
@router.get("/tanks/min-level", summary="获取水箱最小水位", description="获取指定水箱的最小水位值") @router.get("/tanks/min-level", summary="获取水箱最小水位", description="获取指定水箱的最小水位值")
async def fastapi_get_tank_min_level( def fastapi_get_tank_min_level(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID") tank: str = Query(..., description="水箱ID")
) -> float | None: ) -> float | None:
@@ -143,7 +143,7 @@ async def fastapi_get_tank_min_level(
return ps["min_level"] return ps["min_level"]
@router.get("/tanks/max-level", summary="获取水箱最大水位", description="获取指定水箱的最大水位值") @router.get("/tanks/max-level", summary="获取水箱最大水位", description="获取指定水箱的最大水位值")
async def fastapi_get_tank_max_level( def fastapi_get_tank_max_level(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID") tank: str = Query(..., description="水箱ID")
) -> float | None: ) -> float | None:
@@ -161,7 +161,7 @@ async def fastapi_get_tank_max_level(
return ps["max_level"] return ps["max_level"]
@router.get("/tanks/diameter", summary="获取水箱直径", description="获取指定水箱的直径值") @router.get("/tanks/diameter", summary="获取水箱直径", description="获取指定水箱的直径值")
async def fastapi_get_tank_diameter( def fastapi_get_tank_diameter(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID") tank: str = Query(..., description="水箱ID")
) -> float | None: ) -> float | None:
@@ -179,7 +179,7 @@ async def fastapi_get_tank_diameter(
return ps["diameter"] return ps["diameter"]
@router.get("/tanks/min-vol", summary="获取水箱最小体积", description="获取指定水箱的最小体积值") @router.get("/tanks/min-vol", summary="获取水箱最小体积", description="获取指定水箱的最小体积值")
async def fastapi_get_tank_min_vol( def fastapi_get_tank_min_vol(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID") tank: str = Query(..., description="水箱ID")
) -> float | None: ) -> float | None:
@@ -197,7 +197,7 @@ async def fastapi_get_tank_min_vol(
return ps["min_vol"] return ps["min_vol"]
@router.get("/tanks/vol-curve", summary="获取水箱容积曲线", description="获取指定水箱的容积曲线标识") @router.get("/tanks/vol-curve", summary="获取水箱容积曲线", description="获取指定水箱的容积曲线标识")
async def fastapi_get_tank_vol_curve( def fastapi_get_tank_vol_curve(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID") tank: str = Query(..., description="水箱ID")
) -> str | None: ) -> str | None:
@@ -215,7 +215,7 @@ async def fastapi_get_tank_vol_curve(
return ps["vol_curve"] return ps["vol_curve"]
@router.get("/tanks/overflow", summary="获取水箱溢流口", description="获取指定水箱的溢流口配置") @router.get("/tanks/overflow", summary="获取水箱溢流口", description="获取指定水箱的溢流口配置")
async def fastapi_get_tank_overflow( def fastapi_get_tank_overflow(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID") tank: str = Query(..., description="水箱ID")
) -> str | None: ) -> str | None:
@@ -233,7 +233,7 @@ async def fastapi_get_tank_overflow(
return ps["overflow"] return ps["overflow"]
@router.get("/tanks/x", summary="获取水箱X坐标", description="获取指定水箱的X坐标值") @router.get("/tanks/x", summary="获取水箱X坐标", description="获取指定水箱的X坐标值")
async def fastapi_get_tank_x( def fastapi_get_tank_x(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID") tank: str = Query(..., description="水箱ID")
) -> float: ) -> float:
@@ -251,7 +251,7 @@ async def fastapi_get_tank_x(
return ps["x"] return ps["x"]
@router.get("/tanks/y", summary="获取水箱Y坐标", description="获取指定水箱的Y坐标值") @router.get("/tanks/y", summary="获取水箱Y坐标", description="获取指定水箱的Y坐标值")
async def fastapi_get_tank_y( def fastapi_get_tank_y(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID") tank: str = Query(..., description="水箱ID")
) -> float: ) -> float:
@@ -269,7 +269,7 @@ async def fastapi_get_tank_y(
return ps["y"] return ps["y"]
@router.get("/tanks/coord", summary="获取水箱坐标", description="获取指定水箱的X和Y坐标") @router.get("/tanks/coord", summary="获取水箱坐标", description="获取指定水箱的X和Y坐标")
async def fastapi_get_tank_coord( def fastapi_get_tank_coord(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID") tank: str = Query(..., description="水箱ID")
) -> dict[str, float]: ) -> dict[str, float]:
@@ -288,7 +288,7 @@ async def fastapi_get_tank_coord(
return coord return coord
@router.patch("/tanks/elevation", summary="设置水箱标高", description="设置指定水箱的标高值", response_model=None) @router.patch("/tanks/elevation", summary="设置水箱标高", description="设置指定水箱的标高值", response_model=None)
async def fastapi_set_tank_elevation( def fastapi_set_tank_elevation(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID"), tank: str = Query(..., description="水箱ID"),
elevation: float = Query(..., description="新的标高值") elevation: float = Query(..., description="新的标高值")
@@ -308,7 +308,7 @@ async def fastapi_set_tank_elevation(
return set_tank(network, ChangeSet(ps)) return set_tank(network, ChangeSet(ps))
@router.patch("/tanks/init-level", summary="设置水箱初始水位", description="设置指定水箱的初始水位值", response_model=None) @router.patch("/tanks/init-level", summary="设置水箱初始水位", description="设置指定水箱的初始水位值", response_model=None)
async def fastapi_set_tank_init_level( def fastapi_set_tank_init_level(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID"), tank: str = Query(..., description="水箱ID"),
init_level: float = Query(..., description="新的初始水位值") init_level: float = Query(..., description="新的初始水位值")
@@ -328,7 +328,7 @@ async def fastapi_set_tank_init_level(
return set_tank(network, ChangeSet(ps)) return set_tank(network, ChangeSet(ps))
@router.patch("/tanks/min-level", summary="设置水箱最小水位", description="设置指定水箱的最小水位值", response_model=None) @router.patch("/tanks/min-level", summary="设置水箱最小水位", description="设置指定水箱的最小水位值", response_model=None)
async def fastapi_set_tank_min_level( def fastapi_set_tank_min_level(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID"), tank: str = Query(..., description="水箱ID"),
min_level: float = Query(..., description="新的最小水位值") min_level: float = Query(..., description="新的最小水位值")
@@ -348,7 +348,7 @@ async def fastapi_set_tank_min_level(
return set_tank(network, ChangeSet(ps)) return set_tank(network, ChangeSet(ps))
@router.patch("/tanks/max-level", summary="设置水箱最大水位", description="设置指定水箱的最大水位值", response_model=None) @router.patch("/tanks/max-level", summary="设置水箱最大水位", description="设置指定水箱的最大水位值", response_model=None)
async def fastapi_set_tank_max_level( def fastapi_set_tank_max_level(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID"), tank: str = Query(..., description="水箱ID"),
max_level: float = Query(..., description="新的最大水位值") max_level: float = Query(..., description="新的最大水位值")
@@ -368,7 +368,7 @@ async def fastapi_set_tank_max_level(
return set_tank(network, ChangeSet(ps)) return set_tank(network, ChangeSet(ps))
@router.patch("/tanks/diameter", summary="设置水箱直径", description="设置指定水箱的直径值", response_model=None) @router.patch("/tanks/diameter", summary="设置水箱直径", description="设置指定水箱的直径值", response_model=None)
async def fastapi_set_tank_diameter( def fastapi_set_tank_diameter(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID"), tank: str = Query(..., description="水箱ID"),
diameter: float = Query(..., description="新的直径值") diameter: float = Query(..., description="新的直径值")
@@ -388,7 +388,7 @@ async def fastapi_set_tank_diameter(
return set_tank(network, ChangeSet(ps)) return set_tank(network, ChangeSet(ps))
@router.patch("/tanks/min-vol", summary="设置水箱最小体积", description="设置指定水箱的最小体积值", response_model=None) @router.patch("/tanks/min-vol", summary="设置水箱最小体积", description="设置指定水箱的最小体积值", response_model=None)
async def fastapi_set_tank_min_vol( def fastapi_set_tank_min_vol(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID"), tank: str = Query(..., description="水箱ID"),
min_vol: float = Query(..., description="新的最小体积值") min_vol: float = Query(..., description="新的最小体积值")
@@ -408,7 +408,7 @@ async def fastapi_set_tank_min_vol(
return set_tank(network, ChangeSet(ps)) return set_tank(network, ChangeSet(ps))
@router.patch("/tanks/vol-curve", summary="设置水箱容积曲线", description="设置指定水箱的容积曲线标识", response_model=None) @router.patch("/tanks/vol-curve", summary="设置水箱容积曲线", description="设置指定水箱的容积曲线标识", response_model=None)
async def fastapi_set_tank_vol_curve( def fastapi_set_tank_vol_curve(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID"), tank: str = Query(..., description="水箱ID"),
vol_curve: str = Query(..., description="新的容积曲线标识") vol_curve: str = Query(..., description="新的容积曲线标识")
@@ -428,7 +428,7 @@ async def fastapi_set_tank_vol_curve(
return set_tank(network, ChangeSet(ps)) return set_tank(network, ChangeSet(ps))
@router.patch("/tanks/overflow", summary="设置水箱溢流口", description="设置指定水箱的溢流口配置", response_model=None) @router.patch("/tanks/overflow", summary="设置水箱溢流口", description="设置指定水箱的溢流口配置", response_model=None)
async def fastapi_set_tank_overflow( def fastapi_set_tank_overflow(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID"), tank: str = Query(..., description="水箱ID"),
overflow: str = Query(..., description="新的溢流口配置") overflow: str = Query(..., description="新的溢流口配置")
@@ -448,7 +448,7 @@ async def fastapi_set_tank_overflow(
return set_tank(network, ChangeSet(ps)) return set_tank(network, ChangeSet(ps))
@router.patch("/tanks/x", summary="设置水箱X坐标", description="设置指定水箱的X坐标值", response_model=None) @router.patch("/tanks/x", summary="设置水箱X坐标", description="设置指定水箱的X坐标值", response_model=None)
async def fastapi_set_tank_x( def fastapi_set_tank_x(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID"), tank: str = Query(..., description="水箱ID"),
x: float = Query(..., description="新的X坐标值") x: float = Query(..., description="新的X坐标值")
@@ -468,7 +468,7 @@ async def fastapi_set_tank_x(
return set_tank(network, ChangeSet(ps)) return set_tank(network, ChangeSet(ps))
@router.patch("/tanks/y", summary="设置水箱Y坐标", description="设置指定水箱的Y坐标值", response_model=None) @router.patch("/tanks/y", summary="设置水箱Y坐标", description="设置指定水箱的Y坐标值", response_model=None)
async def fastapi_set_tank_y( def fastapi_set_tank_y(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID"), tank: str = Query(..., description="水箱ID"),
y: float = Query(..., description="新的Y坐标值") y: float = Query(..., description="新的Y坐标值")
@@ -488,7 +488,7 @@ async def fastapi_set_tank_y(
return set_tank(network, ChangeSet(ps)) return set_tank(network, ChangeSet(ps))
@router.patch("/tanks/coord", summary="设置水箱坐标", description="设置指定水箱的X和Y坐标", response_model=None) @router.patch("/tanks/coord", summary="设置水箱坐标", description="设置指定水箱的X和Y坐标", response_model=None)
async def fastapi_set_tank_coord( def fastapi_set_tank_coord(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID"), tank: str = Query(..., description="水箱ID"),
x: float = Query(..., description="新的X坐标值"), x: float = Query(..., description="新的X坐标值"),
@@ -510,7 +510,7 @@ async def fastapi_set_tank_coord(
return set_tank(network, ChangeSet(ps)) return set_tank(network, ChangeSet(ps))
@router.get("/tanks/properties", summary="获取水箱属性", description="获取指定水箱的所有属性") @router.get("/tanks/properties", summary="获取水箱属性", description="获取指定水箱的所有属性")
async def fastapi_get_tank_properties( def fastapi_get_tank_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID") tank: str = Query(..., description="水箱ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -527,7 +527,7 @@ async def fastapi_get_tank_properties(
return get_tank(network, tank) return get_tank(network, tank)
@router.get("/tanks", summary="获取所有水箱属性", description="获取指定网络中所有水箱的属性") @router.get("/tanks", summary="获取所有水箱属性", description="获取指定网络中所有水箱的属性")
async def fastapi_get_all_tank_properties( def fastapi_get_all_tank_properties(
network: str = Query(..., description="管网名称(或数据库名称)") network: str = Query(..., description="管网名称(或数据库名称)")
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
""" """
@@ -544,10 +544,10 @@ async def fastapi_get_all_tank_properties(
return results return results
@router.patch("/tanks/properties", summary="设置水箱属性", description="批量设置指定水箱的多个属性", response_model=None) @router.patch("/tanks/properties", summary="设置水箱属性", description="批量设置指定水箱的多个属性", response_model=None)
async def fastapi_set_tank_properties( def fastapi_set_tank_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
tank: str = Query(..., description="水箱ID"), tank: str = Query(..., description="水箱ID"),
req: Request = None payload: dict[str, Any] = Body(...)
) -> ChangeSet: ) -> ChangeSet:
""" """
批量设置水箱的属性。 批量设置水箱的属性。
@@ -560,6 +560,6 @@ async def fastapi_set_tank_properties(
Returns: Returns:
包含变更信息的ChangeSet对象 包含变更信息的ChangeSet对象
""" """
props = await req.json() props = payload
ps = {"id": tank} | props ps = {"id": tank} | props
return set_tank(network, ChangeSet(ps)) return set_tank(network, ChangeSet(ps))
+22 -22
View File
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Request, Query, Path, Body from typing import Any
from typing import Any, List, Dict, Union
from fastapi import APIRouter, Body, Query
from app.services.tjnetwork import ( from app.services.tjnetwork import (
Any,
ChangeSet, ChangeSet,
VALVES_TYPE_PRV, VALVES_TYPE_PRV,
add_valve, add_valve,
@@ -19,7 +19,7 @@ router = APIRouter()
summary="获取阀门架构", summary="获取阀门架构",
description="获取指定水网中所有阀门的架构和字段定义", description="获取指定水网中所有阀门的架构和字段定义",
) )
async def fastapi_get_valve_schema( def fastapi_get_valve_schema(
network: str = Query(..., description="管网名称(或数据库名称)") network: str = Query(..., description="管网名称(或数据库名称)")
) -> dict[str, dict[str, Any]]: ) -> dict[str, dict[str, Any]]:
""" """
@@ -35,7 +35,7 @@ async def fastapi_get_valve_schema(
summary="添加阀门", summary="添加阀门",
description="在指定的水网中添加新的阀门", description="在指定的水网中添加新的阀门",
) )
async def fastapi_add_valve( def fastapi_add_valve(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
valve: str = Query(..., description="阀门ID"), valve: str = Query(..., description="阀门ID"),
node1: str = Query(..., description="起点节点ID"), node1: str = Query(..., description="起点节点ID"),
@@ -68,7 +68,7 @@ async def fastapi_add_valve(
summary="删除阀门", summary="删除阀门",
description="从指定的水网中删除指定的阀门", description="从指定的水网中删除指定的阀门",
) )
async def fastapi_delete_valve( def fastapi_delete_valve(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
valve: str = Query(..., description="阀门ID"), valve: str = Query(..., description="阀门ID"),
) -> ChangeSet: ) -> ChangeSet:
@@ -85,7 +85,7 @@ async def fastapi_delete_valve(
summary="获取阀门起点节点", summary="获取阀门起点节点",
description="获取指定阀门连接的起点节点ID", description="获取指定阀门连接的起点节点ID",
) )
async def fastapi_get_valve_node1( def fastapi_get_valve_node1(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
valve: str = Query(..., description="阀门ID"), valve: str = Query(..., description="阀门ID"),
) -> str | None: ) -> str | None:
@@ -102,7 +102,7 @@ async def fastapi_get_valve_node1(
summary="获取阀门终点节点", summary="获取阀门终点节点",
description="获取指定阀门连接的终点节点ID", description="获取指定阀门连接的终点节点ID",
) )
async def fastapi_get_valve_node2( def fastapi_get_valve_node2(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
valve: str = Query(..., description="阀门ID"), valve: str = Query(..., description="阀门ID"),
) -> str | None: ) -> str | None:
@@ -119,7 +119,7 @@ async def fastapi_get_valve_node2(
summary="获取阀门直径", summary="获取阀门直径",
description="获取指定阀门的直径", description="获取指定阀门的直径",
) )
async def fastapi_get_valve_diameter( def fastapi_get_valve_diameter(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
valve: str = Query(..., description="阀门ID"), valve: str = Query(..., description="阀门ID"),
) -> float | None: ) -> float | None:
@@ -136,7 +136,7 @@ async def fastapi_get_valve_diameter(
summary="获取阀门类型", summary="获取阀门类型",
description="获取指定阀门的类型", description="获取指定阀门的类型",
) )
async def fastapi_get_valve_type( def fastapi_get_valve_type(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
valve: str = Query(..., description="阀门ID"), valve: str = Query(..., description="阀门ID"),
) -> str | None: ) -> str | None:
@@ -153,7 +153,7 @@ async def fastapi_get_valve_type(
summary="获取阀门开度", summary="获取阀门开度",
description="获取指定阀门的开度/设置值", description="获取指定阀门的开度/设置值",
) )
async def fastapi_get_valve_setting( def fastapi_get_valve_setting(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
valve: str = Query(..., description="阀门ID"), valve: str = Query(..., description="阀门ID"),
) -> float | None: ) -> float | None:
@@ -170,7 +170,7 @@ async def fastapi_get_valve_setting(
summary="获取阀门损失系数", summary="获取阀门损失系数",
description="获取指定阀门的损失系数", description="获取指定阀门的损失系数",
) )
async def fastapi_get_valve_minor_loss( def fastapi_get_valve_minor_loss(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
valve: str = Query(..., description="阀门ID"), valve: str = Query(..., description="阀门ID"),
) -> float | None: ) -> float | None:
@@ -188,7 +188,7 @@ async def fastapi_get_valve_minor_loss(
summary="设置阀门起点节点", summary="设置阀门起点节点",
description="设置指定阀门的起点节点", description="设置指定阀门的起点节点",
) )
async def fastapi_set_valve_node1( def fastapi_set_valve_node1(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
valve: str = Query(..., description="阀门ID"), valve: str = Query(..., description="阀门ID"),
node1: str = Query(..., description="新的起点节点ID"), node1: str = Query(..., description="新的起点节点ID"),
@@ -207,7 +207,7 @@ async def fastapi_set_valve_node1(
summary="设置阀门终点节点", summary="设置阀门终点节点",
description="设置指定阀门的终点节点", description="设置指定阀门的终点节点",
) )
async def fastapi_set_valve_node2( def fastapi_set_valve_node2(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
valve: str = Query(..., description="阀门ID"), valve: str = Query(..., description="阀门ID"),
node2: str = Query(..., description="新的终点节点ID"), node2: str = Query(..., description="新的终点节点ID"),
@@ -226,7 +226,7 @@ async def fastapi_set_valve_node2(
summary="设置阀门直径", summary="设置阀门直径",
description="设置指定阀门的直径", description="设置指定阀门的直径",
) )
async def fastapi_set_valve_diameter( def fastapi_set_valve_diameter(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
valve: str = Query(..., description="阀门ID"), valve: str = Query(..., description="阀门ID"),
diameter: float = Query(..., description="新的直径值(mm"), diameter: float = Query(..., description="新的直径值(mm"),
@@ -245,7 +245,7 @@ async def fastapi_set_valve_diameter(
summary="设置阀门类型", summary="设置阀门类型",
description="设置指定阀门的类型", description="设置指定阀门的类型",
) )
async def fastapi_set_valve_type( def fastapi_set_valve_type(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
valve: str = Query(..., description="阀门ID"), valve: str = Query(..., description="阀门ID"),
type: str = Query(..., description="新的阀门类型"), type: str = Query(..., description="新的阀门类型"),
@@ -264,7 +264,7 @@ async def fastapi_set_valve_type(
summary="设置阀门开度", summary="设置阀门开度",
description="设置指定阀门的开度/设置值", description="设置指定阀门的开度/设置值",
) )
async def fastapi_set_valve_setting( def fastapi_set_valve_setting(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
valve: str = Query(..., description="阀门ID"), valve: str = Query(..., description="阀门ID"),
setting: float = Query(..., description="新的开度值"), setting: float = Query(..., description="新的开度值"),
@@ -282,7 +282,7 @@ async def fastapi_set_valve_setting(
summary="获取阀门所有属性", summary="获取阀门所有属性",
description="获取指定阀门的所有属性", description="获取指定阀门的所有属性",
) )
async def fastapi_get_valve_properties( def fastapi_get_valve_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
valve: str = Query(..., description="阀门ID"), valve: str = Query(..., description="阀门ID"),
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -298,7 +298,7 @@ async def fastapi_get_valve_properties(
summary="获取所有阀门属性", summary="获取所有阀门属性",
description="获取指定水网中所有阀门的属性", description="获取指定水网中所有阀门的属性",
) )
async def fastapi_get_all_valve_properties( def fastapi_get_all_valve_properties(
network: str = Query(..., description="管网名称(或数据库名称)") network: str = Query(..., description="管网名称(或数据库名称)")
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
""" """
@@ -316,16 +316,16 @@ async def fastapi_get_all_valve_properties(
summary="批量设置阀门属性", summary="批量设置阀门属性",
description="批量设置指定阀门的多个属性", description="批量设置指定阀门的多个属性",
) )
async def fastapi_set_valve_properties( def fastapi_set_valve_properties(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
valve: str = Query(..., description="阀门ID"), valve: str = Query(..., description="阀门ID"),
req: Request = None, payload: dict[str, Any] = Body(...),
) -> ChangeSet: ) -> ChangeSet:
""" """
批量设置阀门的属性 批量设置阀门的属性
更新指定阀门的一个或多个属性通过JSON请求体传递要更新的属性 更新指定阀门的一个或多个属性通过JSON请求体传递要更新的属性
""" """
props = await req.json() props = payload
ps = {"id": valve} | props ps = {"id": valve} | props
return set_valve(network, ChangeSet(ps)) return set_valve(network, ChangeSet(ps))
+6 -464
View File
@@ -1,49 +1,18 @@
import json import json
from fastapi import APIRouter, Request, HTTPException, Query, Path, Depends from fastapi import APIRouter, HTTPException, Query, Depends
from fastapi.responses import PlainTextResponse
from typing import Any, Dict, List
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
from app.auth.project_dependencies import get_metadata_repository from app.auth.project_dependencies import (
from app.auth.permissions import ( get_metadata_repository,
ENVIRONMENT_MANAGE,
require_permission,
) )
from app.domain.schemas.metadata import ProjectMetaResponse from app.domain.schemas.metadata import ProjectMetaResponse
import app.services.project_info as project_info
from app.infra.db.postgresql.database import get_database_instance as get_pg_db
from app.infra.db.timescaledb.database import get_database_instance as get_ts_db
from app.services.tjnetwork import ( from app.services.tjnetwork import (
ChangeSet, ChangeSet,
list_project,
have_project,
create_project,
delete_project,
is_project_open,
open_project,
close_project,
copy_project,
export_inp, export_inp,
read_inp,
dump_inp,
get_all_vertices, get_all_vertices,
get_all_scada_elements, get_all_scada_info,
get_all_district_metering_areas,
get_all_service_areas,
get_all_virtual_districts,
get_extension_data,
convert_inp_v3_to_v2,
) )
# For inp file upload/download
import os
from fastapi import Response, status
from fastapi.responses import FileResponse
inpDir = "data/" # Assuming data directory exists or is defined somewhere.
# In main.py it was likely global. For safety, let's use a relative path or get from config.
# But let's stick to what main.py probably used or a default.
router = APIRouter() router = APIRouter()
lockedPrjs: Dict[str, str] = {}
@router.get("/projects/current", summary="获取项目信息", description="从数据库获取项目的详细信息,包括地图范围等。", response_model=ProjectMetaResponse) @router.get("/projects/current", summary="获取项目信息", description="从数据库获取项目的详细信息,包括地图范围等。", response_model=ProjectMetaResponse)
async def get_project_info_endpoint( async def get_project_info_endpoint(
@@ -69,126 +38,8 @@ async def get_project_info_endpoint(
project_role="viewer", # Default role for public access project_role="viewer", # Default role for public access
) )
@router.get("/project-codes", summary="获取项目列表", description="获取服务器上所有可用的供水管网项目名称列表。")
async def list_projects_endpoint() -> list[str]:
"""
获取项目列表
返回所有已创建项目的名称列表
"""
return list_project()
@router.get("/projects/existence", summary="检查项目是否存在", description="检查指定名称的项目是否存在。")
async def have_project_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)")
):
"""
检查项目是否存在
- **network**: 管网名称或数据库名称
"""
return have_project(network)
@router.post("/projects", summary="创建新项目", description="创建一个新的供水管网项目。如果项目已存在,可能会覆盖或报错(取决于底层实现)。")
async def create_project_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
_=Depends(require_permission(ENVIRONMENT_MANAGE)),
):
"""
创建新项目
- **network**: 管网名称或数据库名称
"""
create_project(network)
return network
@router.delete("/projects", summary="删除项目", description="永久删除指定的供水管网项目。此操作不可恢复。")
async def delete_project_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
_=Depends(require_permission(ENVIRONMENT_MANAGE)),
):
"""
删除项目
- **network**: 管网名称或数据库名称
"""
delete_project(network)
return True
@router.get("/projects/current/status", summary="检查项目是否已打开", description="检查指定项目是否已被加载到内存中。")
async def is_project_open_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)")
):
"""
检查项目是否已打开
- **network**: 管网名称或数据库名称
"""
return is_project_open(network)
@router.post("/projects/current", summary="打开项目", description="将指定项目加载到内存中,并初始化数据库连接池。")
async def open_project_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)")
):
"""
打开项目
- **network**: 管网名称或数据库名称
"""
open_project(network)
# 尝试连接指定数据库
try:
# 初始化 PostgreSQL 连接池
pg_instance = await get_pg_db(network)
async with pg_instance.get_connection() as conn:
async with conn.cursor() as cur:
await cur.execute("SELECT 1")
# 初始化 TimescaleDB 连接池
ts_instance = await get_ts_db(network)
async with ts_instance.get_connection() as conn:
async with conn.cursor() as cur:
await cur.execute("SELECT 1")
except Exception as e:
# 记录错误但不阻断项目打开,或者根据需求决定是否阻断
# 这里选择打印错误,因为 open_project 原本只负责原生部分
print(f"Failed to connect to databases for {network}: {str(e)}")
# 如果数据库连接是必须的,可以抛出异常:
# raise HTTPException(status_code=500, detail=f"Database connection failed: {str(e)}")
return network
@router.delete("/projects/current", summary="关闭项目", description="将指定项目从内存中卸载,释放资源。")
async def close_project_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)")
):
"""
关闭项目
- **network**: 管网名称或数据库名称
"""
close_project(network)
return True
@router.post("/project-copies", summary="复制项目", description="将现有项目复制为新项目。")
async def copy_project_endpoint(
source: str = Query(..., description="管网名称(或数据库名称)"),
target: str = Query(..., description="管网名称(或数据库名称)"),
_=Depends(require_permission(ENVIRONMENT_MANAGE)),
):
"""
复制项目
- **source**: 管网名称或数据库名称
- **target**: 管网名称或数据库名称
"""
copy_project(source, target)
return True
@router.get("/projects/current/exports/change-set", response_model=None, summary="导出项目为 ChangeSet", description="导出项目的变更集 (ChangeSet),包含顶点、SCADA 元素、DMA、SA、VD 等信息。") @router.get("/projects/current/exports/change-set", response_model=None, summary="导出项目为 ChangeSet", description="导出项目的变更集 (ChangeSet),包含顶点、SCADA 元素、DMA、SA、VD 等信息。")
async def export_inp_endpoint( def export_inp_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
version: str = Query(..., description="版本号 (通常用于增量更新)") version: str = Query(..., description="版本号 (通常用于增量更新)")
) -> ChangeSet: ) -> ChangeSet:
@@ -200,316 +51,7 @@ async def export_inp_endpoint(
""" """
cs = export_inp(network, version) cs = export_inp(network, version)
op = cs.operations[0] op = cs.operations[0]
open_project(network)
op["vertex"] = json.dumps(get_all_vertices(network)) op["vertex"] = json.dumps(get_all_vertices(network))
op["scada"] = json.dumps(get_all_scada_elements(network)) op["scada"] = json.dumps(get_all_scada_info(network))
op["dma"] = json.dumps(get_all_district_metering_areas(network))
op["sa"] = json.dumps(get_all_service_areas(network))
op["vd"] = json.dumps(get_all_virtual_districts(network))
op["legend"] = get_extension_data(network, "legend")
db = get_extension_data(network, "scada_db")
print(db)
scada_db = ""
if db:
scada_db = db
print(scada_db)
op["scada_db"] = scada_db
close_project(network)
return cs
@router.post("/projects/current/imports", summary="读取 INP 文件到项目", description="从服务器文件系统中读取指定的 INP 文件并加载到项目中。")
async def read_inp_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
inp: str = Query(..., description="INP 文件名 (不包含路径)")
) -> bool:
"""
读取 INP 文件到项目
- **network**: 管网名称或数据库名称
- **inp**: INP 文件名
"""
read_inp(network, inp)
return True
@router.post("/projects/current/exports/inp", summary="导出项目到 INP 文件", description="将项目当前状态保存为 INP 文件到服务器文件系统。")
async def dump_inp_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
inp: str = Query(..., description="目标文件名")
) -> bool:
"""
导出项目到 INP 文件
- **network**: 管网名称或数据库名称
- **inp**: 目标文件名
"""
dump_inp(network, inp)
return True
@router.get("/projects/current/lock", summary="检查项目是否被锁定", description="检查指定项目是否处于锁定状态。")
async def is_project_locked_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
):
"""
检查项目是否被锁定
- **network**: 管网名称或数据库名称
"""
return network in lockedPrjs.keys()
@router.get("/projects/current/lock/ownership", summary="检查项目是否被当前用户锁定", description="检查指定项目是否被当前访问地址 (IP) 锁定。")
async def is_project_locked_by_me_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
):
"""
检查项目是否被当前用户锁定
- **network**: 管网名称或数据库名称
"""
client_host = req.client.host
return lockedPrjs.get(network) == client_host
# 0 successfully locked
# 1 already locked by you
# 2 locked by others
@router.post("/projects/current/lock", summary="锁定项目", description="锁定指定项目以防止并发修改。")
async def lock_project_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
):
"""
锁定项目
返回值:
- **0**: 锁定成功
- **1**: 已被当前用户锁定
- **2**: 已被其他用户锁定
"""
client_host = req.client.host
if not network in lockedPrjs.keys():
lockedPrjs[network] = client_host
return 0
else:
if lockedPrjs.get(network) == client_host:
return 1
else:
return 2
@router.delete("/projects/current/lock", summary="解锁项目", description="释放对项目的锁定。")
def unlock_project_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
):
"""
解锁项目
只有锁定者才能解锁
"""
client_host = req.client.host
if lockedPrjs.get(network) == client_host:
print("delete key")
del lockedPrjs[network]
return True
return False
@router.get("/projects/current/files/inp", status_code=status.HTTP_200_OK, summary="下载 INP 文件", description="从服务器数据目录下载指定的 INP 文件。")
async def fastapi_download_inp(
name: str = Query(..., description="文件名"),
response: Response = None
):
"""
下载 INP 文件
- **name**: 文件名
"""
filePath = inpDir + name
if os.path.exists(filePath):
return FileResponse(
filePath, media_type="application/octet-stream", filename="inp.inp"
)
else:
response.status_code = status.HTTP_400_BAD_REQUEST
return True
# DingZQ, 2024-12-28, convert v3 to v2
@router.post("/project-conversions", response_model=None, summary="转换 INP V3 为 V2", description="将 EPANET 3.0 格式的 INP 内容转换为 2.x 格式。")
async def fastapi_convert_v3_to_v2(
req: Request
) -> ChangeSet:
"""
转换 INP V3 V2
- **req**: 请求体需包含 `{"inp": "..."}` 结构
"""
network = "v3Tov2"
jo_root = await req.json()
inp = jo_root["inp"]
cs = convert_inp_v3_to_v2(inp)
op = cs.operations[0]
open_project(network)
op["vertex"] = json.dumps(get_all_vertices(network))
op["scada"] = json.dumps(get_all_scada_elements(network))
op["dma"] = json.dumps(get_all_district_metering_areas(network))
op["sa"] = json.dumps(get_all_service_areas(network))
op["vd"] = json.dumps(get_all_virtual_districts(network))
op["legend"] = get_extension_data(network, "legend")
db = get_extension_data(network, "scada_db")
print(db)
scada_db = ""
if db:
scada_db = db
print(scada_db)
op["scada_db"] = scada_db
close_project(network)
return cs
async def read_inp_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
inp: str = Query(..., description="INP 文件名 (不包含路径)")
) -> bool:
"""
读取 INP 文件到项目
- **network**: 管网名称或数据库名称
- **inp**: INP 文件名
"""
read_inp(network, inp)
return True
async def dump_inp_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
inp: str = Query(..., description="目标文件名")
) -> bool:
"""
导出项目到 INP 文件
- **network**: 管网名称或数据库名称
- **inp**: 目标文件名
"""
dump_inp(network, inp)
return True
async def is_project_locked_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
):
"""
检查项目是否被锁定
- **network**: 管网名称或数据库名称
"""
return network in lockedPrjs.keys()
async def is_project_locked_by_me_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
):
"""
检查项目是否被当前用户锁定
- **network**: 管网名称或数据库名称
"""
client_host = req.client.host
return lockedPrjs.get(network) == client_host
# 0 successfully locked
# 1 already locked by you
# 2 locked by others
async def lock_project_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
):
"""
锁定项目
返回值:
- **0**: 锁定成功
- **1**: 已被当前用户锁定
- **2**: 已被其他用户锁定
"""
client_host = req.client.host
if not network in lockedPrjs.keys():
lockedPrjs[network] = client_host
return 0
else:
if lockedPrjs.get(network) == client_host:
return 1
else:
return 2
def unlock_project_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
):
"""
解锁项目
只有锁定者才能解锁
"""
client_host = req.client.host
if lockedPrjs.get(network) == client_host:
print("delete key")
del lockedPrjs[network]
return True
return False
async def fastapi_download_inp(
name: str = Query(..., description="文件名"),
response: Response = None
):
"""
下载 INP 文件
- **name**: 文件名
"""
filePath = inpDir + name
if os.path.exists(filePath):
return FileResponse(
filePath, media_type="application/octet-stream", filename="inp.inp"
)
else:
response.status_code = status.HTTP_400_BAD_REQUEST
return True
# DingZQ, 2024-12-28, convert v3 to v2
async def fastapi_convert_v3_to_v2(
req: Request
) -> ChangeSet:
"""
转换 INP V3 V2
- **req**: 请求体需包含 `{"inp": "..."}` 结构
"""
network = "v3Tov2"
jo_root = await req.json()
inp = jo_root["inp"]
cs = convert_inp_v3_to_v2(inp)
op = cs.operations[0]
open_project(network)
op["vertex"] = json.dumps(get_all_vertices(network))
op["scada"] = json.dumps(get_all_scada_elements(network))
op["dma"] = json.dumps(get_all_district_metering_areas(network))
op["sa"] = json.dumps(get_all_service_areas(network))
op["vd"] = json.dumps(get_all_virtual_districts(network))
op["legend"] = get_extension_data(network, "legend")
db = get_extension_data(network, "scada_db")
print(db)
scada_db = ""
if db:
scada_db = db
print(scada_db)
op["scada_db"] = scada_db
close_project(network)
return cs return cs
+25 -38
View File
@@ -1,8 +1,9 @@
from fastapi import APIRouter, Depends, HTTPException, Path, Query from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query
from psycopg import AsyncConnection from psycopg import AsyncConnection
from app.infra.db.postgresql.scada import ScadaInfoRepository from app.infra.db.postgresql.analysis import AnalysisRepository
from app.infra.db.postgresql.scheme import SchemeRepository
from app.auth.project_dependencies import get_project_pg_connection from app.auth.project_dependencies import get_project_pg_connection
router = APIRouter() router = APIRouter()
@@ -15,26 +16,8 @@ async def get_database_connection(
yield conn yield conn
@router.get("/scada-info/database-view", summary="获取SCADA信息", description="使用连接池查询所有SCADA信息") @router.get("/analysis/runs", summary="获取分析运行列表")
async def get_scada_info_with_connection( async def get_analysis_runs(
conn: AsyncConnection = Depends(get_database_connection),
):
"""
获取所有SCADA信息
返回项目中所有的SCADA设备信息
"""
try:
scada_data = await ScadaInfoRepository.get_scadas(conn)
return {"success": True, "data": scada_data, "count": len(scada_data)}
except Exception as e:
raise HTTPException(
status_code=500, detail=f"查询SCADA信息时发生错误: {str(e)}"
)
@router.get("/schemes/list-with-connection", summary="获取方案列表", description="使用连接池查询所有方案信息")
async def get_scheme_list_with_connection(
conn: AsyncConnection = Depends(get_database_connection), conn: AsyncConnection = Depends(get_database_connection),
): ):
""" """
@@ -43,14 +26,15 @@ async def get_scheme_list_with_connection(
返回项目中所有方案的详细信息 返回项目中所有方案的详细信息
""" """
try: try:
scheme_data = await SchemeRepository.get_schemes(conn) runs = await AnalysisRepository.list_runs(conn)
return {"success": True, "data": scheme_data, "count": len(scheme_data)} return {"success": True, "data": runs, "count": len(runs)}
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"查询方案信息时发生错误: {str(e)}") raise HTTPException(status_code=500, detail=f"查询分析运行时发生错误: {str(e)}")
@router.get("/burst-locations/database-view", summary="获取爆管定位结果", description="使用连接池查询所有爆管定位结果") @router.get("/analysis/runs/{run_id}", summary="获取分析运行")
async def get_burst_locate_result_with_connection( async def get_analysis_run(
run_id: UUID,
conn: AsyncConnection = Depends(get_database_connection), conn: AsyncConnection = Depends(get_database_connection),
): ):
""" """
@@ -59,17 +43,22 @@ async def get_burst_locate_result_with_connection(
返回项目中所有的爆管定位分析结果 返回项目中所有的爆管定位分析结果
""" """
try: try:
burst_data = await SchemeRepository.get_burst_locate_results(conn) run = await AnalysisRepository.get_run(conn, run_id)
return {"success": True, "data": burst_data, "count": len(burst_data)} if run is None:
raise HTTPException(status_code=404, detail="分析运行不存在")
return run
except HTTPException:
raise
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=500, detail=f"查询爆管定位结果时发生错误: {str(e)}" status_code=500, detail=f"查询分析运行时发生错误: {str(e)}"
) )
@router.get("/burst-locations/{burst_incident}", summary="按事件查询爆管定位结果", description="根据爆管事件ID查询对应的爆管定位结果") @router.get("/analysis/runs/{run_id}/results", summary="获取分析结果")
async def get_burst_locate_result_by_incident( async def get_analysis_results(
burst_incident: str = Path(..., description="爆管事件ID"), run_id: UUID,
result_type: str | None = Query(default=None, description="结果类型"),
conn: AsyncConnection = Depends(get_database_connection), conn: AsyncConnection = Depends(get_database_connection),
): ):
""" """
@@ -79,11 +68,9 @@ async def get_burst_locate_result_by_incident(
burst_incident: 爆管事件的唯一标识符 burst_incident: 爆管事件的唯一标识符
""" """
try: try:
return await SchemeRepository.get_burst_locate_result_by_incident( return await AnalysisRepository.list_results(conn, run_id, result_type)
conn, burst_incident
)
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=500, status_code=500,
detail=f"根据 burst_incident 查询爆管定位结果时发生错误: {str(e)}", detail=f"查询分析结果时发生错误: {str(e)}",
) )
-127
View File
@@ -1,127 +0,0 @@
from typing import Any, List, Dict
from fastapi import APIRouter, Query, Path
from app.services.tjnetwork import (
get_pipe_risk_probability_now,
get_pipe_risk_probability,
get_pipes_risk_probability,
get_network_pipe_risk_probability_now,
get_pipe_risk_probability_geometries,
)
router = APIRouter()
@router.get(
"/pipes/risk-probability-now",
summary="获取管道当前风险概率",
description="获取指定管道当前时刻的风险概率值"
)
async def fastapi_get_pipe_risk_probability_now(
network: str = Query(..., description="管网名称(或数据库名称)"),
pipe_id: str = Query(..., description="管道ID")
) -> dict[str, Any]:
"""
获取管道当前风险概率
查询指定管道在当前时刻的风险概率值
Args:
network: 管网名称或数据库名称
pipe_id: 管道ID
Returns:
包含风险概率信息的字典
"""
return get_pipe_risk_probability_now(network, pipe_id)
@router.get(
"/pipes/risk-probability",
summary="获取管道风险概率历史",
description="获取指定管道的风险概率历史数据"
)
async def fastapi_get_pipe_risk_probability(
network: str = Query(..., description="管网名称(或数据库名称)"),
pipe_id: str = Query(..., description="管道ID")
) -> dict[str, Any]:
"""
获取管道风险概率历史
查询指定管道的历史风险概率数据
Args:
network: 管网名称或数据库名称
pipe_id: 管道ID
Returns:
包含风险概率历史的字典
"""
return get_pipe_risk_probability(network, pipe_id)
@router.get(
"/pipes-risk-probabilities",
summary="批量获取多条管道风险概率",
description="批量获取多条管道的风险概率值"
)
async def fastapi_get_pipes_risk_probability(
network: str = Query(..., description="管网名称(或数据库名称)"),
pipe_ids: str = Query(..., description="逗号分隔的管道ID列表")
) -> list[dict[str, Any]]:
"""
批量获取多条管道风险概率
查询多条指定管道的风险概率值
Args:
network: 管网名称或数据库名称
pipe_ids: 逗号分隔的管道ID列表例如pipe1,pipe2,pipe3
Returns:
包含多条管道风险概率的列表
"""
pipeids = pipe_ids.split(",")
return get_pipes_risk_probability(network, pipeids)
@router.get(
"/network-pipe-risk-probability-nows",
summary="获取整个网络的管道风险概率",
description="获取指定网络中所有管道的当前风险概率值"
)
async def fastapi_get_network_pipe_risk_probability_now(
network: str = Query(..., description="管网名称(或数据库名称)"),
) -> list[dict[str, Any]]:
"""
获取整个网络的管道风险概率
查询指定网络中所有管道在当前时刻的风险概率值
Args:
network: 管网名称或数据库名称
Returns:
包含网络内所有管道风险概率的列表
"""
return get_network_pipe_risk_probability_now(network)
@router.get(
"/pipes/risk-probability-geometries",
summary="获取管道风险几何信息",
description="获取指定网络中管道的风险相关几何数据"
)
async def fastapi_get_pipe_risk_probability_geometries(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> dict[str, Any]:
"""
获取管道风险几何信息
查询指定网络中管道的地理和风险相关的几何数据
Args:
network: 管网名称或数据库名称
Returns:
包含几何信息和风险数据的字典
"""
return get_pipe_risk_probability_geometries(network)
+31 -514
View File
@@ -1,525 +1,42 @@
from typing import Any from typing import Any
from fastapi import APIRouter, Request, Query
from app.services.tjnetwork import ( from fastapi import APIRouter, Depends, HTTPException
ChangeSet, from psycopg import AsyncConnection
get_scada_info,
get_all_scada_info, from app.auth.project_dependencies import get_project_pg_connection
get_scada_device_schema, from app.domain.schemas.scada import ScadaDeviceResponse
get_scada_device, from app.infra.db.postgresql.scada import ScadaInfoRepository, get_scada_info_schema
set_scada_device,
add_scada_device,
delete_scada_device,
clean_scada_device,
get_all_scada_device_ids,
get_all_scada_devices,
get_scada_device_data_schema,
get_scada_device_data,
set_scada_device_data,
add_scada_device_data,
delete_scada_device_data,
clean_scada_device_data,
get_scada_element_schema,
get_scada_element,
set_scada_element,
add_scada_element,
delete_scada_element,
clean_scada_element,
get_all_scada_elements,
get_scada_element_schema,
get_scada_info_schema,
)
router = APIRouter() router = APIRouter()
async def fast_get_scada_properties(
network: str = Query(..., description="管网名称(或数据库名称)"),
scada: str = Query(..., description="SCADA设备ID")
) -> dict[str, Any]:
"""
获取单个SCADA设备的属性信息
根据管网名称和SCADA设备ID获取该设备的完整属性 @router.get("/network-schemas/scada-device", summary="获取 SCADA 设备结构")
def get_scada_device_schema() -> dict[str, dict[str, Any]]:
return get_scada_info_schema("")
Args:
network: 管网名称或数据库名称
scada: SCADA设备ID
Returns: @router.get(
SCADA设备的属性字典 "/scada-devices",
""" summary="获取 SCADA 设备列表",
return get_scada_info(network, scada) response_model=list[ScadaDeviceResponse],
)
async def fast_get_all_scada_properties( async def get_scada_devices(
network: str = Query(..., description="管网名称(或数据库名称)") conn: AsyncConnection = Depends(get_project_pg_connection),
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
""" return await ScadaInfoRepository.get_scadas(conn)
获取指定管网所有SCADA设备的属性信息
查询该管网下所有已配置的SCADA设备的属性列表
Args:
network: 管网名称或数据库名称
Returns:
SCADA设备属性列表
"""
return get_all_scada_info(network)
############################################################ @router.get(
# scada_device 设备管理 "/scada-devices/{device_id}",
############################################################ summary="获取 SCADA 设备",
response_model=ScadaDeviceResponse,
@router.get("/network-schemas/scada-device", summary="获取SCADA设备架构", tags=["SCADA设备"]) )
async def fastapi_get_scada_device_schema( async def get_scada_device(
network: str = Query(..., description="管网名称(或数据库名称)") device_id: str,
) -> dict[str, dict[str, Any]]: conn: AsyncConnection = Depends(get_project_pg_connection),
"""
获取SCADA设备的数据架构
返回SCADA设备表的字段定义和类型信息
Args:
network: 管网名称或数据库名称
Returns:
SCADA设备的字段架构信息
"""
return get_scada_device_schema(network)
@router.get("/scada-devices/detail", summary="获取SCADA设备", tags=["SCADA设备"])
async def fastapi_get_scada_device(
network: str = Query(..., description="管网名称(或数据库名称)"),
id: str = Query(..., description="SCADA设备ID")
) -> dict[str, Any]: ) -> dict[str, Any]:
""" device = await ScadaInfoRepository.get_scada(conn, device_id)
获取单个SCADA设备的信息 if device is None:
raise HTTPException(status_code=404, detail="SCADA 设备不存在")
根据设备ID查询该设备的详细信息 return device
Args:
network: 管网名称或数据库名称
id: SCADA设备ID
Returns:
SCADA设备信息
"""
return get_scada_device(network, id)
@router.patch("/scada-devices", response_model=None, summary="更新SCADA设备", tags=["SCADA设备"])
async def fastapi_set_scada_device(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""
更新SCADA设备信息
修改指定SCADA设备的属性
Args:
network: 管网名称或数据库名称
req: 请求体包含要更新的设备属性
Returns:
变更集合信息
"""
props = await req.json()
return set_scada_device(network, ChangeSet(props))
@router.post("/scada-devices", response_model=None, summary="添加SCADA设备", tags=["SCADA设备"])
async def fastapi_add_scada_device(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""
添加新的SCADA设备
在指定管网中添加一个新的SCADA设备
Args:
network: 管网名称或数据库名称
req: 请求体包含新设备的属性
Returns:
变更集合信息
"""
props = await req.json()
return add_scada_device(network, ChangeSet(props))
@router.delete("/scada-devices", response_model=None, summary="删除SCADA设备", tags=["SCADA设备"])
async def fastapi_delete_scada_device(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""
删除SCADA设备
从指定管网中删除一个SCADA设备
Args:
network: 管网名称或数据库名称
req: 请求体包含要删除的设备ID
Returns:
变更集合信息
"""
props = await req.json()
return delete_scada_device(network, ChangeSet(props))
@router.post("/scada-device-cleaning-runs", response_model=None, summary="清空SCADA设备表", tags=["SCADA设备"])
async def fastapi_clean_scada_device(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> ChangeSet:
"""
清空SCADA设备表
删除指定管网中所有的SCADA设备
Args:
network: 管网名称或数据库名称
Returns:
变更集合信息
"""
return clean_scada_device(network)
@router.get("/scada-devices/ids", summary="获取所有SCADA设备ID", tags=["SCADA设备"])
async def fastapi_get_all_scada_device_ids(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> list[str]:
"""
获取指定管网所有SCADA设备的ID列表
Args:
network: 管网名称或数据库名称
Returns:
SCADA设备ID列表
"""
return get_all_scada_device_ids(network)
@router.get("/scada-devices", summary="获取所有SCADA设备", tags=["SCADA设备"])
async def fastapi_get_all_scada_devices(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> list[dict[str, Any]]:
"""
获取指定管网所有SCADA设备的完整信息
Args:
network: 管网名称或数据库名称
Returns:
SCADA设备信息列表
"""
return get_all_scada_devices(network)
############################################################
# scada_device_data 设备数据管理
############################################################
@router.get("/network-schemas/scada-device-data", summary="获取SCADA设备数据架构", tags=["SCADA设备数据"])
async def fastapi_get_scada_device_data_schema(
network: str = Query(..., description="管网名称(或数据库名称)"),
) -> dict[str, dict[str, Any]]:
"""
获取SCADA设备数据的表结构
返回SCADA设备数据表的字段定义和类型信息
Args:
network: 管网名称或数据库名称
Returns:
SCADA设备数据的字段架构信息
"""
return get_scada_device_data_schema(network)
@router.get("/scada-device-datas/detail", summary="获取SCADA设备数据", tags=["SCADA设备数据"])
async def fastapi_get_scada_device_data(
network: str = Query(..., description="管网名称(或数据库名称)"),
device_id: str = Query(..., description="SCADA设备ID")
) -> dict[str, Any]:
"""
获取单个SCADA设备的数据
查询指定设备的监测数据或配置数据
Args:
network: 管网名称或数据库名称
device_id: SCADA设备ID
Returns:
SCADA设备数据
"""
return get_scada_device_data(network, device_id)
@router.patch("/scada-device-datas", response_model=None, summary="更新SCADA设备数据", tags=["SCADA设备数据"])
async def fastapi_set_scada_device_data(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""
更新SCADA设备数据
修改指定SCADA设备的数据
Args:
network: 管网名称或数据库名称
req: 请求体包含要更新的数据
Returns:
变更集合信息
"""
props = await req.json()
return set_scada_device_data(network, ChangeSet(props))
@router.post("/scada-device-datas", response_model=None, summary="添加SCADA设备数据", tags=["SCADA设备数据"])
async def fastapi_add_scada_device_data(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""
添加新的SCADA设备数据
为指定SCADA设备添加新的数据记录
Args:
network: 管网名称或数据库名称
req: 请求体包含新数据的内容
Returns:
变更集合信息
"""
props = await req.json()
return add_scada_device_data(network, ChangeSet(props))
@router.delete("/scada-device-datas", response_model=None, summary="删除SCADA设备数据", tags=["SCADA设备数据"])
async def fastapi_delete_scada_device_data(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""
删除SCADA设备数据
删除指定SCADA设备的数据记录
Args:
network: 管网名称或数据库名称
req: 请求体包含要删除的数据ID
Returns:
变更集合信息
"""
props = await req.json()
return delete_scada_device_data(network, ChangeSet(props))
@router.post("/scada-device-data-cleaning-runs", response_model=None, summary="清空SCADA设备数据表", tags=["SCADA设备数据"])
async def fastapi_clean_scada_device_data(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> ChangeSet:
"""
清空SCADA设备数据表
删除指定管网中所有SCADA设备的数据
Args:
network: 管网名称或数据库名称
Returns:
变更集合信息
"""
return clean_scada_device_data(network)
############################################################
# scada_element SCADA元素映射
############################################################
@router.get("/network-schemas/scada-element", summary="获取SCADA元素架构", tags=["SCADA元素映射"])
async def fastapi_get_scada_element_schema(
network: str = Query(..., description="管网名称(或数据库名称)"),
) -> dict[str, dict[str, Any]]:
"""
获取SCADA元素映射的表结构
返回SCADA元素映射表的字段定义和类型信息
Args:
network: 管网名称或数据库名称
Returns:
SCADA元素映射的字段架构信息
"""
return get_scada_element_schema(network)
@router.get("/scada-elements", summary="获取所有SCADA元素映射", tags=["SCADA元素映射"])
async def fastapi_get_scada_elements(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> list[dict[str, Any]]:
"""
获取指定管网所有SCADA元素映射
查询所有SCADA设备与管网元素节点/管道的映射关系
Args:
network: 管网名称或数据库名称
Returns:
SCADA元素映射列表
"""
return get_all_scada_elements(network)
@router.get("/scada-elements/detail", summary="获取单个SCADA元素映射", tags=["SCADA元素映射"])
async def fastapi_get_scada_element(
network: str = Query(..., description="管网名称(或数据库名称)"),
id: str = Query(..., description="SCADA元素映射ID")
) -> dict[str, Any]:
"""
获取单个SCADA元素映射的信息
根据ID查询特定的SCADA设备与管网元素的映射关系
Args:
network: 管网名称或数据库名称
id: SCADA元素映射ID
Returns:
SCADA元素映射信息
"""
return get_scada_element(network, id)
@router.patch("/scada-elements", response_model=None, summary="更新SCADA元素映射", tags=["SCADA元素映射"])
async def fastapi_set_scada_element(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""
更新SCADA元素映射
修改SCADA设备与管网元素的映射关系
Args:
network: 管网名称或数据库名称
req: 请求体包含要更新的映射信息
Returns:
变更集合信息
"""
props = await req.json()
return set_scada_element(network, ChangeSet(props))
@router.post("/scada-elements", response_model=None, summary="添加SCADA元素映射", tags=["SCADA元素映射"])
async def fastapi_add_scada_element(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""
添加新的SCADA元素映射
创建SCADA设备与管网元素的新映射关系
Args:
network: 管网名称或数据库名称
req: 请求体包含新映射的信息
Returns:
变更集合信息
"""
props = await req.json()
return add_scada_element(network, ChangeSet(props))
@router.delete("/scada-elements", response_model=None, summary="删除SCADA元素映射", tags=["SCADA元素映射"])
async def fastapi_delete_scada_element(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""
删除SCADA元素映射
移除SCADA设备与管网元素的映射关系
Args:
network: 管网名称或数据库名称
req: 请求体包含要删除的映射ID
Returns:
变更集合信息
"""
props = await req.json()
return delete_scada_element(network, ChangeSet(props))
@router.post("/scada-element-cleaning-runs", response_model=None, summary="清空SCADA元素映射表", tags=["SCADA元素映射"])
async def fastapi_clean_scada_element(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> ChangeSet:
"""
清空SCADA元素映射表
删除指定管网中所有的SCADA元素映射
Args:
network: 管网名称或数据库名称
Returns:
变更集合信息
"""
return clean_scada_element(network)
############################################################
# scada_info SCADA信息
############################################################
@router.get("/scada-info-schemas", summary="获取SCADA信息架构", tags=["SCADA信息"])
async def fastapi_get_scada_info_schema(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> dict[str, dict[str, Any]]:
"""
获取SCADA信息表的结构
返回SCADA信息表的字段定义和类型信息
Args:
network: 管网名称或数据库名称
Returns:
SCADA信息的字段架构信息
"""
return get_scada_info_schema(network)
@router.get("/scada-info/detail", summary="获取SCADA信息", tags=["SCADA信息"])
async def fastapi_get_scada_info(
network: str = Query(..., description="管网名称(或数据库名称)"),
id: str = Query(..., description="SCADA信息ID")
) -> dict[str, Any]:
"""
获取单个SCADA信息
根据ID查询SCADA的详细配置信息
Args:
network: 管网名称或数据库名称
id: SCADA信息ID
Returns:
SCADA信息详情
"""
return get_scada_info(network, id)
@router.get("/scada-info", summary="获取所有SCADA信息", tags=["SCADA信息"])
async def fastapi_get_all_scada_info(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> list[dict[str, Any]]:
"""
获取指定管网所有SCADA的信息
查询该管网下所有已配置的SCADA的完整信息
Args:
network: 管网名称或数据库名称
Returns:
SCADA信息列表
"""
return get_all_scada_info(network)
-60
View File
@@ -1,60 +0,0 @@
from datetime import datetime
from fastapi import APIRouter, HTTPException, Path, Query
from typing import Any
from app.services.tjnetwork import get_scheme_schema, get_scheme, get_all_schemes
from app.services.scheme_management import query_scheme_detail
from app.services.time_api import extract_date
router = APIRouter()
@router.get("/network-schemas/scheme", summary="获取方案模式", description="获取指定网络的方案模式定义")
async def fastapi_get_scheme_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[Any, Any]]:
"""
获取方案模式定义
返回指定网络的方案模式结构定义
"""
return get_scheme_schema(network)
@router.get("/schemes/detail", summary="获取单个方案", description="根据名称获取指定的方案信息")
async def fastapi_get_scheme(network: str = Query(..., description="管网名称(或数据库名称)"), schema_name: str = Query(..., description="方案名称")) -> dict[Any, Any]:
"""
获取单个方案详情
返回指定网络中指定名称的方案详细信息
"""
return get_scheme(network, schema_name)
@router.get("/schemes", summary="获取所有方案", description="获取指定网络的所有方案信息")
async def fastapi_get_all_schemes(
network: str = Query(..., description="管网名称(或数据库名称)"),
scheme_type: str | None = Query(None, description="方案类型;为空时返回全部类型"),
query_date: datetime | None = Query(None, description="查询日期(可选)"),
) -> list[dict[Any, Any]]:
"""
获取所有方案列表
返回指定网络中所有可用的方案
"""
parsed_date = (
extract_date(query_date, field_name="query_date")
if query_date is not None
else None
)
return get_all_schemes(network, scheme_type=scheme_type, query_date=parsed_date)
@router.get("/schemes/{scheme_name}", summary="获取方案详情", description="按方案类型获取指定方案详情")
async def fastapi_get_scheme_detail(
scheme_name: str = Path(..., description="方案名称"),
network: str = Query(..., description="管网名称(或数据库名称)"),
scheme_type: str | None = Query(None, description="方案类型;为空时返回通用方案详情"),
) -> dict[Any, Any]:
result = query_scheme_detail(
name=network,
scheme_name=scheme_name,
scheme_type=scheme_type,
)
if not result:
raise HTTPException(status_code=404, detail=f"Scheme {scheme_name} not found")
return result
+67 -46
View File
@@ -1,17 +1,19 @@
import logging import logging
from typing import Any from typing import Any
from urllib.parse import quote from urllib.parse import quote
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from starlette.concurrency import run_in_threadpool from starlette.concurrency import run_in_threadpool
from app.algorithms.sensor import (
pressure_sensor_placement_kmeans,
pressure_sensor_placement_sensitivity,
)
from app.auth.metadata_dependencies import get_current_metadata_user from app.auth.metadata_dependencies import get_current_metadata_user
from app.auth.project_dependencies import ProjectContext, get_project_context from app.auth.project_dependencies import (
ProjectContext,
get_project_context,
use_project_business_routing,
)
from app.infra.db.project_routing import ActiveProjectRouting
from app.domain.schemas.sensor_placement import ( from app.domain.schemas.sensor_placement import (
SensorPointResponse, SensorPointResponse,
SensorPlacementExportRequest, SensorPlacementExportRequest,
@@ -26,8 +28,11 @@ from app.services.sensor_placement import (
build_sensor_placement_workbook, build_sensor_placement_workbook,
can_edit_sensor_placement, can_edit_sensor_placement,
get_sensor_placement_candidate, get_sensor_placement_candidate,
get_sensor_placement_scheme, get_sensor_placement_run,
update_sensor_placement_scheme, list_sensor_placement_runs,
optimize_sensor_placement_by_kmeans,
optimize_sensor_placement_by_sensitivity,
update_sensor_placement_run,
) )
router = APIRouter() router = APIRouter()
@@ -74,19 +79,19 @@ def _service_http_error(exc: Exception) -> HTTPException:
) )
def _get_scheme_response( def _get_run_response(
network: str, network: str,
scheme_id: int, run_id: UUID,
current_user: Any, current_user: Any,
project_context: ProjectContext, project_context: ProjectContext,
) -> dict[str, Any]: ) -> dict[str, Any]:
try: try:
scheme = get_sensor_placement_scheme(network, scheme_id) run = get_sensor_placement_run(network, run_id)
return { return {
**scheme, **run,
"can_edit": ( "can_edit": (
_can_modify_project(project_context) _can_modify_project(project_context)
and can_edit_sensor_placement(current_user, scheme) and can_edit_sensor_placement(current_user, run)
), ),
} }
except ( except (
@@ -104,6 +109,7 @@ def _get_scheme_response(
async def get_sensor_placement_candidate_detail( async def get_sensor_placement_candidate_detail(
node_id: str = Path(..., min_length=1, max_length=32), node_id: str = Path(..., min_length=1, max_length=32),
project_context: ProjectContext = Depends(get_project_context), project_context: ProjectContext = Depends(get_project_context),
_routing: ActiveProjectRouting = Depends(use_project_business_routing),
) -> dict[str, Any]: ) -> dict[str, Any]:
try: try:
return await run_in_threadpool( return await run_in_threadpool(
@@ -116,7 +122,7 @@ async def get_sensor_placement_candidate_detail(
@router.post( @router.post(
"/sensor-placement-optimization-runs", "/sensor-placement-runs",
response_model=SensorPlacementSchemeResponse, response_model=SensorPlacementSchemeResponse,
summary="创建并返回监测点优化方案", summary="创建并返回监测点优化方案",
) )
@@ -128,21 +134,21 @@ async def optimize_sensor_placement_scheme(
network = _project_network(payload.network, project_context) network = _project_network(payload.network, project_context)
_require_project_write(project_context) _require_project_write(project_context)
optimizer = ( optimizer = (
pressure_sensor_placement_sensitivity optimize_sensor_placement_by_sensitivity
if payload.method == "sensitivity" if payload.method == "sensitivity"
else pressure_sensor_placement_kmeans else optimize_sensor_placement_by_kmeans
) )
try: try:
created = await run_in_threadpool( created = await run_in_threadpool(
optimizer, optimizer,
name=network, project_code=network,
scheme_name=payload.scheme_name, run_name=payload.run_name,
sensor_number=payload.sensor_count, sensor_count=payload.sensor_count,
min_diameter=payload.min_diameter, min_diameter=payload.min_diameter,
username=current_user.username, created_by=current_user.username,
) )
scheme = get_sensor_placement_scheme(network, int(created["id"])) run = get_sensor_placement_run(network, created["run_id"])
return {**scheme, "can_edit": True} return {**run, "can_edit": True}
except ( except (
SensorPlacementConflictError, SensorPlacementConflictError,
SensorPlacementValidationError, SensorPlacementValidationError,
@@ -158,31 +164,46 @@ async def optimize_sensor_placement_scheme(
@router.get( @router.get(
"/sensor-placement-schemes/{scheme_id}", "/sensor-placement-runs",
response_model=list[SensorPlacementSchemeResponse],
summary="获取监测点优化运行",
)
async def get_sensor_placement_runs(
project_context: ProjectContext = Depends(get_project_context),
_routing: ActiveProjectRouting = Depends(use_project_business_routing),
) -> list[dict[str, Any]]:
return await run_in_threadpool(
list_sensor_placement_runs,
project_context.project_code,
)
@router.get(
"/sensor-placement-runs/{run_id}",
response_model=SensorPlacementSchemeResponse, response_model=SensorPlacementSchemeResponse,
summary="获取监测点方案详情", summary="获取监测点方案详情",
) )
async def get_sensor_placement_scheme_detail( def get_sensor_placement_run_detail(
scheme_id: int, run_id: UUID,
network: str = Query(..., min_length=1), network: str = Query(..., min_length=1),
project_context: ProjectContext = Depends(get_project_context), project_context: ProjectContext = Depends(get_project_context),
current_user=Depends(get_current_metadata_user), current_user=Depends(get_current_metadata_user),
) -> dict[str, Any]: ) -> dict[str, Any]:
return _get_scheme_response( return _get_run_response(
_project_network(network, project_context), _project_network(network, project_context),
scheme_id, run_id,
current_user, current_user,
project_context, project_context,
) )
@router.put( @router.put(
"/sensor-placement-schemes/{scheme_id}", "/sensor-placement-runs/{run_id}",
response_model=SensorPlacementSchemeResponse, response_model=SensorPlacementSchemeResponse,
summary="覆盖保存监测点方案", summary="覆盖保存监测点方案",
) )
async def overwrite_sensor_placement_scheme( def overwrite_sensor_placement_run(
scheme_id: int, run_id: UUID,
payload: SensorPlacementUpdateRequest, payload: SensorPlacementUpdateRequest,
network: str = Query(..., min_length=1), network: str = Query(..., min_length=1),
project_context: ProjectContext = Depends(get_project_context), project_context: ProjectContext = Depends(get_project_context),
@@ -190,21 +211,21 @@ async def overwrite_sensor_placement_scheme(
) -> dict[str, Any]: ) -> dict[str, Any]:
network = _project_network(network, project_context) network = _project_network(network, project_context)
_require_project_write(project_context) _require_project_write(project_context)
scheme = _get_scheme_response( run = _get_run_response(
network, network,
scheme_id, run_id,
current_user, current_user,
project_context, project_context,
) )
if not scheme["can_edit"]: if not run["can_edit"]:
raise HTTPException(status_code=403, detail="无权修改该监测点方案") raise HTTPException(status_code=403, detail="无权修改该监测点优化运行")
try: try:
updated = update_sensor_placement_scheme( updated = update_sensor_placement_run(
network, network,
scheme_id, run_id,
expected_sensor_location=payload.expected_sensor_location, expected_sensor_locations=payload.expected_sensor_locations,
sensor_location=payload.sensor_location, sensor_locations=payload.sensor_locations,
) )
return {**updated, "can_edit": True} return {**updated, "can_edit": True}
except ( except (
@@ -216,26 +237,26 @@ async def overwrite_sensor_placement_scheme(
@router.post( @router.post(
"/sensor-placement-schemes/{scheme_id}/exports/excel", "/sensor-placement-runs/{run_id}/exports/excel",
summary="导出监测点工程清单", summary="导出监测点工程清单",
) )
async def export_sensor_placement_excel( async def export_sensor_placement_excel(
scheme_id: int, run_id: UUID,
payload: SensorPlacementExportRequest, payload: SensorPlacementExportRequest,
network: str = Query(..., min_length=1), network: str = Query(..., min_length=1),
project_context: ProjectContext = Depends(get_project_context), project_context: ProjectContext = Depends(get_project_context),
current_user=Depends(get_current_metadata_user), current_user=Depends(get_current_metadata_user),
) -> StreamingResponse: ) -> StreamingResponse:
network = _project_network(network, project_context) network = _project_network(network, project_context)
scheme = _get_scheme_response( run = _get_run_response(
network, network,
scheme_id, run_id,
current_user, current_user,
project_context, project_context,
) )
if ( if (
payload.sensor_location != scheme["sensor_location"] payload.sensor_locations != run["sensor_locations"]
and not scheme["can_edit"] and not run["can_edit"]
): ):
raise HTTPException(status_code=403, detail="无权导出该方案的未保存草稿") raise HTTPException(status_code=403, detail="无权导出该方案的未保存草稿")
@@ -243,14 +264,14 @@ async def export_sensor_placement_excel(
workbook = await run_in_threadpool( workbook = await run_in_threadpool(
build_sensor_placement_workbook, build_sensor_placement_workbook,
network=network, network=network,
scheme=scheme, scheme=run,
sensor_location=payload.sensor_location, sensor_location=payload.sensor_locations,
adjustment_status=payload.adjustment_status, adjustment_status=payload.adjustment_status,
) )
except SensorPlacementValidationError as exc: except SensorPlacementValidationError as exc:
raise _service_http_error(exc) from exc raise _service_http_error(exc) from exc
filename = f"{scheme['scheme_name']}_监测点清单.xlsx" filename = f"{run['name']}_监测点清单.xlsx"
encoded_filename = quote(filename) encoded_filename = quote(filename)
return StreamingResponse( return StreamingResponse(
workbook, workbook,
+34 -463
View File
@@ -1,39 +1,22 @@
from typing import Any, List, Literal, Optional from typing import Any, List, Literal, Optional
from datetime import datetime, timedelta from datetime import datetime, timedelta
import json from fastapi import APIRouter, Body, Depends, HTTPException, Query
import threading
from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
from fastapi.responses import PlainTextResponse from fastapi.responses import PlainTextResponse
from app.auth.keycloak_dependencies import get_current_keycloak_username from app.auth.keycloak_dependencies import get_current_keycloak_username
import app.services.simulation as simulation import app.services.simulation as simulation
import app.services.globals as globals
from app.services.tjnetwork import ( from app.services.tjnetwork import (
run_project, run_project,
run_project_return_dict, run_project_return_dict,
run_inp,
dump_output,
) )
from app.algorithms.simulation.scenarios import ( from app.services.simulation_scenarios import (
burst_analysis, burst_analysis,
valve_close_analysis, valve_close_analysis,
flushing_analysis, flushing_analysis,
contaminant_simulation, contaminant_simulation,
age_analysis,
# scheduling_analysis,
pressure_regulation, pressure_regulation,
) )
from app.algorithms.sensor import (
pressure_sensor_placement_sensitivity,
pressure_sensor_placement_kmeans,
)
from app.services.simulation_ops import (
project_management,
scheduling_simulation,
daily_scheduling_simulation,
)
from app.services.valve_isolation import analyze_valve_isolation from app.services.valve_isolation import analyze_valve_isolation
from app.services.time_api import ( from app.domain.time import (
parse_aware_time, parse_aware_time,
parse_clock_duration_seconds, parse_clock_duration_seconds,
parse_utc_time, parse_utc_time,
@@ -55,65 +38,13 @@ class RunSimulationManuallyByDate(BaseModel):
return value return value
class BurstAnalysis(BaseModel):
name: str = Field(..., description="管网名称(或数据库名称)")
modify_pattern_start_time: str = Field(..., description="模式修改开始时间 (ISO 8601)")
burst_ID: List[str] | str | None = Field(None, description="爆管节点/管段ID列表")
burst_size: List[float] | float | int | None = Field(None, description="爆管流量大小")
modify_total_duration: int = Field(900, description="模拟总时长 (秒)")
modify_fixed_pump_pattern: Optional[dict[str, list]] = Field(None, description="定速泵模式修改")
modify_variable_pump_pattern: Optional[dict[str, list]] = Field(None, description="变速泵模式修改")
modify_valve_opening: Optional[dict[str, float]] = Field(None, description="阀门开度修改")
scheme_name: Optional[str] = Field(None, description="方案名称")
class SchedulingAnalysis(BaseModel):
network: str = Field(..., description="管网名称(或数据库名称)")
start_time: str = Field(..., description="开始时间")
pump_control: dict = Field(..., description="泵控制策略")
tank_id: str = Field(..., description="水箱ID")
water_plant_output_id: str = Field(..., description="水厂出水ID")
time_delta: Optional[int] = Field(300, description="时间步长 (秒)")
class PressureRegulation(BaseModel): class PressureRegulation(BaseModel):
network: str = Field(..., description="管网名称(或数据库名称)") network: str = Field(..., description="管网名称(或数据库名称)")
start_time: str = Field(..., description="开始时间") start_time: str = Field(..., description="开始时间")
pump_control: dict = Field(..., description="泵控制策略") pump_control: dict = Field(..., description="泵控制策略")
tank_init_level: Optional[dict] = Field(None, description="水箱初始水位") tank_init_level: Optional[dict] = Field(None, description="水箱初始水位")
duration: Optional[int] = Field(900, description="持续时间 (秒)") duration: Optional[int] = Field(900, description="持续时间 (秒)")
scheme_name: Optional[str] = Field(None, description="方案名称") scheme_name: str = Field(..., min_length=1, description="方案名称")
class ProjectManagement(BaseModel):
network: str = Field(..., description="管网名称(或数据库名称)")
start_time: str = Field(..., description="开始时间")
pump_control: dict = Field(..., description="泵控制策略")
tank_init_level: Optional[dict] = Field(None, description="水箱初始水位")
region_demand: Optional[dict] = Field(None, description="区域需水量控制")
class DailySchedulingAnalysis(BaseModel):
network: str = Field(..., description="管网名称(或数据库名称)")
start_time: str = Field(..., description="开始时间")
pump_control: dict = Field(..., description="泵控制策略")
reservoir_id: str = Field(..., description="水库ID")
tank_id: str = Field(..., description="水箱ID")
water_plant_output_id: str = Field(..., description="水厂出水ID")
time_delta: Optional[int] = Field(300, description="时间步长 (秒)")
class PumpFailureState(BaseModel):
time: str = Field(..., description="故障发生时间")
pump_status: dict = Field(..., description="泵状态字典")
class PressureSensorPlacement(BaseModel):
name: str = Field(..., description="管网名称(或数据库名称)")
scheme_name: str = Field(..., description="方案名称")
sensor_number: int = Field(..., description="传感器数量")
min_diameter: int = Field(0, description="最小管径限制")
username: str = Field(..., description="用户名")
def run_simulation_manually_by_date( def run_simulation_manually_by_date(
@@ -128,19 +59,23 @@ def run_simulation_manually_by_date(
if hydraulic_step_seconds <= 0: if hydraulic_step_seconds <= 0:
raise ValueError("HYDRAULIC TIMESTEP must be greater than 0.") raise ValueError("HYDRAULIC TIMESTEP must be greater than 0.")
hydraulic_step = timedelta(seconds=hydraulic_step_seconds) hydraulic_step = timedelta(seconds=hydraulic_step_seconds)
scada_mappings = simulation.query_corresponding_element_id_and_query_id(
network_name
)
current_time = start_time current_time = start_time
while current_time < end_datetime: while current_time < end_datetime:
simulation.run_simulation( simulation.run_simulation(
name=network_name, name=network_name,
simulation_type="realtime", simulation_type="realtime",
modify_pattern_start_time=current_time.isoformat(timespec="seconds"), modify_pattern_start_time=current_time.isoformat(timespec="seconds"),
scada_mappings=scada_mappings,
) )
current_time += hydraulic_step current_time += hydraulic_step
# 必须用这个PlainTextResponse,不然每个key都有引号 # 必须用这个PlainTextResponse,不然每个key都有引号
@router.post("/project-runs", response_class=PlainTextResponse, summary="运行项目模拟", description="基于指定的管网项目运行标准水力模拟,返回纯文本格式的模拟报告。") @router.post("/project-runs", response_class=PlainTextResponse, summary="运行项目模拟", description="基于指定的管网项目运行标准水力模拟,返回纯文本格式的模拟报告。")
async def run_project_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> str: def run_project_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> str:
""" """
运行项目模拟 运行项目模拟
@@ -156,7 +91,7 @@ async def run_project_endpoint(network: str = Query(..., description="管网名
# output 是 json # output 是 json
# report 是 text # report 是 text
@router.post("/project-return-dict-runs", summary="运行项目模拟(返回字典)", description="基于指定的管网项目运行标准水力模拟,返回JSON格式的字典,包含输出数据和报告文本。") @router.post("/project-return-dict-runs", summary="运行项目模拟(返回字典)", description="基于指定的管网项目运行标准水力模拟,返回JSON格式的字典,包含输出数据和报告文本。")
async def run_project_return_dict_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]: def run_project_return_dict_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
""" """
运行项目模拟返回字典 运行项目模拟返回字典
@@ -171,41 +106,15 @@ async def run_project_return_dict_endpoint(network: str = Query(..., description
return run_project_return_dict(network) return run_project_return_dict(network)
# put in inp folder, name without extension
@router.post("/inp-runs", summary="运行INP文件", description="运行指定INP文件格式的管网模型进行水力模拟。INP文件应该放在inp文件夹中,参数为文件名不含扩展名。")
async def run_inp_endpoint(network: str = Query(..., description="inp文件名(不含扩展名)")) -> str:
"""
运行INP文件
- **network**: inp文件名不含扩展名
从inp文件夹中读取指定的INP文件并运行模拟
"""
return run_inp(network)
# path is absolute path
@router.get("/outputs", summary="导出模拟输出", description="导出指定路径的模拟输出文件内容。参数应为绝对路径。")
async def dump_output_endpoint(output: str = Query(..., description="模拟输出文件的绝对路径")) -> str:
"""
导出模拟输出
- **output**: 模拟输出文件的绝对路径
读取并返回指定路径的模拟输出内容
"""
return dump_output(output)
# Analysis Endpoints # Analysis Endpoints
@router.post("/burst-analyses", summary="爆管分析(高级)", description="高级版本的爆管分析,支持在指定时间点修改泵控制模式和阀门开度,以分析这些改变对爆管影响的作用。支持固定泵和变速泵的独立控制。") @router.post("/burst-analyses", summary="爆管分析(高级)", description="高级版本的爆管分析,支持在指定时间点修改泵控制模式和阀门开度,以分析这些改变对爆管影响的作用。支持固定泵和变速泵的独立控制。")
async def fastapi_burst_analysis( def fastapi_burst_analysis(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
modify_pattern_start_time: str = Query(..., description="模式修改开始时间(ISO 8601格式)"), modify_pattern_start_time: str = Query(..., description="模式修改开始时间(ISO 8601格式)"),
burst_ID: list[str] = Query(..., description="爆管节点/管段ID列表"), burst_ID: list[str] = Query(..., min_length=1, description="爆管节点/管段ID列表"),
burst_size: list[float] = Query(..., description="对应各爆管点的爆管流量大小列表(L/s)"), burst_size: list[float] = Query(..., min_length=1, description="对应各爆管点的爆管流量大小列表(L/s)"),
modify_total_duration: int = Query(..., description="模拟总时长(秒)"), modify_total_duration: int = Query(..., gt=0, description="模拟总时长(秒)"),
scheme_name: str = Query(..., description="分析方案名称"), scheme_name: str = Query(..., min_length=1, description="分析方案名称"),
username: str = Depends(get_current_keycloak_username), username: str = Depends(get_current_keycloak_username),
) -> str: ) -> str:
""" """
@@ -220,6 +129,11 @@ async def fastapi_burst_analysis(
支持在指定时间修改泵控制模式和阀门开度 支持在指定时间修改泵控制模式和阀门开度
""" """
if len(burst_ID) != len(burst_size):
raise HTTPException(
status_code=422,
detail="burst_id 与 burst_size 的数量必须一致",
)
burst_analysis( burst_analysis(
name=network, name=network,
modify_pattern_start_time=modify_pattern_start_time, modify_pattern_start_time=modify_pattern_start_time,
@@ -233,7 +147,7 @@ async def fastapi_burst_analysis(
@router.post("/valve-closure-analyses", response_class=PlainTextResponse, summary="阀门关闭分析(高级)", description="高级版本的阀门关闭分析,支持同时关闭多个阀门,并在指定持续时间内进行模拟。返回纯文本格式的分析结果。") @router.post("/valve-closure-analyses", response_class=PlainTextResponse, summary="阀门关闭分析(高级)", description="高级版本的阀门关闭分析,支持同时关闭多个阀门,并在指定持续时间内进行模拟。返回纯文本格式的分析结果。")
async def fastapi_valve_close_analysis( def fastapi_valve_close_analysis(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
start_time: str = Query(..., description="阀门关闭开始时间(ISO 8601格式)"), start_time: str = Query(..., description="阀门关闭开始时间(ISO 8601格式)"),
valves: List[str] = Query(..., description="要关闭的阀门ID列表"), valves: List[str] = Query(..., description="要关闭的阀门ID列表"),
@@ -262,7 +176,7 @@ async def fastapi_valve_close_analysis(
@router.post("/valve-isolation-analyses", summary="阀门隔离分析", description="分析当发生突发事件时,通过关闭指定阀门进行隔离,确定哪些阀门必须关闭、哪些可选关闭,以及隔离的可行性。") @router.post("/valve-isolation-analyses", summary="阀门隔离分析", description="分析当发生突发事件时,通过关闭指定阀门进行隔离,确定哪些阀门必须关闭、哪些可选关闭,以及隔离的可行性。")
async def valve_isolation_endpoint( def valve_isolation_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
accident_element: List[str] = Query(..., description="发生事故的管段/节点ID列表"), accident_element: List[str] = Query(..., description="发生事故的管段/节点ID列表"),
disabled_valves: List[str] = Query(None, description="已故障的阀门ID列表(可选)"), disabled_valves: List[str] = Query(None, description="已故障的阀门ID列表(可选)"),
@@ -303,7 +217,7 @@ async def valve_isolation_endpoint(
@router.post("/flushing-analyses", response_class=PlainTextResponse, summary="冲洗分析(高级)", description="高级版本的冲洗分析,支持按状态和设置值控制多个可选阀门,指定排污节点,并设置固定的冲洗流量。返回纯文本格式的分析结果。") @router.post("/flushing-analyses", response_class=PlainTextResponse, summary="冲洗分析(高级)", description="高级版本的冲洗分析,支持按状态和设置值控制多个可选阀门,指定排污节点,并设置固定的冲洗流量。返回纯文本格式的分析结果。")
async def fastapi_flushing_analysis( def fastapi_flushing_analysis(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
start_time: str = Query(..., description="冲洗开始时间(ISO 8601格式)"), start_time: str = Query(..., description="冲洗开始时间(ISO 8601格式)"),
valves: List[str] | None = Query(None, description="参与控制的阀门ID列表(可选)"), valves: List[str] | None = Query(None, description="参与控制的阀门ID列表(可选)"),
@@ -416,7 +330,7 @@ async def fastapi_flushing_analysis(
@router.post("/contaminant-simulations", response_class=PlainTextResponse, summary="污染物模拟", description="对管网中的污染物扩散进行模拟,评估污染源对管网的影响范围和浓度分布。支持指定污染源位置、污染浓度和扩散模式。") @router.post("/contaminant-simulations", response_class=PlainTextResponse, summary="污染物模拟", description="对管网中的污染物扩散进行模拟,评估污染源对管网的影响范围和浓度分布。支持指定污染源位置、污染浓度和扩散模式。")
async def fastapi_contaminant_simulation( def fastapi_contaminant_simulation(
network: str = Query(..., description="管网名称(或数据库名称)"), network: str = Query(..., description="管网名称(或数据库名称)"),
start_time: str = Query(..., description="污染开始时间(ISO 8601格式)"), start_time: str = Query(..., description="污染开始时间(ISO 8601格式)"),
source: str = Query(..., description="污染源节点ID"), source: str = Query(..., description="污染源节点ID"),
@@ -452,50 +366,8 @@ async def fastapi_contaminant_simulation(
return result or "success" return result or "success"
@router.post("/water-age-analyses", response_class=PlainTextResponse, summary="水龄分析(高级)", description="高级版本的水龄分析,在指定时间点进行分析,支持自定义模拟持续时间。返回纯文本格式的分析结果。")
async def fastapi_age_analysis(
network: str = Query(..., description="管网名称(或数据库名称)"),
start_time: str = Query(..., description="分析开始时间(ISO 8601格式)"),
duration: int = Query(..., description="模拟持续时间(秒)"),
) -> str:
"""
水龄分析高级版本
- **network**: 管网名称或数据库名称
- **start_time**: 分析开始时间
- **duration**: 模拟持续时间
分析指定时间段内管网中各节点的水体停留时间
"""
result = age_analysis(network, start_time, duration)
return result or "success"
# @router.get("/schedulinganalysis/")
# async def scheduling_analysis_endpoint(network: str):
# return scheduling_analysis(network)
@router.post("/pressure-regulation-calculations", summary="压力调节(基础)", description="对管网的压力进行调节分析,通过控制泵的运行来维持目标节点的目标压力。此为基础版本。")
async def pressure_regulation_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
target_node: str = Query(..., description="目标节点ID"),
target_pressure: float = Query(..., description="目标压力值(kPa"),
):
"""
压力调节基础版本
- **network**: 管网名称或数据库名称
- **target_node**: 目标节点ID
- **target_pressure**: 目标压力值kPa
通过泵控制维持目标节点的压力
"""
return pressure_regulation(network, target_node, target_pressure)
@router.post("/pressure-regulation-analyses", summary="压力调节(高级)", description="高级版本的压力调节分析,通过JSON请求体提供详细的控制参数,包括固定泵和变速泵的独立控制、水箱初始水位等。") @router.post("/pressure-regulation-analyses", summary="压力调节(高级)", description="高级版本的压力调节分析,通过JSON请求体提供详细的控制参数,包括固定泵和变速泵的独立控制、水箱初始水位等。")
async def fastapi_pressure_regulation(data: PressureRegulation = Body(..., description="压力调节控制参数")) -> str: def fastapi_pressure_regulation(data: PressureRegulation = Body(..., description="压力调节控制参数")) -> str:
""" """
压力调节高级版本 压力调节高级版本
@@ -505,14 +377,16 @@ async def fastapi_pressure_regulation(data: PressureRegulation = Body(..., descr
- **pump_control**: 泵控制策略字典 - **pump_control**: 泵控制策略字典
- **tank_init_level**: 水箱初始水位字典可选 - **tank_init_level**: 水箱初始水位字典可选
- **duration**: 模拟持续时间可选默认900 - **duration**: 模拟持续时间可选默认900
- **scheme_name**: 控制方案名称可选 - **scheme_name**: 控制方案名称
支持固定泵和变速泵的独立控制 支持固定泵和变速泵的独立控制
""" """
item = data.dict() item = data.model_dump()
simulation.query_corresponding_element_id_and_query_id(item["network"]) scada_mappings = simulation.query_corresponding_element_id_and_query_id(
fixed_pumps = set(globals.fixed_pumps_id.keys()) item["network"]
variable_pumps = set(globals.variable_pumps_id.keys()) )
fixed_pumps = set(scada_mappings.fixed_pumps)
variable_pumps = set(scada_mappings.variable_pumps)
fixed_pump_pattern: dict[str, list] = {} fixed_pump_pattern: dict[str, list] = {}
variable_pump_pattern: dict[str, list] = {} variable_pump_pattern: dict[str, list] = {}
for pump_id, values in item["pump_control"].items(): for pump_id, values in item["pump_control"].items():
@@ -528,291 +402,13 @@ async def fastapi_pressure_regulation(data: PressureRegulation = Body(..., descr
modify_fixed_pump_pattern=fixed_pump_pattern or None, modify_fixed_pump_pattern=fixed_pump_pattern or None,
modify_variable_pump_pattern=variable_pump_pattern or None, modify_variable_pump_pattern=variable_pump_pattern or None,
scheme_name=item["scheme_name"], scheme_name=item["scheme_name"],
) scada_mappings=scada_mappings,
return "success"
@router.post("/project-managements", summary="项目管理(高级)", description="高级版本的项目管理,通过JSON请求体提供详细的控制参数,包括泵控制策略、水箱初始水位和区域需水量控制。")
async def fastapi_project_management(data: ProjectManagement = Body(..., description="项目管理控制参数")) -> str:
"""
项目管理高级版本
请求体参数
- **network**: 管网名称或数据库名称
- **start_time**: 管理开始时间
- **pump_control**: 泵控制策略字典
- **tank_init_level**: 水箱初始水位字典可选
- **region_demand**: 区域需水量控制字典可选
支持多维度的项目管理
"""
item = data.dict()
return project_management(
prj_name=item["network"],
start_datetime=item["start_time"],
pump_control=item["pump_control"],
tank_initial_level_control=item["tank_init_level"],
region_demand_control=item["region_demand"],
)
# @router.get("/dailyschedulinganalysis/")
# async def daily_scheduling_analysis_endpoint(network: str):
# return daily_scheduling_analysis(network)
@router.post("/scheduling-analyses", summary="排程分析", description="对管网的供水排程进行分析,优化泵的运行时间和出水流量,平衡水厂出水、水箱进出水,满足用户需求。")
async def fastapi_scheduling_analysis(data: SchedulingAnalysis = Body(..., description="排程分析参数")) -> str:
"""
排程分析
请求体参数
- **network**: 管网名称或数据库名称
- **start_time**: 分析开始时间
- **pump_control**: 泵控制策略字典
- **tank_id**: 水箱ID
- **water_plant_output_id**: 水厂出水ID
- **time_delta**: 时间步长可选默认300
用于优化供水排程
"""
item = data.dict()
return scheduling_simulation(
item["network"],
item["start_time"],
item["pump_control"],
item["tank_id"],
item["water_plant_output_id"],
item["time_delta"],
)
@router.post("/daily-scheduling-analyses", summary="日排程分析", description="对管网的每日供水排程进行分析,优化水库、水厂、水箱和用户需求的协调,制定合理的每日排程方案。")
async def fastapi_daily_scheduling_analysis(data: DailySchedulingAnalysis = Body(..., description="日排程分析参数")) -> str:
"""
日排程分析
请求体参数
- **network**: 管网名称或数据库名称
- **start_time**: 分析开始时间
- **pump_control**: 泵控制策略字典
- **reservoir_id**: 水库ID
- **tank_id**: 水箱ID
- **water_plant_output_id**: 水厂出水ID
- **time_delta**: 时间步长可选默认300
用于制定每日供水排程方案
"""
item = data.dict()
return daily_scheduling_simulation(
item["network"],
item["start_time"],
item["pump_control"],
item["reservoir_id"],
item["tank_id"],
item["water_plant_output_id"],
)
# @router.get("/pumpfailure/")
# async def pump_failure_endpoint(network: str, pump_id: str, time: str):
# return pump_failure(network, pump_id, time)
@router.post("/pump-failure-events", summary="泵故障管理", description="记录和管理泵的故障状态,包括故障发生时间和受影响的泵列表。系统将记录故障日志并更新泵状态。")
async def fastapi_pump_failure(data: PumpFailureState = Body(..., description="泵故障状态信息")) -> str:
"""
泵故障管理
请求体参数
- **time**: 故障发生时间
- **pump_status**: 泵状态字典包含第一阶段和第二阶段泵的故障状态
系统将验证泵信息的有效性并更新故障状态文件
"""
item = data.dict()
with open("./pump_failure_message.txt", "a", encoding="utf-8-sig") as f1:
f1.write("[{}] {}\n".format(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), item))
with open("./pump_failure_status.txt", "r", encoding="utf-8-sig") as f2:
lines = f2.readlines()
first_stage_pump_status_dict = json.loads(json.dumps(eval(lines[0])))
second_stage_pump_status_dict = json.loads(json.dumps(eval(lines[-1])))
pump_status_dict = {
"first": first_stage_pump_status_dict,
"second": second_stage_pump_status_dict,
}
status_info = item.copy()
for pump_type in status_info["pump_status"].keys():
if pump_type in pump_status_dict.keys():
if all(
pump_id in pump_status_dict[pump_type].keys()
for pump_id in status_info["pump_status"][pump_type].keys()
):
for pump_id in status_info["pump_status"][pump_type].keys():
pump_status_dict[pump_type][pump_id] = int(
status_info["pump_status"][pump_type][pump_id]
)
else:
return json.dumps("ERROR: Wrong Pump ID")
else:
return json.dumps("ERROR: Wrong Pump Type")
with open("./pump_failure_status.txt", "w", encoding="utf-8-sig") as f2_:
f2_.write(
"{}\n{}".format(pump_status_dict["first"], pump_status_dict["second"])
)
return json.dumps("SUCCESS")
@router.post("/pressure-sensor-placement-sensitivity-calculations", summary="压力传感器放置-灵敏度分析(基础)", description="基于灵敏度分析方法,为指定管网项目确定最优的压力传感器放置位置。此为基础版本。")
async def pressure_sensor_placement_sensitivity_endpoint(
name: str = Query(..., description="管网名称(或数据库名称)"),
scheme_name: str = Query(..., description="放置方案名称"),
sensor_number: int = Query(..., description="传感器数量"),
min_diameter: int = Query(..., description="最小管径限制(毫米)"),
username: str = Query(..., description="用户名"),
):
"""
压力传感器放置-灵敏度分析基础版本
- **name**: 管网名称或数据库名称
- **scheme_name**: 放置方案名称
- **sensor_number**: 传感器数量
- **min_diameter**: 最小管径限制毫米
- **username**: 用户名
基于灵敏度分析方法确定传感器放置位置
"""
return pressure_sensor_placement_sensitivity(
name, scheme_name, sensor_number, min_diameter, username
)
@router.post("/pressure-sensor-placement-sensitivities", summary="压力传感器放置-灵敏度分析(高级)", description="高级版本的压力传感器放置分析,通过JSON请求体提供详细参数。基于灵敏度分析方法确定最优放置位置。")
async def fastapi_pressure_sensor_placement_sensitivity(
data: PressureSensorPlacement = Body(..., description="传感器放置分析参数"),
) -> None:
"""
压力传感器放置-灵敏度分析高级版本
请求体参数
- **name**: 管网名称或数据库名称
- **scheme_name**: 放置方案名称
- **sensor_number**: 传感器数量
- **min_diameter**: 最小管径限制毫米
- **username**: 用户名
基于灵敏度分析方法确定压力传感器的最优放置位置
"""
item = data.dict()
pressure_sensor_placement_sensitivity(
name=item["name"],
scheme_name=item["scheme_name"],
sensor_number=item["sensor_number"],
min_diameter=item["min_diameter"],
username=item["username"],
)
@router.post("/pressure-sensor-placement-kmeans-calculations", summary="压力传感器放置-KMeans聚类分析(基础)", description="基于KMeans聚类算法,为指定管网项目确定压力传感器的最优放置位置。此为基础版本。")
async def pressure_sensor_placement_kmeans_endpoint(
name: str = Query(..., description="管网名称(或数据库名称)"),
scheme_name: str = Query(..., description="放置方案名称"),
sensor_number: int = Query(..., description="传感器数量"),
min_diameter: int = Query(..., description="最小管径限制(毫米)"),
username: str = Query(..., description="用户名"),
):
"""
压力传感器放置-KMeans聚类分析基础版本
- **name**: 管网名称或数据库名称
- **scheme_name**: 放置方案名称
- **sensor_number**: 传感器数量
- **min_diameter**: 最小管径限制毫米
- **username**: 用户名
基于KMeans聚类算法确定传感器放置位置
"""
return pressure_sensor_placement_kmeans(
name, scheme_name, sensor_number, min_diameter, username
)
@router.post("/pressure-sensor-placement-kmeans", summary="压力传感器放置-KMeans聚类分析(高级)", description="高级版本的压力传感器放置分析,通过JSON请求体提供详细参数。基于KMeans聚类算法确定最优放置位置。")
async def fastapi_pressure_sensor_placement_kmeans(
data: PressureSensorPlacement = Body(..., description="传感器放置分析参数"),
) -> None:
"""
压力传感器放置-KMeans聚类分析高级版本
请求体参数
- **name**: 管网名称或数据库名称
- **scheme_name**: 放置方案名称
- **sensor_number**: 传感器数量
- **min_diameter**: 最小管径限制毫米
- **username**: 用户名
基于KMeans聚类算法确定压力传感器的最优放置位置
"""
item = data.dict()
pressure_sensor_placement_kmeans(
name=item["name"],
scheme_name=item["scheme_name"],
sensor_number=item["sensor_number"],
min_diameter=item["min_diameter"],
username=item["username"],
)
@router.post("/sensor-placement-schemes", summary="传感器放置方案创建", description="创建新的传感器放置方案,支持灵敏度分析和KMeans聚类两种方法。根据指定的方法自动计算最优的传感器放置位置。")
async def fastapi_pressure_sensor_placement(
network: str = Query(..., description="管网名称(或数据库名称)"),
scheme_name: str = Query(..., description="放置方案名称"),
sensor_type: str = Query(..., description="传感器类型"),
method: str = Query(..., description="放置方法('sensitivity''kmeans'"),
sensor_count: int = Query(..., description="传感器数量"),
min_diameter: int = Query(0, description="最小管径限制(毫米),默认0"),
user_name: str = Query(..., description="用户名"),
) -> str:
"""
传感器放置方案创建
- **network**: 管网名称或数据库名称
- **scheme_name**: 放置方案名称
- **sensor_type**: 传感器类型
- **method**: 放置方法'sensitivity''kmeans'
- **sensor_count**: 传感器数量
- **min_diameter**: 最小管径限制毫米默认0
- **user_name**: 用户名
支持两种放置方法
- sensitivity: 基于灵敏度分析
- kmeans: 基于KMeans聚类
"""
if method not in ["sensitivity", "kmeans"]:
raise HTTPException(
status_code=400, detail="Invalid method. Must be 'sensitivity' or 'kmeans'"
)
if method == "sensitivity":
pressure_sensor_placement_sensitivity(
name=network,
scheme_name=scheme_name,
sensor_number=sensor_count,
min_diameter=min_diameter,
username=user_name,
)
elif method == "kmeans":
pressure_sensor_placement_kmeans(
name=network,
scheme_name=scheme_name,
sensor_number=sensor_count,
min_diameter=min_diameter,
username=user_name,
) )
return "success" return "success"
@router.post("/simulation-runs", summary="手动运行日期指定模拟", description="根据指定的开始时间和持续时间,手动运行水力模拟。开始时间必须是显式带时区的 ISO 8601 / RFC3339 时间。") @router.post("/simulation-runs", summary="手动运行日期指定模拟", description="根据指定的开始时间和持续时间,手动运行水力模拟。开始时间必须是显式带时区的 ISO 8601 / RFC3339 时间。")
async def fastapi_run_simulation_manually_by_date( def fastapi_run_simulation_manually_by_date(
data: RunSimulationManuallyByDate = Body(..., description="模拟运行参数"), data: RunSimulationManuallyByDate = Body(..., description="模拟运行参数"),
) -> dict[str, str]: ) -> dict[str, str]:
""" """
@@ -828,31 +424,6 @@ async def fastapi_run_simulation_manually_by_date(
""" """
item = data.model_dump() item = data.model_dump()
try: try:
simulation.query_corresponding_element_id_and_query_id(item["name"])
simulation.query_corresponding_pattern_id_and_query_id(item["name"])
region_result = simulation.query_non_realtime_region(item["name"])
globals.source_outflow_region_id = simulation.get_source_outflow_region_id(
item["name"], region_result
)
globals.realtime_region_pipe_flow_and_demand_id = (
simulation.query_realtime_region_pipe_flow_and_demand_id(
item["name"], region_result
)
)
globals.pipe_flow_region_patterns = simulation.query_pipe_flow_region_patterns(
item["name"]
)
globals.non_realtime_region_patterns = (
simulation.query_non_realtime_region_patterns(item["name"], region_result)
)
(
globals.source_outflow_region_patterns,
globals.realtime_region_pipe_flow_and_demand_patterns,
) = simulation.get_realtime_region_patterns(
item["name"],
globals.source_outflow_region_id,
globals.realtime_region_pipe_flow_and_demand_id,
)
start_time = parse_utc_time(item["start_time"], field_name="start_time") start_time = parse_utc_time(item["start_time"], field_name="start_time")
run_simulation_manually_by_date( run_simulation_manually_by_date(
item["name"], start_time, item["duration"] item["name"], start_time, item["duration"]
-199
View File
@@ -1,199 +0,0 @@
from fastapi import APIRouter, Depends, Request, Query
from app.auth.permissions import SIMULATION_RUN, require_permission
from app.services.tjnetwork import (
ChangeSet,
get_current_operation,
execute_undo,
execute_redo,
list_snapshot,
have_snapshot,
have_snapshot_for_operation,
have_snapshot_for_current_operation,
take_snapshot_for_operation,
take_snapshot_for_current_operation,
take_snapshot,
pick_snapshot,
pick_operation,
sync_with_server,
execute_batch_commands,
execute_batch_command,
get_restore_operation,
set_restore_operation,
)
router = APIRouter()
@router.get("/current-operation-ids", summary="获取当前操作ID", description="获取网络当前的操作ID")
async def get_current_operation_id_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> int:
"""
获取当前操作ID
返回网络当前正在执行的操作ID
"""
return get_current_operation(network)
@router.post("/undos", summary="撤销操作", description="撤销网络上最后的一个操作")
async def undo_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")):
"""
撤销操作
撤销网络上最近执行的一个操作
"""
return execute_undo(network)
@router.post("/redos", summary="重做操作", description="重做网络上被撤销的操作")
async def redo_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")):
"""
重做操作
重做网络上被撤销的操作
"""
return execute_redo(network)
@router.get("/snapshots", summary="获取快照列表", description="获取网络中的所有快照")
async def list_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[tuple[int, str]]:
"""
获取快照列表
返回网络中所有可用的快照及其信息
"""
return list_snapshot(network)
@router.get("/snapshots/existence", summary="检查快照是否存在", description="检查指定标签的快照是否存在")
async def have_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签")) -> bool:
"""
检查快照是否存在
返回指定标签的快照是否存在
"""
return have_snapshot(network, tag)
@router.get("/snapshot-for-operations", summary="检查操作快照是否存在", description="检查指定操作ID的快照是否存在")
async def have_snapshot_for_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), operation: int = Query(..., description="操作ID")) -> bool:
"""
检查操作快照是否存在
返回指定操作ID的快照是否存在
"""
return have_snapshot_for_operation(network, operation)
@router.get("/snapshot-for-current-operations", summary="检查当前操作快照是否存在", description="检查当前操作的快照是否存在")
async def have_snapshot_for_current_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> bool:
"""
检查当前操作快照是否存在
返回当前操作的快照是否存在
"""
return have_snapshot_for_current_operation(network)
@router.post("/snapshot-for-operations", summary="为操作创建快照", description="为指定的操作创建快照")
async def take_snapshot_for_operation_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
operation: int = Query(..., description="操作ID"),
tag: str = Query(..., description="快照标签")
) -> None:
"""
为操作创建快照
为指定操作创建一个带标签的快照
"""
return take_snapshot_for_operation(network, operation, tag)
@router.post("/snapshot-for-current-operations", summary="为当前操作创建快照", description="为当前操作创建快照")
async def take_snapshot_for_current_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签")) -> None:
"""
为当前操作创建快照
为网络当前操作创建一个快照
"""
return take_snapshot_for_current_operation(network, tag)
@router.post("/snapshots", summary="创建快照", description="为网络创建一个快照")
async def take_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签")) -> None:
"""
创建快照
为网络创建一个带标签的快照
"""
return take_snapshot(network, tag)
@router.patch("/snapshots", summary="选择快照", description="选择并恢复到指定的快照", response_model=None)
async def pick_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签"), discard: bool = Query(False, description="是否丢弃当前更改")) -> ChangeSet:
"""
选择快照
选择并恢复到指定的快照
"""
return pick_snapshot(network, tag, discard)
@router.patch("/operations", summary="选择操作", description="选择并恢复到指定的操作", response_model=None)
async def pick_operation_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
operation: int = Query(..., description="操作ID"),
discard: bool = Query(False, description="是否丢弃当前更改")
) -> ChangeSet:
"""
选择操作
选择并恢复到指定的操作
"""
return pick_operation(network, operation, discard)
@router.post("/with-servers", summary="与服务器同步", description="将网络与服务器同步到指定操作", response_model=None)
async def sync_with_server_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
operation: int = Query(..., description="目标操作ID"),
_=Depends(require_permission(SIMULATION_RUN)),
) -> ChangeSet:
"""
与服务器同步
将网络与服务器同步到指定的操作
"""
return sync_with_server(network, operation)
@router.post("/network-command-batches", summary="执行批量命令", description="执行多个网络操作命令", response_model=None)
async def execute_batch_commands_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), req: Request = None) -> ChangeSet:
"""
执行批量命令
在网络上执行多个操作命令
"""
jo_root = await req.json()
cs: ChangeSet = ChangeSet()
cs.operations = jo_root["operations"]
rcs = execute_batch_commands(network, cs)
return rcs
@router.post("/network-command-batches/compressed", summary="执行压缩批量命令", description="执行压缩的批量命令", response_model=None)
async def execute_compressed_batch_commands_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""
执行压缩批量命令
执行压缩格式的批量命令
"""
jo_root = await req.json()
cs: ChangeSet = ChangeSet()
cs.operations = jo_root["operations"]
return execute_batch_command(network, cs)
@router.get("/restore-operations", summary="获取恢复操作ID", description="获取网络的恢复操作ID")
async def get_restore_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> int:
"""
获取恢复操作ID
返回网络的恢复操作ID
"""
return get_restore_operation(network)
@router.patch("/restore-operations", summary="设置恢复操作ID", description="设置网络的恢复操作ID")
async def set_restore_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), operation: int = Query(..., description="操作ID")) -> None:
"""
设置恢复操作ID
设置网络的恢复操作ID
"""
return set_restore_operation(network, operation)
@@ -0,0 +1,61 @@
from datetime import datetime
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query
from psycopg import AsyncConnection
from app.infra.db.timescaledb.repositories.analysis import AnalysisResultsRepository
from .dependencies import get_timescale_connection
router = APIRouter()
@router.get("/timeseries/analysis/runs/{run_id}/nodes/{node_id}")
async def get_analysis_node_series(
run_id: UUID,
node_id: str,
start_time: datetime = Query(...),
end_time: datetime = Query(...),
field: str = Query(...),
conn: AsyncConnection = Depends(get_timescale_connection),
):
try:
return await AnalysisResultsRepository.get_node_series(
conn, run_id, node_id, start_time, end_time, field
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.get("/timeseries/analysis/runs/{run_id}/links/{link_id}")
async def get_analysis_link_series(
run_id: UUID,
link_id: str,
start_time: datetime = Query(...),
end_time: datetime = Query(...),
field: str = Query(...),
conn: AsyncConnection = Depends(get_timescale_connection),
):
try:
return await AnalysisResultsRepository.get_link_series(
conn, run_id, link_id, start_time, end_time, field
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.get("/timeseries/analysis/runs/{run_id}/values")
async def get_analysis_values_at_time(
run_id: UUID,
result_time: datetime = Query(...),
element_type: str = Query(..., pattern="^(node|link)$"),
field: str = Query(...),
conn: AsyncConnection = Depends(get_timescale_connection),
):
try:
return await AnalysisResultsRepository.get_values_at_time(
conn, run_id, element_type, result_time, field
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
+21 -27
View File
@@ -1,8 +1,9 @@
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from datetime import datetime from datetime import datetime
from psycopg import AsyncConnection from psycopg import AsyncConnection
from uuid import UUID
from app.infra.db.timescaledb.composite_queries import CompositeQueries from app.services.timeseries_analysis import TimeseriesAnalysisService
from .dependencies import get_timescale_connection, get_postgres_connection from .dependencies import get_timescale_connection, get_postgres_connection
router = APIRouter() router = APIRouter()
@@ -13,8 +14,7 @@ async def get_scada_associated_simulation_data(
start_time: datetime = Query(..., description="查询开始时间"), start_time: datetime = Query(..., description="查询开始时间"),
end_time: datetime = Query(..., description="查询结束时间"), end_time: datetime = Query(..., description="查询结束时间"),
device_ids: str = Query(..., description="SCADA设备ID列表,逗号分隔"), device_ids: str = Query(..., description="SCADA设备ID列表,逗号分隔"),
scheme_type: str = Query(None, description="方案类型,若为空查询实时数据"), run_id: UUID | None = Query(None, description="分析运行 ID为空查询实时数据"),
scheme_name: str = Query(None, description="方案名称,若为空则查询实时数据"),
timescale_conn: AsyncConnection = Depends(get_timescale_connection), timescale_conn: AsyncConnection = Depends(get_timescale_connection),
postgres_conn: AsyncConnection = Depends(get_postgres_connection), postgres_conn: AsyncConnection = Depends(get_postgres_connection),
): ):
@@ -22,14 +22,13 @@ async def get_scada_associated_simulation_data(
获取SCADA关联的link/node模拟值 获取SCADA关联的link/node模拟值
根据传入的SCADA device_ids找到关联的link/node 根据传入的SCADA device_ids找到关联的link/node
并根据对应的type查询对应的模拟数据支持查询实时或方案数据 并根据对应的type查询对应的模拟数据支持查询实时或分析运行数据
Args: Args:
start_time: 查询开始时间 start_time: 查询开始时间
end_time: 查询结束时间 end_time: 查询结束时间
device_ids: SCADA设备ID列表用逗号分隔 device_ids: SCADA设备ID列表用逗号分隔
scheme_type: 方案类型若为空则查询实时数据 run_id: 分析运行 ID若为空则查询实时数据
scheme_name: 方案名称若为空则查询实时数据
timescale_conn: TimescaleDB连接 timescale_conn: TimescaleDB连接
postgres_conn: PostgreSQL连接 postgres_conn: PostgreSQL连接
@@ -46,19 +45,18 @@ async def get_scada_associated_simulation_data(
else [] else []
) )
if scheme_type and scheme_name: if run_id is not None:
result = await CompositeQueries.get_scada_associated_scheme_simulation_data( result = await TimeseriesAnalysisService.get_scada_associated_analysis_simulation_data(
timescale_conn, timescale_conn,
postgres_conn, postgres_conn,
device_ids_list, device_ids_list,
start_time, start_time,
end_time, end_time,
scheme_type, run_id,
scheme_name,
) )
else: else:
result = ( result = (
await CompositeQueries.get_scada_associated_realtime_simulation_data( await TimeseriesAnalysisService.get_scada_associated_realtime_simulation_data(
timescale_conn, timescale_conn,
postgres_conn, postgres_conn,
device_ids_list, device_ids_list,
@@ -80,23 +78,21 @@ async def get_feature_simulation_data(
feature_infos: str = Query( feature_infos: str = Query(
..., description="特征信息,格式: id1:type1,id2:type2type为pipe(管道)或junction(节点)" ..., description="特征信息,格式: id1:type1,id2:type2type为pipe(管道)或junction(节点)"
), ),
scheme_type: str = Query(None, description="方案类型,若为空查询实时数据"), run_id: UUID | None = Query(None, description="分析运行 ID为空查询实时数据"),
scheme_name: str = Query(None, description="方案名称,若为空则查询实时数据"),
timescale_conn: AsyncConnection = Depends(get_timescale_connection), timescale_conn: AsyncConnection = Depends(get_timescale_connection),
): ):
""" """
获取link/node模拟值 获取link/node模拟值
根据传入的featureInfos找到关联的link/node 根据传入的featureInfos找到关联的link/node
并根据对应的type查询对应的模拟数据支持查询实时或方案数据 并根据对应的type查询对应的模拟数据支持查询实时或分析运行数据
Args: Args:
start_time: 查询开始时间 start_time: 查询开始时间
end_time: 查询结束时间 end_time: 查询结束时间
feature_infos: 格式为 "element_id1:type1,element_id2:type2" feature_infos: 格式为 "element_id1:type1,element_id2:type2"
例如: "P1:pipe,J1:junction" 例如: "P1:pipe,J1:junction"
scheme_type: 方案类型若为空则查询实时数据 run_id: 分析运行 ID若为空则查询实时数据
scheme_name: 方案名称若为空则查询实时数据
timescale_conn: TimescaleDB连接 timescale_conn: TimescaleDB连接
Returns: Returns:
@@ -119,17 +115,16 @@ async def get_feature_simulation_data(
if not feature_infos_list: if not feature_infos_list:
raise HTTPException(status_code=400, detail="feature_infos cannot be empty") raise HTTPException(status_code=400, detail="feature_infos cannot be empty")
if scheme_type and scheme_name: if run_id is not None:
result = await CompositeQueries.get_scheme_simulation_data( result = await TimeseriesAnalysisService.get_analysis_simulation_data(
timescale_conn, timescale_conn,
feature_infos_list, feature_infos_list,
start_time, start_time,
end_time, end_time,
scheme_type, run_id,
scheme_name,
) )
else: else:
result = await CompositeQueries.get_realtime_simulation_data( result = await TimeseriesAnalysisService.get_realtime_simulation_data(
timescale_conn, timescale_conn,
feature_infos_list, feature_infos_list,
start_time, start_time,
@@ -173,7 +168,7 @@ async def get_element_associated_scada_data(
HTTPException: 当查询参数无效时返回400错误未找到关联数据返回404错误 HTTPException: 当查询参数无效时返回400错误未找到关联数据返回404错误
""" """
try: try:
result = await CompositeQueries.get_element_associated_scada_data( result = await TimeseriesAnalysisService.get_element_associated_scada_data(
timescale_conn, postgres_conn, element_id, start_time, end_time, use_cleaned timescale_conn, postgres_conn, element_id, start_time, end_time, use_cleaned
) )
if result is None: if result is None:
@@ -221,7 +216,7 @@ async def clean_scada_data(
if device_ids if device_ids
else [] else []
) )
return await CompositeQueries.clean_scada_data( return await TimeseriesAnalysisService.clean_scada_data(
timescale_conn, postgres_conn, device_ids_list, start_time, end_time timescale_conn, postgres_conn, device_ids_list, start_time, end_time
) )
except ValueError as e: except ValueError as e:
@@ -231,8 +226,8 @@ async def clean_scada_data(
@router.get("/pipeline-health-predictions", summary="预测管道健康状况") @router.get("/pipeline-health-predictions", summary="预测管道健康状况")
async def predict_pipeline_health( async def predict_pipeline_health(
query_time: datetime = Query(..., description="查询时间"), query_time: datetime = Query(..., description="查询时间"),
network_name: str = Query(..., description="管网名称(或数据库名称)"),
timescale_conn: AsyncConnection = Depends(get_timescale_connection), timescale_conn: AsyncConnection = Depends(get_timescale_connection),
postgres_conn: AsyncConnection = Depends(get_postgres_connection),
): ):
""" """
预测管道健康状况 预测管道健康状况
@@ -242,7 +237,6 @@ async def predict_pipeline_health(
Args: Args:
query_time: 查询时间 query_time: 查询时间
network_name: 管网名称或数据库名称
timescale_conn: TimescaleDB连接 timescale_conn: TimescaleDB连接
Returns: Returns:
@@ -252,8 +246,8 @@ async def predict_pipeline_health(
HTTPException: 当模型文件不存在返回404错误其他错误返回400或500错误 HTTPException: 当模型文件不存在返回404错误其他错误返回400或500错误
""" """
try: try:
return await CompositeQueries.predict_pipeline_health( return await TimeseriesAnalysisService.predict_pipeline_health(
timescale_conn, network_name, query_time timescale_conn, postgres_conn, query_time
) )
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
+31 -4
View File
@@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
from typing import List from typing import List
from datetime import datetime from datetime import datetime
from psycopg import AsyncConnection from psycopg import AsyncConnection
from pydantic import BaseModel
from app.infra.db.timescaledb.repositories.realtime import RealtimeRepository from app.infra.db.timescaledb.repositories.realtime import RealtimeRepository
from .dependencies import get_timescale_connection from .dependencies import get_timescale_connection
@@ -13,9 +14,31 @@ TIME_RANGE_START_DESC = f"时间范围开始时间。{TIME_WITH_TZ_DESC}"
TIME_RANGE_END_DESC = f"时间范围结束时间。{TIME_WITH_TZ_DESC}" TIME_RANGE_END_DESC = f"时间范围结束时间。{TIME_WITH_TZ_DESC}"
class RealtimeLinkBatchItem(BaseModel):
time: datetime
id: str
flow: float | None = None
friction: float | None = None
headloss: float | None = None
quality: float | None = None
reaction: float | None = None
setting: float | None = None
status: float | None = None
velocity: float | None = None
class RealtimeNodeBatchItem(BaseModel):
time: datetime
id: str
actual_demand: float | None = None
total_head: float | None = None
pressure: float | None = None
quality: float | None = None
@router.post("/timeseries/realtime/links/batches", status_code=201, summary="批量插入实时管道数据") @router.post("/timeseries/realtime/links/batches", status_code=201, summary="批量插入实时管道数据")
async def insert_realtime_links( async def insert_realtime_links(
data: List[dict] = Body(..., description="管道数据列表,每项包含管道ID、时间戳等信息"), data: List[RealtimeLinkBatchItem] = Body(..., description="同一时间点的管道快照数据"),
conn: AsyncConnection = Depends(get_timescale_connection) conn: AsyncConnection = Depends(get_timescale_connection)
): ):
""" """
@@ -29,7 +52,9 @@ async def insert_realtime_links(
Returns: Returns:
插入成功的记录数 插入成功的记录数
""" """
await RealtimeRepository.insert_links_batch(conn, data) await RealtimeRepository.insert_links_batch(
conn, [item.model_dump() for item in data]
)
return {"message": f"Inserted {len(data)} records"} return {"message": f"Inserted {len(data)} records"}
@@ -119,7 +144,7 @@ async def update_realtime_link_field(
@router.post("/timeseries/realtime/nodes/batches", status_code=201, summary="批量插入实时节点数据") @router.post("/timeseries/realtime/nodes/batches", status_code=201, summary="批量插入实时节点数据")
async def insert_realtime_nodes( async def insert_realtime_nodes(
data: List[dict] = Body(..., description="节点数据列表,每项包含节点ID、时间戳等信息"), data: List[RealtimeNodeBatchItem] = Body(..., description="同一时间点的节点快照数据"),
conn: AsyncConnection = Depends(get_timescale_connection) conn: AsyncConnection = Depends(get_timescale_connection)
): ):
""" """
@@ -133,7 +158,9 @@ async def insert_realtime_nodes(
Returns: Returns:
插入成功的记录数 插入成功的记录数
""" """
await RealtimeRepository.insert_nodes_batch(conn, data) await RealtimeRepository.insert_nodes_batch(
conn, [item.model_dump() for item in data]
)
return {"message": f"Inserted {len(data)} records"} return {"message": f"Inserted {len(data)} records"}
+40 -3
View File
@@ -2,17 +2,41 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
from typing import List from typing import List
from datetime import datetime from datetime import datetime
from psycopg import AsyncConnection from psycopg import AsyncConnection
from pydantic import BaseModel, Field, field_validator
from app.infra.db.postgresql.scada import ScadaInfoRepository
from app.infra.db.timescaledb.repositories.scada import ScadaRepository from app.infra.db.timescaledb.repositories.scada import ScadaRepository
from .dependencies import get_timescale_connection from .dependencies import get_postgres_connection, get_timescale_connection
router = APIRouter() router = APIRouter()
SCADA_BATCH_MAX_ITEMS = 10_000
class ScadaReadingBatchItem(BaseModel):
time: datetime
device_id: str = Field(min_length=1)
monitored_value: float | None = None
cleaned_value: float | None = None
@field_validator("device_id")
@classmethod
def normalize_device_id(cls, value: str) -> str:
normalized = value.strip()
if not normalized:
raise ValueError("device_id must not be blank")
return normalized
@router.post("/timeseries/scada-readings/batches", status_code=201, summary="批量插入SCADA监测数据") @router.post("/timeseries/scada-readings/batches", status_code=201, summary="批量插入SCADA监测数据")
async def insert_scada_data( async def insert_scada_data(
data: List[dict] = Body(..., description="SCADA设备监测数据列表"), data: List[ScadaReadingBatchItem] = Body(
...,
min_length=1,
max_length=SCADA_BATCH_MAX_ITEMS,
description="SCADA设备监测数据列表",
),
conn: AsyncConnection = Depends(get_timescale_connection), conn: AsyncConnection = Depends(get_timescale_connection),
postgres_conn: AsyncConnection = Depends(get_postgres_connection),
): ):
""" """
批量插入SCADA监测数据 批量插入SCADA监测数据
@@ -25,7 +49,20 @@ async def insert_scada_data(
Returns: Returns:
插入成功的记录数 插入成功的记录数
""" """
await ScadaRepository.insert_scada_batch(conn, data) rows = [item.model_dump() for item in data]
requested_ids = list(dict.fromkeys(item["device_id"] for item in rows))
existing_ids = await ScadaInfoRepository.get_existing_device_ids(
postgres_conn, requested_ids
)
missing_ids = [
device_id for device_id in requested_ids if device_id not in existing_ids
]
if missing_ids:
raise HTTPException(
status_code=422,
detail=f"SCADA devices do not exist in BizDB: {', '.join(missing_ids)}",
)
await ScadaRepository.insert_scada_batch(conn, rows)
return {"message": f"Inserted {len(data)} records"} return {"message": f"Inserted {len(data)} records"}
-391
View File
@@ -1,391 +0,0 @@
from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
from typing import List
from datetime import datetime
from psycopg import AsyncConnection
from app.infra.db.timescaledb.repositories.scheme import SchemeRepository
from .dependencies import get_timescale_connection
router = APIRouter()
@router.post("/timeseries/schemes/links/batches", status_code=201, summary="批量插入方案管道数据")
async def insert_scheme_links(
data: List[dict] = Body(..., description="方案管道数据列表"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
批量插入方案管道数据
将特定方案的管道模拟数据批量插入时间序列数据库
Args:
data: 方案管道数据列表
Returns:
插入成功的记录数
"""
await SchemeRepository.insert_links_batch(conn, data)
return {"message": f"Inserted {len(data)} records"}
@router.get("/timeseries/schemes/links", summary="查询方案管道数据")
async def get_scheme_links(
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
start_time: datetime = Query(..., description="查询开始时间"),
end_time: datetime = Query(..., description="查询结束时间"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
查询指定方案和时间范围内的管道数据
根据方案和时间范围查询管道的模拟值
Args:
scheme_type: 方案类型
scheme_name: 方案名称
start_time: 查询开始时间
end_time: 查询结束时间
Returns:
方案管道数据列表
"""
return await SchemeRepository.get_links_by_scheme_and_time_range(
conn, scheme_type, scheme_name, start_time, end_time
)
@router.get("/timeseries/schemes/links/{link_id}/field", summary="查询方案管道字段数据")
async def get_scheme_link_field(
link_id: str = Path(..., description="管道ID"),
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
start_time: datetime = Query(..., description="查询开始时间"),
end_time: datetime = Query(..., description="查询结束时间"),
field: str = Query(..., description="要查询的字段名称"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
查询指定方案管道的特定字段数据
查询特定方案中指定管道在时间范围内的特定字段值
Args:
link_id: 管道ID
scheme_type: 方案类型
scheme_name: 方案名称
start_time: 查询开始时间
end_time: 查询结束时间
field: 字段名称
Returns:
字段数据列表
Raises:
HTTPException: 当查询参数无效时返回400错误
"""
try:
return await SchemeRepository.get_link_field_by_scheme_and_time_range(
conn, scheme_type, scheme_name, start_time, end_time, link_id, field
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.patch("/timeseries/schemes/links/{link_id}/field", summary="更新方案管道字段")
async def update_scheme_link_field(
link_id: str = Path(..., description="管道ID"),
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
time: datetime = Query(..., description="更新数据的时间戳"),
field: str = Query(..., description="要更新的字段名称"),
value: float = Query(..., description="更新的字段值"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
更新指定方案管道的字段值
更新特定方案中指定管道在某个时间的字段数据
Args:
link_id: 管道ID
scheme_type: 方案类型
scheme_name: 方案名称
time: 数据时间戳
field: 字段名称
value: 字段新值
Returns:
更新结果信息
Raises:
HTTPException: 当字段不存在或更新失败时返回400错误
"""
try:
await SchemeRepository.update_link_field(
conn, time, scheme_type, scheme_name, link_id, field, value
)
return {"message": "Updated successfully"}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/timeseries/schemes/links", summary="删除方案管道数据")
async def delete_scheme_links(
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
start_time: datetime = Query(..., description="删除开始时间"),
end_time: datetime = Query(..., description="删除结束时间"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
删除指定方案和时间范围内的管道数据
删除在指定方案和时间范围内的所有管道模拟数据
Args:
scheme_type: 方案类型
scheme_name: 方案名称
start_time: 删除开始时间
end_time: 删除结束时间
Returns:
删除结果信息
"""
await SchemeRepository.delete_links_by_scheme_and_time_range(
conn, scheme_type, scheme_name, start_time, end_time
)
return {"message": "Deleted successfully"}
@router.post("/timeseries/schemes/nodes/batches", status_code=201, summary="批量插入方案节点数据")
async def insert_scheme_nodes(
data: List[dict] = Body(..., description="方案节点数据列表"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
批量插入方案节点数据
将特定方案的节点模拟数据批量插入时间序列数据库
Args:
data: 方案节点数据列表
Returns:
插入成功的记录数
"""
await SchemeRepository.insert_nodes_batch(conn, data)
return {"message": f"Inserted {len(data)} records"}
@router.get("/timeseries/schemes/nodes/{node_id}/field", summary="查询方案节点字段数据")
async def get_scheme_node_field(
node_id: str = Path(..., description="节点ID"),
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
start_time: datetime = Query(..., description="查询开始时间"),
end_time: datetime = Query(..., description="查询结束时间"),
field: str = Query(..., description="要查询的字段名称"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
查询指定方案节点的特定字段数据
查询特定方案中指定节点在时间范围内的特定字段值
Args:
node_id: 节点ID
scheme_type: 方案类型
scheme_name: 方案名称
start_time: 查询开始时间
end_time: 查询结束时间
field: 字段名称
Returns:
字段数据列表
Raises:
HTTPException: 当查询参数无效时返回400错误
"""
try:
return await SchemeRepository.get_node_field_by_scheme_and_time_range(
conn, scheme_type, scheme_name, start_time, end_time, node_id, field
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.patch("/timeseries/schemes/nodes/{node_id}/field", summary="更新方案节点字段")
async def update_scheme_node_field(
node_id: str = Path(..., description="节点ID"),
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
time: datetime = Query(..., description="更新数据的时间戳"),
field: str = Query(..., description="要更新的字段名称"),
value: float = Query(..., description="更新的字段值"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
更新指定方案节点的字段值
更新特定方案中指定节点在某个时间的字段数据
Args:
node_id: 节点ID
scheme_type: 方案类型
scheme_name: 方案名称
time: 数据时间戳
field: 字段名称
value: 字段新值
Returns:
更新结果信息
Raises:
HTTPException: 当字段不存在或更新失败时返回400错误
"""
try:
await SchemeRepository.update_node_field(
conn, time, scheme_type, scheme_name, node_id, field, value
)
return {"message": "Updated successfully"}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/timeseries/schemes/nodes", summary="删除方案节点数据")
async def delete_scheme_nodes(
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
start_time: datetime = Query(..., description="删除开始时间"),
end_time: datetime = Query(..., description="删除结束时间"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
删除指定方案和时间范围内的节点数据
删除在指定方案和时间范围内的所有节点模拟数据
Args:
scheme_type: 方案类型
scheme_name: 方案名称
start_time: 删除开始时间
end_time: 删除结束时间
Returns:
删除结果信息
"""
await SchemeRepository.delete_nodes_by_scheme_and_time_range(
conn, scheme_type, scheme_name, start_time, end_time
)
return {"message": "Deleted successfully"}
@router.post("/timeseries/schemes/simulation-results", status_code=201, summary="存储方案模拟结果")
async def store_scheme_simulation_result(
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
node_result_list: List[dict] = Body(..., description="节点模拟结果列表"),
link_result_list: List[dict] = Body(..., description="管道模拟结果列表"),
result_start_time: str = Query(..., description="模拟结果开始时间"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
存储方案模拟结果到时间序列数据库
将特定方案的节点和管道模拟计算结果批量存储到TimescaleDB数据库
Args:
scheme_type: 方案类型
scheme_name: 方案名称
node_result_list: 节点模拟结果列表
link_result_list: 管道模拟结果列表
result_start_time: 模拟结果对应的起始时间
Returns:
存储结果信息
"""
await SchemeRepository.store_scheme_simulation_result(
conn,
scheme_type,
scheme_name,
node_result_list,
link_result_list,
result_start_time,
)
return {"message": "Scheme simulation results stored successfully"}
@router.get(
"/timeseries/schemes/records", summary="按方案、时间和属性查询数据"
)
async def query_scheme_records_by_scheme_time_property(
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
query_time: str = Query(..., description="查询时间"),
type: str = Query(..., description="元素类型,pipe(管道)或 junction(节点)"),
property: str = Query(..., description="要查询的属性名称"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
按指定方案时间和属性查询所有方案数据
查询在特定方案和时间点所有指定类型元素的特定属性值
Args:
scheme_type: 方案类型
scheme_name: 方案名称
query_time: 查询时间
type: 元素类型pipe或junction
property: 属性名称
Returns:
查询结果列表
Raises:
HTTPException: 当查询参数无效时返回400错误
"""
try:
results = await SchemeRepository.query_all_record_by_scheme_time_property(
conn, scheme_type, scheme_name, query_time, type, property
)
return {"results": results}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/timeseries/schemes/simulation-results", summary="按ID和时间查询方案模拟数据")
async def query_scheme_simulation_by_id_time(
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
id: str = Query(..., description="元素ID(管道ID或节点ID"),
type: str = Query(..., description="元素类型,pipe(管道)或 junction(节点)"),
query_time: str = Query(..., description="查询时间"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
按指定ID和时间查询方案模拟结果
查询特定方案中的元素在某一时间点的模拟数据
Args:
scheme_type: 方案类型
scheme_name: 方案名称
id: 元素ID
type: 元素类型pipe或junction
query_time: 查询时间
Returns:
模拟结果数据
Raises:
HTTPException: 当查询参数无效时返回400错误
"""
try:
result = await SchemeRepository.query_scheme_simulation_result_by_id_time(
conn, scheme_type, scheme_name, id, type, query_time
)
return {"result": result}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
+12 -12
View File
@@ -11,6 +11,7 @@ from fastapi import APIRouter, Depends, Query
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.routing import APIRoute from fastapi.routing import APIRoute
from pydantic import BaseModel, JsonValue, create_model from pydantic import BaseModel, JsonValue, create_model
from starlette.concurrency import run_in_threadpool
from starlette.responses import Response from starlette.responses import Response
from app.api.problem_details import ProblemDetails from app.api.problem_details import ProblemDetails
@@ -38,6 +39,14 @@ class Page(BaseModel, Generic[T]):
offset: int offset: int
async def _call_endpoint(endpoint, *args, **kwargs):
"""Call async handlers directly and offload synchronous handlers."""
if inspect.iscoroutinefunction(endpoint):
return await endpoint(*args, **kwargs)
result = await run_in_threadpool(endpoint, *args, **kwargs)
return await result if inspect.isawaitable(result) else result
_NAME_IS_NETWORK = { _NAME_IS_NETWORK = {
"pressure_sensor_placement_sensitivity_endpoint", "pressure_sensor_placement_sensitivity_endpoint",
"pressure_sensor_placement_kmeans_endpoint", "pressure_sensor_placement_kmeans_endpoint",
@@ -59,7 +68,6 @@ _TIMESCALE_ROUTED_ENDPOINT_MODULES = {
"app.api.v1.endpoints.leakage", "app.api.v1.endpoints.leakage",
"app.api.v1.endpoints.simulation", "app.api.v1.endpoints.simulation",
} }
_TIMESCALE_ROUTED_ENDPOINT_NAMES = {"open_project_endpoint"}
def _clean_name(name: str) -> str: def _clean_name(name: str) -> str:
@@ -183,10 +191,7 @@ def _with_header_project_context(endpoint, route_name: str):
if model_has_username: if model_has_username:
kwargs.pop(injected_user_name, None) kwargs.pop(injected_user_name, None)
with activate_project_routing(project_routing): with activate_project_routing(project_routing):
result = endpoint(*args, **kwargs) return await _call_endpoint(endpoint, *args, **kwargs)
if inspect.isawaitable(result):
return await result
return result
parameters = [] parameters = []
for name, parameter in signature.parameters.items(): for name, parameter in signature.parameters.items():
@@ -206,7 +211,6 @@ def _with_header_project_context(endpoint, route_name: str):
get_project_simulation_routing get_project_simulation_routing
if ( if (
endpoint.__module__ in _TIMESCALE_ROUTED_ENDPOINT_MODULES endpoint.__module__ in _TIMESCALE_ROUTED_ENDPOINT_MODULES
or endpoint.__name__ in _TIMESCALE_ROUTED_ENDPOINT_NAMES
) )
else get_project_business_routing else get_project_business_routing
) )
@@ -262,9 +266,7 @@ def _with_pagination(endpoint):
else: else:
limit = kwargs.pop("_rest_limit") limit = kwargs.pop("_rest_limit")
offset = kwargs.pop("_rest_offset") offset = kwargs.pop("_rest_offset")
result = endpoint(*args, **kwargs) result = await _call_endpoint(endpoint, *args, **kwargs)
if inspect.isawaitable(result):
result = await result
if not isinstance(result, list): if not isinstance(result, list):
return result return result
if handler_handles_pagination: if handler_handles_pagination:
@@ -313,9 +315,7 @@ def _with_jsonable_response(endpoint):
@wraps(endpoint) @wraps(endpoint)
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
result = endpoint(*args, **kwargs) result = await _call_endpoint(endpoint, *args, **kwargs)
if inspect.isawaitable(result):
result = await result
if isinstance(result, Response): if isinstance(result, Response):
return result return result
return jsonable_encoder(result) return jsonable_encoder(result)
+7 -35
View File
@@ -7,20 +7,15 @@ from app.api.v1.endpoints import (
audit, audit,
burst_detection, burst_detection,
burst_location, burst_location,
extension,
geocoding, geocoding,
leakage, leakage,
meta, meta,
misc,
model_import, model_import,
project, project,
project_data, project_data,
risk,
scada, scada,
schemes,
sensor_placement, sensor_placement,
simulation, simulation,
snapshots,
web_search, web_search,
) )
from app.api.v1.endpoints.components import ( from app.api.v1.endpoints.components import (
@@ -45,15 +40,14 @@ from app.api.v1.endpoints.network import (
valves, valves,
) )
from app.api.v1.endpoints.timeseries import ( from app.api.v1.endpoints.timeseries import (
analysis as ts_analysis,
composite as ts_composite, composite as ts_composite,
realtime as ts_realtime, realtime as ts_realtime,
scada as ts_scada, scada as ts_scada,
scheme as ts_scheme,
) )
from app.auth.permissions import ( from app.auth.permissions import (
BURST_RUN, BURST_RUN,
OPTIMIZATION_RUN, OPTIMIZATION_RUN,
RISK_RUN,
SCADA_CLEAN, SCADA_CLEAN,
SCADA_VIEW, SCADA_VIEW,
SIMULATION_RUN, SIMULATION_RUN,
@@ -88,7 +82,6 @@ simulation_access = Depends(
webgis_view_access = Depends(require_permission(WEBGIS_VIEW)) webgis_view_access = Depends(require_permission(WEBGIS_VIEW))
simulation_run_access = Depends(require_permission(SIMULATION_RUN)) simulation_run_access = Depends(require_permission(SIMULATION_RUN))
burst_run_access = Depends(require_permission(BURST_RUN)) burst_run_access = Depends(require_permission(BURST_RUN))
risk_run_access = Depends(require_permission(RISK_RUN))
optimization_run_access = Depends(require_permission(OPTIMIZATION_RUN)) optimization_run_access = Depends(require_permission(OPTIMIZATION_RUN))
# Core services # Core services
@@ -139,32 +132,16 @@ api_router.include_router(
tags=["Simulation Control"], tags=["Simulation Control"],
dependencies=[simulation_run_access], dependencies=[simulation_run_access],
) )
api_router.include_router(scada.router, dependencies=[scada_access]) api_router.include_router(
scada.router,
tags=["SCADA Metadata"],
dependencies=[scada_access],
)
api_router.include_router( api_router.include_router(
sensor_placement.router, sensor_placement.router,
tags=["Sensor Placement"], tags=["Sensor Placement"],
dependencies=[optimization_run_access], dependencies=[optimization_run_access],
) )
api_router.include_router(
snapshots.router,
tags=["Snapshots"],
dependencies=[simulation_access],
)
api_router.include_router(
schemes.router,
tags=["Schemes"],
dependencies=[simulation_access],
)
api_router.include_router(
misc.router,
tags=["Misc"],
dependencies=[webgis_view_access],
)
api_router.include_router(
risk.router,
tags=["Risk"],
dependencies=[risk_run_access],
)
api_router.include_router( api_router.include_router(
web_search.router, web_search.router,
tags=["Web Search"], tags=["Web Search"],
@@ -194,7 +171,7 @@ api_router.include_router(
# TimescaleDB data # TimescaleDB data
for endpoint_router, tag in ( for endpoint_router, tag in (
(ts_realtime.router, "TimescaleDB - Realtime"), (ts_realtime.router, "TimescaleDB - Realtime"),
(ts_scheme.router, "TimescaleDB - Scheme"), (ts_analysis.router, "TimescaleDB - Analysis"),
): ):
api_router.include_router( api_router.include_router(
endpoint_router, endpoint_router,
@@ -217,8 +194,3 @@ api_router.include_router(
tags=["Project Data"], tags=["Project Data"],
dependencies=[webgis_view_access], dependencies=[webgis_view_access],
) )
api_router.include_router(
extension.router,
tags=["Extension"],
dependencies=[webgis_access],
)
+33 -36
View File
@@ -16,7 +16,7 @@ from app.infra.db.metadb.repositories.metadata_repository import (
MetadataRepository, MetadataRepository,
ProjectDbRouting, ProjectDbRouting,
) )
from app.infra.db.project_routing import ActiveProjectRouting from app.infra.db.project_routing import ActiveProjectRouting, activate_project_routing
DB_ROLE_BIZ_DATA = "biz_data" DB_ROLE_BIZ_DATA = "biz_data"
DB_ROLE_IOT_DATA = "iot_data" DB_ROLE_IOT_DATA = "iot_data"
@@ -107,6 +107,14 @@ async def get_project_business_routing(
return await resolve_project_business_routing(ctx, metadata_repo) return await resolve_project_business_routing(ctx, metadata_repo)
async def use_project_business_routing(
routing: ActiveProjectRouting = Depends(get_project_business_routing),
) -> AsyncGenerator[ActiveProjectRouting, None]:
"""Keep the routed BizDB active for an entire endpoint invocation."""
with activate_project_routing(routing):
yield routing
async def resolve_project_business_routing( async def resolve_project_business_routing(
ctx: ProjectContext, ctx: ProjectContext,
metadata_repo: MetadataRepository, metadata_repo: MetadataRepository,
@@ -189,31 +197,6 @@ async def _get_project_routing(
return routing return routing
async def get_project_pg_session(
ctx: ProjectContext = Depends(get_project_context),
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
) -> AsyncGenerator[AsyncSession, None]:
routing = await _get_project_routing(
metadata_repo,
ctx.project_id,
DB_ROLE_BIZ_DATA,
DB_TYPE_POSTGRES,
"PostgreSQL",
)
pool_min_size = routing.pool_min_size or settings.PROJECT_PG_POOL_SIZE
pool_max_size = routing.pool_max_size or settings.PROJECT_PG_POOL_SIZE
sessionmaker = await project_connection_manager.get_pg_sessionmaker(
ctx.project_id,
DB_ROLE_BIZ_DATA,
routing.dsn,
pool_min_size,
pool_max_size,
)
async with sessionmaker() as session:
yield session
async def get_project_pg_connection( async def get_project_pg_connection(
ctx: ProjectContext = Depends(get_project_context), ctx: ProjectContext = Depends(get_project_context),
metadata_repo: MetadataRepository = Depends(get_metadata_repository), metadata_repo: MetadataRepository = Depends(get_metadata_repository),
@@ -226,16 +209,23 @@ async def get_project_pg_connection(
"PostgreSQL", "PostgreSQL",
) )
pool_min_size = routing.pool_min_size or settings.PROJECT_PG_POOL_SIZE pool_min_size = (
pool_max_size = routing.pool_max_size or settings.PROJECT_PG_POOL_SIZE routing.pool_min_size
pool = await project_connection_manager.get_pg_pool( if routing.pool_min_size is not None
else settings.PROJECT_PG_POOL_MIN_SIZE
)
pool_max_size = (
routing.pool_max_size
if routing.pool_max_size is not None
else settings.PROJECT_PG_POOL_SIZE
)
async with project_connection_manager.pg_connection(
ctx.project_id, ctx.project_id,
DB_ROLE_BIZ_DATA, DB_ROLE_BIZ_DATA,
routing.dsn, routing.dsn,
pool_min_size, pool_min_size,
pool_max_size, pool_max_size,
) ) as conn:
async with pool.connection() as conn:
yield conn yield conn
@@ -251,14 +241,21 @@ async def get_project_timescale_connection(
"TimescaleDB", "TimescaleDB",
) )
pool_min_size = routing.pool_min_size or settings.PROJECT_TS_POOL_MIN_SIZE pool_min_size = (
pool_max_size = routing.pool_max_size or settings.PROJECT_TS_POOL_MAX_SIZE routing.pool_min_size
pool = await project_connection_manager.get_timescale_pool( if routing.pool_min_size is not None
else settings.PROJECT_TS_POOL_MIN_SIZE
)
pool_max_size = (
routing.pool_max_size
if routing.pool_max_size is not None
else settings.PROJECT_TS_POOL_MAX_SIZE
)
async with project_connection_manager.timescale_connection(
ctx.project_id, ctx.project_id,
DB_ROLE_IOT_DATA, DB_ROLE_IOT_DATA,
routing.dsn, routing.dsn,
pool_min_size, pool_min_size,
pool_max_size, pool_max_size,
) ) as conn:
async with pool.connection() as conn:
yield conn yield conn
+9 -6
View File
@@ -36,12 +36,15 @@ class Settings(BaseSettings):
METADATA_DB_POOL_SIZE: int = 5 METADATA_DB_POOL_SIZE: int = 5
METADATA_DB_MAX_OVERFLOW: int = 10 METADATA_DB_MAX_OVERFLOW: int = 10
PROJECT_PG_CACHE_SIZE: int = 50 PROJECT_PG_CACHE_SIZE: int = 16
PROJECT_TS_CACHE_SIZE: int = 50 PROJECT_TS_CACHE_SIZE: int = 16
PROJECT_PG_POOL_SIZE: int = 5 PROJECT_PG_POOL_MIN_SIZE: int = 0
PROJECT_PG_MAX_OVERFLOW: int = 10 PROJECT_PG_POOL_SIZE: int = 4
PROJECT_TS_POOL_MIN_SIZE: int = 1 PROJECT_PG_MAX_OVERFLOW: int = 2
PROJECT_TS_POOL_MAX_SIZE: int = 10 PROJECT_TS_POOL_MIN_SIZE: int = 0
PROJECT_TS_POOL_MAX_SIZE: int = 4
WNDB_TEMPLATE_DB_NAME: str = "tjwater_v2_template"
WNDB_TEMP_DB_MAX_COUNT: int = 8
# Keycloak access token verification # Keycloak access token verification
KEYCLOAK_PUBLIC_KEY: str = "" KEYCLOAK_PUBLIC_KEY: str = ""
+18
View File
@@ -0,0 +1,18 @@
from pydantic import BaseModel
class ScadaDeviceResponse(BaseModel):
"""Project SCADA metadata keyed by the canonical device identifier."""
device_id: str
device_type: str
node_id: str | None = None
link_id: str | None = None
api_query_id: str | None = None
transmission_mode: str
transmission_frequency: str
reliability: int | None = None
x: float | None = None
y: float | None = None
longitude: float | None = None
latitude: float | None = None
+14 -12
View File
@@ -1,5 +1,6 @@
from datetime import datetime from datetime import datetime
from typing import Literal from typing import Literal
from uuid import UUID
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
@@ -23,7 +24,7 @@ class SensorPlacementOptimizeRequest(BaseModel):
max_length=63, max_length=63,
pattern=r"^[^/\\\x00]+$", pattern=r"^[^/\\\x00]+$",
) )
scheme_name: str = Field(..., min_length=1, max_length=32) run_name: str = Field(..., min_length=1, max_length=64)
sensor_type: Literal["pressure"] sensor_type: Literal["pressure"]
method: Literal["sensitivity", "kmeans"] method: Literal["sensitivity", "kmeans"]
sensor_count: int = Field(..., gt=0, le=200) sensor_count: int = Field(..., gt=0, le=200)
@@ -39,27 +40,27 @@ class SensorPlacementOptimizeRequest(BaseModel):
class SensorPlacementUpdateRequest(BaseModel): class SensorPlacementUpdateRequest(BaseModel):
expected_sensor_location: list[str] = Field( expected_sensor_locations: list[str] = Field(
..., ...,
min_length=1, min_length=1,
max_length=200, max_length=200,
) )
sensor_location: list[str] = Field(..., min_length=1, max_length=200) sensor_locations: list[str] = Field(..., min_length=1, max_length=200)
@field_validator("expected_sensor_location", "sensor_location") @field_validator("expected_sensor_locations", "sensor_locations")
@classmethod @classmethod
def validate_locations(cls, value: list[str]) -> list[str]: def validate_locations(cls, value: list[str]) -> list[str]:
return _normalize_location_ids(value) return _normalize_location_ids(value)
class SensorPlacementExportRequest(BaseModel): class SensorPlacementExportRequest(BaseModel):
sensor_location: list[str] = Field(..., min_length=1, max_length=200) sensor_locations: list[str] = Field(..., min_length=1, max_length=200)
adjustment_status: dict[str, AdjustmentStatus] = Field( adjustment_status: dict[str, AdjustmentStatus] = Field(
default_factory=dict, default_factory=dict,
max_length=200, max_length=200,
) )
@field_validator("sensor_location") @field_validator("sensor_locations")
@classmethod @classmethod
def validate_locations(cls, value: list[str]) -> list[str]: def validate_locations(cls, value: list[str]) -> list[str]:
return _normalize_location_ids(value) return _normalize_location_ids(value)
@@ -81,12 +82,13 @@ class SensorPointResponse(BaseModel):
class SensorPlacementSchemeResponse(BaseModel): class SensorPlacementSchemeResponse(BaseModel):
id: int run_id: UUID
scheme_name: str name: str
sensor_number: int sensor_count: int
min_diameter: int min_diameter: int
username: str created_by: str
create_time: datetime created_at: datetime
sensor_location: list[str] status: str
sensor_locations: list[str]
sensor_points: list[SensorPointResponse] sensor_points: list[SensorPointResponse]
can_edit: bool = False can_edit: bool = False
+194 -125
View File
@@ -1,40 +1,29 @@
import asyncio import asyncio
import logging import logging
from collections import OrderedDict from collections import OrderedDict
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict from typing import Dict
from uuid import UUID from uuid import UUID
from psycopg import AsyncConnection
from psycopg_pool import AsyncConnectionPool from psycopg_pool import AsyncConnectionPool
from psycopg.rows import dict_row from psycopg.rows import dict_row
from sqlalchemy.engine.url import make_url
from sqlalchemy.ext.asyncio import (
AsyncEngine,
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from app.core.config import settings from app.core.config import settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_check_async_connection = AsyncConnectionPool.check_connection
@dataclass(frozen=True) @dataclass
class PgEngineEntry:
engine: AsyncEngine
sessionmaker: async_sessionmaker[AsyncSession]
connection_url: str
pool_min_size: int
pool_max_size: int
@dataclass(frozen=True)
class PoolEntry: class PoolEntry:
pool: AsyncConnectionPool pool: AsyncConnectionPool
connection_url: str connection_url: str
pool_min_size: int pool_min_size: int
pool_max_size: int pool_max_size: int
borrow_count: int = 0
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -45,83 +34,23 @@ class CacheKey:
class ProjectConnectionManager: class ProjectConnectionManager:
def __init__(self) -> None: def __init__(self) -> None:
self._pg_cache: Dict[CacheKey, PgEngineEntry] = OrderedDict()
self._ts_cache: Dict[CacheKey, PoolEntry] = OrderedDict() self._ts_cache: Dict[CacheKey, PoolEntry] = OrderedDict()
self._pg_raw_cache: Dict[CacheKey, PoolEntry] = OrderedDict() self._pg_raw_cache: Dict[CacheKey, PoolEntry] = OrderedDict()
self._pg_lock = asyncio.Lock() self._retired_ts: list[tuple[CacheKey, PoolEntry]] = []
self._retired_pg: list[tuple[CacheKey, PoolEntry]] = []
self._ts_lock = asyncio.Lock() self._ts_lock = asyncio.Lock()
self._pg_raw_lock = asyncio.Lock() self._pg_raw_lock = asyncio.Lock()
def _normalize_pg_url(self, url: str) -> str: async def _get_timescale_pool_locked(
parsed = make_url(url)
if parsed.drivername in {"postgresql", "postgres"}:
parsed = parsed.set(drivername="postgresql+psycopg")
return parsed.render_as_string(hide_password=False)
async def get_pg_sessionmaker(
self, self,
project_id: UUID, project_id: UUID,
db_role: str, db_role: str,
connection_url: str, connection_url: str,
pool_min_size: int, pool_min_size: int,
pool_max_size: int, pool_max_size: int,
) -> async_sessionmaker[AsyncSession]: ) -> tuple[CacheKey, AsyncConnectionPool]:
async with self._pg_lock: pool_min_size = max(0, pool_min_size)
normalized_url = self._normalize_pg_url(connection_url) pool_max_size = max(1, pool_min_size, pool_max_size)
pool_min_size = max(1, pool_min_size)
pool_max_size = max(pool_min_size, pool_max_size)
key = CacheKey(project_id=project_id, db_role=db_role)
entry = self._pg_cache.get(key)
if entry:
if (
entry.connection_url == normalized_url
and entry.pool_min_size == pool_min_size
and entry.pool_max_size == pool_max_size
):
self._pg_cache.move_to_end(key)
return entry.sessionmaker
await entry.engine.dispose()
logger.info(
"Rebuilding PostgreSQL engine for project %s (%s) due to config change",
project_id,
db_role,
)
self._pg_cache.pop(key, None)
engine = create_async_engine(
normalized_url,
pool_size=pool_min_size,
max_overflow=max(0, pool_max_size - pool_min_size),
pool_pre_ping=True,
)
sessionmaker = async_sessionmaker(engine, expire_on_commit=False)
self._pg_cache[key] = PgEngineEntry(
engine=engine,
sessionmaker=sessionmaker,
connection_url=normalized_url,
pool_min_size=pool_min_size,
pool_max_size=pool_max_size,
)
await self._evict_pg_if_needed()
logger.info(
"Created PostgreSQL engine for project %s (%s)", project_id, db_role
)
return sessionmaker
async def get_timescale_pool(
self,
project_id: UUID,
db_role: str,
connection_url: str,
pool_min_size: int,
pool_max_size: int,
) -> AsyncConnectionPool:
async with self._ts_lock:
pool_min_size = max(1, pool_min_size)
pool_max_size = max(pool_min_size, pool_max_size)
key = CacheKey(project_id=project_id, db_role=db_role) key = CacheKey(project_id=project_id, db_role=db_role)
entry = self._ts_cache.get(key) entry = self._ts_cache.get(key)
if entry: if entry:
@@ -131,15 +60,12 @@ class ProjectConnectionManager:
and entry.pool_max_size == pool_max_size and entry.pool_max_size == pool_max_size
): ):
self._ts_cache.move_to_end(key) self._ts_cache.move_to_end(key)
return entry.pool return key, entry.pool
await entry.pool.close()
logger.info( logger.info(
"Rebuilding TimescaleDB pool for project %s (%s) due to config change", "Rebuilding TimescaleDB pool for project %s (%s) due to config change",
project_id, project_id,
db_role, db_role,
) )
self._ts_cache.pop(key, None)
pool = AsyncConnectionPool( pool = AsyncConnectionPool(
conninfo=connection_url, conninfo=connection_url,
@@ -147,32 +73,33 @@ class ProjectConnectionManager:
max_size=pool_max_size, max_size=pool_max_size,
open=False, open=False,
kwargs={"row_factory": dict_row}, kwargs={"row_factory": dict_row},
check=_check_async_connection,
) )
await pool.open() await pool.open()
if entry is not None:
if entry.borrow_count:
self._retired_ts.append((key, entry))
else:
await entry.pool.close()
self._ts_cache[key] = PoolEntry( self._ts_cache[key] = PoolEntry(
pool=pool, pool=pool,
connection_url=connection_url, connection_url=connection_url,
pool_min_size=pool_min_size, pool_min_size=pool_min_size,
pool_max_size=pool_max_size, pool_max_size=pool_max_size,
) )
await self._evict_ts_if_needed() logger.info("Created TimescaleDB pool for project %s (%s)", project_id, db_role)
logger.info( return key, pool
"Created TimescaleDB pool for project %s (%s)", project_id, db_role
)
return pool
async def get_pg_pool( async def _get_pg_pool_locked(
self, self,
project_id: UUID, project_id: UUID,
db_role: str, db_role: str,
connection_url: str, connection_url: str,
pool_min_size: int, pool_min_size: int,
pool_max_size: int, pool_max_size: int,
) -> AsyncConnectionPool: ) -> tuple[CacheKey, AsyncConnectionPool]:
async with self._pg_raw_lock: pool_min_size = max(0, pool_min_size)
pool_min_size = max(1, pool_min_size) pool_max_size = max(1, pool_min_size, pool_max_size)
pool_max_size = max(pool_min_size, pool_max_size)
key = CacheKey(project_id=project_id, db_role=db_role) key = CacheKey(project_id=project_id, db_role=db_role)
entry = self._pg_raw_cache.get(key) entry = self._pg_raw_cache.get(key)
if entry: if entry:
@@ -182,15 +109,12 @@ class ProjectConnectionManager:
and entry.pool_max_size == pool_max_size and entry.pool_max_size == pool_max_size
): ):
self._pg_raw_cache.move_to_end(key) self._pg_raw_cache.move_to_end(key)
return entry.pool return key, entry.pool
await entry.pool.close()
logger.info( logger.info(
"Rebuilding PostgreSQL pool for project %s (%s) due to config change", "Rebuilding PostgreSQL pool for project %s (%s) due to config change",
project_id, project_id,
db_role, db_role,
) )
self._pg_raw_cache.pop(key, None)
pool = AsyncConnectionPool( pool = AsyncConnectionPool(
conninfo=connection_url, conninfo=connection_url,
@@ -198,33 +122,115 @@ class ProjectConnectionManager:
max_size=pool_max_size, max_size=pool_max_size,
open=False, open=False,
kwargs={"row_factory": dict_row}, kwargs={"row_factory": dict_row},
check=_check_async_connection,
) )
await pool.open() await pool.open()
if entry is not None:
if entry.borrow_count:
self._retired_pg.append((key, entry))
else:
await entry.pool.close()
self._pg_raw_cache[key] = PoolEntry( self._pg_raw_cache[key] = PoolEntry(
pool=pool, pool=pool,
connection_url=connection_url, connection_url=connection_url,
pool_min_size=pool_min_size, pool_min_size=pool_min_size,
pool_max_size=pool_max_size, pool_max_size=pool_max_size,
) )
logger.info("Created PostgreSQL pool for project %s (%s)", project_id, db_role)
return key, pool
@asynccontextmanager
async def pg_connection(
self,
project_id: UUID,
db_role: str,
connection_url: str,
pool_min_size: int,
pool_max_size: int,
) -> AsyncIterator[AsyncConnection]:
async with self._pg_raw_lock:
key, pool = await self._get_pg_pool_locked(
project_id,
db_role,
connection_url,
pool_min_size,
pool_max_size,
)
borrowed_entry = self._pg_raw_cache[key]
borrowed_entry.borrow_count += 1
await self._evict_pg_raw_if_needed() await self._evict_pg_raw_if_needed()
logger.info(
"Created PostgreSQL pool for project %s (%s)", project_id, db_role
)
return pool
async def _evict_pg_if_needed(self) -> None: try:
while len(self._pg_cache) > settings.PROJECT_PG_CACHE_SIZE: async with pool.connection() as conn:
key, entry = self._pg_cache.popitem(last=False) yield conn
await entry.engine.dispose() finally:
logger.info( async with self._pg_raw_lock:
"Evicted PostgreSQL engine for project %s (%s)", borrowed_entry.borrow_count -= 1
key.project_id, if borrowed_entry.borrow_count == 0 and borrowed_entry.pool is not (
key.db_role, self._pg_raw_cache.get(key).pool
) if key in self._pg_raw_cache
else None
):
self._retired_pg = [
item for item in self._retired_pg if item[1] is not borrowed_entry
]
await borrowed_entry.pool.close()
await self._evict_pg_raw_if_needed()
async def _evict_ts_if_needed(self) -> None: @asynccontextmanager
async def timescale_connection(
self,
project_id: UUID,
db_role: str,
connection_url: str,
pool_min_size: int,
pool_max_size: int,
) -> AsyncIterator[AsyncConnection]:
async with self._ts_lock:
key, pool = await self._get_timescale_pool_locked(
project_id,
db_role,
connection_url,
pool_min_size,
pool_max_size,
)
borrowed_entry = self._ts_cache[key]
borrowed_entry.borrow_count += 1
await self._evict_ts_if_needed()
try:
async with pool.connection() as conn:
yield conn
finally:
async with self._ts_lock:
borrowed_entry.borrow_count -= 1
if borrowed_entry.borrow_count == 0 and borrowed_entry.pool is not (
self._ts_cache.get(key).pool
if key in self._ts_cache
else None
):
self._retired_ts = [
item for item in self._retired_ts if item[1] is not borrowed_entry
]
await borrowed_entry.pool.close()
await self._evict_ts_if_needed()
async def _evict_ts_if_needed(
self, protected_key: CacheKey | None = None
) -> None:
while len(self._ts_cache) > settings.PROJECT_TS_CACHE_SIZE: while len(self._ts_cache) > settings.PROJECT_TS_CACHE_SIZE:
key, entry = self._ts_cache.popitem(last=False) idle = next(
(
(key, entry)
for key, entry in self._ts_cache.items()
if entry.borrow_count == 0 and key != protected_key
),
None,
)
if idle is None:
return
key, entry = idle
self._ts_cache.pop(key)
await entry.pool.close() await entry.pool.close()
logger.info( logger.info(
"Evicted TimescaleDB pool for project %s (%s)", "Evicted TimescaleDB pool for project %s (%s)",
@@ -232,9 +238,22 @@ class ProjectConnectionManager:
key.db_role, key.db_role,
) )
async def _evict_pg_raw_if_needed(self) -> None: async def _evict_pg_raw_if_needed(
self, protected_key: CacheKey | None = None
) -> None:
while len(self._pg_raw_cache) > settings.PROJECT_PG_CACHE_SIZE: while len(self._pg_raw_cache) > settings.PROJECT_PG_CACHE_SIZE:
key, entry = self._pg_raw_cache.popitem(last=False) idle = next(
(
(key, entry)
for key, entry in self._pg_raw_cache.items()
if entry.borrow_count == 0 and key != protected_key
),
None,
)
if idle is None:
return
key, entry = idle
self._pg_raw_cache.pop(key)
await entry.pool.close() await entry.pool.close()
logger.info( logger.info(
"Evicted PostgreSQL pool for project %s (%s)", "Evicted PostgreSQL pool for project %s (%s)",
@@ -242,17 +261,61 @@ class ProjectConnectionManager:
key.db_role, key.db_role,
) )
async def close_all(self) -> None: async def close_project(
async with self._pg_lock: self, project_id: UUID, db_role: str | None = None
for key, entry in list(self._pg_cache.items()): ) -> bool:
await entry.engine.dispose() """Close this worker's idle pools for a project.
Returns ``False`` without interrupting requests when any matching pool is
currently borrowed. Callers may retry after those requests complete.
"""
closed = True
for cache, lock, label in (
(self._ts_cache, self._ts_lock, "TimescaleDB"),
(self._pg_raw_cache, self._pg_raw_lock, "PostgreSQL"),
):
async with lock:
keys = [
key
for key in cache
if key.project_id == project_id
and (db_role is None or key.db_role == db_role)
]
for key in keys:
entry = cache[key]
if entry.borrow_count:
closed = False
continue
cache.pop(key)
await entry.pool.close()
logger.info( logger.info(
"Closed PostgreSQL engine for project %s (%s)", "Closed %s pool for project %s (%s)",
label,
key.project_id, key.project_id,
key.db_role, key.db_role,
) )
self._pg_cache.clear() for retired, lock in (
(self._retired_ts, self._ts_lock),
(self._retired_pg, self._pg_raw_lock),
):
async with lock:
matches = [
item
for item in retired
if item[0].project_id == project_id
and (db_role is None or item[0].db_role == db_role)
]
if any(entry.borrow_count for _key, entry in matches):
closed = False
for item in matches:
key, entry = item
if entry.borrow_count:
continue
retired.remove(item)
await entry.pool.close()
return closed
async def close_all(self) -> None:
async with self._ts_lock: async with self._ts_lock:
for key, entry in list(self._ts_cache.items()): for key, entry in list(self._ts_cache.items()):
await entry.pool.close() await entry.pool.close()
@@ -262,6 +325,9 @@ class ProjectConnectionManager:
key.db_role, key.db_role,
) )
self._ts_cache.clear() self._ts_cache.clear()
for _key, entry in self._retired_ts:
await entry.pool.close()
self._retired_ts.clear()
async with self._pg_raw_lock: async with self._pg_raw_lock:
for key, entry in list(self._pg_raw_cache.items()): for key, entry in list(self._pg_raw_cache.items()):
@@ -272,6 +338,9 @@ class ProjectConnectionManager:
key.db_role, key.db_role,
) )
self._pg_raw_cache.clear() self._pg_raw_cache.clear()
for _key, entry in self._retired_pg:
await entry.pool.close()
self._retired_pg.clear()
project_connection_manager = ProjectConnectionManager() project_connection_manager = ProjectConnectionManager()
+148
View File
@@ -0,0 +1,148 @@
from datetime import datetime
from typing import Any
from uuid import UUID, uuid4
from psycopg import AsyncConnection, Connection
from psycopg.types.json import Jsonb
class AnalysisRepository:
@staticmethod
def create_run_sync(
conn: Connection,
*,
name: str,
run_type: str,
created_by: str,
started_at: datetime,
status: str,
parameters: dict[str, Any],
run_id: UUID | None = None,
) -> dict[str, Any]:
execution_id = run_id or uuid4()
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO analysis.runs
(run_id, name, run_type, created_by, created_at,
started_at, status, parameters)
VALUES (%s, %s, %s, %s, now(), %s, %s, %s)
RETURNING run_id, name, run_type, created_by, created_at,
started_at, status, parameters
""",
(
execution_id,
name,
run_type,
created_by,
started_at,
status,
Jsonb(parameters),
),
)
created = cur.fetchone()
if created is None:
raise RuntimeError("analysis run insert returned no row")
return created
@staticmethod
def update_run_sync(
conn: Connection,
run_id: UUID,
*,
status: str,
created_by: str,
parameters: dict[str, Any],
) -> None:
with conn.cursor() as cur:
cur.execute(
"""
UPDATE analysis.runs
SET created_by = %s, status = %s, parameters = %s
WHERE run_id = %s
""",
(created_by, status, Jsonb(parameters), run_id),
)
if cur.rowcount != 1:
raise LookupError(f"analysis run {run_id} does not exist")
@staticmethod
def insert_result_sync(
conn: Connection,
run_id: UUID,
*,
result_type: str,
payload: dict[str, Any],
node_id: str | None = None,
link_id: str | None = None,
) -> None:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO analysis.results
(run_id, result_type, node_id, link_id, payload)
VALUES (%s, %s, %s, %s, %s)
""",
(run_id, result_type, node_id, link_id, Jsonb(payload)),
)
@staticmethod
def get_run_sync(conn: Connection, run_id: UUID) -> dict[str, Any] | None:
with conn.cursor() as cur:
cur.execute(
"""
SELECT run_id, name, run_type, created_by, created_at,
started_at, status, parameters
FROM analysis.runs
WHERE run_id = %s
""",
(run_id,),
)
return cur.fetchone()
@staticmethod
async def list_runs(conn: AsyncConnection) -> list[dict]:
async with conn.cursor() as cur:
await cur.execute(
"""
SELECT run_id, name, run_type, created_by, created_at,
started_at, status, parameters
FROM analysis.runs
ORDER BY created_at DESC, run_id
"""
)
return await cur.fetchall()
@staticmethod
async def get_run(conn: AsyncConnection, run_id: UUID) -> dict | None:
async with conn.cursor() as cur:
await cur.execute(
"""
SELECT run_id, name, run_type, created_by, created_at,
started_at, status, parameters
FROM analysis.runs
WHERE run_id = %s
""",
(run_id,),
)
return await cur.fetchone()
@staticmethod
async def list_results(
conn: AsyncConnection, run_id: UUID, result_type: str | None = None
) -> list[dict]:
query = """
SELECT result_id, run_id, result_type, node_id, link_id,
payload, created_at
FROM analysis.results
WHERE run_id = %s
"""
params: tuple[UUID] | tuple[UUID, str] = (run_id,)
if result_type is not None:
query += " AND result_type = %s"
params = (run_id, result_type)
query += " ORDER BY created_at, result_id"
async with conn.cursor() as cur:
await cur.execute(query, params)
return await cur.fetchall()
-123
View File
@@ -1,123 +0,0 @@
import logging
from contextlib import asynccontextmanager
from typing import AsyncGenerator, Dict, Optional
import psycopg_pool
from psycopg.rows import dict_row
from app.infra.db.project_routing import get_project_pgconn_string
# Configure logging
logger = logging.getLogger(__name__)
class Database:
def __init__(self, db_name=None):
self.pool = None
self.db_name = db_name
self.conninfo = None
def init_pool(self, db_name=None):
"""Initialize the connection pool."""
# Use provided db_name, or the one from constructor, or default from config
target_db_name = db_name or self.db_name
# Get connection string, handling default case where target_db_name might be None
if target_db_name:
conn_string = get_project_pgconn_string(db_name=target_db_name)
else:
conn_string = get_project_pgconn_string()
self.conninfo = conn_string
try:
self.pool = psycopg_pool.AsyncConnectionPool(
conninfo=conn_string,
min_size=5,
max_size=20,
open=False, # Don't open immediately, wait for startup
kwargs={"row_factory": dict_row}, # Return rows as dictionaries
)
logger.info(f"PostgreSQL connection pool initialized for database: {target_db_name or 'default'}")
except Exception as e:
logger.error(f"Failed to initialize postgresql connection pool: {e}")
raise
async def open(self):
if self.pool:
await self.pool.open()
async def close(self):
"""Close the connection pool."""
if self.pool:
await self.pool.close()
logger.info("PostgreSQL connection pool closed.")
@asynccontextmanager
async def get_connection(self) -> AsyncGenerator:
"""Get a connection from the pool."""
if not self.pool:
raise Exception("Database pool is not initialized.")
async with self.pool.connection() as conn:
yield conn
# 默认数据库实例
db = Database()
# 缓存不同数据库的实例 - 避免重复创建连接池
_database_instances: Dict[str, Database] = {}
def create_database_instance(db_name):
"""Create a new Database instance for a specific database."""
return Database(db_name=db_name)
async def get_database_instance(db_name: Optional[str] = None) -> Database:
"""Get or create a database instance for the specified database name."""
if not db_name:
return db # 返回默认数据库实例
expected_conninfo = get_project_pgconn_string(db_name=db_name)
existing = _database_instances.get(db_name)
if existing is not None and existing.conninfo != expected_conninfo:
await existing.close()
del _database_instances[db_name]
if db_name not in _database_instances:
# 创建新的数据库实例
instance = create_database_instance(db_name)
instance.init_pool()
await instance.open()
_database_instances[db_name] = instance
logger.info(f"Created new database instance for: {db_name}")
return _database_instances[db_name]
async def get_db_connection():
"""Dependency for FastAPI to get a database connection."""
async with db.get_connection() as conn:
yield conn
async def get_database_connection(db_name: Optional[str] = None):
"""
FastAPI dependency to get database connection with optional database name.
使用方法: conn: AsyncConnection = Depends(lambda: get_database_connection("your_db_name"))
或在路由函数中: conn: AsyncConnection = Depends(get_database_connection)
"""
instance = await get_database_instance(db_name)
async with instance.get_connection() as conn:
yield conn
async def cleanup_database_instances():
"""Clean up all database instances (call this on application shutdown)."""
for db_name, instance in _database_instances.items():
await instance.close()
logger.info(f"Closed database instance for: {db_name}")
_database_instances.clear()
# 关闭默认数据库
await db.close()
logger.info("All database instances cleaned up.")
+27
View File
@@ -0,0 +1,27 @@
"""Read repositories for project network assets."""
from typing import Any
from psycopg import AsyncConnection
class NetworkAssetRepository:
@staticmethod
async def get_pipes_by_ids(
conn: AsyncConnection,
pipe_ids: list[str],
) -> list[dict[str, Any]]:
if not pipe_ids:
return []
async with conn.cursor() as cur:
await cur.execute(
"""
SELECT id, diameter, start_node_id AS node1,
end_node_id AS node2
FROM gis.pipes
WHERE id = ANY(%s)
ORDER BY id
""",
(pipe_ids,),
)
return await cur.fetchall()
+146 -27
View File
@@ -1,7 +1,42 @@
from typing import Any from dataclasses import dataclass
from types import MappingProxyType
from typing import Any, Mapping
from psycopg import AsyncConnection from psycopg import AsyncConnection
from app.native.wndb.core.database import read_all, try_read
_SCADA_VIEW_SELECT = """
SELECT id AS device_id, device_type, node_id, link_id, api_query_id,
transmission_mode, transmission_frequency, reliability, x, y,
ST_X(ST_Transform(geom, 4326)) AS longitude,
ST_Y(ST_Transform(geom, 4326)) AS latitude
FROM gis.scada_devices
"""
@dataclass(frozen=True)
class ScadaElementMappings:
reservoirs: Mapping[str, str]
tanks: Mapping[str, str]
fixed_pumps: Mapping[str, str]
variable_pumps: Mapping[str, str]
pressure: Mapping[str, str]
demand: Mapping[str, str]
quality: Mapping[str, str]
def _empty_mapping_groups() -> dict[str, dict[str, str]]:
return {
"reservoir_liquid_level": {},
"tank_liquid_level": {},
"fixed_pump": {},
"variable_pump": {},
"pressure": {},
"demand": {},
"quality": {},
}
def _optional_text(value: Any) -> str | None: def _optional_text(value: Any) -> str | None:
return str(value).strip() if value is not None else None return str(value).strip() if value is not None else None
@@ -11,6 +46,27 @@ def _optional_float(value: Any) -> float | None:
return float(value) if value is not None else None return float(value) if value is not None else None
def _optional_int(value: Any) -> int | None:
return int(value) if value is not None else None
def _device(record: dict[str, Any]) -> dict[str, Any]:
return {
"device_id": str(record["device_id"]).strip(),
"device_type": str(record["device_type"]).strip().lower(),
"node_id": _optional_text(record["node_id"]),
"link_id": _optional_text(record["link_id"]),
"api_query_id": _optional_text(record["api_query_id"]),
"transmission_mode": record["transmission_mode"],
"transmission_frequency": record["transmission_frequency"],
"reliability": _optional_int(record["reliability"]),
"x": _optional_float(record["x"]),
"y": _optional_float(record["y"]),
"longitude": _optional_float(record["longitude"]),
"latitude": _optional_float(record["latitude"]),
}
class ScadaInfoRepository: class ScadaInfoRepository:
"""Read SCADA metadata from the current project's business database.""" """Read SCADA metadata from the current project's business database."""
@@ -18,34 +74,97 @@ class ScadaInfoRepository:
async def get_scadas(conn: AsyncConnection) -> list[dict[str, Any]]: async def get_scadas(conn: AsyncConnection) -> list[dict[str, Any]]:
async with conn.cursor() as cur: async with conn.cursor() as cur:
await cur.execute( await cur.execute(
""" _SCADA_VIEW_SELECT + " ORDER BY device_id"
SELECT id,
type,
associated_element_id,
api_query_id,
transmission_mode,
transmission_frequency,
reliability,
x_coor,
y_coor
FROM public.scada_info
"""
) )
records = await cur.fetchall() records = await cur.fetchall()
return [ return [_device(record) for record in records]
{
"id": str(record["id"]).strip(), @staticmethod
"type": str(record["type"]).strip().lower(), async def get_scada(
"associated_element_id": _optional_text( conn: AsyncConnection, device_id: str
record["associated_element_id"] ) -> dict[str, Any] | None:
), async with conn.cursor() as cur:
"api_query_id": record["api_query_id"], await cur.execute(
"transmission_mode": record["transmission_mode"], _SCADA_VIEW_SELECT + " WHERE id = %s",
"transmission_frequency": record["transmission_frequency"], (device_id,),
"reliability": _optional_float(record["reliability"]), )
"x": _optional_float(record["x_coor"]), record = await cur.fetchone()
"y": _optional_float(record["y_coor"]), return _device(record) if record else None
@staticmethod
async def get_existing_device_ids(
conn: AsyncConnection, device_ids: list[str]
) -> set[str]:
if not device_ids:
return set()
async with conn.cursor() as cur:
await cur.execute(
"SELECT device_id FROM asset.scada_devices WHERE device_id = ANY(%s)",
(device_ids,),
)
return {str(row["device_id"]).strip() for row in await cur.fetchall()}
def get_scada_info_schema(name: str) -> dict[str, dict[str, Any]]:
return {
"device_id": {"type": "str", "optional": False, "readonly": True},
"device_type": {"type": "str", "optional": False, "readonly": True},
"node_id": {"type": "str", "optional": True, "readonly": True},
"link_id": {"type": "str", "optional": True, "readonly": True},
"api_query_id": {"type": "str", "optional": True, "readonly": True},
"transmission_mode": {"type": "str", "optional": False, "readonly": True},
"transmission_frequency": {"type": "str", "optional": False, "readonly": True},
"reliability": {"type": "int", "optional": False, "readonly": True},
"x": {"type": "float", "optional": True, "readonly": True},
"y": {"type": "float", "optional": True, "readonly": True},
"longitude": {"type": "float", "optional": True, "readonly": True},
"latitude": {"type": "float", "optional": True, "readonly": True},
} }
for record in records
def get_scada_info(name: str, device_id: str) -> dict[str, Any]:
row = try_read(
name,
_SCADA_VIEW_SELECT + " WHERE id = %s",
(device_id,),
)
return _device(row) if row else {}
def get_all_scada_info(name: str) -> list[dict[str, Any]]:
return [
_device(row)
for row in read_all(name, _SCADA_VIEW_SELECT + " ORDER BY device_id")
] ]
def load_realtime_element_mappings(name: str) -> ScadaElementMappings:
"""Load one project-local immutable SCADA-to-model mapping snapshot."""
groups = _empty_mapping_groups()
rows = read_all(
name,
"""
SELECT device_type, COALESCE(node_id, link_id) AS element_id,
api_query_id
FROM asset.scada_devices
WHERE transmission_mode = 'realtime'
AND api_query_id IS NOT NULL
""",
)
for row in rows:
group = groups.get(str(row["device_type"]).strip().lower())
if group is not None:
group[str(row["element_id"]).strip()] = str(row["api_query_id"]).strip()
immutable = {
name: MappingProxyType(values.copy()) for name, values in groups.items()
}
return ScadaElementMappings(
reservoirs=immutable["reservoir_liquid_level"],
tanks=immutable["tank_liquid_level"],
fixed_pumps=immutable["fixed_pump"],
variable_pumps=immutable["variable_pump"],
pressure=immutable["pressure"],
demand=immutable["demand"],
quality=immutable["quality"],
)
-104
View File
@@ -1,104 +0,0 @@
from typing import List, Optional, Any
from psycopg import AsyncConnection
class SchemeRepository:
@staticmethod
async def get_schemes(conn: AsyncConnection) -> List[dict]:
"""
查询pg数据库中, scheme_list 的所有记录
:param conn: 异步数据库连接
:return: 包含所有记录的列表, 每条记录为一个字典
"""
async with conn.cursor() as cur:
await cur.execute(
"""
SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
FROM public.scheme_list
"""
)
records = await cur.fetchall()
scheme_list = []
for record in records:
scheme_list.append(
{
"scheme_id": record["scheme_id"],
"scheme_name": record["scheme_name"],
"scheme_type": record["scheme_type"],
"username": record["username"],
"create_time": record["create_time"],
"scheme_start_time": record["scheme_start_time"],
"scheme_detail": record["scheme_detail"],
}
)
return scheme_list
@staticmethod
async def get_burst_locate_results(conn: AsyncConnection) -> List[dict]:
"""
查询pg数据库中, burst_locate_result 的所有记录
:param conn: 异步数据库连接
:return: 包含所有记录的列表, 每条记录为一个字典
"""
async with conn.cursor() as cur:
await cur.execute(
"""
SELECT id, type, burst_incident, leakage, detect_time, locate_result
FROM public.burst_locate_result
"""
)
records = await cur.fetchall()
results = []
for record in records:
results.append(
{
"id": record["id"],
"type": record["type"],
"burst_incident": record["burst_incident"],
"leakage": record["leakage"],
"detect_time": record["detect_time"],
"locate_result": record["locate_result"],
}
)
return results
@staticmethod
async def get_burst_locate_result_by_incident(
conn: AsyncConnection, burst_incident: str
) -> List[dict]:
"""
根据 burst_incident 查询爆管定位结果
:param conn: 异步数据库连接
:param burst_incident: 爆管事件标识
:return: 包含匹配记录的列表
"""
async with conn.cursor() as cur:
await cur.execute(
"""
SELECT id, type, burst_incident, leakage, detect_time, locate_result
FROM public.burst_locate_result
WHERE burst_incident = %s
""",
(burst_incident,),
)
records = await cur.fetchall()
results = []
for record in records:
results.append(
{
"id": record["id"],
"type": record["type"],
"burst_incident": record["burst_incident"],
"leakage": record["leakage"],
"detect_time": record["detect_time"],
"locate_result": record["locate_result"],
}
)
return results
+187
View File
@@ -0,0 +1,187 @@
from datetime import datetime, timezone
from typing import Any
from uuid import UUID, uuid4
from psycopg.rows import dict_row
from psycopg.types.json import Jsonb
from app.infra.db.postgresql.analysis import AnalysisRepository
from app.native.wndb.core.connection import project_connection
RUN_TYPE = "sensor_placement"
RESULT_TYPE = "sensor_placement"
def _placement_row(row: dict[str, Any]) -> dict[str, Any]:
payload = row.get("payload") if isinstance(row.get("payload"), dict) else {}
locations = [str(item) for item in payload.get("sensor_locations", [])]
return {
"run_id": row["run_id"],
"name": row["name"],
"sensor_count": len(locations),
"min_diameter": int(payload.get("minimum_diameter", 0)),
"created_by": row["created_by"],
"created_at": row["created_at"],
"status": row["status"],
"sensor_locations": locations,
}
def get_all_sensor_placements(name: str) -> list[dict[str, Any]]:
with project_connection(name) as conn, conn.cursor(row_factory=dict_row) as cur:
cur.execute(
"""
SELECT r.run_id, r.name, r.created_by, r.created_at, r.status,
result.payload
FROM analysis.runs AS r
JOIN LATERAL (
SELECT payload
FROM analysis.results
WHERE run_id = r.run_id AND result_type = %s
ORDER BY created_at DESC, result_id DESC
LIMIT 1
) AS result ON true
WHERE r.run_type = %s
ORDER BY r.created_at DESC, r.run_id
""",
(RESULT_TYPE, RUN_TYPE),
)
return [_placement_row(row) for row in cur.fetchall()]
def create_sensor_placement(
name: str,
*,
run_name: str,
min_diameter: int,
created_by: str,
sensor_locations: list[str],
) -> dict[str, Any]:
run_id = uuid4()
payload = {
"sensor_number": len(sensor_locations),
"minimum_diameter": min_diameter,
"sensor_locations": sensor_locations,
}
with project_connection(name) as conn, conn.transaction():
created = AnalysisRepository.create_run_sync(
conn,
run_id=run_id,
name=run_name,
run_type=RUN_TYPE,
created_by=created_by,
started_at=datetime.now(timezone.utc),
status="completed",
parameters={},
)
AnalysisRepository.insert_result_sync(
conn,
run_id,
result_type=RESULT_TYPE,
payload=payload,
)
return _placement_row(dict(created) | {"payload": payload})
def get_sensor_placement(name: str, run_id: UUID) -> dict[str, Any] | None:
with project_connection(name) as conn, conn.cursor(row_factory=dict_row) as cur:
cur.execute(
"""
SELECT r.run_id, r.name, r.created_by, r.created_at, r.status,
result.payload
FROM analysis.runs AS r
JOIN LATERAL (
SELECT payload
FROM analysis.results
WHERE run_id = r.run_id AND result_type = %s
ORDER BY created_at DESC, result_id DESC
LIMIT 1
) AS result ON true
WHERE r.run_id = %s AND r.run_type = %s
""",
(RESULT_TYPE, run_id, RUN_TYPE),
)
row = cur.fetchone()
return _placement_row(row) if row else None
def get_sensor_placement_nodes(
name: str,
node_ids: list[str],
) -> list[dict[str, Any]]:
if not node_ids:
return []
with project_connection(name) as conn, conn.cursor(row_factory=dict_row) as cur:
cur.execute(
"""
WITH incident_pipe_diameters AS (
SELECT node_id, MAX(diameter) AS max_pipe_diameter
FROM (
SELECT l.start_node_id AS node_id, p.diameter
FROM network.pipes AS p
JOIN network.links AS l ON l.id = p.link_id
WHERE l.start_node_id = ANY(%s)
UNION ALL
SELECT l.end_node_id AS node_id, p.diameter
FROM network.pipes AS p
JOIN network.links AS l ON l.id = p.link_id
WHERE l.end_node_id = ANY(%s)
) AS incident_pipes
GROUP BY node_id
)
SELECT j.node_id,
ipd.max_pipe_diameter,
j.elevation,
ST_X(g.geom) AS project_x,
ST_Y(g.geom) AS project_y,
ST_X(ST_Transform(g.geom, 3857)) AS map_x,
ST_Y(ST_Transform(g.geom, 3857)) AS map_y
FROM network.junctions AS j
JOIN gis.node_geometries AS g ON g.node_id = j.node_id
LEFT JOIN incident_pipe_diameters AS ipd ON ipd.node_id = j.node_id
WHERE j.node_id = ANY(%s)
ORDER BY j.node_id
""",
(node_ids, node_ids, node_ids),
)
return list(cur.fetchall())
def update_sensor_placement(
name: str,
run_id: UUID,
*,
expected_sensor_locations: list[str],
sensor_locations: list[str],
) -> dict[str, Any] | None:
with project_connection(name) as conn, conn.transaction():
with conn.cursor(row_factory=dict_row) as cur:
cur.execute(
"""
SELECT result_id, payload
FROM analysis.results
WHERE run_id = %s AND result_type = %s
ORDER BY created_at DESC, result_id DESC
LIMIT 1
FOR UPDATE
""",
(run_id, RESULT_TYPE),
)
result = cur.fetchone()
if result is None:
return None
payload = result["payload"] if isinstance(result["payload"], dict) else {}
current = [str(item) for item in payload.get("sensor_locations", [])]
if current != expected_sensor_locations:
return None
payload = {
**payload,
"sensor_number": len(sensor_locations),
"sensor_locations": sensor_locations,
}
cur.execute(
"UPDATE analysis.results SET payload = %s WHERE result_id = %s",
(Jsonb(payload), result["result_id"]),
)
return get_sensor_placement(name, run_id)
+30 -2
View File
@@ -5,9 +5,9 @@ from contextvars import ContextVar, Token
from dataclasses import dataclass from dataclasses import dataclass
from typing import Iterator from typing import Iterator
from psycopg.conninfo import make_conninfo from psycopg.conninfo import conninfo_to_dict, make_conninfo
from app.core.config import get_pgconn_string, get_timescaledb_pgconn_string from app.core.config import get_pgconn_string, get_timescaledb_pgconn_string, settings
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -16,6 +16,17 @@ class ActiveProjectRouting:
business_dsn: str business_dsn: str
timescale_dsn: str | None = None timescale_dsn: str | None = None
@property
def business_database_name(self) -> str:
"""Return the physical BizDB name selected by metadata routing."""
database_name = conninfo_to_dict(self.business_dsn).get("dbname")
if not database_name:
raise RuntimeError(
f"Business database routing for project {self.project_code!r} "
"does not contain a database name"
)
return database_name
_active_project_routing: ContextVar[ActiveProjectRouting | None] = ContextVar( _active_project_routing: ContextVar[ActiveProjectRouting | None] = ContextVar(
"active_project_routing", "active_project_routing",
@@ -42,6 +53,23 @@ def _dsn_for_database(dsn: str, database_name: str) -> str:
return make_conninfo(dsn, dbname=database_name) return make_conninfo(dsn, dbname=database_name)
def get_project_database_name(name: str) -> str:
"""Resolve a logical project code to its routed physical BizDB name."""
routing = get_active_project_routing()
if routing is not None and name == routing.project_code:
return routing.business_database_name
return name
def get_project_template_database_name(name: str | None = None) -> str:
"""Return the configured immutable template for the WNDB schema version.
The template belongs to the database schema version, not to an individual
logical project or its temporary physical database name.
"""
return settings.WNDB_TEMPLATE_DB_NAME
def get_project_pgconn_string(db_name: str | None = None) -> str: def get_project_pgconn_string(db_name: str | None = None) -> str:
routing = get_active_project_routing() routing = get_active_project_routing()
if routing is None: if routing is None:
+1 -2
View File
@@ -1,2 +1 @@
from .database import * """TimescaleDB repositories and connection infrastructure."""
from .composite_queries import CompositeQueries
-132
View File
@@ -1,132 +0,0 @@
import logging
from contextlib import asynccontextmanager
from typing import AsyncGenerator, Dict, Optional
import psycopg_pool
from psycopg.rows import dict_row
from app.infra.db.project_routing import get_project_timescale_pgconn_string
# Configure logging
logger = logging.getLogger(__name__)
class Database:
def __init__(self, db_name=None):
self.pool = None
self.db_name = db_name
self.conninfo = None
def init_pool(self, db_name=None):
"""Initialize the connection pool."""
# Use provided db_name, or the one from constructor, or default from config
target_db_name = db_name or self.db_name
# Get connection string, handling default case where target_db_name might be None
if target_db_name:
conn_string = get_project_timescale_pgconn_string(db_name=target_db_name)
else:
conn_string = get_project_timescale_pgconn_string()
self.conninfo = conn_string
try:
self.pool = psycopg_pool.AsyncConnectionPool(
conninfo=conn_string,
min_size=5,
max_size=20,
open=False, # Don't open immediately, wait for startup
kwargs={"row_factory": dict_row}, # Return rows as dictionaries
)
logger.info(
f"TimescaleDB connection pool initialized for database: {target_db_name or 'default'}"
)
except Exception as e:
logger.error(f"Failed to initialize TimescaleDB connection pool: {e}")
raise
async def open(self):
if self.pool:
await self.pool.open()
async def close(self):
"""Close the connection pool."""
if self.pool:
await self.pool.close()
logger.info("TimescaleDB connection pool closed.")
def get_pgconn_string(self, db_name=None):
"""Get the TimescaleDB connection string."""
target_db_name = db_name or self.db_name
if target_db_name:
return get_project_timescale_pgconn_string(db_name=target_db_name)
return get_project_timescale_pgconn_string()
@asynccontextmanager
async def get_connection(self) -> AsyncGenerator:
"""Get a connection from the pool."""
if not self.pool:
raise Exception("Database pool is not initialized.")
async with self.pool.connection() as conn:
yield conn
# 默认数据库实例
db = Database()
# 缓存不同数据库的实例 - 避免重复创建连接池
_database_instances: Dict[str, Database] = {}
def create_database_instance(db_name):
"""Create a new Database instance for a specific database."""
return Database(db_name=db_name)
async def get_database_instance(db_name: Optional[str] = None) -> Database:
"""Get or create a database instance for the specified database name."""
if not db_name:
return db # 返回默认数据库实例
expected_conninfo = get_project_timescale_pgconn_string(db_name=db_name)
existing = _database_instances.get(db_name)
if existing is not None and existing.conninfo != expected_conninfo:
await existing.close()
del _database_instances[db_name]
if db_name not in _database_instances:
# 创建新的数据库实例
instance = create_database_instance(db_name)
instance.init_pool()
await instance.open()
_database_instances[db_name] = instance
logger.info(f"Created new database instance for: {db_name}")
return _database_instances[db_name]
async def get_db_connection():
"""Dependency for FastAPI to get a database connection."""
async with db.get_connection() as conn:
yield conn
async def get_database_connection(db_name: Optional[str] = None):
"""
FastAPI dependency to get database connection with optional database name.
使用方法: conn: AsyncConnection = Depends(lambda: get_database_connection("your_db_name"))
或在路由函数中: conn: AsyncConnection = Depends(get_database_connection)
"""
instance = await get_database_instance(db_name)
async with instance.get_connection() as conn:
yield conn
async def cleanup_database_instances():
"""Clean up all database instances (call this on application shutdown)."""
for db_name, instance in _database_instances.items():
await instance.close()
logger.info(f"Closed database instance for: {db_name}")
_database_instances.clear()
# 关闭默认数据库
await db.close()
logger.info("All database instances cleaned up.")
+51 -101
View File
@@ -2,15 +2,15 @@ from typing import List
from fastapi.logger import logger from fastapi.logger import logger
from datetime import datetime, timedelta from datetime import datetime, timedelta
import psycopg
from psycopg import sql from psycopg import sql
from psycopg.rows import dict_row from psycopg.rows import dict_row
import time import time
from app.infra.db.project_routing import get_project_timescale_pgconn_string from app.infra.db.project_routing import get_project_timescale_pgconn_string
from app.infra.db.timescaledb.repositories.scheme import SchemeRepository from app.infra.db.timescaledb.sync_pool import timescale_connection
from app.infra.db.timescaledb.repositories.analysis import AnalysisResultsRepository
from app.infra.db.timescaledb.repositories.realtime import RealtimeRepository from app.infra.db.timescaledb.repositories.realtime import RealtimeRepository
from app.infra.db.timescaledb.repositories.scada import ScadaRepository from app.infra.db.timescaledb.repositories.scada import ScadaRepository
from app.services.time_api import parse_utc_time from app.domain.time import parse_utc_time
class InternalStorage: class InternalStorage:
@@ -25,12 +25,7 @@ class InternalStorage:
"""存储实时模拟结果""" """存储实时模拟结果"""
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
conn_string = ( with timescale_connection(db_name) as conn:
get_project_timescale_pgconn_string(db_name=db_name)
if db_name
else get_project_timescale_pgconn_string()
)
with psycopg.Connection.connect(conn_string) as conn:
RealtimeRepository.store_realtime_simulation_result_sync( RealtimeRepository.store_realtime_simulation_result_sync(
conn, node_result_list, link_result_list, result_start_time conn, node_result_list, link_result_list, result_start_time
) )
@@ -43,9 +38,8 @@ class InternalStorage:
raise # 达到最大重试次数后抛出异常 raise # 达到最大重试次数后抛出异常
@staticmethod @staticmethod
def store_scheme_simulation( def store_analysis_simulation(
scheme_type: str, run_id,
scheme_name: str,
node_result_list: List[dict], node_result_list: List[dict],
link_result_list: List[dict], link_result_list: List[dict],
result_start_time: str, result_start_time: str,
@@ -54,24 +48,16 @@ class InternalStorage:
db_name: str = None, db_name: str = None,
max_retries: int = 3, max_retries: int = 3,
): ):
"""存储方案模拟结果""" """Store immutable simulation results for one analysis run."""
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
conn_string = ( with timescale_connection(db_name) as conn:
get_project_timescale_pgconn_string(db_name=db_name) node_rows, link_rows = AnalysisResultsRepository.prepare_simulation_rows(
if db_name node_result_list, link_result_list, result_start_time,
else get_project_timescale_pgconn_string() num_periods, result_timestep_seconds or 3600,
) )
with psycopg.Connection.connect(conn_string) as conn: AnalysisResultsRepository.store_results_sync(
SchemeRepository.store_scheme_simulation_result_sync( conn, run_id, node_rows, link_rows
conn,
scheme_type,
scheme_name,
node_result_list,
link_result_list,
result_start_time,
num_periods,
result_timestep_seconds,
) )
break # 成功 break # 成功
except Exception as e: except Exception as e:
@@ -98,25 +84,19 @@ class InternalQueries:
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
conn_string = ( with timescale_connection(db_name) as conn:
get_project_timescale_pgconn_string(db_name=db_name)
if db_name
else get_project_timescale_pgconn_string()
)
with psycopg.Connection.connect(conn_string) as conn:
rows = ScadaRepository.get_scada_by_ids_time_range_sync( rows = ScadaRepository.get_scada_by_ids_time_range_sync(
conn, device_ids, start_time, end_time conn, device_ids, start_time, end_time
) )
# 处理结果,返回每个 device_id 的第一个值 # Rows are ordered by device/time; retain the first sample
result = {} # for each requested device in one pass.
for device_id in device_ids: result = {device_id: None for device_id in device_ids}
device_rows = [ seen: set[str] = set()
row for row in rows if row["device_id"] == device_id for row in rows:
] device_id = str(row["device_id"])
if device_rows: if device_id in result and device_id not in seen:
result[device_id] = device_rows[0]["monitored_value"] result[device_id] = row["monitored_value"]
else: seen.add(device_id)
result[device_id] = None
return result return result
except Exception as e: except Exception as e:
logger.error(f"查询尝试 {attempt + 1} 失败: {e}") logger.error(f"查询尝试 {attempt + 1} 失败: {e}")
@@ -139,12 +119,7 @@ class InternalQueries:
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
conn_string = ( with timescale_connection(db_name) as conn:
get_project_timescale_pgconn_string(db_name=db_name)
if db_name
else get_project_timescale_pgconn_string()
)
with psycopg.Connection.connect(conn_string) as conn:
rows = ScadaRepository.get_scada_by_ids_time_range_sync( rows = ScadaRepository.get_scada_by_ids_time_range_sync(
conn, device_ids, start_dt, end_dt conn, device_ids, start_dt, end_dt
) )
@@ -159,8 +134,6 @@ class InternalQueries:
result.setdefault(device_id, []).append( result.setdefault(device_id, []).append(
{"time": row["time"].isoformat(), "value": value} {"time": row["time"].isoformat(), "value": value}
) )
for device_id in result:
result[device_id].sort(key=lambda item: item["time"])
return result return result
except Exception as e: except Exception as e:
logger.error(f"查询尝试 {attempt + 1} 失败: {e}") logger.error(f"查询尝试 {attempt + 1} 失败: {e}")
@@ -184,12 +157,7 @@ class InternalQueries:
) )
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
conn_string = ( with timescale_connection(db_name) as conn:
get_project_timescale_pgconn_string(db_name=db_name)
if db_name
else get_project_timescale_pgconn_string()
)
with psycopg.Connection.connect(conn_string) as conn:
return ScadaRepository.get_latest_scada_time_sync( return ScadaRepository.get_latest_scada_time_sync(
conn, conn,
device_ids, device_ids,
@@ -224,20 +192,19 @@ class InternalQueries:
) )
@staticmethod @staticmethod
def query_scheme_simulation_by_ids_timerange( def query_analysis_simulation_by_ids_timerange(
element_ids: List[str], element_ids: List[str],
start_time: str | datetime, start_time: str | datetime,
end_time: str | datetime, end_time: str | datetime,
element_type: str, element_type: str,
field: str, field: str,
scheme_type: str, run_id,
scheme_name: str,
db_name: str = None, db_name: str = None,
max_retries: int = 3, max_retries: int = 3,
) -> dict[str, list[dict]]: ) -> dict[str, list[dict]]:
"""查询方案模拟结果,返回 {id: [{time, value}, ...]}""" """Query one analysis run, returning {id: [{time, value}, ...]}."""
return InternalQueries._query_simulation_by_ids_timerange( return InternalQueries._query_simulation_by_ids_timerange(
schema_name="scheme", schema_name="analysis",
element_ids=element_ids, element_ids=element_ids,
start_time=start_time, start_time=start_time,
end_time=end_time, end_time=end_time,
@@ -245,8 +212,7 @@ class InternalQueries:
field=field, field=field,
db_name=db_name, db_name=db_name,
max_retries=max_retries, max_retries=max_retries,
scheme_type=scheme_type, run_id=run_id,
scheme_name=scheme_name,
) )
@staticmethod @staticmethod
@@ -260,8 +226,7 @@ class InternalQueries:
field: str, field: str,
db_name: str = None, db_name: str = None,
max_retries: int = 3, max_retries: int = 3,
scheme_type: str | None = None, run_id=None,
scheme_name: str | None = None,
) -> dict[str, list[dict]]: ) -> dict[str, list[dict]]:
normalized_element_ids = list( normalized_element_ids = list(
dict.fromkeys( dict.fromkeys(
@@ -275,51 +240,45 @@ class InternalQueries:
start_dt = parse_utc_time(start_time, field_name="start_time") start_dt = parse_utc_time(start_time, field_name="start_time")
end_dt = parse_utc_time(end_time, field_name="end_time") end_dt = parse_utc_time(end_time, field_name="end_time")
table_name, valid_fields = InternalQueries._resolve_simulation_table(element_type) table_name, id_column, valid_fields = InternalQueries._resolve_simulation_table(element_type)
if field not in valid_fields: if field not in valid_fields:
raise ValueError(f"Invalid field for {element_type}: {field}") raise ValueError(f"Invalid field for {element_type}: {field}")
if schema_name not in {"realtime", "scheme"}: if schema_name not in {"realtime", "analysis"}:
raise ValueError(f"Unsupported schema_name: {schema_name}") raise ValueError(f"Unsupported schema_name: {schema_name}")
if schema_name == "scheme" and (not scheme_type or not scheme_name): if schema_name == "analysis" and run_id is None:
raise ValueError("scheme 查询必须提供 scheme_type 和 scheme_name。") raise ValueError("analysis query requires run_id")
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
conn_string = ( with timescale_connection(db_name) as conn:
get_project_timescale_pgconn_string(db_name=db_name)
if db_name
else get_project_timescale_pgconn_string()
)
with psycopg.Connection.connect(conn_string) as conn:
with conn.cursor(row_factory=dict_row) as cur: with conn.cursor(row_factory=dict_row) as cur:
if schema_name == "scheme": if schema_name == "analysis":
query = sql.SQL( query = sql.SQL(
"SELECT btrim(id::text) AS id, time, {} FROM {}.{} " "SELECT btrim({}::text) AS id, time, {} FROM {}.{} "
"WHERE scheme_type = %s AND scheme_name = %s " "WHERE run_id = %s AND time >= %s AND time <= %s "
"AND time >= %s AND time <= %s AND btrim(id::text) = ANY(%s)" "AND btrim({}::text) = ANY(%s) ORDER BY id, time"
).format( ).format(
sql.Identifier(id_column),
sql.Identifier(field), sql.Identifier(field),
sql.Identifier(schema_name), sql.Identifier(schema_name),
sql.Identifier(table_name), sql.Identifier(table_name),
sql.Identifier(id_column),
) )
cur.execute( cur.execute(
query, query,
( (run_id, start_dt, end_dt, normalized_element_ids),
scheme_type,
scheme_name,
start_dt,
end_dt,
normalized_element_ids,
),
) )
else: else:
query = sql.SQL( query = sql.SQL(
"SELECT btrim(id::text) AS id, time, {} FROM {}.{} " "SELECT btrim({}::text) AS id, time, {} FROM {}.{} "
"WHERE time >= %s AND time <= %s AND btrim(id::text) = ANY(%s)" "WHERE time >= %s AND time <= %s "
"AND btrim({}::text) = ANY(%s) ORDER BY id, time"
).format( ).format(
sql.Identifier(id_column),
sql.Identifier(field), sql.Identifier(field),
sql.Identifier(schema_name), sql.Identifier(schema_name),
sql.Identifier(table_name), sql.Identifier(table_name),
sql.Identifier(id_column),
) )
cur.execute(query, (start_dt, end_dt, normalized_element_ids)) cur.execute(query, (start_dt, end_dt, normalized_element_ids))
rows = cur.fetchall() rows = cur.fetchall()
@@ -342,19 +301,10 @@ class InternalQueries:
raise raise
@staticmethod @staticmethod
def _resolve_simulation_table(element_type: str) -> tuple[str, set[str]]: def _resolve_simulation_table(element_type: str) -> tuple[str, str, set[str]]:
normalized_type = element_type.lower() normalized_type = element_type.lower()
if normalized_type == "node": if normalized_type == "node":
return "node_simulation", {"actual_demand", "total_head", "pressure", "quality"} return "node_results", "node_id", set(RealtimeRepository.NODE_FIELDS)
if normalized_type == "link": if normalized_type == "link":
return "link_simulation", { return "link_results", "link_id", set(RealtimeRepository.LINK_FIELDS)
"flow",
"friction",
"headloss",
"quality",
"reaction",
"setting",
"status",
"velocity",
}
raise ValueError(f"Unsupported element_type: {element_type}") raise ValueError(f"Unsupported element_type: {element_type}")
@@ -0,0 +1,315 @@
from datetime import datetime, timedelta
from typing import Any
from uuid import UUID
from psycopg import AsyncConnection, Connection, sql
from app.domain.time import parse_utc_time
class AnalysisResultsRepository:
NODE_FIELDS = {"actual_demand", "total_head", "pressure", "quality"}
LINK_FIELDS = {
"flow",
"friction",
"headloss",
"quality",
"reaction",
"setting",
"status",
"velocity",
}
@staticmethod
def prepare_simulation_rows(
node_results: list[dict[str, Any]],
link_results: list[dict[str, Any]],
result_start_time: str,
num_periods: int,
result_timestep_seconds: int,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
start_time = parse_utc_time(
result_start_time, field_name="result_start_time"
)
timestep = timedelta(seconds=result_timestep_seconds)
node_rows: list[dict[str, Any]] = []
for node_result in node_results:
for period_index, values in enumerate(
node_result.get("result", [])[:num_periods]
):
node_rows.append(
{
"time": start_time + timestep * period_index,
"node_id": node_result["node"],
"actual_demand": values.get("demand"),
"total_head": values.get("head"),
"pressure": values.get("pressure"),
"quality": values.get("quality"),
}
)
link_rows: list[dict[str, Any]] = []
for link_result in link_results:
for period_index, values in enumerate(
link_result.get("result", [])[:num_periods]
):
link_rows.append(
{
"time": start_time + timestep * period_index,
"link_id": link_result["link"],
**{field: values.get(field) for field in AnalysisResultsRepository.LINK_FIELDS},
}
)
return node_rows, link_rows
@staticmethod
async def store_results(
conn: AsyncConnection,
run_id: UUID,
node_rows: list[dict[str, Any]],
link_rows: list[dict[str, Any]],
) -> None:
async with conn.transaction(), conn.cursor() as cur:
await AnalysisResultsRepository._lock_run(cur, run_id)
await AnalysisResultsRepository._assert_run_is_empty(cur, run_id)
if node_rows:
async with cur.copy(
"COPY analysis.node_results "
"(time, run_id, node_id, actual_demand, total_head, pressure, quality) "
"FROM STDIN"
) as copy:
for row in node_rows:
await copy.write_row(
(
row["time"],
run_id,
row["node_id"],
row.get("actual_demand"),
row.get("total_head"),
row.get("pressure"),
row.get("quality"),
)
)
if link_rows:
async with cur.copy(
"COPY analysis.link_results "
"(time, run_id, link_id, flow, friction, headloss, quality, "
"reaction, setting, status, velocity) FROM STDIN"
) as copy:
for row in link_rows:
await copy.write_row(
(
row["time"],
run_id,
row["link_id"],
row.get("flow"),
row.get("friction"),
row.get("headloss"),
row.get("quality"),
row.get("reaction"),
row.get("setting"),
row.get("status"),
row.get("velocity"),
)
)
@staticmethod
async def _assert_run_is_empty(cur, run_id: UUID) -> None:
await cur.execute(
"""
SELECT EXISTS (
SELECT 1 FROM analysis.node_results WHERE run_id = %s
UNION ALL
SELECT 1 FROM analysis.link_results WHERE run_id = %s
) AS exists
""",
(run_id, run_id),
)
row = await cur.fetchone()
if row and row["exists"]:
raise ValueError(f"analysis results already exist for run {run_id}")
@staticmethod
async def _lock_run(cur, run_id: UUID) -> None:
await cur.execute(
"SELECT pg_advisory_xact_lock(hashtextextended(%s::text, 0))",
(run_id,),
)
@staticmethod
async def get_node_series(
conn: AsyncConnection,
run_id: UUID,
node_id: str,
start_time: datetime,
end_time: datetime,
field: str,
) -> list[dict[str, Any]]:
if field not in AnalysisResultsRepository.NODE_FIELDS:
raise ValueError(f"invalid node result field: {field}")
query = sql.SQL(
"SELECT time, {} AS value FROM analysis.node_results "
"WHERE run_id = %s AND node_id = %s AND time BETWEEN %s AND %s "
"ORDER BY time"
).format(sql.Identifier(field))
async with conn.cursor() as cur:
await cur.execute(query, (run_id, node_id, start_time, end_time))
return await cur.fetchall()
@staticmethod
async def get_link_series(
conn: AsyncConnection,
run_id: UUID,
link_id: str,
start_time: datetime,
end_time: datetime,
field: str,
) -> list[dict[str, Any]]:
if field not in AnalysisResultsRepository.LINK_FIELDS:
raise ValueError(f"invalid link result field: {field}")
query = sql.SQL(
"SELECT time, {} AS value FROM analysis.link_results "
"WHERE run_id = %s AND link_id = %s AND time BETWEEN %s AND %s "
"ORDER BY time"
).format(sql.Identifier(field))
async with conn.cursor() as cur:
await cur.execute(query, (run_id, link_id, start_time, end_time))
return await cur.fetchall()
@staticmethod
async def get_series_by_ids(
conn: AsyncConnection,
run_id: UUID,
element_type: str,
element_ids: list[str],
start_time: datetime,
end_time: datetime,
field: str,
) -> dict[str, list[dict[str, Any]]]:
if element_type == "node":
table_name, id_column, valid_fields = (
"node_results", "node_id", AnalysisResultsRepository.NODE_FIELDS
)
elif element_type == "link":
table_name, id_column, valid_fields = (
"link_results", "link_id", AnalysisResultsRepository.LINK_FIELDS
)
else:
raise ValueError(f"invalid analysis element type: {element_type}")
if field not in valid_fields:
raise ValueError(f"invalid {element_type} result field: {field}")
result: dict[str, list[dict[str, Any]]] = {
element_id: [] for element_id in element_ids
}
if not element_ids:
return result
query = sql.SQL(
"SELECT {} AS element_id, time, {} AS value FROM analysis.{} "
"WHERE run_id = %s AND {} = ANY(%s) AND time BETWEEN %s AND %s "
"ORDER BY {}, time"
).format(
sql.Identifier(id_column),
sql.Identifier(field),
sql.Identifier(table_name),
sql.Identifier(id_column),
sql.Identifier(id_column),
)
async with conn.cursor() as cur:
await cur.execute(query, (run_id, element_ids, start_time, end_time))
for row in await cur.fetchall():
result.setdefault(str(row["element_id"]), []).append(
{"time": row["time"], "value": row["value"]}
)
return result
@staticmethod
async def get_values_at_time(
conn: AsyncConnection,
run_id: UUID,
element_type: str,
result_time: datetime,
field: str,
) -> dict[str, Any]:
if element_type == "node":
table, id_column, fields = (
"node_results",
"node_id",
AnalysisResultsRepository.NODE_FIELDS,
)
elif element_type == "link":
table, id_column, fields = (
"link_results",
"link_id",
AnalysisResultsRepository.LINK_FIELDS,
)
else:
raise ValueError("element_type must be node or link")
if field not in fields:
raise ValueError(f"invalid {element_type} result field: {field}")
query = sql.SQL(
"SELECT {id_column}, {field} AS value FROM analysis.{table} "
"WHERE run_id = %s AND time = %s ORDER BY {id_column}"
).format(
id_column=sql.Identifier(id_column),
field=sql.Identifier(field),
table=sql.Identifier(table),
)
async with conn.cursor() as cur:
await cur.execute(query, (run_id, result_time))
return {row[id_column]: row["value"] for row in await cur.fetchall()}
@staticmethod
def store_results_sync(
conn: Connection,
run_id: UUID,
node_rows: list[dict[str, Any]],
link_rows: list[dict[str, Any]],
) -> None:
with conn.transaction(), conn.cursor() as cur:
cur.execute(
"SELECT pg_advisory_xact_lock(hashtextextended(%s::text, 0))",
(run_id,),
)
cur.execute(
"""
SELECT EXISTS (
SELECT 1 FROM analysis.node_results WHERE run_id = %s
UNION ALL
SELECT 1 FROM analysis.link_results WHERE run_id = %s
) AS exists
""",
(run_id, run_id),
)
row = cur.fetchone()
if row and row["exists"]:
raise ValueError(f"analysis results already exist for run {run_id}")
if node_rows:
with cur.copy(
"COPY analysis.node_results "
"(time, run_id, node_id, actual_demand, total_head, pressure, quality) "
"FROM STDIN"
) as copy:
for item in node_rows:
copy.write_row(
(
item["time"], run_id, item["node_id"],
item.get("actual_demand"), item.get("total_head"),
item.get("pressure"), item.get("quality"),
)
)
if link_rows:
with cur.copy(
"COPY analysis.link_results "
"(time, run_id, link_id, flow, friction, headloss, quality, "
"reaction, setting, status, velocity) FROM STDIN"
) as copy:
for item in link_rows:
copy.write_row(
(
item["time"], run_id, item["link_id"],
item.get("flow"), item.get("friction"),
item.get("headloss"), item.get("quality"),
item.get("reaction"), item.get("setting"),
item.get("status"), item.get("velocity"),
)
)

Some files were not shown because too many files have changed in this diff Show More