diff --git a/.env.example b/.env.example
index 212753a..75e9e4d 100644
--- a/.env.example
+++ b/.env.example
@@ -39,6 +39,15 @@ METADATA_DB_PORT="5432"
METADATA_DB_USER="tjwater"
METADATA_DB_PASSWORD="password"
+# Per-project synchronous connection pools
+PROJECT_PG_CACHE_SIZE="50"
+PROJECT_TS_CACHE_SIZE="50"
+PROJECT_PG_POOL_MIN_SIZE="0"
+PROJECT_PG_POOL_SIZE="5"
+PROJECT_PG_MAX_OVERFLOW="10"
+PROJECT_TS_POOL_MIN_SIZE="0"
+PROJECT_TS_POOL_MAX_SIZE="10"
+
# ============================================
# Keycloak JWT (可选)
# ============================================
diff --git a/.gitea/workflows/package.yml b/.gitea/workflows/package.yml
index 8a2ce3a..f88d263 100644
--- a/.gitea/workflows/package.yml
+++ b/.gitea/workflows/package.yml
@@ -8,17 +8,16 @@ on:
jobs:
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:
image_name: gitea.waternetwork.cn/orgtjwater/tjwater-backend
dockerfile: Dockerfile
build_context: .
test_command: |
- 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
+ python -m compileall -q app
+ python -c "import app.main"
+ python scripts/check_openapi.py
+ pytest -q tests
deploy_service: backend
deploy_host: 192.168.1.114
secrets:
diff --git a/.gitignore b/.gitignore
index 46780ee..cafd240 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,4 @@ build/
*.dump
.vscode/
app/algorithms/health/model/my_survival_forest_model_quxi.joblib
-inp/
+/inp/
diff --git a/Dockerfile b/Dockerfile
index f144e59..cfdb907 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -17,6 +17,11 @@ RUN uv pip install --system --no-cache-dir -r requirements.txt
# 本地数据目录和环境变量在运行时通过 Compose 挂载或注入,
# 不应进入镜像构建上下文。
COPY app ./app
+COPY contracts ./contracts
+COPY infra ./infra
+COPY resources ./resources
+COPY scripts/check_openapi.py ./scripts/check_openapi.py
+COPY tests ./tests
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}'" && \
rm -f app/algorithms/health/model/my_survival_forest_model_quxi.zip
RUN mkdir -p db_inp temp data inp
diff --git a/app/algorithms/sensor/__init__.py b/app/algorithms/sensor/__init__.py
index b500fc4..b5737cd 100644
--- a/app/algorithms/sensor/__init__.py
+++ b/app/algorithms/sensor/__init__.py
@@ -5,7 +5,7 @@ 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.infra.db.postgresql.sensor_placement import create_sensor_placement
from app.services.sensor_placement import (
SensorPlacementConflictError,
SensorPlacementValidationError,
@@ -50,18 +50,18 @@ def _sensor_inp_lock(name: str):
def _create_validated_placement(
name: str,
*,
- scheme_name: str,
+ run_name: str,
min_diameter: int,
- username: str,
- sensor_location: list[str],
+ created_by: str,
+ sensor_locations: list[str],
) -> dict[str, Any]:
- validate_sensor_placement_nodes(name, sensor_location)
+ validate_sensor_placement_nodes(name, sensor_locations)
return create_sensor_placement(
name,
- scheme_name=scheme_name,
+ run_name=run_name,
min_diameter=min_diameter,
- username=username,
- sensor_location=sensor_location,
+ created_by=created_by,
+ sensor_locations=sensor_locations,
)
@@ -89,10 +89,10 @@ def pressure_sensor_placement_sensitivity(
)
return _create_validated_placement(
name,
- scheme_name=scheme_name,
+ run_name=scheme_name,
min_diameter=min_diameter,
- username=username,
- sensor_location=sensor_location,
+ created_by=username,
+ sensor_locations=sensor_location,
)
@@ -124,8 +124,8 @@ def pressure_sensor_placement_kmeans(
)
return _create_validated_placement(
name,
- scheme_name=scheme_name,
+ run_name=scheme_name,
min_diameter=min_diameter,
- username=username,
- sensor_location=sensor_location,
+ created_by=username,
+ sensor_locations=sensor_location,
)
diff --git a/app/algorithms/simulation/runner.py b/app/algorithms/simulation/runner.py
index b59fa55..f25acd4 100644
--- a/app/algorithms/simulation/runner.py
+++ b/app/algorithms/simulation/runner.py
@@ -454,8 +454,12 @@ class DataLoader:
"""读取选定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")
+ patterns_info = read_all(
+ project_name,
+ "select flow, factor from network.pattern_flow_samples "
+ "where pattern_id = %s order by sequence_no",
+ (pattern_name,),
+ )
for item in patterns_info:
flow_list.append(float(item['flow']))
factors_list.append(float(item['factor']))
diff --git a/app/algorithms/simulation/scenarios.py b/app/algorithms/simulation/scenarios.py
index 2a36544..021696f 100644
--- a/app/algorithms/simulation/scenarios.py
+++ b/app/algorithms/simulation/scenarios.py
@@ -9,7 +9,6 @@ from app.algorithms.simulation.runner import (
run_simulation_ex,
from_clock_to_seconds_2,
)
-from app.services.scheme_management import store_scheme_info
from app.services.tjnetwork import (
ChangeSet,
OPTION_DEMAND_MODEL_PDA,
@@ -111,8 +110,6 @@ def burst_analysis(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Copying Database."
)
- # CopyProjectEx()(name, new_name,
- # ['operation', 'current_operation', 'restore_operation', 'batch_operation', 'operation_table'])
copy_project(name + "_template", new_name)
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
@@ -204,21 +201,15 @@ def burst_analysis(
modify_valve_opening=modify_valve_opening,
scheme_type="burst_analysis",
scheme_name=scheme_name,
+ result_db_name=name,
+ scheme_username=username,
+ scheme_detail=scheme_detail,
)
# step 3. restore the base model status
# execute_undo(name) #有疑惑
if is_project_open(new_name):
close_project(new_name)
delete_project(new_name)
- # 存储方案信息到 PG 数据库
- store_scheme_info(
- name=name,
- scheme_name=scheme_name,
- scheme_type="burst_analysis",
- username=username,
- scheme_start_time=modify_pattern_start_time,
- scheme_detail=scheme_detail,
- )
############################################################
@@ -253,8 +244,6 @@ def valve_close_analysis(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Copying Database."
)
- # CopyProjectEx()(name, new_name,
- # ['operation', 'current_operation', 'restore_operation', 'batch_operation', 'operation_table'])
copy_project(name + "_template", new_name)
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
@@ -293,6 +282,7 @@ def valve_close_analysis(
modify_valve_opening=modify_valve_opening,
scheme_type="valve_close_Analysis",
scheme_name=scheme_name,
+ result_db_name=name,
)
# step 3. restore the base model
# for valve in valves:
@@ -355,8 +345,6 @@ def flushing_analysis(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Copying Database."
)
- # CopyProjectEx()(name, new_name,
- # ['operation', 'current_operation', 'restore_operation', 'batch_operation', 'operation_table'])
copy_project(name + "_template", new_name)
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
@@ -456,21 +444,15 @@ def flushing_analysis(
valve_control=valve_control,
scheme_type="flushing_analysis",
scheme_name=scheme_name,
+ result_db_name=name,
+ scheme_username=username,
+ scheme_detail=scheme_detail,
)
# step 4. restore the base model
if is_project_open(new_name):
close_project(new_name)
delete_project(new_name)
# return result
- # 存储方案信息到 PG 数据库
- store_scheme_info(
- name=name,
- scheme_name=scheme_name,
- scheme_type="flushing_analysis",
- username=username,
- scheme_start_time=modify_pattern_start_time,
- scheme_detail=scheme_detail,
- )
############################################################
@@ -523,8 +505,6 @@ def contaminant_simulation(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Copying Database."
)
- # CopyProjectEx()(name, new_name,
- # ['operation', 'current_operation', 'restore_operation', 'batch_operation', 'operation_table'])
copy_project(name + "_template", new_name)
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
@@ -612,6 +592,9 @@ def contaminant_simulation(
modify_total_duration=modify_total_duration,
scheme_type="contaminant_analysis",
scheme_name=scheme_name,
+ result_db_name=name,
+ scheme_username=username,
+ scheme_detail=scheme_detail,
)
# for i in range(1,operation_step):
@@ -619,15 +602,6 @@ def contaminant_simulation(
if is_project_open(new_name):
close_project(new_name)
delete_project(new_name)
- # 存储方案信息到 PG 数据库
- store_scheme_info(
- name=name,
- scheme_name=scheme_name,
- scheme_type="contaminant_analysis",
- username=username,
- scheme_start_time=modify_pattern_start_time,
- scheme_detail=scheme_detail,
- )
############################################################
@@ -660,8 +634,6 @@ def age_analysis(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Copying Database."
)
- # CopyProjectEx()(name, new_name,
- # ['operation', 'current_operation', 'restore_operation', 'batch_operation', 'operation_table'])
copy_project(name + "_template", new_name)
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
@@ -743,8 +715,6 @@ def pressure_regulation(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Copying Database."
)
- # CopyProjectEx()(name, new_name,
- # ['operation', 'current_operation', 'restore_operation', 'batch_operation', 'operation_table'])
copy_project(name + "_template", new_name)
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
@@ -779,6 +749,7 @@ def pressure_regulation(
modify_variable_pump_pattern=modify_variable_pump_pattern,
scheme_type="pressure_regulation",
scheme_name=scheme_name,
+ result_db_name=name,
)
if is_project_open(new_name):
close_project(new_name)
diff --git a/app/algorithms/water_demand/__init__.py b/app/algorithms/water_demand/__init__.py
new file mode 100644
index 0000000..0d7bf25
--- /dev/null
+++ b/app/algorithms/water_demand/__init__.py
@@ -0,0 +1,19 @@
+"""Water-demand distribution algorithms."""
+
+from .service import (
+ calculate_demand_to_network,
+ calculate_demand_to_nodes,
+ calculate_demand_to_region,
+ distribute_demand_to_nodes,
+ distribute_demand_to_region,
+ get_total_base_demand,
+)
+
+__all__ = [
+ "calculate_demand_to_network",
+ "calculate_demand_to_nodes",
+ "calculate_demand_to_region",
+ "distribute_demand_to_nodes",
+ "distribute_demand_to_region",
+ "get_total_base_demand",
+]
diff --git a/app/native/wndb/s36_wda_cal.py b/app/algorithms/water_demand/service.py
similarity index 90%
rename from app/native/wndb/s36_wda_cal.py
rename to app/algorithms/water_demand/service.py
index 4583993..f6c4521 100644
--- a/app/native/wndb/s36_wda_cal.py
+++ b/app/algorithms/water_demand/service.py
@@ -1,8 +1,8 @@
-from .database import ChangeSet
-from .s0_base import is_junction, get_nodes
-from .s9_demands import get_demand
-from .s32_region_util import Topology, get_nodes_in_region
-from .batch_exe import execute_batch_command
+from app.native.wndb.commands.executor import execute_batch_command
+from app.native.wndb.core.database import ChangeSet
+from app.native.wndb.gis.region_geometry import Topology, get_nodes_in_region
+from app.native.wndb.model.demands import get_demand
+from app.native.wndb.model.elements import get_nodes, is_junction
DISTRIBUTION_TYPE_ADD = 'ADD'
@@ -101,4 +101,4 @@ def get_total_base_demand(name:str,region:str)->float:
ds = get_demand(name, node)['demands']
t_demands= t_demands+ds[0]['demand']
- return t_demands
\ No newline at end of file
+ return t_demands
diff --git a/app/api/v1/endpoints/burst_detection.py b/app/api/v1/endpoints/burst_detection.py
index d9a0d17..98163de 100644
--- a/app/api/v1/endpoints/burst_detection.py
+++ b/app/api/v1/endpoints/burst_detection.py
@@ -1,5 +1,6 @@
from datetime import datetime
from typing import Any
+from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Body
from pydantic import BaseModel, Field
@@ -43,8 +44,7 @@ class BurstDetectionRequest(BaseModel):
sensor_nodes: list[str] | None = Field(None, description="传感器节点列表")
scheme_name: str | None = Field(None, description="方案名称")
data_source: str = Field("monitoring", description="数据来源:monitoring(监测)或simulation(模拟)")
- simulation_scheme_name: str | None = Field(None, description="模拟方案名称")
- simulation_scheme_type: str | None = Field(None, description="模拟方案类型")
+ simulation_run_id: UUID | None = Field(None, description="分析模拟运行 ID")
@router.post(
diff --git a/app/api/v1/endpoints/burst_location.py b/app/api/v1/endpoints/burst_location.py
index e6f52d0..669acbb 100644
--- a/app/api/v1/endpoints/burst_location.py
+++ b/app/api/v1/endpoints/burst_location.py
@@ -2,6 +2,7 @@ from typing import Any
from datetime import datetime
from typing import Literal
+from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Body
from pydantic import BaseModel, Field
@@ -32,9 +33,8 @@ class BurstLocationRequest(BaseModel):
scada_normal_start: datetime | None = Field(None, description="监测数据正常工况开始时间")
scada_normal_end: datetime | None = Field(None, description="监测数据正常工况结束时间")
use_scada_flow: bool = Field(False, description="是否使用SCADA流量数据")
- scheme_name: str | None = Field(None, description="方案名称")
- simulation_scheme_name: str | None = Field(None, description="模拟方案名称")
- simulation_scheme_type: str | None = Field(None, description="模拟方案类型")
+ scheme_name: str | None = Field(None, description="爆管定位运行名称")
+ simulation_run_id: UUID | None = Field(None, description="分析模拟运行 ID")
@router.post(
diff --git a/app/api/v1/endpoints/components/controls.py b/app/api/v1/endpoints/components/controls.py
index d406f20..fd3b653 100644
--- a/app/api/v1/endpoints/components/controls.py
+++ b/app/api/v1/endpoints/components/controls.py
@@ -1,7 +1,6 @@
from fastapi import APIRouter, Request, Query, Path, Body
from typing import Any, List, Dict, Union
from app.services.tjnetwork import (
- Any,
ChangeSet,
get_control,
get_control_schema,
diff --git a/app/api/v1/endpoints/components/curves.py b/app/api/v1/endpoints/components/curves.py
index c462dab..81358f6 100644
--- a/app/api/v1/endpoints/components/curves.py
+++ b/app/api/v1/endpoints/components/curves.py
@@ -1,7 +1,6 @@
from fastapi import APIRouter, Request, Query, Path, Body
from typing import Any, List, Dict, Union
from app.services.tjnetwork import (
- Any,
ChangeSet,
add_curve,
delete_curve,
diff --git a/app/api/v1/endpoints/components/options.py b/app/api/v1/endpoints/components/options.py
index 21083ee..d89d422 100644
--- a/app/api/v1/endpoints/components/options.py
+++ b/app/api/v1/endpoints/components/options.py
@@ -1,7 +1,6 @@
from fastapi import APIRouter, Request, Query, Path, Body
from typing import Any, List, Dict, Union
from app.services.tjnetwork import (
- Any,
ChangeSet,
get_energy,
get_energy_schema,
diff --git a/app/api/v1/endpoints/components/patterns.py b/app/api/v1/endpoints/components/patterns.py
index bb6daea..81edfd3 100644
--- a/app/api/v1/endpoints/components/patterns.py
+++ b/app/api/v1/endpoints/components/patterns.py
@@ -1,7 +1,6 @@
from fastapi import APIRouter, Request, Query, Path, Body
from typing import Any, List, Dict, Union
from app.services.tjnetwork import (
- Any,
ChangeSet,
add_pattern,
delete_pattern,
diff --git a/app/api/v1/endpoints/components/quality.py b/app/api/v1/endpoints/components/quality.py
index db3c6ca..2f5e940 100644
--- a/app/api/v1/endpoints/components/quality.py
+++ b/app/api/v1/endpoints/components/quality.py
@@ -1,11 +1,9 @@
from fastapi import APIRouter, Request, Query, Path, Body
from typing import Any, List, Dict, Union
from app.services.tjnetwork import (
- Any,
ChangeSet,
add_mixing,
add_source,
- api,
delete_mixing,
delete_source,
get_emitter,
@@ -23,6 +21,7 @@ from app.services.tjnetwork import (
get_tank_reaction,
get_tank_reaction_schema,
set_emitter,
+ set_mixing,
set_pipe_reaction,
set_quality,
set_reaction,
@@ -270,7 +269,7 @@ async def fastapi_set_mixing(
更新指定水池的混合属性值。
"""
props = await req.json()
- return api.set_mixing(network, ChangeSet(props))
+ return set_mixing(network, ChangeSet(props))
@router.post("/mixing-configurations", response_model=None, summary="添加混合", description="在网络中添加一个新的混合")
async def fastapi_add_mixing(
diff --git a/app/api/v1/endpoints/components/visuals.py b/app/api/v1/endpoints/components/visuals.py
index 7764d86..571fac8 100644
--- a/app/api/v1/endpoints/components/visuals.py
+++ b/app/api/v1/endpoints/components/visuals.py
@@ -1,7 +1,6 @@
from fastapi import APIRouter, Request, Query, Path, Body, Response
from typing import Any, List, Dict, Union
from app.services.tjnetwork import (
- Any,
ChangeSet,
add_label,
add_vertex,
diff --git a/app/api/v1/endpoints/extension.py b/app/api/v1/endpoints/extension.py
deleted file mode 100644
index d9ce025..0000000
--- a/app/api/v1/endpoints/extension.py
+++ /dev/null
@@ -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
diff --git a/app/api/v1/endpoints/misc.py b/app/api/v1/endpoints/misc.py
deleted file mode 100644
index 255fe47..0000000
--- a/app/api/v1/endpoints/misc.py
+++ /dev/null
@@ -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
diff --git a/app/api/v1/endpoints/network/demands.py b/app/api/v1/endpoints/network/demands.py
index ac63be1..16a169f 100644
--- a/app/api/v1/endpoints/network/demands.py
+++ b/app/api/v1/endpoints/network/demands.py
@@ -1,7 +1,6 @@
from fastapi import APIRouter, Request, Query, Path, Body
from typing import Any, List, Dict, Union
from app.services.tjnetwork import (
- Any,
ChangeSet,
calculate_demand_to_network,
calculate_demand_to_nodes,
diff --git a/app/api/v1/endpoints/network/general.py b/app/api/v1/endpoints/network/general.py
index 0873800..c87cfed 100644
--- a/app/api/v1/endpoints/network/general.py
+++ b/app/api/v1/endpoints/network/general.py
@@ -1,7 +1,6 @@
from fastapi import APIRouter, Request, Query, Path, Body
from typing import Any, List, Dict, Union
from app.services.tjnetwork import (
- Any,
ChangeSet,
delete_junction,
delete_pipe,
diff --git a/app/api/v1/endpoints/network/junctions.py b/app/api/v1/endpoints/network/junctions.py
index b317ae5..7a6c06c 100644
--- a/app/api/v1/endpoints/network/junctions.py
+++ b/app/api/v1/endpoints/network/junctions.py
@@ -1,7 +1,6 @@
from fastapi import APIRouter, Request, Query, Path, Body
from typing import Any, List, Dict, Union
from app.services.tjnetwork import (
- Any,
ChangeSet,
add_junction,
delete_junction,
diff --git a/app/api/v1/endpoints/network/pipes.py b/app/api/v1/endpoints/network/pipes.py
index c149771..a500180 100644
--- a/app/api/v1/endpoints/network/pipes.py
+++ b/app/api/v1/endpoints/network/pipes.py
@@ -1,7 +1,6 @@
from fastapi import APIRouter, Request, Query, Path, Body
from typing import Any, List, Dict, Union
from app.services.tjnetwork import (
- Any,
ChangeSet,
PIPE_STATUS_OPEN,
add_pipe,
diff --git a/app/api/v1/endpoints/network/pumps.py b/app/api/v1/endpoints/network/pumps.py
index 2ef9431..3e6a85a 100644
--- a/app/api/v1/endpoints/network/pumps.py
+++ b/app/api/v1/endpoints/network/pumps.py
@@ -1,7 +1,6 @@
from fastapi import APIRouter, Request, Query, Path, Body
from typing import Any, List, Dict, Union
from app.services.tjnetwork import (
- Any,
ChangeSet,
add_pump,
delete_pump,
diff --git a/app/api/v1/endpoints/network/regions.py b/app/api/v1/endpoints/network/regions.py
index 833852b..52ef2ee 100644
--- a/app/api/v1/endpoints/network/regions.py
+++ b/app/api/v1/endpoints/network/regions.py
@@ -1,534 +1,73 @@
-from fastapi import APIRouter, Request, Query, Path, Body
-from typing import Any, List, Dict, Union
+from typing import Any
+
+from fastapi import APIRouter, Query, Request
+
from app.services.tjnetwork import (
- Any,
ChangeSet,
- add_district_metering_area,
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_service_area,
- 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_nodes_in_region,
get_region,
get_region_schema,
- get_service_area,
- get_service_area_schema,
- get_virtual_district,
- get_virtual_district_schema,
- set_district_metering_area,
+ get_regions,
set_region,
- set_service_area,
- set_virtual_district,
)
+
router = APIRouter()
-############################################################
-# region 32
-############################################################
-@router.get(
- "/network-schemas/region",
- summary="获取区域属性架构",
- description="获取指定水网的区域属性架构定义"
-)
-async def fastapi_get_region_schema(
- network: str = Query(..., description="管网名称(或数据库名称)")
+@router.get("/network-schemas/region", summary="获取区域属性架构")
+async def get_region_schema_endpoint(
+ network: str = Query(..., description="管网名称(或数据库名称)"),
) -> dict[str, dict[str, Any]]:
- """获取区域的属性架构。"""
return get_region_schema(network)
-@router.get(
- "/regions/detail",
- summary="获取区域信息",
- description="获取指定ID的区域详细信息"
-)
-async def fastapi_get_region(
+
+@router.get("/regions", summary="获取区域列表")
+async def get_regions_endpoint(
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="获取区域信息")
+async def get_region_endpoint(
+ network: str = Query(..., description="管网名称(或数据库名称)"),
+ id: str = Query(..., description="区域 ID"),
) -> dict[str, Any]:
- """获取区域的详细信息。"""
return get_region(network, id)
-@router.patch(
- "/regions",
- response_model=None,
- summary="设置区域属性",
- description="修改指定区域的属性信息"
-)
-async def fastapi_set_region(
+
+@router.get("/regions/nodes", summary="获取区域节点")
+async def get_region_nodes_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
- req: Request = None
-) -> 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="管网名称(或数据库名称)")
+ id: str = Query(..., description="区域 ID"),
) -> list[str]:
- """获取所有DMA的ID列表。"""
- return get_all_district_metering_area_ids(network)
+ return get_nodes_in_region(network, id)
-@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(
- "/district-metering-area-generation-runs",
- response_model=None,
- summary="生成DMA分区",
- description="根据参数自动生成水网的DMA分区方案"
-)
-async def fastapi_generate_district_metering_area(
+@router.patch("/regions", summary="修改区域", response_model=None)
+async def set_region_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
- part_count: int = Query(..., description="分区数量", gt=0),
- part_type: int = Query(..., description="分区类型"),
- inflate_delta: float = Query(..., description="膨胀参数")
+ request: Request = None,
) -> ChangeSet:
- """生成DMA分区。"""
- return generate_district_metering_area(
- network, part_count, part_type, inflate_delta
- )
+ return set_region(network, ChangeSet(await request.json()))
-@router.post(
- "/sub-district-metering-areas",
- response_model=None,
- summary="生成DMA子分区",
- description="为指定DMA生成子DMA分区"
-)
-async def fastapi_generate_sub_district_metering_area(
+
+@router.post("/regions", summary="添加区域", response_model=None)
+async def add_region_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
- dma: str = Query(..., description="DMA ID"),
- part_count: int = Query(..., description="分区数量", gt=0),
- part_type: int = Query(..., description="分区类型"),
- inflate_delta: float = Query(..., description="膨胀参数")
+ request: Request = None,
) -> ChangeSet:
- """生成DMA子分区。"""
- return generate_sub_district_metering_area(
- network, dma, part_count, part_type, inflate_delta
- )
+ payload = await request.json()
+ payload["boundary"] = [tuple(point[:2]) for point in payload.get("boundary", [])]
+ return add_region(network, ChangeSet(payload))
-############################################################
-# service_area 34
-############################################################
-
-@router.post(
- "/service-area-calculations",
- summary="计算服务区",
- description="计算指定水网的服务区分区,返回全部时间步结果"
-)
-async def fastapi_calculate_service_area(
+@router.delete("/regions", summary="删除区域", response_model=None)
+async def delete_region_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
-) -> list[dict[str, list[str]]]:
- """计算服务区分区,返回全部时间步结果。"""
- 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
+ request: Request = None,
) -> ChangeSet:
- """设置服务区属性。"""
- 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
- )
+ return delete_region(network, ChangeSet(await request.json()))
diff --git a/app/api/v1/endpoints/network/reservoirs.py b/app/api/v1/endpoints/network/reservoirs.py
index c2e29c0..2b1d018 100644
--- a/app/api/v1/endpoints/network/reservoirs.py
+++ b/app/api/v1/endpoints/network/reservoirs.py
@@ -1,7 +1,6 @@
from fastapi import APIRouter, Request, Query, Path, Body
from typing import Any, List, Dict, Union
from app.services.tjnetwork import (
- Any,
ChangeSet,
add_reservoir,
delete_reservoir,
diff --git a/app/api/v1/endpoints/network/tags.py b/app/api/v1/endpoints/network/tags.py
index 6a0964e..83be913 100644
--- a/app/api/v1/endpoints/network/tags.py
+++ b/app/api/v1/endpoints/network/tags.py
@@ -1,7 +1,6 @@
from fastapi import APIRouter, Request, Query, Path, Body
from typing import Any, List, Dict, Union
from app.services.tjnetwork import (
- Any,
ChangeSet,
get_tag,
get_tag_schema,
diff --git a/app/api/v1/endpoints/network/tanks.py b/app/api/v1/endpoints/network/tanks.py
index 1d5e84d..bc08fc6 100644
--- a/app/api/v1/endpoints/network/tanks.py
+++ b/app/api/v1/endpoints/network/tanks.py
@@ -1,7 +1,6 @@
from fastapi import APIRouter, Request, Query, Path, Body
from typing import Any, List, Dict, Union
from app.services.tjnetwork import (
- Any,
ChangeSet,
add_tank,
delete_tank,
diff --git a/app/api/v1/endpoints/network/valves.py b/app/api/v1/endpoints/network/valves.py
index d9244e8..550b46a 100644
--- a/app/api/v1/endpoints/network/valves.py
+++ b/app/api/v1/endpoints/network/valves.py
@@ -1,7 +1,6 @@
from fastapi import APIRouter, Request, Query, Path, Body
from typing import Any, List, Dict, Union
from app.services.tjnetwork import (
- Any,
ChangeSet,
VALVES_TYPE_PRV,
add_valve,
diff --git a/app/api/v1/endpoints/project.py b/app/api/v1/endpoints/project.py
index aeeebfb..4e5ce40 100644
--- a/app/api/v1/endpoints/project.py
+++ b/app/api/v1/endpoints/project.py
@@ -10,8 +10,6 @@ from app.auth.permissions import (
)
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 (
ChangeSet,
list_project,
@@ -26,11 +24,7 @@ from app.services.tjnetwork import (
read_inp,
dump_inp,
get_all_vertices,
- get_all_scada_elements,
- get_all_district_metering_areas,
- get_all_service_areas,
- get_all_virtual_districts,
- get_extension_data,
+ get_all_scada_info,
convert_inp_v3_to_v2,
)
@@ -137,27 +131,6 @@ async def open_project_endpoint(
"""
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="将指定项目从内存中卸载,释放资源。")
@@ -202,24 +175,11 @@ async def export_inp_endpoint(
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
+ op["scada"] = json.dumps(get_all_scada_info(network))
close_project(network)
return cs
-
@router.post("/projects/current/imports", summary="读取 INP 文件到项目", description="从服务器文件系统中读取指定的 INP 文件并加载到项目中。")
async def read_inp_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
@@ -353,19 +313,7 @@ async def fastapi_convert_v3_to_v2(
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
+ op["scada"] = json.dumps(get_all_scada_info(network))
close_project(network)
@@ -496,19 +444,7 @@ async def fastapi_convert_v3_to_v2(
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
+ op["scada"] = json.dumps(get_all_scada_info(network))
close_project(network)
diff --git a/app/api/v1/endpoints/project_data.py b/app/api/v1/endpoints/project_data.py
index dcf333a..d4706d3 100644
--- a/app/api/v1/endpoints/project_data.py
+++ b/app/api/v1/endpoints/project_data.py
@@ -1,8 +1,10 @@
-from fastapi import APIRouter, Depends, HTTPException, Path, Query
+from uuid import UUID
+
+from fastapi import APIRouter, Depends, HTTPException, Query
from psycopg import AsyncConnection
from app.infra.db.postgresql.scada import ScadaInfoRepository
-from app.infra.db.postgresql.scheme import SchemeRepository
+from app.infra.db.postgresql.analysis import AnalysisRepository
from app.auth.project_dependencies import get_project_pg_connection
router = APIRouter()
@@ -33,8 +35,8 @@ async def get_scada_info_with_connection(
)
-@router.get("/schemes/list-with-connection", summary="获取方案列表", description="使用连接池查询所有方案信息")
-async def get_scheme_list_with_connection(
+@router.get("/analysis/runs", summary="获取分析运行列表")
+async def get_analysis_runs(
conn: AsyncConnection = Depends(get_database_connection),
):
"""
@@ -43,14 +45,15 @@ async def get_scheme_list_with_connection(
返回项目中所有方案的详细信息
"""
try:
- scheme_data = await SchemeRepository.get_schemes(conn)
- return {"success": True, "data": scheme_data, "count": len(scheme_data)}
+ runs = await AnalysisRepository.list_runs(conn)
+ return {"success": True, "data": runs, "count": len(runs)}
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="使用连接池查询所有爆管定位结果")
-async def get_burst_locate_result_with_connection(
+@router.get("/analysis/runs/{run_id}", summary="获取分析运行")
+async def get_analysis_run(
+ run_id: UUID,
conn: AsyncConnection = Depends(get_database_connection),
):
"""
@@ -59,17 +62,22 @@ async def get_burst_locate_result_with_connection(
返回项目中所有的爆管定位分析结果
"""
try:
- burst_data = await SchemeRepository.get_burst_locate_results(conn)
- return {"success": True, "data": burst_data, "count": len(burst_data)}
+ run = await AnalysisRepository.get_run(conn, run_id)
+ if run is None:
+ raise HTTPException(status_code=404, detail="分析运行不存在")
+ return run
+ except HTTPException:
+ raise
except Exception as e:
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查询对应的爆管定位结果")
-async def get_burst_locate_result_by_incident(
- burst_incident: str = Path(..., description="爆管事件ID"),
+@router.get("/analysis/runs/{run_id}/results", summary="获取分析结果")
+async def get_analysis_results(
+ run_id: UUID,
+ result_type: str | None = Query(default=None, description="结果类型"),
conn: AsyncConnection = Depends(get_database_connection),
):
"""
@@ -79,11 +87,9 @@ async def get_burst_locate_result_by_incident(
burst_incident: 爆管事件的唯一标识符
"""
try:
- return await SchemeRepository.get_burst_locate_result_by_incident(
- conn, burst_incident
- )
+ return await AnalysisRepository.list_results(conn, run_id, result_type)
except Exception as e:
raise HTTPException(
status_code=500,
- detail=f"根据 burst_incident 查询爆管定位结果时发生错误: {str(e)}",
+ detail=f"查询分析结果时发生错误: {str(e)}",
)
diff --git a/app/api/v1/endpoints/risk.py b/app/api/v1/endpoints/risk.py
deleted file mode 100644
index 58a2b02..0000000
--- a/app/api/v1/endpoints/risk.py
+++ /dev/null
@@ -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)
diff --git a/app/api/v1/endpoints/scada.py b/app/api/v1/endpoints/scada.py
index 48f822c..4c2f3db 100644
--- a/app/api/v1/endpoints/scada.py
+++ b/app/api/v1/endpoints/scada.py
@@ -1,525 +1,34 @@
from typing import Any
-from fastapi import APIRouter, Request, Query
+
+from fastapi import APIRouter, Query
+
from app.services.tjnetwork import (
- ChangeSet,
- get_scada_info,
get_all_scada_info,
- get_scada_device_schema,
- get_scada_device,
- 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,
get_scada_info_schema,
)
+
router = APIRouter()
-async def fast_get_scada_properties(
- network: str = Query(..., description="管网名称(或数据库名称)"),
- scada: str = Query(..., description="SCADA设备ID")
-) -> dict[str, Any]:
- """
- 获取单个SCADA设备的属性信息
-
- 根据管网名称和SCADA设备ID获取该设备的完整属性。
-
- Args:
- network: 管网名称(或数据库名称)
- scada: SCADA设备ID
-
- Returns:
- SCADA设备的属性字典
- """
- return get_scada_info(network, scada)
-async def fast_get_all_scada_properties(
- network: str = Query(..., description="管网名称(或数据库名称)")
-) -> list[dict[str, Any]]:
- """
- 获取指定管网所有SCADA设备的属性信息
-
- 查询该管网下所有已配置的SCADA设备的属性列表。
-
- Args:
- network: 管网名称(或数据库名称)
-
- Returns:
- SCADA设备属性列表
- """
- return get_all_scada_info(network)
-
-
-############################################################
-# scada_device 设备管理
-############################################################
-
-@router.get("/network-schemas/scada-device", summary="获取SCADA设备架构", tags=["SCADA设备"])
-async def fastapi_get_scada_device_schema(
- network: str = Query(..., description="管网名称(或数据库名称)")
-) -> dict[str, dict[str, Any]]:
- """
- 获取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]:
- """
- 获取单个SCADA设备的信息
-
- 根据设备ID查询该设备的详细信息。
-
- 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(
+@router.get("/network-schemas/scada-device", summary="获取 SCADA 设备结构")
+async def get_scada_device_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="管网名称(或数据库名称)")
+@router.get("/scada-devices", summary="获取 SCADA 设备列表")
+async def get_scada_devices(
+ network: str = Query(..., description="管网名称(或数据库名称)"),
) -> list[dict[str, Any]]:
- """
- 获取指定管网所有SCADA的信息
-
- 查询该管网下所有已配置的SCADA的完整信息。
-
- Args:
- network: 管网名称(或数据库名称)
-
- Returns:
- SCADA信息列表
- """
return get_all_scada_info(network)
+
+
+@router.get("/scada-devices/detail", summary="获取 SCADA 设备")
+async def get_scada_device(
+ network: str = Query(..., description="管网名称(或数据库名称)"),
+ device_id: str = Query(..., description="SCADA 设备 ID"),
+) -> dict[str, Any]:
+ return get_scada_info(network, device_id)
diff --git a/app/api/v1/endpoints/schemes.py b/app/api/v1/endpoints/schemes.py
deleted file mode 100644
index ff365cc..0000000
--- a/app/api/v1/endpoints/schemes.py
+++ /dev/null
@@ -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
diff --git a/app/api/v1/endpoints/sensor_placement.py b/app/api/v1/endpoints/sensor_placement.py
index 5ebdd25..ba3bf16 100644
--- a/app/api/v1/endpoints/sensor_placement.py
+++ b/app/api/v1/endpoints/sensor_placement.py
@@ -1,6 +1,7 @@
import logging
from typing import Any
from urllib.parse import quote
+from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
from fastapi.responses import StreamingResponse
@@ -26,8 +27,9 @@ from app.services.sensor_placement import (
build_sensor_placement_workbook,
can_edit_sensor_placement,
get_sensor_placement_candidate,
- get_sensor_placement_scheme,
- update_sensor_placement_scheme,
+ get_sensor_placement_run,
+ list_sensor_placement_runs,
+ update_sensor_placement_run,
)
router = APIRouter()
@@ -74,19 +76,19 @@ def _service_http_error(exc: Exception) -> HTTPException:
)
-def _get_scheme_response(
+def _get_run_response(
network: str,
- scheme_id: int,
+ run_id: UUID,
current_user: Any,
project_context: ProjectContext,
) -> dict[str, Any]:
try:
- scheme = get_sensor_placement_scheme(network, scheme_id)
+ run = get_sensor_placement_run(network, run_id)
return {
- **scheme,
+ **run,
"can_edit": (
_can_modify_project(project_context)
- and can_edit_sensor_placement(current_user, scheme)
+ and can_edit_sensor_placement(current_user, run)
),
}
except (
@@ -116,7 +118,7 @@ async def get_sensor_placement_candidate_detail(
@router.post(
- "/sensor-placement-optimization-runs",
+ "/sensor-placement-runs",
response_model=SensorPlacementSchemeResponse,
summary="创建并返回监测点优化方案",
)
@@ -136,13 +138,13 @@ async def optimize_sensor_placement_scheme(
created = await run_in_threadpool(
optimizer,
name=network,
- scheme_name=payload.scheme_name,
+ scheme_name=payload.run_name,
sensor_number=payload.sensor_count,
min_diameter=payload.min_diameter,
username=current_user.username,
)
- scheme = get_sensor_placement_scheme(network, int(created["id"]))
- return {**scheme, "can_edit": True}
+ run = get_sensor_placement_run(network, created["run_id"])
+ return {**run, "can_edit": True}
except (
SensorPlacementConflictError,
SensorPlacementValidationError,
@@ -158,31 +160,45 @@ async def optimize_sensor_placement_scheme(
@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),
+) -> 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,
summary="获取监测点方案详情",
)
-async def get_sensor_placement_scheme_detail(
- scheme_id: int,
+async def get_sensor_placement_run_detail(
+ run_id: UUID,
network: str = Query(..., min_length=1),
project_context: ProjectContext = Depends(get_project_context),
current_user=Depends(get_current_metadata_user),
) -> dict[str, Any]:
- return _get_scheme_response(
+ return _get_run_response(
_project_network(network, project_context),
- scheme_id,
+ run_id,
current_user,
project_context,
)
@router.put(
- "/sensor-placement-schemes/{scheme_id}",
+ "/sensor-placement-runs/{run_id}",
response_model=SensorPlacementSchemeResponse,
summary="覆盖保存监测点方案",
)
-async def overwrite_sensor_placement_scheme(
- scheme_id: int,
+async def overwrite_sensor_placement_run(
+ run_id: UUID,
payload: SensorPlacementUpdateRequest,
network: str = Query(..., min_length=1),
project_context: ProjectContext = Depends(get_project_context),
@@ -190,21 +206,21 @@ async def overwrite_sensor_placement_scheme(
) -> dict[str, Any]:
network = _project_network(network, project_context)
_require_project_write(project_context)
- scheme = _get_scheme_response(
+ run = _get_run_response(
network,
- scheme_id,
+ run_id,
current_user,
project_context,
)
- if not scheme["can_edit"]:
- raise HTTPException(status_code=403, detail="无权修改该监测点方案")
+ if not run["can_edit"]:
+ raise HTTPException(status_code=403, detail="无权修改该监测点优化运行")
try:
- updated = update_sensor_placement_scheme(
+ updated = update_sensor_placement_run(
network,
- scheme_id,
- expected_sensor_location=payload.expected_sensor_location,
- sensor_location=payload.sensor_location,
+ run_id,
+ expected_sensor_locations=payload.expected_sensor_locations,
+ sensor_locations=payload.sensor_locations,
)
return {**updated, "can_edit": True}
except (
@@ -216,26 +232,26 @@ async def overwrite_sensor_placement_scheme(
@router.post(
- "/sensor-placement-schemes/{scheme_id}/exports/excel",
+ "/sensor-placement-runs/{run_id}/exports/excel",
summary="导出监测点工程清单",
)
async def export_sensor_placement_excel(
- scheme_id: int,
+ run_id: UUID,
payload: SensorPlacementExportRequest,
network: str = Query(..., min_length=1),
project_context: ProjectContext = Depends(get_project_context),
current_user=Depends(get_current_metadata_user),
) -> StreamingResponse:
network = _project_network(network, project_context)
- scheme = _get_scheme_response(
+ run = _get_run_response(
network,
- scheme_id,
+ run_id,
current_user,
project_context,
)
if (
- payload.sensor_location != scheme["sensor_location"]
- and not scheme["can_edit"]
+ payload.sensor_locations != run["sensor_locations"]
+ and not run["can_edit"]
):
raise HTTPException(status_code=403, detail="无权导出该方案的未保存草稿")
@@ -243,14 +259,14 @@ async def export_sensor_placement_excel(
workbook = await run_in_threadpool(
build_sensor_placement_workbook,
network=network,
- scheme=scheme,
- sensor_location=payload.sensor_location,
+ scheme=run,
+ sensor_location=payload.sensor_locations,
adjustment_status=payload.adjustment_status,
)
except SensorPlacementValidationError as exc:
raise _service_http_error(exc) from exc
- filename = f"{scheme['scheme_name']}_监测点清单.xlsx"
+ filename = f"{run['name']}_监测点清单.xlsx"
encoded_filename = quote(filename)
return StreamingResponse(
workbook,
diff --git a/app/api/v1/endpoints/simulation.py b/app/api/v1/endpoints/simulation.py
index b561359..17da470 100644
--- a/app/api/v1/endpoints/simulation.py
+++ b/app/api/v1/endpoints/simulation.py
@@ -22,11 +22,6 @@ from app.algorithms.simulation.scenarios import (
# scheduling_analysis,
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,
@@ -108,14 +103,6 @@ class PumpFailureState(BaseModel):
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(
network_name: str, start_time: datetime, duration: int
) -> None:
@@ -663,154 +650,6 @@ async def fastapi_pump_failure(data: PumpFailureState = Body(..., description="
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"
-
-
@router.post("/simulation-runs", summary="手动运行日期指定模拟", description="根据指定的开始时间和持续时间,手动运行水力模拟。开始时间必须是显式带时区的 ISO 8601 / RFC3339 时间。")
async def fastapi_run_simulation_manually_by_date(
data: RunSimulationManuallyByDate = Body(..., description="模拟运行参数"),
@@ -829,30 +668,6 @@ async def fastapi_run_simulation_manually_by_date(
item = data.model_dump()
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")
run_simulation_manually_by_date(
item["name"], start_time, item["duration"]
diff --git a/app/api/v1/endpoints/snapshots.py b/app/api/v1/endpoints/snapshots.py
deleted file mode 100644
index 2d6e245..0000000
--- a/app/api/v1/endpoints/snapshots.py
+++ /dev/null
@@ -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)
diff --git a/app/api/v1/endpoints/timeseries/analysis.py b/app/api/v1/endpoints/timeseries/analysis.py
new file mode 100644
index 0000000..763146c
--- /dev/null
+++ b/app/api/v1/endpoints/timeseries/analysis.py
@@ -0,0 +1,82 @@
+from datetime import datetime
+from uuid import UUID
+
+from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
+from psycopg import AsyncConnection
+
+from app.infra.db.timescaledb.repositories.analysis import AnalysisResultsRepository
+
+from .dependencies import get_timescale_connection
+
+router = APIRouter()
+
+
+@router.post("/timeseries/analysis/runs/{run_id}/results", status_code=201)
+async def store_analysis_results(
+ run_id: UUID = Path(..., description="分析运行 ID"),
+ payload: dict = Body(...),
+ conn: AsyncConnection = Depends(get_timescale_connection),
+):
+ try:
+ node_rows = payload.get("node_results", [])
+ link_rows = payload.get("link_results", [])
+ await AnalysisResultsRepository.store_results(
+ conn, run_id, node_rows, link_rows
+ )
+ return {
+ "run_id": run_id,
+ "node_count": len(node_rows),
+ "link_count": len(link_rows),
+ }
+ except ValueError as exc:
+ raise HTTPException(status_code=409, detail=str(exc)) from exc
+
+
+@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
diff --git a/app/api/v1/endpoints/timeseries/composite.py b/app/api/v1/endpoints/timeseries/composite.py
index 7ac740d..06ea8dd 100644
--- a/app/api/v1/endpoints/timeseries/composite.py
+++ b/app/api/v1/endpoints/timeseries/composite.py
@@ -1,6 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from datetime import datetime
from psycopg import AsyncConnection
+from uuid import UUID
from app.infra.db.timescaledb.composite_queries import CompositeQueries
from .dependencies import get_timescale_connection, get_postgres_connection
@@ -13,8 +14,7 @@ async def get_scada_associated_simulation_data(
start_time: datetime = Query(..., description="查询开始时间"),
end_time: datetime = Query(..., description="查询结束时间"),
device_ids: str = Query(..., description="SCADA设备ID列表,逗号分隔"),
- scheme_type: str = Query(None, description="方案类型,若为空则查询实时数据"),
- scheme_name: str = Query(None, description="方案名称,若为空则查询实时数据"),
+ run_id: UUID | None = Query(None, description="分析运行 ID;为空时查询实时数据"),
timescale_conn: AsyncConnection = Depends(get_timescale_connection),
postgres_conn: AsyncConnection = Depends(get_postgres_connection),
):
@@ -22,14 +22,13 @@ async def get_scada_associated_simulation_data(
获取SCADA关联的link/node模拟值
根据传入的SCADA device_ids,找到关联的link/node,
- 并根据对应的type,查询对应的模拟数据。支持查询实时或方案数据。
+ 并根据对应的type,查询对应的模拟数据。支持查询实时或分析运行数据。
Args:
start_time: 查询开始时间
end_time: 查询结束时间
device_ids: SCADA设备ID列表,用逗号分隔
- scheme_type: 方案类型,若为空则查询实时数据
- scheme_name: 方案名称,若为空则查询实时数据
+ run_id: 分析运行 ID,若为空则查询实时数据
timescale_conn: TimescaleDB连接
postgres_conn: PostgreSQL连接
@@ -46,15 +45,14 @@ async def get_scada_associated_simulation_data(
else []
)
- if scheme_type and scheme_name:
- result = await CompositeQueries.get_scada_associated_scheme_simulation_data(
+ if run_id is not None:
+ result = await CompositeQueries.get_scada_associated_analysis_simulation_data(
timescale_conn,
postgres_conn,
device_ids_list,
start_time,
end_time,
- scheme_type,
- scheme_name,
+ run_id,
)
else:
result = (
@@ -80,23 +78,21 @@ async def get_feature_simulation_data(
feature_infos: str = Query(
..., description="特征信息,格式: id1:type1,id2:type2,type为pipe(管道)或junction(节点)"
),
- scheme_type: str = Query(None, description="方案类型,若为空则查询实时数据"),
- scheme_name: str = Query(None, description="方案名称,若为空则查询实时数据"),
+ run_id: UUID | None = Query(None, description="分析运行 ID;为空时查询实时数据"),
timescale_conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
获取link/node模拟值
根据传入的featureInfos,找到关联的link/node,
- 并根据对应的type,查询对应的模拟数据。支持查询实时或方案数据。
+ 并根据对应的type,查询对应的模拟数据。支持查询实时或分析运行数据。
Args:
start_time: 查询开始时间
end_time: 查询结束时间
feature_infos: 格式为 "element_id1:type1,element_id2:type2"
例如: "P1:pipe,J1:junction"
- scheme_type: 方案类型,若为空则查询实时数据
- scheme_name: 方案名称,若为空则查询实时数据
+ run_id: 分析运行 ID,若为空则查询实时数据
timescale_conn: TimescaleDB连接
Returns:
@@ -119,14 +115,13 @@ async def get_feature_simulation_data(
if not feature_infos_list:
raise HTTPException(status_code=400, detail="feature_infos cannot be empty")
- if scheme_type and scheme_name:
- result = await CompositeQueries.get_scheme_simulation_data(
+ if run_id is not None:
+ result = await CompositeQueries.get_analysis_simulation_data(
timescale_conn,
feature_infos_list,
start_time,
end_time,
- scheme_type,
- scheme_name,
+ run_id,
)
else:
result = await CompositeQueries.get_realtime_simulation_data(
diff --git a/app/api/v1/endpoints/timeseries/scheme.py b/app/api/v1/endpoints/timeseries/scheme.py
deleted file mode 100644
index 0c56a75..0000000
--- a/app/api/v1/endpoints/timeseries/scheme.py
+++ /dev/null
@@ -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))
diff --git a/app/api/v1/router.py b/app/api/v1/router.py
index 89133fe..1cd7a6d 100644
--- a/app/api/v1/router.py
+++ b/app/api/v1/router.py
@@ -7,20 +7,15 @@ from app.api.v1.endpoints import (
audit,
burst_detection,
burst_location,
- extension,
geocoding,
leakage,
meta,
- misc,
model_import,
project,
project_data,
- risk,
scada,
- schemes,
sensor_placement,
simulation,
- snapshots,
web_search,
)
from app.api.v1.endpoints.components import (
@@ -45,15 +40,14 @@ from app.api.v1.endpoints.network import (
valves,
)
from app.api.v1.endpoints.timeseries import (
+ analysis as ts_analysis,
composite as ts_composite,
realtime as ts_realtime,
scada as ts_scada,
- scheme as ts_scheme,
)
from app.auth.permissions import (
BURST_RUN,
OPTIMIZATION_RUN,
- RISK_RUN,
SCADA_CLEAN,
SCADA_VIEW,
SIMULATION_RUN,
@@ -88,7 +82,6 @@ simulation_access = Depends(
webgis_view_access = Depends(require_permission(WEBGIS_VIEW))
simulation_run_access = Depends(require_permission(SIMULATION_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))
# Core services
@@ -139,32 +132,16 @@ api_router.include_router(
tags=["Simulation Control"],
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(
sensor_placement.router,
tags=["Sensor Placement"],
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(
web_search.router,
tags=["Web Search"],
@@ -194,7 +171,7 @@ api_router.include_router(
# TimescaleDB data
for endpoint_router, tag in (
(ts_realtime.router, "TimescaleDB - Realtime"),
- (ts_scheme.router, "TimescaleDB - Scheme"),
+ (ts_analysis.router, "TimescaleDB - Analysis"),
):
api_router.include_router(
endpoint_router,
@@ -217,8 +194,3 @@ api_router.include_router(
tags=["Project Data"],
dependencies=[webgis_view_access],
)
-api_router.include_router(
- extension.router,
- tags=["Extension"],
- dependencies=[webgis_access],
-)
diff --git a/app/core/config.py b/app/core/config.py
index abb8d14..c8ea03d 100644
--- a/app/core/config.py
+++ b/app/core/config.py
@@ -38,9 +38,10 @@ class Settings(BaseSettings):
PROJECT_PG_CACHE_SIZE: int = 50
PROJECT_TS_CACHE_SIZE: int = 50
+ PROJECT_PG_POOL_MIN_SIZE: int = 0
PROJECT_PG_POOL_SIZE: int = 5
PROJECT_PG_MAX_OVERFLOW: int = 10
- PROJECT_TS_POOL_MIN_SIZE: int = 1
+ PROJECT_TS_POOL_MIN_SIZE: int = 0
PROJECT_TS_POOL_MAX_SIZE: int = 10
# Keycloak access token verification
diff --git a/app/domain/schemas/sensor_placement.py b/app/domain/schemas/sensor_placement.py
index a8f503b..3852811 100644
--- a/app/domain/schemas/sensor_placement.py
+++ b/app/domain/schemas/sensor_placement.py
@@ -1,5 +1,6 @@
from datetime import datetime
from typing import Literal
+from uuid import UUID
from pydantic import BaseModel, Field, field_validator
@@ -23,7 +24,7 @@ class SensorPlacementOptimizeRequest(BaseModel):
max_length=63,
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"]
method: Literal["sensitivity", "kmeans"]
sensor_count: int = Field(..., gt=0, le=200)
@@ -39,27 +40,27 @@ class SensorPlacementOptimizeRequest(BaseModel):
class SensorPlacementUpdateRequest(BaseModel):
- expected_sensor_location: list[str] = Field(
+ expected_sensor_locations: list[str] = Field(
...,
min_length=1,
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
def validate_locations(cls, value: list[str]) -> list[str]:
return _normalize_location_ids(value)
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(
default_factory=dict,
max_length=200,
)
- @field_validator("sensor_location")
+ @field_validator("sensor_locations")
@classmethod
def validate_locations(cls, value: list[str]) -> list[str]:
return _normalize_location_ids(value)
@@ -81,12 +82,13 @@ class SensorPointResponse(BaseModel):
class SensorPlacementSchemeResponse(BaseModel):
- id: int
- scheme_name: str
- sensor_number: int
+ run_id: UUID
+ name: str
+ sensor_count: int
min_diameter: int
- username: str
- create_time: datetime
- sensor_location: list[str]
+ created_by: str
+ created_at: datetime
+ status: str
+ sensor_locations: list[str]
sensor_points: list[SensorPointResponse]
can_edit: bool = False
diff --git a/app/infra/db/postgresql/analysis.py b/app/infra/db/postgresql/analysis.py
new file mode 100644
index 0000000..7dd405a
--- /dev/null
+++ b/app/infra/db/postgresql/analysis.py
@@ -0,0 +1,52 @@
+from uuid import UUID
+
+from psycopg import AsyncConnection
+
+
+class AnalysisRepository:
+ @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()
diff --git a/app/infra/db/postgresql/database.py b/app/infra/db/postgresql/database.py
deleted file mode 100644
index ad19f15..0000000
--- a/app/infra/db/postgresql/database.py
+++ /dev/null
@@ -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.")
diff --git a/app/infra/db/postgresql/scada.py b/app/infra/db/postgresql/scada.py
index ef33852..6248c81 100644
--- a/app/infra/db/postgresql/scada.py
+++ b/app/infra/db/postgresql/scada.py
@@ -11,6 +11,10 @@ def _optional_float(value: Any) -> float | 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
+
+
class ScadaInfoRepository:
"""Read SCADA metadata from the current project's business database."""
@@ -19,33 +23,34 @@ class ScadaInfoRepository:
async with conn.cursor() as cur:
await cur.execute(
"""
- SELECT id,
- type,
- associated_element_id,
+ SELECT id AS device_id,
+ device_type,
+ node_id,
+ link_id,
api_query_id,
transmission_mode,
transmission_frequency,
reliability,
- x_coor,
- y_coor
- FROM public.scada_info
+ x,
+ y
+ FROM gis.scada_devices
+ ORDER BY id
"""
)
records = await cur.fetchall()
return [
{
- "id": str(record["id"]).strip(),
- "type": str(record["type"]).strip().lower(),
- "associated_element_id": _optional_text(
- record["associated_element_id"]
- ),
- "api_query_id": record["api_query_id"],
+ "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_float(record["reliability"]),
- "x": _optional_float(record["x_coor"]),
- "y": _optional_float(record["y_coor"]),
+ "reliability": _optional_int(record["reliability"]),
+ "x": _optional_float(record["x"]),
+ "y": _optional_float(record["y"]),
}
for record in records
]
diff --git a/app/infra/db/postgresql/scada_assets.py b/app/infra/db/postgresql/scada_assets.py
new file mode 100644
index 0000000..4e501e8
--- /dev/null
+++ b/app/infra/db/postgresql/scada_assets.py
@@ -0,0 +1,66 @@
+from typing import Any
+
+from app.native.wndb.core.database import read_all, try_read
+
+
+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},
+ }
+
+
+_SELECT = """
+ SELECT device_id, device_type, node_id, link_id, api_query_id,
+ transmission_mode, transmission_frequency, reliability,
+ x, y
+ FROM asset.scada_devices
+"""
+
+_SELECT_MATERIALIZED = """
+ SELECT id AS device_id, device_type, node_id, link_id, api_query_id,
+ transmission_mode, transmission_frequency, reliability,
+ x, y
+ FROM gis.scada_devices
+"""
+
+
+def _device(row: dict[str, Any]) -> dict[str, Any]:
+ return {
+ "device_id": str(row["device_id"]),
+ "device_type": str(row["device_type"]),
+ "node_id": str(row["node_id"]) if row["node_id"] is not None else None,
+ "link_id": str(row["link_id"]) if row["link_id"] is not None else None,
+ "api_query_id": (
+ str(row["api_query_id"]) if row["api_query_id"] is not None else None
+ ),
+ "transmission_mode": str(row["transmission_mode"]),
+ "transmission_frequency": str(row["transmission_frequency"]),
+ "reliability": int(row["reliability"]),
+ "x": float(row["x"]) if row["x"] is not None else None,
+ "y": float(row["y"]) if row["y"] is not None else None,
+ }
+
+
+def get_scada_info(name: str, device_id: str) -> dict[str, Any]:
+ row = try_read(
+ name,
+ _SELECT + " WHERE device_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, _SELECT_MATERIALIZED + " ORDER BY device_id")
+ ]
diff --git a/app/infra/db/postgresql/scheme.py b/app/infra/db/postgresql/scheme.py
deleted file mode 100644
index ed9c240..0000000
--- a/app/infra/db/postgresql/scheme.py
+++ /dev/null
@@ -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
diff --git a/app/infra/db/postgresql/sensor_placement.py b/app/infra/db/postgresql/sensor_placement.py
new file mode 100644
index 0000000..3ce1267
--- /dev/null
+++ b/app/infra/db/postgresql/sensor_placement.py
@@ -0,0 +1,189 @@
+from typing import Any
+from uuid import UUID, uuid4
+
+from psycopg.rows import dict_row
+from psycopg.types.json import Jsonb
+
+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():
+ with conn.cursor(row_factory=dict_row) as cur:
+ cur.execute(
+ """
+ INSERT INTO analysis.runs
+ (run_id, name, run_type, created_by, started_at, status, parameters)
+ VALUES (%s, %s, %s, %s, now(), 'completed', '{}'::jsonb)
+ RETURNING run_id, name, created_by, created_at, status
+ """,
+ (run_id, run_name, RUN_TYPE, created_by),
+ )
+ created = cur.fetchone()
+ cur.execute(
+ """
+ INSERT INTO analysis.results (run_id, result_type, payload)
+ VALUES (%s, %s, %s)
+ """,
+ (run_id, RESULT_TYPE, Jsonb(payload)),
+ )
+ if created is None:
+ raise RuntimeError("监测点优化运行写入失败")
+ 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)
diff --git a/app/infra/db/timescaledb/__init__.py b/app/infra/db/timescaledb/__init__.py
index 1ab85a9..3261953 100644
--- a/app/infra/db/timescaledb/__init__.py
+++ b/app/infra/db/timescaledb/__init__.py
@@ -1,2 +1 @@
-from .database import *
-from .composite_queries import CompositeQueries
\ No newline at end of file
+from .composite_queries import CompositeQueries
diff --git a/app/infra/db/timescaledb/composite_queries.py b/app/infra/db/timescaledb/composite_queries.py
index 6baf715..9b83074 100644
--- a/app/infra/db/timescaledb/composite_queries.py
+++ b/app/infra/db/timescaledb/composite_queries.py
@@ -5,15 +5,16 @@ from typing import Any, Dict, List, Optional, Tuple
import numpy as np
import pandas as pd
from psycopg import AsyncConnection
+from uuid import UUID
-import app.native.wndb as wndb
from app.algorithms.cleaning.flow import clean_flow_data_df_kf
from app.algorithms.cleaning.pressure import clean_pressure_data_df_km
from app.algorithms.health.analyzer import PipelineHealthAnalyzer
from app.infra.db.postgresql.scada import ScadaInfoRepository
from app.infra.db.timescaledb.repositories.realtime import RealtimeRepository
-from app.infra.db.timescaledb.repositories.scheme import SchemeRepository
+from app.infra.db.timescaledb.repositories.analysis import AnalysisResultsRepository
from app.infra.db.timescaledb.repositories.scada import ScadaRepository
+from app.native.wndb.model.pipes import get_pipes_by_property
class CompositeQueries:
@@ -26,7 +27,7 @@ class CompositeQueries:
postgres_conn: AsyncConnection,
) -> Dict[str, Dict[str, Any]]:
scadas = await ScadaInfoRepository.get_scadas(postgres_conn)
- return {scada["id"]: scada for scada in scadas}
+ return {scada["device_id"]: scada for scada in scadas}
@staticmethod
async def get_scada_associated_realtime_simulation_data(
@@ -63,8 +64,12 @@ class CompositeQueries:
if not target_scada:
raise ValueError(f"SCADA device {device_id} not found")
- element_id = target_scada["associated_element_id"]
- scada_type = target_scada["type"]
+ scada_type = target_scada["device_type"]
+ element_id = (
+ target_scada["link_id"]
+ if scada_type in {"pipe_flow", "flow"}
+ else target_scada["node_id"]
+ )
if scada_type == "pipe_flow":
# 查询 link 模拟数据
@@ -85,17 +90,16 @@ class CompositeQueries:
return result
@staticmethod
- async def get_scada_associated_scheme_simulation_data(
+ async def get_scada_associated_analysis_simulation_data(
timescale_conn: AsyncConnection,
postgres_conn: AsyncConnection,
device_ids: List[str],
start_time: datetime,
end_time: datetime,
- scheme_type: str,
- scheme_name: str,
+ run_id: UUID,
) -> Dict[str, List[Dict[str, Any]]]:
"""
- 获取 SCADA 关联的 link/node scheme 模拟值
+ 获取 SCADA 关联的 link/node 分析模拟值
根据传入的 SCADA device_ids,找到关联的 link/node,
并根据对应的 type,查询对应的模拟数据
@@ -121,30 +125,22 @@ class CompositeQueries:
if not target_scada:
raise ValueError(f"SCADA device {device_id} not found")
- element_id = target_scada["associated_element_id"]
- scada_type = target_scada["type"]
+ scada_type = target_scada["device_type"]
+ element_id = (
+ target_scada["link_id"]
+ if scada_type in {"pipe_flow", "flow"}
+ else target_scada["node_id"]
+ )
if scada_type == "pipe_flow":
# 查询 link 模拟数据
- res = await SchemeRepository.get_link_field_by_scheme_and_time_range(
- timescale_conn,
- scheme_type,
- scheme_name,
- start_time,
- end_time,
- element_id,
- "flow",
+ res = await AnalysisResultsRepository.get_link_series(
+ timescale_conn, run_id, element_id, start_time, end_time, "flow"
)
elif scada_type == "pressure":
# 查询 node 模拟数据
- res = await SchemeRepository.get_node_field_by_scheme_and_time_range(
- timescale_conn,
- scheme_type,
- scheme_name,
- start_time,
- end_time,
- element_id,
- "pressure",
+ res = await AnalysisResultsRepository.get_node_series(
+ timescale_conn, run_id, element_id, start_time, end_time, "pressure"
)
else:
raise ValueError(f"Unknown SCADA type: {scada_type}")
@@ -201,16 +197,15 @@ class CompositeQueries:
return result
@staticmethod
- async def get_scheme_simulation_data(
+ async def get_analysis_simulation_data(
timescale_conn: AsyncConnection,
feature_infos: List[Tuple[str, str]],
start_time: datetime,
end_time: datetime,
- scheme_type: str,
- scheme_name: str,
+ run_id: UUID,
) -> Dict[str, List[Dict[str, Any]]]:
"""
- 获取 link/node scheme 模拟值
+ 获取 link/node 分析模拟值
根据传入的 feature_infos,找到关联的 link/node,
并根据对应的 type,查询对应的模拟数据
@@ -220,8 +215,7 @@ class CompositeQueries:
feature_infos: 传入的 feature 信息列表,包含 (element_id, type)
start_time: 开始时间
end_time: 结束时间
- scheme_type: 工况类型
- scheme_name: 工况名称
+ run_id: 分析运行 ID
Returns:
模拟数据字典,以 feature_id 为键,值为数据列表,每个数据包含 time, value 和 feature_id
@@ -233,25 +227,13 @@ class CompositeQueries:
for feature_id, feature_type in feature_infos:
if feature_type.lower() == "pipe":
# 查询 link 模拟数据
- res = await SchemeRepository.get_link_field_by_scheme_and_time_range(
- timescale_conn,
- scheme_type,
- scheme_name,
- start_time,
- end_time,
- feature_id,
- "flow",
+ res = await AnalysisResultsRepository.get_link_series(
+ timescale_conn, run_id, feature_id, start_time, end_time, "flow"
)
elif feature_type.lower() == "junction":
# 查询 node 模拟数据
- res = await SchemeRepository.get_node_field_by_scheme_and_time_range(
- timescale_conn,
- scheme_type,
- scheme_name,
- start_time,
- end_time,
- feature_id,
- "pressure",
+ res = await AnalysisResultsRepository.get_node_series(
+ timescale_conn, run_id, feature_id, start_time, end_time, "pressure"
)
else:
raise ValueError(f"Unknown type: {feature_type}")
@@ -296,7 +278,7 @@ class CompositeQueries:
(
scada
for scada in scada_by_id.values()
- if scada["associated_element_id"] == element_id
+ if (scada.get("node_id") or scada.get("link_id")) == element_id
),
None,
)
@@ -304,7 +286,7 @@ class CompositeQueries:
if not associated_scada:
return None
- device_id = associated_scada["id"]
+ device_id = associated_scada["device_id"]
data_field = "cleaned_value" if use_cleaned else "monitored_value"
@@ -358,7 +340,7 @@ class CompositeQueries:
unsupported_ids = [
device_id
for device_id in device_ids
- if scada_by_id[device_id]["type"] not in supported_types
+ if scada_by_id[device_id]["device_type"] not in supported_types
]
if unsupported_ids:
raise ValueError(
@@ -368,7 +350,7 @@ class CompositeQueries:
device_ids = [
device_id
for device_id, info in scada_by_id.items()
- if info["type"] in supported_types
+ if info["device_type"] in supported_types
]
if not device_ids:
@@ -409,12 +391,12 @@ class CompositeQueries:
pressure_ids = [
device_id
for device_id in df.columns
- if scada_by_id[device_id]["type"] == "pressure"
+ if scada_by_id[device_id]["device_type"] == "pressure"
]
flow_ids = [
device_id
for device_id in df.columns
- if scada_by_id[device_id]["type"] in {"pipe_flow", "flow"}
+ if scada_by_id[device_id]["device_type"] in {"pipe_flow", "flow"}
]
updated_rows = 0
@@ -497,7 +479,7 @@ class CompositeQueries:
# 批量查询这些管道的详细信息
fields = ["id", "diameter", "node1", "node2"]
- all_links = wndb.get_pipes_by_property(network_name, fields=fields)
+ all_links = get_pipes_by_property(network_name, fields=fields)
# 转换为字典以快速查找
links_dict = {link["id"]: link for link in all_links}
diff --git a/app/infra/db/timescaledb/database.py b/app/infra/db/timescaledb/database.py
deleted file mode 100644
index fd2bcd4..0000000
--- a/app/infra/db/timescaledb/database.py
+++ /dev/null
@@ -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.")
diff --git a/app/infra/db/timescaledb/internal_queries.py b/app/infra/db/timescaledb/internal_queries.py
index b6d4cbb..2c69db7 100644
--- a/app/infra/db/timescaledb/internal_queries.py
+++ b/app/infra/db/timescaledb/internal_queries.py
@@ -2,12 +2,12 @@ from typing import List
from fastapi.logger import logger
from datetime import datetime, timedelta
-import psycopg
from psycopg import sql
from psycopg.rows import dict_row
import time
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.scada import ScadaRepository
from app.services.time_api import parse_utc_time
@@ -25,12 +25,7 @@ class InternalStorage:
"""存储实时模拟结果"""
for attempt in range(max_retries):
try:
- conn_string = (
- 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 timescale_connection(db_name) as conn:
RealtimeRepository.store_realtime_simulation_result_sync(
conn, node_result_list, link_result_list, result_start_time
)
@@ -43,9 +38,8 @@ class InternalStorage:
raise # 达到最大重试次数后抛出异常
@staticmethod
- def store_scheme_simulation(
- scheme_type: str,
- scheme_name: str,
+ def store_analysis_simulation(
+ run_id,
node_result_list: List[dict],
link_result_list: List[dict],
result_start_time: str,
@@ -54,24 +48,16 @@ class InternalStorage:
db_name: str = None,
max_retries: int = 3,
):
- """存储方案模拟结果"""
+ """Store immutable simulation results for one analysis run."""
for attempt in range(max_retries):
try:
- conn_string = (
- 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:
- SchemeRepository.store_scheme_simulation_result_sync(
- conn,
- scheme_type,
- scheme_name,
- node_result_list,
- link_result_list,
- result_start_time,
- num_periods,
- result_timestep_seconds,
+ with timescale_connection(db_name) as conn:
+ node_rows, link_rows = AnalysisResultsRepository.prepare_simulation_rows(
+ node_result_list, link_result_list, result_start_time,
+ num_periods, result_timestep_seconds or 3600,
+ )
+ AnalysisResultsRepository.store_results_sync(
+ conn, run_id, node_rows, link_rows
)
break # 成功
except Exception as e:
@@ -98,12 +84,7 @@ class InternalQueries:
for attempt in range(max_retries):
try:
- conn_string = (
- 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 timescale_connection(db_name) as conn:
rows = ScadaRepository.get_scada_by_ids_time_range_sync(
conn, device_ids, start_time, end_time
)
@@ -139,12 +120,7 @@ class InternalQueries:
for attempt in range(max_retries):
try:
- conn_string = (
- 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 timescale_connection(db_name) as conn:
rows = ScadaRepository.get_scada_by_ids_time_range_sync(
conn, device_ids, start_dt, end_dt
)
@@ -184,12 +160,7 @@ class InternalQueries:
)
for attempt in range(max_retries):
try:
- conn_string = (
- 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 timescale_connection(db_name) as conn:
return ScadaRepository.get_latest_scada_time_sync(
conn,
device_ids,
@@ -224,20 +195,19 @@ class InternalQueries:
)
@staticmethod
- def query_scheme_simulation_by_ids_timerange(
+ def query_analysis_simulation_by_ids_timerange(
element_ids: List[str],
start_time: str | datetime,
end_time: str | datetime,
element_type: str,
field: str,
- scheme_type: str,
- scheme_name: str,
+ run_id,
db_name: str = None,
max_retries: int = 3,
) -> dict[str, list[dict]]:
- """查询方案模拟结果,返回 {id: [{time, value}, ...]}。"""
+ """Query one analysis run, returning {id: [{time, value}, ...]}."""
return InternalQueries._query_simulation_by_ids_timerange(
- schema_name="scheme",
+ schema_name="analysis",
element_ids=element_ids,
start_time=start_time,
end_time=end_time,
@@ -245,8 +215,7 @@ class InternalQueries:
field=field,
db_name=db_name,
max_retries=max_retries,
- scheme_type=scheme_type,
- scheme_name=scheme_name,
+ run_id=run_id,
)
@staticmethod
@@ -260,8 +229,7 @@ class InternalQueries:
field: str,
db_name: str = None,
max_retries: int = 3,
- scheme_type: str | None = None,
- scheme_name: str | None = None,
+ run_id=None,
) -> dict[str, list[dict]]:
normalized_element_ids = list(
dict.fromkeys(
@@ -275,51 +243,44 @@ class InternalQueries:
start_dt = parse_utc_time(start_time, field_name="start_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:
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}")
- if schema_name == "scheme" and (not scheme_type or not scheme_name):
- raise ValueError("scheme 查询必须提供 scheme_type 和 scheme_name。")
+ if schema_name == "analysis" and run_id is None:
+ raise ValueError("analysis query requires run_id")
for attempt in range(max_retries):
try:
- conn_string = (
- 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 timescale_connection(db_name) as conn:
with conn.cursor(row_factory=dict_row) as cur:
- if schema_name == "scheme":
+ if schema_name == "analysis":
query = sql.SQL(
- "SELECT btrim(id::text) AS id, time, {} FROM {}.{} "
- "WHERE scheme_type = %s AND scheme_name = %s "
- "AND time >= %s AND time <= %s AND btrim(id::text) = ANY(%s)"
+ "SELECT btrim({}::text) AS id, time, {} FROM {}.{} "
+ "WHERE run_id = %s AND time >= %s AND time <= %s "
+ "AND btrim({}::text) = ANY(%s)"
).format(
+ sql.Identifier(id_column),
sql.Identifier(field),
sql.Identifier(schema_name),
sql.Identifier(table_name),
+ sql.Identifier(id_column),
)
cur.execute(
query,
- (
- scheme_type,
- scheme_name,
- start_dt,
- end_dt,
- normalized_element_ids,
- ),
+ (run_id, start_dt, end_dt, normalized_element_ids),
)
else:
query = sql.SQL(
- "SELECT btrim(id::text) AS id, time, {} FROM {}.{} "
- "WHERE time >= %s AND time <= %s AND btrim(id::text) = ANY(%s)"
+ "SELECT btrim({}::text) AS id, time, {} FROM {}.{} "
+ "WHERE time >= %s AND time <= %s AND btrim({}::text) = ANY(%s)"
).format(
+ sql.Identifier(id_column),
sql.Identifier(field),
sql.Identifier(schema_name),
sql.Identifier(table_name),
+ sql.Identifier(id_column),
)
cur.execute(query, (start_dt, end_dt, normalized_element_ids))
rows = cur.fetchall()
@@ -342,12 +303,12 @@ class InternalQueries:
raise
@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()
if normalized_type == "node":
- return "node_simulation", {"actual_demand", "total_head", "pressure", "quality"}
+ return "node_results", "node_id", {"actual_demand", "total_head", "pressure", "quality"}
if normalized_type == "link":
- return "link_simulation", {
+ return "link_results", "link_id", {
"flow",
"friction",
"headloss",
diff --git a/app/infra/db/timescaledb/repositories/analysis.py b/app/infra/db/timescaledb/repositories/analysis.py
new file mode 100644
index 0000000..b2799d5
--- /dev/null
+++ b/app/infra/db/timescaledb/repositories/analysis.py
@@ -0,0 +1,268 @@
+from datetime import datetime, timedelta
+from typing import Any
+from uuid import UUID
+
+from psycopg import AsyncConnection, Connection, sql
+
+from app.services.time_api 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_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"),
+ )
+ )
diff --git a/app/infra/db/timescaledb/repositories/realtime.py b/app/infra/db/timescaledb/repositories/realtime.py
index 6b26fa4..10fe5bc 100644
--- a/app/infra/db/timescaledb/repositories/realtime.py
+++ b/app/infra/db/timescaledb/repositories/realtime.py
@@ -11,7 +11,7 @@ class RealtimeRepository:
@staticmethod
async def insert_links_batch(conn: AsyncConnection, data: List[dict]):
- """Batch insert for realtime.link_simulation using DELETE then COPY for performance."""
+ """Batch insert for realtime.link_results using DELETE then COPY."""
if not data:
return
@@ -21,15 +21,19 @@ class RealtimeRepository:
# 使用事务确保原子性
async with conn.transaction():
async with conn.cursor() as cur:
+ await cur.execute(
+ "SELECT pg_advisory_xact_lock(hashtextextended(%s::text, 1))",
+ (target_time,),
+ )
# 1. 先删除该时间点的旧数据
await cur.execute(
- "DELETE FROM realtime.link_simulation WHERE time = %s",
+ "DELETE FROM realtime.link_results WHERE time = %s",
(target_time,),
)
# 2. 使用 COPY 快速写入新数据
async with cur.copy(
- "COPY realtime.link_simulation (time, id, flow, friction, headloss, quality, reaction, setting, status, velocity) FROM STDIN"
+ "COPY realtime.link_results (time, link_id, flow, friction, headloss, quality, reaction, setting, status, velocity) FROM STDIN"
) as copy:
for item in data:
await copy.write_row(
@@ -49,7 +53,7 @@ class RealtimeRepository:
@staticmethod
def insert_links_batch_sync(conn: Connection, data: List[dict]):
- """Batch insert for realtime.link_simulation using DELETE then COPY for performance (sync version)."""
+ """Synchronous batch insert for realtime.link_results."""
if not data:
return
@@ -59,15 +63,19 @@ class RealtimeRepository:
# 使用事务确保原子性
with conn.transaction():
with conn.cursor() as cur:
+ cur.execute(
+ "SELECT pg_advisory_xact_lock(hashtextextended(%s::text, 1))",
+ (target_time,),
+ )
# 1. 先删除该时间点的旧数据
cur.execute(
- "DELETE FROM realtime.link_simulation WHERE time = %s",
+ "DELETE FROM realtime.link_results WHERE time = %s",
(target_time,),
)
# 2. 使用 COPY 快速写入新数据
with cur.copy(
- "COPY realtime.link_simulation (time, id, flow, friction, headloss, quality, reaction, setting, status, velocity) FROM STDIN"
+ "COPY realtime.link_results (time, link_id, flow, friction, headloss, quality, reaction, setting, status, velocity) FROM STDIN"
) as copy:
for item in data:
copy.write_row(
@@ -91,7 +99,7 @@ class RealtimeRepository:
) -> List[dict]:
async with conn.cursor() as cur:
await cur.execute(
- "SELECT * FROM realtime.link_simulation WHERE time >= %s AND time <= %s AND id = %s",
+ "SELECT * FROM realtime.link_results WHERE time >= %s AND time <= %s AND link_id = %s",
(start_time, end_time, link_id),
)
return await cur.fetchall()
@@ -104,7 +112,7 @@ class RealtimeRepository:
normalized_end_time = parse_utc_time(end_time, field_name="end_time")
async with conn.cursor() as cur:
await cur.execute(
- "SELECT * FROM realtime.link_simulation WHERE time >= %s AND time <= %s",
+ "SELECT * FROM realtime.link_results WHERE time >= %s AND time <= %s",
(normalized_start_time, normalized_end_time),
)
return await cur.fetchall()
@@ -132,7 +140,7 @@ class RealtimeRepository:
raise ValueError(f"Invalid field: {field}")
query = sql.SQL(
- "SELECT time, {} FROM realtime.link_simulation WHERE time >= %s AND time <= %s AND id = %s"
+ "SELECT time, {} FROM realtime.link_results WHERE time >= %s AND time <= %s AND link_id = %s"
).format(sql.Identifier(field))
async with conn.cursor() as cur:
@@ -164,7 +172,7 @@ class RealtimeRepository:
raise ValueError(f"Invalid field: {field}")
query = sql.SQL(
- "SELECT id, time, {} FROM realtime.link_simulation WHERE time >= %s AND time <= %s"
+ "SELECT link_id, time, {} FROM realtime.link_results WHERE time >= %s AND time <= %s"
).format(sql.Identifier(field))
async with conn.cursor() as cur:
@@ -172,7 +180,7 @@ class RealtimeRepository:
rows = await cur.fetchall()
result = defaultdict(list)
for row in rows:
- result[row["id"]].append(
+ result[row["link_id"]].append(
{"time": row["time"].isoformat(), "value": row[field]}
)
return dict(result)
@@ -199,7 +207,7 @@ class RealtimeRepository:
raise ValueError(f"Invalid field: {field}")
query = sql.SQL(
- "UPDATE realtime.link_simulation SET {} = %s WHERE time = %s AND id = %s"
+ "UPDATE realtime.link_results SET {} = %s WHERE time = %s AND link_id = %s"
).format(sql.Identifier(field))
async with conn.cursor() as cur:
@@ -211,7 +219,7 @@ class RealtimeRepository:
):
async with conn.cursor() as cur:
await cur.execute(
- "DELETE FROM realtime.link_simulation WHERE time >= %s AND time <= %s",
+ "DELETE FROM realtime.link_results WHERE time >= %s AND time <= %s",
(start_time, end_time),
)
@@ -228,15 +236,19 @@ class RealtimeRepository:
# 使用事务确保原子性
async with conn.transaction():
async with conn.cursor() as cur:
+ await cur.execute(
+ "SELECT pg_advisory_xact_lock(hashtextextended(%s::text, 1))",
+ (target_time,),
+ )
# 1. 先删除该时间点的旧数据
await cur.execute(
- "DELETE FROM realtime.node_simulation WHERE time = %s",
+ "DELETE FROM realtime.node_results WHERE time = %s",
(target_time,),
)
# 2. 使用 COPY 快速写入新数据
async with cur.copy(
- "COPY realtime.node_simulation (time, id, actual_demand, total_head, pressure, quality) FROM STDIN"
+ "COPY realtime.node_results (time, node_id, actual_demand, total_head, pressure, quality) FROM STDIN"
) as copy:
for item in data:
await copy.write_row(
@@ -261,15 +273,19 @@ class RealtimeRepository:
# 使用事务确保原子性
with conn.transaction():
with conn.cursor() as cur:
+ cur.execute(
+ "SELECT pg_advisory_xact_lock(hashtextextended(%s::text, 1))",
+ (target_time,),
+ )
# 1. 先删除该时间点的旧数据
cur.execute(
- "DELETE FROM realtime.node_simulation WHERE time = %s",
+ "DELETE FROM realtime.node_results WHERE time = %s",
(target_time,),
)
# 2. 使用 COPY 快速写入新数据
with cur.copy(
- "COPY realtime.node_simulation (time, id, actual_demand, total_head, pressure, quality) FROM STDIN"
+ "COPY realtime.node_results (time, node_id, actual_demand, total_head, pressure, quality) FROM STDIN"
) as copy:
for item in data:
copy.write_row(
@@ -289,7 +305,7 @@ class RealtimeRepository:
) -> List[dict]:
async with conn.cursor() as cur:
await cur.execute(
- "SELECT * FROM realtime.node_simulation WHERE time >= %s AND time <= %s AND id = %s",
+ "SELECT * FROM realtime.node_results WHERE time >= %s AND time <= %s AND node_id = %s",
(start_time, end_time, node_id),
)
return await cur.fetchall()
@@ -302,7 +318,7 @@ class RealtimeRepository:
normalized_end_time = parse_utc_time(end_time, field_name="end_time")
async with conn.cursor() as cur:
await cur.execute(
- "SELECT * FROM realtime.node_simulation WHERE time >= %s AND time <= %s",
+ "SELECT * FROM realtime.node_results WHERE time >= %s AND time <= %s",
(normalized_start_time, normalized_end_time),
)
return await cur.fetchall()
@@ -320,7 +336,7 @@ class RealtimeRepository:
raise ValueError(f"Invalid field: {field}")
query = sql.SQL(
- "SELECT time, {} FROM realtime.node_simulation WHERE time >= %s AND time <= %s AND id = %s"
+ "SELECT time, {} FROM realtime.node_results WHERE time >= %s AND time <= %s AND node_id = %s"
).format(sql.Identifier(field))
async with conn.cursor() as cur:
@@ -339,7 +355,7 @@ class RealtimeRepository:
raise ValueError(f"Invalid field: {field}")
query = sql.SQL(
- "SELECT id, time, {} FROM realtime.node_simulation WHERE time >= %s AND time <= %s"
+ "SELECT node_id, time, {} FROM realtime.node_results WHERE time >= %s AND time <= %s"
).format(sql.Identifier(field))
async with conn.cursor() as cur:
@@ -347,7 +363,7 @@ class RealtimeRepository:
rows = await cur.fetchall()
result = defaultdict(list)
for row in rows:
- result[row["id"]].append(
+ result[row["node_id"]].append(
{"time": row["time"].isoformat(), "value": row[field]}
)
return dict(result)
@@ -365,7 +381,7 @@ class RealtimeRepository:
raise ValueError(f"Invalid field: {field}")
query = sql.SQL(
- "UPDATE realtime.node_simulation SET {} = %s WHERE time = %s AND id = %s"
+ "UPDATE realtime.node_results SET {} = %s WHERE time = %s AND node_id = %s"
).format(sql.Identifier(field))
async with conn.cursor() as cur:
@@ -377,7 +393,7 @@ class RealtimeRepository:
):
async with conn.cursor() as cur:
await cur.execute(
- "DELETE FROM realtime.node_simulation WHERE time >= %s AND time <= %s",
+ "DELETE FROM realtime.node_results WHERE time >= %s AND time <= %s",
(start_time, end_time),
)
@@ -439,12 +455,15 @@ class RealtimeRepository:
}
)
- # Insert data using batch methods
- if node_data:
- await RealtimeRepository.insert_nodes_batch(conn, node_data)
+ # Keep node and link replacement atomic. The batch helpers use nested
+ # transactions (savepoints), while this outer transaction guarantees
+ # that a link write failure also rolls back the node replacement.
+ async with conn.transaction():
+ if node_data:
+ await RealtimeRepository.insert_nodes_batch(conn, node_data)
- if link_data:
- await RealtimeRepository.insert_links_batch(conn, link_data)
+ if link_data:
+ await RealtimeRepository.insert_links_batch(conn, link_data)
@staticmethod
def store_realtime_simulation_result_sync(
@@ -502,12 +521,15 @@ class RealtimeRepository:
}
)
- # Insert data using batch methods
- if node_data:
- RealtimeRepository.insert_nodes_batch_sync(conn, node_data)
+ # Keep node and link replacement atomic. The batch helpers use nested
+ # transactions (savepoints), while this outer transaction guarantees
+ # that a link write failure also rolls back the node replacement.
+ with conn.transaction():
+ if node_data:
+ RealtimeRepository.insert_nodes_batch_sync(conn, node_data)
- if link_data:
- RealtimeRepository.insert_links_batch_sync(conn, link_data)
+ if link_data:
+ RealtimeRepository.insert_links_batch_sync(conn, link_data)
@staticmethod
async def query_all_record_by_time_property(
diff --git a/app/infra/db/timescaledb/repositories/scada.py b/app/infra/db/timescaledb/repositories/scada.py
index b28dfea..09ef7e5 100644
--- a/app/infra/db/timescaledb/repositories/scada.py
+++ b/app/infra/db/timescaledb/repositories/scada.py
@@ -14,7 +14,7 @@ class ScadaRepository:
async with conn.cursor() as cur:
async with cur.copy(
- "COPY scada.scada_data (time, device_id, monitored_value, cleaned_value) FROM STDIN"
+ "COPY scada.measurements (time, device_id, monitored_value, cleaned_value) FROM STDIN"
) as copy:
for item in data:
await copy.write_row(
@@ -35,7 +35,7 @@ class ScadaRepository:
) -> List[dict]:
async with conn.cursor() as cur:
await cur.execute(
- "SELECT * FROM scada.scada_data WHERE device_id = ANY(%s) AND time >= %s AND time <= %s",
+ "SELECT * FROM scada.measurements WHERE device_id = ANY(%s) AND time >= %s AND time <= %s",
(device_ids, start_time, end_time),
)
return await cur.fetchall()
@@ -49,7 +49,7 @@ class ScadaRepository:
) -> List[dict]:
with conn.cursor(row_factory=dict_row) as cur:
cur.execute(
- "SELECT * FROM scada.scada_data WHERE device_id = ANY(%s) AND time >= %s AND time <= %s",
+ "SELECT * FROM scada.measurements WHERE device_id = ANY(%s) AND time >= %s AND time <= %s",
(device_ids, start_time, end_time),
)
return cur.fetchall()
@@ -63,12 +63,12 @@ class ScadaRepository:
with conn.cursor(row_factory=dict_row) as cur:
if before_time is None:
cur.execute(
- "SELECT max(time) AS time FROM scada.scada_data WHERE device_id = ANY(%s)",
+ "SELECT max(time) AS time FROM scada.measurements WHERE device_id = ANY(%s)",
(device_ids,),
)
else:
cur.execute(
- "SELECT max(time) AS time FROM scada.scada_data "
+ "SELECT max(time) AS time FROM scada.measurements "
"WHERE device_id = ANY(%s) AND time <= %s",
(device_ids, before_time),
)
@@ -88,7 +88,7 @@ class ScadaRepository:
raise ValueError(f"Invalid field: {field}")
query = sql.SQL(
- "SELECT device_id, time, {} FROM scada.scada_data WHERE time >= %s AND time <= %s AND device_id = ANY(%s)"
+ "SELECT device_id, time, {} FROM scada.measurements WHERE time >= %s AND time <= %s AND device_id = ANY(%s)"
).format(sql.Identifier(field))
async with conn.cursor() as cur:
@@ -111,10 +111,10 @@ class ScadaRepository:
raise ValueError(f"Invalid field: {field}")
update_query = sql.SQL(
- "UPDATE scada.scada_data SET {} = %s WHERE time = %s AND device_id = %s"
+ "UPDATE scada.measurements SET {} = %s WHERE time = %s AND device_id = %s"
).format(sql.Identifier(field))
insert_query = sql.SQL(
- "INSERT INTO scada.scada_data (time, device_id, {}) VALUES (%s, %s, %s)"
+ "INSERT INTO scada.measurements (time, device_id, {}) VALUES (%s, %s, %s)"
).format(sql.Identifier(field))
async with conn.cursor() as cur:
@@ -128,6 +128,6 @@ class ScadaRepository:
):
async with conn.cursor() as cur:
await cur.execute(
- "DELETE FROM scada.scada_data WHERE device_id = %s AND time >= %s AND time <= %s",
+ "DELETE FROM scada.measurements WHERE device_id = %s AND time >= %s AND time <= %s",
(device_id, start_time, end_time),
)
diff --git a/app/infra/db/timescaledb/repositories/scheme.py b/app/infra/db/timescaledb/repositories/scheme.py
deleted file mode 100644
index 3903fc0..0000000
--- a/app/infra/db/timescaledb/repositories/scheme.py
+++ /dev/null
@@ -1,710 +0,0 @@
-from typing import List, Any, Dict
-from datetime import datetime, timedelta
-from collections import defaultdict
-from psycopg import AsyncConnection, Connection, sql
-import app.services.globals as globals
-from app.services.time_api import parse_clock_duration_seconds, parse_utc_time
-
-
-class SchemeRepository:
- @staticmethod
- def _get_result_timestep(result_timestep_seconds: int | None) -> timedelta:
- if result_timestep_seconds is not None:
- if result_timestep_seconds <= 0:
- raise ValueError("result_timestep_seconds must be greater than 0.")
- return timedelta(seconds=result_timestep_seconds)
-
- timestep_seconds = parse_clock_duration_seconds(
- globals.hydraulic_timestep,
- field_name="HYDRAULIC TIMESTEP",
- )
- if timestep_seconds <= 0:
- raise ValueError("HYDRAULIC TIMESTEP must be greater than 0.")
- return timedelta(seconds=timestep_seconds)
-
- # --- Link Simulation ---
-
- @staticmethod
- async def insert_links_batch(conn: AsyncConnection, data: List[dict]):
- """Batch insert for scheme.link_simulation using DELETE then COPY for performance."""
- if not data:
- return
-
- # 获取批次中所有不同的时间点
- all_times = list(set(item["time"] for item in data))
- target_scheme_type = data[0]["scheme_type"]
- target_scheme_name = data[0]["scheme_name"]
-
- # 使用事务确保原子性
- async with conn.transaction():
- async with conn.cursor() as cur:
- # 1. 删除该批次涉及的所有时间点、scheme_type、scheme_name 的旧数据
- await cur.execute(
- "DELETE FROM scheme.link_simulation WHERE time = ANY(%s) AND scheme_type = %s AND scheme_name = %s",
- (all_times, target_scheme_type, target_scheme_name),
- )
-
- # 2. 使用 COPY 快速写入新数据
- async with cur.copy(
- "COPY scheme.link_simulation (time, scheme_type, scheme_name, id, flow, friction, headloss, quality, reaction, setting, status, velocity) FROM STDIN"
- ) as copy:
- for item in data:
- await copy.write_row(
- (
- item["time"],
- item["scheme_type"],
- item["scheme_name"],
- item["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"),
- )
- )
-
- @staticmethod
- def insert_links_batch_sync(conn: Connection, data: List[dict]):
- """Batch insert for scheme.link_simulation using DELETE then COPY for performance (sync version)."""
- if not data:
- return
-
- # 获取批次中所有不同的时间点
- all_times = list(set(item["time"] for item in data))
- target_scheme_type = data[0]["scheme_type"]
- target_scheme_name = data[0]["scheme_name"]
-
- # 使用事务确保原子性
- with conn.transaction():
- with conn.cursor() as cur:
- # 1. 删除该批次涉及的所有时间点、scheme_type、scheme_name 的旧数据
- cur.execute(
- "DELETE FROM scheme.link_simulation WHERE time = ANY(%s) AND scheme_type = %s AND scheme_name = %s",
- (all_times, target_scheme_type, target_scheme_name),
- )
-
- # 2. 使用 COPY 快速写入新数据
- with cur.copy(
- "COPY scheme.link_simulation (time, scheme_type, scheme_name, id, flow, friction, headloss, quality, reaction, setting, status, velocity) FROM STDIN"
- ) as copy:
- for item in data:
- copy.write_row(
- (
- item["time"],
- item["scheme_type"],
- item["scheme_name"],
- item["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"),
- )
- )
-
- @staticmethod
- async def get_link_by_scheme_and_time_range(
- conn: AsyncConnection,
- scheme_type: str,
- scheme_name: str,
- start_time: datetime,
- end_time: datetime,
- link_id: str,
- ) -> List[dict]:
- async with conn.cursor() as cur:
- await cur.execute(
- "SELECT * FROM scheme.link_simulation WHERE scheme_type = %s AND scheme_name = %s AND time >= %s AND time <= %s AND id = %s",
- (scheme_type, scheme_name, start_time, end_time, link_id),
- )
- return await cur.fetchall()
-
- @staticmethod
- async def get_links_by_scheme_and_time_range(
- conn: AsyncConnection,
- scheme_type: str,
- scheme_name: str,
- start_time: datetime,
- end_time: datetime,
- ) -> List[dict]:
- async with conn.cursor() as cur:
- await cur.execute(
- "SELECT * FROM scheme.link_simulation WHERE scheme_type = %s AND scheme_name = %s AND time >= %s AND time <= %s",
- (scheme_type, scheme_name, start_time, end_time),
- )
- return await cur.fetchall()
-
- @staticmethod
- async def get_link_field_by_scheme_and_time_range(
- conn: AsyncConnection,
- scheme_type: str,
- scheme_name: str,
- start_time: datetime,
- end_time: datetime,
- link_id: str,
- field: str,
- ) -> List[Dict[str, Any]]:
- # Validate field name to prevent SQL injection
- valid_fields = {
- "flow",
- "friction",
- "headloss",
- "quality",
- "reaction",
- "setting",
- "status",
- "velocity",
- }
- if field not in valid_fields:
- raise ValueError(f"Invalid field: {field}")
-
- query = sql.SQL(
- "SELECT time, {} FROM scheme.link_simulation WHERE scheme_type = %s AND scheme_name = %s AND time >= %s AND time <= %s AND id = %s"
- ).format(sql.Identifier(field))
-
- async with conn.cursor() as cur:
- await cur.execute(
- query, (scheme_type, scheme_name, start_time, end_time, link_id)
- )
- rows = await cur.fetchall()
- return [
- {"time": row["time"].isoformat(), "value": row[field]} for row in rows
- ]
-
- @staticmethod
- async def get_links_field_by_scheme_and_time_range(
- conn: AsyncConnection,
- scheme_type: str,
- scheme_name: str,
- start_time: datetime,
- end_time: datetime,
- field: str,
- ) -> dict:
- # Validate field name to prevent SQL injection
- valid_fields = {
- "flow",
- "friction",
- "headloss",
- "quality",
- "reaction",
- "setting",
- "status",
- "velocity",
- }
- if field not in valid_fields:
- raise ValueError(f"Invalid field: {field}")
-
- query = sql.SQL(
- "SELECT id, time, {} FROM scheme.link_simulation WHERE scheme_type = %s AND scheme_name = %s AND time >= %s AND time <= %s"
- ).format(sql.Identifier(field))
-
- async with conn.cursor() as cur:
- await cur.execute(query, (scheme_type, scheme_name, start_time, end_time))
- rows = await cur.fetchall()
- result = defaultdict(list)
- for row in rows:
- result[row["id"]].append(
- {"time": row["time"].isoformat(), "value": row[field]}
- )
- return dict(result)
-
- @staticmethod
- async def update_link_field(
- conn: AsyncConnection,
- time: datetime,
- scheme_type: str,
- scheme_name: str,
- link_id: str,
- field: str,
- value: Any,
- ):
- valid_fields = {
- "flow",
- "friction",
- "headloss",
- "quality",
- "reaction",
- "setting",
- "status",
- "velocity",
- }
- if field not in valid_fields:
- raise ValueError(f"Invalid field: {field}")
-
- query = sql.SQL(
- "UPDATE scheme.link_simulation SET {} = %s WHERE time = %s AND scheme_type = %s AND scheme_name = %s AND id = %s"
- ).format(sql.Identifier(field))
-
- async with conn.cursor() as cur:
- await cur.execute(query, (value, time, scheme_type, scheme_name, link_id))
-
- @staticmethod
- async def delete_links_by_scheme_and_time_range(
- conn: AsyncConnection,
- scheme_type: str,
- scheme_name: str,
- start_time: datetime,
- end_time: datetime,
- ):
- async with conn.cursor() as cur:
- await cur.execute(
- "DELETE FROM scheme.link_simulation WHERE scheme_type = %s AND scheme_name = %s AND time >= %s AND time <= %s",
- (scheme_type, scheme_name, start_time, end_time),
- )
-
- # --- Node Simulation ---
-
- @staticmethod
- async def insert_nodes_batch(conn: AsyncConnection, data: List[dict]):
- if not data:
- return
-
- # 获取批次中所有不同的时间点
- all_times = list(set(item["time"] for item in data))
- target_scheme_type = data[0]["scheme_type"]
- target_scheme_name = data[0]["scheme_name"]
-
- # 使用事务确保原子性
- async with conn.transaction():
- async with conn.cursor() as cur:
- # 1. 删除该批次涉及的所有时间点、scheme_type、scheme_name 的旧数据
- await cur.execute(
- "DELETE FROM scheme.node_simulation WHERE time = ANY(%s) AND scheme_type = %s AND scheme_name = %s",
- (all_times, target_scheme_type, target_scheme_name),
- )
-
- # 2. 使用 COPY 快速写入新数据
- async with cur.copy(
- "COPY scheme.node_simulation (time, scheme_type, scheme_name, id, actual_demand, total_head, pressure, quality) FROM STDIN"
- ) as copy:
- for item in data:
- await copy.write_row(
- (
- item["time"],
- item["scheme_type"],
- item["scheme_name"],
- item["id"],
- item.get("actual_demand"),
- item.get("total_head"),
- item.get("pressure"),
- item.get("quality"),
- )
- )
-
- @staticmethod
- def insert_nodes_batch_sync(conn: Connection, data: List[dict]):
- if not data:
- return
-
- # 获取批次中所有不同的时间点
- all_times = list(set(item["time"] for item in data))
- target_scheme_type = data[0]["scheme_type"]
- target_scheme_name = data[0]["scheme_name"]
-
- # 使用事务确保原子性
- with conn.transaction():
- with conn.cursor() as cur:
- # 1. 删除该批次涉及的所有时间点、scheme_type、scheme_name 的旧数据
- cur.execute(
- "DELETE FROM scheme.node_simulation WHERE time = ANY(%s) AND scheme_type = %s AND scheme_name = %s",
- (all_times, target_scheme_type, target_scheme_name),
- )
-
- # 2. 使用 COPY 快速写入新数据
- with cur.copy(
- "COPY scheme.node_simulation (time, scheme_type, scheme_name, id, actual_demand, total_head, pressure, quality) FROM STDIN"
- ) as copy:
- for item in data:
- copy.write_row(
- (
- item["time"],
- item["scheme_type"],
- item["scheme_name"],
- item["id"],
- item.get("actual_demand"),
- item.get("total_head"),
- item.get("pressure"),
- item.get("quality"),
- )
- )
-
- @staticmethod
- async def get_node_by_scheme_and_time_range(
- conn: AsyncConnection,
- scheme_type: str,
- scheme_name: str,
- start_time: datetime,
- end_time: datetime,
- node_id: str,
- ) -> List[dict]:
- async with conn.cursor() as cur:
- await cur.execute(
- "SELECT * FROM scheme.node_simulation WHERE scheme_type = %s AND scheme_name = %s AND time >= %s AND time <= %s AND id = %s",
- (scheme_type, scheme_name, start_time, end_time, node_id),
- )
- return await cur.fetchall()
-
- @staticmethod
- async def get_nodes_by_scheme_and_time_range(
- conn: AsyncConnection,
- scheme_type: str,
- scheme_name: str,
- start_time: datetime,
- end_time: datetime,
- ) -> List[dict]:
- async with conn.cursor() as cur:
- await cur.execute(
- "SELECT * FROM scheme.node_simulation WHERE scheme_type = %s AND scheme_name = %s AND time >= %s AND time <= %s",
- (scheme_type, scheme_name, start_time, end_time),
- )
- return await cur.fetchall()
-
- @staticmethod
- async def get_node_field_by_scheme_and_time_range(
- conn: AsyncConnection,
- scheme_type: str,
- scheme_name: str,
- start_time: datetime,
- end_time: datetime,
- node_id: str,
- field: str,
- ) -> List[Dict[str, Any]]:
- # Validate field name to prevent SQL injection
- valid_fields = {"actual_demand", "total_head", "pressure", "quality"}
- if field not in valid_fields:
- raise ValueError(f"Invalid field: {field}")
-
- query = sql.SQL(
- "SELECT time, {} FROM scheme.node_simulation WHERE scheme_type = %s AND scheme_name = %s AND time >= %s AND time <= %s AND id = %s"
- ).format(sql.Identifier(field))
-
- async with conn.cursor() as cur:
- await cur.execute(
- query, (scheme_type, scheme_name, start_time, end_time, node_id)
- )
- rows = await cur.fetchall()
- return [
- {"time": row["time"].isoformat(), "value": row[field]} for row in rows
- ]
-
- @staticmethod
- async def get_nodes_field_by_scheme_and_time_range(
- conn: AsyncConnection,
- scheme_type: str,
- scheme_name: str,
- start_time: datetime,
- end_time: datetime,
- field: str,
- ) -> dict:
- # Validate field name to prevent SQL injection
- valid_fields = {"actual_demand", "total_head", "pressure", "quality"}
- if field not in valid_fields:
- raise ValueError(f"Invalid field: {field}")
-
- query = sql.SQL(
- "SELECT id, time, {} FROM scheme.node_simulation WHERE scheme_type = %s AND scheme_name = %s AND time >= %s AND time <= %s"
- ).format(sql.Identifier(field))
-
- async with conn.cursor() as cur:
- await cur.execute(query, (scheme_type, scheme_name, start_time, end_time))
- rows = await cur.fetchall()
- result = defaultdict(list)
- for row in rows:
- result[row["id"]].append(
- {"time": row["time"].isoformat(), "value": row[field]}
- )
- return dict(result)
-
- @staticmethod
- async def update_node_field(
- conn: AsyncConnection,
- time: datetime,
- scheme_type: str,
- scheme_name: str,
- node_id: str,
- field: str,
- value: Any,
- ):
- valid_fields = {"actual_demand", "total_head", "pressure", "quality"}
- if field not in valid_fields:
- raise ValueError(f"Invalid field: {field}")
-
- query = sql.SQL(
- "UPDATE scheme.node_simulation SET {} = %s WHERE time = %s AND scheme_type = %s AND scheme_name = %s AND id = %s"
- ).format(sql.Identifier(field))
-
- async with conn.cursor() as cur:
- await cur.execute(query, (value, time, scheme_type, scheme_name, node_id))
-
- @staticmethod
- async def delete_nodes_by_scheme_and_time_range(
- conn: AsyncConnection,
- scheme_type: str,
- scheme_name: str,
- start_time: datetime,
- end_time: datetime,
- ):
- async with conn.cursor() as cur:
- await cur.execute(
- "DELETE FROM scheme.node_simulation WHERE scheme_type = %s AND scheme_name = %s AND time >= %s AND time <= %s",
- (scheme_type, scheme_name, start_time, end_time),
- )
-
- # --- 复合查询 ---
-
- @staticmethod
- async def store_scheme_simulation_result(
- conn: AsyncConnection,
- scheme_type: str,
- scheme_name: str,
- node_result_list: List[Dict[str, any]],
- link_result_list: List[Dict[str, any]],
- result_start_time: str,
- num_periods: int = 1,
- result_timestep_seconds: int | None = None,
- ):
- """
- Store scheme simulation results to TimescaleDB.
-
- Args:
- conn: Database connection
- scheme_type: Scheme type
- scheme_name: Scheme name
- node_result_list: List of node simulation results
- link_result_list: List of link simulation results
- result_start_time: Start time for the results (ISO format string)
- """
- simulation_time = parse_utc_time(
- result_start_time, field_name="result_start_time"
- )
-
- timestep = SchemeRepository._get_result_timestep(result_timestep_seconds)
-
- # Prepare node data for batch insert
- node_data = []
- for node_result in node_result_list:
- node_id = node_result.get("node")
- result_rows = node_result.get("result", [])
- for period_index in range(min(num_periods, len(result_rows))):
- current_time = simulation_time + (timestep * period_index)
- data = result_rows[period_index]
- node_data.append(
- {
- "time": current_time,
- "scheme_type": scheme_type,
- "scheme_name": scheme_name,
- "id": node_id,
- "actual_demand": data.get("demand"),
- "total_head": data.get("head"),
- "pressure": data.get("pressure"),
- "quality": data.get("quality"),
- }
- )
-
- # Prepare link data for batch insert
- link_data = []
- for link_result in link_result_list:
- link_id = link_result.get("link")
- result_rows = link_result.get("result", [])
- for period_index in range(min(num_periods, len(result_rows))):
- current_time = simulation_time + (timestep * period_index)
- data = result_rows[period_index]
- link_data.append(
- {
- "time": current_time,
- "scheme_type": scheme_type,
- "scheme_name": scheme_name,
- "id": link_id,
- "flow": data.get("flow"),
- "friction": data.get("friction"),
- "headloss": data.get("headloss"),
- "quality": data.get("quality"),
- "reaction": data.get("reaction"),
- "setting": data.get("setting"),
- "status": data.get("status"),
- "velocity": data.get("velocity"),
- }
- )
-
- # Insert data using batch methods
- if node_data:
- await SchemeRepository.insert_nodes_batch(conn, node_data)
-
- if link_data:
- await SchemeRepository.insert_links_batch(conn, link_data)
-
- @staticmethod
- def store_scheme_simulation_result_sync(
- conn: Connection,
- scheme_type: str,
- scheme_name: str,
- node_result_list: List[Dict[str, any]],
- link_result_list: List[Dict[str, any]],
- result_start_time: str,
- num_periods: int = 1,
- result_timestep_seconds: int | None = None,
- ):
- """
- Store scheme simulation results to TimescaleDB (sync version).
-
- Args:
- conn: Database connection
- scheme_type: Scheme type
- scheme_name: Scheme name
- node_result_list: List of node simulation results
- link_result_list: List of link simulation results
- result_start_time: Start time for the results (ISO format string)
- """
- simulation_time = parse_utc_time(
- result_start_time, field_name="result_start_time"
- )
-
- timestep = SchemeRepository._get_result_timestep(result_timestep_seconds)
-
- # Prepare node data for batch insert
- node_data = []
- for node_result in node_result_list:
- node_id = node_result.get("node")
- result_rows = node_result.get("result", [])
- for period_index in range(min(num_periods, len(result_rows))):
- current_time = simulation_time + (timestep * period_index)
- data = result_rows[period_index]
- node_data.append(
- {
- "time": current_time,
- "scheme_type": scheme_type,
- "scheme_name": scheme_name,
- "id": node_id,
- "actual_demand": data.get("demand"),
- "total_head": data.get("head"),
- "pressure": data.get("pressure"),
- "quality": data.get("quality"),
- }
- )
-
- # Prepare link data for batch insert
- link_data = []
- for link_result in link_result_list:
- link_id = link_result.get("link")
- result_rows = link_result.get("result", [])
- for period_index in range(min(num_periods, len(result_rows))):
- current_time = simulation_time + (timestep * period_index)
- data = result_rows[period_index]
- link_data.append(
- {
- "time": current_time,
- "scheme_type": scheme_type,
- "scheme_name": scheme_name,
- "id": link_id,
- "flow": data.get("flow"),
- "friction": data.get("friction"),
- "headloss": data.get("headloss"),
- "quality": data.get("quality"),
- "reaction": data.get("reaction"),
- "setting": data.get("setting"),
- "status": data.get("status"),
- "velocity": data.get("velocity"),
- }
- )
-
- # Insert data using batch methods
- if node_data:
- SchemeRepository.insert_nodes_batch_sync(conn, node_data)
-
- if link_data:
- SchemeRepository.insert_links_batch_sync(conn, link_data)
-
- @staticmethod
- async def query_all_record_by_scheme_time_property(
- conn: AsyncConnection,
- scheme_type: str,
- scheme_name: str,
- query_time: str,
- type: str,
- property: str,
- ) -> list:
- """
- Query all records by scheme, time and property from TimescaleDB.
-
- Args:
- conn: Database connection
- scheme_type: Scheme type
- scheme_name: Scheme name
- query_time: Time to query (ISO format string)
- type: Type of data ("node" or "link")
- property: Property/field to query
-
- Returns:
- List of records matching the criteria
- """
- target_time = parse_utc_time(query_time, field_name="query_time")
-
- # Create time range: query_time ± 1 second
- start_time = target_time - timedelta(seconds=1)
- end_time = target_time + timedelta(seconds=1)
-
- # Query based on type
- if type.lower() == "node":
- data = await SchemeRepository.get_nodes_field_by_scheme_and_time_range(
- conn, scheme_type, scheme_name, start_time, end_time, property
- )
- elif type.lower() == "link":
- data = await SchemeRepository.get_links_field_by_scheme_and_time_range(
- conn, scheme_type, scheme_name, start_time, end_time, property
- )
- else:
- raise ValueError(f"Invalid type: {type}. Must be 'node' or 'link'")
-
- # Format the results
- # Format the results
- result = []
- for id, items in data.items():
- for item in items:
- result.append({"ID": id, "value": item["value"]})
- return result
-
- @staticmethod
- async def query_scheme_simulation_result_by_id_time(
- conn: AsyncConnection,
- scheme_type: str,
- scheme_name: str,
- id: str,
- type: str,
- query_time: str,
- ) -> list[dict]:
- """
- Query scheme simulation results by id and time from TimescaleDB.
-
- Args:
- conn: Database connection
- scheme_type: Scheme type
- scheme_name: Scheme name
- id: The id of the node or link
- type: Type of data ("node" or "link")
- query_time: Time to query (ISO format string)
-
- Returns:
- List of records matching the criteria
- """
- target_time = parse_utc_time(query_time, field_name="query_time")
-
- # Create time range: query_time ± 1 second
- start_time = target_time - timedelta(seconds=1)
- end_time = target_time + timedelta(seconds=1)
-
- # Query based on type
- if type.lower() == "node":
- return await SchemeRepository.get_node_by_scheme_and_time_range(
- conn, scheme_type, scheme_name, start_time, end_time, id
- )
- elif type.lower() == "link":
- return await SchemeRepository.get_link_by_scheme_and_time_range(
- conn, scheme_type, scheme_name, start_time, end_time, id
- )
- else:
- raise ValueError(f"Invalid type: {type}. Must be 'node' or 'link'")
diff --git a/app/infra/db/timescaledb/sync_pool.py b/app/infra/db/timescaledb/sync_pool.py
new file mode 100644
index 0000000..6d703d7
--- /dev/null
+++ b/app/infra/db/timescaledb/sync_pool.py
@@ -0,0 +1,95 @@
+from collections import OrderedDict
+from collections.abc import Iterator
+from contextlib import contextmanager
+from threading import RLock
+
+from psycopg import Connection
+from psycopg.rows import dict_row
+from psycopg_pool import ConnectionPool
+
+from app.core.config import settings
+from app.infra.db.project_routing import get_project_timescale_pgconn_string
+
+
+_pools: OrderedDict[str, ConnectionPool] = OrderedDict()
+_pool_conninfo: dict[str, str] = {}
+_pool_borrows: dict[str, int] = {}
+_lock = RLock()
+
+
+def _evict_idle_pools(*, protected: str | None = None) -> None:
+ limit = max(1, settings.PROJECT_TS_CACHE_SIZE)
+ while len(_pools) > limit:
+ candidate = next(
+ (key for key in _pools if key != protected and _pool_borrows.get(key, 0) == 0),
+ None,
+ )
+ if candidate is None:
+ return
+ pool = _pools.pop(candidate)
+ _pool_conninfo.pop(candidate, None)
+ _pool_borrows.pop(candidate, None)
+ if not pool.closed:
+ pool.close()
+
+
+def get_timescale_pool(db_name: str) -> ConnectionPool:
+ conninfo = get_project_timescale_pgconn_string(db_name=db_name)
+ with _lock:
+ pool = _pools.get(db_name)
+ if pool is not None and _pool_conninfo.get(db_name) == conninfo and not pool.closed:
+ _pools.move_to_end(db_name)
+ return pool
+ if pool is not None and not pool.closed:
+ if _pool_borrows.get(db_name, 0):
+ raise RuntimeError(f"Cannot replace active TimescaleDB pool {db_name!r}")
+ pool.close()
+ pool = ConnectionPool(
+ conninfo=conninfo,
+ min_size=settings.PROJECT_TS_POOL_MIN_SIZE,
+ max_size=settings.PROJECT_TS_POOL_MAX_SIZE,
+ kwargs={"row_factory": dict_row},
+ open=True,
+ )
+ _pools[db_name] = pool
+ _pool_conninfo[db_name] = conninfo
+ _pool_borrows.setdefault(db_name, 0)
+ _evict_idle_pools(protected=db_name)
+ return pool
+
+
+@contextmanager
+def timescale_connection(db_name: str) -> Iterator[Connection]:
+ with _lock:
+ pool = get_timescale_pool(db_name)
+ _pool_borrows[db_name] = _pool_borrows.get(db_name, 0) + 1
+ try:
+ with pool.connection() as conn:
+ yield conn
+ finally:
+ with _lock:
+ _pool_borrows[db_name] -= 1
+ _evict_idle_pools()
+
+
+def close_timescale_pool(db_name: str) -> None:
+ with _lock:
+ if _pool_borrows.get(db_name, 0):
+ raise RuntimeError(f"Cannot close active TimescaleDB pool {db_name!r}")
+ pool = _pools.pop(db_name, None)
+ _pool_conninfo.pop(db_name, None)
+ _pool_borrows.pop(db_name, None)
+ if pool is not None and not pool.closed:
+ pool.close()
+
+
+def close_all_timescale_pools() -> None:
+ """Close every synchronous TimescaleDB pool."""
+ with _lock:
+ pools = list(_pools.values())
+ _pools.clear()
+ _pool_conninfo.clear()
+ _pool_borrows.clear()
+ for pool in pools:
+ if not pool.closed:
+ pool.close()
diff --git a/app/infra/epanet/epanet.py b/app/infra/epanet/epanet.py
index a2e490d..cc86302 100644
--- a/app/infra/epanet/epanet.py
+++ b/app/infra/epanet/epanet.py
@@ -11,8 +11,8 @@ from typing import Any
import uuid
sys.path.append("..")
-from app.native.wndb import project
-from app.native.wndb import inp_out
+from app.native.wndb.core import projects
+from app.native.wndb.inp import exporter
def _verify_platform():
@@ -326,13 +326,13 @@ def _make_isolated_run_paths(base_name: str, cwd: str) -> tuple[str, str, str]:
# DingZQ, 2025-02-04, 返回dict[str, Any]
def run_project_return_dict(name: str, readable_output: bool = True) -> dict[str, Any]:
- if not project.have_project(name):
+ if not projects.have_project(name):
raise Exception(f"Not found project [{name}]")
cwd = os.path.abspath(os.getcwd())
inp, rpt, opt = _make_isolated_run_paths(name, cwd)
- inp_out.dump_inp(name, inp, "2")
+ exporter.dump_inp(name, inp, "2")
if platform.system() == "Windows":
exe = os.path.join(os.path.dirname(__file__), "windows", "runepanet.exe")
@@ -389,13 +389,13 @@ def run_project_return_dict(name: str, readable_output: bool = True) -> dict[str
# original code
def run_project(name: str, readable_output: bool = True) -> str:
- if not project.have_project(name):
+ if not projects.have_project(name):
raise Exception(f"Not found project [{name}]")
cwd = os.path.abspath(os.getcwd())
inp, rpt, opt = _make_isolated_run_paths(name, cwd)
- inp_out.dump_inp(name, inp, "2")
+ exporter.dump_inp(name, inp, "2")
if platform.system() == "Windows":
exe = os.path.join(os.path.dirname(__file__), "windows", "runepanet.exe")
diff --git a/app/main.py b/app/main.py
index d3fe175..5896c3e 100644
--- a/app/main.py
+++ b/app/main.py
@@ -8,10 +8,10 @@ from datetime import datetime
import app.services.project_info as project_info
from app.api.problem_details import install_problem_details_handlers
from app.api.v1.rest_router import api_router
-from app.infra.db.timescaledb.database import db as tsdb
-from app.infra.db.postgresql.database import db as pgdb
from app.infra.db.dynamic_manager import project_connection_manager
from app.infra.db.metadb.database import close_metadata_engine
+from app.infra.db.timescaledb.sync_pool import close_all_timescale_pools
+from app.native.wndb.core.connection import close_all_project_pools
from app.services.tjnetwork import open_project
from app.core.config import settings
@@ -30,26 +30,15 @@ async def lifespan(app: FastAPI):
logger.info("TJWater CloudService is starting...")
logger.info("**********************************************************")
- # 初始化数据库连接池
- tsdb.init_pool()
- pgdb.init_pool()
-
- await tsdb.open()
- await pgdb.open()
-
- # 将数据库实例存储到 app.state,供依赖项使用
- app.state.db = pgdb
- logger.info("Database connection pool initialized and stored in app.state")
-
if project_info.name:
print(project_info.name)
open_project(project_info.name)
yield
# 清理资源
- await tsdb.close()
- await pgdb.close()
await project_connection_manager.close_all()
+ close_all_timescale_pools()
+ close_all_project_pools()
await close_metadata_engine()
logger.info("Database connections closed")
diff --git a/app/native/wndb/__init__.py b/app/native/wndb/__init__.py
index 35b9c20..915a3c4 100644
--- a/app/native/wndb/__init__.py
+++ b/app/native/wndb/__init__.py
@@ -1,480 +1,6 @@
-"""`app.native.wndb` 的公共 API 门面。
+"""Water-network database package.
-调用建议:
-- 推荐使用模块方式导入,保持调用点清晰:
- `import app.native.wndb as wndb`
-- 典型流程:
- 1) 项目生命周期:`open_project(...)` / `close_project(...)`
- 2) 模型数据读写:`get_*`, `set_*`, `add_*`, `delete_*`
- 3) 持久化与恢复:`take_snapshot(...)`, `execute_undo()`, `restore(...)`
-
-该文件刻意保持平铺导出,以兼容历史调用。
+Import from the responsibility-specific modules under ``core``, ``model``,
+``gis``, ``inp``, and ``commands``. This package intentionally does not expose
+the former flat compatibility API.
"""
-
-# -----------------------------------------------------------------------------
-# 项目生命周期与 INP 导入导出
-# -----------------------------------------------------------------------------
-from .project import (
- list_project,
- have_project,
- create_project,
- delete_project,
- clean_project,
-)
-from .project import is_project_open, open_project, close_project
-from .project import copy_project
-
-# DingZQ, 2024-12-28: 将 INP v3 转换为 v2
-from .inp_in import read_inp, import_inp, convert_inp_v3_to_v2
-from .inp_out import dump_inp, export_inp
-
-# -----------------------------------------------------------------------------
-# 数据库操作、快照与撤销重做
-# -----------------------------------------------------------------------------
-from .database import API_ADD, API_UPDATE, API_DELETE
-from .database import ChangeSet
-from .database import get_current_operation
-from .database import execute_undo, execute_redo
-from .database import list_snapshot
-from .database import (
- have_snapshot,
- have_snapshot_for_operation,
- have_snapshot_for_current_operation,
-)
-from .database import (
- take_snapshot_for_operation,
- take_snapshot_for_current_operation,
- take_snapshot,
-)
-from .database import update_snapshot, update_snapshot_for_current_operation
-from .database import delete_snapshot, delete_snapshot_by_operation
-from .database import get_operation_by_snapshot, get_snapshot_by_operation
-from .database import pick_snapshot
-from .database import pick_operation, sync_with_server
-from .database import (
- get_restore_operation,
- set_restore_operation,
- set_restore_operation_to_current,
- restore,
-)
-from .database import read, try_read, read_all, write
-
-# -----------------------------------------------------------------------------
-# 批处理执行与扩展数据
-# -----------------------------------------------------------------------------
-from .batch_exe import execute_batch_commands, execute_batch_command
-
-from .extension_data import (
- get_all_extension_data_keys,
- get_all_extension_data,
- get_extension_data,
- set_extension_data,
-)
-
-# -----------------------------------------------------------------------------
-# 核心网络模型基础类型与辅助方法
-# -----------------------------------------------------------------------------
-from .s0_base import JUNCTION, RESERVOIR, TANK, PIPE, PUMP, VALVE, PATTERN, CURVE
-from .s0_base import is_node, is_junction, is_reservoir, is_tank
-from .s0_base import is_link, is_pipe, is_pump, is_valve
-from .s0_base import is_curve
-from .s0_base import is_pattern
-from .s0_base import (
- get_nodes,
- get_nodes_id_and_type,
- get_junctions,
- get_reservoirs,
- get_tanks,
- get_links,
- get_links_id_and_type,
- get_pipes,
- get_pumps,
- get_valves,
- get_curves,
- get_patterns,
-)
-from .s0_base import (
- get_node_type,
- get_link_type,
- get_element_type,
- get_element_type_value,
-)
-from .s0_base import get_node_links, get_link_nodes
-from .s0_base import get_major_nodes, get_major_pipes
-
-# -----------------------------------------------------------------------------
-# EPANET 基础分段(S1-S27)
-# -----------------------------------------------------------------------------
-from .s1_title import get_title_schema, get_title, set_title
-
-from .s2_junctions import (
- get_junction_schema,
- add_junction,
- get_junction,
- set_junction,
- get_all_junctions,
-)
-from .batch_api import delete_junction_cascade
-
-from .s3_reservoirs import (
- get_reservoir_schema,
- add_reservoir,
- get_reservoir,
- set_reservoir,
- get_all_reservoirs,
-)
-from .batch_api import delete_reservoir_cascade
-
-from .s4_tanks import OVERFLOW_YES, OVERFLOW_NO
-from .s4_tanks import get_tank_schema, add_tank, get_tank, set_tank, get_all_tanks
-from .batch_api import delete_tank_cascade
-
-from .s5_pipes import PIPE_STATUS_OPEN, PIPE_STATUS_CLOSED, PIPE_STATUS_CV
-from .s5_pipes import (
- get_pipe_schema,
- add_pipe,
- get_pipe,
- set_pipe,
- get_all_pipes,
- get_pipes_by_property,
-)
-from .batch_api import delete_pipe_cascade
-
-from .s6_pumps import get_pump_schema, add_pump, get_pump, set_pump, get_all_pumps
-from .batch_api import delete_pump_cascade
-
-from .s7_valves import (
- VALVES_TYPE_PRV,
- VALVES_TYPE_PSV,
- VALVES_TYPE_PBV,
- VALVES_TYPE_FCV,
- VALVES_TYPE_TCV,
- VALVES_TYPE_GPV,
-)
-from .s7_valves import get_valve_schema, add_valve, get_valve, set_valve, get_all_valves
-from .batch_api import delete_valve_cascade
-
-from .s8_tags import TAG_TYPE_NODE, TAG_TYPE_LINK
-from .s8_tags import get_tag_schema, get_tags, get_tag, set_tag
-
-from .s9_demands import get_demand_schema, get_demand, set_demand
-
-from .s10_status import LINK_STATUS_OPEN, LINK_STATUS_CLOSED, LINK_STATUS_ACTIVE
-from .s10_status import get_status_schema, get_status, set_status
-
-from .s11_patterns import get_pattern_schema, get_pattern, set_pattern, add_pattern
-from .batch_api import delete_pattern_cascade
-
-from .s12_curves import (
- CURVE_TYPE_PUMP,
- CURVE_TYPE_EFFICIENCY,
- CURVE_TYPE_VOLUME,
- CURVE_TYPE_HEADLOSS,
-)
-from .s12_curves import get_curve_schema, get_curve, set_curve, add_curve
-from .batch_api import delete_curve_cascade
-
-from .s13_controls import get_control_schema, get_control, set_control
-
-from .s14_rules import get_rule_schema, get_rule, set_rule
-
-from .s15_energy import get_energy_schema, get_energy, set_energy
-from .s15_energy import get_pump_energy_schema, get_pump_energy, set_pump_energy
-
-from .s16_emitters import get_emitter_schema, get_emitter, set_emitter
-
-from .s17_quality import get_quality_schema, get_quality, set_quality
-
-from .s18_sources import (
- SOURCE_TYPE_CONCEN,
- SOURCE_TYPE_MASS,
- SOURCE_TYPE_FLOWPACED,
- SOURCE_TYPE_SETPOINT,
-)
-from .s18_sources import (
- get_source_schema,
- get_source,
- set_source,
- add_source,
- delete_source,
-)
-
-from .s19_reactions import get_reaction_schema, get_reaction, set_reaction
-from .s19_reactions import (
- get_pipe_reaction_schema,
- get_pipe_reaction,
- set_pipe_reaction,
-)
-from .s19_reactions import (
- get_tank_reaction_schema,
- get_tank_reaction,
- set_tank_reaction,
-)
-
-from .s20_mixing import (
- MIXING_MODEL_MIXED,
- MIXING_MODEL_2COMP,
- MIXING_MODEL_FIFO,
- MIXING_MODEL_LIFO,
-)
-from .s20_mixing import (
- get_mixing_schema,
- get_mixing,
- set_mixing,
- add_mixing,
- delete_mixing,
-)
-
-from .s21_times import (
- TIME_STATISTIC_NONE,
- TIME_STATISTIC_AVERAGED,
- TIME_STATISTIC_MINIMUM,
- TIME_STATISTIC_MAXIMUM,
- TIME_STATISTIC_RANGE,
-)
-from .s21_times import get_time_schema, get_time, set_time
-
-from .s23_options_util import (
- OPTION_UNITS_CFS,
- OPTION_UNITS_GPM,
- OPTION_UNITS_MGD,
- OPTION_UNITS_IMGD,
- OPTION_UNITS_AFD,
- OPTION_UNITS_LPS,
- OPTION_UNITS_LPM,
- OPTION_UNITS_MLD,
- OPTION_UNITS_CMH,
- OPTION_UNITS_CMD,
-)
-from .s23_options_util import (
- OPTION_PRESSURE_PSI,
- OPTION_PRESSURE_KPA,
- OPTION_PRESSURE_METERS,
-)
-from .s23_options_util import OPTION_HEADLOSS_HW, OPTION_HEADLOSS_DW, OPTION_HEADLOSS_CM
-from .s23_options_util import OPTION_UNBALANCED_STOP, OPTION_UNBALANCED_CONTINUE
-from .s23_options_util import OPTION_DEMAND_MODEL_DDA, OPTION_DEMAND_MODEL_PDA
-from .s23_options_util import (
- OPTION_QUALITY_NONE,
- OPTION_QUALITY_CHEMICAL,
- OPTION_QUALITY_AGE,
- OPTION_QUALITY_TRACE,
-)
-from .s23_options_util import get_option_schema, get_option
-from .batch_api import set_option_ex
-
-from .s23_options_util import (
- OPTION_V3_FLOW_UNITS_CFS,
- OPTION_V3_FLOW_UNITS_GPM,
- OPTION_V3_FLOW_UNITS_MGD,
- OPTION_V3_FLOW_UNITS_IMGD,
- OPTION_V3_FLOW_UNITS_AFD,
- OPTION_V3_FLOW_UNITS_LPS,
- OPTION_V3_FLOW_UNITS_LPM,
- OPTION_V3_FLOW_UNITS_MLD,
- OPTION_V3_FLOW_UNITS_CMH,
- OPTION_V3_FLOW_UNITS_CMD,
-)
-from .s23_options_util import (
- OPTION_V3_PRESSURE_UNITS_PSI,
- OPTION_V3_PRESSURE_UNITS_KPA,
- OPTION_V3_PRESSURE_UNITS_METERS,
-)
-from .s23_options_util import (
- OPTION_V3_HEADLOSS_MODEL_HW,
- OPTION_V3_HEADLOSS_MODEL_DW,
- OPTION_V3_HEADLOSS_MODEL_CM,
-)
-from .s23_options_util import (
- OPTION_V3_STEP_SIZING_FULL,
- OPTION_V3_STEP_SIZING_RELAXATION,
- OPTION_V3_STEP_SIZING_LINESEARCH,
-)
-from .s23_options_util import (
- OPTION_V3_IF_UNBALANCED_STOP,
- OPTION_V3_IF_UNBALANCED_CONTINUE,
-)
-from .s23_options_util import (
- OPTION_V3_DEMAND_MODEL_FIXED,
- OPTION_V3_DEMAND_MODEL_CONSTRAINED,
- OPTION_V3_DEMAND_MODEL_POWER,
- OPTION_V3_DEMAND_MODEL_LOGISTIC,
-)
-from .s23_options_util import (
- OPTION_V3_LEAKAGE_MODEL_NONE,
- OPTION_V3_LEAKAGE_MODEL_POWER,
- OPTION_V3_LEAKAGE_MODEL_FAVAD,
-)
-from .s23_options_util import (
- OPTION_V3_QUALITY_MODEL_NONE,
- OPTION_V3_QUALITY_MODEL_CHEMICAL,
- OPTION_V3_QUALITY_MODEL_AGE,
- OPTION_V3_QUALITY_MODEL_TRACE,
-)
-from .s23_options_util import (
- OPTION_V3_QUALITY_UNITS_HRS,
- OPTION_V3_QUALITY_UNITS_PCNT,
- OPTION_V3_QUALITY_UNITS_MGL,
- OPTION_V3_QUALITY_UNITS_UGL,
-)
-from .s23_options_util import get_option_v3_schema, get_option_v3
-from .batch_api import set_option_v3_ex
-
-from .s24_coordinates import (
- get_links_in_extent,
- get_node_coord,
- get_nodes_in_extent,
-)
-
-from .s25_vertices import (
- get_vertex_schema,
- get_vertex,
- set_vertex,
- add_vertex,
- delete_vertex,
-)
-from .s25_vertices import get_all_vertex_links, get_all_vertices
-
-from .s26_labels import get_label_schema, get_label, set_label, add_label, delete_label
-
-from .s27_backdrop import get_backdrop_schema, get_backdrop, set_backdrop
-
-# -----------------------------------------------------------------------------
-# SCADA 映射与遥测实体
-# -----------------------------------------------------------------------------
-from .s29_scada_device import (
- SCADA_DEVICE_TYPE_PRESSURE,
- SCADA_DEVICE_TYPE_DEMAND,
- SCADA_DEVICE_TYPE_QUALITY,
- SCADA_DEVICE_TYPE_LEVEL,
- SCADA_DEVICE_TYPE_FLOW,
- SCADA_DEVICE_TYPE_UNKNOWN,
-)
-from .s29_scada_device import (
- get_scada_device_schema,
- get_scada_device,
- set_scada_device,
- add_scada_device,
- delete_scada_device,
-)
-from .s29_scada_device import get_all_scada_device_ids, get_all_scada_devices
-from .clean_api import clean_scada_device
-
-from .s30_scada_device_data import (
- get_scada_device_data_schema,
- get_scada_device_data,
- set_scada_device_data,
- add_scada_device_data,
- delete_scada_device_data,
-)
-from .clean_api import clean_scada_device_data
-
-from .s31_scada_element import (
- SCADA_MODEL_TYPE_JUNCTION,
- SCADA_MODEL_TYPE_RESERVOIR,
- SCADA_MODEL_TYPE_TANK,
- SCADA_MODEL_TYPE_PIPE,
- SCADA_MODEL_TYPE_PUMP,
- SCADA_MODEL_TYPE_VALVE,
-)
-from .s31_scada_element import SCADA_ELEMENT_STATUS_OFFLINE, SCADA_ELEMENT_STATUS_ONLINE
-from .s31_scada_element import (
- get_scada_element_schema,
- get_scada_element,
- set_scada_element,
- add_scada_element,
- delete_scada_element,
-)
-from .s31_scada_element import get_all_scada_element_ids, get_all_scada_elements
-from .clean_api import clean_scada_element
-
-# -----------------------------------------------------------------------------
-# 区域、DMA、服务分区、虚拟分区与需水分配
-# -----------------------------------------------------------------------------
-from .s32_region_util import (
- get_nodes_in_boundary,
- get_nodes_in_region,
- get_links_on_region_boundary,
- calculate_convex_hull,
- calculate_boundary,
- inflate_boundary,
- inflate_region,
-)
-from .s32_region import (
- get_region_schema,
- get_region,
- set_region,
- add_region,
- delete_region,
-)
-
-from .s33_dma_cal import PARTITION_TYPE_RB, PARTITION_TYPE_KWAY
-from .s33_dma_cal import (
- calculate_district_metering_area_for_nodes,
- calculate_district_metering_area_for_region,
- calculate_district_metering_area_for_network,
-)
-from .s33_dma import (
- get_district_metering_area_schema,
- get_district_metering_area,
- set_district_metering_area,
- add_district_metering_area,
- delete_district_metering_area,
-)
-from .s33_dma import get_all_district_metering_area_ids, get_all_district_metering_areas
-from .s33_dma_gen import (
- generate_district_metering_area,
- generate_sub_district_metering_area,
-)
-
-from .s34_sa_cal import calculate_service_area
-from .s34_sa import (
- get_service_area_schema,
- get_service_area,
- set_service_area,
- add_service_area,
- delete_service_area,
-)
-from .s34_sa import get_all_service_area_ids, get_all_service_areas
-from .s34_sa_gen import generate_service_area
-
-from .s35_vd_cal import calculate_virtual_district
-from .s35_vd import (
- get_virtual_district_schema,
- get_virtual_district,
- set_virtual_district,
- add_virtual_district,
- delete_virtual_district,
-)
-from .s35_vd import get_all_virtual_district_ids, get_all_virtual_districts
-from .s35_vd_gen import generate_virtual_district
-
-from .s36_wda_cal import (
- calculate_demand_to_nodes,
- calculate_demand_to_region,
- calculate_demand_to_network,
-)
-
-# -----------------------------------------------------------------------------
-# 元数据与高级分析
-# -----------------------------------------------------------------------------
-from .s38_scada_info import get_scada_info_schema, get_scada_info, get_all_scada_info
-
-from .s40_schema import get_scheme_schema, get_scheme, get_all_schemes
-
-from .s41_pipe_risk_probability import (
- get_pipe_risk_probability_now,
- get_pipe_risk_probability,
- get_network_pipe_risk_probability_now,
- get_pipes_risk_probability,
- get_pipe_risk_probability_geometries,
-)
-
-from .s42_sensor_placement import (
- get_all_sensor_placements,
- get_sensor_placement,
- get_sensor_placement_nodes,
- update_sensor_placement,
-)
-
-from .s43_burst_locate_result import get_all_burst_locate_results
diff --git a/app/native/wndb/batch_api.py b/app/native/wndb/batch_api.py
deleted file mode 100644
index 1c47d54..0000000
--- a/app/native/wndb/batch_api.py
+++ /dev/null
@@ -1,53 +0,0 @@
-from .sections import *
-from .database import ChangeSet, API_DELETE, API_UPDATE
-from .batch_exe import execute_batch_command
-
-
-def delete_junction_cascade(name: str, cs: ChangeSet) -> ChangeSet:
- cs.operations[0] |= { 'operation' : API_DELETE, 'type' : s2_junction }
- return execute_batch_command(name, cs)
-
-
-def delete_reservoir_cascade(name: str, cs: ChangeSet) -> ChangeSet:
- cs.operations[0] |= { 'operation' : API_DELETE, 'type' : s3_reservoir }
- return execute_batch_command(name, cs)
-
-
-def delete_tank_cascade(name: str, cs: ChangeSet) -> ChangeSet:
- cs.operations[0] |= { 'operation' : API_DELETE, 'type' : s4_tank }
- return execute_batch_command(name, cs)
-
-
-def delete_pipe_cascade(name: str, cs: ChangeSet) -> ChangeSet:
- cs.operations[0] |= { 'operation' : API_DELETE, 'type' : s5_pipe }
- return execute_batch_command(name, cs)
-
-
-def delete_pump_cascade(name: str, cs: ChangeSet) -> ChangeSet:
- cs.operations[0] |= { 'operation' : API_DELETE, 'type' : s6_pump }
- return execute_batch_command(name, cs)
-
-
-def delete_valve_cascade(name: str, cs: ChangeSet) -> ChangeSet:
- cs.operations[0] |= { 'operation' : API_DELETE, 'type' : s7_valve }
- return execute_batch_command(name, cs)
-
-
-def delete_pattern_cascade(name: str, cs: ChangeSet) -> ChangeSet:
- cs.operations[0] |= { 'operation' : API_DELETE, 'type' : s11_pattern }
- return execute_batch_command(name, cs)
-
-
-def delete_curve_cascade(name: str, cs: ChangeSet) -> ChangeSet:
- cs.operations[0] |= { 'operation' : API_DELETE, 'type' : s12_curve }
- return execute_batch_command(name, cs)
-
-
-def set_option_ex(name: str, cs: ChangeSet) -> ChangeSet:
- cs.operations[0] |= { 'operation' : API_UPDATE, 'type' : s23_option }
- return execute_batch_command(name, cs)
-
-
-def set_option_v3_ex(name: str, cs: ChangeSet) -> ChangeSet:
- cs.operations[0] |= { 'operation' : API_UPDATE, 'type' : s23_option_v3 }
- return execute_batch_command(name, cs)
diff --git a/app/native/wndb/batch_api_cs.py b/app/native/wndb/batch_api_cs.py
deleted file mode 100644
index ec94f53..0000000
--- a/app/native/wndb/batch_api_cs.py
+++ /dev/null
@@ -1,238 +0,0 @@
-from .database import ChangeSet, g_delete_prefix, API_DELETE, API_UPDATE, try_read
-from .sections import *
-
-from .s0_base import *
-
-from .s3_reservoirs import unset_reservoir_by_pattern
-from .s4_tanks import unset_tank_by_curve
-from .s6_pumps import unset_pump_by_curve, unset_pump_by_pattern
-from .s8_tags import delete_tag_by_node, delete_tag_by_link
-from .s9_demands import delete_demand_by_junction, unset_demand_by_pattern
-from .s10_status import delete_status_by_link
-from .s15_energy import delete_pump_energy_by_pump, unset_pump_energy_by_pattern, unset_pump_energy_by_curve
-from .s16_emitters import delete_emitter_by_junction
-from .s17_quality import delete_quality_by_node
-from .s18_sources import delete_source_by_node, unset_source_by_pattern
-from .s19_reactions import delete_pipe_reaction_by_pipe, delete_tank_reaction_by_tank
-from .s20_mixing import delete_mixing_by_tank
-from .s25_vertices import delete_vertex_by_link
-from .s26_labels import unset_label_by_node
-
-from .s23_options_util import generate_v2, generate_v3
-
-
-def delete_junction_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet:
- result = ChangeSet()
-
- id = cs.operations[0]['id']
- row = try_read(name, f"select * from junctions where id = '{id}'")
- if row == None:
- return result
-
- links = get_node_links(name, id)
-
- for link in links:
- if is_pipe(name, link):
- result.merge(delete_pipe_cascade_batch_cs(name, ChangeSet(g_delete_prefix | {'type': 'pipe', 'id': link})))
- if is_pump(name, link):
- result.merge(delete_pump_cascade_batch_cs(name, ChangeSet(g_delete_prefix | {'type': 'pump', 'id': link})))
- if is_valve(name, link):
- result.merge(delete_valve_cascade_batch_cs(name, ChangeSet(g_delete_prefix | {'type': 'valve', 'id': link})))
-
- result.merge(delete_tag_by_node(name, id))
- result.merge(delete_demand_by_junction(name, id))
- result.merge(delete_emitter_by_junction(name, id))
- result.merge(delete_quality_by_node(name, id))
- result.merge(delete_source_by_node(name, id))
- result.merge(unset_label_by_node(name, id))
- result.merge(cs)
-
- return result
-
-
-def delete_reservoir_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet:
- result = ChangeSet()
-
- id = cs.operations[0]['id']
- row = try_read(name, f"select * from reservoirs where id = '{id}'")
- if row == None:
- return result
-
- links = get_node_links(name, id)
-
- for link in links:
- if is_pipe(name, link):
- result.merge(delete_pipe_cascade_batch_cs(name, ChangeSet(g_delete_prefix | {'type': 'pipe', 'id': link})))
- if is_pump(name, link):
- result.merge(delete_pump_cascade_batch_cs(name, ChangeSet(g_delete_prefix | {'type': 'pump', 'id': link})))
- if is_valve(name, link):
- result.merge(delete_valve_cascade_batch_cs(name, ChangeSet(g_delete_prefix | {'type': 'valve', 'id': link})))
-
- result.merge(delete_tag_by_node(name, id))
- result.merge(delete_quality_by_node(name, id))
- result.merge(delete_source_by_node(name, id))
- result.merge(unset_label_by_node(name, id))
- result.merge(cs)
-
- return result
-
-
-def delete_tank_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet:
- result = ChangeSet()
-
- id = cs.operations[0]['id']
- row = try_read(name, f"select * from tanks where id = '{id}'")
- if row == None:
- return result
-
- links = get_node_links(name, id)
-
- for link in links:
- if is_pipe(name, link):
- result.merge(delete_pipe_cascade_batch_cs(name, ChangeSet(g_delete_prefix | {'type': 'pipe', 'id': link})))
- if is_pump(name, link):
- result.merge(delete_pump_cascade_batch_cs(name, ChangeSet(g_delete_prefix | {'type': 'pump', 'id': link})))
- if is_valve(name, link):
- result.merge(delete_valve_cascade_batch_cs(name, ChangeSet(g_delete_prefix | {'type': 'valve', 'id': link})))
-
- result.merge(delete_tag_by_node(name, id))
- result.merge(delete_quality_by_node(name, id))
- result.merge(delete_source_by_node(name, id))
- result.merge(delete_tank_reaction_by_tank(name, id))
- result.merge(delete_mixing_by_tank(name, id))
- result.merge(unset_label_by_node(name, id))
- result.merge(cs)
-
- return result
-
-
-def delete_pipe_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet:
- result = ChangeSet()
-
- id = cs.operations[0]['id']
- row = try_read(name, f"select * from pipes where id = '{id}'")
- if row == None:
- return result
-
- result.merge(delete_tag_by_link(name, id))
- result.merge(delete_status_by_link(name, id))
- result.merge(delete_pipe_reaction_by_pipe(name, id))
- result.merge(delete_vertex_by_link(name, id))
- result.merge(cs)
-
- return result
-
-
-def delete_pump_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet:
- result = ChangeSet()
-
- id = cs.operations[0]['id']
- row = try_read(name, f"select * from pumps where id = '{id}'")
- if row == None:
- return result
-
- result.merge(delete_tag_by_link(name, id))
- result.merge(delete_status_by_link(name, id))
- result.merge(delete_pump_energy_by_pump(name, id))
- result.merge(delete_vertex_by_link(name, id))
- result.merge(cs)
-
- return result
-
-
-def delete_valve_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet:
- result = ChangeSet()
-
- id = cs.operations[0]['id']
- row = try_read(name, f"select * from valves where id = '{id}'")
- if row == None:
- return result
-
- result.merge(delete_tag_by_link(name, id))
- result.merge(delete_status_by_link(name, id))
- result.merge(delete_vertex_by_link(name, id))
-
- result.merge(cs)
-
- return result
-
-
-def delete_pattern_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet:
- result = ChangeSet()
-
- id = cs.operations[0]['id']
- row = try_read(name, f"select * from _pattern where id = '{id}'")
- if row == None:
- return result
-
- result.merge(unset_reservoir_by_pattern(name, id))
- result.merge(unset_pump_by_pattern(name, id))
- result.merge(unset_demand_by_pattern(name, id))
- result.merge(unset_pump_energy_by_pattern(name, id))
- result.merge(unset_source_by_pattern(name, id))
- result.merge(cs)
-
- return result
-
-
-def delete_curve_cascade_batch_cs(name: str, cs: ChangeSet) -> ChangeSet:
- result = ChangeSet()
-
- id = cs.operations[0]['id']
- row = try_read(name, f"select * from _curve where id = '{id}'")
- if row == None:
- return result
-
- result.merge(unset_tank_by_curve(name, id))
- result.merge(unset_pump_by_curve(name, id))
- result.merge(unset_pump_energy_by_curve(name, id))
- result.merge(cs)
-
- return result
-
-
-def set_option_cs(cs: ChangeSet) -> ChangeSet:
- cs.operations[0]['operation'] = API_UPDATE
- cs.operations[0]['type'] = 'option'
- new_cs = cs
- new_cs.merge(generate_v3(cs))
- return new_cs
-
-
-def set_option_v3_cs(cs: ChangeSet) -> ChangeSet:
- cs.operations[0]['operation'] = API_UPDATE
- cs.operations[0]['type'] = 'option_v3'
- new_cs = cs
- new_cs.merge(generate_v2(cs))
- return new_cs
-
-
-def rewrite_batch_api(name: str, cs: ChangeSet) -> ChangeSet:
- op = cs.operations[0]
- api = op['operation']
- type = op['type']
-
- if api == API_DELETE:
- if type == s2_junction:
- return delete_junction_cascade_batch_cs(name, cs)
- elif type == s3_reservoir:
- return delete_reservoir_cascade_batch_cs(name, cs)
- elif type == s4_tank:
- return delete_tank_cascade_batch_cs(name, cs)
- elif type == s5_pipe:
- return delete_pipe_cascade_batch_cs(name, cs)
- elif type == s6_pump:
- return delete_pump_cascade_batch_cs(name, cs)
- elif type == s7_valve:
- return delete_valve_cascade_batch_cs(name, cs)
- elif type == s11_pattern:
- return delete_pattern_cascade_batch_cs(name, cs)
- elif type == s12_curve:
- return delete_curve_cascade_batch_cs(name, cs)
- elif api == API_UPDATE:
- if type == s23_option:
- return set_option_cs(cs)
- elif type == s23_option_v3:
- return set_option_v3_cs(cs)
-
- return cs
diff --git a/app/native/wndb/batch_exe.py b/app/native/wndb/batch_exe.py
deleted file mode 100644
index 080688e..0000000
--- a/app/native/wndb/batch_exe.py
+++ /dev/null
@@ -1,380 +0,0 @@
-from typing import Any
-from .sections import *
-from .database import API_ADD, API_UPDATE, API_DELETE, ChangeSet, write, read, read_all, get_current_operation
-from .extension_data import set_extension_data
-from .s1_title import set_title
-from .s2_junctions import set_junction, add_junction, delete_junction
-from .s3_reservoirs import set_reservoir, add_reservoir, delete_reservoir
-from .s4_tanks import set_tank, add_tank, delete_tank
-from .s5_pipes import set_pipe, add_pipe, delete_pipe
-from .s6_pumps import set_pump, add_pump, delete_pump
-from .s7_valves import set_valve, add_valve, delete_valve
-from .s8_tags import set_tag
-from .s9_demands import set_demand
-from .s10_status import set_status
-from .s11_patterns import set_pattern, add_pattern, delete_pattern
-from .s12_curves import set_curve, add_curve, delete_curve
-from .s13_controls import set_control
-from .s14_rules import set_rule
-from .s15_energy import set_energy, set_pump_energy
-from .s16_emitters import set_emitter
-from .s17_quality import set_quality
-from .s18_sources import set_source, add_source, delete_source
-from .s19_reactions import set_reaction, set_pipe_reaction, set_tank_reaction
-from .s20_mixing import set_mixing, add_mixing, delete_mixing
-from .s21_times import set_time
-from .s23_options_util import set_option, set_option_v3
-from .s25_vertices import set_vertex, add_vertex, delete_vertex
-from .s26_labels import set_label, add_label, delete_label
-from .s27_backdrop import set_backdrop
-from .s29_scada_device import set_scada_device, add_scada_device, delete_scada_device
-from .s30_scada_device_data import set_scada_device_data, add_scada_device_data, delete_scada_device_data
-from .s31_scada_element import set_scada_element, add_scada_element, delete_scada_element
-from .s32_region import set_region, add_region, delete_region
-from .s33_dma import set_district_metering_area, add_district_metering_area, delete_district_metering_area
-from .s34_sa import set_service_area, add_service_area, delete_service_area
-from .s35_vd import set_virtual_district, add_virtual_district, delete_virtual_district
-from .batch_api_cs import rewrite_batch_api
-
-
-def _execute_add_command(name: str, cs: ChangeSet) -> ChangeSet:
- type = cs.operations[0]['type']
-
- if type == s1_title:
- return ChangeSet()
- if type == s2_junction:
- return add_junction(name, cs)
- elif type == s3_reservoir:
- return add_reservoir(name, cs)
- elif type == s4_tank:
- return add_tank(name, cs)
- elif type == s5_pipe:
- return add_pipe(name, cs)
- elif type == s6_pump:
- return add_pump(name, cs)
- elif type == s7_valve:
- return add_valve(name, cs)
- elif type == s8_tag:
- return ChangeSet()
- elif type == s9_demand:
- return ChangeSet()
- elif type == s10_status:
- return ChangeSet()
- elif type == s11_pattern:
- return add_pattern(name, cs)
- elif type == s12_curve:
- return add_curve(name, cs)
- elif type == s13_control:
- return ChangeSet()
- elif type == s14_rule:
- return ChangeSet()
- elif type == s15_energy:
- return ChangeSet()
- elif type == s15_pump_energy:
- return ChangeSet()
- elif type == s16_emitter:
- return ChangeSet()
- elif type == s17_quality:
- return ChangeSet()
- elif type == s18_source:
- return add_source(name, cs)
- elif type == s19_reaction:
- return ChangeSet()
- elif type == s19_pipe_reaction:
- return ChangeSet()
- elif type == s19_tank_reaction:
- return ChangeSet()
- elif type == s20_mixing:
- return add_mixing(name, cs)
- elif type == s21_time:
- return ChangeSet()
- elif type == s22_report:
- return ChangeSet()
- elif type == s23_option:
- return ChangeSet()
- elif type == s23_option_v3:
- return ChangeSet()
- elif type == s24_coordinate:
- return ChangeSet()
- elif type == s25_vertex:
- return add_vertex(name, cs)
- elif type == s26_label:
- return add_label(name, cs)
- elif type == s27_backdrop:
- return ChangeSet()
- elif type == s28_end:
- return ChangeSet()
- elif type == s29_scada_device:
- return add_scada_device(name, cs)
- elif type == s30_scada_device_data:
- return add_scada_device_data(name, cs)
- elif type == s31_scada_element:
- return add_scada_element(name, cs)
- elif type == s32_region:
- return add_region(name, cs)
- elif type == s33_dma:
- return add_district_metering_area(name, cs)
- elif type == s34_sa:
- return add_service_area(name, cs)
- elif type == s35_vd:
- return add_virtual_district(name, cs)
-
- return ChangeSet()
-
-
-def _execute_update_command(name: str, cs: ChangeSet) -> ChangeSet:
- type = cs.operations[0]['type']
-
- if type == 'extension_data':
- return set_extension_data(name, cs)
- if type == s1_title:
- return set_title(name, cs)
- if type == s2_junction:
- return set_junction(name, cs)
- elif type == s3_reservoir:
- return set_reservoir(name, cs)
- elif type == s4_tank:
- return set_tank(name, cs)
- elif type == s5_pipe:
- return set_pipe(name, cs)
- elif type == s6_pump:
- return set_pump(name, cs)
- elif type == s7_valve:
- return set_valve(name, cs)
- elif type == s8_tag:
- return set_tag(name, cs)
- elif type == s9_demand:
- return set_demand(name, cs)
- elif type == s10_status:
- return set_status(name, cs)
- elif type == s11_pattern:
- return set_pattern(name, cs)
- elif type == s12_curve:
- return set_curve(name, cs)
- elif type == s13_control:
- return set_control(name, cs)
- elif type == s14_rule:
- return set_rule(name, cs)
- elif type == s15_energy:
- return set_energy(name, cs)
- elif type == s15_pump_energy:
- return set_pump_energy(name, cs)
- elif type == s16_emitter:
- return set_emitter(name, cs)
- elif type == s17_quality:
- return set_quality(name, cs)
- elif type == s18_source:
- return set_source(name, cs)
- elif type == s19_reaction:
- return set_reaction(name, cs)
- elif type == s19_pipe_reaction:
- return set_pipe_reaction(name, cs)
- elif type == s19_tank_reaction:
- return set_tank_reaction(name, cs)
- elif type == s20_mixing:
- return set_mixing(name, cs)
- elif type == s21_time:
- return set_time(name, cs)
- elif type == s22_report: # no api now
- return ChangeSet()
- elif type == s23_option:
- return set_option(name, cs)
- elif type == s23_option_v3:
- return set_option_v3(name, cs)
- elif type == s24_coordinate: # do not support update here
- return ChangeSet()
- elif type == s25_vertex:
- return set_vertex(name, cs)
- elif type == s26_label:
- return set_label(name, cs)
- elif type == s27_backdrop:
- return set_backdrop(name, cs)
- elif type == s28_end: # end
- return ChangeSet()
- elif type == s29_scada_device:
- return set_scada_device(name, cs)
- elif type == s30_scada_device_data:
- return set_scada_device_data(name, cs)
- elif type == s31_scada_element:
- return set_scada_element(name, cs)
- elif type == s32_region:
- return set_region(name, cs)
- elif type == s33_dma:
- return set_district_metering_area(name, cs)
- elif type == s34_sa:
- return set_service_area(name, cs)
- elif type == s35_vd:
- return set_virtual_district(name, cs)
-
- return ChangeSet()
-
-
-def _execute_delete_command(name: str, cs: ChangeSet) -> ChangeSet:
- type = cs.operations[0]['type']
-
- if type == s1_title:
- return ChangeSet()
- if type == s2_junction:
- return delete_junction(name, cs)
- elif type == s3_reservoir:
- return delete_reservoir(name, cs)
- elif type == s4_tank:
- return delete_tank(name, cs)
- elif type == s5_pipe:
- return delete_pipe(name, cs)
- elif type == s6_pump:
- return delete_pump(name, cs)
- elif type == s7_valve:
- return delete_valve(name, cs)
- elif type == s8_tag:
- return ChangeSet()
- elif type == s9_demand:
- return ChangeSet()
- elif type == s10_status:
- return ChangeSet()
- elif type == s11_pattern:
- return delete_pattern(name, cs)
- elif type == s12_curve:
- return delete_curve(name, cs)
- elif type == s13_control:
- return ChangeSet()
- elif type == s14_rule:
- return ChangeSet()
- elif type == s15_energy:
- return ChangeSet()
- elif type == s15_pump_energy:
- return ChangeSet()
- elif type == s16_emitter:
- return ChangeSet()
- elif type == s17_quality:
- return ChangeSet()
- elif type == s18_source:
- return delete_source(name, cs)
- elif type == s19_reaction:
- return ChangeSet()
- elif type == s19_pipe_reaction:
- return ChangeSet()
- elif type == s19_tank_reaction:
- return ChangeSet()
- elif type == s20_mixing:
- return delete_mixing(name, cs)
- elif type == s21_time:
- return ChangeSet()
- elif type == s22_report:
- return ChangeSet()
- elif type == s23_option:
- return ChangeSet()
- elif type == s23_option_v3:
- return ChangeSet()
- elif type == s24_coordinate:
- return ChangeSet()
- elif type == s25_vertex:
- return delete_vertex(name, cs)
- elif type == s26_label:
- return delete_label(name, cs)
- elif type == s27_backdrop:
- return ChangeSet()
- elif type == s28_end:
- return ChangeSet()
- elif type == s29_scada_device:
- return delete_scada_device(name, cs)
- elif type == s30_scada_device_data:
- return delete_scada_device_data(name, cs)
- elif type == s31_scada_element:
- return delete_scada_element(name, cs)
- elif type == s32_region:
- return delete_region(name, cs)
- elif type == s33_dma:
- return delete_district_metering_area(name, cs)
- elif type == s34_sa:
- return delete_service_area(name, cs)
- elif type == s35_vd:
- return delete_virtual_district(name, cs)
-
- return ChangeSet()
-
-
-def execute_batch_commands(name: str, cs: ChangeSet) -> ChangeSet:
- new_cs = ChangeSet()
- for op in cs.operations:
- new_cs.merge(rewrite_batch_api(name, ChangeSet(op)))
-
- result = ChangeSet()
-
- todo = {}
-
- try:
- for op in new_cs.operations:
- todo = op
- operation = op['operation']
- if operation == API_ADD:
- result.merge(_execute_add_command(name, ChangeSet(op)))
- elif operation == API_UPDATE:
- result.merge(_execute_update_command(name, ChangeSet(op)))
- elif operation == API_DELETE:
- result.merge(_execute_delete_command(name, ChangeSet(op)))
- except:
- print(f'ERROR: Fail to execute {todo}')
-
- return result
-
-
-def execute_batch_command(name: str, cs: ChangeSet) -> ChangeSet:
- write(name, 'delete from batch_operation where id > 0')
- write(name, "update operation_table set option = 'batch_operation' where option = 'operation'")
-
- new_cs = ChangeSet()
- for op in cs.operations:
- new_cs.merge(rewrite_batch_api(name, ChangeSet(op)))
-
- result = ChangeSet()
-
- todo = {}
-
- try:
- for op in new_cs.operations:
- todo = op
- operation = op['operation']
- if operation == API_ADD:
- result.merge(_execute_add_command(name, ChangeSet(op)))
- elif operation == API_UPDATE:
- result.merge(_execute_update_command(name, ChangeSet(op)))
- elif operation == API_DELETE:
- result.merge(_execute_delete_command(name, ChangeSet(op)))
- except:
- print(f'ERROR: Fail to execute {todo}')
-
- count = read(name, 'select count(*) as count from batch_operation')['count']
- if count == 1:
- write(name, 'delete from batch_operation where id > 0')
- write(name, "update operation_table set option = 'operation' where option = 'batch_operation'")
- return ChangeSet()
-
- redo_list: list[str] = []
- redo_cs_list: list[dict[str, Any]] = []
- redo_rows = read_all(name, 'select redo, redo_cs from batch_operation where id > 0 order by id asc')
- for row in redo_rows:
- redo_list.append(row['redo'])
- redo_cs_list += eval(row['redo_cs'])
-
- undo_list: list[str] = []
- undo_cs_list: list[dict[str, Any]] = []
- undo_rows = read_all(name, 'select undo, undo_cs from batch_operation where id > 0 order by id desc')
- for row in undo_rows:
- undo_list.append(row['undo'])
- undo_cs_list += eval(row['undo_cs'])
-
- redo = '\n'.join(redo_list).replace("'", "''")
- redo_cs = str(redo_cs_list).replace("'", "''")
- undo = '\n'.join(undo_list).replace("'", "''")
- undo_cs = str(undo_cs_list).replace("'", "''")
-
- parent = get_current_operation(name)
- write(name, f"insert into operation (id, redo, undo, parent, redo_cs, undo_cs) values (default, '{redo}', '{undo}', {parent}, '{redo_cs}', '{undo_cs}')")
- current = read(name, 'select max(id) as id from operation')['id']
- write(name, f"update current_operation set id = {current}")
-
- write(name, 'delete from batch_operation where id > 0')
- write(name, "update operation_table set option = 'operation' where option = 'batch_operation'")
-
- return result
diff --git a/app/native/wndb/clean_api.py b/app/native/wndb/clean_api.py
deleted file mode 100644
index ba3d7f8..0000000
--- a/app/native/wndb/clean_api.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from .database import ChangeSet, read_all
-from .batch_exe import execute_batch_command
-
-# TODO: merge to batch_api
-
-def clean_scada_device_cs(name: str) -> ChangeSet:
- cs = ChangeSet()
-
- rows = read_all(name, 'select id from scada_device acs')
- for row in rows:
- cs.delete({ 'type': 'scada_device', 'id': row['id'] })
-
- return cs
-
-
-def clean_scada_device_data_cs(name: str) -> ChangeSet:
- cs = ChangeSet()
-
- rows = read_all(name, 'select distinct device_id from scada_device_data acs')
- for row in rows:
- cs.update({ 'type': 'scada_device_data', 'device_id': row['device_id'], 'data': [] })
-
- return cs
-
-
-def clean_scada_element_cs(name: str) -> ChangeSet:
- cs = ChangeSet()
-
- rows = read_all(name, 'select id from scada_element acs')
- for row in rows:
- cs.delete({ 'type': 'scada_element', 'id': row['id'] })
-
- return cs
-
-
-def clean_scada_device(name: str) -> ChangeSet:
- return execute_batch_command(name, clean_scada_device_cs(name))
-
-
-def clean_scada_device_data(name: str) -> ChangeSet:
- return execute_batch_command(name, clean_scada_device_data_cs(name))
-
-
-def clean_scada_element(name: str) -> ChangeSet:
- return execute_batch_command(name, clean_scada_element_cs(name))
diff --git a/app/native/wndb/commands/__init__.py b/app/native/wndb/commands/__init__.py
new file mode 100644
index 0000000..a1a7f7f
--- /dev/null
+++ b/app/native/wndb/commands/__init__.py
@@ -0,0 +1 @@
+"""Model command rewriting, cascade handling, and execution."""
diff --git a/app/native/wndb/commands/api.py b/app/native/wndb/commands/api.py
new file mode 100644
index 0000000..f5e99b6
--- /dev/null
+++ b/app/native/wndb/commands/api.py
@@ -0,0 +1,65 @@
+"""Convenience entry points for cascade deletes and option synchronization."""
+
+from ..core.database import API_DELETE, API_UPDATE, ChangeSet
+from .executor import execute_batch_command
+
+
+def _execute(
+ name: str,
+ change_set: ChangeSet,
+ *,
+ operation: str,
+ element_type: str,
+) -> ChangeSet:
+ change_set.operations[0].update(
+ {"operation": operation, "type": element_type}
+ )
+ return execute_batch_command(name, change_set)
+
+
+def delete_junction_cascade(name: str, change_set: ChangeSet) -> ChangeSet:
+ return _execute(
+ name, change_set, operation=API_DELETE, element_type="junction"
+ )
+
+
+def delete_reservoir_cascade(name: str, change_set: ChangeSet) -> ChangeSet:
+ return _execute(
+ name, change_set, operation=API_DELETE, element_type="reservoir"
+ )
+
+
+def delete_tank_cascade(name: str, change_set: ChangeSet) -> ChangeSet:
+ return _execute(name, change_set, operation=API_DELETE, element_type="tank")
+
+
+def delete_pipe_cascade(name: str, change_set: ChangeSet) -> ChangeSet:
+ return _execute(name, change_set, operation=API_DELETE, element_type="pipe")
+
+
+def delete_pump_cascade(name: str, change_set: ChangeSet) -> ChangeSet:
+ return _execute(name, change_set, operation=API_DELETE, element_type="pump")
+
+
+def delete_valve_cascade(name: str, change_set: ChangeSet) -> ChangeSet:
+ return _execute(name, change_set, operation=API_DELETE, element_type="valve")
+
+
+def delete_pattern_cascade(name: str, change_set: ChangeSet) -> ChangeSet:
+ return _execute(
+ name, change_set, operation=API_DELETE, element_type="pattern"
+ )
+
+
+def delete_curve_cascade(name: str, change_set: ChangeSet) -> ChangeSet:
+ return _execute(name, change_set, operation=API_DELETE, element_type="curve")
+
+
+def set_option_ex(name: str, change_set: ChangeSet) -> ChangeSet:
+ return _execute(name, change_set, operation=API_UPDATE, element_type="option")
+
+
+def set_option_v3_ex(name: str, change_set: ChangeSet) -> ChangeSet:
+ return _execute(
+ name, change_set, operation=API_UPDATE, element_type="option_v3"
+ )
diff --git a/app/native/wndb/commands/cascade.py b/app/native/wndb/commands/cascade.py
new file mode 100644
index 0000000..b0669a9
--- /dev/null
+++ b/app/native/wndb/commands/cascade.py
@@ -0,0 +1,244 @@
+from collections.abc import Callable
+
+from ..core.database import API_DELETE, API_UPDATE, ChangeSet, g_delete_prefix, try_read
+from ..model.elements import get_node_links, is_pipe, is_pump, is_valve
+
+from ..model.reservoirs import unset_reservoir_by_pattern
+from ..model.tanks import unset_tank_by_curve
+from ..model.pumps import unset_pump_by_curve, unset_pump_by_pattern
+from ..model.tags import delete_tag_by_node, delete_tag_by_link
+from ..model.demands import delete_demand_by_junction, unset_demand_by_pattern
+from ..model.status import delete_status_by_link
+from ..model.energy import delete_pump_energy_by_pump, unset_pump_energy_by_pattern, unset_pump_energy_by_curve
+from ..model.emitters import delete_emitter_by_junction
+from ..model.quality import delete_quality_by_node
+from ..model.sources import delete_source_by_node, unset_source_by_pattern
+from ..model.reactions import delete_pipe_reaction_by_pipe, delete_tank_reaction_by_tank
+from ..model.mixing import delete_mixing_by_tank
+from ..gis.vertices import delete_vertex_by_link
+from ..gis.labels import unset_label_by_node
+
+from ..model.options import generate_v2, generate_v3
+
+
+def expand_junction_delete(name: str, cs: ChangeSet) -> ChangeSet:
+ result = ChangeSet()
+
+ id = cs.operations[0]['id']
+ row = try_read(name, "select 1 from network.junctions where node_id = %s", (id,))
+ if row == None:
+ return result
+
+ links = get_node_links(name, id)
+
+ for link in links:
+ if is_pipe(name, link):
+ result.merge(expand_pipe_delete(name, ChangeSet(g_delete_prefix | {'type': 'pipe', 'id': link})))
+ if is_pump(name, link):
+ result.merge(expand_pump_delete(name, ChangeSet(g_delete_prefix | {'type': 'pump', 'id': link})))
+ if is_valve(name, link):
+ result.merge(expand_valve_delete(name, ChangeSet(g_delete_prefix | {'type': 'valve', 'id': link})))
+
+ result.merge(delete_tag_by_node(name, id))
+ result.merge(delete_demand_by_junction(name, id))
+ result.merge(delete_emitter_by_junction(name, id))
+ result.merge(delete_quality_by_node(name, id))
+ result.merge(delete_source_by_node(name, id))
+ result.merge(unset_label_by_node(name, id))
+ result.merge(cs)
+
+ return result
+
+
+def expand_reservoir_delete(name: str, cs: ChangeSet) -> ChangeSet:
+ result = ChangeSet()
+
+ id = cs.operations[0]['id']
+ row = try_read(name, "select 1 from network.reservoirs where node_id = %s", (id,))
+ if row == None:
+ return result
+
+ links = get_node_links(name, id)
+
+ for link in links:
+ if is_pipe(name, link):
+ result.merge(expand_pipe_delete(name, ChangeSet(g_delete_prefix | {'type': 'pipe', 'id': link})))
+ if is_pump(name, link):
+ result.merge(expand_pump_delete(name, ChangeSet(g_delete_prefix | {'type': 'pump', 'id': link})))
+ if is_valve(name, link):
+ result.merge(expand_valve_delete(name, ChangeSet(g_delete_prefix | {'type': 'valve', 'id': link})))
+
+ result.merge(delete_tag_by_node(name, id))
+ result.merge(delete_quality_by_node(name, id))
+ result.merge(delete_source_by_node(name, id))
+ result.merge(unset_label_by_node(name, id))
+ result.merge(cs)
+
+ return result
+
+
+def expand_tank_delete(name: str, cs: ChangeSet) -> ChangeSet:
+ result = ChangeSet()
+
+ id = cs.operations[0]['id']
+ row = try_read(name, "select 1 from network.tanks where node_id = %s", (id,))
+ if row == None:
+ return result
+
+ links = get_node_links(name, id)
+
+ for link in links:
+ if is_pipe(name, link):
+ result.merge(expand_pipe_delete(name, ChangeSet(g_delete_prefix | {'type': 'pipe', 'id': link})))
+ if is_pump(name, link):
+ result.merge(expand_pump_delete(name, ChangeSet(g_delete_prefix | {'type': 'pump', 'id': link})))
+ if is_valve(name, link):
+ result.merge(expand_valve_delete(name, ChangeSet(g_delete_prefix | {'type': 'valve', 'id': link})))
+
+ result.merge(delete_tag_by_node(name, id))
+ result.merge(delete_quality_by_node(name, id))
+ result.merge(delete_source_by_node(name, id))
+ result.merge(delete_tank_reaction_by_tank(name, id))
+ result.merge(delete_mixing_by_tank(name, id))
+ result.merge(unset_label_by_node(name, id))
+ result.merge(cs)
+
+ return result
+
+
+def expand_pipe_delete(name: str, cs: ChangeSet) -> ChangeSet:
+ result = ChangeSet()
+
+ id = cs.operations[0]['id']
+ row = try_read(name, "select 1 from network.pipes where link_id = %s", (id,))
+ if row == None:
+ return result
+
+ result.merge(delete_tag_by_link(name, id))
+ result.merge(delete_status_by_link(name, id))
+ result.merge(delete_pipe_reaction_by_pipe(name, id))
+ result.merge(delete_vertex_by_link(name, id))
+ result.merge(cs)
+
+ return result
+
+
+def expand_pump_delete(name: str, cs: ChangeSet) -> ChangeSet:
+ result = ChangeSet()
+
+ id = cs.operations[0]['id']
+ row = try_read(name, "select 1 from network.pumps where link_id = %s", (id,))
+ if row == None:
+ return result
+
+ result.merge(delete_tag_by_link(name, id))
+ result.merge(delete_status_by_link(name, id))
+ result.merge(delete_pump_energy_by_pump(name, id))
+ result.merge(delete_vertex_by_link(name, id))
+ result.merge(cs)
+
+ return result
+
+
+def expand_valve_delete(name: str, cs: ChangeSet) -> ChangeSet:
+ result = ChangeSet()
+
+ id = cs.operations[0]['id']
+ row = try_read(name, "select 1 from network.valves where link_id = %s", (id,))
+ if row == None:
+ return result
+
+ result.merge(delete_tag_by_link(name, id))
+ result.merge(delete_status_by_link(name, id))
+ result.merge(delete_vertex_by_link(name, id))
+
+ result.merge(cs)
+
+ return result
+
+
+def expand_pattern_delete(name: str, cs: ChangeSet) -> ChangeSet:
+ result = ChangeSet()
+
+ id = cs.operations[0]['id']
+ row = try_read(name, "select 1 from network.patterns where id = %s", (id,))
+ if row == None:
+ return result
+
+ result.merge(unset_reservoir_by_pattern(name, id))
+ result.merge(unset_pump_by_pattern(name, id))
+ result.merge(unset_demand_by_pattern(name, id))
+ result.merge(unset_pump_energy_by_pattern(name, id))
+ result.merge(unset_source_by_pattern(name, id))
+ result.merge(cs)
+
+ return result
+
+
+def expand_curve_delete(name: str, cs: ChangeSet) -> ChangeSet:
+ result = ChangeSet()
+
+ id = cs.operations[0]['id']
+ row = try_read(name, "select 1 from network.curves where id = %s", (id,))
+ if row == None:
+ return result
+
+ result.merge(unset_tank_by_curve(name, id))
+ result.merge(unset_pump_by_curve(name, id))
+ result.merge(unset_pump_energy_by_curve(name, id))
+ result.merge(cs)
+
+ return result
+
+
+def expand_legacy_options_update(cs: ChangeSet) -> ChangeSet:
+ cs.operations[0]['operation'] = API_UPDATE
+ cs.operations[0]['type'] = 'option'
+ new_cs = cs
+ new_cs.merge(generate_v3(cs))
+ return new_cs
+
+
+def expand_v3_options_update(cs: ChangeSet) -> ChangeSet:
+ cs.operations[0]['operation'] = API_UPDATE
+ cs.operations[0]['type'] = 'option_v3'
+ new_cs = cs
+ new_cs.merge(generate_v2(cs))
+ return new_cs
+
+
+def expand_command(name: str, cs: ChangeSet) -> ChangeSet:
+ op = cs.operations[0]
+ operation = op['operation']
+ element_type = op['type']
+
+ if operation == API_DELETE:
+ handler = _DELETE_REWRITERS.get(element_type)
+ if handler:
+ return handler(name, cs)
+ elif operation == API_UPDATE:
+ handler = _UPDATE_REWRITERS.get(element_type)
+ if handler:
+ return handler(cs)
+
+ return cs
+
+
+DeleteRewriter = Callable[[str, ChangeSet], ChangeSet]
+UpdateRewriter = Callable[[ChangeSet], ChangeSet]
+
+_DELETE_REWRITERS: dict[str, DeleteRewriter] = {
+ "junction": expand_junction_delete,
+ "reservoir": expand_reservoir_delete,
+ "tank": expand_tank_delete,
+ "pipe": expand_pipe_delete,
+ "pump": expand_pump_delete,
+ "valve": expand_valve_delete,
+ "pattern": expand_pattern_delete,
+ "curve": expand_curve_delete,
+}
+
+_UPDATE_REWRITERS: dict[str, UpdateRewriter] = {
+ "option": expand_legacy_options_update,
+ "option_v3": expand_v3_options_update,
+}
diff --git a/app/native/wndb/commands/executor.py b/app/native/wndb/commands/executor.py
new file mode 100644
index 0000000..7eea13e
--- /dev/null
+++ b/app/native/wndb/commands/executor.py
@@ -0,0 +1,155 @@
+"""Transactional command dispatch for WNDB model mutations."""
+
+from collections.abc import Callable
+
+from ..core.connection import project_transaction
+from ..core.database import (
+ API_ADD,
+ API_DELETE,
+ API_UPDATE,
+ ChangeSet,
+ refresh_materialized_views,
+)
+from ..gis.backdrop import set_backdrop
+from ..gis.labels import add_label, delete_label, set_label
+from ..gis.regions import add_region, delete_region, set_region
+from ..gis.vertices import add_vertex, delete_vertex, set_vertex
+from ..model.controls import set_control
+from ..model.curves import add_curve, delete_curve, set_curve
+from ..model.demands import set_demand
+from ..model.emitters import set_emitter
+from ..model.energy import set_energy, set_pump_energy
+from ..model.junctions import add_junction, delete_junction, set_junction
+from ..model.mixing import add_mixing, delete_mixing, set_mixing
+from ..model.options import set_option, set_option_v3
+from ..model.patterns import add_pattern, delete_pattern, set_pattern
+from ..model.pipes import add_pipe, delete_pipe, set_pipe
+from ..model.pumps import add_pump, delete_pump, set_pump
+from ..model.quality import set_quality
+from ..model.reactions import (
+ set_pipe_reaction,
+ set_reaction,
+ set_tank_reaction,
+)
+from ..model.reservoirs import add_reservoir, delete_reservoir, set_reservoir
+from ..model.rules import set_rule
+from ..model.sources import add_source, delete_source, set_source
+from ..model.status import set_status
+from ..model.tags import set_tag
+from ..model.tanks import add_tank, delete_tank, set_tank
+from ..model.times import set_time
+from ..model.title import set_title
+from ..model.valves import add_valve, delete_valve, set_valve
+from .cascade import expand_command
+
+CommandHandler = Callable[[str, ChangeSet], ChangeSet]
+
+_ADD_HANDLERS: dict[str, CommandHandler] = {
+ "junction": add_junction,
+ "reservoir": add_reservoir,
+ "tank": add_tank,
+ "pipe": add_pipe,
+ "pump": add_pump,
+ "valve": add_valve,
+ "pattern": add_pattern,
+ "curve": add_curve,
+ "source": add_source,
+ "mixing": add_mixing,
+ "vertex": add_vertex,
+ "label": add_label,
+ "region": add_region,
+}
+
+_UPDATE_HANDLERS: dict[str, CommandHandler] = {
+ "title": set_title,
+ "junction": set_junction,
+ "reservoir": set_reservoir,
+ "tank": set_tank,
+ "pipe": set_pipe,
+ "pump": set_pump,
+ "valve": set_valve,
+ "tag": set_tag,
+ "demand": set_demand,
+ "status": set_status,
+ "pattern": set_pattern,
+ "curve": set_curve,
+ "control": set_control,
+ "rule": set_rule,
+ "energy": set_energy,
+ "pump_energy": set_pump_energy,
+ "emitter": set_emitter,
+ "quality": set_quality,
+ "source": set_source,
+ "reaction": set_reaction,
+ "pipe_reaction": set_pipe_reaction,
+ "tank_reaction": set_tank_reaction,
+ "mixing": set_mixing,
+ "time": set_time,
+ "option": set_option,
+ "option_v3": set_option_v3,
+ "vertex": set_vertex,
+ "label": set_label,
+ "backdrop": set_backdrop,
+ "region": set_region,
+}
+
+_DELETE_HANDLERS: dict[str, CommandHandler] = {
+ "junction": delete_junction,
+ "reservoir": delete_reservoir,
+ "tank": delete_tank,
+ "pipe": delete_pipe,
+ "pump": delete_pump,
+ "valve": delete_valve,
+ "pattern": delete_pattern,
+ "curve": delete_curve,
+ "source": delete_source,
+ "mixing": delete_mixing,
+ "vertex": delete_vertex,
+ "label": delete_label,
+ "region": delete_region,
+}
+
+
+def _dispatch(
+ handlers: dict[str, CommandHandler], name: str, change_set: ChangeSet
+) -> ChangeSet:
+ element_type = change_set.operations[0]["type"]
+ handler = handlers.get(element_type)
+ return handler(name, change_set) if handler else ChangeSet()
+
+
+def _execute_add_command(name: str, change_set: ChangeSet) -> ChangeSet:
+ return _dispatch(_ADD_HANDLERS, name, change_set)
+
+
+def _execute_update_command(name: str, change_set: ChangeSet) -> ChangeSet:
+ return _dispatch(_UPDATE_HANDLERS, name, change_set)
+
+
+def _execute_delete_command(name: str, change_set: ChangeSet) -> ChangeSet:
+ return _dispatch(_DELETE_HANDLERS, name, change_set)
+
+
+def execute_batch_commands(name: str, change_set: ChangeSet) -> ChangeSet:
+ with project_transaction(name):
+ rewritten = ChangeSet()
+ for operation in change_set.operations:
+ rewritten.merge(expand_command(name, ChangeSet(operation)))
+
+ result = ChangeSet()
+ for operation in rewritten.operations:
+ operation_type = operation["operation"]
+ if operation_type == API_ADD:
+ result.merge(_execute_add_command(name, ChangeSet(operation)))
+ elif operation_type == API_UPDATE:
+ result.merge(_execute_update_command(name, ChangeSet(operation)))
+ elif operation_type == API_DELETE:
+ result.merge(_execute_delete_command(name, ChangeSet(operation)))
+
+ if rewritten.operations:
+ refresh_materialized_views(name)
+ return result
+
+
+def execute_batch_command(name: str, change_set: ChangeSet) -> ChangeSet:
+ return execute_batch_commands(name, change_set)
diff --git a/app/native/wndb/connection.py b/app/native/wndb/connection.py
deleted file mode 100644
index db4cce1..0000000
--- a/app/native/wndb/connection.py
+++ /dev/null
@@ -1,90 +0,0 @@
-from collections.abc import Iterator
-from contextlib import contextmanager
-from threading import RLock
-
-import psycopg as pg
-
-from app.infra.db.project_routing import get_project_pgconn_string
-
-g_conn_dict: dict[str, pg.Connection] = {}
-g_conninfo_dict: dict[str, str] = {}
-_registry_lock = RLock()
-_project_locks: dict[str, RLock] = {}
-
-
-def _is_closed(connection: pg.Connection) -> bool:
- return bool(getattr(connection, "closed", False))
-
-
-def _close_connection(connection: pg.Connection) -> None:
- if not _is_closed(connection):
- connection.close()
-
-
-def _is_healthy(connection: pg.Connection) -> bool:
- if _is_closed(connection):
- return False
- try:
- with connection.cursor() as cur:
- cur.execute("SELECT 1")
- except pg.Error:
- return False
- return True
-
-
-def _get_project_lock(name: str) -> RLock:
- with _registry_lock:
- lock = _project_locks.get(name)
- if lock is None:
- lock = RLock()
- _project_locks[name] = lock
- return lock
-
-
-def open_connection(name: str) -> pg.Connection:
- with _get_project_lock(name):
- conninfo = get_project_pgconn_string(db_name=name)
- connection = g_conn_dict.get(name)
- if (
- connection is None
- or g_conninfo_dict.get(name) != conninfo
- or not _is_healthy(connection)
- ):
- if connection is not None:
- _close_connection(connection)
- connection = pg.connect(conninfo=conninfo, autocommit=True)
- g_conn_dict[name] = connection
- g_conninfo_dict[name] = conninfo
- return connection
-
-
-def is_connection_open(name: str) -> bool:
- with _get_project_lock(name):
- connection = g_conn_dict.get(name)
- if connection is None:
- return False
- if not _is_healthy(connection):
- del g_conn_dict[name]
- g_conninfo_dict.pop(name, None)
- _close_connection(connection)
- return False
- if g_conninfo_dict.get(name) != get_project_pgconn_string(db_name=name):
- del g_conn_dict[name]
- g_conninfo_dict.pop(name, None)
- _close_connection(connection)
- return False
- return True
-
-
-def close_connection(name: str) -> None:
- with _get_project_lock(name):
- connection = g_conn_dict.pop(name, None)
- g_conninfo_dict.pop(name, None)
- if connection is not None:
- _close_connection(connection)
-
-
-@contextmanager
-def project_connection(name: str) -> Iterator[pg.Connection]:
- with _get_project_lock(name):
- yield open_connection(name)
diff --git a/app/native/wndb/core/__init__.py b/app/native/wndb/core/__init__.py
new file mode 100644
index 0000000..abacd0c
--- /dev/null
+++ b/app/native/wndb/core/__init__.py
@@ -0,0 +1 @@
+"""WNDB connection, transaction, and project lifecycle infrastructure."""
diff --git a/app/native/wndb/core/connection.py b/app/native/wndb/core/connection.py
new file mode 100644
index 0000000..d3e6d82
--- /dev/null
+++ b/app/native/wndb/core/connection.py
@@ -0,0 +1,224 @@
+from collections import OrderedDict
+from collections.abc import Iterator
+from contextlib import contextmanager
+from contextvars import ContextVar
+from threading import RLock
+
+from psycopg import Connection
+from psycopg.rows import dict_row
+from psycopg_pool import ConnectionPool
+
+from app.core.config import settings
+from app.infra.db.project_routing import get_project_pgconn_string
+
+_pools: OrderedDict[str, ConnectionPool] = OrderedDict()
+_pool_conninfo: dict[str, str] = {}
+_pool_borrows: dict[str, int] = {}
+_admin_pools: OrderedDict[str, ConnectionPool] = OrderedDict()
+_admin_pool_borrows: dict[str, int] = {}
+_registry_lock = RLock()
+_active_project_connection: ContextVar[tuple[str, Connection] | None] = ContextVar(
+ "wndb_active_project_connection",
+ default=None,
+)
+
+
+def _close_pool(pool: ConnectionPool) -> None:
+ if not pool.closed:
+ pool.close()
+
+
+def _evict_idle_project_pools(*, protected: str | None = None) -> None:
+ limit = max(1, settings.PROJECT_PG_CACHE_SIZE)
+ while len(_pools) > limit:
+ candidate = next(
+ (key for key in _pools if key != protected and _pool_borrows.get(key, 0) == 0),
+ None,
+ )
+ if candidate is None:
+ return
+ pool = _pools.pop(candidate)
+ _pool_conninfo.pop(candidate, None)
+ _pool_borrows.pop(candidate, None)
+ _close_pool(pool)
+
+
+def _evict_idle_admin_pools(*, protected: str | None = None) -> None:
+ limit = max(1, settings.PROJECT_PG_CACHE_SIZE)
+ while len(_admin_pools) > limit:
+ candidate = next(
+ (
+ key
+ for key in _admin_pools
+ if key != protected and _admin_pool_borrows.get(key, 0) == 0
+ ),
+ None,
+ )
+ if candidate is None:
+ return
+ pool = _admin_pools.pop(candidate)
+ _admin_pool_borrows.pop(candidate, None)
+ _close_pool(pool)
+
+
+def get_project_pool(name: str) -> ConnectionPool:
+ """Return the routed synchronous pool used by native WNDB operations."""
+ conninfo = get_project_pgconn_string(db_name=name)
+ with _registry_lock:
+ pool = _pools.get(name)
+ if pool is not None and _pool_conninfo.get(name) == conninfo and not pool.closed:
+ _pools.move_to_end(name)
+ return pool
+ if pool is not None:
+ if _pool_borrows.get(name, 0):
+ raise RuntimeError(f"Cannot replace active project pool {name!r}")
+ _close_pool(pool)
+ pool = ConnectionPool(
+ conninfo=conninfo,
+ min_size=settings.PROJECT_PG_POOL_MIN_SIZE,
+ max_size=settings.PROJECT_PG_POOL_SIZE + settings.PROJECT_PG_MAX_OVERFLOW,
+ kwargs={"autocommit": True, "row_factory": dict_row},
+ open=True,
+ )
+ _pools[name] = pool
+ _pool_conninfo[name] = conninfo
+ _pool_borrows.setdefault(name, 0)
+ _evict_idle_project_pools(protected=name)
+ return pool
+
+
+def is_project_pool_open(name: str) -> bool:
+ with _registry_lock:
+ pool = _pools.get(name)
+ if pool is None or pool.closed:
+ return False
+ if _pool_conninfo.get(name) != get_project_pgconn_string(db_name=name):
+ if _pool_borrows.get(name, 0):
+ return False
+ _close_pool(pool)
+ _pools.pop(name, None)
+ _pool_conninfo.pop(name, None)
+ _pool_borrows.pop(name, None)
+ return False
+ return True
+
+
+def close_project_pool(name: str) -> None:
+ with _registry_lock:
+ if _pool_borrows.get(name, 0):
+ raise RuntimeError(f"Cannot close active project pool {name!r}")
+ pool = _pools.pop(name, None)
+ _pool_conninfo.pop(name, None)
+ _pool_borrows.pop(name, None)
+ if pool is not None:
+ _close_pool(pool)
+
+
+def close_all_project_pools() -> None:
+ """Close every WNDB project pool and the administration pool."""
+ with _registry_lock:
+ pools = [*_pools.values(), *_admin_pools.values()]
+ _pools.clear()
+ _pool_conninfo.clear()
+ _pool_borrows.clear()
+ _admin_pools.clear()
+ _admin_pool_borrows.clear()
+ for pool in pools:
+ _close_pool(pool)
+
+
+def get_admin_pool() -> ConnectionPool:
+ """Return the administration pool for the current routed PostgreSQL host."""
+ conninfo = get_project_pgconn_string(db_name="postgres")
+ with _registry_lock:
+ pool = _admin_pools.get(conninfo)
+ if pool is not None and not pool.closed:
+ _admin_pools.move_to_end(conninfo)
+ return pool
+ pool = ConnectionPool(
+ conninfo=conninfo,
+ min_size=settings.PROJECT_PG_POOL_MIN_SIZE,
+ max_size=settings.PROJECT_PG_POOL_SIZE,
+ kwargs={"autocommit": True, "row_factory": dict_row},
+ open=True,
+ )
+ _admin_pools[conninfo] = pool
+ _admin_pool_borrows.setdefault(conninfo, 0)
+ _evict_idle_admin_pools(protected=conninfo)
+ return pool
+
+
+@contextmanager
+def project_connection(name: str) -> Iterator[Connection]:
+ """Borrow one routed WNDB connection and return it to its pool on exit."""
+ active = _active_project_connection.get()
+ if active is not None:
+ active_name, conn = active
+ if active_name != name:
+ raise RuntimeError(
+ f"Cannot access project {name!r} inside transaction for {active_name!r}"
+ )
+ yield conn
+ return
+ with _registry_lock:
+ pool = get_project_pool(name)
+ _pool_borrows[name] = _pool_borrows.get(name, 0) + 1
+ try:
+ with pool.connection() as conn:
+ yield conn
+ finally:
+ with _registry_lock:
+ _pool_borrows[name] -= 1
+ _evict_idle_project_pools()
+
+
+@contextmanager
+def project_transaction(name: str) -> Iterator[Connection]:
+ """Run all nested WNDB operations on one pooled connection and transaction."""
+ active = _active_project_connection.get()
+ if active is not None:
+ active_name, conn = active
+ if active_name != name:
+ raise RuntimeError(
+ f"Cannot nest project {name!r} inside transaction for {active_name!r}"
+ )
+ with conn.transaction():
+ yield conn
+ return
+
+ with _registry_lock:
+ pool = get_project_pool(name)
+ _pool_borrows[name] = _pool_borrows.get(name, 0) + 1
+ try:
+ with pool.connection() as conn:
+ token = _active_project_connection.set((name, conn))
+ try:
+ with conn.transaction():
+ yield conn
+ finally:
+ _active_project_connection.reset(token)
+ finally:
+ with _registry_lock:
+ _pool_borrows[name] -= 1
+ _evict_idle_project_pools()
+
+
+def is_project_transaction_active(name: str) -> bool:
+ active = _active_project_connection.get()
+ return active is not None and active[0] == name
+
+
+@contextmanager
+def admin_connection() -> Iterator[Connection]:
+ """Borrow a PostgreSQL administration connection from its pool."""
+ with _registry_lock:
+ pool = get_admin_pool()
+ conninfo = pool.conninfo
+ _admin_pool_borrows[conninfo] = _admin_pool_borrows.get(conninfo, 0) + 1
+ try:
+ with pool.connection() as conn:
+ yield conn
+ finally:
+ with _registry_lock:
+ _admin_pool_borrows[conninfo] -= 1
+ _evict_idle_admin_pools()
diff --git a/app/native/wndb/core/database.py b/app/native/wndb/core/database.py
new file mode 100644
index 0000000..b71d7fb
--- /dev/null
+++ b/app/native/wndb/core/database.py
@@ -0,0 +1,143 @@
+from collections.abc import Mapping, Sequence
+from typing import Any
+
+from psycopg import sql
+from psycopg.rows import Row, dict_row
+
+from .connection import is_project_transaction_active, project_connection
+
+API_ADD = "add"
+API_UPDATE = "update"
+API_DELETE = "delete"
+
+g_add_prefix = {"operation": API_ADD}
+g_update_prefix = {"operation": API_UPDATE}
+g_delete_prefix = {"operation": API_DELETE}
+
+
+class ChangeSet:
+ def __init__(self, ps: dict[str, Any] | None = None):
+ self.operations: list[dict[str, Any]] = []
+ if ps is not None:
+ self.append(ps)
+
+ @staticmethod
+ def from_list(ps: list[dict[str, Any]]):
+ change_set = ChangeSet()
+ for item in ps:
+ change_set.append(item)
+ return change_set
+
+ def add(self, ps: dict[str, Any]):
+ self.operations.append(g_add_prefix | ps)
+ return self
+
+ def update(self, ps: dict[str, Any]):
+ self.operations.append(g_update_prefix | ps)
+ return self
+
+ def delete(self, ps: dict[str, Any]):
+ self.operations.append(g_delete_prefix | ps)
+ return self
+
+ def append(self, ps: dict[str, Any]):
+ self.operations.append(ps)
+ return self
+
+ def merge(self, change_set):
+ self.operations.extend(change_set.operations)
+ return self
+
+ def dump(self):
+ for operation in self.operations:
+ print(operation)
+
+ def compress(self):
+ return self
+
+
+class DatabaseCommand:
+ def __init__(self, statement: str, changes: list[dict[str, Any]]) -> None:
+ self.sql = statement
+ self.changes = changes
+
+QueryParams = Sequence[Any] | Mapping[str, Any]
+
+
+def sql_literal(value: Any) -> str:
+ """Render one PostgreSQL literal for legacy WNDB SQL batch builders.
+
+ WNDB still assembles multi-statement model changes before executing them as
+ one transaction. Every interpolated value must pass through this helper;
+ identifiers remain static strings owned by the backend.
+ """
+ return sql.Literal(value).as_string()
+
+
+def _execute(cur, query: str, params: QueryParams | None = None):
+ return cur.execute(query, params) if params is not None else cur.execute(query)
+
+
+def read(name: str, query: str, params: QueryParams | None = None) -> Row:
+ with project_connection(name) as conn, conn.cursor(row_factory=dict_row) as cur:
+ _execute(cur, query, params)
+ row = cur.fetchone()
+ if row is None:
+ raise LookupError(query)
+ return row
+
+
+def read_all(
+ name: str, query: str, params: QueryParams | None = None
+) -> list[Row]:
+ with project_connection(name) as conn, conn.cursor(row_factory=dict_row) as cur:
+ _execute(cur, query, params)
+ return cur.fetchall()
+
+
+def try_read(
+ name: str, query: str, params: QueryParams | None = None
+) -> Row | None:
+ with project_connection(name) as conn, conn.cursor(row_factory=dict_row) as cur:
+ _execute(cur, query, params)
+ return cur.fetchone()
+
+
+def write(name: str, query: str, params: QueryParams | None = None) -> None:
+ with project_connection(name) as conn, conn.cursor() as cur:
+ _execute(cur, query, params)
+
+
+def refresh_materialized_views(name: str, *, concurrently: bool = True) -> None:
+ """Refresh the GIS query layer after committed model or asset changes."""
+ with project_connection(name) as conn, conn.cursor() as cur:
+ cur.execute("CALL gis.refresh_all_materialized_views(%s)", (concurrently,))
+
+
+_MATERIALIZED_VIEW_SOURCES = (
+ "network.nodes",
+ "network.junctions",
+ "network.reservoirs",
+ "network.tanks",
+ "network.links",
+ "network.pipes",
+ "network.pumps",
+ "network.valves",
+ "network.demands",
+ "gis.node_geometries",
+ "gis.link_vertices",
+ "asset.scada_devices",
+)
+
+
+def _affects_materialized_views(command: DatabaseCommand) -> bool:
+ statement = command.sql.lower()
+ return any(source in statement for source in _MATERIALIZED_VIEW_SOURCES)
+
+
+def execute_command(name: str, command: DatabaseCommand) -> ChangeSet:
+ """Apply a model mutation without the removed database undo/redo journal."""
+ write(name, command.sql)
+ if _affects_materialized_views(command) and not is_project_transaction_active(name):
+ refresh_materialized_views(name)
+ return ChangeSet.from_list(command.changes)
diff --git a/app/native/wndb/core/projects.py b/app/native/wndb/core/projects.py
new file mode 100644
index 0000000..cd2ea0c
--- /dev/null
+++ b/app/native/wndb/core/projects.py
@@ -0,0 +1,107 @@
+from psycopg import sql
+from psycopg.rows import dict_row
+from .connection import (
+ admin_connection,
+ close_project_pool,
+ get_project_pool,
+ is_project_pool_open,
+)
+
+_server_databases = ["template0", "template1", "postgres", "project"]
+
+
+def list_project() -> list[str]:
+ ps = []
+ with admin_connection() as conn:
+ with conn.cursor(row_factory=dict_row) as cur:
+ for p in cur.execute(
+ "select datname from pg_database where datname <> all(%s) order by datname",
+ (_server_databases,),
+ ):
+ ps.append(p["datname"])
+ return ps
+
+
+def have_project(name: str) -> bool:
+ with admin_connection() as conn:
+ with conn.cursor() as cur:
+ cur.execute("select 1 from pg_database where datname = %s", (name,))
+ return cur.fetchone() is not None
+
+
+def copy_project(source: str, new: str) -> None:
+ close_project_pool(source)
+
+ with admin_connection() as admin_conn:
+ with admin_conn.cursor() as cur:
+ cur.execute(
+ "update pg_database set datallowconn = false where datname = %s",
+ (source,),
+ )
+ try:
+ cur.execute(
+ "select pg_terminate_backend(pid) from pg_stat_activity where datname = %s and pid <> pg_backend_pid()",
+ (source,),
+ )
+ cur.execute(
+ sql.SQL("create database {} with template = {}").format(
+ sql.Identifier(new), sql.Identifier(source)
+ )
+ )
+ finally:
+ cur.execute(
+ "update pg_database set datallowconn = true where datname = %s",
+ (source,),
+ )
+
+
+def create_project(name: str) -> None:
+ return copy_project("project", name)
+
+
+def delete_project(name: str) -> None:
+ close_project_pool(name)
+ with admin_connection() as conn:
+ with conn.cursor() as cur:
+ cur.execute(
+ "select pg_terminate_backend(pid) from pg_stat_activity "
+ "where datname = %s and pid <> pg_backend_pid()",
+ (name,),
+ )
+ cur.execute(
+ sql.SQL("drop database {}").format(sql.Identifier(name))
+ )
+
+
+def clean_project(excluded: list[str] = []) -> None:
+ projects = list_project()
+ with admin_connection() as conn:
+ with conn.cursor(row_factory=dict_row) as cur:
+ row = cur.execute("select current_database()").fetchone()
+ if row != None:
+ current_db = row["current_database"]
+ if current_db in projects:
+ projects.remove(current_db)
+ for project in projects:
+ if project in _server_databases or project in excluded:
+ continue
+ cur.execute(
+ "select pg_terminate_backend(pid) from pg_stat_activity "
+ "where datname = %s and pid <> pg_backend_pid()",
+ (project,),
+ )
+ cur.execute(
+ sql.SQL("drop database {}").format(sql.Identifier(project))
+ )
+
+
+def open_project(name: str) -> None:
+ get_project_pool(name)
+
+
+def is_project_open(name: str) -> bool:
+ return is_project_pool_open(name)
+
+
+def close_project(name: str) -> None:
+ close_project_pool(name)
diff --git a/app/native/wndb/database.py b/app/native/wndb/database.py
deleted file mode 100644
index 5009418..0000000
--- a/app/native/wndb/database.py
+++ /dev/null
@@ -1,368 +0,0 @@
-from collections.abc import Mapping, Sequence
-from typing import Any
-from psycopg.rows import dict_row, Row
-from .connection import project_connection
-
-API_ADD = 'add'
-API_UPDATE = 'update'
-API_DELETE = 'delete'
-
-g_add_prefix = { 'operation': API_ADD }
-g_update_prefix = { 'operation': API_UPDATE }
-g_delete_prefix = { 'operation': API_DELETE }
-
-
-class ChangeSet:
- def __init__(self, ps: dict[str, Any] | None = None):
- self.operations : list[dict[str, Any]] = []
- if ps != None:
- self.append(ps)
-
- @staticmethod
- def from_list(ps: list[dict[str, Any]]):
- cs = ChangeSet()
- for _cs in ps:
- cs.append(_cs)
- return cs
-
- def add(self, ps: dict[str, Any]):
- self.operations.append(g_add_prefix | ps)
- return self
-
- def update(self, ps: dict[str, Any]):
- self.operations.append(g_update_prefix | ps)
- return self
-
- def delete(self, ps: dict[str, Any]):
- self.operations.append(g_delete_prefix | ps)
- return self
-
- def append(self, ps: dict[str, Any]):
- self.operations.append(ps)
- return self
-
- def merge(self, cs):
- if len(cs.operations) > 0:
- self.operations += cs.operations
- return self
-
- def dump(self):
- for op in self.operations:
- print(op)
-
- def compress(self):
- return self
-
-
-class DbChangeSet:
- def __init__(self, redo_sql: str, undo_sql: str, redo_cs: list[dict[str, Any]], undo_cs: list[dict[str, Any]]) -> None:
- self.redo_sql = redo_sql
- self.undo_sql = undo_sql
- self.redo_cs = redo_cs
- self.undo_cs = undo_cs
-
- @staticmethod
- def from_list(css):
- redo_sql_s : list[str] = []
- undo_sql_s : list[str] = []
- redo_cs_s : list[dict[str, Any]] = []
- undo_cs_s : list[dict[str, Any]] = []
-
- for r in css:
- redo_sql_s.append(r.redo_sql)
- undo_sql_s.append(r.undo_sql)
- redo_cs_s += r.redo_cs
- r.undo_cs.reverse() # reverse again...
- undo_cs_s += r.undo_cs
-
- redo_sql = '\n'.join(redo_sql_s)
- undo_sql_s.reverse()
- undo_sql = '\n'.join(undo_sql_s)
- undo_cs_s.reverse()
-
- return DbChangeSet(redo_sql, undo_sql, redo_cs_s, undo_cs_s)
-
-
-QueryParams = Sequence[Any] | Mapping[str, Any]
-
-
-def _execute(cur, sql: str, params: QueryParams | None = None):
- return cur.execute(sql, params) if params is not None else cur.execute(sql)
-
-
-def read(name: str, sql: str, params: QueryParams | None = None) -> Row:
- with project_connection(name) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- _execute(cur, sql, params)
- row = cur.fetchone()
- if row == None:
- raise Exception(sql)
- return row
-
-
-def read_all(
- name: str, sql: str, params: QueryParams | None = None
-) -> list[Row]:
- with project_connection(name) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- _execute(cur, sql, params)
- return cur.fetchall()
-
-
-def try_read(
- name: str, sql: str, params: QueryParams | None = None
-) -> Row | None:
- with project_connection(name) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- _execute(cur, sql, params)
- return cur.fetchone()
-
-
-def write(name: str, sql: str) -> None:
- with project_connection(name) as conn:
- with conn.cursor() as cur:
- cur.execute(sql)
-
-
-def get_current_operation(name: str) -> int:
- return int(read(name, 'select id from current_operation')['id'])
-
-
-def execute_command(name: str, command: DbChangeSet, undo_redo: bool = True) -> ChangeSet:
- write(name, command.redo_sql)
-
- if undo_redo:
- op_table = read(name, "select * from operation_table")['option']
- parent = get_current_operation(name)
- redo_sql = command.redo_sql.replace("'", "''")
- undo_sql = command.undo_sql.replace("'", "''")
- redo_cs_str = str(command.redo_cs).replace("'", "''")
- undo_cs_str = str(command.undo_cs).replace("'", "''")
- write(name, f"insert into {op_table} (id, redo, undo, parent, redo_cs, undo_cs) values (default, '{redo_sql}', '{undo_sql}', {parent}, '{redo_cs_str}', '{undo_cs_str}')")
-
- if op_table == 'operation':
- current = read(name, 'select max(id) as id from operation')['id']
- write(name, f"update current_operation set id = {current}")
-
- return ChangeSet.from_list(command.redo_cs)
-
-
-def execute_undo(name: str, discard: bool = False) -> ChangeSet:
- row = read(name, f'select * from operation where id = {get_current_operation(name)}')
-
- write(name, row['undo'])
-
- parent = row['parent'] if row['parent'] != None else 0
-
- # update foreign key
- write(name, f"update current_operation set id = {parent} where id = {row['id']}")
-
- if discard:
- # update foreign key
- write(name, f"update operation set redo_child = null where id = {parent}")
- # on delete cascade => child & snapshot
- write(name, f"delete from operation where id = {row['id']}")
- else:
- write(name, f"update operation set redo_child = {row['id']} where id = {parent}")
-
- e = eval(row['undo_cs']) if row['undo_cs'] not in [None, ''] else []
- return ChangeSet.from_list(e)
-
-
-def execute_redo(name: str) -> ChangeSet:
- row = read(name, f'select * from operation where id = {get_current_operation(name)}')
- if row['redo_child'] == None:
- return ChangeSet()
-
- row = read(name, f"select * from operation where id = {row['redo_child']}")
- write(name, row['redo'])
-
- parent = row['parent'] if row['parent'] != None else 0
- write(name, f"update current_operation set id = {row['id']} where id = {parent}")
-
- e = eval(row['redo_cs']) if row['redo_cs'] not in [None, ''] else []
- return ChangeSet.from_list(e)
-
-
-def list_snapshot(name: str) -> list[tuple[int, str]]:
- rows = read_all(name, f'select * from snapshot_operation order by id')
- result = []
- for row in rows:
- result.append((int(row['id']), str(row['tag'])))
- return result
-
-
-def have_snapshot(name: str, tag: str) -> bool:
- return try_read(name, f"select id from snapshot_operation where tag = '{tag}'") != None
-
-
-def have_snapshot_for_operation(name: str, operation: int) -> bool:
- return try_read(name, f"select id from snapshot_operation where id = {operation}") != None
-
-
-def have_snapshot_for_current_operation(name: str) -> bool:
- return have_snapshot_for_operation(name, get_current_operation(name))
-
-
-def take_snapshot_for_operation(name: str, operation: int, tag: str) -> None:
- if tag == None or tag == '':
- return None
- write(name, f"insert into snapshot_operation (id, tag) values ({operation}, '{tag}')")
-
-
-def take_snapshot_for_current_operation(name: str, tag: str) -> None:
- take_snapshot_for_operation(name, get_current_operation(name), tag)
-
-
-# deprecated ! use take_snapshot_for_current_operation instead
-def take_snapshot(name: str, tag: str) -> None:
- take_snapshot_for_current_operation(name, tag)
-
-
-def update_snapshot(name: str, operation: int, tag: str) -> None:
- if tag == None or tag == '':
- return None
- if have_snapshot_for_operation(name, operation):
- write(name, f"update snapshot_operation set tag = '{tag}' where id = {operation}")
- else:
- take_snapshot_for_operation(name, operation, tag)
-
-
-def update_snapshot_for_current_operation(name: str, tag: str) -> None:
- return update_snapshot(name, get_current_operation(name), tag)
-
-
-def delete_snapshot(name: str, tag: str) -> None:
- write(name, f"delete from snapshot_operation where tag = '{tag}'")
-
-
-def delete_snapshot_by_operation(name: str, operation: int) -> None:
- write(name, f"delete from snapshot_operation where id = {operation}")
-
-
-def get_operation_by_snapshot(name: str, tag: str) -> int | None:
- row = try_read(name, f"select id from snapshot_operation where tag = '{tag}'")
- return int(row['id']) if row != None else None
-
-
-def get_snapshot_by_operation(name: str, operation: int) -> str | None:
- row = try_read(name, f"select tag from snapshot_operation where id = {operation}")
- return str(row['tag']) if row != None else None
-
-
-def _get_parents(name: str, id: int) -> list[int]:
- ids = [id]
- while ids[-1] != 0:
- row = read(name, f'select parent from operation where id = {ids[-1]}')
- ids.append(int(row['parent']))
- return ids
-
-
-def pick_operation(name: str, operation: int, discard: bool) -> ChangeSet:
- target = operation
- curr = get_current_operation(name)
-
- curr_parents = _get_parents(name, curr)
- target_parents = _get_parents(name, target)
-
- change = ChangeSet()
-
- if target in curr_parents:
- for _ in range(curr_parents.index(target)):
- change.merge(execute_undo(name, discard))
-
- elif curr in target_parents:
- target_parents.reverse()
- curr_index = target_parents.index(curr)
- for i in range(curr_index, len(target_parents) - 1):
- write(name, f"update operation set redo_child = '{target_parents[i + 1]}' where id = '{target_parents[i]}'")
- change.merge(execute_redo(name))
-
- else:
- ancestor_index = -1
- while curr_parents[ancestor_index] == target_parents[ancestor_index]:
- ancestor_index -= 1
- ancestor = curr_parents[ancestor_index + 1]
-
- for _ in range(curr_parents.index(ancestor)):
- change.merge(execute_undo(name, discard))
-
- target_parents.reverse()
- curr_index = target_parents.index(ancestor)
- for i in range(curr_index, len(target_parents) - 1):
- write(name, f"update operation set redo_child = '{target_parents[i + 1]}' where id = '{target_parents[i]}'")
- change.merge(execute_redo(name))
-
- return change.compress()
-
-
-def pick_snapshot(name: str, tag: str, discard: bool) -> ChangeSet:
- if not have_snapshot(name, tag):
- return ChangeSet()
-
- target = int(read(name, f"select id from snapshot_operation where tag = '{tag}'")['id'])
- return pick_operation(name, target, discard)
-
-
-def _get_change_set(name: str, operation: int, undo: bool) -> ChangeSet:
- row = read(name, f'select * from operation where id = {operation}')
- field= 'undo_cs' if undo else 'redo_cs'
- return ChangeSet.from_list(eval(row[field]))
-
-
-def sync_with_server(name: str, operation: int) -> ChangeSet:
- fr = operation
- to = get_current_operation(name)
-
- fr_parents = _get_parents(name, fr)
- to_parents = _get_parents(name, to)
-
- change = ChangeSet()
-
- if fr in to_parents:
- index = to_parents.index(fr) - 1
- while index >= 0:
- change.merge(_get_change_set(name, to_parents[index], False)) #redo
- index -= 1
-
- elif to in fr_parents:
- index = 0
- while index <= fr_parents.index(to) - 1:
- change.merge(_get_change_set(name, fr_parents[index], True))
- index += 1
-
- else:
- ancestor_index = -1
- while fr_parents[ancestor_index] == to_parents[ancestor_index]:
- ancestor_index -= 1
-
- ancestor = fr_parents[ancestor_index + 1]
-
- index = 0
- while index <= fr_parents.index(ancestor) - 1:
- change.merge(_get_change_set(name, fr_parents[index], True))
- index += 1
-
- index = to_parents.index(ancestor) - 1
- while index >= 0:
- change.merge(_get_change_set(name, to_parents[index], False))
- index -= 1
-
- return change.compress()
-
-
-def get_restore_operation(name: str) -> int:
- return read(name, f'select * from restore_operation')['id']
-
-
-def set_restore_operation(name: str, operation: int) -> None:
- write(name, f'update restore_operation set id = {operation}')
-
-
-def set_restore_operation_to_current(name: str) -> None:
- return set_restore_operation(name, get_current_operation(name))
-
-
-def restore(name: str, discard: bool) -> ChangeSet:
- op = get_restore_operation(name)
- return pick_operation(name, op, discard)
diff --git a/app/native/wndb/extension_data.py b/app/native/wndb/extension_data.py
deleted file mode 100644
index cfce4ea..0000000
--- a/app/native/wndb/extension_data.py
+++ /dev/null
@@ -1,62 +0,0 @@
-from .database import *
-
-
-def get_all_extension_data_keys(name: str) -> list[str]:
- result: list[str] = []
- for row in read_all(name, 'select key from extension_data'):
- result.append(row['key'])
- return result
-
-
-def get_all_extension_data(name: str) -> dict[str, Any]:
- result: dict[str, Any] = {}
- for row in read_all(name, 'select key, value from extension_data'):
- result[row['key']] = row['value']
- return result
-
-
-def get_extension_data(name: str, key: str) -> str | None:
- if key == None or key == '':
- return None
- row = try_read(name, f"select value from extension_data where key = '{key}'")
- if row == None:
- return None
- return row['value']
-
-
-def _set_extension_data(name: str, cs: ChangeSet) -> DbChangeSet:
- op = cs.operations[0]
- key, new_val = op['key'], op['value']
-
- f_new_val = f"'{new_val}'" if new_val != None else 'null'
-
- old_val = get_extension_data(name, key)
- f_old_val = f"'{old_val}'" if old_val != None else 'null'
-
- redo_sql = f"delete from extension_data where key = '{key}';"
- if new_val != None:
- redo_sql += f"insert into extension_data (key, value) values ('{key}', {f_new_val});"
-
- undo_sql = f"delete from extension_data where key = '{key}';"
- if old_val != None:
- undo_sql += f"insert into extension_data (key, value) values ('{key}', {f_old_val});"
-
- redo_cs = g_update_prefix | { 'type': 'extension_data', 'key': key, 'value': new_val }
- undo_cs = g_update_prefix | { 'type': 'extension_data', 'key': key, 'value': old_val }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def set_extension_data(name: str, cs: ChangeSet) -> ChangeSet:
- if len(cs.operations) != 1:
- return ChangeSet()
-
- op = cs.operations[0]
- if 'key' not in op or 'value' not in op:
- return ChangeSet()
-
- key = op['key']
- if key == None or key == '':
- return ChangeSet()
-
- return execute_command(name, _set_extension_data(name, cs))
diff --git a/app/native/wndb/gis/__init__.py b/app/native/wndb/gis/__init__.py
new file mode 100644
index 0000000..a73bf8e
--- /dev/null
+++ b/app/native/wndb/gis/__init__.py
@@ -0,0 +1 @@
+"""GIS persistence and network geometry operations."""
diff --git a/app/native/wndb/gis/backdrop.py b/app/native/wndb/gis/backdrop.py
new file mode 100644
index 0000000..4834ab9
--- /dev/null
+++ b/app/native/wndb/gis/backdrop.py
@@ -0,0 +1,44 @@
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_update_prefix,
+ read,
+ sql_literal,
+)
+
+
+def get_backdrop_schema(name: str) -> dict[str, dict[str, Any]]:
+ return { 'content' : {'type': 'str' , 'optional': False , 'readonly': False} }
+
+
+def get_backdrop(name: str) -> dict[str, Any]:
+ e = read(name, "select content from gis.backdrops where id = true")
+ return { 'content': e['content'] }
+
+
+def _set_backdrop(name: str, cs: ChangeSet) -> DatabaseCommand:
+ statement = f"update gis.backdrops set content = {sql_literal(cs.operations[0]['content'])} where id = true;"
+
+ change = g_update_prefix | { 'type': 'backdrop', 'content': cs.operations[0]['content'] }
+
+ return DatabaseCommand(statement, [change])
+
+
+def set_backdrop(name: str, cs: ChangeSet) -> ChangeSet:
+ return execute_command(name, _set_backdrop(name, cs))
+
+
+def inp_in_backdrop(section: list[str]) -> str:
+ if section == []:
+ return str('')
+
+ content = '\n'.join(section)
+ return str(f"update gis.backdrops set content = {sql_literal(content)} where id = true;")
+
+
+def inp_out_backdrop(name: str) -> list[str]:
+ obj = str(get_backdrop(name)['content'])
+ return obj.split('\n')
diff --git a/app/native/wndb/s24_coordinates.py b/app/native/wndb/gis/coordinates.py
similarity index 64%
rename from app/native/wndb/s24_coordinates.py
rename to app/native/wndb/gis/coordinates.py
index 46f2bf5..1c53780 100644
--- a/app/native/wndb/s24_coordinates.py
+++ b/app/native/wndb/gis/coordinates.py
@@ -1,20 +1,23 @@
-from .database import *
-from .connection import project_connection
-from .s0_base import get_link_nodes
+from psycopg.rows import dict_row
+
+from ..core.connection import project_connection
+from ..core.database import read_all, sql_literal, try_read, write
+from ..core.connection import project_connection
+from ..model.elements import get_link_nodes
from psycopg.rows import dict_row
def sql_update_coord(node: str, x: float, y: float) -> str:
- coord = f"st_geomfromtext('point({x} {y})')"
- return str(f"update coordinates set coord = {coord} where node = '{node}';")
+ geom = f"st_setsrid(st_makepoint({sql_literal(x)}, {sql_literal(y)}), 900914)"
+ return f"update gis.node_geometries set geom = {geom} where node_id = {sql_literal(node)};"
def sql_insert_coord(node: str, x: float, y: float) -> str:
- coord = f"st_geomfromtext('point({x} {y})')"
- return str(f"insert into coordinates (node, coord) values ('{node}', {coord});")
+ geom = f"st_setsrid(st_makepoint({sql_literal(x)}, {sql_literal(y)}), 900914)"
+ return f"insert into gis.node_geometries (node_id, geom) values ({sql_literal(node)}, {geom});"
def sql_delete_coord(node: str) -> str:
- return str(f"delete from coordinates where node = '{node}';")
+ return f"delete from gis.node_geometries where node_id = {sql_literal(node)};"
def from_postgis_point(coord: str) -> dict[str, float]:
@@ -25,7 +28,7 @@ def from_postgis_point(coord: str) -> dict[str, float]:
def get_node_coord(name: str, node: str) -> dict[str, float]:
row = try_read(
name,
- "select st_astext(coord) as coord_geom from coordinates where node = %s",
+ "select st_astext(geom) as coord_geom from gis.node_geometries where node_id = %s",
(node,),
)
if row == None:
@@ -38,9 +41,9 @@ def get_node_coord(name: str, node: str) -> dict[str, float]:
# node_id:junction:x:y
def get_nodes_in_extent(name: str, x1: float, y1: float, x2: float, y2: float) -> list[str]:
nodes = []
- objs = read_all(name, 'select node, st_astext(coord) as coord_geom from coordinates')
+ objs = read_all(name, 'select node_id, st_astext(geom) as coord_geom from gis.node_geometries')
for obj in objs:
- node_id = obj['node']
+ node_id = obj['node_id']
coord = from_postgis_point(obj['coord_geom'])
x = coord['x']
y = coord['y']
@@ -57,9 +60,9 @@ def get_links_in_extent(name: str, x1: float, y1: float, x2: float, y2: float) -
all_link_ids = []
with project_connection(name) as conn:
with conn.cursor(row_factory=dict_row) as cur:
- cur.execute(f"select id from pipes")
+ cur.execute("select link_id from network.pipes")
for record in cur:
- all_link_ids.append(record['id'])
+ all_link_ids.append(record['link_id'])
links = []
for link_id in all_link_ids:
@@ -71,7 +74,7 @@ def get_links_in_extent(name: str, x1: float, y1: float, x2: float, y2: float) -
def node_has_coord(name: str, node: str) -> bool:
return try_read(
- name, "select node from coordinates where node = %s", (node,)
+ name, "select node_id from gis.node_geometries where node_id = %s", (node,)
) != None
@@ -85,15 +88,15 @@ def node_has_coord(name: str, node: str) -> bool:
def inp_in_coord(line: str) -> str:
tokens = line.split()
node = tokens[0]
- coord = f"st_geomfromtext('point({tokens[1]} {tokens[2]})')"
- return str(f"insert into coordinates (node, coord) values ('{node}', {coord});")
+ x, y = float(tokens[1]), float(tokens[2])
+ return sql_insert_coord(node, x, y)
def inp_out_coord(name: str) -> list[str]:
lines = []
- objs = read_all(name, 'select node, st_astext(coord) as coord_geom from coordinates')
+ objs = read_all(name, 'select node_id, st_astext(geom) as coord_geom from gis.node_geometries')
for obj in objs:
- node = obj['node']
+ node = obj['node_id']
coord = from_postgis_point(obj['coord_geom'])
x = coord['x']
y = coord['y']
diff --git a/app/native/wndb/s26_labels.py b/app/native/wndb/gis/labels.py
similarity index 55%
rename from app/native/wndb/s26_labels.py
rename to app/native/wndb/gis/labels.py
index 3bb0596..4eba2a4 100644
--- a/app/native/wndb/s26_labels.py
+++ b/app/native/wndb/gis/labels.py
@@ -1,4 +1,16 @@
-from .database import *
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_add_prefix,
+ g_delete_prefix,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+ try_read,
+)
def get_label_schema(name: str) -> dict[str, dict[str, Any]]:
@@ -12,7 +24,7 @@ def get_label(name: str, x: float, y: float) -> dict[str, Any]:
d = {}
d['x'] = x
d['y'] = y
- l = try_read(name, f'select * from labels where x = {x} and y = {y}')
+ l = try_read(name, "select label, node_id as node from gis.labels where geom = st_setsrid(st_makepoint(%s, %s), 900914)", (x, y))
if l == None:
d['label'] = None
d['node'] = None
@@ -30,21 +42,16 @@ class Label(object):
self.label = str(input['label'])
self.node = str(input['node']) if 'node' in input and input['node'] != None else None
- self.f_type = f"'{self.type}'"
- self.f_x = self.x
- self.f_y = self.y
- self.f_label = f"'{self.label}'"
- self.f_node = f"'{self.node}'" if self.node != None else 'null'
+ self.f_type = sql_literal(self.type)
+ self.f_x = sql_literal(self.x)
+ self.f_y = sql_literal(self.y)
+ self.f_label = sql_literal(self.label)
+ self.f_node = sql_literal(self.node)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'x': self.x, 'y': self.y, 'label': self.label, 'node': self.node }
- def as_xy_dict(self) -> dict[str, Any]:
- return { 'type': self.type, 'x': self.x, 'y': self.y }
-
-
-def _set_label(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Label(get_label(name, cs.operations[0]['x'], cs.operations[0]['y']))
+def _set_label(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_label(name, cs.operations[0]['x'], cs.operations[0]['y'])
new_dict = cs.operations[0]
@@ -54,45 +61,42 @@ def _set_label(name: str, cs: ChangeSet) -> DbChangeSet:
raw_new[key] = new_dict[key]
new = Label(raw_new)
- redo_sql = f"update labels set label = {new.f_label}, node = {new.f_node} where x = {new.f_x} and y = {new.f_y};"
- undo_sql = f"update labels set label = {old.f_label}, node = {old.f_node} where x = {old.f_x} and y = {old.f_y};"
+ statement = f"update gis.labels set label = {new.f_label}, node_id = {new.f_node} where geom = st_setsrid(st_makepoint({new.f_x}, {new.f_y}), 900914);"
- redo_cs = g_update_prefix | new.as_dict()
- undo_cs = g_update_prefix | old.as_dict()
+ change = g_update_prefix | new.as_dict()
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_label(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_label(name, cs))
-def _add_label(name: str, cs: ChangeSet) -> DbChangeSet:
+def _add_label(name: str, cs: ChangeSet) -> DatabaseCommand:
new = Label(cs.operations[0])
- redo_sql = f"insert into labels (x, y, label, node) values ({new.f_x}, {new.f_y}, {new.f_label}, {new.f_node});"
- undo_sql = f"delete from labels where x = {new.f_x} and y = {new.f_y};"
+ statement = f"insert into gis.labels (id, node_id, label, geom) values ((select coalesce(max(id) + 1, 1) from gis.labels), {new.f_node}, {new.f_label}, st_setsrid(st_makepoint({new.f_x}, {new.f_y}), 900914));"
- redo_cs = g_add_prefix | new.as_dict()
- undo_cs = g_delete_prefix | new.as_xy_dict()
+ change = g_add_prefix | new.as_dict()
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def add_label(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _add_label(name, cs))
-def _delete_label(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Label(get_label(name, cs.operations[0]['x'], cs.operations[0]['y']))
+def _delete_label(name: str, cs: ChangeSet) -> DatabaseCommand:
+ x = float(cs.operations[0]['x'])
+ y = float(cs.operations[0]['y'])
+ f_x = sql_literal(x)
+ f_y = sql_literal(y)
- redo_sql = f"delete from labels where x = {old.f_x} and y = {old.f_y};"
- undo_sql = f"insert into labels (x, y, label, node) values ({old.f_x}, {old.f_y}, {old.f_label}, {old.f_node});"
+ statement = f"delete from gis.labels where geom = st_setsrid(st_makepoint({f_x}, {f_y}), 900914);"
- redo_cs = g_delete_prefix | old.as_xy_dict()
- undo_cs = g_add_prefix | old.as_dict()
+ change = g_delete_prefix | {'type': 'label', 'x': x, 'y': y}
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def delete_label(name: str, cs: ChangeSet) -> ChangeSet:
@@ -110,14 +114,12 @@ def inp_in_label(line: str) -> str:
y = float(tokens[1])
label = str(tokens[2])
node = str(tokens[3]) if num >= 4 else None
- node = f"'{node}'" if node != None else 'null'
-
- return str(f"insert into labels (x, y, label, node) values ({x}, {y}, '{label}', {node});")
+ return str(f"insert into gis.labels (id, node_id, label, geom) values ((select coalesce(max(id) + 1, 1) from gis.labels), {sql_literal(node)}, {sql_literal(label)}, st_setsrid(st_makepoint({sql_literal(x)}, {sql_literal(y)}), 900914));")
def inp_out_label(name: str) -> list[str]:
lines = []
- objs = read_all(name, 'select * from labels')
+ objs = read_all(name, 'select st_x(geom) as x, st_y(geom) as y, label, node_id as node from gis.labels order by id')
for obj in objs:
x = obj['x']
y = obj['y']
@@ -130,7 +132,7 @@ def inp_out_label(name: str) -> list[str]:
def unset_label_by_node(name: str, node: str) -> ChangeSet:
cs = ChangeSet()
- rows = read_all(name, f"select x, y from labels where node = '{node}'")
+ rows = read_all(name, "select st_x(geom) as x, st_y(geom) as y from gis.labels where node_id = %s", (node,))
for row in rows:
cs.append(g_update_prefix | {'type': 'label', 'x': row['x'], 'y': row['y'], 'node': None})
diff --git a/app/native/wndb/s32_region_util.py b/app/native/wndb/gis/region_geometry.py
similarity index 56%
rename from app/native/wndb/s32_region_util.py
rename to app/native/wndb/gis/region_geometry.py
index f9c5e88..ac0e51c 100644
--- a/app/native/wndb/s32_region_util.py
+++ b/app/native/wndb/gis/region_geometry.py
@@ -1,12 +1,11 @@
import platform
-import os
import math
from typing import Any
import pyclipper
-from .s0_base import get_node_links, get_link_nodes, is_pipe
-from .s5_pipes import get_pipe
-from .database import read, try_read, read_all, write
-from .s24_coordinates import node_has_coord, get_node_coord
+from ..model.elements import get_node_links, get_link_nodes, is_pipe
+from ..model.pipes import get_pipe
+from ..core.database import read, try_read, read_all
+from .coordinates import node_has_coord, get_node_coord
def from_postgis_polygon(polygon: str) -> list[tuple[float, float]]:
@@ -33,17 +32,13 @@ def to_postgis_linestring(boundary: list[tuple[float, float]]) -> str:
def get_nodes_in_boundary(name: str, boundary: list[tuple[float, float]]) -> list[str]:
- api = 'get_nodes_in_boundary'
- write(name, f"delete from temp_region where id = '{api}'")
- write(name, f"insert into temp_region (id, boundary) values ('{api}', '{to_postgis_polygon(boundary)}')")
-
- nodes: list[str] = []
- for row in read_all(name, f"select c.node from coordinates as c, temp_region as r where ST_Intersects(c.coord, r.boundary) and r.id = '{api}'"):
- nodes.append(row['node'])
-
- write(name, f"delete from temp_region where id = '{api}'")
-
- return nodes
+ rows = read_all(
+ name,
+ "select node_id from gis.node_geometries "
+ "where st_intersects(geom, st_geomfromtext(%s, 900914)) order by node_id",
+ (to_postgis_polygon(boundary),),
+ )
+ return [str(row["node_id"]) for row in rows]
def _get_links_on_boundary(name: str, nodes: list[str]) -> list[str]:
@@ -64,54 +59,36 @@ def _get_links_on_boundary(name: str, nodes: list[str]) -> list[str]:
return links
-# if region is general or wda => get_nodes_in_boundary
-# if region is dma, sa or vd => get stored nodes in table
def get_nodes_in_region(name: str, region_id: str) -> list[str]:
- nodes: list[str] = []
-
- row = try_read(name, f"select r_type from region where id = '{region_id}'")
- if row == None:
- return nodes
-
- r_type = str(row['r_type'])
-
- if r_type == 'DMA' or r_type == 'SA' or r_type == 'VD':
- table = ''
- if r_type == 'DMA':
- table = 'region_dma'
- elif r_type == 'SA':
- table = 'region_sa'
- elif r_type == 'VD':
- table = 'region_vd'
-
- if table != '':
- row = try_read(name, f"select nodes from {table} where id = '{region_id}'")
- if row != None:
- nodes = eval(str(row['nodes']))
-
- if nodes == []:
- for row in read_all(name, f"select c.node from coordinates as c, region as r where ST_Intersects(c.coord, r.boundary) and r.id = '{region_id}'"):
- nodes.append(row['node'])
-
- return nodes
+ stored = read_all(
+ name,
+ "select node_id from gis.region_nodes where region_id = %s order by node_id",
+ (region_id,),
+ )
+ if stored:
+ return [str(row["node_id"]) for row in stored]
+ rows = read_all(
+ name,
+ "select n.node_id from gis.node_geometries n join gis.regions r "
+ "on st_intersects(n.geom, r.boundary) where r.id = %s order by n.node_id",
+ (region_id,),
+ )
+ return [str(row["node_id"]) for row in rows]
def get_links_on_region_boundary(name: str, region_id: str) -> list[str]:
nodes = get_nodes_in_region(name, region_id)
- print(nodes)
return _get_links_on_boundary(name, nodes)
def calculate_convex_hull(name: str, nodes: list[str]) -> list[tuple[float, float]]:
- write(name, f'delete from temp_node')
- for node in nodes:
- write(name, f"insert into temp_node values ('{node}')")
-
- # TODO: check none
- polygon = read(name, f'select st_astext(st_convexhull(st_collect(array(select coord from coordinates where node in (select * from temp_node))))) as boundary' )['boundary']
- write(name, f'delete from temp_node')
-
- return from_postgis_polygon(polygon)
+ row = read(
+ name,
+ "select st_astext(st_convexhull(st_collect(geom))) as boundary "
+ "from gis.node_geometries where node_id = any(%s)",
+ (nodes,),
+ )
+ return from_postgis_polygon(str(row["boundary"]))
def _verify_platform():
@@ -292,115 +269,7 @@ def calculate_boundary(name: str, nodes: list[str], accurate = False) -> list[tu
topology = Topology(name, nodes)
t_nodes = topology.nodes()
t_links = topology.links()
-
- vertices, path, boundary = _calculate_boundary(topology.max_x_node(), t_nodes, t_links)
-
- if not accurate:
- return boundary
-
- api = 'calculate_boundary'
- write(name, f"delete from temp_region where id = '{api}'")
- # use linestring instead of polygon to reduce strict limitation
- # TODO: linestring can not work well
- write(name, f"insert into temp_region (id, boundary) values ('{api}', '{to_postgis_polygon(boundary)}')")
-
- write(name, f'delete from temp_node')
- for node in nodes:
- write(name, f"insert into temp_node values ('{node}')")
-
- for row in read_all(name, f"select n.node from coordinates as c, temp_node as n, temp_region as r where c.node = n.node and ST_Intersects(c.coord, r.boundary) and r.id = '{api}'"):
- node = row['node']
- write(name, f"delete from temp_node where node = '{node}'")
-
- outside_nodes: list[str] = []
- for row in read_all(name, "select node from temp_node"):
- outside_nodes.append(row['node'])
-
- # no outside nodes, return
- if len(outside_nodes) == 0:
- write(name, f'delete from temp_node')
- write(name, f"delete from temp_region where id = '{api}'")
- return boundary
-
- new_nodes: dict[str, Any] = {}
- new_links: dict[str, Any] = {}
-
- boundary_links: dict[str, list[str]] = {}
- write(name, "delete from temp_link_2")
- for node in outside_nodes:
- for link in t_nodes[node]['links']:
- node1 = t_links[link]['node1']
- node2 = t_links[link]['node2']
- if node1 in outside_nodes and node2 not in outside_nodes and node2 not in vertices and link:
- if link not in boundary:
- boundary_links[link] = []
- line = f"LINESTRING({t_nodes[node1]['x']} {t_nodes[node1]['y']}, {t_nodes[node2]['x']} {t_nodes[node2]['y']})"
- write(name, f"insert into temp_link_2 values ('{link}', '{line}')")
- if node2 in outside_nodes and node1 not in outside_nodes and node1 not in vertices:
- if link not in boundary:
- boundary_links[link] = []
- line = f"LINESTRING({t_nodes[node1]['x']} {t_nodes[node1]['y']}, {t_nodes[node2]['x']} {t_nodes[node2]['y']})"
- write(name, f"insert into temp_link_2 values ('{link}', '{line}')")
- if node1 in outside_nodes and node2 in outside_nodes:
- x1, x2 = t_nodes[node1]['x'], t_nodes[node2]['x']
- y1, y2 = t_nodes[node1]['y'], t_nodes[node2]['y']
- if node1 not in new_nodes:
- new_nodes[node1] = { 'x': x1, 'y': y1, 'links': [] }
- if node2 not in new_nodes:
- new_nodes[node2] = { 'x': x2, 'y': y2, 'links': [] }
- if link not in new_links:
- new_links[link] = t_links[link]
-
- # no boundary links, return
- if len(boundary_links) == 0:
- write(name, "delete from temp_link_2")
- write(name, f'delete from temp_node')
- write(name, f"delete from temp_region where id = '{api}'")
- return boundary
-
- write(name, "delete from temp_link_1")
- for link, _ in path.items():
- node1 = t_links[link]['node1']
- node2 = t_links[link]['node2']
- line = f"LINESTRING({t_nodes[node1]['x']} {t_nodes[node1]['y']}, {t_nodes[node2]['x']} {t_nodes[node2]['y']})"
- write(name, f"insert into temp_link_1 (link, geom) values ('{link}', '{line}')")
-
- has_intersection = False
- for row in read_all(name, f"select l1.link as l, l2.link as r, st_astext(st_intersection(l1.geom, l2.geom)) as p from temp_link_1 as l1, temp_link_2 as l2 where st_intersects(l1.geom, l2.geom)"):
- has_intersection = True
-
- link1, link2, pt = str(row['l']), str(row['r']), str(row['p'])
- pts = pt.lower().removeprefix('point(').removesuffix(')').split(' ')
- xy = (float(pts[0]), float(pts[1]))
-
- new_node = f'NODE_[{link1}]_[{link2}]'
- new_nodes[new_node] = { 'x': xy[0], 'y': xy[1], 'links': [] }
-
- path[link1].append(new_node)
- boundary_links[link2].append(new_node)
-
- # no intersection, return
- if not has_intersection:
- write(name, "delete from temp_link_1")
- write(name, "delete from temp_link_2")
- write(name, 'delete from temp_node')
- write(name, f"delete from temp_region where id = '{api}'")
- return boundary
-
- new_nodes, new_links = _collect_new_links(path, t_nodes, t_links, new_nodes, new_links)
- new_nodes, new_links = _collect_new_links(boundary_links, t_nodes, t_links, new_nodes, new_links)
-
- for link, values in new_links.items():
- new_nodes[values['node1']]['links'].append(link)
- new_nodes[values['node2']]['links'].append(link)
-
- _, _, boundary = _calculate_boundary(topology.max_x_node(), new_nodes, new_links)
-
- write(name, "delete from temp_link_1")
- write(name, "delete from temp_link_2")
- write(name, 'delete from temp_node')
- write(name, f"delete from temp_region where id = '{api}'")
-
+ _, _, boundary = _calculate_boundary(topology.max_x_node(), t_nodes, t_links)
return boundary
@@ -432,7 +301,7 @@ def inflate_boundary(name: str, boundary: list[tuple[float, float]], delta: floa
def inflate_region(name: str, region_id: str, delta: float = 0.5) -> list[tuple[float, float]]:
- r = try_read(name, f"select id, st_astext(boundary) as boundary_geom from region where id = '{region_id}'")
+ r = try_read(name, "select id, st_astext(boundary) as boundary_geom from gis.regions where id = %s", (region_id,))
if r == None:
return []
boundary = from_postgis_polygon(str(r['boundary_geom']))
diff --git a/app/native/wndb/gis/regions.py b/app/native/wndb/gis/regions.py
new file mode 100644
index 0000000..0dbe18b
--- /dev/null
+++ b/app/native/wndb/gis/regions.py
@@ -0,0 +1,127 @@
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_add_prefix,
+ g_delete_prefix,
+ g_update_prefix,
+ sql_literal,
+ try_read,
+)
+from .region_geometry import from_postgis_polygon, to_postgis_polygon
+
+
+def get_region_schema(name: str) -> dict[str, dict[str, Any]]:
+ return {
+ "id": {"type": "str", "optional": False, "readonly": True},
+ "region_type": {"type": "str", "optional": False, "readonly": False},
+ "boundary": {"type": "tuple_list", "optional": False, "readonly": False},
+ }
+
+
+def get_region(name: str, id: str) -> dict[str, Any]:
+ row = try_read(
+ name,
+ "select id, region_type, st_astext(boundary) as boundary_geom "
+ "from gis.regions where id = %s",
+ (id,),
+ )
+ if row is None:
+ return {}
+ return {
+ "id": str(row["id"]),
+ "region_type": str(row["region_type"]),
+ "boundary": from_postgis_polygon(str(row["boundary_geom"])),
+ }
+
+
+def _valid_boundary(boundary: list[Any]) -> bool:
+ return len(boundary) >= 4 and boundary[0] == boundary[-1]
+
+
+def _set_region(name: str, cs: ChangeSet) -> DatabaseCommand:
+ region_id = cs.operations[0]["id"]
+ old = get_region(name, region_id)
+ new = old | {
+ key: cs.operations[0][key]
+ for key in ("region_type", "boundary")
+ if key in cs.operations[0]
+ }
+ statement = (
+ "update gis.regions set "
+ f"region_type = {sql_literal(new['region_type'])}, "
+ f"boundary = st_geomfromtext({sql_literal(to_postgis_polygon(new['boundary']))}, 900914) "
+ f"where id = {sql_literal(region_id)};"
+ )
+ return DatabaseCommand(
+ statement,
+ [g_update_prefix | {"type": "region"} | new],
+ )
+
+
+def set_region(name: str, cs: ChangeSet) -> ChangeSet:
+ operation = cs.operations[0]
+ if "id" not in operation or get_region(name, operation["id"]) == {}:
+ return ChangeSet()
+ if "boundary" in operation and not _valid_boundary(operation["boundary"]):
+ return ChangeSet()
+ return execute_command(name, _set_region(name, cs))
+
+
+def _add_region(name: str, cs: ChangeSet) -> DatabaseCommand:
+ operation = cs.operations[0]
+ region_id = operation["id"]
+ region_type = str(operation.get("region_type", "none"))
+ boundary = operation["boundary"]
+ statement = (
+ "insert into gis.regions (id, region_type, boundary) values "
+ f"({sql_literal(region_id)}, {sql_literal(region_type)}, "
+ f"st_geomfromtext({sql_literal(to_postgis_polygon(boundary))}, 900914));"
+ )
+ value = {"type": "region", "id": region_id, "region_type": region_type, "boundary": boundary}
+ return DatabaseCommand(
+ statement,
+ [g_add_prefix | value],
+ )
+
+
+def add_region(name: str, cs: ChangeSet) -> ChangeSet:
+ operation = cs.operations[0]
+ if "id" not in operation or "boundary" not in operation:
+ return ChangeSet()
+ if not _valid_boundary(operation["boundary"]):
+ return ChangeSet()
+ if get_region(name, operation["id"]) != {}:
+ return ChangeSet()
+ return execute_command(name, _add_region(name, cs))
+
+
+def _delete_region(name: str, cs: ChangeSet) -> DatabaseCommand:
+ region_id = cs.operations[0]["id"]
+ statement = f"delete from gis.regions where id = {sql_literal(region_id)};"
+ return DatabaseCommand(
+ statement,
+ [g_delete_prefix | {"type": "region", "id": region_id}],
+ )
+
+
+def delete_region(name: str, cs: ChangeSet) -> ChangeSet:
+ if "id" not in cs.operations[0] or get_region(name, cs.operations[0]["id"]) == {}:
+ return ChangeSet()
+ return execute_command(name, _delete_region(name, cs))
+
+
+def inp_in_region(line: str) -> str:
+ tokens = line.split()
+ return f"insert into gis.regions (id, region_type) values ({sql_literal(tokens[0])}, {sql_literal(tokens[1])});"
+
+
+def inp_in_bound(line: str) -> str:
+ return line.split()[0]
+
+
+def inp_in_regionnodes(line: str) -> str:
+ tokens = line.split()
+ return f"insert into gis.region_nodes (region_id, node_id) values ({sql_literal(tokens[0])}, {sql_literal(tokens[1])});"
diff --git a/app/native/wndb/s25_vertices.py b/app/native/wndb/gis/vertices.py
similarity index 53%
rename from app/native/wndb/s25_vertices.py
rename to app/native/wndb/gis/vertices.py
index 0bd7647..50e1e71 100644
--- a/app/native/wndb/s25_vertices.py
+++ b/app/native/wndb/gis/vertices.py
@@ -1,4 +1,16 @@
-from .database import *
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_add_prefix,
+ g_delete_prefix,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+ try_read,
+)
def get_vertex_schema(name: str) -> dict[str, dict[str, Any]]:
@@ -9,59 +21,48 @@ def get_vertex_schema(name: str) -> dict[str, dict[str, Any]]:
def get_vertex(name: str, link: str) -> dict[str, Any]:
- cus = read_all(name, f"select * from vertices where link = '{link}' order by _order")
+ cus = read_all(name, "select st_x(geom) as x, st_y(geom) as y from gis.link_vertices where link_id = %s order by sequence_no", (link,))
cs = []
for r in cus:
cs.append({ 'x': float(r['x']), 'y': float(r['y']) })
return { 'link': link, 'coords': cs }
-def _set_vertex(name: str, cs: ChangeSet) -> DbChangeSet:
+def _set_vertex(name: str, cs: ChangeSet) -> DatabaseCommand:
link = cs.operations[0]['link']
- old = get_vertex(name, link)
new = { 'link': link, 'coords': [] }
- f_link = f"'{link}'"
+ f_link = sql_literal(link)
- # TODO: transaction ?
- redo_sql = f"delete from vertices where link = {f_link};"
- for xy in cs.operations[0]['coords']:
+ statement = f"delete from gis.link_vertices where link_id = {f_link};"
+ for sequence_no, xy in enumerate(cs.operations[0]['coords']):
x, y = float(xy['x']), float(xy['y'])
- f_x, f_y = x, y
- redo_sql += f"\ninsert into vertices (link, x, y) values ({f_link}, {f_x}, {f_y});"
+ f_x, f_y = sql_literal(x), sql_literal(y)
+ statement += f"\ninsert into gis.link_vertices (link_id, sequence_no, geom) values ({f_link}, {sequence_no}, st_setsrid(st_makepoint({f_x}, {f_y}), 900914));"
new['coords'].append({ 'x': x, 'y': y })
- undo_sql = f"delete from vertices where link = {f_link};"
- for xy in old['coords']:
- f_x, f_y = xy['x'], xy['y']
- undo_sql += f"\ninsert into vertices (link, x, y) values ({f_link}, {f_x}, {f_y});"
+ change = { 'type': 'vertex' } | new
- redo_cs = { 'type': 'vertex' } | new
- undo_cs = { 'type': 'vertex' } | old
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_vertex(name: str, cs: ChangeSet) -> ChangeSet:
result = _set_vertex(name, cs)
- result.redo_cs[0] |= g_update_prefix
- result.undo_cs[0] |= g_update_prefix
+ result.changes[0] |= g_update_prefix
return execute_command(name, result)
-def _add_vertex(name: str, cs: ChangeSet) -> DbChangeSet:
+def _add_vertex(name: str, cs: ChangeSet) -> DatabaseCommand:
result = _set_vertex(name, cs)
- result.redo_cs[0] |= g_add_prefix
- result.undo_cs[0] |= g_delete_prefix
+ result.changes[0] |= g_add_prefix
return result
-def _delete_vertex(name: str, cs: ChangeSet) -> DbChangeSet:
+def _delete_vertex(name: str, cs: ChangeSet) -> DatabaseCommand:
cs.operations[0]['coords'] = []
result = _set_vertex(name, cs)
- result.redo_cs[0] |= g_delete_prefix
- result.undo_cs[0] |= g_add_prefix
+ result.changes[0] |= g_delete_prefix
return result
@@ -77,14 +78,14 @@ def delete_vertex(name: str, cs: ChangeSet) -> ChangeSet:
def get_all_vertex_links(name: str) -> list[str]:
result : list[str] = []
- rows = read_all(name, 'select link from vertices order by link')
+ rows = read_all(name, 'select distinct link_id from gis.link_vertices order by link_id')
for row in rows:
- result.append(str(row['link']))
+ result.append(str(row['link_id']))
return result
def get_all_vertices(name: str) -> list[dict[str, Any]]:
- return read_all(name, 'select * from vertices order by link')
+ return read_all(name, 'select link_id, sequence_no, st_x(geom) as x, st_y(geom) as y from gis.link_vertices order by link_id, sequence_no')
#--------------------------------------------------------------
@@ -99,14 +100,15 @@ def inp_in_vertex(line: str) -> str:
link = tokens[0]
x = float(tokens[1])
y = float(tokens[2])
- return str(f"insert into vertices (link, x, y) values ('{link}', {x}, {y});")
+ link_sql = sql_literal(link)
+ return f"insert into gis.link_vertices (link_id, sequence_no, geom) values ({link_sql}, (select coalesce(max(sequence_no) + 1, 0) from gis.link_vertices where link_id = {link_sql}), st_setsrid(st_makepoint({sql_literal(x)}, {sql_literal(y)}), 900914));"
def inp_out_vertex(name: str) -> list[str]:
lines = []
- objs = read_all(name, f"select * from vertices order by _order")
+ objs = read_all(name, "select link_id, st_x(geom) as x, st_y(geom) as y from gis.link_vertices order by link_id, sequence_no")
for obj in objs:
- link = obj['link']
+ link = obj['link_id']
x = obj['x']
y = obj['y']
lines.append(f"{link} {x} {y}")
@@ -114,7 +116,7 @@ def inp_out_vertex(name: str) -> list[str]:
def delete_vertex_by_link(name: str, link: str) -> ChangeSet:
- row = try_read(name, f"select * from vertices where link = '{link}'")
+ row = try_read(name, "select * from gis.link_vertices where link_id = %s", (link,))
if row == None:
return ChangeSet()
return ChangeSet(g_delete_prefix | {'type': 'vertex', 'link' : link})
diff --git a/app/native/wndb/inp/__init__.py b/app/native/wndb/inp/__init__.py
new file mode 100644
index 0000000..a6b4fd5
--- /dev/null
+++ b/app/native/wndb/inp/__init__.py
@@ -0,0 +1 @@
+"""EPANET INP import, export, and section mapping."""
diff --git a/app/native/wndb/inp_out.py b/app/native/wndb/inp/exporter.py
similarity index 79%
rename from app/native/wndb/inp_out.py
rename to app/native/wndb/inp/exporter.py
index aa7de7e..4638f88 100644
--- a/app/native/wndb/inp_out.py
+++ b/app/native/wndb/inp/exporter.py
@@ -1,35 +1,67 @@
import os
-from .project import *
-from .database import ChangeSet
-from .sections import *
-from .s1_title import inp_out_title
-from .s2_junctions import inp_out_junction
-from .s3_reservoirs import inp_out_reservoir
-from .s4_tanks import inp_out_tank
-from .s5_pipes import inp_out_pipe
-from .s6_pumps import inp_out_pump
-from .s7_valves import inp_out_valve
-from .s8_tags import inp_out_tag
-from .s9_demands import inp_out_demand
-from .s10_status import inp_out_status
-from .s11_patterns import inp_out_pattern, inp_out_pattern_v3
-from .s12_curves import inp_out_curve, inp_out_curve_v3
-from .s13_controls import inp_out_control
-from .s14_rules import inp_out_rule
-from .s15_energy import inp_out_energy
-from .s16_emitters import inp_out_emitter
-from .s17_quality import inp_out_quality
-from .s18_sources import inp_out_source
-from .s19_reactions import inp_out_reaction
-from .s20_mixing import inp_out_mixing
-from .s21_times import inp_out_time
-from .s22_report import inp_out_report
-from .s23_options import inp_out_option
-from .s23_options_v3 import inp_out_option_v3
-from .s24_coordinates import inp_out_coord
-from .s25_vertices import inp_out_vertex
-from .s26_labels import inp_out_label
-from .s27_backdrop import inp_out_backdrop
+
+from ..core.projects import close_project, have_project, is_project_open, open_project
+from ..core.database import ChangeSet
+from .sections import (
+ BACKDROP,
+ CONTROLS,
+ COORDINATES,
+ CURVES,
+ DEMANDS,
+ EMITTERS,
+ END,
+ ENERGY,
+ JUNCTIONS,
+ LABELS,
+ MIXING,
+ OPTIONS,
+ PATTERNS,
+ PIPES,
+ PUMPS,
+ QUALITY,
+ REACTIONS,
+ REPORT,
+ RESERVOIRS,
+ RULES,
+ SOURCES,
+ STATUS,
+ TAGS,
+ TANKS,
+ TIMES,
+ TITLE,
+ VALVES,
+ VERTICES,
+ section_name,
+ section_names_for_epanetv2,
+)
+from ..model.title import inp_out_title
+from ..model.junctions import inp_out_junction
+from ..model.reservoirs import inp_out_reservoir
+from ..model.tanks import inp_out_tank
+from ..model.pipes import inp_out_pipe
+from ..model.pumps import inp_out_pump
+from ..model.valves import inp_out_valve
+from ..model.tags import inp_out_tag
+from ..model.demands import inp_out_demand
+from ..model.status import inp_out_status
+from ..model.patterns import inp_out_pattern, inp_out_pattern_v3
+from ..model.curves import inp_out_curve, inp_out_curve_v3
+from ..model.controls import inp_out_control
+from ..model.rules import inp_out_rule
+from ..model.energy import inp_out_energy
+from ..model.emitters import inp_out_emitter
+from ..model.quality import inp_out_quality
+from ..model.sources import inp_out_source
+from ..model.reactions import inp_out_reaction
+from ..model.mixing import inp_out_mixing
+from ..model.times import inp_out_time
+from ..model.reports import inp_out_report
+from ..model.options_legacy import inp_out_option
+from ..model.options_v3 import inp_out_option_v3
+from ..gis.coordinates import inp_out_coord
+from ..gis.vertices import inp_out_vertex
+from ..gis.labels import inp_out_label
+from ..gis.backdrop import inp_out_backdrop
#from .s28_end import *
diff --git a/app/native/wndb/inp_in.py b/app/native/wndb/inp/importer.py
similarity index 70%
rename from app/native/wndb/inp_in.py
rename to app/native/wndb/inp/importer.py
index a283f93..03a6d73 100644
--- a/app/native/wndb/inp_in.py
+++ b/app/native/wndb/inp/importer.py
@@ -1,42 +1,84 @@
import datetime
import os
-from .project import *
-from .database import ChangeSet, write
-from .sections import *
-from .s0_base import get_region_type
-from .s1_title import inp_in_title
-from .s2_junctions import inp_in_junction
-from .s3_reservoirs import inp_in_reservoir
-from .s4_tanks import inp_in_tank
-from .s5_pipes import inp_in_pipe
-from .s6_pumps import inp_in_pump
-from .s7_valves import inp_in_valve
-from .s8_tags import inp_in_tag
-from .s9_demands import inp_in_demand
-from .s10_status import inp_in_status
-from .s11_patterns import pattern_v3_types, inp_in_pattern
-from .s12_curves import curve_types, inp_in_curve
-from .s13_controls import inp_in_control
-from .s14_rules import inp_in_rule
-from .s15_energy import inp_in_energy
-from .s16_emitters import inp_in_emitter
-from .s17_quality import inp_in_quality
-from .s18_sources import inp_in_source
-from .s19_reactions import inp_in_reaction
-from .s20_mixing import inp_in_mixing
-from .s21_times import inp_in_time
-from .s22_report import inp_in_report
-from .s23_options import inp_in_option
-from .s23_options_v3 import inp_in_option_v3
-from .s24_coordinates import inp_in_coord
-from .s25_vertices import inp_in_vertex
-from .s26_labels import inp_in_label
-from .s27_backdrop import inp_in_backdrop
-from .s32_region import inp_in_region, inp_in_bound, inp_in_regionnodes
-from .s32_region_util import from_postgis_polygon, to_postgis_polygon
+
+from psycopg import sql
+
+from ..core.projects import (
+ close_project,
+ create_project,
+ delete_project,
+ have_project,
+ is_project_open,
+ open_project,
+)
+from ..core.connection import project_transaction
+from ..core.database import ChangeSet, refresh_materialized_views, sql_literal, write
+from .sections import (
+ BACKDROP,
+ BOUND,
+ CONTROLS,
+ COORDINATES,
+ CURVES,
+ DEMANDS,
+ EMITTERS,
+ ENERGY,
+ JUNCTIONS,
+ LABELS,
+ MIXING,
+ OPTIONS,
+ PATTERNS,
+ PIPES,
+ PUMPS,
+ QUALITY,
+ REACTIONS,
+ REGION,
+ REGION_NODES,
+ REPORT,
+ RESERVOIRS,
+ RULES,
+ SOURCES,
+ STATUS,
+ TAGS,
+ TANKS,
+ TIMES,
+ TITLE,
+ VALVES,
+ VERTICES,
+ section_name,
+)
+from ..model.title import inp_in_title
+from ..model.junctions import inp_in_junction
+from ..model.reservoirs import inp_in_reservoir
+from ..model.tanks import inp_in_tank
+from ..model.pipes import inp_in_pipe
+from ..model.pumps import inp_in_pump
+from ..model.valves import inp_in_valve
+from ..model.tags import inp_in_tag
+from ..model.demands import inp_in_demand
+from ..model.status import inp_in_status
+from ..model.patterns import pattern_v3_types, inp_in_pattern
+from ..model.curves import curve_types, inp_in_curve
+from ..model.controls import inp_in_control
+from ..model.rules import inp_in_rule
+from ..model.energy import inp_in_energy
+from ..model.emitters import inp_in_emitter
+from ..model.quality import inp_in_quality
+from ..model.sources import inp_in_source
+from ..model.reactions import inp_in_reaction
+from ..model.mixing import inp_in_mixing
+from ..model.times import inp_in_time
+from ..model.reports import inp_in_report
+from ..model.options_legacy import inp_in_option
+from ..model.options_v3 import inp_in_option_v3
+from ..gis.coordinates import inp_in_coord
+from ..gis.vertices import inp_in_vertex
+from ..gis.labels import inp_in_label
+from ..gis.backdrop import inp_in_backdrop
+from ..gis.regions import inp_in_region, inp_in_bound, inp_in_regionnodes
+from ..gis.region_geometry import to_postgis_polygon
# DingZQ, 2024-12-28, export inp
-from .inp_out import export_inp
+from .exporter import export_inp
_S = "S"
_L = "L"
@@ -205,8 +247,6 @@ def parse_file(project: str, inp: str, version: str = "3") -> None:
current_bound = []
current_bound.clear()
region_list = {}
- current_region_nodes = []
- current_region_nodes.clear()
sql_batch = SQLBatch(project)
_print_time("Second scan...")
@@ -254,7 +294,7 @@ def parse_file(project: str, inp: str, version: str = "3") -> None:
if tokens[1].upper() in pattern_v3_types: # v3
sql_batch.add(
- f"insert into _pattern (id) values ('{tokens[0]}');"
+ f"insert into network.patterns (id) values ({sql_literal(tokens[0])});"
)
current_pattern = tokens[0]
if tokens[1].upper() == "VARIABLE":
@@ -263,7 +303,7 @@ def parse_file(project: str, inp: str, version: str = "3") -> None:
if current_pattern != tokens[0]:
sql_batch.add(
- f"insert into _pattern (id) values ('{tokens[0]}');"
+ f"insert into network.patterns (id) values ({sql_literal(tokens[0])});"
)
current_pattern = tokens[0]
@@ -272,7 +312,7 @@ def parse_file(project: str, inp: str, version: str = "3") -> None:
if tokens[1].upper() in curve_types: # v3
sql_batch.add(
- f"insert into _curve (id, type) values ('{tokens[0]}', '{tokens[1].upper()}');"
+ f"insert into network.curves (id, curve_type) values ({sql_literal(tokens[0])}, {sql_literal(tokens[1].upper())});"
)
current_curve = tokens[0]
continue
@@ -282,51 +322,39 @@ def parse_file(project: str, inp: str, version: str = "3") -> None:
if curve_type_desc_line != None:
type = curve_type_desc_line.split(":")[0].strip()
sql_batch.add(
- f"insert into _curve (id, type) values ('{tokens[0]}', '{type}');"
+ f"insert into network.curves (id, curve_type) values ({sql_literal(tokens[0])}, {sql_literal(type)});"
)
current_curve = tokens[0]
curve_type_desc_line = None
elif s == REGION:
tokens = line.split()
region_list[tokens[0]] = tokens[1]
+ continue
elif s == BOUND:
tokens = line.split()
if tokens[0] != current_region and len(current_bound) > 0:
- # insert the previous region after get all the vertex of the attatched geometry
current_bound.append(current_bound[0])
current_geometry = to_postgis_polygon(current_bound)
- region_type = map_regiontype[region_list[tokens[0]]]
+ region_type = map_regiontype.get(
+ region_list[current_region],
+ region_list[current_region],
+ )
sql_batch.add(
- f"insert into region(id, boundary,r_type) values ('{current_region}', '{current_geometry}','{region_type}');"
+ "insert into gis.regions(id, region_type, boundary) "
+ f"values ({sql_literal(current_region)}, {sql_literal(region_type)}, "
+ f"st_geomfromtext({sql_literal(current_geometry)}, 900914));"
)
- # start the new region
current_bound.clear()
vertex_point = (float(tokens[1]), float(tokens[2]))
current_bound.append(vertex_point)
current_region = tokens[0]
- elif s == REGION_NODES:
- tokens = line.split()
- if (
- tokens[0] != current_region
- and len(current_region_nodes) > 0
- ):
- # insert the previous region after get all the vertex of the attatched geometry
- sql_batch.add(
- get_insert_into_region_sql(
- current_region, current_region_nodes
- )
- )
- # start the new region
- current_region_nodes.clear()
- current_region_nodes.append(tokens[1])
- current_region = tokens[0]
if s == JUNCTIONS:
sql_batch.add(handler(line, demand_outside))
elif s == PATTERNS:
sql_batch.add(
handler(line, current_pattern not in variable_patterns)
)
- elif s == BOUND or s == REGION_NODES:
+ elif s == BOUND:
continue
else:
sql_batch.add(handler(line))
@@ -342,42 +370,21 @@ def parse_file(project: str, inp: str, version: str = "3") -> None:
if len(current_bound) > 0:
current_bound.append(current_bound[0])
current_geometry = to_postgis_polygon(current_bound)
- region_type = map_regiontype[region_list[current_region]]
- sql_batch.add(
- f"insert into region(id, boundary,r_type) values ('{current_region}', '{current_geometry}','{region_type}');"
+ region_type = map_regiontype.get(
+ region_list[current_region],
+ region_list[current_region],
)
- # reset the current region to none for the [REGION_NODES] session reading
- # current_region=None
- # need to insert the last region_nodes into database
- if len(current_region_nodes) > 0:
sql_batch.add(
- get_insert_into_region_sql(current_region, current_region_nodes)
+ "insert into gis.regions(id, region_type, boundary) "
+ f"values ({sql_literal(current_region)}, {sql_literal(region_type)}, "
+ f"st_geomfromtext({sql_literal(current_geometry)}, 900914));"
)
- # current_region=None
sql_batch.flush()
end = _print_time(f'End reading file "{inp}"')
print(f"Total (in second): {(end-start).seconds}(s)")
-def get_insert_into_region_sql(region: str, nodes: list[str]) -> str:
- str_sql = ""
- str_nodes = str(nodes).replace("'", "''")
- r_type = region[0 : region.index("_")]
- if r_type == "DMA" or r_type == "SA" or r_type == "VD":
- table = ""
- if r_type == "DMA":
- table = "region_dma"
- elif r_type == "SA":
- table = "region_sa"
- source = region[region.index("_") + 1 :]
- str_sql = f"insert into region_sa(id,time_index,source,nodes) values ('{region}', 0,'{source}','{str_nodes}');"
- elif r_type == "VD":
- table = "region_vd"
-
- return str_sql
-
-
def read_inp(project: str, inp: str, version: str = "3") -> bool:
if version != "3" and version != "2":
version = "2"
@@ -391,7 +398,9 @@ def read_inp(project: str, inp: str, version: str = "3") -> bool:
create_project(project)
open_project(project)
- parse_file(project, inp, version)
+ with project_transaction(project):
+ parse_file(project, inp, version)
+ refresh_materialized_views(project)
"""try:
parse_file(project, inp, version)
diff --git a/app/native/wndb/sections.py b/app/native/wndb/inp/sections.py
similarity index 63%
rename from app/native/wndb/sections.py
rename to app/native/wndb/inp/sections.py
index 8c48a0d..9f774ad 100644
--- a/app/native/wndb/sections.py
+++ b/app/native/wndb/inp/sections.py
@@ -1,43 +1,3 @@
-s1_title = 'title'
-s2_junction = 'junction'
-s3_reservoir = 'reservoir'
-s4_tank = 'tank'
-s5_pipe = 'pipe'
-s6_pump = 'pump'
-s7_valve = 'valve'
-s8_tag = 'tag'
-s9_demand = 'demand'
-s10_status = 'status'
-s11_pattern = 'pattern'
-s12_curve = 'curve'
-s13_control = 'control'
-s14_rule = 'rule'
-s15_energy = 'energy'
-s15_pump_energy = 'pump_energy'
-s16_emitter = 'emitter'
-s17_quality = 'quality'
-s18_source = 'source'
-s19_reaction = 'reaction'
-s19_pipe_reaction = 'pipe_reaction'
-s19_tank_reaction = 'tank_reaction'
-s20_mixing = 'mixing'
-s21_time = 'time'
-s22_report = 'report'
-s23_option = 'option'
-s23_option_v3 = 'option_v3'
-s24_coordinate = 'coordinate'
-s25_vertex = 'vertex'
-s26_label = 'label'
-s27_backdrop = 'backdrop'
-s28_end = 'end'
-s29_scada_device = 'scada_device'
-s30_scada_device_data = 'scada_device_data'
-s31_scada_element = 'scada_element'
-s32_region = 'region'
-s33_dma = 'district_metering_area'
-s34_sa = 'service_area'
-s35_vd = 'virtual_district'
-
TITLE = 'TITLE'
JUNCTIONS = 'JUNCTIONS'
RESERVOIRS = 'RESERVOIRS'
@@ -87,4 +47,4 @@ section_names_for_epanetv2 = [TITLE, JUNCTIONS, RESERVOIRS, TANKS, PIPE
PATTERNS, CURVES, CONTROLS, RULES, ENERGY,
EMITTERS, QUALITY, SOURCES, REACTIONS, MIXING,
TIMES, REPORT, OPTIONS, COORDINATES, VERTICES,
- LABELS, BACKDROP, END]
\ No newline at end of file
+ LABELS, BACKDROP, END]
diff --git a/app/native/wndb/model/__init__.py b/app/native/wndb/model/__init__.py
new file mode 100644
index 0000000..a037248
--- /dev/null
+++ b/app/native/wndb/model/__init__.py
@@ -0,0 +1 @@
+"""Water-network model persistence grouped by domain entity."""
diff --git a/app/native/wndb/s13_controls.py b/app/native/wndb/model/controls.py
similarity index 52%
rename from app/native/wndb/s13_controls.py
rename to app/native/wndb/model/controls.py
index 7b43f2b..b3c5f17 100644
--- a/app/native/wndb/s13_controls.py
+++ b/app/native/wndb/model/controls.py
@@ -1,4 +1,13 @@
-from .database import *
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+)
def get_control_schema(name: str) -> dict[str, dict[str, Any]]:
@@ -6,28 +15,21 @@ def get_control_schema(name: str) -> dict[str, dict[str, Any]]:
def get_control(name: str) -> dict[str, Any]:
- cs = read_all(name, f"select * from controls")
+ cs = read_all(name, "select line from network.controls order by sequence_no")
ds = []
for c in cs:
ds.append(c['line'])
return { 'controls': ds }
-def _set_control(name: str, cs: ChangeSet) -> DbChangeSet:
- old = get_control(name)
+def _set_control(name: str, cs: ChangeSet) -> DatabaseCommand:
+ statement = 'delete from network.controls;'
+ for sequence_no, line in enumerate(cs.operations[0]['controls']):
+ statement += f"\ninsert into network.controls (sequence_no, line) values ({sequence_no}, {sql_literal(line)});"
- redo_sql = 'delete from controls;'
- for line in cs.operations[0]['controls']:
- redo_sql += f"\ninsert into controls (line) values ('{line}');"
+ change = g_update_prefix | { 'type': 'control', 'controls': cs.operations[0]['controls'] }
- undo_sql = 'delete from controls;'
- for line in old['controls']:
- undo_sql += f"\ninsert into controls (line) values ('{line}');"
-
- redo_cs = g_update_prefix | { 'type': 'control', 'controls': cs.operations[0]['controls'] }
- undo_cs = g_update_prefix | { 'type': 'control', 'controls': old['controls'] }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_control(name: str, cs: ChangeSet) -> ChangeSet:
@@ -45,7 +47,7 @@ def set_control(name: str, cs: ChangeSet) -> ChangeSet:
def inp_in_control(line: str) -> str:
- return str(f"insert into controls (line) values ('{line}');")
+ return str(f"insert into network.controls (sequence_no, line) values ((select coalesce(max(sequence_no) + 1, 0) from network.controls), {sql_literal(line)});")
def inp_out_control(name: str) -> list[str]:
diff --git a/app/native/wndb/s12_curves.py b/app/native/wndb/model/curves.py
similarity index 54%
rename from app/native/wndb/s12_curves.py
rename to app/native/wndb/model/curves.py
index eb37982..7fe841e 100644
--- a/app/native/wndb/s12_curves.py
+++ b/app/native/wndb/model/curves.py
@@ -1,4 +1,16 @@
-from .database import *
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_add_prefix,
+ g_delete_prefix,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+ try_read,
+)
CURVE_TYPE_PUMP = 'PUMP'
CURVE_TYPE_EFFICIENCY = 'EFFICIENCY'
@@ -16,26 +28,25 @@ def get_curve_schema(name: str) -> dict[str, dict[str, Any]]:
def get_curve(name: str, id: str) -> dict[str, Any]:
- c_one = try_read(name, f"select * from _curve where id = '{id}'")
+ c_one = try_read(name, "select id, curve_type from network.curves where id = %s", (id,))
if c_one == None:
return {}
- cus = read_all(name, f"select * from curves where id = '{id}' order by _order")
+ cus = read_all(name, "select x, y from network.curve_points where curve_id = %s order by sequence_no", (id,))
cs = []
for r in cus:
cs.append({ 'x': float(r['x']), 'y': float(r['y']) })
d = {}
d['id'] = id
- d['c_type'] = c_one['type']
+ d['c_type'] = c_one['curve_type']
d['coords'] = cs
return d
-def _set_curve(name: str, cs: ChangeSet) -> DbChangeSet:
+def _set_curve(name: str, cs: ChangeSet) -> DatabaseCommand:
id = cs.operations[0]['id']
- f_id = f"'{id}'"
+ f_id = sql_literal(id)
old = get_curve(name, id)
- old_f_type = f"'{old['c_type']}'"
new = { 'id': id }
if 'coords' in cs.operations[0]:
@@ -46,25 +57,17 @@ def _set_curve(name: str, cs: ChangeSet) -> DbChangeSet:
new['c_type'] = cs.operations[0]['c_type']
else:
new['c_type'] = old['c_type']
- new_f_type = f"'{new['c_type']}'"
+ new_f_type = sql_literal(new['c_type'])
- # TODO: transaction ?
- redo_sql = f"delete from curves where id = {f_id};"
- redo_sql += f"\nupdate _curve set type = {new_f_type} where id = {f_id};"
- for xy in new['coords']:
- f_x, f_y = xy['x'], xy['y']
- redo_sql += f"\ninsert into curves (id, x, y) values ({f_id}, {f_x}, {f_y});"
+ statement = f"delete from network.curve_points where curve_id = {f_id};"
+ statement += f"\nupdate network.curves set curve_type = {new_f_type} where id = {f_id};"
+ for sequence_no, xy in enumerate(new['coords']):
+ f_x, f_y = sql_literal(xy['x']), sql_literal(xy['y'])
+ statement += f"\ninsert into network.curve_points (curve_id, sequence_no, x, y) values ({f_id}, {sequence_no}, {f_x}, {f_y});"
- undo_sql = f"delete from curves where id = {f_id};"
- undo_sql += f"\nupdate _curve set type = {old_f_type} where id = {f_id};"
- for xy in old['coords']:
- f_x, f_y = xy['x'], xy['y']
- undo_sql += f"\ninsert into curves (id, x, y) values ({f_id}, {f_x}, {f_y});"
+ change = g_update_prefix | { 'type': 'curve' } | new
- redo_cs = g_update_prefix | { 'type': 'curve' } | new
- undo_cs = g_update_prefix | { 'type': 'curve' } | old
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_curve(name: str, cs: ChangeSet) -> ChangeSet:
@@ -75,28 +78,23 @@ def set_curve(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_curve(name, cs))
-def _add_curve(name: str, cs: ChangeSet) -> DbChangeSet:
+def _add_curve(name: str, cs: ChangeSet) -> DatabaseCommand:
id = cs.operations[0]['id']
- f_id = f"'{id}'"
+ f_id = sql_literal(id)
new = { 'id': id, 'c_type': cs.operations[0]['c_type'], 'coords': [] }
- new_f_type = f"'{new['c_type']}'"
+ new_f_type = sql_literal(new['c_type'])
- # TODO: transaction ?
- redo_sql = f"insert into _curve (id, type) values ({f_id}, {new_f_type});"
- for xy in cs.operations[0]['coords']:
+ statement = f"insert into network.curves (id, curve_type) values ({f_id}, {new_f_type});"
+ for sequence_no, xy in enumerate(cs.operations[0]['coords']):
x, y = float(xy['x']), float(xy['y'])
- f_x, f_y = x, y
- redo_sql += f"\ninsert into curves (id, x, y) values ({f_id}, {f_x}, {f_y});"
+ f_x, f_y = sql_literal(x), sql_literal(y)
+ statement += f"\ninsert into network.curve_points (curve_id, sequence_no, x, y) values ({f_id}, {sequence_no}, {f_x}, {f_y});"
new['coords'].append({ 'x': x, 'y': y })
- undo_sql = f"delete from curves where id = {f_id};"
- undo_sql += f"\ndelete from _curve where id = {f_id};"
+ change = g_add_prefix | { 'type': 'curve' } | new
- redo_cs = g_add_prefix | { 'type': 'curve' } | new
- undo_cs = g_delete_prefix | { 'type': 'curve' } | { 'id' : id }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def add_curve(name: str, cs: ChangeSet) -> ChangeSet:
@@ -107,26 +105,15 @@ def add_curve(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _add_curve(name, cs))
-def _delete_curve(name: str, cs: ChangeSet) -> DbChangeSet:
+def _delete_curve(name: str, cs: ChangeSet) -> DatabaseCommand:
id = cs.operations[0]['id']
- f_id = f"'{id}'"
+ f_id = sql_literal(id)
- old = get_curve(name, id)
- old_f_type = f"'{old['c_type']}'"
+ statement = f"delete from network.curves where id = {f_id};"
- redo_sql = f"delete from curves where id = {f_id};"
- redo_sql += f"\ndelete from _curve where id = {f_id};"
+ change = g_delete_prefix | { 'type': 'curve' } | { 'id' : id }
- # TODO: transaction ?
- undo_sql = f"insert into _curve (id, type) values ({f_id}, {old_f_type});"
- for xy in old['coords']:
- f_x, f_y = xy['x'], xy['y']
- undo_sql += f"\ninsert into curves (id, x, y) values ({f_id}, {f_x}, {f_y});"
-
- redo_cs = g_delete_prefix | { 'type': 'curve' } | { 'id' : id }
- undo_cs = g_add_prefix | { 'type': 'curve' } | old
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def delete_curve(name: str, cs: ChangeSet) -> ChangeSet:
@@ -151,17 +138,18 @@ def delete_curve(name: str, cs: ChangeSet) -> ChangeSet:
def inp_in_curve(line: str) -> str:
tokens = line.split()
- return str(f"insert into curves (id, x, y) values ('{tokens[0]}', {float(tokens[1])}, {float(tokens[2])});")
+ curve_id = sql_literal(tokens[0])
+ return str(f"insert into network.curve_points (curve_id, sequence_no, x, y) values ({curve_id}, (select coalesce(max(sequence_no) + 1, 0) from network.curve_points where curve_id = {curve_id}), {sql_literal(float(tokens[1]))}, {sql_literal(float(tokens[2]))});")
def inp_out_curve(name: str) -> list[str]:
lines = []
- types = read_all(name, f"select * from _curve")
+ types = read_all(name, "select id, curve_type as type from network.curves order by id")
for type in types:
id = type['id']
# ;type: desc
lines.append(f";{type['type']}:")
- objs = read_all(name, f"select * from curves where id = '{id}' order by _order")
+ objs = read_all(name, "select curve_id as id, x, y from network.curve_points where curve_id = %s order by sequence_no", (id,))
for obj in objs:
id = obj['id']
x = obj['x']
@@ -172,12 +160,12 @@ def inp_out_curve(name: str) -> list[str]:
def inp_out_curve_v3(name: str) -> list[str]:
lines = []
- types = read_all(name, f"select * from _curve")
+ types = read_all(name, "select id, curve_type as type from network.curves order by id")
for type in types:
id = type['id']
# id type
lines.append(f"{id} {type['type']}")
- objs = read_all(name, f"select * from curves where id = '{id}' order by _order")
+ objs = read_all(name, "select curve_id as id, x, y from network.curve_points where curve_id = %s order by sequence_no", (id,))
for obj in objs:
id = obj['id']
x = obj['x']
diff --git a/app/native/wndb/s9_demands.py b/app/native/wndb/model/demands.py
similarity index 58%
rename from app/native/wndb/s9_demands.py
rename to app/native/wndb/model/demands.py
index e1b8126..f0a1228 100644
--- a/app/native/wndb/s9_demands.py
+++ b/app/native/wndb/model/demands.py
@@ -1,4 +1,12 @@
-from .database import read_all, ChangeSet, DbChangeSet, g_update_prefix, execute_command, try_read
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+ try_read,
+)
from typing import Any
def get_demand_schema(name: str) -> dict[str, dict[str, Any]]:
@@ -10,7 +18,7 @@ def get_demand_schema(name: str) -> dict[str, dict[str, Any]]:
def get_demand(name: str, junction: str) -> dict[str, Any]:
- des = read_all(name, f"select * from demands where junction = '{junction}' order by _order")
+ des = read_all(name, "select base_demand as demand, pattern_id as pattern, category from network.demands where junction_id = %s order by sequence_no", (junction,))
ds = []
for r in des:
d = {}
@@ -21,39 +29,26 @@ def get_demand(name: str, junction: str) -> dict[str, Any]:
return { 'junction': junction, 'demands': ds }
-def _set_demand(name: str, cs: ChangeSet) -> DbChangeSet:
+def _set_demand(name: str, cs: ChangeSet) -> DatabaseCommand:
junction = cs.operations[0]['junction']
- old = get_demand(name, junction)
new = { 'junction': junction, 'demands': [] }
- f_junction = f"'{junction}'"
+ f_junction = sql_literal(junction)
- # TODO: transaction ?
- redo_sql = f"delete from demands where junction = {f_junction};"
- for r in cs.operations[0]['demands']:
+ statement = f"delete from network.demands where junction_id = {f_junction};"
+ for sequence_no, r in enumerate(cs.operations[0]['demands']):
demand = float(r['demand'])
pattern = str(r['pattern']) if 'pattern' in r and r['pattern'] != None else None
category = str(r['category']) if 'category' in r and r['category'] != None else None
- f_demand = demand
- f_pattern = f"'{pattern}'" if pattern is not None else 'null'
- f_category = f"'{category}'" if category is not None else 'null'
- redo_sql += f"\ninsert into demands (junction, demand, pattern, category) values ({f_junction}, {f_demand}, {f_pattern}, {f_category});"
+ f_demand = sql_literal(demand)
+ f_pattern = sql_literal(pattern)
+ f_category = sql_literal(category)
+ statement += f"\ninsert into network.demands (junction_id, sequence_no, base_demand, pattern_id, category) values ({f_junction}, {sequence_no}, {f_demand}, {f_pattern}, {f_category});"
new['demands'].append({ 'demand': demand, 'pattern': pattern, 'category': category })
- undo_sql = f"delete from demands where junction = {f_junction};"
- for r in old['demands']:
- demand = float(r['demand'])
- pattern = str(r['pattern']) if 'pattern' in r and r['pattern'] != None else None
- category = str(r['category']) if 'category' in r and r['category'] != None else None
- f_demand = demand
- f_pattern = f"'{pattern}'" if pattern is not None else 'null'
- f_category = f"'{category}'" if category is not None else 'null'
- undo_sql += f"\ninsert into demands (junction, demand, pattern, category) values ({f_junction}, {f_demand}, {f_pattern}, {f_category});"
+ change = g_update_prefix | { 'type': 'demand' } | new
- redo_cs = g_update_prefix | { 'type': 'demand' } | new
- undo_cs = g_update_prefix | { 'type': 'demand' } | old
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_demand(name: str, cs: ChangeSet) -> ChangeSet:
@@ -76,16 +71,15 @@ def inp_in_demand(line: str) -> str:
junction = str(tokens[0])
demand = float(tokens[1])
pattern = str(tokens[2]) if num_without_desc >= 3 else None
- pattern = f"'{pattern}'" if pattern is not None else 'null'
category = str(tokens[3]) if num_without_desc >= 4 else None
- category = f"'{category}'" if category is not None else 'null'
- return str(f"insert into demands (junction, demand, pattern, category) values ('{junction}', {demand}, {pattern}, {category});")
+ junction_sql = sql_literal(junction)
+ return str(f"insert into network.demands (junction_id, sequence_no, base_demand, pattern_id, category) values ({junction_sql}, (select coalesce(max(sequence_no) + 1, 0) from network.demands where junction_id = {junction_sql}), {sql_literal(demand)}, {sql_literal(pattern)}, {sql_literal(category)});")
def inp_out_demand(name: str) -> list[str]:
lines = []
- objs = read_all(name, "select * from demands order by _order")
+ objs = read_all(name, "select junction_id as junction, base_demand as demand, pattern_id as pattern, category from network.demands order by junction_id, sequence_no")
for obj in objs:
junction = obj['junction']
demand = obj['demand']
@@ -96,7 +90,7 @@ def inp_out_demand(name: str) -> list[str]:
def delete_demand_by_junction(name: str, junction: str) -> ChangeSet:
- row = try_read(name, f"select * from demands where junction = '{junction}'")
+ row = try_read(name, "select 1 from network.demands where junction_id = %s", (junction,))
if row is None:
return ChangeSet()
return ChangeSet(g_update_prefix | {'type': 'demand', 'junction': junction, 'demands': []})
@@ -105,7 +99,7 @@ def delete_demand_by_junction(name: str, junction: str) -> ChangeSet:
def unset_demand_by_pattern(name: str, pattern: str) -> ChangeSet:
cs = ChangeSet()
- rows = read_all(name, f"select distinct junction from demands where pattern = '{pattern}'")
+ rows = read_all(name, "select distinct junction_id as junction from network.demands where pattern_id = %s", (pattern,))
for row in rows:
ds = get_demand(name, row['junction'])
for d in ds['demands']:
diff --git a/app/native/wndb/model/elements.py b/app/native/wndb/model/elements.py
new file mode 100644
index 0000000..fe8cb93
--- /dev/null
+++ b/app/native/wndb/model/elements.py
@@ -0,0 +1,282 @@
+from typing import Any
+
+from psycopg import sql
+from psycopg.rows import Row, dict_row
+
+from ..core.connection import project_connection
+from ..core.database import read
+
+_NODE = "network.nodes"
+_LINK = "network.links"
+_CURVE = "network.curves"
+_PATTERN = "network.patterns"
+_REGION = "gis.regions"
+
+JUNCTION = "junction"
+RESERVOIR = "reservoir"
+TANK = "tank"
+PIPE = "pipe"
+PUMP = "pump"
+VALVE = "valve"
+PATTERN = "pattern"
+CURVE = "curve"
+REGION = "region"
+
+ELEMENT_TYPES: dict[str, int] = {
+ RESERVOIR: 0,
+ TANK: 1,
+ JUNCTION: 2,
+ PIPE: 3,
+ PUMP: 4,
+ VALVE: 5,
+}
+
+
+def _table_identifier(table: str):
+ return sql.Identifier(*table.split("."))
+
+
+def _get_from(name: str, element_id: str, table: str) -> Row | None:
+ query = sql.SQL("SELECT * FROM {} WHERE id = %s").format(
+ _table_identifier(table)
+ )
+ with project_connection(name) as conn, conn.cursor(row_factory=dict_row) as cur:
+ cur.execute(query, (element_id,))
+ return cur.fetchone()
+
+
+def is_node(name: str, element_id: str) -> bool:
+ return _get_from(name, element_id, _NODE) is not None
+
+
+def is_junction(name: str, element_id: str) -> bool:
+ row = _get_from(name, element_id, _NODE)
+ return row is not None and row["node_type"] == JUNCTION
+
+
+def is_reservoir(name: str, element_id: str) -> bool:
+ row = _get_from(name, element_id, _NODE)
+ return row is not None and row["node_type"] == RESERVOIR
+
+
+def is_tank(name: str, element_id: str) -> bool:
+ row = _get_from(name, element_id, _NODE)
+ return row is not None and row["node_type"] == TANK
+
+
+def is_link(name: str, element_id: str) -> bool:
+ return _get_from(name, element_id, _LINK) is not None
+
+
+def is_pipe(name: str, element_id: str) -> bool:
+ row = _get_from(name, element_id, _LINK)
+ return row is not None and row["link_type"] == PIPE
+
+
+def is_pump(name: str, element_id: str) -> bool:
+ row = _get_from(name, element_id, _LINK)
+ return row is not None and row["link_type"] == PUMP
+
+
+def is_valve(name: str, element_id: str) -> bool:
+ row = _get_from(name, element_id, _LINK)
+ return row is not None and row["link_type"] == VALVE
+
+
+def get_node_type(name: str, node_id: str) -> str:
+ row = _get_from(name, node_id, _NODE)
+ if row is None:
+ raise LookupError(node_id)
+ return row["node_type"]
+
+
+def get_link_type(name: str, link_id: str) -> str:
+ row = _get_from(name, link_id, _LINK)
+ if row is None:
+ raise LookupError(link_id)
+ return row["link_type"]
+
+
+def get_element_type(name: str, element_id: str) -> str | None:
+ if is_node(name, element_id):
+ return get_node_type(name, element_id)
+ if is_link(name, element_id):
+ return get_link_type(name, element_id)
+ return None
+
+
+def get_element_type_value(name: str, element_id: str) -> int:
+ element_type = get_element_type(name, element_id)
+ if element_type is None:
+ raise LookupError(element_id)
+ return ELEMENT_TYPES[element_type]
+
+
+def is_curve(name: str, element_id: str) -> bool:
+ return _get_from(name, element_id, _CURVE) is not None
+
+
+def is_pattern(name: str, element_id: str) -> bool:
+ return _get_from(name, element_id, _PATTERN) is not None
+
+
+def is_region(name: str, element_id: str) -> bool:
+ return _get_from(name, element_id, _REGION) is not None
+
+
+def _get_all(name: str, table: str) -> list[str]:
+ query = sql.SQL("SELECT id FROM {} ORDER BY id").format(_table_identifier(table))
+ with project_connection(name) as conn, conn.cursor(row_factory=dict_row) as cur:
+ cur.execute(query)
+ return [row["id"] for row in cur]
+
+
+def _get_nodes_by_type(name: str, node_type: str) -> list[str]:
+ rows = read_all_typed(
+ name,
+ "SELECT id FROM network.nodes WHERE node_type = %s ORDER BY id",
+ (node_type,),
+ )
+ return [row["id"] for row in rows]
+
+
+def _get_links_by_type(name: str, link_type: str) -> list[str]:
+ rows = read_all_typed(
+ name,
+ "SELECT id FROM network.links WHERE link_type = %s ORDER BY id",
+ (link_type,),
+ )
+ return [row["id"] for row in rows]
+
+
+def read_all_typed(name: str, query: str, params: tuple[Any, ...]) -> list[Row]:
+ with project_connection(name) as conn, conn.cursor(row_factory=dict_row) as cur:
+ cur.execute(query, params)
+ return cur.fetchall()
+
+
+def get_nodes(name: str) -> list[str]:
+ return _get_all(name, _NODE)
+
+
+def get_nodes_id_and_type(name: str) -> dict[str, str]:
+ rows = read_all_typed(name, "SELECT id, node_type FROM network.nodes", ())
+ return {row["id"]: row["node_type"] for row in rows}
+
+
+def get_major_nodes(name: str, diameter: int) -> list[str]:
+ rows = read_all_typed(
+ name,
+ """
+ SELECT DISTINCT endpoint
+ FROM network.links AS l
+ JOIN network.pipes AS p ON p.link_id = l.id
+ CROSS JOIN LATERAL (VALUES (l.start_node_id), (l.end_node_id)) AS e(endpoint)
+ WHERE p.diameter > %s
+ """,
+ (diameter,),
+ )
+ return [row["endpoint"] for row in rows]
+
+
+def get_junctions(name: str) -> list[str]:
+ return _get_nodes_by_type(name, JUNCTION)
+
+
+def get_reservoirs(name: str) -> list[str]:
+ return _get_nodes_by_type(name, RESERVOIR)
+
+
+def get_tanks(name: str) -> list[str]:
+ return _get_nodes_by_type(name, TANK)
+
+
+def get_links(name: str) -> list[str]:
+ return _get_all(name, _LINK)
+
+
+def get_links_id_and_type(name: str) -> dict[str, str]:
+ rows = read_all_typed(name, "SELECT id, link_type FROM network.links", ())
+ return {row["id"]: row["link_type"] for row in rows}
+
+
+def get_major_pipes(name: str, diameter: int) -> list[str]:
+ rows = read_all_typed(
+ name,
+ "SELECT link_id FROM network.pipes WHERE diameter > %s ORDER BY link_id",
+ (diameter,),
+ )
+ return [row["link_id"] for row in rows]
+
+
+def get_pipes(name: str) -> list[str]:
+ return _get_links_by_type(name, PIPE)
+
+
+def get_pumps(name: str) -> list[str]:
+ return _get_links_by_type(name, PUMP)
+
+
+def get_valves(name: str) -> list[str]:
+ return _get_links_by_type(name, VALVE)
+
+
+def get_curves(name: str) -> list[str]:
+ return _get_all(name, _CURVE)
+
+
+def get_patterns(name: str) -> list[str]:
+ return _get_all(name, _PATTERN)
+
+
+def get_regions(name: str) -> list[str]:
+ return _get_all(name, _REGION)
+
+
+def get_node_links(name: str, node_id: str) -> list[str]:
+ rows = read_all_typed(
+ name,
+ """
+ SELECT id FROM network.links
+ WHERE start_node_id = %s OR end_node_id = %s
+ ORDER BY id
+ """,
+ (node_id, node_id),
+ )
+ return [row["id"] for row in rows]
+
+
+def get_all_node_links(name: str) -> dict[str, list[str]]:
+ """Build the node adjacency map with one scan of the link table."""
+ rows = read_all_typed(
+ name,
+ "SELECT id, start_node_id, end_node_id FROM network.links ORDER BY id",
+ (),
+ )
+ result: dict[str, list[str]] = {}
+ for row in rows:
+ link_id = str(row["id"])
+ result.setdefault(str(row["start_node_id"]), []).append(link_id)
+ result.setdefault(str(row["end_node_id"]), []).append(link_id)
+ return result
+
+
+def get_link_nodes(name: str, link_id: str) -> list[str]:
+ row = read(
+ name,
+ """
+ SELECT start_node_id, end_node_id
+ FROM network.links WHERE id = %s
+ """,
+ (link_id,),
+ )
+ return [str(row["start_node_id"]), str(row["end_node_id"])]
+
+
+def get_region_type(name: str, region_id: str) -> str:
+ row = read(
+ name,
+ "SELECT region_type FROM gis.regions WHERE id = %s",
+ (region_id,),
+ )
+ return row["region_type"]
diff --git a/app/native/wndb/s16_emitters.py b/app/native/wndb/model/emitters.py
similarity index 66%
rename from app/native/wndb/s16_emitters.py
rename to app/native/wndb/model/emitters.py
index 8f510a9..591ede3 100644
--- a/app/native/wndb/s16_emitters.py
+++ b/app/native/wndb/model/emitters.py
@@ -1,4 +1,14 @@
-from .database import *
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+ try_read,
+)
def get_emitter_schema(name: str) -> dict[str, dict[str, Any]]:
@@ -7,7 +17,7 @@ def get_emitter_schema(name: str) -> dict[str, dict[str, Any]]:
def get_emitter(name: str, junction: str) -> dict[str, Any]:
- e = try_read(name, f"select * from emitters where junction = '{junction}'")
+ e = try_read(name, "select junction_id as junction, coefficient from network.emitters where junction_id = %s", (junction,))
if e == None:
return { 'junction': junction, 'coefficient': None }
d = {}
@@ -22,16 +32,15 @@ class Emitter(object):
self.junction = str(input['junction'])
self.coefficient = float(input['coefficient']) if 'coefficient' in input and input['coefficient'] != None else None
- self.f_type = f"'{self.type}'"
- self.f_junction = f"'{self.junction}'"
- self.f_coefficient = self.coefficient if self.coefficient != None else 'null'
+ self.f_type = sql_literal(self.type)
+ self.f_junction = sql_literal(self.junction)
+ self.f_coefficient = sql_literal(self.coefficient)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'junction': self.junction, 'coefficient': self.coefficient }
-def _set_emitter(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Emitter(get_emitter(name, cs.operations[0]['junction']))
+def _set_emitter(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_emitter(name, cs.operations[0]['junction'])
new_dict = cs.operations[0]
@@ -41,18 +50,13 @@ def _set_emitter(name: str, cs: ChangeSet) -> DbChangeSet:
raw_new[key] = new_dict[key]
new = Emitter(raw_new)
- redo_sql = f"delete from emitters where junction = {new.f_junction};"
+ statement = f"delete from network.emitters where junction_id = {new.f_junction};"
if new.coefficient != None:
- redo_sql += f"\ninsert into emitters (junction, coefficient) values ({new.f_junction}, {new.f_coefficient});"
+ statement += f"\ninsert into network.emitters (junction_id, coefficient) values ({new.f_junction}, {new.f_coefficient});"
- undo_sql = f"delete from emitters where junction = {old.f_junction};"
- if old.coefficient != None:
- undo_sql += f"\ninsert into emitters (junction, coefficient) values ({old.f_junction}, {old.f_coefficient});"
+ change = g_update_prefix | new.as_dict()
- redo_cs = g_update_prefix | new.as_dict()
- undo_cs = g_update_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_emitter(name: str, cs: ChangeSet) -> ChangeSet:
@@ -78,12 +82,12 @@ def inp_in_emitter(line: str) -> str:
junction = str(tokens[0])
coefficient = float(tokens[1])
- return str(f"insert into emitters (junction, coefficient) values ('{junction}', {coefficient});")
+ return str(f"insert into network.emitters (junction_id, coefficient) values ({sql_literal(junction)}, {sql_literal(coefficient)});")
def inp_out_emitter(name: str) -> list[str]:
lines = []
- objs = read_all(name, 'select * from emitters')
+ objs = read_all(name, 'select junction_id as junction, coefficient from network.emitters order by junction_id')
for obj in objs:
junction = obj['junction']
coefficient = obj['coefficient']
@@ -92,7 +96,7 @@ def inp_out_emitter(name: str) -> list[str]:
def delete_emitter_by_junction(name: str, junction: str) -> ChangeSet:
- row = try_read(name, f"select * from emitters where junction = '{junction}'")
+ row = try_read(name, "select 1 from network.emitters where junction_id = %s", (junction,))
if row == None:
return ChangeSet()
return ChangeSet(g_update_prefix | {'type' : 'emitter', 'junction': junction, 'coefficient': None})
diff --git a/app/native/wndb/model/energy.py b/app/native/wndb/model/energy.py
new file mode 100644
index 0000000..7f60051
--- /dev/null
+++ b/app/native/wndb/model/energy.py
@@ -0,0 +1,220 @@
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+ try_read,
+)
+
+
+element_schema = {'type': 'str' , 'optional': True , 'readonly': False}
+
+
+def get_energy_schema(name: str) -> dict[str, dict[str, Any]]:
+ return { 'GLOBAL PRICE' : element_schema,
+ 'GLOBAL PATTERN' : element_schema,
+ 'GLOBAL EFFIC' : element_schema,
+ 'DEMAND CHARGE' : element_schema }
+
+
+def get_energy(name: str) -> dict[str, Any]:
+ ts = read_all(name, "select key, value from network.energy_settings")
+ d = {}
+ for e in ts:
+ d[e['key']] = str(e['value'])
+ return d
+
+
+def _set_energy(name: str, cs: ChangeSet) -> DatabaseCommand:
+ new = {}
+
+ new_dict = cs.operations[0]
+ schema = get_energy_schema(name)
+ for key in schema.keys():
+ if key in new_dict:
+ new[key] = str(new_dict[key])
+
+ change = g_update_prefix | { 'type' : 'energy' }
+
+ statement = ''
+ for key, value in new.items():
+ if statement != '':
+ statement += '\n'
+ statement += f"update network.energy_settings set value = {sql_literal(value)} where key = {sql_literal(key)};"
+ change |= { key: value }
+
+ return DatabaseCommand(statement, [change])
+
+
+def set_energy(name: str, cs: ChangeSet) -> ChangeSet:
+ return execute_command(name, _set_energy(name, cs))
+
+
+def get_pump_energy_schema(name: str) -> dict[str, dict[str, Any]]:
+ return { 'pump' : {'type': 'str' , 'optional': False , 'readonly': True },
+ 'price' : {'type': 'float' , 'optional': True , 'readonly': False},
+ 'pattern' : {'type': 'str' , 'optional': True , 'readonly': False},
+ 'effic' : {'type': 'str' , 'optional': True , 'readonly': False} }
+
+
+def get_pump_energy(name: str, pump: str) -> dict[str, Any]:
+ d = {}
+ d['pump'] = pump
+ pe = try_read(name, "select price, pattern_id as pattern, efficiency_curve_id as effic from network.pump_energy_settings where pump_id = %s", (pump,))
+ d['price'] = float(pe['price']) if pe is not None and pe['price'] is not None else None
+ d['pattern'] = str(pe['pattern']) if pe is not None and pe['pattern'] is not None else None
+ d['effic'] = str(pe['effic']) if pe is not None and pe['effic'] is not None else None
+ return d
+
+
+class PumpEnergy(object):
+ def __init__(self, input: dict[str, Any]) -> None:
+ self.type = 'pump_energy'
+ self.pump = str(input['pump'])
+ self.price = float(input['price']) if 'price' in input and input['price'] != None else None
+ self.pattern = str(input['pattern']) if 'pattern' in input and input['pattern'] != None else None
+ self.effic = str(input['effic']) if 'effic' in input and input['effic'] != None else None
+
+ self.f_type = sql_literal(self.type)
+ self.f_pump = sql_literal(self.pump)
+ self.f_price = sql_literal(self.price)
+ self.f_pattern = sql_literal(self.pattern)
+ self.f_effic = sql_literal(self.effic)
+
+ def as_dict(self) -> dict[str, Any]:
+ return { 'type': self.type, 'pump': self.pump, 'price': self.price, 'pattern': self.pattern, 'effic': self.effic }
+
+
+def _set_pump_energy(name: str, cs: ChangeSet) -> DatabaseCommand:
+ raw_new = get_pump_energy(name, cs.operations[0]['pump'])
+
+ new_dict = cs.operations[0]
+ schema = get_pump_energy_schema(name)
+ for key, value in schema.items():
+ if key in new_dict and not value['readonly']:
+ raw_new[key] = new_dict[key]
+ new = PumpEnergy(raw_new)
+
+ statement = f"delete from network.pump_energy_settings where pump_id = {new.f_pump};"
+ if new.price is not None or new.pattern is not None or new.effic is not None:
+ statement += f"\ninsert into network.pump_energy_settings (pump_id, efficiency_curve_id, pattern_id, price) values ({new.f_pump}, {new.f_effic}, {new.f_pattern}, {new.f_price});"
+
+ change = g_update_prefix | new.as_dict()
+
+ return DatabaseCommand(statement, [change])
+
+
+def set_pump_energy(name: str, cs: ChangeSet) -> ChangeSet:
+ return execute_command(name, _set_pump_energy(name, cs))
+
+
+#--------------------------------------------------------------
+# [EPA2][EPA3][IN][OUT]
+# GLOBAL {PRICE/PATTERN/EFFIC} value
+# PUMP id {PRICE/PATTERN/EFFIC} value
+# DEMAND CHARGE value
+#--------------------------------------------------------------
+
+
+def inp_in_energy(line: str) -> str:
+ tokens = line.split()
+
+ if tokens[0].upper() == 'PUMP':
+ pump = tokens[1]
+ key = tokens[2].lower()
+ value = tokens[3]
+ if key == 'price':
+ value = float(value)
+ if key == 'efficiency':
+ key = 'effic'
+
+ column = {'price': 'price', 'pattern': 'pattern_id', 'effic': 'efficiency_curve_id'}[key]
+ return str(f"insert into network.pump_energy_settings (pump_id, {column}) values ({sql_literal(pump)}, {sql_literal(value)}) on conflict (pump_id) do update set {column} = excluded.{column};")
+
+ else:
+ line = line.upper().strip()
+ for key in get_energy_schema('').keys():
+ if line.startswith(key):
+ value = line.removeprefix(key).strip()
+
+ # exception here
+ if line.startswith('GLOBAL EFFICIENCY'):
+ value = line.removeprefix('GLOBAL EFFICIENCY').strip()
+
+ return str(f"update network.energy_settings set value = {sql_literal(value)} where key = {sql_literal(key)};")
+
+ return str('')
+
+
+def inp_out_energy(name: str) -> list[str]:
+ lines = []
+
+ objs = read_all(name, "select key, value from network.energy_settings order by key")
+ for obj in objs:
+ key = obj['key']
+ value = obj['value']
+ if value.strip() != '':
+ lines.append(f'{key} {value}')
+
+ objs = read_all(name, "select pump_id as pump, price, pattern_id as pattern, efficiency_curve_id as effic from network.pump_energy_settings order by pump_id")
+ for obj in objs:
+ pump = obj['pump']
+ if obj['price'] is not None:
+ lines.append(f"PUMP {pump} PRICE {obj['price']}")
+ if obj['pattern'] is not None:
+ lines.append(f"PUMP {pump} PATTERN {obj['pattern']}")
+ if obj['effic'] is not None:
+ lines.append(f"PUMP {pump} EFFIC {obj['effic']}")
+
+ return lines
+
+
+def delete_pump_energy_by_pump(name: str, pump: str) -> ChangeSet:
+ row = try_read(
+ name,
+ "select pump_id from network.pump_energy_settings where pump_id = %s",
+ (pump,),
+ )
+ if row is None:
+ return ChangeSet()
+ return ChangeSet(g_update_prefix | {'type': 'pump_energy', 'pump' : pump, 'price': None, 'pattern': None, 'effic': None})
+
+
+def unset_pump_energy_by_pattern(name: str, pattern: str) -> ChangeSet:
+ cs = ChangeSet()
+
+ rows = read_all(
+ name,
+ "select pump_id as pump, price, efficiency_curve_id as effic "
+ "from network.pump_energy_settings where pattern_id = %s",
+ (pattern,),
+ )
+ for row in rows:
+ pump = row['pump']
+ price = float(row['price']) if row['price'] is not None else None
+ effic = str(row['effic']) if row['effic'] is not None else None
+ cs.append(g_update_prefix | {'type': 'pump_energy', 'pump' : pump, 'price': price, 'pattern': None, 'effic': effic})
+
+ return cs
+
+
+def unset_pump_energy_by_curve(name: str, curve: str) -> ChangeSet:
+ cs = ChangeSet()
+
+ rows = read_all(
+ name,
+ "select pump_id as pump, price, pattern_id as pattern "
+ "from network.pump_energy_settings where efficiency_curve_id = %s",
+ (curve,),
+ )
+ for row in rows:
+ pump = row['pump']
+ price = float(row['price']) if row['price'] is not None else None
+ pattern = str(row['pattern']) if row['pattern'] is not None else None
+ cs.append(g_update_prefix | {'type': 'pump_energy', 'pump' : pump, 'price': price, 'pattern': pattern, 'effic': None})
+
+ return cs
diff --git a/app/native/wndb/s2_junctions.py b/app/native/wndb/model/junctions.py
similarity index 54%
rename from app/native/wndb/s2_junctions.py
rename to app/native/wndb/model/junctions.py
index ac59a8e..e7af019 100644
--- a/app/native/wndb/s2_junctions.py
+++ b/app/native/wndb/model/junctions.py
@@ -1,6 +1,20 @@
-from .database import *
-from .s0_base import *
-from .s24_coordinates import *
+from typing import Any
+
+from psycopg import sql
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_add_prefix,
+ g_delete_prefix,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+ try_read,
+)
+from ..gis.coordinates import sql_delete_coord, sql_insert_coord, sql_update_coord
+from .elements import get_all_node_links
def get_junction_schema(name: str) -> dict[str, dict[str, Any]]:
@@ -12,34 +26,55 @@ def get_junction_schema(name: str) -> dict[str, dict[str, Any]]:
def get_junction(name: str, id: str) -> dict[str, Any]:
- j = try_read(name, "select * from junctions where id = %s", (id,))
+ j = try_read(
+ name,
+ """
+ SELECT n.id, j.elevation, ST_X(g.geom) AS x, ST_Y(g.geom) AS y,
+ COALESCE(array_agg(l.id ORDER BY l.id)
+ FILTER (WHERE l.id IS NOT NULL), '{}') AS links
+ FROM network.nodes AS n
+ JOIN network.junctions AS j ON j.node_id = n.id
+ LEFT JOIN gis.node_geometries AS g ON g.node_id = n.id
+ LEFT JOIN network.links AS l
+ ON l.start_node_id = n.id OR l.end_node_id = n.id
+ WHERE n.id = %s
+ GROUP BY n.id, j.elevation, g.geom
+ """,
+ (id,),
+ )
if j == None:
return {}
- xy = get_node_coord(name, id)
d = {}
d['id'] = str(j['id'])
- d['x'] = float(xy['x'])
- d['y'] = float(xy['y'])
+ d['x'] = float(j['x'] or 0.0)
+ d['y'] = float(j['y'] or 0.0)
d['elevation'] = float(j['elevation'])
- d['links'] = get_node_links(name, id)
+ d['links'] = list(j['links'])
return d
# DingZQ, 2025-03-29
def get_all_junctions(name: str) -> list[dict[str, Any]]:
- rows = read_all(name, f"select * from junctions")
+ rows = read_all(
+ name,
+ """
+ SELECT id, elevation, x, y
+ FROM gis.junctions
+ ORDER BY id
+ """,
+ )
if rows == None:
return []
+ links_by_node = get_all_node_links(name)
result = []
for row in rows:
d = {}
id = str(row['id'])
- xy = get_node_coord(name, id)
d['id'] = id
- d['x'] = float(xy['x'])
- d['y'] = float(xy['y'])
+ d['x'] = float(row['x'] or 0.0)
+ d['y'] = float(row['y'] or 0.0)
d['elevation'] = float(row['elevation'])
- d['links'] = get_node_links(name, id)
+ d['links'] = links_by_node.get(id, [])
result.append(d)
return result
@@ -52,19 +87,14 @@ class Junction(object):
self.y = float(input['y'])
self.elevation = float(input['elevation'])
- self.f_type = f"'{self.type}'"
- self.f_id = f"'{self.id}'"
- self.f_elevation = self.elevation
+ self.f_type = sql_literal(self.type)
+ self.f_id = sql_literal(self.id)
+ self.f_elevation = sql_literal(self.elevation)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'id': self.id, 'x': self.x, 'y': self.y, 'elevation': self.elevation }
- def as_id_dict(self) -> dict[str, Any]:
- return { 'type': self.type, 'id': self.id }
-
-
-def _set_junction(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Junction(get_junction(name, cs.operations[0]['id']))
+def _set_junction(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_junction(name, cs.operations[0]['id'])
new_dict = cs.operations[0]
@@ -74,16 +104,12 @@ def _set_junction(name: str, cs: ChangeSet) -> DbChangeSet:
raw_new[key] = new_dict[key]
new = Junction(raw_new)
- redo_sql = f"update junctions set elevation = {new.f_elevation} where id = {new.f_id};"
- redo_sql += f"\n{sql_update_coord(new.id, new.x, new.y)}"
+ statement = f"update network.junctions set elevation = {new.f_elevation} where node_id = {new.f_id};"
+ statement += f"\n{sql_update_coord(new.id, new.x, new.y)}"
- undo_sql = sql_update_coord(old.id, old.x, old.y)
- undo_sql += f"\nupdate junctions set elevation = {old.f_elevation} where id = {old.f_id};"
+ change = g_update_prefix | new.as_dict()
- redo_cs = g_update_prefix | new.as_dict()
- undo_cs = g_update_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_junction(name: str, cs: ChangeSet) -> ChangeSet:
@@ -94,21 +120,16 @@ def set_junction(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_junction(name, cs))
-def _add_junction(name: str, cs: ChangeSet) -> DbChangeSet:
+def _add_junction(name: str, cs: ChangeSet) -> DatabaseCommand:
new = Junction(cs.operations[0])
- redo_sql = f"insert into _node (id, type) values ({new.f_id}, {new.f_type});"
- redo_sql += f"\ninsert into junctions (id, elevation) values ({new.f_id}, {new.f_elevation});"
- redo_sql += f"\n{sql_insert_coord(new.id, new.x, new.y)}"
+ statement = f"insert into network.nodes (id, node_type) values ({new.f_id}, {new.f_type});"
+ statement += f"\ninsert into network.junctions (node_id, elevation) values ({new.f_id}, {new.f_elevation});"
+ statement += f"\n{sql_insert_coord(new.id, new.x, new.y)}"
- undo_sql = sql_delete_coord(new.id)
- undo_sql += f"\ndelete from junctions where id = {new.f_id};"
- undo_sql += f"\ndelete from _node where id = {new.f_id};"
+ change = g_add_prefix | new.as_dict()
- redo_cs = g_add_prefix | new.as_dict()
- undo_cs = g_delete_prefix | new.as_id_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def add_junction(name: str, cs: ChangeSet) -> ChangeSet:
@@ -119,21 +140,16 @@ def add_junction(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _add_junction(name, cs))
-def _delete_junction(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Junction(get_junction(name, cs.operations[0]['id']))
+def _delete_junction(name: str, cs: ChangeSet) -> DatabaseCommand:
+ element_id = str(cs.operations[0]['id'])
+ f_id = sql_literal(element_id)
- redo_sql = sql_delete_coord(old.id)
- redo_sql += f"\ndelete from junctions where id = {old.f_id};"
- redo_sql += f"\ndelete from _node where id = {old.f_id};"
+ statement = sql_delete_coord(element_id)
+ statement += f"\ndelete from network.nodes where id = {f_id};"
- undo_sql = f"insert into _node (id, type) values ({old.f_id}, {old.f_type});"
- undo_sql += f"\ninsert into junctions (id, elevation) values ({old.f_id}, {old.f_elevation});"
- undo_sql += f"\n{sql_insert_coord(old.id, old.x, old.y)}"
+ change = g_delete_prefix | {'type': 'junction', 'id': element_id}
- redo_cs = g_delete_prefix | old.as_id_dict()
- undo_cs = g_add_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def delete_junction(name: str, cs: ChangeSet) -> ChangeSet:
@@ -170,19 +186,18 @@ def inp_in_junction(line: str, demand_outside: bool) -> str:
elevation = float(tokens[1])
demand = float(tokens[2]) if num_without_desc >= 3 and tokens[2] != '*' else None
pattern = str(tokens[3]) if num_without_desc >= 4 and tokens[3] != '*' else None
- pattern = f"'{pattern}'" if pattern != None else 'null'
desc = str(tokens[-1]) if has_desc else None
- sql = f"insert into _node (id, type) values ('{id}', 'junction');insert into junctions (id, elevation) values ('{id}', {elevation});"
+ sql = f"insert into network.nodes (id, node_type) values ({sql_literal(id)}, 'junction');insert into network.junctions (node_id, elevation) values ({sql_literal(id)}, {sql_literal(elevation)});"
if demand != None and demand_outside == False:
- sql += f"insert into demands (junction, demand, pattern) values ('{id}', {demand}, {pattern});"
+ sql += f"insert into network.demands (junction_id, sequence_no, base_demand, pattern_id) values ({sql_literal(id)}, 0, {sql_literal(demand)}, {sql_literal(pattern)});"
return str(sql)
def inp_out_junction(name: str) -> list[str]:
lines = []
- objs = read_all(name, 'select * from junctions')
+ objs = read_all(name, 'select node_id as id, elevation from network.junctions order by node_id')
for obj in objs:
id = obj['id']
elev = obj['elevation']
diff --git a/app/native/wndb/s20_mixing.py b/app/native/wndb/model/mixing.py
similarity index 61%
rename from app/native/wndb/s20_mixing.py
rename to app/native/wndb/model/mixing.py
index 5bef359..de1b113 100644
--- a/app/native/wndb/s20_mixing.py
+++ b/app/native/wndb/model/mixing.py
@@ -1,5 +1,16 @@
-from .database import *
-from .s0_base import *
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_add_prefix,
+ g_delete_prefix,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+ try_read,
+)
MIXING_MODEL_MIXED = 'MIXED'
MIXING_MODEL_2COMP = '2COMP'
@@ -13,7 +24,7 @@ def get_mixing_schema(name: str) -> dict[str, dict[str, Any]]:
def get_mixing(name: str, tank: str) -> dict[str, Any]:
- m = try_read(name, f"select * from mixing where tank = '{tank}'")
+ m = try_read(name, "select tank_id as tank, model, value from network.tank_mixing where tank_id = %s", (tank,))
if m == None:
return {}
d = {}
@@ -30,20 +41,15 @@ class Mixing(object):
self.model = str(input['model'])
self.value = float(input['value']) if 'value' in input and input['value'] != None else None
- self.f_type = f"'{self.type}'"
- self.f_tank = f"'{self.tank}'"
- self.f_model = f"'{self.model}'"
- self.f_value = self.value if self.value != None else 'null'
+ self.f_type = sql_literal(self.type)
+ self.f_tank = sql_literal(self.tank)
+ self.f_model = sql_literal(self.model)
+ self.f_value = sql_literal(self.value)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'tank': self.tank, 'model': self.model, 'value': self.value }
- def as_id_dict(self) -> dict[str, Any]:
- return { 'type': self.type, 'tank': self.tank }
-
-
-def _set_mixing(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Mixing(get_mixing(name, cs.operations[0]['tank']))
+def _set_mixing(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_mixing(name, cs.operations[0]['tank'])
new_dict = cs.operations[0]
@@ -53,13 +59,11 @@ def _set_mixing(name: str, cs: ChangeSet) -> DbChangeSet:
raw_new[key] = new_dict[key]
new = Mixing(raw_new)
- redo_sql = f"update mixing set model = {new.f_model}, value = {new.f_value} where tank = {new.f_tank};"
- undo_sql = f"update mixing set model = {old.f_model}, value = {old.f_value} where tank = {old.f_tank};"
+ statement = f"update network.tank_mixing set model = {new.f_model}, value = {new.f_value} where tank_id = {new.f_tank};"
- redo_cs = g_update_prefix | new.as_dict()
- undo_cs = g_update_prefix | old.as_dict()
+ change = g_update_prefix | new.as_dict()
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_mixing(name: str, cs: ChangeSet) -> ChangeSet:
@@ -70,16 +74,14 @@ def set_mixing(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_mixing(name, cs))
-def _add_mixing(name: str, cs: ChangeSet) -> DbChangeSet:
+def _add_mixing(name: str, cs: ChangeSet) -> DatabaseCommand:
new = Mixing(cs.operations[0])
- redo_sql = f"insert into mixing (tank, model, value) values ({new.f_tank}, {new.f_model}, {new.f_value});"
- undo_sql = f"delete from mixing where tank = {new.f_tank};"
+ statement = f"insert into network.tank_mixing (tank_id, model, value) values ({new.f_tank}, {new.f_model}, {new.f_value});"
- redo_cs = g_add_prefix | new.as_dict()
- undo_cs = g_delete_prefix | new.as_id_dict()
+ change = g_add_prefix | new.as_dict()
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def add_mixing(name: str, cs: ChangeSet) -> ChangeSet:
@@ -90,16 +92,15 @@ def add_mixing(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _add_mixing(name, cs))
-def _delete_mixing(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Mixing(get_mixing(name, cs.operations[0]['tank']))
+def _delete_mixing(name: str, cs: ChangeSet) -> DatabaseCommand:
+ tank = str(cs.operations[0]['tank'])
+ f_tank = sql_literal(tank)
- redo_sql = f"delete from mixing where tank = {old.f_tank};"
- undo_sql = f"insert into mixing (tank, model, value) values ({old.f_tank}, {old.f_model}, {old.f_value});"
+ statement = f"delete from network.tank_mixing where tank_id = {f_tank};"
- redo_cs = g_delete_prefix | old.as_id_dict()
- undo_cs = g_add_prefix | old.as_dict()
+ change = g_delete_prefix | {'type': 'mixing', 'tank': tank}
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def delete_mixing(name: str, cs: ChangeSet) -> ChangeSet:
@@ -127,14 +128,12 @@ def inp_in_mixing(line: str) -> str:
tank = str(tokens[0])
model = str(tokens[1].upper())
value = float(tokens[3]) if num_without_desc >= 4 else None
- value = value if value != None else 'null'
-
- return str(f"insert into mixing (tank, model, value) values ('{tank}', '{model}', {value});")
+ return str(f"insert into network.tank_mixing (tank_id, model, value) values ({sql_literal(tank)}, {sql_literal(model)}, {sql_literal(value)});")
def inp_out_mixing(name: str) -> list[str]:
lines = []
- objs = read_all(name, 'select * from mixing')
+ objs = read_all(name, 'select tank_id as tank, model, value from network.tank_mixing order by tank_id')
for obj in objs:
tank = obj['tank']
model = obj['model']
@@ -144,7 +143,7 @@ def inp_out_mixing(name: str) -> list[str]:
def delete_mixing_by_tank(name: str, tank: str) -> ChangeSet:
- row = try_read(name, f"select * from mixing where tank = '{tank}'")
+ row = try_read(name, "select 1 from network.tank_mixing where tank_id = %s", (tank,))
if row == None:
return ChangeSet()
return ChangeSet(g_delete_prefix | {'type' : 'mixing', 'tank': tank})
diff --git a/app/native/wndb/s23_options_util.py b/app/native/wndb/model/options.py
similarity index 88%
rename from app/native/wndb/s23_options_util.py
rename to app/native/wndb/model/options.py
index f273125..b08306f 100644
--- a/app/native/wndb/s23_options_util.py
+++ b/app/native/wndb/model/options.py
@@ -1,4 +1,13 @@
-from .database import *
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+)
#--------------------------------------------------------------
@@ -99,45 +108,32 @@ def get_option_schema(name: str) -> dict[str, dict[str, Any]]:
def get_option(name: str) -> dict[str, Any]:
- ts = read_all(name, f"select * from options")
+ ts = read_all(name, "select key, value from network.simulation_settings where engine_version = 'legacy'")
d = {}
for e in ts:
d[e['key']] = str(e['value'])
return d
-def _set_option(name: str, cs: ChangeSet) -> DbChangeSet:
- raw_old = get_option(name)
-
- old = {}
+def _set_option(name: str, cs: ChangeSet) -> DatabaseCommand:
new = {}
new_dict = cs.operations[0]
schema = get_option_schema(name)
for key in schema.keys():
if key in new_dict:
- old[key] = str(raw_old[key])
new[key] = str(new_dict[key])
- redo_cs = g_update_prefix | { 'type' : 'option' }
+ change = g_update_prefix | { 'type' : 'option' }
- redo_sql = ''
+ statement = ''
for key, value in new.items():
- if redo_sql != '':
- redo_sql += '\n'
- redo_sql += f"update options set value = '{value}' where key = '{key}';"
- redo_cs |= { key: value }
+ if statement != '':
+ statement += '\n'
+ statement += f"update network.simulation_settings set value = {sql_literal(value)} where engine_version = 'legacy' and key = {sql_literal(key)};"
+ change |= { key: value }
- undo_cs = g_update_prefix | { 'type' : 'option' }
-
- undo_sql = ''
- for key, value in old.items():
- if undo_sql != '':
- undo_sql += '\n'
- undo_sql += f"update options set value = '{value}' where key = '{key}';"
- undo_cs |= { key: value }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_option(name: str, cs: ChangeSet) -> ChangeSet:
@@ -223,45 +219,32 @@ def get_option_v3_schema(name: str) -> dict[str, dict[str, Any]]:
def get_option_v3(name: str) -> dict[str, Any]:
- ts = read_all(name, f"select * from options_v3")
+ ts = read_all(name, "select key, value from network.simulation_settings where engine_version = 'v3'")
d = {}
for e in ts:
d[e['key']] = str(e['value'])
return d
-def _set_option_v3(name: str, cs: ChangeSet) -> DbChangeSet:
- raw_old = get_option_v3(name)
-
- old = {}
+def _set_option_v3(name: str, cs: ChangeSet) -> DatabaseCommand:
new = {}
new_dict = cs.operations[0]
schema = get_option_v3_schema(name)
for key in schema.keys():
if key in new_dict:
- old[key] = str(raw_old[key])
new[key] = str(new_dict[key])
- redo_cs = g_update_prefix | { 'type' : 'option_v3' }
+ change = g_update_prefix | { 'type' : 'option_v3' }
- redo_sql = ''
+ statement = ''
for key, value in new.items():
- if redo_sql != '':
- redo_sql += '\n'
- redo_sql += f"update options_v3 set value = '{value}' where key = '{key}';"
- redo_cs |= { key: value }
+ if statement != '':
+ statement += '\n'
+ statement += f"update network.simulation_settings set value = {sql_literal(value)} where engine_version = 'v3' and key = {sql_literal(key)};"
+ change |= { key: value }
- undo_cs = g_update_prefix | { 'type' : 'option_v3' }
-
- undo_sql = ''
- for key, value in old.items():
- if undo_sql != '':
- undo_sql += '\n'
- undo_sql += f"update options_v3 set value = '{value}' where key = '{key}';"
- undo_cs |= { key: value }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_option_v3(name: str, cs: ChangeSet) -> ChangeSet:
@@ -398,4 +381,3 @@ def generate_v3(cs: ChangeSet) -> ChangeSet:
return ChangeSet(cs_v3)
return ChangeSet()
-
diff --git a/app/native/wndb/s23_options.py b/app/native/wndb/model/options_legacy.py
similarity index 79%
rename from app/native/wndb/s23_options.py
rename to app/native/wndb/model/options_legacy.py
index f1a372c..253c416 100644
--- a/app/native/wndb/s23_options.py
+++ b/app/native/wndb/model/options_legacy.py
@@ -1,81 +1,83 @@
-from .database import *
-from .s23_options_util import get_option_schema, generate_v3
-
-
-def _inp_in_option(section: list[str]) -> ChangeSet:
- if len(section) <= 0:
- return ChangeSet()
-
- cs = g_update_prefix | { 'type' : 'option' }
- for s in section:
- if s.startswith(';'):
- continue
-
- tokens = s.strip().split()
- if tokens[0].upper() == 'PATTERN': # can not upper id
- value = tokens[1] if len(tokens) > 1 else ''
- cs |= { 'PATTERN' : value }
- elif tokens[0].upper() == 'QUALITY': # can not upper trace node
- value = tokens[1] if len(tokens) > 1 else ''
- if len(tokens) > 2:
- value += f' {tokens[2]}'
- cs |= { 'QUALITY' : value }
- else:
- line = s.upper().strip()
- for key in get_option_schema('').keys():
- if line.startswith(key):
- value = line.removeprefix(key).strip()
- cs |= { key : value }
-
- result = ChangeSet(cs)
- result.merge(generate_v3(result))
- return result
-
-
-def inp_in_option(section: list[str]) -> str:
- sql = ''
- result = _inp_in_option(section)
- for op in result.operations:
- for key in op.keys():
- if key == 'operation' or key == 'type':
- continue
- if op['type'] == 'option':
- sql += f"update options set value = '{op[key]}' where key = '{key}';"
- else:
- sql += f"update options_v3 set value = '{op[key]}' where key = '{key}';"
- return sql
-
-
-def inp_out_option(name: str) -> list[str]:
- lines = []
- objs = read_all(name, f"select * from options")
-
- is_dda = False
-
- for obj in objs:
- if obj['key'] == 'DEMAND MODEL':
- is_dda = obj['value'] == 'DDA'
-
- dda_ignore = [
- 'HEADERROR', # TODO: default is 0 which is conflict with PDA
- 'FLOWCHANGE', # TODO: default is 0 which is conflict with PDA
- 'MINIMUM PRESSURE',
- 'REQUIRED PRESSURE',
- 'PRESSURE EXPONENT'
- ]
-
- for obj in objs:
- key = obj['key']
- # why write this ?
- if key == 'PRESSURE':
- continue
- # release version does not support new keys and has error message
- if key == 'HTOL' or key == 'QTOL' or key == 'RQTOL':
- continue
- # ignore some weird settings for DDA
- if is_dda and key in dda_ignore:
- continue
- value = obj['value']
- if str(value).strip() != '':
- lines.append(f'{key} {value}')
- return lines
+from psycopg import sql
+
+from ..core.database import ChangeSet, g_update_prefix, read_all, sql_literal
+from .options import get_option_schema, generate_v3
+
+
+def _inp_in_option(section: list[str]) -> ChangeSet:
+ if len(section) <= 0:
+ return ChangeSet()
+
+ cs = g_update_prefix | { 'type' : 'option' }
+ for s in section:
+ if s.startswith(';'):
+ continue
+
+ tokens = s.strip().split()
+ if tokens[0].upper() == 'PATTERN': # can not upper id
+ value = tokens[1] if len(tokens) > 1 else ''
+ cs |= { 'PATTERN' : value }
+ elif tokens[0].upper() == 'QUALITY': # can not upper trace node
+ value = tokens[1] if len(tokens) > 1 else ''
+ if len(tokens) > 2:
+ value += f' {tokens[2]}'
+ cs |= { 'QUALITY' : value }
+ else:
+ line = s.upper().strip()
+ for key in get_option_schema('').keys():
+ if line.startswith(key):
+ value = line.removeprefix(key).strip()
+ cs |= { key : value }
+
+ result = ChangeSet(cs)
+ result.merge(generate_v3(result))
+ return result
+
+
+def inp_in_option(section: list[str]) -> str:
+ sql = ''
+ result = _inp_in_option(section)
+ for op in result.operations:
+ for key in op.keys():
+ if key == 'operation' or key == 'type':
+ continue
+ if op['type'] == 'option':
+ sql += f"update network.simulation_settings set value = {sql_literal(op[key])} where engine_version = 'legacy' and key = {sql_literal(key)};"
+ else:
+ sql += f"update network.simulation_settings set value = {sql_literal(op[key])} where engine_version = 'v3' and key = {sql_literal(key)};"
+ return sql
+
+
+def inp_out_option(name: str) -> list[str]:
+ lines = []
+ objs = read_all(name, "select key, value from network.simulation_settings where engine_version = 'legacy' order by key")
+
+ is_dda = False
+
+ for obj in objs:
+ if obj['key'] == 'DEMAND MODEL':
+ is_dda = obj['value'] == 'DDA'
+
+ dda_ignore = [
+ 'HEADERROR', # TODO: default is 0 which is conflict with PDA
+ 'FLOWCHANGE', # TODO: default is 0 which is conflict with PDA
+ 'MINIMUM PRESSURE',
+ 'REQUIRED PRESSURE',
+ 'PRESSURE EXPONENT'
+ ]
+
+ for obj in objs:
+ key = obj['key']
+ # why write this ?
+ if key == 'PRESSURE':
+ continue
+ # release version does not support new keys and has error message
+ if key == 'HTOL' or key == 'QTOL' or key == 'RQTOL':
+ continue
+ # ignore some weird settings for DDA
+ if is_dda and key in dda_ignore:
+ continue
+ value = obj['value']
+ if str(value).strip() != '':
+ lines.append(f'{key} {value}')
+ return lines
diff --git a/app/native/wndb/s23_options_v3.py b/app/native/wndb/model/options_v3.py
similarity index 77%
rename from app/native/wndb/s23_options_v3.py
rename to app/native/wndb/model/options_v3.py
index 0a7738b..ecf84b7 100644
--- a/app/native/wndb/s23_options_v3.py
+++ b/app/native/wndb/model/options_v3.py
@@ -1,5 +1,7 @@
-from .database import *
-from .s23_options_util import get_option_schema, get_option_v3_schema, generate_v2, generate_v3
+from psycopg import sql
+
+from ..core.database import ChangeSet, g_update_prefix, read_all, sql_literal
+from .options import get_option_schema, get_option_v3_schema, generate_v2, generate_v3
def _parse_v2(v2_lines: list[str]) -> dict[str, str]:
@@ -62,15 +64,15 @@ def inp_in_option_v3(section: list[str]) -> str:
if key == 'operation' or key == 'type':
continue
if op['type'] == 'option_v3':
- sql += f"update options_v3 set value = '{op[key]}' where key = '{key}';"
+ sql += f"update network.simulation_settings set value = {sql_literal(op[key])} where engine_version = 'v3' and key = {sql_literal(key)};"
else:
- sql += f"update options set value = '{op[key]}' where key = '{key}';"
+ sql += f"update network.simulation_settings set value = {sql_literal(op[key])} where engine_version = 'legacy' and key = {sql_literal(key)};"
return sql
def inp_out_option_v3(name: str) -> list[str]:
lines = []
- objs = read_all(name, f"select * from options_v3")
+ objs = read_all(name, "select key, value from network.simulation_settings where engine_version = 'v3' order by key")
for obj in objs:
key = obj['key']
value = obj['value']
diff --git a/app/native/wndb/s11_patterns.py b/app/native/wndb/model/patterns.py
similarity index 53%
rename from app/native/wndb/s11_patterns.py
rename to app/native/wndb/model/patterns.py
index 382d554..ffe8f70 100644
--- a/app/native/wndb/s11_patterns.py
+++ b/app/native/wndb/model/patterns.py
@@ -1,4 +1,18 @@
-from .database import *
+from typing import Any
+
+from psycopg import sql
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_add_prefix,
+ g_delete_prefix,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+ try_read,
+)
PATTERN_V3_TYPE_FIXED = 'FIXED'
PATTERN_V3_TYPE_VARIABLE = 'VARIABLE'
@@ -11,19 +25,19 @@ def get_pattern_schema(name: str) -> dict[str, dict[str, Any]]:
def get_pattern(name: str, id: str) -> dict[str, Any]:
- p_one = try_read(name, f"select * from _pattern where id = '{id}'")
+ p_one = try_read(name, "select id from network.patterns where id = %s", (id,))
if p_one == None:
return {}
- pas = read_all(name, f"select * from patterns where id = '{id}' order by _order")
+ pas = read_all(name, "select factor from network.pattern_values where pattern_id = %s order by sequence_no", (id,))
ps = []
for r in pas:
ps.append(float(r['factor']))
return { 'id': id, 'factors': ps }
-def _set_pattern(name: str, cs: ChangeSet) -> DbChangeSet:
+def _set_pattern(name: str, cs: ChangeSet) -> DatabaseCommand:
id = cs.operations[0]['id']
- f_id = f"'{id}'"
+ f_id = sql_literal(id)
old = get_pattern(name, id)
@@ -33,19 +47,14 @@ def _set_pattern(name: str, cs: ChangeSet) -> DbChangeSet:
else:
new['factors'] = old['factors']
- # TODO: transaction ?
- redo_sql = f"delete from patterns where id = {f_id};"
- for f_factor in new['factors']:
- redo_sql += f"\ninsert into patterns (id, factor) values ({f_id}, {f_factor});"
+ statement = f"delete from network.pattern_values where pattern_id = {f_id};"
+ for sequence_no, factor in enumerate(new['factors']):
+ f_factor = sql_literal(factor)
+ statement += f"\ninsert into network.pattern_values (pattern_id, sequence_no, factor) values ({f_id}, {sequence_no}, {f_factor});"
- undo_sql = f"delete from patterns where id = {f_id};"
- for f_factor in old['factors']:
- undo_sql += f"\ninsert into patterns (id, factor) values ({f_id}, {f_factor});"
+ change = g_update_prefix | { 'type': 'pattern' } | new
- redo_cs = g_update_prefix | { 'type': 'pattern' } | new
- undo_cs = g_update_prefix | { 'type': 'pattern' } | old
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_pattern(name: str, cs: ChangeSet) -> ChangeSet:
@@ -56,24 +65,20 @@ def set_pattern(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_pattern(name, cs))
-def _add_pattern(name: str, cs: ChangeSet) -> DbChangeSet:
+def _add_pattern(name: str, cs: ChangeSet) -> DatabaseCommand:
id = cs.operations[0]['id']
- f_id = f"'{id}'"
+ f_id = sql_literal(id)
new = { 'id': id, 'factors': cs.operations[0]['factors'] }
- # TODO: transaction ?
- redo_sql = f"insert into _pattern (id) values ({f_id});"
- for f_factor in new['factors']:
- redo_sql += f"\ninsert into patterns (id, factor) values ({f_id}, {f_factor});"
+ statement = f"insert into network.patterns (id) values ({f_id});"
+ for sequence_no, factor in enumerate(new['factors']):
+ f_factor = sql_literal(factor)
+ statement += f"\ninsert into network.pattern_values (pattern_id, sequence_no, factor) values ({f_id}, {sequence_no}, {f_factor});"
- undo_sql = f"delete from patterns where id = {f_id};"
- undo_sql += f"\ndelete from _pattern where id = {f_id};"
+ change = g_add_prefix | { 'type': 'pattern' } | new
- redo_cs = g_add_prefix | { 'type': 'pattern' } | new
- undo_cs = g_delete_prefix | { 'type': 'pattern' } | { 'id': id }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def add_pattern(name: str, cs: ChangeSet) -> ChangeSet:
@@ -84,24 +89,15 @@ def add_pattern(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _add_pattern(name, cs))
-def _delete_pattern(name: str, cs: ChangeSet) -> DbChangeSet:
+def _delete_pattern(name: str, cs: ChangeSet) -> DatabaseCommand:
id = cs.operations[0]['id']
- f_id = f"'{id}'"
+ f_id = sql_literal(id)
- old = get_pattern(name, id)
+ statement = f"delete from network.patterns where id = {f_id};"
- redo_sql = f"delete from patterns where id = {f_id};"
- redo_sql += f"\ndelete from _pattern where id = {f_id};"
+ change = g_delete_prefix | { 'type': 'pattern' } | { 'id': id }
- # TODO: transaction ?
- undo_sql = f"insert into _pattern (id) values ({f_id});"
- for f_factor in old['factors']:
- undo_sql += f"\ninsert into patterns (id, factor) values ({f_id}, {f_factor});"
-
- redo_cs = g_delete_prefix | { 'type': 'pattern' } | { 'id': id }
- undo_cs = g_add_prefix | { 'type': 'pattern' } | old
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def delete_pattern(name: str, cs: ChangeSet) -> ChangeSet:
@@ -129,18 +125,21 @@ def delete_pattern(name: str, cs: ChangeSet) -> ChangeSet:
def inp_in_pattern(line: str, fixed: bool = True) -> str:
tokens = line.split()
sql = ''
+ pattern_id = sql_literal(tokens[0])
if fixed:
for token in tokens[1:]:
- sql += f"insert into patterns (id, factor) values ('{tokens[0]}', {float(token)});"
+ factor = sql_literal(float(token))
+ sql += f"insert into network.pattern_values (pattern_id, sequence_no, factor) values ({pattern_id}, (select coalesce(max(sequence_no) + 1, 0) from network.pattern_values where pattern_id = {pattern_id}), {factor});"
else:
for token in tokens[1::2]:
- sql += f"insert into patterns (id, factor) values ('{tokens[0]}', {float(token)});"
+ factor = sql_literal(float(token))
+ sql += f"insert into network.pattern_values (pattern_id, sequence_no, factor) values ({pattern_id}, (select coalesce(max(sequence_no) + 1, 0) from network.pattern_values where pattern_id = {pattern_id}), {factor});"
return sql
def inp_out_pattern(name: str) -> list[str]:
lines = []
- objs = read_all(name, f"select * from patterns order by _order")
+ objs = read_all(name, "select pattern_id as id, factor from network.pattern_values order by pattern_id, sequence_no")
for obj in objs:
id = obj['id']
factor = obj['factor']
@@ -150,7 +149,7 @@ def inp_out_pattern(name: str) -> list[str]:
def inp_out_pattern_v3(name: str) -> list[str]:
lines = []
- objs = read_all(name, f"select * from patterns order by _order")
+ objs = read_all(name, "select pattern_id as id, factor from network.pattern_values order by pattern_id, sequence_no")
ids = []
for obj in objs:
id = obj['id']
diff --git a/app/native/wndb/s5_pipes.py b/app/native/wndb/model/pipes.py
similarity index 64%
rename from app/native/wndb/s5_pipes.py
rename to app/native/wndb/model/pipes.py
index 91b50cc..ae45e30 100644
--- a/app/native/wndb/s5_pipes.py
+++ b/app/native/wndb/model/pipes.py
@@ -1,5 +1,16 @@
-from .database import *
-from .s0_base import *
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_add_prefix,
+ g_delete_prefix,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+ try_read,
+)
PIPE_STATUS_OPEN = 'OPEN'
@@ -19,7 +30,7 @@ def get_pipe_schema(name: str) -> dict[str, dict[str, Any]]:
def get_pipe(name: str, id: str) -> dict[str, Any]:
- p = try_read(name, f"select * from pipes where id = '{id}'")
+ p = try_read(name, "select l.id, l.start_node_id as node1, l.end_node_id as node2, p.length, p.diameter, p.roughness, p.minor_loss, p.status from network.links l join network.pipes p on p.link_id = l.id where l.id = %s", (id,))
if p == None:
return {}
d = {}
@@ -35,7 +46,11 @@ def get_pipe(name: str, id: str) -> dict[str, Any]:
# DingZQ, 2025-03-29
def get_all_pipes(name: str) -> list[dict[str, Any]]:
- rows = read_all(name, f"select * from pipes")
+ rows = read_all(
+ name,
+ "SELECT id, start_node_id AS node1, end_node_id AS node2, length, "
+ "diameter, roughness, minor_loss, status FROM gis.pipes ORDER BY id",
+ )
if rows == None:
return []
@@ -72,7 +87,11 @@ def get_pipes_by_property(
'status',
]
- rows = read_all(name, "select * from pipes")
+ rows = read_all(
+ name,
+ "SELECT id, start_node_id AS node1, end_node_id AS node2, length, "
+ "diameter, roughness, minor_loss, status FROM gis.pipes ORDER BY id",
+ )
if rows == None:
return []
@@ -112,25 +131,20 @@ class Pipe(object):
self.minor_loss = float(input['minor_loss'])
self.status = str(input['status'])
- self.f_type = f"'{self.type}'"
- self.f_id = f"'{self.id}'"
- self.f_node1 = f"'{self.node1}'"
- self.f_node2 = f"'{self.node2}'"
- self.f_length = self.length
- self.f_diameter = self.diameter
- self.f_roughness = self.roughness
- self.f_minor_loss = self.minor_loss
- self.f_status = f"'{self.status}'"
+ self.f_type = sql_literal(self.type)
+ self.f_id = sql_literal(self.id)
+ self.f_node1 = sql_literal(self.node1)
+ self.f_node2 = sql_literal(self.node2)
+ self.f_length = sql_literal(self.length)
+ self.f_diameter = sql_literal(self.diameter)
+ self.f_roughness = sql_literal(self.roughness)
+ self.f_minor_loss = sql_literal(self.minor_loss)
+ self.f_status = sql_literal(self.status)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'id': self.id, 'node1': self.node1, 'node2': self.node2, 'length': self.length, 'diameter': self.diameter, 'roughness': self.roughness, 'minor_loss': self.minor_loss, 'status': self.status }
- def as_id_dict(self) -> dict[str, Any]:
- return { 'type': self.type, 'id': self.id }
-
-
-def _set_pipe(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Pipe(get_pipe(name, cs.operations[0]['id']))
+def _set_pipe(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_pipe(name, cs.operations[0]['id'])
new_dict = cs.operations[0]
@@ -140,13 +154,11 @@ def _set_pipe(name: str, cs: ChangeSet) -> DbChangeSet:
raw_new[key] = new_dict[key]
new = Pipe(raw_new)
- redo_sql = f"update pipes set node1 = {new.f_node1}, node2 = {new.f_node2}, length = {new.f_length}, diameter = {new.f_diameter}, roughness = {new.f_roughness}, minor_loss = {new.f_minor_loss}, status = {new.f_status} where id = {new.f_id};"
- undo_sql = f"update pipes set node1 = {old.f_node1}, node2 = {old.f_node2}, length = {old.f_length}, diameter = {old.f_diameter}, roughness = {old.f_roughness}, minor_loss = {old.f_minor_loss}, status = {old.f_status} where id = {old.f_id};"
+ statement = f"update network.links set start_node_id = {new.f_node1}, end_node_id = {new.f_node2} where id = {new.f_id};"
+ statement += f"\nupdate network.pipes set length = {new.f_length}, diameter = {new.f_diameter}, roughness = {new.f_roughness}, minor_loss = {new.f_minor_loss}, status = {new.f_status} where link_id = {new.f_id};"
+ change = g_update_prefix | new.as_dict()
- redo_cs = g_update_prefix | new.as_dict()
- undo_cs = g_update_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_pipe(name: str, cs: ChangeSet) -> ChangeSet:
@@ -157,19 +169,15 @@ def set_pipe(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_pipe(name, cs))
-def _add_pipe(name: str, cs: ChangeSet) -> DbChangeSet:
+def _add_pipe(name: str, cs: ChangeSet) -> DatabaseCommand:
new = Pipe(cs.operations[0])
- redo_sql = f"insert into _link (id, type) values ({new.f_id}, {new.f_type});"
- redo_sql += f"\ninsert into pipes (id, node1, node2, length, diameter, roughness, minor_loss, status) values ({new.f_id}, {new.f_node1}, {new.f_node2}, {new.f_length}, {new.f_diameter}, {new.f_roughness}, {new.f_minor_loss}, {new.f_status});"
+ statement = f"insert into network.links (id, link_type, start_node_id, end_node_id) values ({new.f_id}, {new.f_type}, {new.f_node1}, {new.f_node2});"
+ statement += f"\ninsert into network.pipes (link_id, length, diameter, roughness, minor_loss, status) values ({new.f_id}, {new.f_length}, {new.f_diameter}, {new.f_roughness}, {new.f_minor_loss}, {new.f_status});"
- undo_sql = f"delete from pipes where id = {new.f_id};"
- undo_sql += f"\ndelete from _link where id = {new.f_id};"
+ change = g_add_prefix | new.as_dict()
- redo_cs = g_add_prefix | new.as_dict()
- undo_cs = g_delete_prefix | new.as_id_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def add_pipe(name: str, cs: ChangeSet) -> ChangeSet:
@@ -180,19 +188,15 @@ def add_pipe(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _add_pipe(name, cs))
-def _delete_pipe(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Pipe(get_pipe(name, cs.operations[0]['id']))
+def _delete_pipe(name: str, cs: ChangeSet) -> DatabaseCommand:
+ element_id = str(cs.operations[0]['id'])
+ f_id = sql_literal(element_id)
- redo_sql = f"delete from pipes where id = {old.f_id};"
- redo_sql += f"\ndelete from _link where id = {old.f_id};"
+ statement = f"delete from network.links where id = {f_id};"
- undo_sql = f"insert into _link (id, type) values ({old.f_id}, {old.f_type});"
- undo_sql += f"\ninsert into pipes (id, node1, node2, length, diameter, roughness, minor_loss, status) values ({old.f_id}, {old.f_node1}, {old.f_node2}, {old.f_length}, {old.f_diameter}, {old.f_roughness}, {old.f_minor_loss}, {old.f_status});"
+ change = g_delete_prefix | {'type': 'pipe', 'id': element_id}
- redo_cs = g_delete_prefix | old.as_id_dict()
- undo_cs = g_add_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def delete_pipe(name: str, cs: ChangeSet) -> ChangeSet:
@@ -230,12 +234,12 @@ def inp_in_pipe(line: str) -> str:
status = str(tokens[7].upper()) if num_without_desc >= 8 else PIPE_STATUS_OPEN
desc = str(tokens[-1]) if has_desc else None
- return str(f"insert into _link (id, type) values ('{id}', 'pipe');insert into pipes (id, node1, node2, length, diameter, roughness, minor_loss, status) values ('{id}', '{node1}', '{node2}', {length}, {diameter}, {roughness}, {minor_loss}, '{status}');")
+ return str(f"insert into network.links (id, link_type, start_node_id, end_node_id) values ({sql_literal(id)}, 'pipe', {sql_literal(node1)}, {sql_literal(node2)});insert into network.pipes (link_id, length, diameter, roughness, minor_loss, status) values ({sql_literal(id)}, {sql_literal(length)}, {sql_literal(diameter)}, {sql_literal(roughness)}, {sql_literal(minor_loss)}, {sql_literal(status)});")
def inp_out_pipe(name: str) -> list[str]:
lines = []
- objs = read_all(name, 'select * from pipes')
+ objs = read_all(name, 'select l.id, l.start_node_id as node1, l.end_node_id as node2, p.length, p.diameter, p.roughness, p.minor_loss, p.status from network.links l join network.pipes p on p.link_id = l.id order by l.id')
for obj in objs:
id = obj['id']
node1 = obj['node1']
@@ -248,13 +252,3 @@ def inp_out_pipe(name: str) -> list[str]:
desc = ';'
lines.append(f'{id} {node1} {node2} {length} {diameter} {roughness} {minor_loss} {status} {desc}')
return lines
-
-
-'''def delete_pipe_by_node(name: str, node: str) -> ChangeSet:
- cs = ChangeSet()
-
- rows = read_all(name, f"select id from pipes where node1 = '{node}' or node2 = '{node}'")
- for row in rows:
- cs.append(g_delete_prefix | {'type': 'pipe', 'id': row['id']})
-
- return cs'''
diff --git a/app/native/wndb/s6_pumps.py b/app/native/wndb/model/pumps.py
similarity index 62%
rename from app/native/wndb/s6_pumps.py
rename to app/native/wndb/model/pumps.py
index 8ac4622..fb2657c 100644
--- a/app/native/wndb/s6_pumps.py
+++ b/app/native/wndb/model/pumps.py
@@ -1,5 +1,16 @@
-from .database import *
-from .s0_base import *
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_add_prefix,
+ g_delete_prefix,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+ try_read,
+)
def get_pump_schema(name: str) -> dict[str, dict[str, Any]]:
@@ -13,7 +24,7 @@ def get_pump_schema(name: str) -> dict[str, dict[str, Any]]:
def get_pump(name: str, id: str) -> dict[str, Any]:
- p = try_read(name, f"select * from pumps where id = '{id}'")
+ p = try_read(name, "select l.id, l.start_node_id as node1, l.end_node_id as node2, p.power, p.head_curve_id as head, p.speed, p.pattern_id as pattern from network.links l join network.pumps p on p.link_id = l.id where l.id = %s", (id,))
if p == None:
return {}
d = {}
@@ -28,7 +39,12 @@ def get_pump(name: str, id: str) -> dict[str, Any]:
# DingZQ, 2025-03-29
def get_all_pumps(name: str) -> list[dict[str, Any]]:
- rows = read_all(name, f"select * from pumps")
+ rows = read_all(
+ name,
+ "SELECT id, start_node_id AS node1, end_node_id AS node2, power, "
+ "head_curve_id AS head, speed, pattern_id AS pattern "
+ "FROM gis.pumps ORDER BY id",
+ )
if rows == None:
return []
@@ -59,24 +75,19 @@ class Pump(object):
self.speed = float(input['speed']) if 'speed' in input and input['speed'] != None else None
self.pattern = str(input['pattern']) if 'pattern' in input and input['pattern'] != None else None
- self.f_type = f"'{self.type}'"
- self.f_id = f"'{self.id}'"
- self.f_node1 = f"'{self.node1}'"
- self.f_node2 = f"'{self.node2}'"
- self.f_power = self.power if self.power != None else 'null'
- self.f_head = f"'{self.head}'" if self.head != None else 'null'
- self.f_speed = self.speed if self.speed != None else 'null'
- self.f_pattern = f"'{self.pattern}'" if self.pattern != None else 'null'
+ self.f_type = sql_literal(self.type)
+ self.f_id = sql_literal(self.id)
+ self.f_node1 = sql_literal(self.node1)
+ self.f_node2 = sql_literal(self.node2)
+ self.f_power = sql_literal(self.power)
+ self.f_head = sql_literal(self.head)
+ self.f_speed = sql_literal(self.speed)
+ self.f_pattern = sql_literal(self.pattern)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'id': self.id, 'node1': self.node1, 'node2': self.node2, 'power': self.power, 'head': self.head, 'speed': self.speed, 'pattern': self.pattern }
- def as_id_dict(self) -> dict[str, Any]:
- return { 'type': self.type, 'id': self.id }
-
-
-def _set_pump(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Pump(get_pump(name, cs.operations[0]['id']))
+def _set_pump(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_pump(name, cs.operations[0]['id'])
new_dict = cs.operations[0]
@@ -86,13 +97,11 @@ def _set_pump(name: str, cs: ChangeSet) -> DbChangeSet:
raw_new[key] = new_dict[key]
new = Pump(raw_new)
- redo_sql = f"update pumps set node1 = {new.f_node1}, node2 = {new.f_node2}, power = {new.f_power}, head = {new.f_head}, speed = {new.f_speed}, pattern = {new.f_pattern} where id = {new.f_id};"
- undo_sql = f"update pumps set node1 = {old.f_node1}, node2 = {old.f_node2}, power = {old.f_power}, head = {old.f_head}, speed = {old.f_speed}, pattern = {old.f_pattern} where id = {old.f_id};"
+ statement = f"update network.links set start_node_id = {new.f_node1}, end_node_id = {new.f_node2} where id = {new.f_id};"
+ statement += f"\nupdate network.pumps set power = {new.f_power}, head_curve_id = {new.f_head}, speed = {new.f_speed}, pattern_id = {new.f_pattern} where link_id = {new.f_id};"
+ change = g_update_prefix | new.as_dict()
- redo_cs = g_update_prefix | new.as_dict()
- undo_cs = g_update_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_pump(name: str, cs: ChangeSet) -> ChangeSet:
@@ -103,19 +112,15 @@ def set_pump(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_pump(name, cs))
-def _add_pump(name: str, cs: ChangeSet) -> DbChangeSet:
+def _add_pump(name: str, cs: ChangeSet) -> DatabaseCommand:
new = Pump(cs.operations[0])
- redo_sql = f"insert into _link (id, type) values ({new.f_id}, {new.f_type});"
- redo_sql += f"\ninsert into pumps (id, node1, node2, power, head, speed, pattern) values ({new.f_id}, {new.f_node1}, {new.f_node2}, {new.f_power}, {new.f_head}, {new.f_speed}, {new.f_pattern});"
+ statement = f"insert into network.links (id, link_type, start_node_id, end_node_id) values ({new.f_id}, {new.f_type}, {new.f_node1}, {new.f_node2});"
+ statement += f"\ninsert into network.pumps (link_id, power, head_curve_id, speed, pattern_id) values ({new.f_id}, {new.f_power}, {new.f_head}, {new.f_speed}, {new.f_pattern});"
- undo_sql = f"delete from pumps where id = {new.f_id};"
- undo_sql += f"\ndelete from _link where id = {new.f_id};"
+ change = g_add_prefix | new.as_dict()
- redo_cs = g_add_prefix | new.as_dict()
- undo_cs = g_delete_prefix | new.as_id_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def add_pump(name: str, cs: ChangeSet) -> ChangeSet:
@@ -126,19 +131,15 @@ def add_pump(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _add_pump(name, cs))
-def _delete_pump(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Pump(get_pump(name, cs.operations[0]['id']))
+def _delete_pump(name: str, cs: ChangeSet) -> DatabaseCommand:
+ element_id = str(cs.operations[0]['id'])
+ f_id = sql_literal(element_id)
- redo_sql = f"delete from pumps where id = {old.f_id};"
- redo_sql += f"\ndelete from _link where id = {old.f_id};"
+ statement = f"delete from network.links where id = {f_id};"
- undo_sql = f"insert into _link (id, type) values ({old.f_id}, {old.f_type});"
- undo_sql += f"\ninsert into pumps (id, node1, node2, power, head, speed, pattern) values ({old.f_id}, {old.f_node1}, {old.f_node2}, {old.f_power}, {old.f_head}, {old.f_speed}, {old.f_pattern});"
+ change = g_delete_prefix | {'type': 'pump', 'id': element_id}
- redo_cs = g_delete_prefix | old.as_id_dict()
- undo_cs = g_add_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def delete_pump(name: str, cs: ChangeSet) -> ChangeSet:
@@ -170,21 +171,17 @@ def inp_in_pump(line: str) -> str:
for i in range(3, num_without_desc, 2):
props |= { tokens[i].lower(): tokens[i + 1] }
power = float(props['power']) if 'power' in props else None
- power = power if power != None else 'null'
head = str(props['head']) if 'head' in props else None
- head = f"'{head}'" if head != None else 'null'
speed = float(props['speed']) if 'speed' in props else None
- speed = speed if speed != None else 'null'
pattern = str(props['pattern']) if 'pattern' in props else None
- pattern = f"'{pattern}'" if pattern != None else 'null'
desc = str(tokens[-1]) if has_desc else None
- return str(f"insert into _link (id, type) values ('{id}', 'pump');insert into pumps (id, node1, node2, power, head, speed, pattern) values ('{id}', '{node1}', '{node2}', {power}, {head}, {speed}, {pattern});")
+ return str(f"insert into network.links (id, link_type, start_node_id, end_node_id) values ({sql_literal(id)}, 'pump', {sql_literal(node1)}, {sql_literal(node2)});insert into network.pumps (link_id, power, head_curve_id, speed, pattern_id) values ({sql_literal(id)}, {sql_literal(power)}, {sql_literal(head)}, {sql_literal(speed)}, {sql_literal(pattern)});")
def inp_out_pump(name: str) -> list[str]:
lines = []
- objs = read_all(name, 'select * from pumps')
+ objs = read_all(name, 'select l.id, l.start_node_id as node1, l.end_node_id as node2, p.power, p.head_curve_id as head, p.speed, p.pattern_id as pattern from network.links l join network.pumps p on p.link_id = l.id order by l.id')
for obj in objs:
id = obj['id']
node1 = obj['node1']
@@ -198,20 +195,10 @@ def inp_out_pump(name: str) -> list[str]:
return lines
-'''def delete_pump_by_node(name: str, node: str) -> ChangeSet:
- cs = ChangeSet()
-
- rows = read_all(name, f"select id from pumps where node1 = '{node}' or node2 = '{node}'")
- for row in rows:
- cs.append(g_delete_prefix | {'type': 'pump', 'id': row['id']})
-
- return cs'''
-
-
def unset_pump_by_curve(name: str, curve: str) -> ChangeSet:
cs = ChangeSet()
- rows = read_all(name, f"select * from pumps where head = '{curve}'")
+ rows = read_all(name, "select link_id as id, power from network.pumps where head_curve_id = %s", (curve,))
for row in rows:
if row['power'] != None:
cs.append(g_update_prefix | {'type': 'pump', 'id': row['id'], 'head': None})
@@ -224,7 +211,7 @@ def unset_pump_by_curve(name: str, curve: str) -> ChangeSet:
def unset_pump_by_pattern(name: str, pattern: str) -> ChangeSet:
cs = ChangeSet()
- rows = read_all(name, f"select id from pumps where pattern = '{pattern}'")
+ rows = read_all(name, "select link_id as id from network.pumps where pattern_id = %s", (pattern,))
for row in rows:
cs.append(g_update_prefix | {'type': 'pump', 'id': row['id'], 'pattern': None})
diff --git a/app/native/wndb/s17_quality.py b/app/native/wndb/model/quality.py
similarity index 65%
rename from app/native/wndb/s17_quality.py
rename to app/native/wndb/model/quality.py
index 539b205..e8d6c8d 100644
--- a/app/native/wndb/s17_quality.py
+++ b/app/native/wndb/model/quality.py
@@ -1,4 +1,14 @@
-from .database import *
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+ try_read,
+)
def get_quality_schema(name: str) -> dict[str, dict[str, Any]]:
@@ -7,7 +17,7 @@ def get_quality_schema(name: str) -> dict[str, dict[str, Any]]:
def get_quality(name: str, node: str) -> dict[str, Any]:
- e = try_read(name, f"select * from quality where node = '{node}'")
+ e = try_read(name, "select node_id as node, value as quality from network.initial_quality where node_id = %s", (node,))
if e == None:
return { 'node': node, 'quality': None }
d = {}
@@ -22,16 +32,15 @@ class Quality(object):
self.node = str(input['node'])
self.quality = float(input['quality']) if 'quality' in input and input['quality'] != None else None
- self.f_type = f"'{self.type}'"
- self.f_node = f"'{self.node}'"
- self.f_quality = self.quality if self.quality != None else 'null'
+ self.f_type = sql_literal(self.type)
+ self.f_node = sql_literal(self.node)
+ self.f_quality = sql_literal(self.quality)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'node': self.node, 'quality': self.quality }
-def _set_quality(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Quality(get_quality(name, cs.operations[0]['node']))
+def _set_quality(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_quality(name, cs.operations[0]['node'])
new_dict = cs.operations[0]
@@ -41,18 +50,13 @@ def _set_quality(name: str, cs: ChangeSet) -> DbChangeSet:
raw_new[key] = new_dict[key]
new = Quality(raw_new)
- redo_sql = f"delete from quality where node = {new.f_node};"
+ statement = f"delete from network.initial_quality where node_id = {new.f_node};"
if new.quality != None:
- redo_sql += f"\ninsert into quality (node, quality) values ({new.f_node}, {new.f_quality});"
+ statement += f"\ninsert into network.initial_quality (node_id, value) values ({new.f_node}, {new.f_quality});"
- undo_sql = f"delete from quality where node = {old.f_node};"
- if old.quality != None:
- undo_sql += f"\ninsert into quality (node, quality) values ({old.f_node}, {old.f_quality});"
+ change = g_update_prefix | new.as_dict()
- redo_cs = g_update_prefix | new.as_dict()
- undo_cs = g_update_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_quality(name: str, cs: ChangeSet) -> ChangeSet:
@@ -75,12 +79,12 @@ def inp_in_quality(line: str) -> str:
node = str(tokens[0])
quality = float(tokens[1])
- return str(f"insert into quality (node, quality) values ('{node}', {quality});")
+ return str(f"insert into network.initial_quality (node_id, value) values ({sql_literal(node)}, {sql_literal(quality)});")
def inp_out_quality(name: str) -> list[str]:
lines = []
- objs = read_all(name, 'select * from quality')
+ objs = read_all(name, 'select node_id as node, value as quality from network.initial_quality order by node_id')
for obj in objs:
node = obj['node']
quality = obj['quality']
@@ -89,7 +93,7 @@ def inp_out_quality(name: str) -> list[str]:
def delete_quality_by_node(name: str, node: str) -> ChangeSet:
- row = try_read(name, f"select * from quality where node = '{node}'")
+ row = try_read(name, "select 1 from network.initial_quality where node_id = %s", (node,))
if row == None:
return ChangeSet()
return ChangeSet(g_update_prefix | {'type' : 'quality', 'node': node, 'quality': None})
diff --git a/app/native/wndb/s19_reactions.py b/app/native/wndb/model/reactions.py
similarity index 56%
rename from app/native/wndb/s19_reactions.py
rename to app/native/wndb/model/reactions.py
index bf0faf5..1f675fd 100644
--- a/app/native/wndb/s19_reactions.py
+++ b/app/native/wndb/model/reactions.py
@@ -1,4 +1,14 @@
-from .database import *
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+ try_read,
+)
element_schema = {'type': 'str' , 'optional': True , 'readonly': False}
@@ -15,45 +25,32 @@ def get_reaction_schema(name: str) -> dict[str, dict[str, Any]]:
def get_reaction(name: str) -> dict[str, Any]:
- ts = read_all(name, f"select * from reactions")
+ ts = read_all(name, "select key, value from network.reaction_settings")
d = {}
for e in ts:
d[e['key']] = str(e['value'])
return d
-def _set_reaction(name: str, cs: ChangeSet) -> DbChangeSet:
- raw_old = get_reaction(name)
-
- old = {}
+def _set_reaction(name: str, cs: ChangeSet) -> DatabaseCommand:
new = {}
new_dict = cs.operations[0]
schema = get_reaction_schema(name)
for key in schema.keys():
if key in new_dict:
- old[key] = str(raw_old[key])
new[key] = str(new_dict[key])
- redo_cs = g_update_prefix | { 'type' : 'reaction' }
+ change = g_update_prefix | { 'type' : 'reaction' }
- redo_sql = ''
+ statement = ''
for key, value in new.items():
- if redo_sql != '':
- redo_sql += '\n'
- redo_sql += f"update reactions set value = '{value}' where key = '{key}';"
- redo_cs |= { key: value }
+ if statement != '':
+ statement += '\n'
+ statement += f"update network.reaction_settings set value = {sql_literal(value)} where key = {sql_literal(key)};"
+ change |= { key: value }
- undo_cs = g_update_prefix | { 'type' : 'reaction' }
-
- undo_sql = ''
- for key, value in old.items():
- if undo_sql != '':
- undo_sql += '\n'
- undo_sql += f"update reactions set value = '{value}' where key = '{key}';"
- undo_cs |= { key: value }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_reaction(name: str, cs: ChangeSet) -> ChangeSet:
@@ -69,10 +66,9 @@ def get_pipe_reaction_schema(name: str) -> dict[str, dict[str, Any]]:
def get_pipe_reaction(name: str, pipe: str) -> dict[str, Any]:
d = {}
d['pipe'] = pipe
- pr = try_read(name, f"select * from reactions_pipe_bulk where pipe = '{pipe}'")
- d['bulk'] = float(pr['value']) if pr != None else None
- pr = try_read(name, f"select * from reactions_pipe_wall where pipe = '{pipe}'")
- d['wall'] = float(pr['value']) if pr != None else None
+ pr = try_read(name, "select bulk_coefficient as bulk, wall_coefficient as wall from network.pipe_reaction_coefficients where pipe_id = %s", (pipe,))
+ d['bulk'] = float(pr['bulk']) if pr is not None and pr['bulk'] is not None else None
+ d['wall'] = float(pr['wall']) if pr is not None and pr['wall'] is not None else None
return d
@@ -83,17 +79,16 @@ class PipeReaction(object):
self.bulk = float(input['bulk']) if 'bulk' in input and input['bulk'] != None else None
self.wall = float(input['wall']) if 'wall' in input and input['wall'] != None else None
- self.f_type = f"'{self.type}'"
- self.f_pipe = f"'{self.pipe}'"
- self.f_bulk = self.bulk if self.bulk != None else 'null'
- self.f_wall = self.wall if self.wall != None else 'null'
+ self.f_type = sql_literal(self.type)
+ self.f_pipe = sql_literal(self.pipe)
+ self.f_bulk = sql_literal(self.bulk)
+ self.f_wall = sql_literal(self.wall)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'pipe': self.pipe, 'bulk': self.bulk, 'wall': self.wall }
-def _set_pipe_reaction(name: str, cs: ChangeSet) -> DbChangeSet:
- old = PipeReaction(get_pipe_reaction(name, cs.operations[0]['pipe']))
+def _set_pipe_reaction(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_pipe_reaction(name, cs.operations[0]['pipe'])
new_dict = cs.operations[0]
@@ -103,22 +98,13 @@ def _set_pipe_reaction(name: str, cs: ChangeSet) -> DbChangeSet:
raw_new[key] = new_dict[key]
new = PipeReaction(raw_new)
- redo_sql = f"delete from reactions_pipe_bulk where pipe = {new.f_pipe};\ndelete from reactions_pipe_wall where pipe = {new.f_pipe};"
- if new.bulk != None:
- redo_sql += f"\ninsert into reactions_pipe_bulk (pipe, value) values ({new.f_pipe}, {new.f_bulk});"
- if new.wall != None:
- redo_sql += f"\ninsert into reactions_pipe_wall (pipe, value) values ({new.f_pipe}, {new.f_wall});"
+ statement = f"delete from network.pipe_reaction_coefficients where pipe_id = {new.f_pipe};"
+ if new.bulk is not None or new.wall is not None:
+ statement += f"\ninsert into network.pipe_reaction_coefficients (pipe_id, bulk_coefficient, wall_coefficient) values ({new.f_pipe}, {new.f_bulk}, {new.f_wall});"
- undo_sql = f"delete from reactions_pipe_bulk where pipe = {old.f_pipe};\ndelete from reactions_pipe_wall where pipe = {old.f_pipe};"
- if old.bulk != None:
- undo_sql += f"\ninsert into reactions_pipe_bulk (pipe, value) values ({old.f_pipe}, {old.f_bulk});"
- if old.wall != None:
- undo_sql += f"\ninsert into reactions_pipe_wall (pipe, value) values ({old.f_pipe}, {old.f_wall});"
+ change = g_update_prefix | new.as_dict()
- redo_cs = g_update_prefix | new.as_dict()
- undo_cs = g_update_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_pipe_reaction(name: str, cs: ChangeSet) -> ChangeSet:
@@ -133,8 +119,8 @@ def get_tank_reaction_schema(name: str) -> dict[str, dict[str, Any]]:
def get_tank_reaction(name: str, tank: str) -> dict[str, Any]:
d = {}
d['tank'] = tank
- pr = try_read(name, f"select * from reactions_tank where tank = '{tank}'")
- d['value'] = float(pr['value']) if pr != None else None
+ pr = try_read(name, "select coefficient as value from network.tank_reaction_coefficients where tank_id = %s", (tank,))
+ d['value'] = float(pr['value']) if pr is not None else None
return d
@@ -144,16 +130,15 @@ class TankReaction(object):
self.tank = str(input['tank'])
self.value = float(input['value']) if 'value' in input and input['value'] != None else None
- self.f_type = f"'{self.type}'"
- self.f_tank = f"'{self.tank}'"
- self.f_value = self.value if self.value != None else 'null'
+ self.f_type = sql_literal(self.type)
+ self.f_tank = sql_literal(self.tank)
+ self.f_value = sql_literal(self.value)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'tank': self.tank, 'value': self.value }
-def _set_tank_reaction(name: str, cs: ChangeSet) -> DbChangeSet:
- old = TankReaction(get_tank_reaction(name, cs.operations[0]['tank']))
+def _set_tank_reaction(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_tank_reaction(name, cs.operations[0]['tank'])
new_dict = cs.operations[0]
@@ -163,18 +148,13 @@ def _set_tank_reaction(name: str, cs: ChangeSet) -> DbChangeSet:
raw_new[key] = new_dict[key]
new = TankReaction(raw_new)
- redo_sql = f"delete from reactions_tank where tank = {new.f_tank};"
+ statement = f"delete from network.tank_reaction_coefficients where tank_id = {new.f_tank};"
if new.value != None:
- redo_sql += f"\ninsert into reactions_tank (tank, value) values ({new.f_tank}, {new.f_value});"
+ statement += f"\ninsert into network.tank_reaction_coefficients (tank_id, coefficient) values ({new.f_tank}, {new.f_value});"
- undo_sql = f"delete from reactions_tank where tank = {old.f_tank};"
- if old.value != None:
- undo_sql += f"\ninsert into reactions_tank (tank, value) values ({old.f_tank}, {old.f_value});"
+ change = g_update_prefix | new.as_dict()
- redo_cs = g_update_prefix | new.as_dict()
- undo_cs = g_update_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_tank_reaction(name: str, cs: ChangeSet) -> ChangeSet:
@@ -200,20 +180,21 @@ def inp_in_reaction(line: str) -> str:
if token0 == 'BULK' or token0 == 'WALL':
pipe = tokens[1]
key = token0.lower()
- value = tokens[2]
- return str(f"insert into reactions_pipe_{key} (pipe, value) values ('{pipe}', {value});")
+ value = float(tokens[2])
+ column = 'bulk_coefficient' if key == 'bulk' else 'wall_coefficient'
+ return str(f"insert into network.pipe_reaction_coefficients (pipe_id, {column}) values ({sql_literal(pipe)}, {sql_literal(value)}) on conflict (pipe_id) do update set {column} = excluded.{column};")
elif token0 == 'TANK':
tank = tokens[1]
- value = tokens[2]
- return str(f"insert into reactions_tank (tank, value) values ('{tank}', {value});")
+ value = float(tokens[2])
+ return str(f"insert into network.tank_reaction_coefficients (tank_id, coefficient) values ({sql_literal(tank)}, {sql_literal(value)});")
else:
line = line.upper().strip()
for key in get_reaction_schema('').keys():
if line.startswith(key):
value = line.removeprefix(key).strip()
- return str(f"update reactions set value = '{value}' where key = '{key}';")
+ return str(f"update network.reaction_settings set value = {sql_literal(value)} where key = {sql_literal(key)};")
return str('')
@@ -221,25 +202,25 @@ def inp_in_reaction(line: str) -> str:
def inp_out_reaction(name: str) -> list[str]:
lines = []
- objs = read_all(name, f"select * from reactions")
+ objs = read_all(name, "select key, value from network.reaction_settings order by key")
for obj in objs:
key = obj['key']
value = obj['value']
lines.append(f'{key} {value}')
- objs = read_all(name, f"select * from reactions_pipe_bulk")
+ objs = read_all(name, "select pipe_id as pipe, bulk_coefficient as value from network.pipe_reaction_coefficients where bulk_coefficient is not null order by pipe_id")
for obj in objs:
pipe = obj['pipe']
value = obj['value']
lines.append(f'BULK {pipe} {value}')
- objs = read_all(name, f"select * from reactions_pipe_wall")
+ objs = read_all(name, "select pipe_id as pipe, wall_coefficient as value from network.pipe_reaction_coefficients where wall_coefficient is not null order by pipe_id")
for obj in objs:
pipe = obj['pipe']
value = obj['value']
lines.append(f'WALL {pipe} {value}')
- objs = read_all(name, f"select * from reactions_tank")
+ objs = read_all(name, "select tank_id as tank, coefficient as value from network.tank_reaction_coefficients order by tank_id")
for obj in objs:
tank = obj['tank']
value = obj['value']
@@ -249,15 +230,14 @@ def inp_out_reaction(name: str) -> list[str]:
def delete_pipe_reaction_by_pipe(name: str, pipe: str) -> ChangeSet:
- row1 = try_read(name, f"select * from reactions_pipe_bulk where pipe = '{pipe}'")
- row2 = try_read(name, f"select * from reactions_pipe_wall where pipe = '{pipe}'")
- if row1 == None and row2 == None:
+ row = try_read(name, "select 1 from network.pipe_reaction_coefficients where pipe_id = %s", (pipe,))
+ if row is None:
return ChangeSet()
return ChangeSet(g_update_prefix | {'type': 'pipe_reaction', 'pipe': pipe, 'bulk': None, 'wall': None})
def delete_tank_reaction_by_tank(name: str, tank: str) -> ChangeSet:
- row = try_read(name, f"select * from reactions_tank where tank = '{tank}'")
+ row = try_read(name, "select 1 from network.tank_reaction_coefficients where tank_id = %s", (tank,))
if row == None:
return ChangeSet()
return ChangeSet(g_update_prefix | {'type': 'tank_reaction', 'tank': tank, 'value': None})
diff --git a/app/native/wndb/s22_report.py b/app/native/wndb/model/reports.py
similarity index 85%
rename from app/native/wndb/s22_report.py
rename to app/native/wndb/model/reports.py
index 02b675b..75bfadd 100644
--- a/app/native/wndb/s22_report.py
+++ b/app/native/wndb/model/reports.py
@@ -1,4 +1,4 @@
-from .database import *
+from ..core.database import read_all
#--------------------------------------------------------------
@@ -26,7 +26,7 @@ def inp_in_report(section: list[str]) -> str:
def inp_out_report(name: str) -> list[str]:
lines = []
- objs = read_all(name, f"select * from report")
+ objs = read_all(name, "select key, value from network.report_settings order by key")
for obj in objs:
key = obj['key']
value = obj['value']
diff --git a/app/native/wndb/s3_reservoirs.py b/app/native/wndb/model/reservoirs.py
similarity index 58%
rename from app/native/wndb/s3_reservoirs.py
rename to app/native/wndb/model/reservoirs.py
index 181b707..1e0bceb 100644
--- a/app/native/wndb/s3_reservoirs.py
+++ b/app/native/wndb/model/reservoirs.py
@@ -1,6 +1,23 @@
-from .database import *
-from .s0_base import *
-from .s24_coordinates import *
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_add_prefix,
+ g_delete_prefix,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+ try_read,
+)
+from ..gis.coordinates import (
+ get_node_coord,
+ sql_delete_coord,
+ sql_insert_coord,
+ sql_update_coord,
+)
+from .elements import get_all_node_links, get_node_links
def get_reservoir_schema(name: str) -> dict[str, dict[str, Any]]:
@@ -13,7 +30,7 @@ def get_reservoir_schema(name: str) -> dict[str, dict[str, Any]]:
def get_reservoir(name: str, id: str) -> dict[str, Any]:
- r = try_read(name, f"select * from reservoirs where id = '{id}'")
+ r = try_read(name, "select node_id as id, head, pattern_id as pattern from network.reservoirs where node_id = %s", (id,))
if r == None:
return {}
xy = get_node_coord(name, id)
@@ -28,21 +45,25 @@ def get_reservoir(name: str, id: str) -> dict[str, Any]:
# DingZQ, 2025-03-29
def get_all_reservoirs(name: str) -> list[dict[str, Any]]:
- rows = read_all(name, f"select * from reservoirs")
+ rows = read_all(
+ name,
+ "SELECT id, head, pattern_id AS pattern, x, y "
+ "FROM gis.reservoirs ORDER BY id",
+ )
if rows == None:
return []
+ links_by_node = get_all_node_links(name)
result = []
for row in rows:
d = {}
id = str(row['id'])
- xy = get_node_coord(name, id)
d['id'] = id
- d['x'] = float(xy['x'])
- d['y'] = float(xy['y'])
+ d['x'] = float(row['x'] or 0.0)
+ d['y'] = float(row['y'] or 0.0)
d['head'] = float(row['head']) if row['head'] != None else None
d['pattern'] = str(row['pattern']) if row['pattern'] != None else None
- d['links'] = get_node_links(name, id)
+ d['links'] = links_by_node.get(id, [])
result.append(d)
return result
@@ -56,20 +77,15 @@ class Reservoir(object):
self.head = float(input['head'])
self.pattern = str(input['pattern']) if 'pattern' in input and input['pattern'] != None else None
- self.f_type = f"'{self.type}'"
- self.f_id = f"'{self.id}'"
- self.f_head = self.head
- self.f_pattern = f"'{self.pattern}'" if self.pattern != None else 'null'
+ self.f_type = sql_literal(self.type)
+ self.f_id = sql_literal(self.id)
+ self.f_head = sql_literal(self.head)
+ self.f_pattern = sql_literal(self.pattern)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'id': self.id, 'x': self.x, 'y': self.y, 'head': self.head, 'pattern': self.pattern }
- def as_id_dict(self) -> dict[str, Any]:
- return { 'type': self.type, 'id': self.id }
-
-
-def _set_reservoir(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Reservoir(get_reservoir(name, cs.operations[0]['id']))
+def _set_reservoir(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_reservoir(name, cs.operations[0]['id'])
new_dict = cs.operations[0]
@@ -79,16 +95,12 @@ def _set_reservoir(name: str, cs: ChangeSet) -> DbChangeSet:
raw_new[key] = new_dict[key]
new = Reservoir(raw_new)
- redo_sql = f"update reservoirs set head = {new.f_head}, pattern = {new.f_pattern} where id = {new.f_id};"
- redo_sql += f"\n{sql_update_coord(new.id, new.x, new.y)}"
+ statement = f"update network.reservoirs set head = {new.f_head}, pattern_id = {new.f_pattern} where node_id = {new.f_id};"
+ statement += f"\n{sql_update_coord(new.id, new.x, new.y)}"
- undo_sql = sql_update_coord(old.id, old.x, old.y)
- undo_sql += f"\nupdate reservoirs set head = {old.f_head}, pattern = {old.f_pattern} where id = {old.f_id};"
+ change = g_update_prefix | new.as_dict()
- redo_cs = g_update_prefix | new.as_dict()
- undo_cs = g_update_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_reservoir(name: str, cs: ChangeSet) -> ChangeSet:
@@ -99,21 +111,16 @@ def set_reservoir(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_reservoir(name, cs))
-def _add_reservoir(name: str, cs: ChangeSet) -> DbChangeSet:
+def _add_reservoir(name: str, cs: ChangeSet) -> DatabaseCommand:
new = Reservoir(cs.operations[0])
- redo_sql = f"insert into _node (id, type) values ({new.f_id}, {new.f_type});"
- redo_sql += f"\ninsert into reservoirs (id, head, pattern) values ({new.f_id}, {new.f_head}, {new.f_pattern});"
- redo_sql += f"\n{sql_insert_coord(new.id, new.x, new.y)}"
+ statement = f"insert into network.nodes (id, node_type) values ({new.f_id}, {new.f_type});"
+ statement += f"\ninsert into network.reservoirs (node_id, head, pattern_id) values ({new.f_id}, {new.f_head}, {new.f_pattern});"
+ statement += f"\n{sql_insert_coord(new.id, new.x, new.y)}"
- undo_sql = sql_delete_coord(new.id)
- undo_sql += f"\ndelete from reservoirs where id = {new.f_id};"
- undo_sql += f"\ndelete from _node where id = {new.f_id};"
+ change = g_add_prefix | new.as_dict()
- redo_cs = g_add_prefix | new.as_dict()
- undo_cs = g_delete_prefix | new.as_id_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def add_reservoir(name: str, cs: ChangeSet) -> ChangeSet:
@@ -124,21 +131,16 @@ def add_reservoir(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _add_reservoir(name, cs))
-def _delete_reservoir(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Reservoir(get_reservoir(name, cs.operations[0]['id']))
+def _delete_reservoir(name: str, cs: ChangeSet) -> DatabaseCommand:
+ element_id = str(cs.operations[0]['id'])
+ f_id = sql_literal(element_id)
- redo_sql = sql_delete_coord(old.id)
- redo_sql += f"\ndelete from reservoirs where id = {old.f_id};"
- redo_sql += f"\ndelete from _node where id = {old.f_id};"
+ statement = sql_delete_coord(element_id)
+ statement += f"\ndelete from network.nodes where id = {f_id};"
- undo_sql = f"insert into _node (id, type) values ({old.f_id}, {old.f_type});"
- undo_sql += f"\ninsert into reservoirs (id, head, pattern) values ({old.f_id}, {old.f_head}, {old.f_pattern});"
- undo_sql += f"\n{sql_insert_coord(old.id, old.x, old.y)}"
+ change = g_delete_prefix | {'type': 'reservoir', 'id': element_id}
- redo_cs = g_delete_prefix | old.as_id_dict()
- undo_cs = g_add_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def delete_reservoir(name: str, cs: ChangeSet) -> ChangeSet:
@@ -165,15 +167,14 @@ def inp_in_reservoir(line: str) -> str:
id = str(tokens[0])
head = float(tokens[1])
pattern = str(tokens[2]) if num_without_desc >= 3 else None
- pattern = f"'{pattern}'" if pattern != None else 'null'
desc = str(tokens[-1]) if has_desc else None
- return str(f"insert into _node (id, type) values ('{id}', 'reservoir');insert into reservoirs (id, head, pattern) values ('{id}', {head}, {pattern});")
+ return str(f"insert into network.nodes (id, node_type) values ({sql_literal(id)}, 'reservoir');insert into network.reservoirs (node_id, head, pattern_id) values ({sql_literal(id)}, {sql_literal(head)}, {sql_literal(pattern)});")
def inp_out_reservoir(name: str) -> list[str]:
lines = []
- objs = read_all(name, 'select * from reservoirs')
+ objs = read_all(name, 'select node_id as id, head, pattern_id as pattern from network.reservoirs order by node_id')
for obj in objs:
id = obj['id']
head = obj['head']
@@ -186,7 +187,7 @@ def inp_out_reservoir(name: str) -> list[str]:
def unset_reservoir_by_pattern(name: str, pattern: str) -> ChangeSet:
cs = ChangeSet()
- rows = read_all(name, f"select id from reservoirs where pattern = '{pattern}'")
+ rows = read_all(name, "select node_id as id from network.reservoirs where pattern_id = %s", (pattern,))
for row in rows:
cs.append(g_update_prefix | {'type': 'reservoir', 'id': row['id'], 'pattern': None})
diff --git a/app/native/wndb/model/rules.py b/app/native/wndb/model/rules.py
new file mode 100644
index 0000000..3dd4c25
--- /dev/null
+++ b/app/native/wndb/model/rules.py
@@ -0,0 +1,50 @@
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+)
+
+
+def get_rule_schema(name: str) -> dict[str, dict[str, Any]]:
+ return { 'rules' : {'type': 'str_list' , 'optional': False , 'readonly': False} }
+
+
+def get_rule(name: str) -> dict[str, Any]:
+ cs = read_all(name, "select line from network.rules order by sequence_no")
+ ds = []
+ for c in cs:
+ ds.append(c['line'])
+ return { 'rules': ds }
+
+
+def _set_rule(name: str, cs: ChangeSet) -> DatabaseCommand:
+ statement = 'delete from network.rules;'
+ for sequence_no, line in enumerate(cs.operations[0]['rules']):
+ statement += f"\ninsert into network.rules (sequence_no, line) values ({sequence_no}, {sql_literal(line)});"
+
+ change = g_update_prefix | { 'type': 'rule', 'rules': cs.operations[0]['rules'] }
+
+ return DatabaseCommand(statement, [change])
+
+
+def set_rule(name: str, cs: ChangeSet) -> ChangeSet:
+ return execute_command(name, _set_rule(name, cs))
+
+
+#--------------------------------------------------------------
+# [EPA2][EPA3]
+# TODO...
+#--------------------------------------------------------------
+
+
+def inp_in_rule(line: str) -> str:
+ return str(f"insert into network.rules (sequence_no, line) values ((select coalesce(max(sequence_no) + 1, 0) from network.rules), {sql_literal(line)});")
+
+
+def inp_out_rule(name: str) -> list[str]:
+ return get_rule(name)['rules']
diff --git a/app/native/wndb/s18_sources.py b/app/native/wndb/model/sources.py
similarity index 58%
rename from app/native/wndb/s18_sources.py
rename to app/native/wndb/model/sources.py
index 0abaeda..327cee5 100644
--- a/app/native/wndb/s18_sources.py
+++ b/app/native/wndb/model/sources.py
@@ -1,5 +1,16 @@
-from .database import *
-from .s0_base import *
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_add_prefix,
+ g_delete_prefix,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+ try_read,
+)
SOURCE_TYPE_CONCEN = 'CONCEN'
SOURCE_TYPE_MASS = 'MASS'
@@ -14,7 +25,7 @@ def get_source_schema(name: str) -> dict[str, dict[str, Any]]:
def get_source(name: str, node: str) -> dict[str, Any]:
- s = try_read(name, f"select * from sources where node = '{node}'")
+ s = try_read(name, "select node_id as node, source_type as s_type, strength, pattern_id as pattern from network.sources where node_id = %s", (node,))
if s == None:
return {}
d = {}
@@ -33,21 +44,16 @@ class Source(object):
self.strength = float(input['strength'])
self.pattern = str(input['pattern']) if 'pattern' in input and input['pattern'] != None else None
- self.f_type = f"'{self.type}'"
- self.f_node = f"'{self.node}'"
- self.f_s_type = f"'{self.s_type}'"
- self.f_strength = self.strength
- self.f_pattern = f"'{self.pattern}'" if self.pattern != None else 'null'
+ self.f_type = sql_literal(self.type)
+ self.f_node = sql_literal(self.node)
+ self.f_s_type = sql_literal(self.s_type)
+ self.f_strength = sql_literal(self.strength)
+ self.f_pattern = sql_literal(self.pattern)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'node': self.node, 's_type': self.s_type, 'strength': self.strength, 'pattern': self.pattern }
- def as_id_dict(self) -> dict[str, Any]:
- return { 'type': self.type, 'node': self.node }
-
-
-def _set_source(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Source(get_source(name, cs.operations[0]['node']))
+def _set_source(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_source(name, cs.operations[0]['node'])
new_dict = cs.operations[0]
@@ -57,45 +63,40 @@ def _set_source(name: str, cs: ChangeSet) -> DbChangeSet:
raw_new[key] = new_dict[key]
new = Source(raw_new)
- redo_sql = f"update sources set s_type = {new.f_s_type}, strength = {new.f_strength}, pattern = {new.f_pattern} where node = {new.f_node};"
- undo_sql = f"update sources set s_type = {old.f_s_type}, strength = {old.f_strength}, pattern = {old.f_pattern} where node = {old.f_node};"
+ statement = f"update network.sources set source_type = {new.f_s_type}, strength = {new.f_strength}, pattern_id = {new.f_pattern} where node_id = {new.f_node};"
- redo_cs = g_update_prefix | new.as_dict()
- undo_cs = g_update_prefix | old.as_dict()
+ change = g_update_prefix | new.as_dict()
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_source(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_source(name, cs))
-def _add_source(name: str, cs: ChangeSet) -> DbChangeSet:
+def _add_source(name: str, cs: ChangeSet) -> DatabaseCommand:
new = Source(cs.operations[0])
- redo_sql = f"insert into sources (node, s_type, strength, pattern) values ({new.f_node}, {new.f_s_type}, {new.f_strength}, {new.f_pattern});"
- undo_sql = f"delete from sources where node = {new.f_node};"
+ statement = f"insert into network.sources (node_id, source_type, strength, pattern_id) values ({new.f_node}, {new.f_s_type}, {new.f_strength}, {new.f_pattern});"
- redo_cs = g_add_prefix | new.as_dict()
- undo_cs = g_delete_prefix | new.as_id_dict()
+ change = g_add_prefix | new.as_dict()
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def add_source(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _add_source(name, cs))
-def _delete_source(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Source(get_source(name, cs.operations[0]['node']))
+def _delete_source(name: str, cs: ChangeSet) -> DatabaseCommand:
+ node = str(cs.operations[0]['node'])
+ f_node = sql_literal(node)
- redo_sql = f"delete from sources where node = {old.f_node};"
- undo_sql = f"insert into sources (node, s_type, strength, pattern) values ({old.f_node}, {old.f_s_type}, {old.f_strength}, {old.f_pattern});"
+ statement = f"delete from network.sources where node_id = {f_node};"
- redo_cs = g_delete_prefix | old.as_id_dict()
- undo_cs = g_add_prefix | old.as_dict()
+ change = g_delete_prefix | {'type': 'source', 'node': node}
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def delete_source(name: str, cs: ChangeSet) -> ChangeSet:
@@ -119,14 +120,12 @@ def inp_in_source(line: str) -> str:
s_type = str(tokens[1].upper())
strength = float(tokens[2])
pattern = str(tokens[3]) if num_without_desc >= 4 else None
- pattern = f"'{pattern}'" if pattern != None else 'null'
-
- return str(f"insert into sources (node, s_type, strength, pattern) values ('{node}', '{s_type}', {strength}, {pattern});")
+ return str(f"insert into network.sources (node_id, source_type, strength, pattern_id) values ({sql_literal(node)}, {sql_literal(s_type)}, {sql_literal(strength)}, {sql_literal(pattern)});")
def inp_out_source(name: str) -> list[str]:
lines = []
- objs = read_all(name, 'select * from sources')
+ objs = read_all(name, 'select node_id as node, source_type as s_type, strength, pattern_id as pattern from network.sources order by node_id')
for obj in objs:
node = obj['node']
s_type = obj['s_type']
@@ -137,7 +136,7 @@ def inp_out_source(name: str) -> list[str]:
def delete_source_by_node(name: str, node: str) -> ChangeSet:
- row = try_read(name, f"select * from sources where node = '{node}'")
+ row = try_read(name, "select 1 from network.sources where node_id = %s", (node,))
if row == None:
return ChangeSet()
return ChangeSet(g_delete_prefix | {'type' : 'source', 'node': node})
@@ -146,7 +145,7 @@ def delete_source_by_node(name: str, node: str) -> ChangeSet:
def unset_source_by_pattern(name: str, pattern: str) -> ChangeSet:
cs = ChangeSet()
- rows = read_all(name, f"select node from sources where pattern = '{pattern}'")
+ rows = read_all(name, "select node_id as node from network.sources where pattern_id = %s", (pattern,))
for row in rows:
cs.append(g_update_prefix | {'type': 'source', 'node': row['node'], 'pattern': None})
diff --git a/app/native/wndb/s10_status.py b/app/native/wndb/model/status.py
similarity index 66%
rename from app/native/wndb/s10_status.py
rename to app/native/wndb/model/status.py
index 33b2a7d..b056c11 100644
--- a/app/native/wndb/s10_status.py
+++ b/app/native/wndb/model/status.py
@@ -1,4 +1,14 @@
-from .database import *
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+ try_read,
+)
LINK_STATUS_OPEN = 'OPEN'
@@ -13,7 +23,7 @@ def get_status_schema(name: str) -> dict[str, dict[str, Any]]:
def get_status(name: str, link: str) -> dict[str, Any]:
- s = try_read(name, f"select * from status where link = '{link}'")
+ s = try_read(name, "select link_id as link, status, setting from network.link_initial_settings where link_id = %s", (link,))
if s == None:
return { 'link': link, 'status': None, 'setting': None }
d = {}
@@ -30,17 +40,16 @@ class Status(object):
self.status = str(input['status']) if 'status' in input and input['status'] != None else None
self.setting = float(input['setting']) if 'setting' in input and input['setting'] != None else None
- self.f_type = f"'{self.type}'"
- self.f_link = f"'{self.link}'"
- self.f_status = f"'{self.status}'" if self.status != None else 'null'
- self.f_setting = self.setting if self.setting != None else 'null'
+ self.f_type = sql_literal(self.type)
+ self.f_link = sql_literal(self.link)
+ self.f_status = sql_literal(self.status)
+ self.f_setting = sql_literal(self.setting)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'link': self.link, 'status': self.status, 'setting': self.setting }
-def _set_status(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Status(get_status(name, cs.operations[0]['link']))
+def _set_status(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_status(name, cs.operations[0]['link'])
new_dict = cs.operations[0]
@@ -50,18 +59,13 @@ def _set_status(name: str, cs: ChangeSet) -> DbChangeSet:
raw_new[key] = new_dict[key]
new = Status(raw_new)
- redo_sql = f"delete from status where link = {new.f_link};"
+ statement = f"delete from network.link_initial_settings where link_id = {new.f_link};"
if new.status != None or new.setting != None:
- redo_sql += f"\ninsert into status (link, status, setting) values ({new.f_link}, {new.f_status}, {new.f_setting});"
+ statement += f"\ninsert into network.link_initial_settings (link_id, status, setting) values ({new.f_link}, {new.f_status}, {new.f_setting});"
- undo_sql = f"delete from status where link = {old.f_link};"
- if old.status != None or old.setting != None:
- undo_sql += f"\ninsert into status (link, status, setting) values ({old.f_link}, {old.f_status}, {old.f_setting});"
+ change = g_update_prefix | new.as_dict()
- redo_cs = g_update_prefix | new.as_dict()
- undo_cs = g_update_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_status(name: str, cs: ChangeSet) -> ChangeSet:
@@ -84,14 +88,14 @@ def inp_in_status(line: str) -> str:
link = str(tokens[0])
value = tokens[1].upper()
if value == LINK_STATUS_OPEN or value == LINK_STATUS_CLOSED or value == LINK_STATUS_ACTIVE:
- return str(f"insert into status (link, status, setting) values ('{link}', '{value}', null);")
+ return str(f"insert into network.link_initial_settings (link_id, status, setting) values ({sql_literal(link)}, {sql_literal(value)}, null);")
else:
- return str(f"insert into status (link, status, setting) values ('{link}', null, {float(value)});")
+ return str(f"insert into network.link_initial_settings (link_id, status, setting) values ({sql_literal(link)}, null, {sql_literal(float(value))});")
def inp_out_status(name: str) -> list[str]:
lines = []
- objs = read_all(name, 'select * from status')
+ objs = read_all(name, 'select link_id as link, status, setting from network.link_initial_settings order by link_id')
for obj in objs:
link = obj['link']
status = obj['status'] if obj['status'] != None else ''
@@ -104,7 +108,7 @@ def inp_out_status(name: str) -> list[str]:
def delete_status_by_link(name: str, link: str) -> ChangeSet:
- row = try_read(name, f"select * from status where link = '{link}'")
+ row = try_read(name, "select 1 from network.link_initial_settings where link_id = %s", (link,))
if row == None:
return ChangeSet()
return ChangeSet(g_update_prefix | {'type': 'status', 'link': link, 'status': None, 'setting': None})
diff --git a/app/native/wndb/model/tags.py b/app/native/wndb/model/tags.py
new file mode 100644
index 0000000..c22d6a8
--- /dev/null
+++ b/app/native/wndb/model/tags.py
@@ -0,0 +1,123 @@
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+ try_read,
+)
+
+
+TAG_TYPE_NODE = "NODE"
+TAG_TYPE_LINK = "LINK"
+
+
+def get_tag_schema(name: str) -> dict[str, dict[str, Any]]:
+ return {
+ "t_type": {"type": "str", "optional": False, "readonly": False},
+ "id": {"type": "str", "optional": False, "readonly": False},
+ "tag": {"type": "str", "optional": True, "readonly": False},
+ }
+
+
+def get_tags(name: str) -> list[dict[str, Any]]:
+ rows = read_all(
+ name,
+ """
+ select 'NODE' as t_type, node_id as id, tag from network.node_tags
+ union all
+ select 'LINK' as t_type, link_id as id, tag from network.link_tags
+ order by t_type, id
+ """,
+ )
+ return [
+ {
+ "t_type": str(row["t_type"]),
+ "id": str(row["id"]),
+ "tag": str(row["tag"]) if row["tag"] is not None else None,
+ }
+ for row in rows
+ ]
+
+
+def _tag_table(t_type: str) -> tuple[str, str]:
+ if t_type == TAG_TYPE_NODE:
+ return "network.node_tags", "node_id"
+ if t_type == TAG_TYPE_LINK:
+ return "network.link_tags", "link_id"
+ raise ValueError("Only NODE and LINK tags are supported")
+
+
+def get_tag(name: str, t_type: str, id: str) -> dict[str, Any]:
+ table, id_column = _tag_table(t_type)
+ row = try_read(
+ name,
+ f"select {id_column} as id, tag from {table} where {id_column} = %s",
+ (id,),
+ )
+ return {
+ "t_type": t_type,
+ "id": id,
+ "tag": str(row["tag"]) if row and row["tag"] is not None else None,
+ }
+
+
+def _replace_tag_sql(t_type: str, element_id: str, tag: str | None) -> str:
+ table, id_column = _tag_table(t_type)
+ element_sql = sql_literal(element_id)
+ statements = [f"delete from {table} where {id_column} = {element_sql};"]
+ if tag is not None:
+ statements.append(
+ f"insert into {table} ({id_column}, tag) "
+ f"values ({element_sql}, {sql_literal(tag)});"
+ )
+ return "\n".join(statements)
+
+
+def _set_tag(name: str, cs: ChangeSet) -> DatabaseCommand:
+ operation = cs.operations[0]
+ t_type = str(operation["t_type"]).upper()
+ element_id = str(operation["id"])
+ new = {"t_type": t_type, "id": element_id, "tag": operation.get("tag")}
+ return DatabaseCommand(
+ _replace_tag_sql(t_type, element_id, new["tag"]),
+ [g_update_prefix | {"type": "tag"} | new],
+ )
+
+
+def set_tag(name: str, cs: ChangeSet) -> ChangeSet:
+ if not {"t_type", "id", "tag"} <= cs.operations[0].keys():
+ return ChangeSet()
+ return execute_command(name, _set_tag(name, cs))
+
+
+def inp_in_tag(line: str) -> str:
+ tokens = line.split()
+ if len(tokens) < 3:
+ return ""
+ return _replace_tag_sql(tokens[0].upper(), tokens[1], tokens[2])
+
+
+def inp_out_tag(name: str) -> list[str]:
+ return [f"{row['t_type']} {row['id']} {row['tag']}" for row in get_tags(name)]
+
+
+def delete_tag_by_node(name: str, node: str) -> ChangeSet:
+ row = get_tag(name, TAG_TYPE_NODE, node)
+ return (
+ ChangeSet(g_update_prefix | {"type": "tag"} | row | {"tag": None})
+ if row["tag"] is not None
+ else ChangeSet()
+ )
+
+
+def delete_tag_by_link(name: str, link: str) -> ChangeSet:
+ row = get_tag(name, TAG_TYPE_LINK, link)
+ return (
+ ChangeSet(g_update_prefix | {"type": "tag"} | row | {"tag": None})
+ if row["tag"] is not None
+ else ChangeSet()
+ )
diff --git a/app/native/wndb/s4_tanks.py b/app/native/wndb/model/tanks.py
similarity index 62%
rename from app/native/wndb/s4_tanks.py
rename to app/native/wndb/model/tanks.py
index 306c9d7..d610df7 100644
--- a/app/native/wndb/s4_tanks.py
+++ b/app/native/wndb/model/tanks.py
@@ -1,6 +1,23 @@
-from .database import *
-from .s0_base import *
-from .s24_coordinates import *
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_add_prefix,
+ g_delete_prefix,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+ try_read,
+)
+from ..gis.coordinates import (
+ get_node_coord,
+ sql_delete_coord,
+ sql_insert_coord,
+ sql_update_coord,
+)
+from .elements import get_all_node_links, get_node_links
OVERFLOW_YES = 'YES'
@@ -23,7 +40,7 @@ def get_tank_schema(name: str) -> dict[str, dict[str, Any]]:
def get_tank(name: str, id: str) -> dict[str, Any]:
- t = try_read(name, f"select * from tanks where id = '{id}'")
+ t = try_read(name, "select node_id as id, elevation, initial_level as init_level, minimum_level as min_level, maximum_level as max_level, diameter, minimum_volume as min_vol, volume_curve_id as vol_curve, overflow from network.tanks where node_id = %s", (id,))
if t == None:
return {}
xy = get_node_coord(name, id)
@@ -44,18 +61,24 @@ def get_tank(name: str, id: str) -> dict[str, Any]:
# DingZQ, 2025-03-29
def get_all_tanks(name: str) -> list[dict[str, Any]]:
- rows = read_all(name, f"select * from tanks")
+ rows = read_all(
+ name,
+ "SELECT id, elevation, initial_level AS init_level, "
+ "minimum_level AS min_level, maximum_level AS max_level, diameter, "
+ "minimum_volume AS min_vol, volume_curve_id AS vol_curve, overflow, "
+ "x, y FROM gis.tanks ORDER BY id",
+ )
if rows == None:
return []
+ links_by_node = get_all_node_links(name)
result = []
for row in rows:
d = {}
id = str(row['id'])
- xy = get_node_coord(name, id)
d['id'] = id
- d['x'] = float(xy['x'])
- d['y'] = float(xy['y'])
+ d['x'] = float(row['x'] or 0.0)
+ d['y'] = float(row['y'] or 0.0)
d['elevation'] = float(row['elevation'])
d['init_level'] = float(row['init_level'])
d['min_level'] = float(row['min_level'])
@@ -64,7 +87,7 @@ def get_all_tanks(name: str) -> list[dict[str, Any]]:
d['min_vol'] = float(row['min_vol'])
d['vol_curve'] = str(row['vol_curve']) if row['vol_curve'] != None else None
d['overflow'] = str(row['overflow']) if row['overflow'] != None else None
- d['links'] = get_node_links(name, id)
+ d['links'] = links_by_node.get(id, [])
result.append(d)
return result
@@ -84,26 +107,21 @@ class Tank(object):
self.vol_curve = str(input['vol_curve']) if 'vol_curve' in input and input['vol_curve'] != None else None
self.overflow = str(input['overflow']) if 'overflow' in input and input['overflow'] != None else None
- self.f_type = f"'{self.type}'"
- self.f_id = f"'{self.id}'"
- self.f_elevation = self.elevation
- self.f_init_level = self.init_level
- self.f_min_level = self.min_level
- self.f_max_level = self.max_level
- self.f_diameter = self.diameter
- self.f_min_vol = self.min_vol
- self.f_vol_curve = f"'{self.vol_curve}'" if self.vol_curve != None else 'null'
- self.f_overflow = f"'{self.overflow}'" if self.overflow != None else 'null'
+ self.f_type = sql_literal(self.type)
+ self.f_id = sql_literal(self.id)
+ self.f_elevation = sql_literal(self.elevation)
+ self.f_init_level = sql_literal(self.init_level)
+ self.f_min_level = sql_literal(self.min_level)
+ self.f_max_level = sql_literal(self.max_level)
+ self.f_diameter = sql_literal(self.diameter)
+ self.f_min_vol = sql_literal(self.min_vol)
+ self.f_vol_curve = sql_literal(self.vol_curve)
+ self.f_overflow = sql_literal(self.overflow)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'id': self.id, 'x': self.x, 'y': self.y, 'elevation': self.elevation, 'init_level': self.init_level, 'min_level': self.min_level, 'max_level': self.max_level, 'diameter': self.diameter, 'min_vol': self.min_vol, 'vol_curve': self.vol_curve, 'overflow': self.overflow }
- def as_id_dict(self) -> dict[str, Any]:
- return { 'type': self.type, 'id': self.id }
-
-
-def _set_tank(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Tank(get_tank(name, cs.operations[0]['id']))
+def _set_tank(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_tank(name, cs.operations[0]['id'])
new_dict = cs.operations[0]
@@ -113,16 +131,12 @@ def _set_tank(name: str, cs: ChangeSet) -> DbChangeSet:
raw_new[key] = new_dict[key]
new = Tank(raw_new)
- redo_sql = f"update tanks set elevation = {new.f_elevation}, init_level = {new.f_init_level}, min_level = {new.f_min_level}, max_level = {new.f_max_level}, diameter = {new.f_diameter}, min_vol = {new.f_min_vol}, vol_curve = {new.f_vol_curve}, overflow = {new.f_overflow} where id = {new.f_id};"
- redo_sql += f"\n{sql_update_coord(new.id, new.x, new.y)}"
+ statement = f"update network.tanks set elevation = {new.f_elevation}, initial_level = {new.f_init_level}, minimum_level = {new.f_min_level}, maximum_level = {new.f_max_level}, diameter = {new.f_diameter}, minimum_volume = {new.f_min_vol}, volume_curve_id = {new.f_vol_curve}, overflow = {new.f_overflow} where node_id = {new.f_id};"
+ statement += f"\n{sql_update_coord(new.id, new.x, new.y)}"
- undo_sql = sql_update_coord(old.id, old.x, old.y)
- undo_sql += f"\nupdate tanks set elevation = {old.f_elevation}, init_level = {old.f_init_level}, min_level = {old.f_min_level}, max_level = {old.f_max_level}, diameter = {old.f_diameter}, min_vol = {old.f_min_vol}, vol_curve = {old.f_vol_curve}, overflow = {old.f_overflow} where id = {old.f_id};"
+ change = g_update_prefix | new.as_dict()
- redo_cs = g_update_prefix | new.as_dict()
- undo_cs = g_update_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_tank(name: str, cs: ChangeSet) -> ChangeSet:
@@ -133,21 +147,16 @@ def set_tank(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_tank(name, cs))
-def _add_tank(name: str, cs: ChangeSet) -> DbChangeSet:
+def _add_tank(name: str, cs: ChangeSet) -> DatabaseCommand:
new = Tank(cs.operations[0])
- redo_sql = f"insert into _node (id, type) values ({new.f_id}, {new.f_type});"
- redo_sql += f"\ninsert into tanks (id, elevation, init_level, min_level, max_level, diameter, min_vol, vol_curve, overflow) values ({new.f_id}, {new.f_elevation}, {new.f_init_level}, {new.f_min_level}, {new.f_max_level}, {new.f_diameter}, {new.f_min_vol}, {new.f_vol_curve}, {new.f_overflow});"
- redo_sql += f"\n{sql_insert_coord(new.id, new.x, new.y)}"
+ statement = f"insert into network.nodes (id, node_type) values ({new.f_id}, {new.f_type});"
+ statement += f"\ninsert into network.tanks (node_id, elevation, initial_level, minimum_level, maximum_level, diameter, minimum_volume, volume_curve_id, overflow) values ({new.f_id}, {new.f_elevation}, {new.f_init_level}, {new.f_min_level}, {new.f_max_level}, {new.f_diameter}, {new.f_min_vol}, {new.f_vol_curve}, {new.f_overflow});"
+ statement += f"\n{sql_insert_coord(new.id, new.x, new.y)}"
- undo_sql = sql_delete_coord(new.id)
- undo_sql += f"\ndelete from tanks where id = {new.f_id};"
- undo_sql += f"\ndelete from _node where id = {new.f_id};"
+ change = g_add_prefix | new.as_dict()
- redo_cs = g_add_prefix | new.as_dict()
- undo_cs = g_delete_prefix | new.as_id_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def add_tank(name: str, cs: ChangeSet) -> ChangeSet:
@@ -158,21 +167,16 @@ def add_tank(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _add_tank(name, cs))
-def _delete_tank(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Tank(get_tank(name, cs.operations[0]['id']))
+def _delete_tank(name: str, cs: ChangeSet) -> DatabaseCommand:
+ element_id = str(cs.operations[0]['id'])
+ f_id = sql_literal(element_id)
- redo_sql = sql_delete_coord(old.id)
- redo_sql += f"\ndelete from tanks where id = {old.f_id};"
- redo_sql += f"\ndelete from _node where id = {old.f_id};"
+ statement = sql_delete_coord(element_id)
+ statement += f"\ndelete from network.nodes where id = {f_id};"
- undo_sql = f"insert into _node (id, type) values ({old.f_id}, {old.f_type});"
- undo_sql += f"\ninsert into tanks (id, elevation, init_level, min_level, max_level, diameter, min_vol, vol_curve, overflow) values ({old.f_id}, {old.f_elevation}, {old.f_init_level}, {old.f_min_level}, {old.f_max_level}, {old.f_diameter}, {old.f_min_vol}, {old.f_vol_curve}, {old.f_overflow});"
- undo_sql += f"\n{sql_insert_coord(old.id, old.x, old.y)}"
+ change = g_delete_prefix | {'type': 'tank', 'id': element_id}
- redo_cs = g_delete_prefix | old.as_id_dict()
- undo_cs = g_add_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def delete_tank(name: str, cs: ChangeSet) -> ChangeSet:
@@ -212,17 +216,15 @@ def inp_in_tank(line: str) -> str:
diameter = float(tokens[5])
min_vol = float(tokens[6]) if num_without_desc >= 7 else 0.0
vol_curve = str(tokens[7]) if num_without_desc >= 8 and tokens[7] != '*' else None
- vol_curve = f"'{vol_curve}'" if vol_curve != None else 'null'
overflow = str(tokens[8].upper()) if num_without_desc >= 9 else None
- overflow = f"'{overflow}'" if overflow != None else 'null'
desc = str(tokens[-1]) if has_desc else None
- return str(f"insert into _node (id, type) values ('{id}', 'tank');insert into tanks (id, elevation, init_level, min_level, max_level, diameter, min_vol, vol_curve, overflow) values ('{id}', {elevation}, {init_level}, {min_level}, {max_level}, {diameter}, {min_vol}, {vol_curve}, {overflow});")
+ return str(f"insert into network.nodes (id, node_type) values ({sql_literal(id)}, 'tank');insert into network.tanks (node_id, elevation, initial_level, minimum_level, maximum_level, diameter, minimum_volume, volume_curve_id, overflow) values ({sql_literal(id)}, {sql_literal(elevation)}, {sql_literal(init_level)}, {sql_literal(min_level)}, {sql_literal(max_level)}, {sql_literal(diameter)}, {sql_literal(min_vol)}, {sql_literal(vol_curve)}, {sql_literal(overflow)});")
def inp_out_tank(name: str) -> list[str]:
lines = []
- objs = read_all(name, 'select * from tanks')
+ objs = read_all(name, 'select node_id as id, elevation, initial_level as init_level, minimum_level as min_level, maximum_level as max_level, diameter, minimum_volume as min_vol, volume_curve_id as vol_curve, overflow from network.tanks order by node_id')
for obj in objs:
id = obj['id']
elevation = obj['elevation']
@@ -243,7 +245,7 @@ def inp_out_tank(name: str) -> list[str]:
def unset_tank_by_curve(name: str, curve: str) -> ChangeSet:
cs = ChangeSet()
- rows = read_all(name, f"select id from tanks where vol_curve = '{curve}'")
+ rows = read_all(name, "select node_id as id from network.tanks where volume_curve_id = %s", (curve,))
for row in rows:
cs.append(g_update_prefix | {'type': 'tank', 'id': row['id'], 'vol_curve': None})
diff --git a/app/native/wndb/s21_times.py b/app/native/wndb/model/times.py
similarity index 71%
rename from app/native/wndb/s21_times.py
rename to app/native/wndb/model/times.py
index 44520d8..1777fec 100644
--- a/app/native/wndb/s21_times.py
+++ b/app/native/wndb/model/times.py
@@ -1,112 +1,110 @@
-from .database import *
-
-TIME_STATISTIC_NONE = 'NONE'
-TIME_STATISTIC_AVERAGED = 'AVERAGED'
-TIME_STATISTIC_MINIMUM = 'MINIMUM'
-TIME_STATISTIC_MAXIMUM = 'MAXIMUM'
-TIME_STATISTIC_RANGE = 'RANGE'
-
-element_schema = {'type': 'str' , 'optional': True , 'readonly': False}
-
-def get_time_schema(name: str) -> dict[str, dict[str, Any]]:
- return { 'DURATION' : element_schema,
- 'HYDRAULIC TIMESTEP' : element_schema,
- 'QUALITY TIMESTEP' : element_schema,
- 'RULE TIMESTEP' : element_schema,
- 'PATTERN TIMESTEP' : element_schema,
- 'PATTERN START' : element_schema,
- 'REPORT TIMESTEP' : element_schema,
- 'REPORT START' : element_schema,
- 'START CLOCKTIME' : element_schema,
- 'STATISTIC' : element_schema}
-
-
-def get_time(name: str) -> dict[str, Any]:
- ts = read_all(name, f"select * from times")
- d = {}
- for e in ts:
- d[e['key']] = str(e['value'])
- return d
-
-
-def _set_time(name: str, cs: ChangeSet) -> DbChangeSet:
- raw_old = get_time(name)
-
- old = {}
- new = {}
-
- new_dict = cs.operations[0]
- schema = get_time_schema(name)
- for key in schema.keys():
- if key in new_dict:
- old[key] = str(raw_old[key])
- new[key] = str(new_dict[key])
-
- redo_cs = g_update_prefix | { 'type' : 'time' }
-
- redo_sql = ''
- for key, value in new.items():
- if redo_sql != '':
- redo_sql += '\n'
- redo_sql += f"update times set value = '{value}' where key = '{key}';"
- redo_cs |= { key: value }
-
- undo_cs = g_update_prefix | { 'type' : 'time' }
-
- undo_sql = ''
- for key, value in old.items():
- if undo_sql != '':
- undo_sql += '\n'
- undo_sql += f"update times set value = '{value}' where key = '{key}';"
- undo_cs |= { key: value }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def set_time(name: str, cs: ChangeSet) -> ChangeSet:
- return execute_command(name, _set_time(name, cs))
-
-
-#--------------------------------------------------------------
-# [EPA2][EPA3]
-# STATISTIC {NONE/AVERAGE/MIN/MAX/RANGE}
-# DURATION value (units)
-# HYDRAULIC TIMESTEP value (units)
-# QUALITY TIMESTEP value (units)
-# RULE TIMESTEP value (units)
-# PATTERN TIMESTEP value (units)
-# PATTERN START value (units)
-# REPORT TIMESTEP value (units)
-# REPORT START value (units)
-# START CLOCKTIME value (AM PM)
-# [EPA3] supports [EPA2] keyword
-#--------------------------------------------------------------
-
-
-def inp_in_time(section: list[str]) -> str:
- sql = ''
- for s in section:
- if s.startswith(';'):
- continue
-
- line = s.upper().strip()
-
- # TOTAL DURATION => DURATION
- if line.startswith('TOTAL DURATION'):
- line = line.replace('TOTAL DURATION', 'DURATION')
-
- for key in get_time_schema('').keys():
- if line.startswith(key):
- value = line.removeprefix(key).strip()
- sql += f"update times set value = '{value}' where key = '{key}';"
- return sql
-
-
-def inp_out_time(name: str) -> list[str]:
- lines = []
- objs = read_all(name, f"select * from times")
- for obj in objs:
- key = obj['key']
- value = obj['value']
- lines.append(f'{key} {value}')
- return lines
+from typing import Any
+
+from psycopg import sql
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+)
+
+TIME_STATISTIC_NONE = 'NONE'
+TIME_STATISTIC_AVERAGED = 'AVERAGED'
+TIME_STATISTIC_MINIMUM = 'MINIMUM'
+TIME_STATISTIC_MAXIMUM = 'MAXIMUM'
+TIME_STATISTIC_RANGE = 'RANGE'
+
+element_schema = {'type': 'str' , 'optional': True , 'readonly': False}
+
+def get_time_schema(name: str) -> dict[str, dict[str, Any]]:
+ return { 'DURATION' : element_schema,
+ 'HYDRAULIC TIMESTEP' : element_schema,
+ 'QUALITY TIMESTEP' : element_schema,
+ 'RULE TIMESTEP' : element_schema,
+ 'PATTERN TIMESTEP' : element_schema,
+ 'PATTERN START' : element_schema,
+ 'REPORT TIMESTEP' : element_schema,
+ 'REPORT START' : element_schema,
+ 'START CLOCKTIME' : element_schema,
+ 'STATISTIC' : element_schema}
+
+
+def get_time(name: str) -> dict[str, Any]:
+ ts = read_all(name, "select key, value from network.time_settings")
+ d = {}
+ for e in ts:
+ d[e['key']] = str(e['value'])
+ return d
+
+
+def _set_time(name: str, cs: ChangeSet) -> DatabaseCommand:
+ new = {}
+
+ new_dict = cs.operations[0]
+ schema = get_time_schema(name)
+ for key in schema.keys():
+ if key in new_dict:
+ new[key] = str(new_dict[key])
+
+ change = g_update_prefix | { 'type' : 'time' }
+
+ statement = ''
+ for key, value in new.items():
+ if statement != '':
+ statement += '\n'
+ statement += f"update network.time_settings set value = {sql_literal(value)} where key = {sql_literal(key)};"
+ change |= { key: value }
+
+ return DatabaseCommand(statement, [change])
+
+
+def set_time(name: str, cs: ChangeSet) -> ChangeSet:
+ return execute_command(name, _set_time(name, cs))
+
+
+#--------------------------------------------------------------
+# [EPA2][EPA3]
+# STATISTIC {NONE/AVERAGE/MIN/MAX/RANGE}
+# DURATION value (units)
+# HYDRAULIC TIMESTEP value (units)
+# QUALITY TIMESTEP value (units)
+# RULE TIMESTEP value (units)
+# PATTERN TIMESTEP value (units)
+# PATTERN START value (units)
+# REPORT TIMESTEP value (units)
+# REPORT START value (units)
+# START CLOCKTIME value (AM PM)
+# [EPA3] supports [EPA2] keyword
+#--------------------------------------------------------------
+
+
+def inp_in_time(section: list[str]) -> str:
+ sql = ''
+ for s in section:
+ if s.startswith(';'):
+ continue
+
+ line = s.upper().strip()
+
+ # TOTAL DURATION => DURATION
+ if line.startswith('TOTAL DURATION'):
+ line = line.replace('TOTAL DURATION', 'DURATION')
+
+ for key in get_time_schema('').keys():
+ if line.startswith(key):
+ value = line.removeprefix(key).strip()
+ sql += f"update network.time_settings set value = {sql_literal(value)} where key = {sql_literal(key)};"
+ return sql
+
+
+def inp_out_time(name: str) -> list[str]:
+ lines = []
+ objs = read_all(name, "select key, value from network.time_settings order by key")
+ for obj in objs:
+ key = obj['key']
+ value = obj['value']
+ lines.append(f'{key} {value}')
+ return lines
diff --git a/app/native/wndb/model/title.py b/app/native/wndb/model/title.py
new file mode 100644
index 0000000..93652c5
--- /dev/null
+++ b/app/native/wndb/model/title.py
@@ -0,0 +1,52 @@
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+)
+
+
+def get_title_schema(name: str) -> dict[str, dict[str, Any]]:
+ return {"value": {"type": "str", "optional": False, "readonly": False}}
+
+
+def get_title(name: str) -> dict[str, Any]:
+ rows = read_all(
+ name,
+ "select value from network.model_titles order by sequence_no",
+ )
+ return {"value": "\n".join(str(row["value"]) for row in rows)}
+
+
+def _replace_title_sql(value: str) -> str:
+ statements = ["delete from network.model_titles;"]
+ for sequence_no, line in enumerate(value.split("\n")):
+ statements.append(
+ "insert into network.model_titles (sequence_no, value) "
+ f"values ({sequence_no}, {sql_literal(line)});"
+ )
+ return "\n".join(statements)
+
+
+def _set_title(name: str, cs: ChangeSet) -> DatabaseCommand:
+ new = str(cs.operations[0]["value"])
+ return DatabaseCommand(
+ _replace_title_sql(new),
+ [g_update_prefix | {"type": "title", "value": new}],
+ )
+
+
+def set_title(name: str, cs: ChangeSet) -> ChangeSet:
+ return execute_command(name, _set_title(name, cs))
+
+
+def inp_in_title(section: list[str]) -> str:
+ return _replace_title_sql("\n".join(section))
+
+
+def inp_out_title(name: str) -> list[str]:
+ return str(get_title(name)["value"]).split("\n")
diff --git a/app/native/wndb/s7_valves.py b/app/native/wndb/model/valves.py
similarity index 59%
rename from app/native/wndb/s7_valves.py
rename to app/native/wndb/model/valves.py
index 3192088..687af97 100644
--- a/app/native/wndb/s7_valves.py
+++ b/app/native/wndb/model/valves.py
@@ -1,5 +1,16 @@
-from .database import *
-from .s0_base import *
+from typing import Any
+
+from ..core.database import (
+ ChangeSet,
+ DatabaseCommand,
+ execute_command,
+ g_add_prefix,
+ g_delete_prefix,
+ g_update_prefix,
+ read_all,
+ sql_literal,
+ try_read,
+)
VALVES_TYPE_PRV = 'PRV'
@@ -21,7 +32,7 @@ def get_valve_schema(name: str) -> dict[str, dict[str, Any]]:
def get_valve(name: str, id: str) -> dict[str, Any]:
- p = try_read(name, f"select * from valves where id = '{id}'")
+ p = try_read(name, "select l.id, l.start_node_id as node1, l.end_node_id as node2, v.diameter, v.valve_type as v_type, v.setting, v.minor_loss from network.links l join network.valves v on v.link_id = l.id where l.id = %s", (id,))
if p == None:
return {}
d = {}
@@ -35,7 +46,11 @@ def get_valve(name: str, id: str) -> dict[str, Any]:
return d
def get_all_valves(name: str) -> list[dict[str, Any]]:
- rows = read_all(name, f"select * from valves")
+ rows = read_all(
+ name,
+ "SELECT id, start_node_id AS node1, end_node_id AS node2, diameter, "
+ "valve_type AS v_type, setting, minor_loss FROM gis.valves ORDER BY id",
+ )
if rows == None:
return []
@@ -67,24 +82,19 @@ class Valve(object):
self.setting = str(input['setting'])
self.minor_loss = float(input['minor_loss'])
- self.f_type = f"'{self.type}'"
- self.f_id = f"'{self.id}'"
- self.f_node1 = f"'{self.node1}'"
- self.f_node2 = f"'{self.node2}'"
- self.f_diameter = self.diameter
- self.f_v_type = f"'{self.v_type}'"
- self.f_setting = f"'{self.setting}'"
- self.f_minor_loss = self.minor_loss
+ self.f_type = sql_literal(self.type)
+ self.f_id = sql_literal(self.id)
+ self.f_node1 = sql_literal(self.node1)
+ self.f_node2 = sql_literal(self.node2)
+ self.f_diameter = sql_literal(self.diameter)
+ self.f_v_type = sql_literal(self.v_type)
+ self.f_setting = sql_literal(self.setting)
+ self.f_minor_loss = sql_literal(self.minor_loss)
def as_dict(self) -> dict[str, Any]:
return { 'type': self.type, 'id': self.id, 'node1': self.node1, 'node2': self.node2, 'diameter': self.diameter, 'v_type': self.v_type, 'setting': self.setting, 'minor_loss': self.minor_loss }
- def as_id_dict(self) -> dict[str, Any]:
- return { 'type': self.type, 'id': self.id }
-
-
-def _set_valve(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Valve(get_valve(name, cs.operations[0]['id']))
+def _set_valve(name: str, cs: ChangeSet) -> DatabaseCommand:
raw_new = get_valve(name, cs.operations[0]['id'])
new_dict = cs.operations[0]
@@ -94,13 +104,11 @@ def _set_valve(name: str, cs: ChangeSet) -> DbChangeSet:
raw_new[key] = new_dict[key]
new = Valve(raw_new)
- redo_sql = f"update valves set node1 = {new.f_node1}, node2 = {new.f_node2}, diameter = {new.f_diameter}, v_type = {new.f_v_type}, setting = {new.f_setting}, minor_loss = {new.f_minor_loss} where id = {new.f_id};"
- undo_sql = f"update valves set node1 = {old.f_node1}, node2 = {old.f_node2}, diameter = {old.f_diameter}, v_type = {old.f_v_type}, setting = {old.f_setting}, minor_loss = {old.f_minor_loss} where id = {old.f_id};"
+ statement = f"update network.links set start_node_id = {new.f_node1}, end_node_id = {new.f_node2} where id = {new.f_id};"
+ statement += f"\nupdate network.valves set diameter = {new.f_diameter}, valve_type = {new.f_v_type}, setting = {new.f_setting}, minor_loss = {new.f_minor_loss} where link_id = {new.f_id};"
+ change = g_update_prefix | new.as_dict()
- redo_cs = g_update_prefix | new.as_dict()
- undo_cs = g_update_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def set_valve(name: str, cs: ChangeSet) -> ChangeSet:
@@ -111,19 +119,15 @@ def set_valve(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _set_valve(name, cs))
-def _add_valve(name: str, cs: ChangeSet) -> DbChangeSet:
+def _add_valve(name: str, cs: ChangeSet) -> DatabaseCommand:
new = Valve(cs.operations[0])
- redo_sql = f"insert into _link (id, type) values ({new.f_id}, {new.f_type});"
- redo_sql += f"\ninsert into valves (id, node1, node2, diameter, v_type, setting, minor_loss) values ({new.f_id}, {new.f_node1}, {new.f_node2}, {new.f_diameter}, {new.f_v_type}, {new.f_setting}, {new.f_minor_loss});"
+ statement = f"insert into network.links (id, link_type, start_node_id, end_node_id) values ({new.f_id}, {new.f_type}, {new.f_node1}, {new.f_node2});"
+ statement += f"\ninsert into network.valves (link_id, diameter, valve_type, setting, minor_loss) values ({new.f_id}, {new.f_diameter}, {new.f_v_type}, {new.f_setting}, {new.f_minor_loss});"
- undo_sql = f"delete from valves where id = {new.f_id};"
- undo_sql += f"\ndelete from _link where id = {new.f_id};"
+ change = g_add_prefix | new.as_dict()
- redo_cs = g_add_prefix | new.as_dict()
- undo_cs = g_delete_prefix | new.as_id_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def add_valve(name: str, cs: ChangeSet) -> ChangeSet:
@@ -134,19 +138,15 @@ def add_valve(name: str, cs: ChangeSet) -> ChangeSet:
return execute_command(name, _add_valve(name, cs))
-def _delete_valve(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Valve(get_valve(name, cs.operations[0]['id']))
+def _delete_valve(name: str, cs: ChangeSet) -> DatabaseCommand:
+ element_id = str(cs.operations[0]['id'])
+ f_id = sql_literal(element_id)
- redo_sql = f"delete from valves where id = {old.f_id};"
- redo_sql += f"\ndelete from _link where id = {old.f_id};"
+ statement = f"delete from network.links where id = {f_id};"
- undo_sql = f"insert into _link (id, type) values ({old.f_id}, {old.f_type});"
- undo_sql += f"\ninsert into valves (id, node1, node2, diameter, v_type, setting, minor_loss) values ({old.f_id}, {old.f_node1}, {old.f_node2}, {old.f_diameter}, {old.f_v_type}, {old.f_setting}, {old.f_minor_loss});"
+ change = g_delete_prefix | {'type': 'valve', 'id': element_id}
- redo_cs = g_delete_prefix | old.as_id_dict()
- undo_cs = g_add_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
+ return DatabaseCommand(statement, [change])
def delete_valve(name: str, cs: ChangeSet) -> ChangeSet:
@@ -181,12 +181,12 @@ def inp_in_valve(line: str) -> str:
minor_loss = float(tokens[6]) if len(tokens) >= 7 else 0.0
desc = str(tokens[-1]) if has_desc else None
- return str(f"insert into _link (id, type) values ('{id}', 'valve');insert into valves (id, node1, node2, diameter, v_type, setting, minor_loss) values ('{id}', '{node1}', '{node2}', {diameter}, '{v_type}', '{setting}', {minor_loss});")
+ return str(f"insert into network.links (id, link_type, start_node_id, end_node_id) values ({sql_literal(id)}, 'valve', {sql_literal(node1)}, {sql_literal(node2)});insert into network.valves (link_id, diameter, valve_type, setting, minor_loss) values ({sql_literal(id)}, {sql_literal(diameter)}, {sql_literal(v_type)}, {sql_literal(setting)}, {sql_literal(minor_loss)});")
def inp_out_valve(name: str) -> list[str]:
lines = []
- objs = read_all(name, 'select * from valves')
+ objs = read_all(name, 'select l.id, l.start_node_id as node1, l.end_node_id as node2, v.diameter, v.valve_type as v_type, v.setting, v.minor_loss from network.links l join network.valves v on v.link_id = l.id order by l.id')
for obj in objs:
id = obj['id']
node1 = obj['node1']
@@ -198,13 +198,3 @@ def inp_out_valve(name: str) -> list[str]:
desc = ';'
lines.append(f'{id} {node1} {node2} {diameter} {v_type} {setting} {minor_loss} {desc}')
return lines
-
-
-'''def delete_valve_by_node(name: str, node: str) -> ChangeSet:
- cs = ChangeSet()
-
- rows = read_all(name, f"select id from valves where node1 = '{node}' or node2 = '{node}'")
- for row in rows:
- cs.append(g_delete_prefix | {'type': 'valve', 'id': row['id']})
-
- return cs'''
diff --git a/app/native/wndb/project.py b/app/native/wndb/project.py
deleted file mode 100644
index 2a7bca6..0000000
--- a/app/native/wndb/project.py
+++ /dev/null
@@ -1,192 +0,0 @@
-import os
-import psycopg as pg
-from psycopg import sql
-from psycopg.rows import dict_row
-from .connection import (
- close_connection,
- is_connection_open,
- open_connection,
-)
-from app.core.config import get_pg_config, get_pg_password
-from app.infra.db.project_routing import get_project_pgconn_string
-
-# no undo/redo
-
-_server_databases = ["template0", "template1", "postgres", "project"]
-
-
-def list_project() -> list[str]:
- ps = []
- with pg.connect(conninfo=get_project_pgconn_string(), autocommit=True) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- for p in cur.execute(
- f"select datname from pg_database where datname <> 'postgres' and datname <> 'template0' and datname <> 'template1' and datname <> 'project'"
- ):
- ps.append(p["datname"])
- return ps
-
-
-def have_project(name: str) -> bool:
- with pg.connect(
- conninfo=get_project_pgconn_string(db_name="postgres"), autocommit=True
- ) as conn:
- with conn.cursor() as cur:
- cur.execute("select 1 from pg_database where datname = %s", (name,))
- return cur.fetchone() is not None
-
-
-def copy_project(source: str, new: str) -> None:
- close_connection(source)
-
- with pg.connect(
- conninfo=get_project_pgconn_string(db_name="postgres"), autocommit=True
- ) as admin_conn:
- with admin_conn.cursor() as cur:
- cur.execute(
- "update pg_database set datallowconn = false where datname = %s",
- (source,),
- )
- try:
- cur.execute(
- "select pg_terminate_backend(pid) from pg_stat_activity where datname = %s and pid <> pg_backend_pid()",
- (source,),
- )
- cur.execute(
- sql.SQL("create database {} with template = {}").format(
- sql.Identifier(new), sql.Identifier(source)
- )
- )
- finally:
- cur.execute(
- "update pg_database set datallowconn = true where datname = %s",
- (source,),
- )
-
-
-# 2025-02-07, WMH
-# copyproject会把pg中operation这个表的全部内容也加进去,我们实际项目运行一周后operation这个表会变得特别大,导致CopyProject花费的时间很长,CopyProjectEx把operation的在复制时没有一块复制过去,节省时间
-class CopyProjectEx:
- @staticmethod
- def create_database(connection, new_db):
- with connection.cursor() as cursor:
- cursor.execute(f'create database "{new_db}"')
- connection.commit()
-
- @staticmethod
- def execute_pg_dump(source_db, exclude_table_list):
-
- os.environ["PGPASSWORD"] = get_pg_password() # 设置密码环境变量
- pg_config = get_pg_config()
- host = pg_config["host"]
- port = pg_config["port"]
- user = pg_config["user"]
- dump_command_structure = f"pg_dump -h {host} -p {port} -U {user} -F c -s -f source_db_structure.dump {source_db}"
- os.system(dump_command_structure)
-
- if exclude_table_list is not None:
- exclude_table = " ".join(["-T {}".format(i) for i in exclude_table_list])
- dump_command_db = f"pg_dump -h {host} -p {port} -U {user} -F c -a {exclude_table} -f source_db.dump {source_db}"
- else:
- dump_command_db = f"pg_dump -h {host} -p {port} -U {user} -F c -a -f source_db.dump {source_db}"
- os.system(dump_command_db)
-
- @staticmethod
- def execute_pg_restore(new_db):
- os.environ["PGPASSWORD"] = get_pg_password() # 设置密码环境变量
- pg_config = get_pg_config()
- host = pg_config["host"]
- port = pg_config["port"]
- user = pg_config["user"]
- restore_command_structure = f"pg_restore -h {host} -p {port} -U {user} -d {new_db} source_db_structure.dump"
- os.system(restore_command_structure)
-
- restore_command_db = (
- f"pg_restore -h {host} -p {port} -U {user} -d {new_db} source_db.dump"
- )
- os.system(restore_command_db)
-
- @staticmethod
- def init_operation_table(connection, excluded_table):
- with connection.cursor() as cursor:
- if "operation" in excluded_table:
- insert_query = "insert into operation (id, redo, undo, redo_cs, undo_cs) values (0, '', '', '', '')"
- cursor.execute(insert_query)
-
- if "current_operation" in excluded_table:
- insert_query = "insert into current_operation (id) values (0)"
- cursor.execute(insert_query)
-
- if "restore_operation" in excluded_table:
- insert_query = "insert into restore_operation (id) values (0)"
- cursor.execute(insert_query)
-
- if "batch_operation" in excluded_table:
- insert_query = "insert into batch_operation (id, redo, undo, redo_cs, undo_cs) values (0, '', '', '', '')"
- cursor.execute(insert_query)
-
- if "operation_table" in excluded_table:
- insert_query = (
- "insert into operation_table (option) values ('operation')"
- )
- cursor.execute(insert_query)
- connection.commit()
-
- def __call__(self, source: str, new_db: str, excluded_tables: [str] = None) -> None:
- source_connection = pg.connect(
- conninfo=get_project_pgconn_string(), autocommit=True
- )
-
- self.create_database(source_connection, new_db)
-
- self.execute_pg_dump(source, excluded_tables)
- self.execute_pg_restore(new_db)
- source_connection.close()
-
- new_db_connection = pg.connect(
- conninfo=get_project_pgconn_string(db_name=new_db), autocommit=True
- )
- self.init_operation_table(new_db_connection, excluded_tables)
- new_db_connection.close()
-
-
-def create_project(name: str) -> None:
- return copy_project("project", name)
-
-
-def delete_project(name: str) -> None:
- with pg.connect(conninfo=get_project_pgconn_string(), autocommit=True) as conn:
- with conn.cursor() as cur:
- cur.execute(
- f"select pg_terminate_backend(pid) from pg_stat_activity where datname = '{name}'"
- )
- cur.execute(f'drop database "{name}"')
-
-
-def clean_project(excluded: list[str] = []) -> None:
- projects = list_project()
- with pg.connect(conninfo=get_project_pgconn_string(), autocommit=True) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- row = cur.execute(f"select current_database()").fetchone()
- if row != None:
- current_db = row["current_database"]
- if current_db in projects:
- projects.remove(current_db)
- for project in projects:
- if project in _server_databases or project in excluded:
- continue
- cur.execute(
- f"select pg_terminate_backend(pid) from pg_stat_activity where datname = '{project}'"
- )
- cur.execute(f'drop database "{project}"')
-
-
-def open_project(name: str) -> None:
- open_connection(name)
-
-
-def is_project_open(name: str) -> bool:
- return is_connection_open(name)
-
-
-def close_project(name: str) -> None:
- close_connection(name)
diff --git a/app/native/wndb/s0_base.py b/app/native/wndb/s0_base.py
deleted file mode 100644
index 02ee733..0000000
--- a/app/native/wndb/s0_base.py
+++ /dev/null
@@ -1,281 +0,0 @@
-from psycopg import sql
-from psycopg.rows import dict_row, Row
-from .connection import project_connection
-from .database import read
-from typing import Any
-
-_NODE = '_node'
-_LINK = '_link'
-_CURVE = '_curve'
-_PATTERN = '_pattern'
-_REGION = '_region'
-
-JUNCTION = 'junction'
-RESERVOIR = 'reservoir'
-TANK = 'tank'
-PIPE = 'pipe'
-PUMP = 'pump'
-VALVE = 'valve'
-
-PATTERN = 'pattern'
-CURVE = 'curve'
-
-REGION = 'region'
-
-# DingZQ, 2025-02-05
-'''
- C++ 代码里已经定义了这些 enum 值
-{
- kNothing = -1,
-
- //Node
- kReservoir = 0,
- kTank,
- kJunction,
-
- //Link
- kPipe,
- kPump,
- kValve,
-'''
-ELEMENT_TYPES : dict[str, int] = {
- RESERVOIR : 0,
- TANK : 1,
- JUNCTION : 2,
- PIPE : 3,
- PUMP : 4,
- VALVE : 5,
-}
-
-def _get_from(name: str, id: str, base_type: str) -> Row | None:
- with project_connection(name) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- cur.execute(
- sql.SQL("select * from {} where id = %s").format(
- sql.Identifier(base_type)
- ),
- (id,),
- )
- return cur.fetchone()
-
-
-def is_node(name: str, id: str) -> bool:
- return _get_from(name, id, _NODE) != None
-
-
-def is_junction(name: str, id: str) -> bool:
- row = _get_from(name, id, _NODE)
- return row != None and row['type'] == JUNCTION
-
-
-def is_reservoir(name: str, id: str) -> bool:
- row = _get_from(name, id, _NODE)
- return row != None and row['type'] == RESERVOIR
-
-
-def is_tank(name: str, id: str) -> bool:
- row = _get_from(name, id, _NODE)
- return row != None and row['type'] == TANK
-
-
-def is_link(name: str, id: str) -> bool:
- return _get_from(name, id, _LINK) != None
-
-
-def is_pipe(name: str, id: str) -> bool:
- row = _get_from(name, id, _LINK)
- return row != None and row['type'] == PIPE
-
-
-def is_pump(name: str, id: str) -> bool:
- row = _get_from(name, id, _LINK)
- return row != None and row['type'] == PUMP
-
-
-def is_valve(name: str, id: str) -> bool:
- row = _get_from(name, id, _LINK)
- return row != None and row['type'] == VALVE
-
-# DingZQ, 2025-02-05
-def get_node_type(name: str, node_id: str) -> str:
- row = _get_from(name, node_id, _NODE)
- return row['type']
-
-
-def get_link_type(name: str, link_id: str) -> str:
- row = _get_from(name, link_id, _LINK)
- return row['type']
-
-def get_element_type(name: str, element_id: str) -> str:
- if is_node(name, element_id):
- return get_node_type(name, element_id)
- elif is_link(name, element_id):
- return get_link_type(name, element_id)
- else:
- return None
-
-def get_element_type_value(name: str, element_id: str) -> int:
- return ELEMENT_TYPES[get_element_type(name, element_id)]
-
-def is_curve(name: str, id: str) -> bool:
-
- return _get_from(name, id, _CURVE) != None
-
-
-def is_pattern(name: str, id: str) -> bool:
- return _get_from(name, id, _PATTERN) != None
-
-
-def is_region(name: str, id: str) -> bool:
- return _get_from(name, id, _REGION) != None
-
-
-def _get_all(name: str, base_type: str) -> list[str]:
- ids : list[str] = []
- with project_connection(name) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- cur.execute(f"select id from {base_type} order by id")
- for record in cur:
- ids.append(record['id'])
- return ids
-
-
-def get_nodes(name: str) -> list[str]:
- return _get_all(name, _NODE)
-
-# DingZQ
-def _get_nodes_by_type(name: str, type: str) -> list[str]:
- ids : list[str] = []
- with project_connection(name) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- cur.execute(f"select id from {_NODE} where type = '{type}' order by id")
- for record in cur:
- ids.append(record['id'])
- return ids
-
-# DingZQ
-def get_nodes_id_and_type(name: str) -> dict[str, str]:
- nodes_id_and_type: dict[str, str] = {}
- with project_connection(name) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- cur.execute(f"select id, type from {_NODE} order by id")
- for record in cur:
- nodes_id_and_type[record['id']] = record['type']
- return nodes_id_and_type
-
-# DingZQ 2024-12-31
-def get_major_nodes(name: str, diameter: int) -> list[str]:
- major_nodes_set = set()
- with project_connection(name) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- cur.execute(f"select node1, node2 from pipes where diameter > {diameter}")
- for record in cur:
- major_nodes_set.add(record['node1'])
- major_nodes_set.add(record['node2'])
-
- return list(major_nodes_set)
-
-# DingZQs
-def get_junctions(name: str) -> list[str]:
- return _get_nodes_by_type(name, JUNCTION)
-
-# DingZQ
-def get_reservoirs(name: str) -> list[str]:
- return _get_nodes_by_type(name, RESERVOIR)
-
-# DingZQ
-def get_tanks(name: str) -> list[str]:
- return _get_nodes_by_type(name, TANK)
-
-# DingZQ
-def get_links(name: str) -> list[str]:
- return _get_all(name, _LINK)
-
-# DingZQ
-def _get_links_by_type(name: str, type: str) -> list[str]:
- ids : list[str] = []
- with project_connection(name) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- cur.execute(f"select id from {_LINK} where type = '{type}' order by id")
- for record in cur:
- ids.append(record['id'])
- return ids
-
-# DingZQ
-def get_links_id_and_type(name: str) -> dict[str, str]:
- links_id_and_type: dict[str, str] = {}
- with project_connection(name) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- cur.execute(f"select id, type from {_LINK} order by id")
- for record in cur:
- links_id_and_type[record['id']] = record['type']
- return links_id_and_type
-
-# DingZQ 2024-12-31
-# 获取直径大于800的管道
-def get_major_pipes(name: str, diameter: int) -> list[str]:
- major_pipe_ids: list[str] = []
- with project_connection(name) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- cur.execute(f"select id from pipes where diameter > {diameter} order by id")
- for record in cur:
- major_pipe_ids.append(record['id'])
- return major_pipe_ids
-
-# DingZQ
-def get_pipes(name: str) -> list[str]:
- return _get_links_by_type(name, PIPE)
-
-# DingZQ
-def get_pumps(name: str) -> list[str]:
- return _get_links_by_type(name, PUMP)
-
-# DingZQ
-def get_valves(name: str) -> list[str]:
- return _get_links_by_type(name, VALVE)
-
-
-def get_curves(name: str) -> list[str]:
- return _get_all(name, _CURVE)
-
-
-def get_patterns(name: str) -> list[str]:
- return _get_all(name, _PATTERN)
-
-def get_regions(name: str) -> list[str]:
- return _get_all(name, _REGION)
-
-def get_node_links(name: str, id: str) -> list[str]:
- with project_connection(name) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- links: list[str] = []
- for p in cur.execute(
- "select id from pipes where node1 = %s or node2 = %s", (id, id)
- ).fetchall():
- links.append(p['id'])
- for p in cur.execute(
- "select id from pumps where node1 = %s or node2 = %s", (id, id)
- ).fetchall():
- links.append(p['id'])
- for p in cur.execute(
- "select id from valves where node1 = %s or node2 = %s", (id, id)
- ).fetchall():
- links.append(p['id'])
- return links
-
-
-def get_link_nodes(name: str, id: str) -> list[str]:
- row = {}
- if is_pipe(name, id):
- row = read(name, "select node1, node2 from pipes where id = %s", (id,))
- elif is_pump(name, id):
- row = read(name, "select node1, node2 from pumps where id = %s", (id,))
- elif is_valve(name, id):
- row = read(name, "select node1, node2 from valves where id = %s", (id,))
- return [str(row['node1']), str(row['node2'])]
-
-def get_region_type(name: str, id: str)->str:
- if(is_region(name,id)):
- type = read(name, "select type from _region where id = %s", (id,))
- return type
-
diff --git a/app/native/wndb/s14_rules.py b/app/native/wndb/s14_rules.py
deleted file mode 100644
index b210f71..0000000
--- a/app/native/wndb/s14_rules.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from .database import *
-
-
-def get_rule_schema(name: str) -> dict[str, dict[str, Any]]:
- return { 'rules' : {'type': 'str_list' , 'optional': False , 'readonly': False} }
-
-
-def get_rule(name: str) -> dict[str, Any]:
- cs = read_all(name, f"select * from rules")
- ds = []
- for c in cs:
- ds.append(c['line'])
- return { 'rules': ds }
-
-
-def _set_rule(name: str, cs: ChangeSet) -> DbChangeSet:
- old = get_rule(name)
-
- redo_sql = 'delete from rules;'
- for line in cs.operations[0]['rules']:
- redo_sql += f"\ninsert into rules (line) values ('{line}');"
-
- undo_sql = 'delete from rules;'
- for line in old['rules']:
- undo_sql += f"\ninsert into rules (line) values ('{line}');"
-
- redo_cs = g_update_prefix | { 'type': 'rule', 'rules': cs.operations[0]['rules'] }
- undo_cs = g_update_prefix | { 'type': 'rule', 'rules': old['rules'] }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def set_rule(name: str, cs: ChangeSet) -> ChangeSet:
- return execute_command(name, _set_rule(name, cs))
-
-
-#--------------------------------------------------------------
-# [EPA2][EPA3]
-# TODO...
-#--------------------------------------------------------------
-
-
-def inp_in_rule(line: str) -> str:
- return str(f"insert into rules (line) values ('{line}');")
-
-
-def inp_out_rule(name: str) -> list[str]:
- return get_rule(name)['rules']
\ No newline at end of file
diff --git a/app/native/wndb/s15_energy.py b/app/native/wndb/s15_energy.py
deleted file mode 100644
index 3abb998..0000000
--- a/app/native/wndb/s15_energy.py
+++ /dev/null
@@ -1,240 +0,0 @@
-from .database import *
-
-
-element_schema = {'type': 'str' , 'optional': True , 'readonly': False}
-
-
-def get_energy_schema(name: str) -> dict[str, dict[str, Any]]:
- return { 'GLOBAL PRICE' : element_schema,
- 'GLOBAL PATTERN' : element_schema,
- 'GLOBAL EFFIC' : element_schema,
- 'DEMAND CHARGE' : element_schema }
-
-
-def get_energy(name: str) -> dict[str, Any]:
- ts = read_all(name, f"select * from energy")
- d = {}
- for e in ts:
- d[e['key']] = str(e['value'])
- return d
-
-
-def _set_energy(name: str, cs: ChangeSet) -> DbChangeSet:
- raw_old = get_energy(name)
-
- old = {}
- new = {}
-
- new_dict = cs.operations[0]
- schema = get_energy_schema(name)
- for key in schema.keys():
- if key in new_dict:
- old[key] = str(raw_old[key])
- new[key] = str(new_dict[key])
-
- redo_cs = g_update_prefix | { 'type' : 'energy' }
-
- redo_sql = ''
- for key, value in new.items():
- if redo_sql != '':
- redo_sql += '\n'
- redo_sql += f"update energy set value = '{value}' where key = '{key}';"
- redo_cs |= { key: value }
-
- undo_cs = g_update_prefix | { 'type' : 'energy' }
-
- undo_sql = ''
- for key, value in old.items():
- if undo_sql != '':
- undo_sql += '\n'
- undo_sql += f"update energy set value = '{value}' where key = '{key}';"
- undo_cs |= { key: value }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def set_energy(name: str, cs: ChangeSet) -> ChangeSet:
- return execute_command(name, _set_energy(name, cs))
-
-
-def get_pump_energy_schema(name: str) -> dict[str, dict[str, Any]]:
- return { 'pump' : {'type': 'str' , 'optional': False , 'readonly': True },
- 'price' : {'type': 'float' , 'optional': True , 'readonly': False},
- 'pattern' : {'type': 'str' , 'optional': True , 'readonly': False},
- 'effic' : {'type': 'str' , 'optional': True , 'readonly': False} }
-
-
-def get_pump_energy(name: str, pump: str) -> dict[str, Any]:
- d = {}
- d['pump'] = pump
- pe = try_read(name, f"select * from energy_pump_price where pump = '{pump}'")
- d['price'] = float(pe['price']) if pe != None else None
- pe = try_read(name, f"select * from energy_pump_pattern where pump = '{pump}'")
- d['pattern'] = str(pe['pattern']) if pe != None else None
- pe = try_read(name, f"select * from energy_pump_effic where pump = '{pump}'")
- d['effic'] = str(pe['effic']) if pe != None else None
- return d
-
-
-class PumpEnergy(object):
- def __init__(self, input: dict[str, Any]) -> None:
- self.type = 'pump_energy'
- self.pump = str(input['pump'])
- self.price = float(input['price']) if 'price' in input and input['price'] != None else None
- self.pattern = str(input['pattern']) if 'pattern' in input and input['pattern'] != None else None
- self.effic = str(input['effic']) if 'effic' in input and input['effic'] != None else None
-
- self.f_type = f"'{self.type}'"
- self.f_pump = f"'{self.pump}'"
- self.f_price = self.price if self.price != None else 'null'
- self.f_pattern = f"'{self.pattern}'" if self.pattern != None else 'null'
- self.f_effic = f"'{self.effic}'" if self.effic != None else 'null'
-
- def as_dict(self) -> dict[str, Any]:
- return { 'type': self.type, 'pump': self.pump, 'price': self.price, 'pattern': self.pattern, 'effic': self.effic }
-
-
-def _set_pump_energy(name: str, cs: ChangeSet) -> DbChangeSet:
- old = PumpEnergy(get_pump_energy(name, cs.operations[0]['pump']))
- raw_new = get_pump_energy(name, cs.operations[0]['pump'])
-
- new_dict = cs.operations[0]
- schema = get_pump_energy_schema(name)
- for key, value in schema.items():
- if key in new_dict and not value['readonly']:
- raw_new[key] = new_dict[key]
- new = PumpEnergy(raw_new)
-
- redo_sql = f"delete from energy_pump_price where pump = {new.f_pump};\ndelete from energy_pump_pattern where pump = {new.f_pump};\ndelete from energy_pump_effic where pump = {new.f_pump};"
- if new.price != None:
- redo_sql += f"\ninsert into energy_pump_price (pump, price) values ({new.f_pump}, {new.f_price});"
- if new.pattern != None:
- redo_sql += f"\ninsert into energy_pump_pattern (pump, pattern) values ({new.f_pump}, {new.f_pattern});"
- if new.effic != None:
- redo_sql += f"\ninsert into energy_pump_effic (pump, effic) values ({new.f_pump}, {new.f_effic});"
-
- undo_sql = f"delete from energy_pump_price where pump = {old.f_pump};\ndelete from energy_pump_pattern where pump = {old.f_pump};\ndelete from energy_pump_effic where pump = {old.f_pump};"
- if old.price != None:
- undo_sql += f"\ninsert into energy_pump_price (pump, price) values ({old.f_pump}, {old.f_price});"
- if old.pattern != None:
- undo_sql += f"\ninsert into energy_pump_pattern (pump, pattern) values ({old.f_pump}, {old.f_pattern});"
- if old.effic != None:
- undo_sql += f"\ninsert into energy_pump_effic (pump, effic) values ({old.f_pump}, {old.f_effic});"
-
- redo_cs = g_update_prefix | new.as_dict()
- undo_cs = g_update_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def set_pump_energy(name: str, cs: ChangeSet) -> ChangeSet:
- return execute_command(name, _set_pump_energy(name, cs))
-
-
-#--------------------------------------------------------------
-# [EPA2][EPA3][IN][OUT]
-# GLOBAL {PRICE/PATTERN/EFFIC} value
-# PUMP id {PRICE/PATTERN/EFFIC} value
-# DEMAND CHARGE value
-#--------------------------------------------------------------
-
-
-def inp_in_energy(line: str) -> str:
- tokens = line.split()
-
- if tokens[0].upper() == 'PUMP':
- pump = tokens[1]
- key = tokens[2].lower()
- value = tokens[3]
- if key == 'price':
- value = float(value)
- else:
- value = f"'{value}'"
- if key == 'efficiency':
- key = 'effic'
-
- return str(f"insert into energy_pump_{key} (pump, {key}) values ('{pump}', {value});")
-
- else:
- line = line.upper().strip()
- for key in get_energy_schema('').keys():
- if line.startswith(key):
- value = line.removeprefix(key).strip()
-
- # exception here
- if line.startswith('GLOBAL EFFICIENCY'):
- value = line.removeprefix('GLOBAL EFFICIENCY').strip()
-
- return str(f"update energy set value = '{value}' where key = '{key}';")
-
- return str('')
-
-
-def inp_out_energy(name: str) -> list[str]:
- lines = []
-
- objs = read_all(name, f"select * from energy")
- for obj in objs:
- key = obj['key']
- value = obj['value']
- if value.strip() != '':
- lines.append(f'{key} {value}')
-
- objs = read_all(name, f"select * from energy_pump_price")
- for obj in objs:
- pump = obj['pump']
- value = obj['price']
- lines.append(f'PUMP {pump} PRICE {value}')
-
- objs = read_all(name, f"select * from energy_pump_pattern")
- for obj in objs:
- pump = obj['pump']
- value = obj['pattern']
- lines.append(f'PUMP {pump} PATTERN {value}')
-
- objs = read_all(name, f"select * from energy_pump_effic")
- for obj in objs:
- pump = obj['pump']
- value = obj['effic']
- lines.append(f'PUMP {pump} EFFIC {value}')
-
- return lines
-
-
-def delete_pump_energy_by_pump(name: str, pump: str) -> ChangeSet:
- row1 = try_read(name, f"select * from energy_pump_price where pump = '{pump}'")
- row2 = try_read(name, f"select * from energy_pump_pattern where pump = '{pump}'")
- row3 = try_read(name, f"select * from energy_pump_effic where pump = '{pump}'")
- if row1 == None and row2 == None and row3 == None:
- return ChangeSet()
- return ChangeSet(g_update_prefix | {'type': 'pump_energy', 'pump' : pump, 'price': None, 'pattern': None, 'effic': None})
-
-
-def unset_pump_energy_by_pattern(name: str, pattern: str) -> ChangeSet:
- cs = ChangeSet()
-
- rows = read_all(name, f"select * from energy_pump_pattern where pattern = '{pattern}'")
- for row in rows:
- pump = row['pump']
- row1 = try_read(name, f"select * from energy_pump_price where pump = '{pump}'")
- price = float(row1['price']) if row1 != None else None
- row2 = try_read(name, f"select * from energy_pump_effic where pump = '{pump}'")
- effic = str(row2['effic']) if row2 != None else None
- cs.append(g_update_prefix | {'type': 'pump_energy', 'pump' : pump, 'price': price, 'pattern': None, 'effic': effic})
-
- return cs
-
-
-def unset_pump_energy_by_curve(name: str, curve: str) -> ChangeSet:
- cs = ChangeSet()
-
- rows = read_all(name, f"select * from energy_pump_effic where effic = '{curve}'")
- for row in rows:
- pump = row['pump']
- row1 = try_read(name, f"select * from energy_pump_price where pump = '{pump}'")
- price = float(row1['price']) if row1 != None else None
- row2 = try_read(name, f"select * from energy_pump_pattern where pump = '{pump}'")
- pattern = str(row2['pattern']) if row2 != None else None
- cs.append(g_update_prefix | {'type': 'pump_energy', 'pump' : pump, 'price': price, 'pattern': pattern, 'effic': None})
-
- return cs
diff --git a/app/native/wndb/s1_title.py b/app/native/wndb/s1_title.py
deleted file mode 100644
index 3ec5d6a..0000000
--- a/app/native/wndb/s1_title.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from .database import *
-
-
-def get_title_schema(name: str) -> dict[str, dict[str, Any]]:
- return {'value': {'type': 'float', 'optional': False, 'readonly': False}}
-
-
-def get_title(name: str) -> dict[str, Any]:
- title = read(name, 'select * from title')
- return { 'value': title['value'] }
-
-
-def _set_title(name: str, cs: ChangeSet) -> DbChangeSet:
- new = cs.operations[0]['value']
- old = get_title(name)['value']
-
- redo_sql = f"update title set value = '{new}';"
- undo_sql = f"update title set value = '{old}';"
-
- redo_cs = g_update_prefix | { 'type': 'title', 'value': new }
- undo_cs = g_update_prefix | { 'type': 'title', 'value': old }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def set_title(name: str, cs: ChangeSet) -> ChangeSet:
- return execute_command(name, _set_title(name ,cs))
-
-
-def inp_in_title(section: list[str]) -> str:
- if section == []:
- return str('')
-
- title = '\n'.join(section)
- return str(f"update title set value = '{title}';")
-
-
-def inp_out_title(name: str) -> list[str]:
- obj = str(get_title(name)['value'])
- return obj.split('\n')
diff --git a/app/native/wndb/s27_backdrop.py b/app/native/wndb/s27_backdrop.py
deleted file mode 100644
index bedc8fa..0000000
--- a/app/native/wndb/s27_backdrop.py
+++ /dev/null
@@ -1,39 +0,0 @@
-from .database import *
-
-
-def get_backdrop_schema(name: str) -> dict[str, dict[str, Any]]:
- return { 'content' : {'type': 'str' , 'optional': False , 'readonly': False} }
-
-
-def get_backdrop(name: str) -> dict[str, Any]:
- e = read(name, f"select * from backdrop")
- return { 'content': e['content'] }
-
-
-def _set_backdrop(name: str, cs: ChangeSet) -> DbChangeSet:
- old = get_backdrop(name)
-
- redo_sql = f"update backdrop set content = '{cs.operations[0]['content']}' where content = '{old['content']}';"
- undo_sql = f"update backdrop set content = '{old['content']}' where content = '{cs.operations[0]['content']}';"
-
- redo_cs = g_update_prefix | { 'type': 'backdrop', 'content': cs.operations[0]['content'] }
- undo_cs = g_update_prefix | { 'type': 'backdrop', 'content': old['content'] }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def set_backdrop(name: str, cs: ChangeSet) -> ChangeSet:
- return execute_command(name, _set_backdrop(name, cs))
-
-
-def inp_in_backdrop(section: list[str]) -> str:
- if section == []:
- return str('')
-
- content = '\n'.join(section)
- return str(f"update backdrop set content = '{content}';")
-
-
-def inp_out_backdrop(name: str) -> list[str]:
- obj = str(get_backdrop(name)['content'])
- return obj.split('\n')
\ No newline at end of file
diff --git a/app/native/wndb/s28_end.py b/app/native/wndb/s28_end.py
deleted file mode 100644
index e69de29..0000000
diff --git a/app/native/wndb/s29_scada_device.py b/app/native/wndb/s29_scada_device.py
deleted file mode 100644
index ec2f139..0000000
--- a/app/native/wndb/s29_scada_device.py
+++ /dev/null
@@ -1,123 +0,0 @@
-from .database import *
-
-
-SCADA_DEVICE_TYPE_PRESSURE = 'PRESSURE'
-SCADA_DEVICE_TYPE_DEMAND = 'DEMAND'
-SCADA_DEVICE_TYPE_QUALITY = 'QUALITY'
-SCADA_DEVICE_TYPE_LEVEL = 'LEVEL'
-SCADA_DEVICE_TYPE_FLOW = 'FLOW'
-SCADA_DEVICE_TYPE_UNKNOWN = 'UNKNOWN'
-
-
-def get_scada_device_schema(name: str) -> dict[str, dict[str, Any]]:
- return { 'id' : {'type': 'str', 'optional': False, 'readonly': True },
- 'name' : {'type': 'str', 'optional': True , 'readonly': False},
- 'address': {'type': 'str', 'optional': True , 'readonly': False},
- 'sd_type': {'type': 'str', 'optional': True , 'readonly': False}}
-
-
-def get_scada_device(name: str, id: str) -> dict[str, Any]:
- sm = try_read(name, f"select * from scada_device where id = '{id}'")
- if sm == None:
- return {}
- d = {}
- d['id'] = str(sm['id'])
- d['name'] = str(sm['name']) if sm['name'] != None else None
- d['address'] = str(sm['address']) if sm['address'] != None else None
- d['sd_type'] = str(sm['sd_type']) if sm['sd_type'] != None else None
- return d
-
-
-class ScadaDevice(object):
- def __init__(self, input: dict[str, Any]) -> None:
- self.type = 'scada_device'
- self.id = str(input['id'])
- self.name = str(input['name']) if 'name' in input and input['name'] != None else None
- self.address = str(input['address']) if 'address' in input and input['address'] != None else None
- self.sd_type = str(input['sd_type']) if 'sd_type' in input and input['sd_type'] != None else None
-
- self.f_type = f"'{self.type}'"
- self.f_id = f"'{self.id}'"
- self.f_name = f"'{self.name}'" if self.name != None else 'null'
- self.f_address = f"'{self.address}'" if self.address != None else 'null'
- self.f_sd_type = f"'{self.sd_type}'" if self.sd_type != None else 'null'
-
- def as_dict(self) -> dict[str, Any]:
- return { 'type': self.type, 'id': self.id, 'name': self.name, 'address': self.address, 'sd_type': self.sd_type }
-
- def as_id_dict(self) -> dict[str, Any]:
- return { 'type': self.type, 'id': self.id }
-
-
-def _set_scada_device(name: str, cs: ChangeSet) -> DbChangeSet:
- old = ScadaDevice(get_scada_device(name, cs.operations[0]['id']))
- raw_new = get_scada_device(name, cs.operations[0]['id'])
-
- new_dict = cs.operations[0]
- schema = get_scada_device_schema(name)
- for key, value in schema.items():
- if key in new_dict and not value['readonly']:
- raw_new[key] = new_dict[key]
- new = ScadaDevice(raw_new)
-
- redo_sql = f"update scada_device set name = {new.f_name}, address = {new.f_address}, sd_type = {new.f_sd_type} where id = {new.f_id};"
- undo_sql = f"update scada_device set name = {old.f_name}, address = {old.f_address}, sd_type = {old.f_sd_type} where id = {old.f_id};"
-
- redo_cs = g_update_prefix | new.as_dict()
- undo_cs = g_update_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def set_scada_device(name: str, cs: ChangeSet) -> ChangeSet:
- if get_scada_device(name, cs.operations[0]['id']) == {}:
- return ChangeSet()
- return execute_command(name, _set_scada_device(name, cs))
-
-
-def _add_scada_device(name: str, cs: ChangeSet) -> DbChangeSet:
- new = ScadaDevice(cs.operations[0])
-
- redo_sql = f"insert into scada_device (id, name, address, sd_type) values ({new.f_id}, {new.f_name}, {new.f_address}, {new.f_sd_type});"
- undo_sql = f"delete from scada_device where id = {new.f_id};"
-
- redo_cs = g_add_prefix | new.as_dict()
- undo_cs = g_delete_prefix | new.as_id_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def add_scada_device(name: str, cs: ChangeSet) -> ChangeSet:
- if get_scada_device(name, cs.operations[0]['id']) != {}:
- return ChangeSet()
- return execute_command(name, _add_scada_device(name, cs))
-
-
-def _delete_scada_device(name: str, cs: ChangeSet) -> DbChangeSet:
- old = ScadaDevice(get_scada_device(name, cs.operations[0]['id']))
-
- redo_sql = f"delete from scada_device where id = {old.f_id};"
- undo_sql = f"insert into scada_device (id, name, address, sd_type) values ({old.f_id}, {old.f_name}, {old.f_address}, {old.f_sd_type});"
-
- redo_cs = g_delete_prefix | old.as_id_dict()
- undo_cs = g_add_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def delete_scada_device(name: str, cs: ChangeSet) -> ChangeSet:
- if get_scada_device(name, cs.operations[0]['id']) == {}:
- return ChangeSet()
- return execute_command(name, _delete_scada_device(name, cs))
-
-
-def get_all_scada_device_ids(name: str) -> list[str]:
- result : list[str] = []
- rows = read_all(name, 'select id from scada_device order by id')
- for row in rows:
- result.append(str(row['id']))
- return result
-
-
-def get_all_scada_devices(name: str) -> list[dict[str, Any]]:
- return read_all(name, 'select * from scada_device order by id')
diff --git a/app/native/wndb/s30_scada_device_data.py b/app/native/wndb/s30_scada_device_data.py
deleted file mode 100644
index 5f23800..0000000
--- a/app/native/wndb/s30_scada_device_data.py
+++ /dev/null
@@ -1,90 +0,0 @@
-from .database import *
-
-
-def get_scada_device_data_schema(name: str) -> dict[str, dict[str, Any]]:
- return { 'device_id' : {'type': 'str' , 'optional': False , 'readonly': True },
- 'data' : {'type': 'list' , 'optional': False , 'readonly': False,
- 'element': { 'time' : {'type': 'str' , 'optional': False , 'readonly': False },
- 'value' : {'type': 'float' , 'optional': False , 'readonly': False } }}}
-
-
-def get_scada_device_data(name: str, device_id: str) -> dict[str, Any]:
- sds = read_all(name, f"select * from scada_device_data where device_id = '{device_id}' order by time")
- ds = []
- for r in sds:
- ds.append({ 'time': str(r['time']), 'value': float(r['value']) })
- return { 'device_id': device_id, 'data': ds }
-
-
-def _set_scada_device_data(name: str, cs: ChangeSet) -> DbChangeSet:
- device_id = cs.operations[0]['device_id']
-
- old = get_scada_device_data(name, device_id)
- new = { 'device_id': device_id, 'data': [] }
-
- f_device_id = f"'{device_id}'"
-
- # TODO: transaction ?
- redo_sql = f"delete from scada_device_data where device_id = {f_device_id};"
- for tv in cs.operations[0]['data']:
- time, value = str(tv['time']), float(tv['value'])
- f_time, f_value = f"'{time}'", value
- redo_sql += f"\ninsert into scada_device_data (device_id, time, value) values ({f_device_id}, {f_time}, {f_value});"
- new['data'].append({ 'time': time, 'value': value })
-
- undo_sql = f"delete from scada_device_data where device_id = {f_device_id};"
- for tv in old['data']:
- time, value = str(tv['time']), float(tv['value'])
- f_time, f_value = f"'{time}'", value
- undo_sql += f"\ninsert into scada_device_data (device_id, time, value) values ({f_device_id}, {f_time}, {f_value});"
-
- redo_cs = g_update_prefix | { 'type': 'scada_device_data' } | new
- undo_cs = g_update_prefix | { 'type': 'scada_device_data' } | old
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def set_scada_device_data(name: str, cs: ChangeSet) -> ChangeSet:
- return execute_command(name, _set_scada_device_data(name, cs))
-
-
-def _add_scada_device_data(name: str, cs: ChangeSet) -> DbChangeSet:
- values = cs.operations[0]
- device_id = values['device_id']
- time = values['time']
- value = float(values['value'])
-
- redo_sql = f"insert into scada_device_data (device_id, time, value) values ('{device_id}', '{time}', {value});"
- undo_sql = f"delete from scada_device_data where device_id = '{device_id}' and time = '{time}';"
- redo_cs = g_add_prefix | { 'type': 'scada_device_data', 'device_id': device_id, 'time': time, 'value': value }
- undo_cs = g_delete_prefix | { 'type': 'scada_device_data', 'device_id': device_id, 'time': time }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def add_scada_device_data(name: str, cs: ChangeSet) -> ChangeSet:
- row = try_read(name, f"select * from scada_device_data where device_id = '{cs.operations[0]['device_id']}' and time = '{cs.operations[0]['time']}'")
- if row != None:
- return ChangeSet()
- return execute_command(name, _add_scada_device_data(name, cs))
-
-
-def _delete_scada_device_data(name: str, cs: ChangeSet) -> DbChangeSet:
- values = cs.operations[0]
- device_id = values['device_id']
- time = values['time']
- value = float(read(name, f"select * from scada_device_data where device_id = '{device_id}' and time = '{time}'")['value'])
-
- redo_sql = f"delete from scada_device_data where device_id = '{device_id}' and time = '{time}';"
- undo_sql = f"insert into scada_device_data (device_id, time, value) values ('{device_id}', '{time}', {value});"
- redo_cs = g_delete_prefix | { 'type': 'scada_device_data', 'device_id': device_id, 'time': time }
- undo_cs = g_add_prefix | { 'type': 'scada_device_data', 'device_id': device_id, 'time': time, 'value': value }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def delete_scada_device_data(name: str, cs: ChangeSet) -> ChangeSet:
- row = try_read(name, f"select * from scada_device_data where device_id = '{cs.operations[0]['device_id']}' and time = '{cs.operations[0]['time']}'")
- if row == None:
- return ChangeSet()
- return execute_command(name, _delete_scada_device_data(name, cs))
diff --git a/app/native/wndb/s31_scada_element.py b/app/native/wndb/s31_scada_element.py
deleted file mode 100644
index 457def3..0000000
--- a/app/native/wndb/s31_scada_element.py
+++ /dev/null
@@ -1,197 +0,0 @@
-from .database import *
-from .s0_base import *
-
-
-SCADA_TYPE_PRESSURE = 'PRESSURE'
-SCADA_TYPE_DEMAND = 'DEMAND'
-SCADA_TYPE_QUALITY = 'QUALITY'
-SCADA_TYPE_LEVEL = 'LEVEL'
-SCADA_TYPE_FLOW = 'FLOW'
-
-
-SCADA_MODEL_TYPE_JUNCTION = 'JUNCTION'
-SCADA_MODEL_TYPE_RESERVOIR = 'RESERVOIR'
-SCADA_MODEL_TYPE_TANK = 'TANK'
-SCADA_MODEL_TYPE_PIPE = 'PIPE'
-SCADA_MODEL_TYPE_PUMP = 'PUMP'
-SCADA_MODEL_TYPE_VALVE = 'VALVE'
-
-
-SCADA_ELEMENT_STATUS_OFFLINE = 'OFF'
-SCADA_ELEMENT_STATUS_ONLINE = 'ON'
-
-
-_scada_model_types = [SCADA_MODEL_TYPE_JUNCTION, SCADA_MODEL_TYPE_RESERVOIR, SCADA_MODEL_TYPE_TANK, SCADA_MODEL_TYPE_PIPE, SCADA_MODEL_TYPE_PUMP, SCADA_MODEL_TYPE_VALVE]
-
-
-def _check_model(name: str, cs: ChangeSet) -> bool:
- has_model_id = 'model_id' in cs.operations[0]
- has_model_type = 'model_type' in cs.operations[0]
-
- if has_model_id and has_model_type:
- pass
- elif has_model_id and not has_model_type:
- return False
- elif not has_model_id and has_model_type:
- return False
- elif not has_model_id and not has_model_type:
- return True
-
- _model_id = cs.operations[0]['model_id']
- _model_type = cs.operations[0]['model_type']
- if _model_type == SCADA_MODEL_TYPE_JUNCTION:
- return is_junction(name, _model_id)
- elif _model_type == SCADA_MODEL_TYPE_RESERVOIR:
- return is_reservoir(name, _model_id)
- elif _model_type == SCADA_MODEL_TYPE_TANK:
- return is_tank(name, _model_id)
- elif _model_type == SCADA_MODEL_TYPE_PIPE:
- return is_pipe(name, _model_id)
- elif _model_type == SCADA_MODEL_TYPE_PUMP:
- return is_pump(name, _model_id)
- elif _model_type == SCADA_MODEL_TYPE_VALVE:
- return is_valve(name, _model_id)
- return False
-
-
-def get_scada_element_schema(name: str) -> dict[str, dict[str, Any]]:
- return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True },
- 'x' : {'type': 'float' , 'optional': False , 'readonly': False},
- 'y' : {'type': 'float' , 'optional': False , 'readonly': False},
- 'device_id' : {'type': 'str' , 'optional': True , 'readonly': False},
- 'model_id' : {'type': 'str' , 'optional': True , 'readonly': False},
- 'model_type' : {'type': 'str' , 'optional': True , 'readonly': False},
- 'status' : {'type': 'str' , 'optional': True , 'readonly': False} }
-
-
-def get_scada_element(name: str, id: str) -> dict[str, Any]:
- sm = try_read(name, f"select * from scada_element where id = '{id}'")
- if sm == None:
- return {}
- d = {}
- d['id'] = str(sm['id'])
- d['x'] = float(sm['x'])
- d['y'] = float(sm['y'])
- d['device_id'] = str(sm['device_id']) if sm['device_id'] != None else None
- d['model_id'] = str(sm['model_id']) if sm['model_id'] != None else None
- d['model_type'] = str(sm['model_type']) if sm['model_type'] != None else None
- d['status'] = str(sm['status'])
- return d
-
-
-class ScadaModel(object):
- def __init__(self, input: dict[str, Any]) -> None:
- self.type = 'scada_element'
- self.id = str(input['id'])
- self.x = float(input['x'])
- self.y = float(input['y'])
- self.device_id = str(input['device_id']) if 'device_id' in input and input['device_id'] != None else None
- self.model_id = str(input['model_id']) if 'model_id' in input and input['model_id'] != None else None
- self.model_type = str(input['model_type']) if 'model_type' in input and input['model_type'] != None else None
- self.status = str(input['status']) if 'status' in input and input['status'] != None else SCADA_ELEMENT_STATUS_OFFLINE
-
- self.f_type = f"'{self.type}'"
- self.f_id = f"'{self.id}'"
- self.f_x = self.x
- self.f_y = self.y
- self.f_device_id = f"'{self.device_id}'" if self.device_id != None else 'null'
- self.f_model_id = f"'{self.model_id}'" if self.model_id != None else 'null'
- self.f_model_type = f"'{self.model_type}'" if self.model_type != None else 'null'
- self.f_status = f"'{self.status}'"
-
- def as_dict(self) -> dict[str, Any]:
- return { 'type': self.type, 'id': self.id, 'x': self.x, 'y': self.y, 'device_id': self.device_id, 'model_id': self.model_id, 'model_type': self.model_type, 'status': self.status }
-
- def as_id_dict(self) -> dict[str, Any]:
- return { 'type': self.type, 'id': self.id }
-
-
-def _set_scada_element(name: str, cs: ChangeSet) -> DbChangeSet:
- old = ScadaModel(get_scada_element(name, cs.operations[0]['id']))
- raw_new = get_scada_element(name, cs.operations[0]['id'])
-
- new_dict = cs.operations[0]
- schema = get_scada_element_schema(name)
- for key, value in schema.items():
- if key in new_dict and not value['readonly']:
- raw_new[key] = new_dict[key]
- new = ScadaModel(raw_new)
-
- redo_sql = f"update scada_element set x = {new.f_x}, y = {new.f_y}, device_id = {new.f_device_id}, model_id = {new.f_model_id}, model_type = {new.f_model_type}, status = {new.f_status} where id = {new.f_id};"
- undo_sql = f"update scada_element set x = {old.f_x}, y = {old.f_y}, device_id = {old.f_device_id}, model_id = {old.f_model_id}, model_type = {old.f_model_type}, status = {old.f_status} where id = {old.f_id};"
-
- redo_cs = g_update_prefix | new.as_dict()
- undo_cs = g_update_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def set_scada_element(name: str, cs: ChangeSet) -> ChangeSet:
- if get_scada_element(name, cs.operations[0]['id']) == {}:
- return ChangeSet()
- if _check_model(name, cs) == False:
- return ChangeSet()
- return execute_command(name, _set_scada_element(name, cs))
-
-
-def _add_scada_element(name: str, cs: ChangeSet) -> DbChangeSet:
- new = ScadaModel(cs.operations[0])
-
- redo_sql = f"insert into scada_element (id, x, y, device_id, model_id, model_type, status) values ({new.f_id}, {new.f_x}, {new.f_y}, {new.f_device_id}, {new.f_model_id}, {new.f_model_type}, {new.f_status});"
- undo_sql = f"delete from scada_element where id = {new.f_id};"
-
- redo_cs = g_add_prefix | new.as_dict()
- undo_cs = g_delete_prefix | new.as_id_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def add_scada_element(name: str, cs: ChangeSet) -> ChangeSet:
- if get_scada_element(name, cs.operations[0]['id']) != {}:
- return ChangeSet()
- if _check_model(name, cs) == False:
- return ChangeSet()
- return execute_command(name, _add_scada_element(name, cs))
-
-
-def _delete_scada_element(name: str, cs: ChangeSet) -> DbChangeSet:
- old = ScadaModel(get_scada_element(name, cs.operations[0]['id']))
-
- redo_sql = f"delete from scada_element where id = {old.f_id};"
- undo_sql = f"insert into scada_element (id, x, y, device_id, model_id, model_type, status) values ({old.f_id}, {old.f_x}, {old.f_y}, {old.f_device_id}, {old.f_model_id}, {old.f_model_type}, {old.f_status});"
-
- redo_cs = g_delete_prefix | old.as_id_dict()
- undo_cs = g_add_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def delete_scada_element(name: str, cs: ChangeSet) -> ChangeSet:
- if get_scada_element(name, cs.operations[0]['id']) == {}:
- return ChangeSet()
- return execute_command(name, _delete_scada_element(name, cs))
-
-
-def get_all_scada_element_ids(name: str) -> list[str]:
- result : list[str] = []
- rows = read_all(name, 'select id from scada_element order by id')
- for row in rows:
- result.append(str(row['id']))
- return result
-
-#
-# create table scada_element
-# (
-# id text primary key
-# , x float8 not null
-# , y float8 not null
-# , device_id text references scada_device(id)
-# , model_id varchar(32) -- add constraint in API
-# , model_type scada_model_type
-# , status scada_element_status not null default 'OFF'
-# );
-#
-# 返回list,list里每个item是dict,内容是 'id':'abc' 这样
-# scada_model type 是类似pressure,flow之类的,是由Device 决定 的
-def get_all_scada_elements(name: str) -> list[dict[str, Any]]:
- return read_all(name, 'select * from scada_element order by id')
diff --git a/app/native/wndb/s32_region.py b/app/native/wndb/s32_region.py
deleted file mode 100644
index 95fd67f..0000000
--- a/app/native/wndb/s32_region.py
+++ /dev/null
@@ -1,95 +0,0 @@
-from .database import *
-from .s32_region_util import from_postgis_polygon, to_postgis_polygon
-
-def get_region_schema(name: str) -> dict[str, dict[str, Any]]:
- return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True },
- 'boundary' : {'type': 'tuple_list' , 'optional': False , 'readonly': False} }
-
-
-def get_region(name: str, id: str) -> dict[str, Any]:
- r = try_read(name, f"select id, st_astext(boundary) as boundary_geom from region where id = '{id}'")
- if r == None:
- return {}
- d = {}
- d['id'] = str(r['id'])
- d['boundary'] = from_postgis_polygon(str(r['boundary_geom']))
- return d
-
-
-def _set_region(name: str, cs: ChangeSet) -> DbChangeSet:
- id = cs.operations[0]['id']
- new = cs.operations[0]['boundary']
- old = get_region(name, id)['boundary']
-
- redo_sql = f"update region set boundary = st_geomfromtext('{to_postgis_polygon(new)}') where id = '{id}';"
- undo_sql = f"update region set boundary = st_geomfromtext('{to_postgis_polygon(old)}') where id = '{id}';"
- redo_cs = g_update_prefix | { 'type': 'region', 'id': id, 'boundary': new }
- undo_cs = g_update_prefix | { 'type': 'region', 'id': id, 'boundary': old }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def set_region(name: str, cs: ChangeSet) -> ChangeSet:
- if 'id' not in cs.operations[0] or 'boundary' not in cs.operations[0]:
- return ChangeSet()
- b = cs.operations[0]['boundary']
- if len(b) < 4 or b[0] != b[-1]:
- return ChangeSet()
- if get_region(name, cs.operations[0]['id']) == {}:
- return ChangeSet()
- return execute_command(name, _set_region(name, cs))
-
-
-def _add_region(name: str, cs: ChangeSet) -> DbChangeSet:
- id = cs.operations[0]['id']
- new = cs.operations[0]['boundary']
-
- redo_sql = f"insert into region (id, boundary) values ('{id}', '{to_postgis_polygon(new)}');"
- undo_sql = f"delete from region where id = '{id}';"
- redo_cs = g_add_prefix | { 'type': 'region', 'id': id, 'boundary': new }
- undo_cs = g_delete_prefix | { 'type': 'region', 'id': id }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def add_region(name: str, cs: ChangeSet) -> ChangeSet:
- if 'id' not in cs.operations[0] or 'boundary' not in cs.operations[0]:
- return ChangeSet()
- b = cs.operations[0]['boundary']
- if len(b) < 4 or b[0] != b[-1]:
- return ChangeSet()
- if get_region(name, cs.operations[0]['id']) != {}:
- return ChangeSet()
- return execute_command(name, _add_region(name, cs))
-
-
-def _delete_region(name: str, cs: ChangeSet) -> DbChangeSet:
- id = cs.operations[0]['id']
- old = get_region(name, id)['boundary']
-
- redo_sql = f"delete from region where id = '{id}';"
- undo_sql = f"insert into region (id, boundary) values ('{id}', '{to_postgis_polygon(old)}');"
- redo_cs = g_delete_prefix | { 'type': 'region', 'id': id }
- undo_cs = g_add_prefix | { 'type': 'region', 'id': id, 'boundary': old }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def delete_region(name: str, cs: ChangeSet) -> ChangeSet:
- if 'id' not in cs.operations[0]:
- return ChangeSet()
- if get_region(name, cs.operations[0]['id']) == {}:
- return ChangeSet()
- return execute_command(name, _delete_region(name, cs))
-
-def inp_in_region(line: str) -> str:
- tokens = line.split()
- return str(f"insert into _region (id, type) values ('{tokens[0]}', '{tokens[1]}');")
-
-def inp_in_bound(line: str) -> str:
- tokens = line.split()
- return tokens[0]
-
-def inp_in_regionnodes(line: str)->str:
- tokens = line.split()
- return tokens[0]
\ No newline at end of file
diff --git a/app/native/wndb/s33_dma.py b/app/native/wndb/s33_dma.py
deleted file mode 100644
index 49198c9..0000000
--- a/app/native/wndb/s33_dma.py
+++ /dev/null
@@ -1,230 +0,0 @@
-from .database import *
-from .s0_base import is_node
-from .s32_region_util import to_postgis_polygon
-from .s32_region import get_region
-
-def get_district_metering_area_schema(name: str) -> dict[str, dict[str, Any]]:
- return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True },
- 'boundary' : {'type': 'tuple_list' , 'optional': False , 'readonly': False },
- 'parent' : {'type': 'str' , 'optional': True , 'readonly': False },
- 'level' : {'type': 'int' , 'optional': False , 'readonly': True } }
-
-
-def get_district_metering_area(name: str, id: str) -> dict[str, Any]:
- dma = get_region(name, id)
- if dma == {}:
- return {}
- r = try_read(name, f"select * from region_dma where id = '{id}'")
- if r == None:
- return {}
- dma['parent'] = r['parent']
- dma['nodes'] = list(eval(r['nodes']))
- dma['level'] = 1
-
- if dma['parent'] != None:
- parent = dma['parent']
- while parent != None:
- parent = read(name, f"select parent from region_dma where id = '{parent}'")['parent']
- dma['level'] += 1
-
- return dma
-
-
-def _set_district_metering_area(name: str, cs: ChangeSet) -> DbChangeSet:
- id = cs.operations[0]['id']
-
- new_boundary = cs.operations[0]['boundary']
- old_boundary = get_region(name, id)['boundary']
-
- new_parent = cs.operations[0]['parent']
- f_new_parent = f"'{new_parent}'" if new_parent != None else 'null'
-
- new_nodes = cs.operations[0]['nodes']
- str_new_nodes = str(new_nodes).replace("'", "''")
-
- old = get_district_metering_area(name, id)
- old_parent = old['parent']
- f_old_parent = f"'{old_parent}'" if old_parent != None else 'null'
-
- old_nodes = old['nodes']
- str_old_nodes = str(old_nodes).replace("'", "''")
-
- redo_sql = f"update region set boundary = st_geomfromtext('{to_postgis_polygon(new_boundary)}') where id = '{id}';"
- redo_sql += f"update region_dma set parent = {f_new_parent}, nodes = '{str_new_nodes}' where id = '{id}';"
-
- undo_sql = f"update region_dma set parent = {f_old_parent}, nodes = '{str_old_nodes}' where id = '{id}';"
- undo_sql += f"update region set boundary = st_geomfromtext('{to_postgis_polygon(old_boundary)}') where id = '{id}';"
-
- redo_cs = g_update_prefix | { 'type': 'district_metering_area', 'id': id, 'boundary': new_boundary, 'parent': new_parent, 'nodes': new_nodes }
- undo_cs = g_update_prefix | { 'type': 'district_metering_area', 'id': id, 'boundary': old_boundary, 'parent': old_parent, 'nodes': old_nodes }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def set_district_metering_area(name: str, cs: ChangeSet) -> ChangeSet:
- ops = cs.operations
-
- if len(cs.operations) == 0:
- return ChangeSet()
-
- op = ops[0]
-
- if 'id' not in op:
- return ChangeSet()
-
- dma = get_district_metering_area(name, op['id'])
- if dma == {}:
- return ChangeSet()
-
- if 'boundary' not in op:
- op['boundary'] = dma['boundary']
- else:
- b = op['boundary']
- if len(b) < 4 or b[0] != b[-1]:
- return ChangeSet()
-
- if 'parent' not in op:
- op['parent'] = dma['parent']
-
- if op['parent'] != None and get_district_metering_area(name, op['parent']) == {}:
- return ChangeSet()
-
- if 'nodes' not in op:
- op['nodes'] = dma['nodes']
- else:
- for node in op['nodes']:
- if not is_node(name, node):
- return ChangeSet()
-
- return execute_command(name, _set_district_metering_area(name, cs))
-
-
-def _add_district_metering_area(name: str, cs: ChangeSet) -> DbChangeSet:
- id = cs.operations[0]['id']
-
- boundary = cs.operations[0]['boundary']
-
- parent = cs.operations[0]['parent']
- f_parent = f"'{parent}'" if parent != None else 'null'
-
- nodes = cs.operations[0]['nodes']
- str_nodes = str(nodes).replace("'", "''")
-
- redo_sql = f"insert into region (id, boundary, r_type) values ('{id}', '{to_postgis_polygon(boundary)}', 'DMA');"
- redo_sql += f"insert into region_dma (id, parent, nodes) values ('{id}', {f_parent}, '{str_nodes}');"
-
- undo_sql = f"delete from region_dma where id = '{id}';"
- undo_sql += f"delete from region where id = '{id}';"
-
- redo_cs = g_add_prefix | { 'type': 'district_metering_area', 'id': id, 'boundary': boundary, 'parent': parent, 'nodes': nodes }
- undo_cs = g_delete_prefix | { 'type': 'district_metering_area', 'id': id }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def add_district_metering_area(name: str, cs: ChangeSet) -> ChangeSet:
- ops = cs.operations
-
- if len(cs.operations) == 0:
- return ChangeSet()
-
- op = ops[0]
-
- if 'id' not in op:
- return ChangeSet()
-
- dma = get_district_metering_area(name, op['id'])
- if dma != {}:
- return ChangeSet()
-
- if 'boundary' not in op:
- return ChangeSet()
- else:
- b = op['boundary']
- if len(b) < 4 or b[0] != b[-1]:
- return ChangeSet()
-
- if 'parent' not in op:
- op['parent'] = None
-
- if op['parent'] != None and get_district_metering_area(name, op['parent']) == {}:
- return ChangeSet()
-
- if 'nodes' not in op:
- op['nodes'] = []
- else:
- for node in op['nodes']:
- if not is_node(name, node):
- return ChangeSet()
-
- return execute_command(name, _add_district_metering_area(name, cs))
-
-
-def _delete_district_metering_area(name: str, cs: ChangeSet) -> DbChangeSet:
- id = cs.operations[0]['id']
- dma = get_district_metering_area(name, id)
- boundary = dma['boundary']
- parent = dma['parent']
- f_parent = f"'{parent}'" if parent != None else 'null'
- nodes = dma['nodes']
- str_nodes = str(nodes).replace("'", "''")
-
- redo_sql = f"delete from region_dma where id = '{id}';"
- redo_sql += f"delete from region where id = '{id}';"
-
- undo_sql = f"insert into region (id, boundary, r_type) values ('{id}', '{to_postgis_polygon(boundary)}', 'DMA');"
- undo_sql += f"insert into region_dma (id, parent, nodes) values ('{id}', {f_parent}, '{str_nodes}');"
-
- redo_cs = g_delete_prefix | { 'type': 'district_metering_area', 'id': id }
- undo_cs = g_add_prefix | { 'type': 'district_metering_area', 'id': id, 'boundary': boundary, 'parent': parent, 'nodes': nodes }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def _has_child(name: str, parent: str) -> bool:
- return try_read(name, f"select * from region_dma where parent = '{parent}'") != None
-
-
-def is_descendant_of(name: str, descendant: str, ancestor: str) -> bool:
- parent = descendant
- while parent != None:
- parent = read(name, f"select parent from region_dma where id = '{parent}'")['parent']
- if parent == ancestor:
- return True
- return False
-
-
-def delete_district_metering_area(name: str, cs: ChangeSet) -> ChangeSet:
- ops = cs.operations
-
- if len(cs.operations) == 0:
- return ChangeSet()
-
- op = ops[0]
-
- if 'id' not in op:
- return ChangeSet()
-
- dma = get_district_metering_area(name, op['id'])
- if dma == {}:
- return ChangeSet()
-
- #TODO: cascade ?
- if _has_child(name, dma['id']):
- return ChangeSet()
-
- return execute_command(name, _delete_district_metering_area(name, cs))
-
-
-def get_all_district_metering_area_ids(name: str) -> list[str]:
- ids = []
- for row in read_all(name, f"select id from region_dma"):
- ids.append(row['id'])
- return ids
-
-
-def get_all_district_metering_areas(name: str) -> list[dict[str, Any]]:
- result = []
- for id in get_all_district_metering_area_ids(name):
- result.append(get_district_metering_area(name, id))
- return result
diff --git a/app/native/wndb/s33_dma_cal.py b/app/native/wndb/s33_dma_cal.py
deleted file mode 100644
index 5da2ea6..0000000
--- a/app/native/wndb/s33_dma_cal.py
+++ /dev/null
@@ -1,168 +0,0 @@
-import ctypes
-import os
-import numpy as np
-import pymetis
-from .database import *
-from .s0_base import get_nodes
-from .s32_region_util import get_nodes_in_region
-from .s32_region_util import Topology
-
-
-PARTITION_TYPE_RB = 0
-PARTITION_TYPE_KWAY = 1
-
-'''
-adjacency_list = [np.array([4, 2, 1]),
- np.array([0, 2, 3]),
- np.array([4, 3, 1, 0]),
- np.array([1, 2, 5, 6]),
- np.array([0, 2, 5]),
- np.array([4, 3, 6]),
- np.array([5, 3])]
-n_cuts, membership = pymetis.part_graph(2, adjacency=adjacency_list)
-# n_cuts = 3
-# membership = [1, 1, 1, 0, 1, 0, 0]
-
-nodes_part_0 = np.argwhere(np.array(membership) == 0).ravel() # [3, 5, 6]
-nodes_part_1 = np.argwhere(np.array(membership) == 1).ravel() # [0, 1, 2, 4]
-
-print(nodes_part_0)
-print(nodes_part_1)
-'''
-
-
-def calculate_district_metering_area_for_nodes(name: str, nodes: list[str], part_count: int = 1, part_type: int = PARTITION_TYPE_RB) -> list[list[str]]:
- if part_type != PARTITION_TYPE_RB and part_type != PARTITION_TYPE_KWAY:
- return []
- if part_count <= 0:
- return []
- elif part_count == 1:
- return [nodes]
-
- topology = Topology(name, nodes)
- t_nodes = topology.nodes()
- t_links = topology.links()
- t_node_list = topology.node_list()
-
- adjacency_list = []
-
- for node in t_node_list:
- links: list[str] = t_nodes[node]['links']
- a_nodes: list[int] = []
- for link in links:
- if t_links[link]['node1'] == node:
- i = t_node_list.index(t_links[link]['node2'])
- a_nodes.append(i)
- elif t_links[link]['node2'] == node:
- i = t_node_list.index(t_links[link]['node1'])
- a_nodes.append(i)
- adjacency_list.append(np.array(a_nodes))
-
- recursive = part_type == PARTITION_TYPE_RB
- options = pymetis.Options()
- options.set_defaults()
- options._set(pymetis.OptionKey.CONTIG, 1)
- options._set(pymetis.OptionKey.SEED, 0)
- n_cuts, membership = pymetis.part_graph(
- nparts=part_count,
- adjacency=adjacency_list,
- recursive=recursive,
- options=options,
- )
-
- result: list[list[str]] = []
- for i in range(0, part_count):
- indices: list[int] = list(np.argwhere(np.array(membership) == i).ravel())
- index_strs: list[str] = []
- for index in indices:
- index_strs.append(t_node_list[index])
- result.append(index_strs)
-
- return result
-
-
-def _calculate_district_metering_area_for_nodes(name: str, nodes: list[str], part_count: int = 1, part_type: int = PARTITION_TYPE_RB) -> list[list[str]]:
- if part_type != PARTITION_TYPE_RB and part_type != PARTITION_TYPE_KWAY:
- return []
- if part_count <= 0:
- return []
- elif part_count == 1:
- return [nodes]
-
- lib = ctypes.CDLL(os.path.join(os.getcwd(), 'api', 'CMetis.dll'))
-
- METIS_NOPTIONS = 40
- c_options = (ctypes.c_int64 * METIS_NOPTIONS)()
-
- METIS_OK = 1
- result = lib.set_default_options(c_options)
- if result != METIS_OK:
- return []
-
- METIS_OPTION_PTYPE , METIS_OPTION_CONTIG = 0, 13
- c_options[METIS_OPTION_PTYPE] = part_type
- c_options[METIS_OPTION_CONTIG] = 1
-
- topology = Topology(name, nodes)
- t_nodes = topology.nodes()
- t_links = topology.links()
- t_node_list = topology.node_list()
- t_link_list = topology.link_list()
-
- nedges = len(t_link_list) * 2
-
- c_nvtxs = ctypes.c_int64(len(t_node_list))
- c_ncon = ctypes.c_int64(1)
- c_xadj = (ctypes.c_int64 * (c_nvtxs.value + 1))()
- c_adjncy = (ctypes.c_int64 * nedges)()
- c_vwgt = (ctypes.c_int64 * (c_ncon.value * c_nvtxs.value))()
- c_adjwgt = (ctypes.c_int64 * nedges)()
- c_vsize = (ctypes.c_int64 * c_nvtxs.value)()
-
- c_xadj[0] = 0
-
- l, n = 0, 0
- c_xadj_i = 1
- for node in t_node_list:
- links = t_nodes[node]['links']
- for link in links:
- node1 = t_links[link]['node1']
- node2 = t_links[link]['node2']
- c_adjncy[l] = t_node_list.index(node2) if node2 != node else t_node_list.index(node1)
- c_adjwgt[l] = 1
- l += 1
- if len(links) > 0:
- c_xadj[c_xadj_i] = l # adjncy.size()
- c_xadj_i += 1
- c_vwgt[n] = 1
- c_vsize[n] = 1
- n += 1
-
- part_func = lib.part_graph_recursive if part_type == PARTITION_TYPE_RB else lib.part_graph_kway
-
- c_nparts = ctypes.c_int64(part_count)
- c_tpwgts = ctypes.POINTER(ctypes.c_double)()
- c_ubvec = ctypes.POINTER(ctypes.c_double)()
- c_out_edgecut = ctypes.c_int64(0)
- c_out_part = (ctypes.c_int64 * c_nvtxs.value)()
- result = part_func(ctypes.byref(c_nvtxs), ctypes.byref(c_ncon), c_xadj, c_adjncy, c_vwgt, c_vsize, c_adjwgt, ctypes.byref(c_nparts), c_tpwgts, c_ubvec, c_options, ctypes.byref(c_out_edgecut), c_out_part)
- if result != METIS_OK:
- return []
-
- dmas : list[list[str]]= []
- for i in range(part_count):
- dmas.append([])
- for i in range(c_nvtxs.value):
- dmas[c_out_part[i]].append(t_node_list[i])
-
- return dmas
-
-
-def calculate_district_metering_area_for_region(name: str, region: str, part_count: int = 1, part_type: int = PARTITION_TYPE_RB) -> list[list[str]]:
- nodes = get_nodes_in_region(name, region)
- return calculate_district_metering_area_for_nodes(name, nodes, part_count, part_type)
-
-
-def calculate_district_metering_area_for_network(name: str, part_count: int = 1, part_type: int = PARTITION_TYPE_RB) -> list[list[str]]:
- nodes = get_nodes(name)
- return calculate_district_metering_area_for_nodes(name, nodes, part_count, part_type)
diff --git a/app/native/wndb/s33_dma_gen.py b/app/native/wndb/s33_dma_gen.py
deleted file mode 100644
index 4a88fdc..0000000
--- a/app/native/wndb/s33_dma_gen.py
+++ /dev/null
@@ -1,47 +0,0 @@
-from .s32_region_util import calculate_boundary, inflate_boundary
-from .s33_dma_cal import *
-from .s33_dma import get_all_district_metering_area_ids, get_all_district_metering_areas, get_district_metering_area, is_descendant_of
-from .batch_exe import execute_batch_command
-
-
-def generate_district_metering_area(name: str, part_count: int = 1, part_type: int = PARTITION_TYPE_RB, inflate_delta: float = 0.5) -> ChangeSet:
- cs = ChangeSet()
-
- dmas = get_all_district_metering_areas(name)
- max_level = 0
- for dma in dmas:
- if dma['level'] > max_level:
- max_level = dma['level']
- while max_level > 0:
- for dma in dmas:
- if dma['level'] == max_level:
- cs.delete({ 'type': 'district_metering_area', 'id': dma['id'] })
- max_level -= 1
-
- i = 1
- for nodes in calculate_district_metering_area_for_network(name, part_count, part_type):
- boundary = calculate_boundary(name, nodes)
- boundary = inflate_boundary(name, boundary, inflate_delta)
- cs.add({ 'type': 'district_metering_area', 'id': f"DMA_1_{i}", 'boundary': boundary, 'parent': None, 'nodes': nodes })
- i += 1
-
- return execute_batch_command(name, cs)
-
-
-def generate_sub_district_metering_area(name: str, dma: str, part_count: int = 1, part_type: int = PARTITION_TYPE_RB, inflate_delta: float = 0.5) -> ChangeSet:
- cs = ChangeSet()
-
- for id in get_all_district_metering_area_ids(name):
- if is_descendant_of(name, id, dma):
- cs.delete({ 'type': 'district_metering_area', 'id': id })
-
- level = get_district_metering_area(name, dma)['level'] + 1
-
- i = 1
- for nodes in calculate_district_metering_area_for_region(name, dma, part_count, part_type):
- boundary = calculate_boundary(name, nodes)
- boundary = inflate_boundary(name, boundary, inflate_delta)
- cs.add({ 'type': 'district_metering_area', 'id': f"DMA_[{dma}]_{level}_{i}", 'boundary': boundary, 'parent': dma, 'nodes': nodes })
- i += 1
-
- return execute_batch_command(name, cs)
diff --git a/app/native/wndb/s34_sa.py b/app/native/wndb/s34_sa.py
deleted file mode 100644
index ec54c2c..0000000
--- a/app/native/wndb/s34_sa.py
+++ /dev/null
@@ -1,217 +0,0 @@
-from .database import *
-from .s0_base import is_node
-from .s32_region_util import to_postgis_polygon
-from .s32_region import get_region
-
-def get_service_area_schema(name: str) -> dict[str, dict[str, Any]]:
- return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True },
- 'boundary' : {'type': 'tuple_list' , 'optional': False , 'readonly': False },
- 'source' : {'type': 'str' , 'optional': False , 'readonly': False },
- 'time_index' : {'type': 'int' , 'optional': False , 'readonly': False } }
-
-def get_service_area(name: str, id: str) -> dict[str, Any]:
- sa = get_region(name, id)
- if sa == {}:
- return {}
- r = try_read(name, f"select * from region_sa where id = '{id}'")
- if r == None:
- return {}
- sa['source'] = r['source']
- sa['nodes'] = list(eval(r['nodes']))
- sa['time_index'] = r['time_index']
- return sa
-
-def _set_service_area(name: str, cs: ChangeSet) -> DbChangeSet:
- id = cs.operations[0]['id']
-
- new_boundary = cs.operations[0]['boundary']
- old_boundary = get_region(name, id)['boundary']
-
- new_source = cs.operations[0]['source']
- f_new_source = f"'{new_source}'"
-
- new_nodes = cs.operations[0]['nodes']
- str_new_nodes = str(new_nodes).replace("'", "''")
-
- new_time_index = cs.operations[0]['time_index']
-
- old = get_service_area(name, id)
- old_source = old['source']
- f_old_source = f"'{old_source}'"
-
- old_nodes = old['nodes']
- str_old_nodes = str(old_nodes).replace("'", "''")
-
- old_time_index = old['time_index']
-
- redo_sql = f"update region set boundary = st_geomfromtext('{to_postgis_polygon(new_boundary)}') where id = '{id}';"
- redo_sql += f"update region_sa set time_index = {new_time_index}, source = {f_new_source}, nodes = '{str_new_nodes}' where id = '{id}';"
-
- undo_sql = f"update region_sa set time_index = {old_time_index}, source = {f_old_source}, nodes = '{str_old_nodes}' where id = '{id}';"
- undo_sql += f"update region set boundary = st_geomfromtext('{to_postgis_polygon(old_boundary)}') where id = '{id}';"
-
- redo_cs = g_update_prefix | { 'type': 'service_area', 'id': id, 'boundary': new_boundary, 'time_index': new_time_index, 'source': new_source, 'nodes': new_nodes }
- undo_cs = g_update_prefix | { 'type': 'service_area', 'id': id, 'boundary': old_boundary, 'time_index': old_time_index, 'source': old_source, 'nodes': old_nodes }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def set_service_area(name: str, cs: ChangeSet) -> ChangeSet:
- ops = cs.operations
-
- if len(cs.operations) == 0:
- return ChangeSet()
-
- op = ops[0]
-
- if 'id' not in op:
- return ChangeSet()
-
- sa = get_service_area(name, op['id'])
- if sa == {}:
- return ChangeSet()
-
- if 'boundary' not in op:
- op['boundary'] = sa['boundary']
- else:
- b = op['boundary']
- if len(b) < 4 or b[0] != b[-1]:
- return ChangeSet()
-
- if 'time_index' not in op:
- op['time_index'] = sa['time_index']
-
- if 'source' not in op:
- op['source'] = sa['source']
-
- if not is_node(name, op['source']):
- return ChangeSet()
-
- if 'nodes' not in op:
- op['nodes'] = sa['nodes']
- else:
- for node in op['nodes']:
- if not is_node(name, node):
- return ChangeSet()
-
- return execute_command(name, _set_service_area(name, cs))
-
-
-def _add_service_area(name: str, cs: ChangeSet) -> DbChangeSet:
- id = cs.operations[0]['id']
-
- boundary = cs.operations[0]['boundary']
-
- time_index = cs.operations[0]['time_index']
-
- source = cs.operations[0]['source']
- f_source = f"'{source}'"
-
- nodes = cs.operations[0]['nodes']
- str_nodes = str(nodes).replace("'", "''")
-
- redo_sql = f"insert into region (id, boundary, r_type) values ('{id}', '{to_postgis_polygon(boundary)}', 'SA');"
- redo_sql += f"insert into region_sa (id, time_index, source, nodes) values ('{id}', {time_index}, {f_source}, '{str_nodes}');"
-
- undo_sql = f"delete from region_sa where id = '{id}';"
- undo_sql += f"delete from region where id = '{id}';"
-
- redo_cs = g_add_prefix | { 'type': 'service_area', 'id': id, 'boundary': boundary, 'time_index': time_index, 'source': source, 'nodes': nodes }
- undo_cs = g_delete_prefix | { 'type': 'service_area', 'id': id }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def add_service_area(name: str, cs: ChangeSet) -> ChangeSet:
- ops = cs.operations
-
- if len(cs.operations) == 0:
- return ChangeSet()
-
- op = ops[0]
-
- if 'id' not in op:
- return ChangeSet()
-
- sa = get_service_area(name, op['id'])
- if sa != {}:
- return ChangeSet()
-
- if 'boundary' not in op:
- return ChangeSet()
- else:
- b = op['boundary']
- if len(b) < 4 or b[0] != b[-1]:
- return ChangeSet()
-
- if 'time_index' not in op:
- return ChangeSet()
-
- if 'source' not in op:
- return ChangeSet()
-
- if not is_node(name, op['source']):
- return ChangeSet()
-
- if 'nodes' not in op:
- op['nodes'] = []
- else:
- for node in op['nodes']:
- if not is_node(name, node):
- return ChangeSet()
-
- return execute_command(name, _add_service_area(name, cs))
-
-
-def _delete_service_area(name: str, cs: ChangeSet) -> DbChangeSet:
- id = cs.operations[0]['id']
- sa = get_service_area(name, id)
- boundary = sa['boundary']
- time_index = sa['time_index']
- source = sa['source']
- f_source = f"'{source}'"
- nodes = sa['nodes']
- str_nodes = str(nodes).replace("'", "''")
-
- redo_sql = f"delete from region_sa where id = '{id}';"
- redo_sql += f"delete from region where id = '{id}';"
-
- undo_sql = f"insert into region (id, boundary, r_type) values ('{id}', '{to_postgis_polygon(boundary)}', 'SA');"
- undo_sql += f"insert into region_sa (id, time_index, source, nodes) values ('{id}', {time_index}, {f_source}, '{str_nodes}');"
-
- redo_cs = g_delete_prefix | { 'type': 'service_area', 'id': id }
- undo_cs = g_add_prefix | { 'type': 'service_area', 'id': id, 'boundary': boundary, 'time_index': time_index, 'source': source, 'nodes': nodes }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def delete_service_area(name: str, cs: ChangeSet) -> ChangeSet:
- ops = cs.operations
-
- if len(cs.operations) == 0:
- return ChangeSet()
-
- op = ops[0]
-
- if 'id' not in op:
- return ChangeSet()
-
- sa = get_service_area(name, op['id'])
- if sa == {}:
- return ChangeSet()
-
- return execute_command(name, _delete_service_area(name, cs))
-
-
-def get_all_service_area_ids(name: str) -> list[str]:
- ids = []
- for row in read_all(name, f"select id from region_sa"):
- ids.append(row['id'])
- return ids
-
-
-def get_all_service_areas(name: str) -> list[dict[str, Any]]:
- result = []
- for id in get_all_service_area_ids(name):
- result.append(get_service_area(name, id))
- return result
diff --git a/app/native/wndb/s34_sa_cal.py b/app/native/wndb/s34_sa_cal.py
deleted file mode 100644
index 158a350..0000000
--- a/app/native/wndb/s34_sa_cal.py
+++ /dev/null
@@ -1,210 +0,0 @@
-import os
-import platform
-import subprocess
-import uuid
-from queue import Queue
-from typing import Any
-
-from app.infra.epanet.epanet import Output
-
-from .inp_out import dump_inp
-from .project import have_project
-from .s0_base import get_link_nodes, get_node_links
-from .s23_options_util import get_option_v3
-
-
-def _update_section(lines: list[str], section: str, transform) -> list[str]:
- result: list[str] = []
- i = 0
- while i < len(lines):
- line = lines[i]
- if line.strip() == f'[{section}]':
- result.append(line)
- i += 1
- section_lines: list[str] = []
- while i < len(lines) and not lines[i].startswith('['):
- section_lines.append(lines[i])
- i += 1
- result.extend(transform(section_lines))
- continue
- result.append(line)
- i += 1
- return result
-
-
-def _build_service_area_input(name: str, inp_path: str) -> None:
- dump_inp(name, inp_path, '2')
-
- with open(inp_path, encoding='utf-8') as file:
- lines = file.read().splitlines()
-
- unbalanced = get_option_v3(name).get('IF_UNBALANCED', '').strip()
- if unbalanced != '':
- lines = _update_section(
- lines,
- 'OPTIONS',
- lambda option_lines: [
- f'UNBALANCED {unbalanced}' if line.startswith('UNBALANCED ') else line
- for line in option_lines
- ],
- )
-
- with open(inp_path, mode='w', encoding='utf-8') as file:
- file.write('\n'.join(lines) + '\n')
-
-
-def _run_epanet_output(inp_path: str, rpt_path: str, out_path: str) -> dict[str, Any]:
- epanet_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'infra', 'epanet'))
- if platform.system() == 'Windows':
- exe = os.path.join(epanet_dir, 'windows', 'runepanet.exe')
- else:
- exe = os.path.join(epanet_dir, 'linux', 'runepanet')
- if not os.access(exe, os.X_OK):
- os.chmod(exe, 0o755)
-
- env = os.environ.copy()
- if platform.system() == 'Linux':
- lib_dir = os.path.dirname(exe)
- env['LD_LIBRARY_PATH'] = f"{lib_dir}:{env.get('LD_LIBRARY_PATH', '')}"
-
- process = subprocess.run([exe, inp_path, rpt_path, out_path], env=env, capture_output=True, text=True)
- if process.returncode != 0:
- raise RuntimeError(
- f'EPANET failed for [{inp_path}] with code {process.returncode}: '
- f'stdout={process.stdout} stderr={process.stderr}'
- )
-
- return Output(out_path).dump()
-
-
-def _calculate_service_area(name: str, output: dict[str, Any], time_index: int) -> dict[str, list[str]]:
- sources: dict[str, list[str]] = {}
- for node_result in output['node_results']:
- result = node_result['result'][time_index]
- if result['demand'] < 0:
- sources[node_result['node']] = []
-
- link_flows: dict[str, float] = {}
- for link_result in output['link_results']:
- result = link_result['result'][time_index]
- link_flows[link_result['link']] = float(result['flow'])
-
- for source in sources:
- queue: Queue[str] = Queue()
- queue.put(source)
-
- while not queue.empty():
- cursor = queue.get()
- if cursor not in sources[source]:
- sources[source].append(cursor)
-
- links = get_node_links(name, cursor)
- for link in links:
- node1, node2 = get_link_nodes(name, link)
- if node1 == cursor and link_flows[link] > 0:
- queue.put(node2)
- elif node2 == cursor and link_flows[link] < 0:
- queue.put(node1)
-
- concentration_map: dict[str, dict[str, float]] = {}
- node_wip: list[str] = []
- for source, nodes in sources.items():
- for node in nodes:
- if node not in concentration_map:
- concentration_map[node] = {}
- concentration_map[node][source] = 0.0
- if node not in node_wip:
- node_wip.append(node)
-
- for node, concentrations in concentration_map.items():
- if len(concentrations) == 1:
- node_wip.remove(node)
- for source in concentrations.keys():
- concentration_map[node][source] = 1.0
-
- node_upstream: dict[str, list[tuple[str, str]]] = {}
- for node in node_wip:
- node_upstream[node] = []
-
- links = get_node_links(name, node)
- for link in links:
- node1, node2 = get_link_nodes(name, link)
- if node2 == node and link_flows[link] > 0:
- node_upstream[node].append((link, node1))
- elif node1 == node and link_flows[link] < 0:
- node_upstream[node].append((link, node2))
-
- while len(node_wip) != 0:
- done: list[str] = []
- for node in node_wip:
- up_link_nodes = node_upstream[node]
- ready = True
- for link_node in up_link_nodes:
- if link_node[1] in node_wip:
- ready = False
- break
- if not ready:
- continue
-
- for link, upstream_node in up_link_nodes:
- if upstream_node not in concentration_map:
- continue
- for source, concentration in concentration_map[upstream_node].items():
- concentration_map[node][source] += concentration * abs(link_flows[link])
-
- total_concentration = sum(concentration_map[node].values())
- if total_concentration == 0:
- raise RuntimeError(f'Failed to normalize service area concentration for node [{node}] at time [{time_index}]')
-
- for source in concentration_map[node].keys():
- concentration_map[node][source] /= total_concentration
-
- done.append(node)
-
- if len(done) == 0:
- raise RuntimeError(f'Failed to resolve service area graph for time [{time_index}]')
-
- for node in done:
- node_wip.remove(node)
-
- source_to_main_node: dict[str, list[str]] = {}
- for node, concentrations in concentration_map.items():
- max_source = ''
- max_concentration = 0.0
- for source, concentration in concentrations.items():
- if concentration > max_concentration:
- max_concentration = concentration
- max_source = source
- if max_source not in source_to_main_node:
- source_to_main_node[max_source] = []
- source_to_main_node[max_source].append(node)
-
- return source_to_main_node
-
-
-def calculate_service_area(name: str) -> list[dict[str, list[str]]]:
- if not have_project(name):
- raise Exception(f'Not found project [{name}]')
-
- root = os.path.abspath(os.getcwd())
- token = f'{os.getpid()}_{uuid.uuid4().hex}'
- inp_path = os.path.join(root, 'db_inp', f'{name}.service_area.{token}.inp')
- rpt_path = os.path.join(root, 'temp', f'{name}.service_area.{token}.rpt')
- out_path = os.path.join(root, 'temp', f'{name}.service_area.{token}.opt')
-
- os.makedirs(os.path.dirname(inp_path), exist_ok=True)
- os.makedirs(os.path.dirname(rpt_path), exist_ok=True)
-
- try:
- _build_service_area_input(name, inp_path)
- output = _run_epanet_output(inp_path, rpt_path, out_path)
-
- results: list[dict[str, list[str]]] = []
- time_count = len(output['node_results'][0]['result'])
- for time_index in range(time_count):
- results.append(_calculate_service_area(name, output, time_index))
- return results
- finally:
- for path in (inp_path, rpt_path, out_path):
- if os.path.exists(path):
- os.remove(path)
diff --git a/app/native/wndb/s34_sa_gen.py b/app/native/wndb/s34_sa_gen.py
deleted file mode 100644
index 7eb7ff8..0000000
--- a/app/native/wndb/s34_sa_gen.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from .s32_region_util import calculate_boundary, inflate_boundary
-from .s34_sa_cal import *
-from .s34_sa import get_all_service_area_ids
-from .batch_exe import execute_batch_command
-from .database import ChangeSet
-
-def generate_service_area(name: str, inflate_delta: float = 0.5) -> ChangeSet:
- cs = ChangeSet()
-
- for id in get_all_service_area_ids(name):
- cs.delete({'type': 'service_area', 'id': id})
-
- sass = calculate_service_area(name)
-
- time_index = 0
- for sas in sass:
- for source, nodes in sas.items():
- boundary = calculate_boundary(name, nodes)
- boundary = inflate_boundary(name, boundary, inflate_delta)
- cs.add({ 'type': 'service_area', 'id': f"SA_{source}_{time_index}", 'boundary': boundary, 'time_index': time_index, 'source': source, 'nodes': nodes })
- time_index += 1
-
- return execute_batch_command(name, cs)
diff --git a/app/native/wndb/s35_vd.py b/app/native/wndb/s35_vd.py
deleted file mode 100644
index feddd1d..0000000
--- a/app/native/wndb/s35_vd.py
+++ /dev/null
@@ -1,202 +0,0 @@
-from .database import *
-from .s0_base import is_node
-from .s32_region_util import to_postgis_polygon
-from .s32_region import get_region
-
-def get_virtual_district_schema(name: str) -> dict[str, dict[str, Any]]:
- return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True },
- 'boundary' : {'type': 'tuple_list' , 'optional': False , 'readonly': False },
- 'center' : {'type': 'str' , 'optional': False , 'readonly': False } }
-
-def get_virtual_district(name: str, id: str) -> dict[str, Any]:
- vd = get_region(name, id)
- if vd == {}:
- return {}
- r = try_read(name, f"select * from region_vd where id = '{id}'")
- if r == None:
- return {}
- vd['center'] = r['center']
- vd['nodes'] = list(eval(r['nodes']))
- return vd
-
-def _set_virtual_district(name: str, cs: ChangeSet) -> DbChangeSet:
- id = cs.operations[0]['id']
-
- new_boundary = cs.operations[0]['boundary']
- old_boundary = get_region(name, id)['boundary']
-
- new_center = cs.operations[0]['center']
- f_new_center = f"'{new_center}'"
-
- new_nodes = cs.operations[0]['nodes']
- str_new_nodes = str(new_nodes).replace("'", "''")
-
- old = get_virtual_district(name, id)
- old_center = old['center']
- f_old_center = f"'{old_center}'"
-
- old_nodes = old['nodes']
- str_old_nodes = str(old_nodes).replace("'", "''")
-
- redo_sql = f"update region set boundary = st_geomfromtext('{to_postgis_polygon(new_boundary)}') where id = '{id}';"
- redo_sql += f"update region_vd set center = {f_new_center}, nodes = '{str_new_nodes}' where id = '{id}';"
-
- undo_sql = f"update region_vd set center = {f_old_center}, nodes = '{str_old_nodes}' where id = '{id}';"
- undo_sql += f"update region set boundary = st_geomfromtext('{to_postgis_polygon(old_boundary)}') where id = '{id}';"
-
- redo_cs = g_update_prefix | { 'type': 'virtual_district', 'id': id, 'boundary': new_boundary, 'center': new_center, 'nodes': new_nodes }
- undo_cs = g_update_prefix | { 'type': 'virtual_district', 'id': id, 'boundary': old_boundary, 'center': old_center, 'nodes': old_nodes }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def set_virtual_district(name: str, cs: ChangeSet) -> ChangeSet:
- ops = cs.operations
-
- if len(cs.operations) == 0:
- return ChangeSet()
-
- op = ops[0]
-
- if 'id' not in op:
- return ChangeSet()
-
- vd = get_virtual_district(name, op['id'])
- if vd == {}:
- return ChangeSet()
-
- if 'boundary' not in op:
- op['boundary'] = vd['boundary']
- else:
- b = op['boundary']
- if len(b) < 4 or b[0] != b[-1]:
- return ChangeSet()
-
- if 'center' not in op:
- op['center'] = vd['center']
-
- if not is_node(name, op['center']):
- return ChangeSet()
-
- if 'nodes' not in op:
- op['nodes'] = vd['nodes']
- else:
- for node in op['nodes']:
- if not is_node(name, node):
- return ChangeSet()
-
- return execute_command(name, _set_virtual_district(name, cs))
-
-
-def _add_virtual_district(name: str, cs: ChangeSet) -> DbChangeSet:
- id = cs.operations[0]['id']
-
- boundary = cs.operations[0]['boundary']
-
- center = cs.operations[0]['center']
- f_center = f"'{center}'"
-
- nodes = cs.operations[0]['nodes']
- str_nodes = str(nodes).replace("'", "''")
-
- redo_sql = f"insert into region (id, boundary, r_type) values ('{id}', '{to_postgis_polygon(boundary)}', 'VD');"
- redo_sql += f"insert into region_vd (id, center, nodes) values ('{id}', {f_center}, '{str_nodes}');"
-
- undo_sql = f"delete from region_vd where id = '{id}';"
- undo_sql += f"delete from region where id = '{id}';"
-
- redo_cs = g_add_prefix | { 'type': 'virtual_district', 'id': id, 'boundary': boundary, 'center': center, 'nodes': nodes }
- undo_cs = g_delete_prefix | { 'type': 'virtual_district', 'id': id }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def add_virtual_district(name: str, cs: ChangeSet) -> ChangeSet:
- ops = cs.operations
-
- if len(cs.operations) == 0:
- return ChangeSet()
-
- op = ops[0]
-
- if 'id' not in op:
- return ChangeSet()
-
- vd = get_virtual_district(name, op['id'])
- if vd != {}:
- return ChangeSet()
-
- if 'boundary' not in op:
- return ChangeSet()
- else:
- b = op['boundary']
- if len(b) < 4 or b[0] != b[-1]:
- return ChangeSet()
-
- if 'center' not in op:
- return ChangeSet()
-
- if not is_node(name, op['center']):
- return ChangeSet()
-
- if 'nodes' not in op:
- op['nodes'] = []
- else:
- for node in op['nodes']:
- if not is_node(name, node):
- return ChangeSet()
-
- return execute_command(name, _add_virtual_district(name, cs))
-
-
-def _delete_virtual_district(name: str, cs: ChangeSet) -> DbChangeSet:
- id = cs.operations[0]['id']
- vd = get_virtual_district(name, id)
- boundary = vd['boundary']
- center = vd['center']
- f_center = f"'{center}'"
- nodes = vd['nodes']
- str_nodes = str(nodes).replace("'", "''")
-
- redo_sql = f"delete from region_vd where id = '{id}';"
- redo_sql += f"delete from region where id = '{id}';"
-
- undo_sql = f"insert into region (id, boundary, r_type) values ('{id}', '{to_postgis_polygon(boundary)}', 'VD');"
- undo_sql += f"insert into region_vd (id, center, nodes) values ('{id}', {f_center}, '{str_nodes}');"
-
- redo_cs = g_delete_prefix | { 'type': 'virtual_district', 'id': id }
- undo_cs = g_add_prefix | { 'type': 'virtual_district', 'id': id, 'boundary': boundary, 'center': center, 'nodes': nodes }
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def delete_virtual_district(name: str, cs: ChangeSet) -> ChangeSet:
- ops = cs.operations
-
- if len(cs.operations) == 0:
- return ChangeSet()
-
- op = ops[0]
-
- if 'id' not in op:
- return ChangeSet()
-
- vd = get_virtual_district(name, op['id'])
- if vd == {}:
- return ChangeSet()
-
- return execute_command(name, _delete_virtual_district(name, cs))
-
-
-def get_all_virtual_district_ids(name: str) -> list[str]:
- ids = []
- for row in read_all(name, f"select id from region_vd"):
- ids.append(row['id'])
- return ids
-
-
-def get_all_virtual_districts(name: str) -> list[dict[str, Any]]:
- result = []
- for id in get_all_virtual_district_ids(name):
- result.append(get_virtual_district(name, id))
- return result
diff --git a/app/native/wndb/s35_vd_cal.py b/app/native/wndb/s35_vd_cal.py
deleted file mode 100644
index b115814..0000000
--- a/app/native/wndb/s35_vd_cal.py
+++ /dev/null
@@ -1,66 +0,0 @@
-from .database import *
-from .s0_base import get_node_links
-
-
-def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, list[Any]]:
- write(name, 'delete from temp_vd_topology')
-
- # map node name to index
- i = 0
- isolated_nodes = []
- node_index: dict[str, int] = {}
- for row in read_all(name, 'select id from _node'):
- node = str(row['id'])
- if get_node_links(name, node) == []:
- isolated_nodes.append(node)
- continue
- i += 1
- node_index[node] = i
-
- # build topology graph
- pipes = read_all(name, 'select node1, node2, length from pipes')
- for pipe in pipes:
- source = node_index[str(pipe['node1'])]
- target = node_index[str(pipe['node2'])]
- cost = float(pipe['length'])
- write(name, f"insert into temp_vd_topology (source, target, cost) values ({source}, {target}, {cost})")
- pumps = read_all(name, 'select node1, node2 from pumps')
- for pump in pumps:
- source = node_index[str(pump['node1'])]
- target = node_index[str(pump['node2'])]
- write(name, f"insert into temp_vd_topology (source, target, cost) values ({source}, {target}, 0.0)")
- valves = read_all(name, 'select node1, node2 from valves')
- for valve in valves:
- source = node_index[str(valve['node1'])]
- target = node_index[str(valve['node2'])]
- write(name, f"insert into temp_vd_topology (source, target, cost) values ({source}, {target}, 0.0)")
-
- # dijkstra distance
- node_distance: dict[str, dict[str, Any]] = {}
- for center in centers:
- for node, index in node_index.items():
- if node == center:
- node_distance[node] = { 'center': center, 'distance' : 0.0 }
- continue
- # TODO: check none
- distance = float(read(name, f"select max(agg_cost) as distance from pgr_dijkstraCost('select id, source, target, cost from temp_vd_topology', {index}, {node_index[center]}, false)")['distance'])
- if node not in node_distance:
- node_distance[node] = { 'center': center, 'distance' : distance }
- elif distance < node_distance[node]['distance']:
- node_distance[node] = { 'center': center, 'distance' : distance }
-
- write(name, 'delete from temp_vd_topology')
-
- # reorganize the distance result
- center_node: dict[str, list[str]] = {}
- for node, value in node_distance.items():
- if value['center'] not in center_node:
- center_node[value['center']] = []
- center_node[value['center']].append(node)
-
- vds: list[dict[str, Any]] = []
-
- for center, value in center_node.items():
- vds.append({ 'center': center, 'nodes': value })
-
- return { 'virtual_districts': vds, 'isolated_nodes': isolated_nodes }
diff --git a/app/native/wndb/s35_vd_gen.py b/app/native/wndb/s35_vd_gen.py
deleted file mode 100644
index 6a3bc72..0000000
--- a/app/native/wndb/s35_vd_gen.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from .s32_region_util import calculate_boundary, inflate_boundary
-from .s35_vd_cal import *
-from .s35_vd import get_all_virtual_district_ids
-from .batch_exe import execute_batch_command
-
-def generate_virtual_district(name: str, centers: list[str], inflate_delta: float = 0.5) -> ChangeSet:
- cs = ChangeSet()
-
- for id in get_all_virtual_district_ids(name):
- cs.delete({'type': 'virtual_district', 'id': id})
-
- vds = calculate_virtual_district(name, centers)['virtual_districts']
-
- for vd in vds:
- center = vd['center']
- nodes = vd['nodes']
- boundary = calculate_boundary(name, nodes)
- boundary = inflate_boundary(name, boundary, inflate_delta)
- cs.add({ 'type': 'virtual_district', 'id': f"VD_{center}", 'boundary': boundary, 'center': center, 'nodes': nodes })
-
- return execute_batch_command(name, cs)
diff --git a/app/native/wndb/s36_wda.py b/app/native/wndb/s36_wda.py
deleted file mode 100644
index e69de29..0000000
diff --git a/app/native/wndb/s38_scada_info.py b/app/native/wndb/s38_scada_info.py
deleted file mode 100644
index c7b9001..0000000
--- a/app/native/wndb/s38_scada_info.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from .database import *
-
-
-def get_scada_info_schema(name: str) -> dict[str, dict[str, Any]]:
- return {
- "id": {"type": "str", "optional": False, "readonly": True},
- "type": {"type": "str", "optional": False, "readonly": True},
- "x": {"type": "float", "optional": False, "readonly": False},
- "y": {"type": "float", "optional": False, "readonly": False},
- "query_api_id": {"type": "str", "optional": False, "readonly": False},
- "transmission_mode": {"type": "str", "optional": True, "readonly": True},
- "transmission_frequency": {"type": "float", "optional": True, "readonly": True},
- "reliability": {"type": "float", "optional": True, "readonly": True},
- "associated_element_id": {"type": "str", "optional": False, "readonly": True},
- }
-
-
-def get_scada_info(name: str, id: str) -> dict[str, Any]:
- si = try_read(name, f"select * from scada_info where id = '{id}'")
- if si is None:
- return {}
-
- d = {}
- d["id"] = si["id"]
- d["type"] = si["type"]
- d["x"] = float(si["x_coor"])
- d["y"] = float(si["y_coor"])
- d["api_query_id"] = si["api_query_id"]
- d["transmission_mode"] = si.get("transmission_mode")
- d["transmission_frequency"] = si.get("transmission_frequency")
- d["reliability"] = si.get("reliability")
- d["associated_element_id"] = si["associated_element_id"]
-
- return d
-
-
-def get_all_scada_info(name: str) -> list[dict[str, Any]]:
- sis = read_all(name, f"select * from scada_info")
- if sis is None:
- return []
-
- d = []
- for si in sis:
- d.append(
- {
- "id": si["id"],
- "type": si["type"],
- "x": float(si["x_coor"]),
- "y": float(si["y_coor"]),
- "api_query_id": si["api_query_id"],
- "transmission_mode": si.get("transmission_mode"),
- "transmission_frequency": si.get("transmission_frequency"),
- "reliability": si.get("reliability"),
- "associated_element_id": si["associated_element_id"],
- }
- )
-
- return d
diff --git a/app/native/wndb/s40_schema.py b/app/native/wndb/s40_schema.py
deleted file mode 100644
index 4a646ee..0000000
--- a/app/native/wndb/s40_schema.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from .database import *
-from .s0_base import *
-
-
-def get_scheme_schema(name: str) -> dict[str, dict[Any, Any]]:
- return {
- "id": {"type": "str", "optional": False, "readonly": True},
- "name": {"type": "str", "optional": False, "readonly": False},
- "type": {"type": "str", "optional": False, "readonly": False},
- "create_time": {"type": "str", "optional": False, "readonly": True},
- "start_time": {"type": "str", "optional": False, "readonly": True},
- "detail": {"type": "str", "optional": False, "readonly": True},
- }
-
-
-def get_scheme(name: str, schema_name: str) -> dict[Any, Any]:
- t = try_read(name, f"select * from scheme_list where scheme_name = '{schema_name}'")
- if t == None:
- return {}
-
- d = {}
- d["id"] = str(t["scheme_id"])
- d["name"] = str(t["scheme_name"])
- d["type"] = str(t["scheme_type"])
- d["create_time"] = str(t["create_time"])
- d["start_time"] = str(t["start_time"])
- d["detail"] = str(t["detail"])
-
- return d
-
-
-def get_all_schemes(name: str) -> list[dict[Any, Any]]:
- return read_all(name, "select * from scheme_list")
diff --git a/app/native/wndb/s41_pipe_risk_probability.py b/app/native/wndb/s41_pipe_risk_probability.py
deleted file mode 100644
index b441305..0000000
--- a/app/native/wndb/s41_pipe_risk_probability.py
+++ /dev/null
@@ -1,92 +0,0 @@
-from .database import *
-from .connection import project_connection
-from .s0_base import *
-from psycopg.rows import dict_row
-import json
-
-def get_pipe_risk_probability_now(name: str, pipe_id: str) -> dict[str, Any]:
- t = try_read(name, f"select * from pipe_risk_probability where pipeid = '{pipe_id}'")
- if t == None:
- return {}
-
- d = {}
- d['pipeid'] = str(t['pipeid'])
- d['pipeage'] = t['pipeage']
- d['risk_probability_now'] = t['risk_probability_now']
-
- return d
-
-def get_pipe_risk_probability(name: str, pipe_id: str) -> dict[str, Any]:
- t = try_read(name, f"select * from pipe_risk_probability where pipeid = '{pipe_id}'")
- if t == None:
- return {}
-
- d = {}
- d['pipeid'] = t['pipeid']
- d['x'] = t['x']
- d['y'] = t['y']
-
- return d
-
-def get_network_pipe_risk_probability_now(name: str) -> list[dict[str, Any]]:
- pipe_risk_probability_list = []
- with project_connection(name) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- cur.execute(f"select * from pipe_risk_probability")
- for record in cur:
- #pipe_risk_probability_list.append(record)
- t = {}
- t['pipeid'] = record['pipeid']
- t['pipeage'] = record['pipeage']
- t['risk_probability_now'] = record['risk_probability_now']
- pipe_risk_probability_list.append(t)
-
- return pipe_risk_probability_list
-
-def get_pipes_risk_probability(name: str, pipe_ids: list[str]) -> list[dict[str, Any]]:
- pipe_risk_probability_list = []
- with project_connection(name) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- cur.execute(f"select * from pipe_risk_probability")
- for record in cur:
- if record['pipeid'] in pipe_ids:
- t = {}
- t['pipeid'] = record['pipeid']
- t['x'] = record['x']
- t['y'] = record['y']
- pipe_risk_probability_list.append(t)
-
- return pipe_risk_probability_list
-
-def get_pipe_risk_probability_geometries(name: str) -> dict[str, Any]:
- '''
- 获取管道的几何信息
- 返回一个字典,key 是管道的 id,value 是管道的几何信息
- 几何信息是一个字典,包含 start 和 end 两个 key,value 是管道的起点和终点的坐标
- '''
- pipe_risk_probability_geometries = {}
-
- key_pipeId = '编码'
- # key_startnode = '上游节点'
- # key_endnode = '下游节点'
- key_geometry = 'geometry'
-
- with project_connection(name) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- cur.execute(f"select *, ST_AsGeoJSON(geometry) AS {key_geometry} from gis_pipe")
-
- for record in cur:
- id = record[key_pipeId]
- geom = json.loads(record[key_geometry])
-
- pipe_risk_probability_geometries[id] = {
- 'points': geom['coordinates']
- }
-
- for col in record:
- if col != key_geometry:
- pipe_risk_probability_geometries[id][col] = record[col]
-
- # print(len(pipe_risk_probability_geometries))
-
- return pipe_risk_probability_geometries
diff --git a/app/native/wndb/s42_sensor_placement.py b/app/native/wndb/s42_sensor_placement.py
deleted file mode 100644
index 38eb57a..0000000
--- a/app/native/wndb/s42_sensor_placement.py
+++ /dev/null
@@ -1,124 +0,0 @@
-from typing import Any
-
-from psycopg.rows import dict_row
-
-from .connection import project_connection
-from .database import read_all
-
-
-def get_all_sensor_placements(name: str) -> list[dict[str, Any]]:
- return read_all(name, "select * from sensor_placement")
-
-
-def create_sensor_placement(
- name: str,
- *,
- scheme_name: str,
- min_diameter: int,
- username: str,
- sensor_location: list[str],
-) -> dict[str, Any]:
- with project_connection(name) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- cur.execute(
- """
- INSERT INTO sensor_placement (
- scheme_name,
- sensor_number,
- min_diameter,
- username,
- sensor_location
- )
- VALUES (%s, %s, %s, %s, %s)
- RETURNING *
- """,
- (
- scheme_name,
- len(sensor_location),
- min_diameter,
- username,
- sensor_location,
- ),
- )
- created = cur.fetchone()
- if created is None:
- raise RuntimeError("监测点方案写入失败")
- return dict(created)
-
-
-def get_sensor_placement(name: str, scheme_id: int) -> dict[str, Any] | None:
- with project_connection(name) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- cur.execute(
- "SELECT * FROM sensor_placement WHERE id = %s",
- (scheme_id,),
- )
- return cur.fetchone()
-
-
-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:
- with 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 node1 AS node_id, diameter
- FROM pipes
- WHERE node1 = ANY(%s)
- UNION ALL
- SELECT node2 AS node_id, diameter
- FROM pipes
- WHERE node2 = ANY(%s)
- ) AS incident_pipes
- GROUP BY node_id
- )
- SELECT DISTINCT ON (gj.id)
- gj.id AS node_id,
- ipd.max_pipe_diameter,
- gj.elevation,
- ST_X(c.coord) AS project_x,
- ST_Y(c.coord) AS project_y,
- ST_X(gj.geom) AS map_x,
- ST_Y(gj.geom) AS map_y
- FROM geo_junctions_mat AS gj
- JOIN coordinates AS c ON c.node = gj.id
- LEFT JOIN incident_pipe_diameters AS ipd ON ipd.node_id = gj.id
- WHERE gj.id = ANY(%s)
- ORDER BY gj.id
- """,
- (node_ids, node_ids, node_ids),
- )
- return list(cur.fetchall())
-
-
-def update_sensor_placement(
- name: str,
- scheme_id: int,
- *,
- expected_sensor_location: list[str],
- sensor_location: list[str],
-) -> dict[str, Any] | None:
- with project_connection(name) as conn:
- with conn.cursor(row_factory=dict_row) as cur:
- cur.execute(
- """
- UPDATE sensor_placement
- SET sensor_location = %s, sensor_number = %s
- WHERE id = %s AND sensor_location = %s
- RETURNING *
- """,
- (
- sensor_location,
- len(sensor_location),
- scheme_id,
- expected_sensor_location,
- ),
- )
- return cur.fetchone()
diff --git a/app/native/wndb/s43_burst_locate_result.py b/app/native/wndb/s43_burst_locate_result.py
deleted file mode 100644
index ac26463..0000000
--- a/app/native/wndb/s43_burst_locate_result.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from .database import *
-from .s0_base import *
-import json
-
-def get_all_burst_locate_results(name: str) -> list[dict[Any, Any]]:
- return read_all(name, "select * from burst_locate_result")
\ No newline at end of file
diff --git a/app/native/wndb/s8_tags.py b/app/native/wndb/s8_tags.py
deleted file mode 100644
index 3a77576..0000000
--- a/app/native/wndb/s8_tags.py
+++ /dev/null
@@ -1,142 +0,0 @@
-from typing import Any
-from .database import ChangeSet, execute_command, try_read, read_all, DbChangeSet, g_update_prefix
-
-TAG_TYPE_NODE = 'NODE'
-TAG_TYPE_LINK = 'LINK'
-
-def get_tag_schema(name: str) -> dict[str, dict[str, Any]]:
- return { 't_type' : {'type': 'str' , 'optional': False , 'readonly': False},
- 'id' : {'type': 'str' , 'optional': False , 'readonly': False},
- 'tag' : {'type': 'str' , 'optional': True , 'readonly': False},}
-
-
-def get_tags(name: str) -> list[dict[str, Any]]:
- results: list[dict[str, Any]] = []
- rows = read_all(name, "select * from tags_node")
- for row in rows:
- tag = str(row['tag']) if row['tag'] != None else None
- results.append({ 't_type': TAG_TYPE_NODE, 'id': str(row['id']), 'tag': tag })
- rows = read_all(name, "select * from tags_link")
- for row in rows:
- tag = str(row['tag']) if row['tag'] != None else None
- results.append({ 't_type': TAG_TYPE_LINK, 'id': str(row['id']), 'tag': tag })
- return results
-
-
-def get_tag(name: str, t_type: str, id: str) -> dict[str, Any]:
- t = None
- if t_type == TAG_TYPE_NODE:
- t = try_read(name, f"select * from tags_node where id = '{id}'")
- elif t_type == TAG_TYPE_LINK:
- t = try_read(name, f"select * from tags_link where id = '{id}'")
- if t is None:
- return { 't_type': t_type, 'id': id, 'tag': None }
- d = {}
- d['t_type'] = t_type
- d['id'] = str(t['id'])
- d['tag'] = str(t['tag']) if t['tag'] is not None else None
- return d
-
-
-class Tag(object):
- def __init__(self, input: dict[str, Any]) -> None:
- self.type = 'tag'
- self.t_type = str(input['t_type'])
- self.id = str(input['id'])
- self.tag = str(input['tag']) if 'tag' in input and input['tag'] != None else None
-
- self.f_type = f"'{self.type}'"
- self.f_t_type = f"'{self.t_type}'"
- self.f_id = f"'{self.id}'"
- self.f_tag = f"'{self.tag}'" if self.tag != None else 'null'
-
- def as_dict(self) -> dict[str, Any]:
- return { 'type': self.type, 't_type': self.t_type, 'id': self.id, 'tag': self.tag }
-
-
-def _set_tag(name: str, cs: ChangeSet) -> DbChangeSet:
- old = Tag(get_tag(name, cs.operations[0]['t_type'], cs.operations[0]['id']))
- raw_new = get_tag(name, cs.operations[0]['t_type'], cs.operations[0]['id'])
-
- new_dict = cs.operations[0]
- schema = get_tag_schema(name)
- for key, value in schema.items():
- if key in new_dict and not value['readonly']:
- raw_new[key] = new_dict[key]
- new = Tag(raw_new)
-
- table = ''
- if cs.operations[0]['t_type'] == TAG_TYPE_NODE:
- table = 'tags_node'
- elif cs.operations[0]['t_type'] == TAG_TYPE_LINK:
- table = 'tags_link'
- else:
- raise Exception('Only support NODE and Link')
-
- redo_sql = f"delete from {table} where id = {new.f_id};"
- if new.tag is not None:
- redo_sql += f"\ninsert into {table} (id, tag) values ({new.f_id}, {new.f_tag});"
-
- undo_sql = f"delete from {table} where id = {old.f_id};"
- if old.tag is not None:
- undo_sql += f"\ninsert into {table} (id, tag) values ({old.f_id}, {old.f_tag});"
-
- redo_cs = g_update_prefix | new.as_dict()
- undo_cs = g_update_prefix | old.as_dict()
-
- return DbChangeSet(redo_sql, undo_sql, [redo_cs], [undo_cs])
-
-
-def set_tag(name: str, cs: ChangeSet) -> ChangeSet:
- if 't_type' not in cs.operations[0] or 'id' not in cs.operations[0] or 'tag' not in cs.operations[0]:
- return ChangeSet()
- return execute_command(name, _set_tag(name, cs))
-
-
-def inp_in_tag(line: str) -> str:
- tokens = line.split()
-
- num = len(tokens)
- has_desc = tokens[-1].startswith(';')
- num_without_desc = (num - 1) if has_desc else num
-
- t_type = str(tokens[0].upper())
- id = str(tokens[1])
- tag = str(tokens[2])
-
- if t_type == TAG_TYPE_NODE:
- return str(f"insert into tags_node (id, tag) values ('{id}', '{tag}');")
- elif t_type == TAG_TYPE_LINK:
- return str(f"insert into tags_link (id, tag) values ('{id}', '{tag}');")
- return str('')
-
-
-def inp_out_tag(name: str) -> list[str]:
- lines = []
- objs = read_all(name, 'select * from tags_node')
- for obj in objs:
- t_type = TAG_TYPE_NODE
- id = obj['id']
- tag = obj['tag']
- lines.append(f'{t_type} {id} {tag}')
- objs = read_all(name, 'select * from tags_link')
- for obj in objs:
- t_type = TAG_TYPE_LINK
- id = obj['id']
- tag = obj['tag']
- lines.append(f'{t_type} {id} {tag}')
- return lines
-
-
-def delete_tag_by_node(name: str, node: str) -> ChangeSet:
- row = try_read(name, f"select * from tags_node where id = '{node}'")
- if row is None:
- return ChangeSet()
- return ChangeSet(g_update_prefix | {'type': 'tag', 't_type': TAG_TYPE_NODE, 'id': node, 'tag': None })
-
-
-def delete_tag_by_link(name: str, link: str) -> ChangeSet:
- row = try_read(name, f"select * from tags_link where id = '{link}'")
- if row is None:
- return ChangeSet()
- return ChangeSet(g_update_prefix | {'type': 'tag', 't_type': TAG_TYPE_LINK, 'id': link, 'tag': None })
diff --git a/app/services/burst_detection.py b/app/services/burst_detection.py
index e9d35a4..9f45acb 100644
--- a/app/services/burst_detection.py
+++ b/app/services/burst_detection.py
@@ -3,6 +3,7 @@ from __future__ import annotations
from collections import Counter
from datetime import datetime, timedelta
from typing import Any
+from uuid import UUID
import numpy as np
import pandas as pd
@@ -49,8 +50,7 @@ def run_burst_detection(
sensor_nodes: list[str] | None = None,
scheme_name: str | None = None,
data_source: str = "monitoring",
- simulation_scheme_name: str | None = None,
- simulation_scheme_type: str | None = None,
+ simulation_run_id: UUID | None = None,
) -> dict[str, Any]:
"""
运行爆管侦测服务入口。
@@ -153,18 +153,17 @@ def run_burst_detection(
else _get_pressure_sensor_nodes(network)
)
if data_source == "simulation":
- if not simulation_scheme_name:
- raise ValueError("模拟方案模式必须提供 simulation_scheme_name。")
+ if not simulation_run_id:
+ raise ValueError("模拟数据模式必须提供 simulation_run_id。")
observed_df = _build_observed_pressure_from_simulation(
network=network,
sensor_nodes=scada_sensor_nodes,
scada_start=scada_start,
scada_end=scada_end,
- simulation_scheme_name=simulation_scheme_name,
- simulation_scheme_type=simulation_scheme_type,
+ simulation_run_id=simulation_run_id,
)
observed_input = observed_df
- observed_source = "simulation_scheme_timerange"
+ observed_source = "analysis_run_timerange"
else:
observed_df = _build_observed_pressure_from_scada(
network=network,
@@ -224,9 +223,8 @@ def run_burst_detection(
}
if data_source == "simulation":
payload["data_source"] = "simulation"
- payload["simulation_scheme"] = {
- "name": simulation_scheme_name,
- "type": simulation_scheme_type,
+ payload["simulation_run"] = {
+ "run_id": str(simulation_run_id),
}
else:
payload["data_source"] = "monitoring"
@@ -299,11 +297,10 @@ def _build_observed_pressure_from_simulation(
sensor_nodes: list[str],
scada_start: datetime | str | None,
scada_end: datetime | str | None,
- simulation_scheme_name: str | None,
- simulation_scheme_type: str | None = None,
+ simulation_run_id: UUID,
) -> pd.DataFrame:
if scada_start is None or scada_end is None:
- raise ValueError("使用模拟方案查询时必须同时提供 scada_start 与 scada_end。")
+ raise ValueError("使用分析模拟数据时必须同时提供 scada_start 与 scada_end。")
start_dt = _to_datetime(scada_start)
end_dt = _to_datetime(scada_end)
@@ -316,12 +313,9 @@ def _build_observed_pressure_from_simulation(
# Check for missing nodes in simulation result if needed, but InternalQueries handles some of it.
# We assume sensor_nodes are valid pressure nodes.
- scheme_type = simulation_scheme_type or "burst_analysis"
-
- simulation_data = InternalQueries.query_scheme_simulation_by_ids_timerange(
+ simulation_data = InternalQueries.query_analysis_simulation_by_ids_timerange(
db_name=network,
- scheme_type=scheme_type,
- scheme_name=simulation_scheme_name,
+ run_id=simulation_run_id,
element_ids=sensor_nodes,
start_time=start_dt.isoformat(),
end_time=end_dt.isoformat(),
@@ -646,8 +640,8 @@ def _resolve_sampling_interval_minutes(
inferred_intervals = [
parsed
for item in get_all_scada_info(network)
- if str(item.get("type", "")).lower() == "pressure"
- and str(item.get("associated_element_id", "")) in selected_nodes
+ if str(item.get("device_type", "")).lower() == "pressure"
+ and str(item.get("node_id", "")) in selected_nodes
and (
parsed := _parse_sampling_interval_minutes(
item.get("transmission_frequency")
@@ -685,9 +679,9 @@ def _parse_sampling_interval_minutes(value: Any) -> int | None:
def _get_pressure_sensor_mapping(network: str) -> dict[str, str]:
node_query_id: dict[str, str] = {}
for item in get_all_scada_info(network):
- if str(item.get("type", "")).lower() != "pressure":
+ if str(item.get("device_type", "")).lower() != "pressure":
continue
- node_id = item.get("associated_element_id")
+ node_id = item.get("node_id")
query_id = item.get("api_query_id")
if node_id and query_id is not None:
node_query_id[str(node_id)] = str(query_id)
@@ -697,14 +691,16 @@ def _get_pressure_sensor_mapping(network: str) -> dict[str, str]:
def _get_pressure_sensor_nodes(network: str) -> list[str]:
sensor_nodes: list[str] = []
for item in get_all_scada_info(network):
- if str(item.get("type", "")).lower() != "pressure":
+ if str(item.get("device_type", "")).lower() != "pressure":
continue
- node_id = item.get("associated_element_id")
+ node_id = item.get("node_id")
if isinstance(node_id, str) and node_id:
sensor_nodes.append(node_id)
sensor_nodes = list(dict.fromkeys(sensor_nodes))
if not sensor_nodes:
- raise ValueError("未找到压力传感器对应节点(scada_info.type=pressure)。")
+ raise ValueError(
+ "未找到压力传感器对应节点(asset.scada_devices.device_type=pressure)。"
+ )
return sensor_nodes
diff --git a/app/services/burst_location.py b/app/services/burst_location.py
index 589d122..517b95d 100644
--- a/app/services/burst_location.py
+++ b/app/services/burst_location.py
@@ -3,6 +3,7 @@ from __future__ import annotations
import os
from datetime import datetime, timedelta
from typing import Any
+from uuid import UUID
import pandas as pd
@@ -11,7 +12,7 @@ from app.infra.db.timescaledb.internal_queries import InternalQueries
from app.services.scheme_management import (
query_burst_location_scheme_detail,
query_burst_location_schemes,
- query_scheme_list,
+ get_analysis_run,
scheme_name_exists,
store_scheme_info,
)
@@ -21,7 +22,6 @@ from app.services.time_api import extract_date, parse_utc_time, utc_now
SeriesInput = pd.Series | dict[str, Any] | list[dict[str, Any]]
FLOW_SCADA_TYPES = {"pipe_flow", "flow", "demand"}
SIMULATION_DATA_SOURCES = {"monitoring", "simulation"}
-DEFAULT_SIMULATION_SCHEME_TYPE = "burst_analysis"
def _normalize_series(data: SeriesInput, field_name: str) -> pd.Series:
@@ -65,16 +65,12 @@ def run_burst_location_by_network(
scada_normal_end: datetime | str | None = None,
use_scada_flow: bool = False,
scheme_name: str | None = None,
- simulation_scheme_name: str | None = None,
- simulation_scheme_type: str | None = None,
+ simulation_run_id: UUID | None = None,
) -> dict[str, Any]:
if not network:
raise ValueError("network is required.")
normalized_data_source = _normalize_data_source(
- data_source, simulation_scheme_name=simulation_scheme_name
- )
- resolved_simulation_scheme_type = (
- simulation_scheme_type or DEFAULT_SIMULATION_SCHEME_TYPE
+ data_source, simulation_run_id=simulation_run_id
)
selected_pressure_ids = (
@@ -123,8 +119,8 @@ def run_burst_location_by_network(
else None
)
if normalized_data_source == "simulation":
- if not simulation_scheme_name:
- raise ValueError("模拟方案模式必须提供 simulation_scheme_name。")
+ if not simulation_run_id:
+ raise ValueError("模拟数据模式必须提供 simulation_run_id。")
normal_start_dt = burst_start_dt
normal_end_dt = burst_end_dt
(
@@ -137,9 +133,8 @@ def run_burst_location_by_network(
end_dt=burst_end_dt,
data_type="pressure",
series_name="burst_pressure",
- simulation_source="scheme",
- simulation_scheme_name=simulation_scheme_name,
- simulation_scheme_type=resolved_simulation_scheme_type,
+ simulation_source="analysis",
+ simulation_run_id=simulation_run_id,
)
(
normal_pressure_series,
@@ -152,10 +147,9 @@ def run_burst_location_by_network(
data_type="pressure",
series_name="normal_pressure",
simulation_source="realtime",
- simulation_scheme_name=None,
- simulation_scheme_type=resolved_simulation_scheme_type,
+ simulation_run_id=None,
)
- observed_source = "simulation_scheme_burst_realtime_normal_timerange"
+ observed_source = "analysis_run_burst_realtime_normal_timerange"
else:
if normal_pressure_from_payload is None and (
normal_start_dt is None or normal_end_dt is None
@@ -230,8 +224,8 @@ def run_burst_location_by_network(
else None
)
if normalized_data_source == "simulation":
- if not simulation_scheme_name:
- raise ValueError("模拟方案模式必须提供 simulation_scheme_name。")
+ if not simulation_run_id:
+ raise ValueError("模拟数据模式必须提供 simulation_run_id。")
burst_flow_series, burst_flow_samples = (
_build_observed_series_from_simulation(
network=network,
@@ -240,9 +234,8 @@ def run_burst_location_by_network(
end_dt=burst_end_dt,
data_type="flow",
series_name="burst_flow",
- simulation_source="scheme",
- simulation_scheme_name=simulation_scheme_name,
- simulation_scheme_type=resolved_simulation_scheme_type,
+ simulation_source="analysis",
+ simulation_run_id=simulation_run_id,
)
)
normal_flow_series, normal_flow_samples = (
@@ -254,8 +247,7 @@ def run_burst_location_by_network(
data_type="flow",
series_name="normal_flow",
simulation_source="realtime",
- simulation_scheme_name=None,
- simulation_scheme_type=resolved_simulation_scheme_type,
+ simulation_run_id=None,
)
)
else:
@@ -354,14 +346,12 @@ def run_burst_location_by_network(
}
)
if normalized_data_source == "simulation":
- simulation_burst_ids = _get_simulation_scheme_burst_ids(
+ simulation_burst_ids = _get_simulation_run_burst_ids(
network=network,
- scheme_name=simulation_scheme_name,
- scheme_type=resolved_simulation_scheme_type,
+ run_id=simulation_run_id,
)
- payload["simulation_scheme"] = {
- "name": simulation_scheme_name,
- "type": resolved_simulation_scheme_type,
+ payload["simulation_run"] = {
+ "run_id": str(simulation_run_id),
"burst_ids": simulation_burst_ids,
}
if scheme_name:
@@ -471,20 +461,15 @@ def _validate_time_window(
return start_dt, end_dt
-def _get_simulation_scheme_burst_ids(
- *, network: str, scheme_name: str | None, scheme_type: str
+def _get_simulation_run_burst_ids(
+ *, network: str, run_id: UUID | None
) -> list[str]:
- if not scheme_name:
+ if run_id is None:
return []
- rows = query_scheme_list(network, scheme_type=scheme_type) or []
- for row in rows:
- if len(row) < 7:
- continue
- if row[1] != scheme_name or row[2] != scheme_type:
- continue
- detail = row[6] if isinstance(row[6], dict) else {}
- return _normalize_burst_ids(detail.get("burst_ID"))
- return []
+ run = get_analysis_run(network, run_id)
+ if not run:
+ raise ValueError(f"未找到模拟运行: {run_id}")
+ return _normalize_burst_ids(run["parameters"].get("burst_ID"))
def _normalize_burst_ids(value: Any) -> list[str]:
@@ -566,8 +551,7 @@ def _build_observed_series_from_simulation(
data_type: str,
series_name: str,
simulation_source: str,
- simulation_scheme_name: str | None,
- simulation_scheme_type: str,
+ simulation_run_id: UUID | None,
) -> tuple[pd.Series, int]:
sensor_ids = _dedupe_ids(sensor_ids)
sensor_metadata = _build_sensor_metadata(network=network, data_type=data_type)
@@ -586,8 +570,7 @@ def _build_observed_series_from_simulation(
end_dt=end_dt,
data_type=data_type,
simulation_source=simulation_source,
- simulation_scheme_name=simulation_scheme_name,
- simulation_scheme_type=simulation_scheme_type,
+ simulation_run_id=simulation_run_id,
)
simulation_data = _normalize_timeseries_by_id(simulation_data)
values: dict[str, float] = {}
@@ -625,10 +608,9 @@ def _query_simulation_data_by_sensor_ids(
end_dt: datetime,
data_type: str,
simulation_source: str,
- simulation_scheme_name: str | None,
- simulation_scheme_type: str,
+ simulation_run_id: UUID | None,
) -> dict[str, list[dict[str, Any]]]:
- if simulation_source not in {"scheme", "realtime"}:
+ if simulation_source not in {"analysis", "realtime"}:
raise ValueError(f"Unsupported simulation_source: {simulation_source}")
sensor_ids = _dedupe_ids(sensor_ids)
@@ -645,8 +627,7 @@ def _query_simulation_data_by_sensor_ids(
start_dt=start_dt,
end_dt=end_dt,
simulation_source=simulation_source,
- simulation_scheme_name=simulation_scheme_name,
- simulation_scheme_type=simulation_scheme_type,
+ simulation_run_id=simulation_run_id,
)
)
return result
@@ -679,8 +660,7 @@ def _query_simulation_data_by_sensor_ids(
start_dt=start_dt,
end_dt=end_dt,
simulation_source=simulation_source,
- simulation_scheme_name=simulation_scheme_name,
- simulation_scheme_type=simulation_scheme_type,
+ simulation_run_id=simulation_run_id,
)
)
if demand_ids:
@@ -693,8 +673,7 @@ def _query_simulation_data_by_sensor_ids(
start_dt=start_dt,
end_dt=end_dt,
simulation_source=simulation_source,
- simulation_scheme_name=simulation_scheme_name,
- simulation_scheme_type=simulation_scheme_type,
+ simulation_run_id=simulation_run_id,
)
)
return result
@@ -709,19 +688,17 @@ def _query_simulation_values(
start_dt: datetime,
end_dt: datetime,
simulation_source: str,
- simulation_scheme_name: str | None,
- simulation_scheme_type: str,
+ simulation_run_id: UUID | None,
) -> dict[str, list[dict[str, Any]]]:
element_ids = _dedupe_ids(element_ids)
if not element_ids:
return {}
- if simulation_source == "scheme":
- if not simulation_scheme_name:
- raise ValueError("读取方案模拟数据时必须提供 simulation_scheme_name。")
- return InternalQueries.query_scheme_simulation_by_ids_timerange(
+ if simulation_source == "analysis":
+ if not simulation_run_id:
+ raise ValueError("读取分析模拟数据时必须提供 simulation_run_id。")
+ return InternalQueries.query_analysis_simulation_by_ids_timerange(
db_name=network,
- scheme_type=simulation_scheme_type,
- scheme_name=simulation_scheme_name,
+ run_id=simulation_run_id,
element_ids=element_ids,
start_time=start_dt.isoformat(),
end_time=end_dt.isoformat(),
@@ -743,7 +720,7 @@ def _query_simulation_values(
def _build_sensor_metadata(network: str, data_type: str) -> dict[str, dict[str, str]]:
metadata: dict[str, dict[str, str]] = {}
for item in get_all_scada_info(network):
- scada_type = str(item.get("type", "")).lower()
+ scada_type = str(item.get("device_type", "")).lower()
if data_type == "pressure":
if scada_type != "pressure":
continue
@@ -752,7 +729,7 @@ def _build_sensor_metadata(network: str, data_type: str) -> dict[str, dict[str,
continue
else:
raise ValueError(f"Unsupported data_type: {data_type}")
- element_id = _normalize_identifier(item.get("associated_element_id"))
+ element_id = _normalize_identifier(item.get("node_id") or item.get("link_id"))
query_id = _normalize_identifier(item.get("api_query_id"))
if element_id and query_id:
metadata[element_id] = {"query_id": query_id, "scada_type": scada_type}
@@ -765,11 +742,11 @@ def _build_scada_mapping(network: str, data_type: str) -> dict[str, str]:
def _normalize_data_source(
- data_source: str | None, simulation_scheme_name: str | None = None
+ data_source: str | None, simulation_run_id: UUID | None = None
) -> str:
normalized = str(data_source or "").strip().lower()
if not normalized:
- return "simulation" if simulation_scheme_name else "monitoring"
+ return "simulation" if simulation_run_id else "monitoring"
if normalized not in SIMULATION_DATA_SOURCES:
allowed_sources = ", ".join(sorted(SIMULATION_DATA_SOURCES))
raise ValueError(
@@ -783,7 +760,7 @@ def _get_sensor_nodes(network: str, data_type: str) -> list[str]:
sensor_ids = sorted(mapping.keys())
if not sensor_ids:
type_name = "压力" if data_type == "pressure" else "流量"
- raise ValueError(f"未找到{type_name}传感器对应节点(scada_info.type)。")
+ raise ValueError(f"未找到{type_name}传感器对应节点(asset.scada_devices.device_type)。")
return sensor_ids
diff --git a/app/services/globals.py b/app/services/globals.py
index 681ed05..9df7d0a 100644
--- a/app/services/globals.py
+++ b/app/services/globals.py
@@ -1,54 +1,14 @@
-# simulation.py中的全局变量
-# reservoir basic height
-RESERVOIR_BASIC_HEIGHT = float(250.35)
-PATTERN_TIME_STEP = None # 浮点数
-# 实时数据类:element_id和api_query_id对应
-reservoirs_id = {}
-tanks_id = {}
-fixed_pumps_id ={}
-variable_pumps_id = {}
-pressure_id = {}
-demand_id = {}
-quality_id = {}
-# 实时数据类:pattern_id和api_query_id对应
-source_outflow_pattern_id = {}
-realtime_pipe_flow_pattern_id = {}
-pipe_flow_region_patterns = {} # 根据realtime的pipe_flow,对non_realtime的demand进行分区
-# 分区查询
-source_outflow_region = {} # 以绑定的管段作为value
-source_outflow_region_id = {} # 以api_query_id作为value
-source_outflow_region_patterns = {} # 以associated_pattern作为value
-# 非实时数据的pattern
-non_realtime_region_patterns = {} # 基于source_outflow_region进行区分
-realtime_region_pipe_flow_and_demand_id = {} # 基于source_outflow_region搜索该分区中的实时pipe_flow和demand的api_query_id,后续用region的流量 - 实时流量计的流量
-realtime_region_pipe_flow_and_demand_patterns = {} # 基于source_outflow_region搜索该分区中的实时pipe_flow和demand的associated_pattern,后续用region的流量 - 实时流量计的流量
-# ---------------------------------------------------------
-# 全局变量,用于存储不同类型的realtime api_query_id
-reservoir_liquid_level_realtime_ids = []
-tank_liquid_level_realtime_ids = []
-fixed_pump_realtime_ids = []
-variable_pump_realtime_ids = []
-source_outflow_realtime_ids = []
-pipe_flow_realtime_ids = []
-pressure_realtime_ids = []
-demand_realtime_ids = []
-quality_realtime_ids = []
-# transmission_frequency的最大值
-transmission_frequency = None
-hydraulic_timestep = None # 时间字符串
-reservoir_liquid_level_non_realtime_ids = []
-tank_liquid_level_non_realtime_ids = []
-fixed_pump_non_realtime_ids = []
-variable_pump_non_realtime_ids = []
-source_outflow_non_realtime_ids = []
-pipe_flow_non_realtime_ids = []
-pressure_non_realtime_ids = []
-demand_non_realtime_ids = []
-quality_non_realtime_ids = []
+"""Mutable state used by the legacy synchronous simulation runner."""
-# api_query_id和associated_element_id对应,不包含液位和泵
-scheme_source_outflow_ids = {}
-scheme_pipe_flow_ids = {}
-scheme_pressure_ids = {}
-scheme_demand_ids = {}
-scheme_quality_ids = {}
+RESERVOIR_BASIC_HEIGHT = 250.35
+PATTERN_TIME_STEP: float | None = None
+hydraulic_timestep: str | None = None
+
+# Element ID -> SCADA api_query_id, loaded per project before simulation.
+reservoirs_id: dict[str, str] = {}
+tanks_id: dict[str, str] = {}
+fixed_pumps_id: dict[str, str] = {}
+variable_pumps_id: dict[str, str] = {}
+pressure_id: dict[str, str] = {}
+demand_id: dict[str, str] = {}
+quality_id: dict[str, str] = {}
diff --git a/app/services/leakage_identifier.py b/app/services/leakage_identifier.py
index a85d653..beacc04 100644
--- a/app/services/leakage_identifier.py
+++ b/app/services/leakage_identifier.py
@@ -194,18 +194,18 @@ def get_leakage_identify_scheme_detail(
def _get_pressure_sensor_nodes(network: str) -> list[str]:
- scada_info = get_all_scada_info(network)
+ scada_devices = get_all_scada_info(network)
sensor_nodes: list[str] = []
- for item in scada_info:
- scada_type = str(item.get("type", "")).lower()
+ for item in scada_devices:
+ scada_type = str(item.get("device_type", "")).lower()
if scada_type != "pressure":
continue
- node_id = item.get("associated_element_id")
+ node_id = item.get("node_id")
if isinstance(node_id, str) and node_id:
sensor_nodes.append(node_id)
sensor_nodes = list(dict.fromkeys(sensor_nodes))
if not sensor_nodes:
- raise ValueError("未找到压力传感器对应节点(scada_info.type=pressure)。")
+ raise ValueError("未找到关联节点的压力 SCADA 设备。")
return sensor_nodes
@@ -462,9 +462,9 @@ def _build_observed_pressure_from_scada(
node_query_id: dict[str, str] = {}
for item in get_all_scada_info(network):
- if str(item.get("type", "")).lower() != "pressure":
+ if str(item.get("device_type", "")).lower() != "pressure":
continue
- node_id = item.get("associated_element_id")
+ node_id = item.get("node_id")
query_id = item.get("api_query_id")
if (
isinstance(node_id, str)
diff --git a/app/services/network_import.py b/app/services/network_import.py
index 378ffa2..7a98c0a 100644
--- a/app/services/network_import.py
+++ b/app/services/network_import.py
@@ -1,196 +1,6 @@
-import csv
-import os
-
-import chardet
-import psycopg
-from psycopg import sql
-
-from app.infra.db.project_routing import get_project_pgconn_string
from app.services.tjnetwork import read_inp
-############################################################
-# network_update 10
-############################################################
-
-
def network_update(file_path: str, project_code: str) -> None:
- """
- 更新pg数据库中的inp文件
- :param file_path: inp文件
- :param project_code: 元数据项目代码
- :return:
- """
+ """Replace one project's hydraulic model from an EPANET INP file."""
read_inp(project_code, file_path)
-
- csv_path = "./history_pattern_flow.csv"
-
- # # 检查文件是否存在
- # if os.path.exists(csv_path):
- # print(f"history_patterns_flows文件存在,开始处理...")
- #
- # # 读取 CSV 文件
- # df = pd.read_csv(csv_path)
- #
- # # 连接到 PostgreSQL 数据库(这里是数据库 "bb")
- # with psycopg.connect("dbname=bb host=127.0.0.1") as conn:
- # with conn.cursor() as cur:
- # for index, row in df.iterrows():
- # # 直接将数据插入,不进行唯一性检查
- # insert_sql = sql.SQL("""
- # INSERT INTO history_patterns_flows (id, factor, flow)
- # VALUES (%s, %s, %s);
- # """)
- # # 将数据插入数据库
- # cur.execute(insert_sql, (row['id'], row['factor'], row['flow']))
- # conn.commit()
- # print("数据成功导入到 'history_patterns_flows' 表格。")
- # else:
- # print(f"history_patterns_flows文件不存在。")
- # 检查文件是否存在
- if os.path.exists(csv_path):
- print(f"history_patterns_flows文件存在,开始处理...")
-
- with psycopg.connect(get_project_pgconn_string(project_code)) as conn:
- with conn.cursor() as cur:
- with open(csv_path, newline="", encoding="utf-8-sig") as csvfile:
- reader = csv.DictReader(csvfile)
- for row in reader:
- # 直接将数据插入,不进行唯一性检查
- insert_sql = sql.SQL(
- """
- INSERT INTO history_patterns_flows (id, factor, flow)
- VALUES (%s, %s, %s);
- """
- )
- # 将数据插入数据库
- cur.execute(insert_sql, (row["id"], row["factor"], row["flow"]))
- conn.commit()
- print("数据成功导入到 'history_patterns_flows' 表格。")
- else:
- print(f"history_patterns_flows文件不存在。")
-
-
-def submit_scada_info(name: str, coord_id: str) -> None:
- """
- 将scada信息表导入pg数据库
- :param name: 项目名称(数据库名称)
- :param coord_id: 坐标系的id,如4326,根据原始坐标信息输入
- :return:
- """
- scada_info_path = "./scada_info.csv"
- # 检查文件是否存在
- if os.path.exists(scada_info_path):
- print(f"scada_info文件存在,开始处理...")
-
- # 自动检测文件编码
- with open(scada_info_path, "rb") as file:
- raw_data = file.read()
- detected = chardet.detect(raw_data)
- file_encoding = detected["encoding"]
- print(f"检测到的文件编码:{file_encoding}")
- try:
- # 动态替换数据库名称
- conn_string = get_project_pgconn_string(db_name=name)
-
- # 连接到 PostgreSQL 数据库(这里是数据库 "bb")
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- # 检查 scada_info 表是否为空
- cur.execute("SELECT COUNT(*) FROM scada_info;")
- count = cur.fetchone()[0]
-
- if count > 0:
- print("scada_info表中已有数据,正在清空记录...")
- cur.execute("DELETE FROM scada_info;")
- print("表记录已清空。")
-
- with open(
- scada_info_path, newline="", encoding=file_encoding
- ) as csvfile:
- reader = csv.DictReader(csvfile)
- for row in reader:
- # 将CSV单元格值为空的字段转换为 None
- cleaned_row = {
- key: (value if value.strip() else None)
- for key, value in row.items()
- }
-
- # 处理 associated_source_outflow_id 列动态变化
- associated_columns = [
- f"associated_source_outflow_id{i}" for i in range(1, 21)
- ]
- associated_values = [
- (
- cleaned_row.get(col).strip()
- if cleaned_row.get(col)
- and cleaned_row.get(col).strip()
- else None
- )
- for col in associated_columns
- ]
-
- # 将 X_coor 和 Y_coor 转换为 geometry 类型
- x_coor = (
- float(cleaned_row["X_coor"])
- if cleaned_row["X_coor"]
- else None
- )
- y_coor = (
- float(cleaned_row["Y_coor"])
- if cleaned_row["Y_coor"]
- else None
- )
- coord = (
- f"SRID={coord_id};POINT({x_coor} {y_coor})"
- if x_coor and y_coor
- else None
- )
-
- # 准备插入 SQL 语句
- insert_sql = sql.SQL(
- """
- INSERT INTO scada_info (
- id, type, associated_element_id, associated_pattern,
- associated_pipe_flow_id, {associated_columns},
- API_query_id, transmission_mode, transmission_frequency,
- reliability, X_coor, Y_coor, coord
- )
- VALUES (
- %s, %s, %s, %s, %s, {associated_placeholders},
- %s, %s, %s, %s, %s, %s, %s
- );
- """
- ).format(
- associated_columns=sql.SQL(", ").join(
- sql.Identifier(col) for col in associated_columns
- ),
- associated_placeholders=sql.SQL(", ").join(
- sql.Placeholder() for _ in associated_columns
- ),
- )
- # 将数据插入数据库
- cur.execute(
- insert_sql,
- (
- cleaned_row["id"],
- cleaned_row["type"],
- cleaned_row["associated_element_id"],
- cleaned_row.get("associated_pattern"),
- cleaned_row.get("associated_pipe_flow_id"),
- *associated_values,
- cleaned_row.get("API_query_id"),
- cleaned_row["transmission_mode"],
- cleaned_row["transmission_frequency"],
- cleaned_row["reliability"],
- x_coor,
- y_coor,
- coord,
- ),
- )
- conn.commit()
- print("数据成功导入到 'scada_info' 表格。")
- except Exception as e:
- print(f"导入时出错:{e}")
- else:
- print(f"scada_info文件不存在。")
diff --git a/app/services/scheme_management.py b/app/services/scheme_management.py
index b0bfe06..516c5f1 100644
--- a/app/services/scheme_management.py
+++ b/app/services/scheme_management.py
@@ -1,43 +1,23 @@
-import ast
-import json
from datetime import date, datetime
+from typing import Any
+from uuid import UUID, uuid4
-import geopandas as gpd
-import pandas as pd
-import psycopg
-from sqlalchemy import create_engine
+from psycopg.types.json import Jsonb
-from app.infra.db.project_routing import get_project_pgconn_string
+from app.native.wndb.core.connection import project_connection
from app.services.time_api import parse_utc_time
-# 2025/03/23
def scheme_name_exists(name: str, scheme_name: str) -> bool:
- """
- 判断传入的 scheme_name 是否已存在于 scheme_list 表中,用于输入框判断
- :param name: 数据库名称
- :param scheme_name: 需要判断的方案名称
- :return: 如果存在返回 True,否则返回 False
- """
- try:
- conn_string = get_project_pgconn_string(db_name=name)
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- cur.execute(
- "SELECT COUNT(*) FROM scheme_list WHERE scheme_name = %s",
- (scheme_name,),
- )
- result = cur.fetchone()
- if result is not None and result[0] > 0:
- return True
- else:
- return False
- except Exception as e:
- print(f"查询 scheme_name 时出错:{e}")
- return False
+ with project_connection(name) as conn, conn.cursor() as cur:
+ cur.execute(
+ "select exists(select 1 from analysis.runs where name = %s)",
+ (scheme_name,),
+ )
+ row = cur.fetchone()
+ return bool(row and row[0])
-# 2025/03/23
def store_scheme_info(
name: str,
scheme_name: str,
@@ -45,202 +25,165 @@ def store_scheme_info(
username: str,
scheme_start_time: datetime | str,
scheme_detail: dict,
-):
- """
- 将一条方案记录插入 scheme_list 表中
- :param name: 数据库名称
- :param scheme_name: 方案名称
- :param scheme_type: 方案类型
- :param username: MetaDB 中的用户名快照
- :param scheme_start_time: 带时区的方案起始时间;写入前统一转换为 UTC
- :param scheme_detail: 方案详情(字典,会转换为 JSON)
- :return:
- """
- try:
- conn_string = get_project_pgconn_string(db_name=name)
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- sql = """
- INSERT INTO scheme_list (scheme_name, scheme_type, username, scheme_start_time, scheme_detail)
- VALUES (%s, %s, %s, %s, %s)
- """
- # 将字典转换为 JSON 字符串
- scheme_detail_json = json.dumps(scheme_detail)
- normalized_scheme_start_time = parse_utc_time(
- scheme_start_time, field_name="scheme_start_time"
- )
- cur.execute(
- sql,
- (
- scheme_name,
- scheme_type,
- username,
- normalized_scheme_start_time,
- scheme_detail_json,
- ),
- )
- conn.commit()
- print("方案信息存储成功!")
- except Exception as e:
- print(f"存储方案信息时出错:{e}")
+) -> UUID:
+ """Create one completed, immutable analysis run."""
+ return create_analysis_run(
+ name=name,
+ scheme_name=scheme_name,
+ scheme_type=scheme_type,
+ username=username,
+ scheme_start_time=scheme_start_time,
+ scheme_detail=scheme_detail,
+ status="completed",
+ )
-# 2025/03/23
-def delete_scheme_info(name: str, scheme_name: str) -> None:
- """
- 从 scheme_list 表中删除指定的方案
- :param name: 数据库名称
- :param scheme_name: 要删除的方案名称
- """
- try:
- conn_string = get_project_pgconn_string(db_name=name)
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- # 使用参数化查询删除方案记录
- cur.execute(
- "DELETE FROM scheme_list WHERE scheme_name = %s", (scheme_name,)
- )
- conn.commit()
- print(f"方案 {scheme_name} 删除成功!")
- except Exception as e:
- print(f"删除方案时出错:{e}")
+def create_analysis_run(
+ name: str,
+ scheme_name: str,
+ scheme_type: str,
+ username: str,
+ scheme_start_time: datetime | str,
+ scheme_detail: dict,
+ *,
+ status: str = "running",
+) -> UUID:
+ """Create a distinct execution record; names are labels, not identities."""
+ started_at = parse_utc_time(scheme_start_time, field_name="scheme_start_time")
+ run_id = uuid4()
+ with project_connection(name) as conn, 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)
+ """,
+ (
+ run_id,
+ scheme_name,
+ scheme_type,
+ username,
+ started_at,
+ status,
+ Jsonb(scheme_detail),
+ ),
+ )
+ return run_id
+
+
+def update_analysis_run(
+ name: str,
+ run_id: UUID,
+ *,
+ status: str,
+ username: str,
+ scheme_detail: dict,
+) -> None:
+ """Update lifecycle state and metadata for one execution identity."""
+ with project_connection(name) as conn, conn.cursor() as cur:
+ cur.execute(
+ """
+ update analysis.runs
+ set created_by = %s, status = %s, parameters = %s
+ where run_id = %s
+ """,
+ (username, status, Jsonb(scheme_detail), run_id),
+ )
+ if cur.rowcount != 1:
+ raise LookupError(f"analysis run {run_id} does not exist")
+
+
+def _run_row(row: dict[str, Any]) -> dict[str, Any]:
+ parameters = row.get("parameters") if isinstance(row.get("parameters"), dict) else {}
+ return {
+ "run_id": row["run_id"],
+ "name": row["name"],
+ "run_type": row["run_type"],
+ "created_by": row["created_by"],
+ "created_at": row["created_at"],
+ "started_at": row["started_at"],
+ "status": row["status"],
+ "parameters": parameters,
+ }
+
+
+def _list_runs(
+ name: str,
+ run_type: str | None = None,
+ query_date: date | None = None,
+) -> list[dict[str, Any]]:
+ clauses: list[str] = []
+ params: list[Any] = []
+ if run_type:
+ clauses.append("run_type = %s")
+ params.append(run_type)
+ if query_date is not None:
+ clauses.append("created_at::date = %s")
+ params.append(query_date)
+ where = f"where {' and '.join(clauses)}" if clauses else ""
+ with project_connection(name) as conn, conn.cursor() as cur:
+ cur.execute(
+ f"select run_id, name, run_type, created_by, created_at, started_at, status, parameters from analysis.runs {where} order by created_at desc",
+ params,
+ )
+ return [_run_row(row) for row in cur.fetchall()]
-# 2025/03/23
def query_scheme_list(
name: str,
scheme_type: str | None = None,
query_date: date | None = None,
-) -> list:
- """
- 查询pg数据库中的scheme_list,按照 create_time 降序排列,离现在时间最近的记录排在最前面
- :param name: 项目名称(数据库名称)
- :param scheme_type: 方案类型;为空时返回全部类型
- :param query_date: 查询日期;为空时不按日期过滤
- :return: 返回查询结果的所有行
- """
- try:
- # 动态替换数据库名称
- conn_string = get_project_pgconn_string(db_name=name)
- # 连接到 PostgreSQL 数据库(这里是数据库 "bb")
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- if scheme_type and query_date is not None:
- cur.execute(
- """
- SELECT *
- FROM scheme_list
- WHERE scheme_type = %s AND DATE(create_time) = %s
- ORDER BY create_time DESC
- """,
- (scheme_type, query_date),
- )
- elif scheme_type:
- cur.execute(
- """
- SELECT *
- FROM scheme_list
- WHERE scheme_type = %s
- ORDER BY create_time DESC
- """,
- (scheme_type,),
- )
- elif query_date is not None:
- cur.execute(
- """
- SELECT *
- FROM scheme_list
- WHERE DATE(create_time) = %s
- ORDER BY create_time DESC
- """,
- (query_date,),
- )
- else:
- cur.execute("SELECT * FROM scheme_list ORDER BY create_time DESC")
- rows = cur.fetchall()
- return rows
-
- except Exception as e:
- print(f"查询错误:{e}")
+) -> list[dict[str, Any]]:
+ return _list_runs(name, scheme_type, query_date)
-def _filter_scheme_detail_scope(
- result: dict,
+def _get_run_by_name(
name: str,
- scheme_type: str | None = None,
-) -> dict:
- if not result:
- return {}
- if scheme_type and result.get("scheme_type") != scheme_type:
- return {}
- network = result.get("network")
- if network not in (None, name):
- return {}
- return result
+ run_name: str,
+ run_type: str | None = None,
+) -> dict[str, Any]:
+ params: list[Any] = [run_name]
+ type_clause = ""
+ if run_type:
+ type_clause = "and run_type = %s"
+ params.append(run_type)
+ with project_connection(name) as conn, conn.cursor() as cur:
+ cur.execute(
+ f"""
+ select run_id, name, run_type, created_by, created_at, started_at,
+ status, parameters
+ from analysis.runs
+ where name = %s {type_clause}
+ order by created_at desc
+ limit 1
+ """,
+ params,
+ )
+ row = cur.fetchone()
+ return _run_row(row) if row else {}
+
+
+def get_analysis_run(name: str, run_id: UUID) -> dict[str, Any]:
+ with project_connection(name) as conn, 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,),
+ )
+ row = cur.fetchone()
+ return _run_row(row) if row else {}
def query_scheme_detail(
name: str,
scheme_name: str,
scheme_type: str | None = None,
-) -> dict:
- if scheme_type == "dma_leak_identification":
- return _filter_scheme_detail_scope(
- query_leakage_identify_scheme_detail(name, scheme_name),
- name,
- scheme_type,
- )
- if scheme_type == "burst_detection":
- return _filter_scheme_detail_scope(
- query_burst_detection_scheme_detail(name, scheme_name),
- name,
- scheme_type,
- )
- if scheme_type == "burst_location":
- return _filter_scheme_detail_scope(
- query_burst_location_scheme_detail(name, scheme_name),
- name,
- scheme_type,
- )
-
- conn_string = get_project_pgconn_string(db_name=name)
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- if scheme_type:
- cur.execute(
- """
- SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
- FROM public.scheme_list
- WHERE scheme_name = %s AND scheme_type = %s
- LIMIT 1
- """,
- (scheme_name, scheme_type),
- )
- else:
- cur.execute(
- """
- SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
- FROM public.scheme_list
- WHERE scheme_name = %s
- LIMIT 1
- """,
- (scheme_name,),
- )
- row = cur.fetchone()
- if row is None:
- return {}
- detail = row[6] if isinstance(row[6], dict) else {}
- return _filter_scheme_detail_scope({
- "scheme_id": row[0],
- "scheme_name": row[1],
- "scheme_type": row[2],
- "username": row[3],
- "create_time": row[4],
- "scheme_start_time": row[5],
- "scheme_detail": detail,
- "network": detail.get("network"),
- "result_payload": detail.get("result_payload", {}),
- }, name, scheme_type)
+) -> dict[str, Any]:
+ return _get_run_by_name(name, scheme_name, scheme_type)
def store_leakage_identify_result(
@@ -255,42 +198,51 @@ def store_leakage_identify_result(
run_status: str = "completed",
error_message: str | None = None,
) -> None:
- conn_string = get_project_pgconn_string(db_name=name)
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- cur.execute(
- """
- INSERT INTO public.leakage_identify_result
- (
- scheme_name, network, run_status, error_message,
- sensor_nodes, result_rows, node_area_map, areas, drawing_payload
- )
- VALUES (%s, %s, %s, %s, %s::jsonb, %s::jsonb, %s::jsonb, %s::jsonb, %s::jsonb)
- ON CONFLICT (scheme_name)
- DO UPDATE SET
- network = EXCLUDED.network,
- run_status = EXCLUDED.run_status,
- error_message = EXCLUDED.error_message,
- sensor_nodes = EXCLUDED.sensor_nodes,
- result_rows = EXCLUDED.result_rows,
- node_area_map = EXCLUDED.node_area_map,
- areas = EXCLUDED.areas,
- drawing_payload = EXCLUDED.drawing_payload,
- created_at = NOW();
- """,
- (
- scheme_name,
- network,
- run_status,
- error_message,
- json.dumps(sensor_nodes),
- json.dumps(result_rows),
- json.dumps(node_area_map),
- json.dumps(areas),
- json.dumps(drawing_payload or {}),
- ),
- )
- conn.commit()
+ run = _get_run_by_name(name, scheme_name, "dma_leak_identification")
+ if not run:
+ raise LookupError(f"analysis run {scheme_name!r} does not exist")
+ payload = {
+ "network": network,
+ "run_status": run_status,
+ "error_message": error_message,
+ "sensor_nodes": sensor_nodes,
+ "rows": result_rows,
+ "node_area_map": node_area_map,
+ "areas": areas,
+ "drawing_payload": drawing_payload or {},
+ }
+ with project_connection(name) as conn, conn.cursor() as cur:
+ cur.execute(
+ "insert into analysis.results (run_id, result_type, payload) values (%s, 'leakage_identification', %s)",
+ (run["run_id"], Jsonb(payload)),
+ )
+
+
+def _list_typed_runs(
+ name: str,
+ network: str,
+ run_type: str,
+ query_date: date | None,
+) -> list[dict[str, Any]]:
+ rows = _list_runs(name, run_type, query_date)
+ return [
+ row
+ for row in rows
+ if not network or row["parameters"].get("network") in (None, network)
+ ]
+
+
+def _typed_run_detail(name: str, run_name: str, run_type: str) -> dict[str, Any]:
+ run = _get_run_by_name(name, run_name, run_type)
+ if not run:
+ return {}
+ with project_connection(name) as conn, conn.cursor() as cur:
+ cur.execute(
+ "select result_type, payload, created_at from analysis.results where run_id = %s order by created_at, result_id",
+ (run["run_id"],),
+ )
+ results = [dict(row) for row in cur.fetchall()]
+ return run | {"results": results}
def query_leakage_identify_schemes(
@@ -298,100 +250,12 @@ def query_leakage_identify_schemes(
network: str,
scheme_type: str = "dma_leak_identification",
query_date: date | None = None,
-) -> list[dict]:
- conn_string = get_project_pgconn_string(db_name=name)
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- if query_date is None:
- cur.execute(
- """
- SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
- FROM public.scheme_list
- WHERE scheme_type = %s
- ORDER BY create_time DESC
- """,
- (scheme_type,),
- )
- else:
- cur.execute(
- """
- SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
- FROM public.scheme_list
- WHERE scheme_type = %s AND DATE(create_time) = %s
- ORDER BY create_time DESC
- """,
- (scheme_type, query_date),
- )
- rows = cur.fetchall()
- result = []
- for row in rows:
- detail = row[6] if isinstance(row[6], dict) else {}
- if network and detail.get("network") not in (None, network):
- continue
- result.append(
- {
- "scheme_id": row[0],
- "scheme_name": row[1],
- "scheme_type": row[2],
- "username": row[3],
- "create_time": row[4],
- "scheme_start_time": row[5],
- "scheme_detail": detail,
- }
- )
- return result
+) -> list[dict[str, Any]]:
+ return _list_typed_runs(name, network, scheme_type, query_date)
-def query_leakage_identify_scheme_detail(name: str, scheme_name: str) -> dict:
- conn_string = get_project_pgconn_string(db_name=name)
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- cur.execute(
- """
- SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
- FROM public.scheme_list
- WHERE scheme_name = %s
- LIMIT 1
- """,
- (scheme_name,),
- )
- base_row = cur.fetchone()
- if base_row is None:
- return {}
- cur.execute(
- """
- SELECT network, created_at, run_status, error_message, sensor_nodes, result_rows, node_area_map, areas, drawing_payload
- FROM public.leakage_identify_result
- WHERE scheme_name = %s
- LIMIT 1
- """,
- (scheme_name,),
- )
- result_row = cur.fetchone()
- if result_row is None:
- return {}
- return {
- "scheme_id": base_row[0],
- "scheme_name": base_row[1],
- "scheme_type": base_row[2],
- "username": base_row[3],
- "create_time": base_row[4],
- "scheme_start_time": base_row[5],
- "scheme_detail": base_row[6] if isinstance(base_row[6], dict) else {},
- "network": result_row[0],
- "result_created_at": result_row[1],
- "run_status": result_row[2],
- "error_message": result_row[3],
- "sensor_nodes": result_row[4] if isinstance(result_row[4], list) else [],
- "rows": result_row[5] if isinstance(result_row[5], list) else [],
- "node_area_map": result_row[6] if isinstance(result_row[6], dict) else {},
- "areas": result_row[7] if isinstance(result_row[7], list) else [],
- "drawing_payload": (
- result_row[8]
- if isinstance(result_row[8], dict)
- else {"type": "FeatureCollection", "features": []}
- ),
- }
+def query_leakage_identify_scheme_detail(name: str, scheme_name: str) -> dict[str, Any]:
+ return _typed_run_detail(name, scheme_name, "dma_leak_identification")
def query_burst_location_schemes(
@@ -399,78 +263,12 @@ def query_burst_location_schemes(
network: str,
scheme_type: str = "burst_location",
query_date: date | None = None,
-) -> list[dict]:
- conn_string = get_project_pgconn_string(db_name=name)
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- if query_date is None:
- cur.execute(
- """
- SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
- FROM public.scheme_list
- WHERE scheme_type = %s
- ORDER BY create_time DESC
- """,
- (scheme_type,),
- )
- else:
- cur.execute(
- """
- SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
- FROM public.scheme_list
- WHERE scheme_type = %s AND DATE(create_time) = %s
- ORDER BY create_time DESC
- """,
- (scheme_type, query_date),
- )
- rows = cur.fetchall()
- result = []
- for row in rows:
- detail = row[6] if isinstance(row[6], dict) else {}
- if network and detail.get("network") not in (None, network):
- continue
- result.append(
- {
- "scheme_id": row[0],
- "scheme_name": row[1],
- "scheme_type": row[2],
- "username": row[3],
- "create_time": row[4],
- "scheme_start_time": row[5],
- "scheme_detail": detail,
- }
- )
- return result
+) -> list[dict[str, Any]]:
+ return _list_typed_runs(name, network, scheme_type, query_date)
-def query_burst_location_scheme_detail(name: str, scheme_name: str) -> dict:
- conn_string = get_project_pgconn_string(db_name=name)
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- cur.execute(
- """
- SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
- FROM public.scheme_list
- WHERE scheme_name = %s
- LIMIT 1
- """,
- (scheme_name,),
- )
- base_row = cur.fetchone()
- if base_row is None:
- return {}
- detail = base_row[6] if isinstance(base_row[6], dict) else {}
- return {
- "scheme_id": base_row[0],
- "scheme_name": base_row[1],
- "scheme_type": base_row[2],
- "username": base_row[3],
- "create_time": base_row[4],
- "scheme_start_time": base_row[5],
- "scheme_detail": detail,
- "network": detail.get("network"),
- "result_payload": detail.get("result_payload", {}),
- }
+def query_burst_location_scheme_detail(name: str, scheme_name: str) -> dict[str, Any]:
+ return _typed_run_detail(name, scheme_name, "burst_location")
def query_burst_detection_schemes(
@@ -478,171 +276,9 @@ def query_burst_detection_schemes(
network: str,
scheme_type: str = "burst_detection",
query_date: date | None = None,
-) -> list[dict]:
- conn_string = get_project_pgconn_string(db_name=name)
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- if query_date is None:
- cur.execute(
- """
- SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
- FROM public.scheme_list
- WHERE scheme_type = %s
- ORDER BY create_time DESC
- """,
- (scheme_type,),
- )
- else:
- cur.execute(
- """
- SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
- FROM public.scheme_list
- WHERE scheme_type = %s AND DATE(create_time) = %s
- ORDER BY create_time DESC
- """,
- (scheme_type, query_date),
- )
- rows = cur.fetchall()
- result = []
- for row in rows:
- detail = row[6] if isinstance(row[6], dict) else {}
- if network and detail.get("network") not in (None, network):
- continue
- result.append(
- {
- "scheme_id": row[0],
- "scheme_name": row[1],
- "scheme_type": row[2],
- "username": row[3],
- "create_time": row[4],
- "scheme_start_time": row[5],
- "scheme_detail": detail,
- }
- )
- return result
+) -> list[dict[str, Any]]:
+ return _list_typed_runs(name, network, scheme_type, query_date)
-def query_burst_detection_scheme_detail(name: str, scheme_name: str) -> dict:
- conn_string = get_project_pgconn_string(db_name=name)
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- cur.execute(
- """
- SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
- FROM public.scheme_list
- WHERE scheme_name = %s
- LIMIT 1
- """,
- (scheme_name,),
- )
- base_row = cur.fetchone()
- if base_row is None:
- return {}
- detail = base_row[6] if isinstance(base_row[6], dict) else {}
- return {
- "scheme_id": base_row[0],
- "scheme_name": base_row[1],
- "scheme_type": base_row[2],
- "username": base_row[3],
- "create_time": base_row[4],
- "scheme_start_time": base_row[5],
- "scheme_detail": detail,
- "network": detail.get("network"),
- "result_payload": detail.get("result_payload", {}),
- }
-
-
-# 2025/03/23
-def upload_shp_to_pg(name: str, table_name: str, role: str, shp_file_path: str):
- """
- 将 Shapefile 文件上传到 PostgreSQL 数据库
- :param name: 项目名称(数据库名称)
- :param table_name: 创建表的名字
- :param role: 数据库角色名,位于c盘user中查看
- :param shp_file_path: shp文件的路径
- :return:
- """
- try:
- # 动态连接到指定的数据库
- conn_string = get_project_pgconn_string(db_name=name)
- with psycopg.connect(conn_string) as conn:
- # 读取 Shapefile 文件
- gdf = gpd.read_file(shp_file_path)
-
- # 检查投影坐标系(CRS),并确保是 EPSG:4326
- if gdf.crs.to_string() != "EPSG:4490":
- gdf = gdf.to_crs(epsg=4490)
-
- # 使用 GeoDataFrame 的 .to_postgis 方法将数据写入 PostgreSQL
- # 需要在数据库中提前安装 PostGIS 扩展
- engine = create_engine(f"postgresql+psycopg2://{role}:@127.0.0.1/{name}")
- gdf.to_postgis(
- table_name, engine, if_exists="replace", index=True, index_label="id"
- )
-
- print(
- f"Shapefile 文件成功上传到 PostgreSQL 数据库 '{name}' 的表 '{table_name}'."
- )
-
- except Exception as e:
- print(f"上传 Shapefile 到 PostgreSQL 时出错:{e}")
-
-
-def submit_risk_probability_result(name: str, result_file_path: str) -> None:
- """
- 将管网风险评估结果导入pg数据库
- :param name: 项目名称(数据库名称)
- :param result_file_path: 结果文件路径
- :return:
- """
- # 自动检测文件编码
- # with open({result_file_path}, 'rb') as file:
- # raw_data = file.read()
- # detected = chardet.detect(raw_data)
- # file_encoding = detected['encoding']
- # print(f"检测到的文件编码:{file_encoding}")
-
- try:
- # 动态替换数据库名称
- conn_string = get_project_pgconn_string(db_name=name)
-
- # 连接到 PostgreSQL 数据库
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- # 检查 scada_info 表是否为空
- cur.execute("SELECT COUNT(*) FROM pipe_risk_probability;")
- count = cur.fetchone()[0]
-
- if count > 0:
- print("pipe_risk_probability表中已有数据,正在清空记录...")
- cur.execute("DELETE FROM pipe_risk_probability;")
- print("表记录已清空。")
-
- # 读取Excel并转换x/y列为列表
- df = pd.read_excel(result_file_path, sheet_name="Sheet1")
- df["x"] = df["x"].apply(ast.literal_eval)
- df["y"] = df["y"].apply(ast.literal_eval)
-
- # 批量插入数据
- for index, row in df.iterrows():
- insert_query = """
- INSERT INTO pipe_risk_probability
- (pipeID, pipeage, risk_probability_now, x, y)
- VALUES (%s, %s, %s, %s, %s)
- """
- cur.execute(
- insert_query,
- (
- row["pipeID"],
- row["pipeage"],
- row["risk_probability_now"],
- row["x"], # 直接传递列表
- row["y"], # 同上
- ),
- )
-
- conn.commit()
- print("风险评估结果导入成功")
-
- except Exception as e:
- print(f"导入时出错:{e}")
+def query_burst_detection_scheme_detail(name: str, scheme_name: str) -> dict[str, Any]:
+ return _typed_run_detail(name, scheme_name, "burst_detection")
diff --git a/app/services/sensor_placement.py b/app/services/sensor_placement.py
index 8d4f4f7..b1dddc7 100644
--- a/app/services/sensor_placement.py
+++ b/app/services/sensor_placement.py
@@ -1,6 +1,7 @@
from datetime import datetime
from io import BytesIO
from typing import Any
+from uuid import UUID
from openpyxl import Workbook
from openpyxl.styles import Alignment, Font, PatternFill
@@ -8,7 +9,7 @@ from openpyxl.worksheet.worksheet import Worksheet
from openpyxl.utils import get_column_letter
from pyproj import Transformer
-from app.native import wndb
+from app.infra.db.postgresql import sensor_placement as sensor_placement_repository
class SensorPlacementNotFoundError(LookupError):
@@ -61,7 +62,9 @@ def _sensor_points(
network: str,
sensor_location: list[str],
) -> list[dict[str, Any]]:
- nodes = wndb.get_sensor_placement_nodes(network, sensor_location)
+ nodes = sensor_placement_repository.get_sensor_placement_nodes(
+ network, sensor_location
+ )
by_id = {str(node["node_id"]): node for node in nodes}
missing = [node_id for node_id in sensor_location if node_id not in by_id]
if missing:
@@ -113,49 +116,59 @@ def validate_sensor_placement_nodes(
_sensor_points(network, _normalize_locations(sensor_location))
-def get_sensor_placement_scheme(network: str, scheme_id: int) -> dict[str, Any]:
- scheme = wndb.get_sensor_placement(network, scheme_id)
- if scheme is None:
- raise SensorPlacementNotFoundError("监测点方案不存在")
+def get_sensor_placement_run(network: str, run_id: UUID) -> dict[str, Any]:
+ run = sensor_placement_repository.get_sensor_placement(network, run_id)
+ if run is None:
+ raise SensorPlacementNotFoundError("监测点优化运行不存在")
- locations = [str(item) for item in (scheme.get("sensor_location") or [])]
+ locations = [str(item) for item in (run.get("sensor_locations") or [])]
return {
- **scheme,
- "sensor_number": len(locations),
- "sensor_location": locations,
+ **run,
+ "sensor_count": len(locations),
+ "sensor_locations": locations,
"sensor_points": _sensor_points(network, locations),
}
-def update_sensor_placement_scheme(
+def list_sensor_placement_runs(network: str) -> list[dict[str, Any]]:
+ return [
+ {
+ **run,
+ "sensor_points": _sensor_points(network, run["sensor_locations"]),
+ }
+ for run in sensor_placement_repository.get_all_sensor_placements(network)
+ ]
+
+
+def update_sensor_placement_run(
network: str,
- scheme_id: int,
+ run_id: UUID,
*,
- expected_sensor_location: list[str],
- sensor_location: list[str],
+ expected_sensor_locations: list[str],
+ sensor_locations: list[str],
) -> dict[str, Any]:
- expected = _normalize_locations(expected_sensor_location)
- next_locations = _normalize_locations(sensor_location)
+ expected = _normalize_locations(expected_sensor_locations)
+ next_locations = _normalize_locations(sensor_locations)
_sensor_points(network, next_locations)
- updated = wndb.update_sensor_placement(
+ updated = sensor_placement_repository.update_sensor_placement(
network,
- scheme_id,
- expected_sensor_location=expected,
- sensor_location=next_locations,
+ run_id,
+ expected_sensor_locations=expected,
+ sensor_locations=next_locations,
)
if updated is None:
- if wndb.get_sensor_placement(network, scheme_id) is None:
- raise SensorPlacementNotFoundError("监测点方案不存在")
- raise SensorPlacementConflictError("方案已被其他用户修改,请重新加载")
- return get_sensor_placement_scheme(network, scheme_id)
+ if sensor_placement_repository.get_sensor_placement(network, run_id) is None:
+ raise SensorPlacementNotFoundError("监测点优化运行不存在")
+ raise SensorPlacementConflictError("运行结果已被其他用户修改,请重新加载")
+ return get_sensor_placement_run(network, run_id)
-def can_edit_sensor_placement(user: Any, scheme: dict[str, Any]) -> bool:
+def can_edit_sensor_placement(user: Any, run: dict[str, Any]) -> bool:
return bool(
getattr(user, "is_superuser", False)
or getattr(user, "role", None) == "admin"
- or getattr(user, "username", None) == scheme.get("username")
+ or getattr(user, "username", None) == run.get("created_by")
)
@@ -174,16 +187,16 @@ def _populate_info_sheet(
location_count: int,
is_draft: bool,
) -> None:
- created_at = scheme["create_time"]
+ created_at = scheme["created_at"]
if isinstance(created_at, datetime):
created_at = created_at.isoformat(timespec="minutes")
rows = [
("项目", network),
- ("方案名称", scheme["scheme_name"]),
+ ("运行名称", scheme["name"]),
("监测点数量", location_count),
("最小管径", scheme["min_diameter"]),
- ("创建人", scheme["username"]),
+ ("创建人", scheme["created_by"]),
("创建时间", created_at),
("导出时间", datetime.now().astimezone().isoformat(timespec="minutes")),
("文档状态", "未保存草稿" if is_draft else "当前方案"),
@@ -245,7 +258,7 @@ def build_sensor_placement_workbook(
) -> BytesIO:
locations = _normalize_locations(sensor_location)
points = _sensor_points(network, locations)
- is_draft = locations != list(scheme["sensor_location"])
+ is_draft = locations != list(scheme["sensor_locations"])
workbook = Workbook()
info_sheet = workbook.active
diff --git a/app/services/simulation.py b/app/services/simulation.py
index 0ee8b4b..6ea495c 100644
--- a/app/services/simulation.py
+++ b/app/services/simulation.py
@@ -28,16 +28,17 @@ import pytz
import requests
import time
from typing import Optional, Tuple
+from uuid import UUID
import typing
-import psycopg
import logging
import app.services.globals as globals
import app.services.project_info as project_info
from app.services.time_api import parse_beijing_time, parse_clock_duration_seconds
-from app.infra.db.project_routing import get_project_pgconn_string
+from app.native.wndb.core.connection import project_connection
from app.infra.db.timescaledb.internal_queries import (
InternalQueries as TimescaleInternalQueries,
)
+from app.services.scheme_management import create_analysis_run, update_analysis_run
from app.infra.db.timescaledb.internal_queries import (
InternalStorage as TimescaleInternalStorage,
)
@@ -48,569 +49,35 @@ logging.basicConfig(
def query_corresponding_element_id_and_query_id(name: str) -> None:
- """
- 查询scada_info这张表中,realtime类型的记录中,associated_element_id与api_query_id的对应关系
- :param name: 数据库名称
- :return:
- """
- # 连接数据库
- conn_string = get_project_pgconn_string(db_name=name)
- try:
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- # 查询 transmission_mode 为 'realtime' 的记录
- cur.execute(
- """
- SELECT type, associated_element_id, api_query_id
- FROM scada_info
- WHERE transmission_mode = 'realtime';
- """
- )
- records = cur.fetchall()
- # 遍历查询结果,并根据 type 将数据存储到相应的字典中
- for record in records:
- type_, associated_element_id, api_query_id = record
- if type_ == "reservoir_liquid_level":
- globals.reservoirs_id[associated_element_id] = api_query_id
- elif type_ == "tank_liquid_level":
- globals.tanks_id[associated_element_id] = api_query_id
- elif type_ == "fixed_pump":
- globals.fixed_pumps_id[associated_element_id] = api_query_id
- elif type_ == "variable_pump":
- globals.variable_pumps_id[associated_element_id] = api_query_id
- elif type_ == "pressure":
- globals.pressure_id[associated_element_id] = api_query_id
- elif type_ == "demand":
- globals.demand_id[associated_element_id] = api_query_id
- elif type_ == "quality":
- globals.quality_id[associated_element_id] = api_query_id
- else:
- # 如果遇到未定义的类型,可以选择记录日志或忽略
- print(f"未处理的类型: {type_}")
- except psycopg.Error as e:
- print(f"数据库连接或查询出错: {e}")
-
-
-def query_corresponding_pattern_id_and_query_id(name: str) -> None:
- """
- 查询 scada_info 表中 transmission_mode 为 'realtime',且 type 为 'source_outflow' 或 'pipe_flow' 的记录,
- 提取 associated_pattern 和 api_query_id 的对应关系,并分别存储到对应的字典中。
- :param name: 数据库名称
- :return:
- """
- # 连接数据库
- conn_string = get_project_pgconn_string(db_name=name)
- try:
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- # 查询 transmission_mode 为 'realtime' 且 type 为 'source_outflow' 或 'pipe_flow' 的记录
- cur.execute(
- """
- SELECT type, associated_pattern, api_query_id
- FROM scada_info
- WHERE transmission_mode = 'realtime'
- AND type IN ('source_outflow', 'pipe_flow');
- """
- )
- records = cur.fetchall()
- # 遍历查询结果,并根据 type 将数据存储到相应的字典中
- for record in records:
- type_, associated_pattern, api_query_id = record
- if type_ == "source_outflow":
- globals.source_outflow_pattern_id[associated_pattern] = (
- api_query_id
- )
- elif type_ == "pipe_flow":
- globals.realtime_pipe_flow_pattern_id[associated_pattern] = (
- api_query_id
- )
- except psycopg.Error as e:
- print(f"数据库连接或查询出错: {e}")
-
-
-# 2025/01/11
-def query_non_realtime_region(name: str) -> dict:
- """
- 查询 scada_info 表中 transmission_mode 为 'non_realtime',且 type 为 'pipe_flow' 的记录,
- 提取所有以 'associated_source_outflow_id' 开头的列的值,并将每条记录的这些值作为一个 region(region1, region2, ...),
- 最后去掉重复的 region,并存储到 source_outflow_region 字典中。
- :param name: 数据库名字
- :return: 包含区域与对应 associated_source_outflow_id 的字典
- """
- source_outflow_regions = [] # 用于存储所有 region(包含重复的)
- # 构建连接字符串
- conn_string = get_project_pgconn_string(db_name=name)
- try:
- # 连接到数据库
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- # 执行查询,筛选出 transmission_mode 为 'non_realtime' 且 type 为 'pipe_flow' 的记录
- cur.execute(
- """
- SELECT *
- FROM scada_info
- WHERE transmission_mode = 'non_realtime'
- AND type = 'pipe_flow';
- """
- )
- records = cur.fetchall()
- col_names = [desc.name for desc in cur.description]
- # 找出所有以 'associated_source_outflow_id' 开头的列
- source_outflow_cols = [
- col
- for col in col_names
- if col.startswith("associated_source_outflow_id")
- ]
- logging.info(
- f"Identified source_outflow columns: {source_outflow_cols}"
- )
- for record in records:
- # 提取所有以 'associated_source_outflow_id' 开头的列的值,排除 None
- values = [
- record[col_names.index(col)]
- for col in source_outflow_cols
- if record[col_names.index(col)] is not None
- ]
- # 如果该记录有相关的值,则将其作为一个 region
- if values:
- # 将值排序以确保相同的组合顺序一致(如果顺序不重要)
- # 如果顺序重要,请删除排序步骤
- region_tuple = tuple(sorted(values))
- source_outflow_regions.append(region_tuple)
- # 移除重复的 regions
- unique_regions = []
- seen = set()
- for region in source_outflow_regions:
- if region not in seen:
- seen.add(region)
- unique_regions.append(region)
- # 为每个唯一的 region 分配一个 region 键
- for idx, region in enumerate(unique_regions, 1):
- region_key = f"region{idx}"
- globals.source_outflow_region[region_key] = list(region)
- logging.info("查询并处理数据成功。")
- except psycopg.Error as e:
- logging.error(f"数据库连接或查询出错: {e}")
- except Exception as ex:
- logging.error(f"处理数据时出错: {ex}")
- return globals.source_outflow_region
-
-
-# 2025/01/18
-def query_non_realtime_region_patterns(
- name: str,
- source_outflow_region: dict,
- column_prefix: str = "associated_source_outflow_id",
-) -> dict:
- """
- 根据 source_outflow_region,对 scada_info 表中 transmission_mode 为 'non_realtime'的记录进行分组,
- 将匹配的记录的 associated_pattern 存入 non_realtime_region_patterns 字典中,同时把用 realtime pipe_flow修正的 non_realtime demand 去掉
- :param name: 数据库名称
- :param source_outflow_region: 包含区域与对应 associated_source_outflow_id 的字典
- :param column_prefix: 需要提取的列的前缀
- :return: 包含区域与对应 associated_pattern 的字典
- """
- globals.non_realtime_region_patterns = {
- region: [] for region in globals.source_outflow_region.keys()
+ """Load realtime device-to-element mappings from the new asset schema."""
+ target_maps = {
+ "reservoir_liquid_level": globals.reservoirs_id,
+ "tank_liquid_level": globals.tanks_id,
+ "fixed_pump": globals.fixed_pumps_id,
+ "variable_pump": globals.variable_pumps_id,
+ "pressure": globals.pressure_id,
+ "demand": globals.demand_id,
+ "quality": globals.quality_id,
}
- region_tuple_to_key = {
- frozenset(ids): region for region, ids in globals.source_outflow_region.items()
- }
- conn_string = get_project_pgconn_string(db_name=name)
- try:
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- # 执行查询,筛选出 transmission_mode 为 'non_realtime'
- cur.execute(
- """
- SELECT *
- FROM scada_info
- WHERE transmission_mode = 'non_realtime'
- """
- )
- records = cur.fetchall()
- col_names = [desc.name for desc in cur.description]
- # 找出所有以指定前缀开头的列
- source_outflow_cols = [
- col for col in col_names if col.startswith(column_prefix)
- ]
- logging.info(
- f"Identified source_outflow columns: {source_outflow_cols}"
- )
- # 确保 'associated_pattern' 列存在
- if "associated_pattern" not in col_names:
- logging.error(
- "'associated_pattern' column not found in scada_info table."
- )
- return globals.non_realtime_region_patterns
- # 获取 'associated_pattern' 列的索引
- pattern_idx = col_names.index("associated_pattern")
- for record in records:
- # 提取所有以 'associated_source_outflow_id' 开头的列的值,排除 None
- values = [
- record[col_names.index(col)]
- for col in source_outflow_cols
- if record[col_names.index(col)] is not None
- ]
- if values:
- # 将值转换为 frozenset 以便与 region_tuple_to_key 进行匹配
- region_frozenset = frozenset(values)
- # 检查是否存在匹配的 region
- region_key = region_tuple_to_key.get(region_frozenset)
- if region_key:
- # 获取 'associated_pattern' 的值
- associated_pattern = record[pattern_idx]
- if associated_pattern is not None:
- globals.non_realtime_region_patterns[region_key].append(
- associated_pattern
- )
- logging.info("生成 regions_patterns 成功。")
- except psycopg.Error as e:
- logging.error(f"数据库连接或查询出错: {e}")
- except Exception as ex:
- logging.error(f"处理数据时出错: {ex}")
- # 获取pipe_flow_region_patterns中的所有区域
- exclude_regions = set(
- region
- for regions in globals.pipe_flow_region_patterns.values()
- for region in regions
- )
- # 从non_realtime_region_patterns中去除这些区域
- for region_key, regions in globals.non_realtime_region_patterns.items():
- globals.non_realtime_region_patterns[region_key] = [
- region for region in regions if region not in exclude_regions
- ]
- return globals.non_realtime_region_patterns
-
-
-# 2025/01/18
-def query_realtime_region_pipe_flow_and_demand_id(
- name: str,
- source_outflow_region: dict,
- column_prefix: str = "associated_source_outflow_id",
-) -> dict:
- """
- 根据 source_outflow_region,对 scada_info 表中 transmission_mode 为 'realtime',
- 且 type 为 'pipe_flow' 或 ‘demand’ 的记录进行分组,将匹配的记录的 api_query_id 存入 realtime_region_pipe_flow_and_demand_id 字典中。
- :param name: 数据库名称
- :param source_outflow_region: 包含区域与对应 associated_source_outflow_id 的字典
- :param column_prefix: 需要提取的列的前缀
- :return: 包含区域与对应 api_query_id 的字典
- """
- globals.realtime_region_pipe_flow_and_demand_id = {
- region: [] for region in globals.source_outflow_region.keys()
- }
- # 创建一个映射,从 frozenset(ids) 到 region_key
- region_tuple_to_key = {
- frozenset(ids): region for region, ids in globals.source_outflow_region.items()
- }
- conn_string = get_project_pgconn_string(db_name=name)
- try:
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- # 执行查询,筛选出 transmission_mode 为 'realtime' 且 type 为 'pipe_flow' 或 'demand' 的记录
- cur.execute(
- """
- SELECT *
- FROM scada_info
- WHERE transmission_mode = 'realtime'
- AND type IN ('pipe_flow', 'demand');
- """
- )
- records = cur.fetchall()
- col_names = [desc.name for desc in cur.description]
- # 找出所有以指定前缀开头的列
- source_outflow_cols = [
- col for col in col_names if col.startswith(column_prefix)
- ]
- logging.info(
- f"Identified source_outflow columns: {source_outflow_cols}"
- )
- # 确保 'api_query_id' 列存在
- if "api_query_id" not in col_names:
- logging.error(
- "'api_query_id' column not found in scada_info table."
- )
- return globals.realtime_region_pipe_flow_and_demand_id
- # 获取 'api_query_id' 列的索引
- api_query_id_idx = col_names.index("api_query_id")
- for record in records:
- # 提取所有以 'associated_source_outflow_id' 开头的列的值,排除 None
- values = [
- record[col_names.index(col)]
- for col in source_outflow_cols
- if record[col_names.index(col)] is not None
- ]
- if values:
- # 将值转换为 frozenset 以便与 region_tuple_to_key 进行匹配
- region_frozenset = frozenset(values)
- # 检查是否存在匹配的 region
- region_key = region_tuple_to_key.get(region_frozenset)
- if region_key:
- # 获取 'api_query_id' 的值
- api_query_id = record[api_query_id_idx]
- if api_query_id is not None:
- globals.realtime_region_pipe_flow_and_demand_id[
- region_key
- ].append(api_query_id)
- logging.info("生成 realtime_region_pipe_flow_and_demand_id 成功。")
- except psycopg.Error as e:
- logging.error(f"数据库连接或查询出错: {e}")
- except Exception as ex:
- logging.error(f"处理数据时出错: {ex}")
- return globals.realtime_region_pipe_flow_and_demand_id
-
-
-# 2025/01/17
-def query_pipe_flow_region_patterns(
- name: str, column_prefix: str = "associated_pipe_flow_id"
-) -> dict:
- """
- 查询 scada_info 表中 type 为 'demand' 且 transmission_mode 为 'non_realtime' 的记录,
- 记录该记录的 associated_pattern。
- 如果该记录的 associated_pipe_flow_id 存在,
- 且根据 associated_pipe_flow_id 查询的 associated_element_id 对应的记录的 transmission_mode 为 'realtime',
- 则将该记录的 associated_pattern 作为值记录到字典中,字典的 key 为 pipe_flow 类的 associated_pattern。
- 字典样式为:{'region1': ['P17021', 'ZBBGXSZW000377'], 'region2': ['P16504']}
- :param name: 数据库名称
- :param column_prefix: 需要提取的列的前缀
- :return: pipe_flow_region_patterns 字典
- """
- conn_string = get_project_pgconn_string(db_name=name)
- try:
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- # 查询 type 为 'demand' 且 transmission_mode 为 'non_realtime' 的记录
- cur.execute(
- """
- SELECT associated_pattern, associated_pipe_flow_id
- FROM scada_info
- WHERE type = 'demand'
- AND transmission_mode = 'non_realtime';
- """
- )
- records = cur.fetchall()
- col_names = [desc.name for desc in cur.description]
- # 获取列索引
- pattern_idx = col_names.index("associated_pattern")
- pipe_flow_id_idx = col_names.index("associated_pipe_flow_id")
- for record in records:
- associated_pattern = record[pattern_idx]
- associated_pipe_flow_id = record[pipe_flow_id_idx]
- if associated_pipe_flow_id:
- # 根据 associated_pipe_flow_id 查询对应的记录
- cur.execute(
- """
- SELECT associated_pattern, transmission_mode
- FROM scada_info
- WHERE associated_element_id = %s;
- """,
- (associated_pipe_flow_id,),
- )
- pipe_flow_record = cur.fetchone()
- if pipe_flow_record:
- pipe_flow_associated_pattern = pipe_flow_record[0]
- transmission_mode = pipe_flow_record[1]
- if transmission_mode == "realtime":
- # 将 associated_pattern 记录到字典中
- if (
- pipe_flow_associated_pattern
- not in globals.pipe_flow_region_patterns
- ):
- globals.pipe_flow_region_patterns[
- pipe_flow_associated_pattern
- ] = []
- globals.pipe_flow_region_patterns[
- pipe_flow_associated_pattern
- ].append(associated_pattern)
- logging.info("生成 pipe_flow_region_patterns 成功。")
- except psycopg.Error as e:
- logging.error(f"数据库连接或查询出错: {e}")
- except Exception as ex:
- logging.error(f"处理数据时出错: {ex}")
- return globals.pipe_flow_region_patterns
-
-
-# 2025/02/15
-def query_SCADA_ID_corresponding_info(name: str, SCADA_ID: str) -> dict:
- """
- 在地图上拾取SCADA元素后,获取SCADA_ID,在pg数据库中查询该SCADA设备对应的associated_element_id和api_query_id
- :param name: pg数据库的名称
- :param SCADA_ID: SCADA设备的ID
- :return: 包含associated_element_id和api_query_id的字典
- """
- conn_string = get_project_pgconn_string(db_name=name)
- try:
- # 使用 psycopg.connect 创建连接
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- # 执行查询
- query = """
- SELECT associated_element_id, API_query_id
- FROM scada_info
- WHERE id = %s
- """
- cur.execute(query, (SCADA_ID,)) # 执行查询并传递参数
- # 获取查询结果
- result = cur.fetchone()
- if result:
- # 将结果转换为字典
- associated_info = {
- "associated_element_id": result[0],
- "API_query_id": result[1],
- }
- return associated_info
- else:
- # 如果没有找到记录
- return None
- except Exception as e:
- print(f"An error occurred: {e}")
- return None
-
-
-# 2025/01/11
-def get_source_outflow_region_id(
- name: str,
- source_outflow_region: dict,
- column_prefix: str = "associated_source_outflow_id",
-) -> dict:
- """
- 基于 source_outflow_region,将其中的 associated_source_outflow_id 替换为对应的 api_query_id,
- 生成新的字典 source_outflow_region_id。
-
- :param name: 数据库名称
- :param source_outflow_region: 包含区域与对应 associated_source_outflow_id 的字典
- :param column_prefix: 需要提取的列的前缀
- :return: 包含区域与对应 api_query_id 的字典
- """
- globals.source_outflow_region_id = {
- region: [] for region in globals.source_outflow_region.keys()
- }
- # 提取所有唯一的 associated_source_outflow_id
- all_ids = set()
- for ids in globals.source_outflow_region.values():
- all_ids.update(ids)
- if not all_ids:
- logging.warning(
- "No associated_source_outflow_id found in source_outflow_region."
+ for mapping in target_maps.values():
+ mapping.clear()
+ with project_connection(name) as conn, conn.cursor() as cur:
+ cur.execute(
+ """
+ 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
+ """
)
- return globals.source_outflow_region_id
- conn_string = get_project_pgconn_string(db_name=name)
- try:
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- # 查询 associated_element_id 和 api_query_id
- query = f"""
- SELECT associated_element_id, api_query_id
- FROM scada_info
- WHERE associated_element_id = ANY(%s)
- """
- cur.execute(query, (list(all_ids),))
- rows = cur.fetchall()
- # 构建 associated_source_outflow_id 到 api_query_id 的映射
- id_to_api_query_id = {}
- for row in rows:
- associated_id = row[0]
- api_query_id = row[1]
- if associated_id in all_ids and api_query_id is not None:
- id_to_api_query_id[associated_id] = str(api_query_id)
- # 替换 source_outflow_region 中的 associated_source_outflow_id 为 api_query_id
- for region, ids in globals.source_outflow_region.items():
- for id_ in ids:
- api_id = id_to_api_query_id.get(id_)
- if api_id:
- globals.source_outflow_region_id[region].append(api_id)
- else:
- logging.warning(
- f"No api_query_id found for associated_source_outflow_id: {id_}"
- )
- except psycopg.Error as e:
- logging.error(f"数据库连接或查询出错: {e}")
- except Exception as ex:
- logging.error(f"处理数据时出错: {ex}")
- return globals.source_outflow_region_id
-
-
-# 2025/01/18
-def get_realtime_region_patterns(
- name: str,
- source_outflow_region_id: dict,
- realtime_region_pipe_flow_and_demand_id: dict,
-) -> Tuple[dict, dict]:
- """
- 根据每个 region,从 scada_info 表中查询 api_query_id 对应的 associated_pattern。
- 将结果分别存储到 source_outflow_region_patterns 和 realtime_region_pipe_flow_and_demand_patterns 两个字典中。
- :param name: 数据库名称
- :param source_outflow_region_id: 包含 region 与对应 api_query_id 的字典
- :param realtime_region_pipe_flow_and_demand_id: 包含 region 与对应 api_query_id 的字典
- :return: source_outflow_region_patterns 和 realtime_region_pipe_flow_and_demand_patterns 两个字典
- """
- # 初始化返回的字典
- globals.source_outflow_region_patterns = {
- region: [] for region in globals.source_outflow_region_id.keys()
- }
- globals.realtime_region_pipe_flow_and_demand_patterns = {
- region: [] for region in globals.realtime_region_pipe_flow_and_demand_id.keys()
- }
- conn_string = get_project_pgconn_string(db_name=name)
- try:
- with psycopg.connect(conn_string) as conn:
- with conn.cursor() as cur:
- # 遍历每个 region
- for region in globals.source_outflow_region_id.keys():
- # 获取 source_outflow_region_id 的 api_query_id 并查询 associated_pattern
- source_outflow_api_ids = globals.source_outflow_region_id[region]
- if source_outflow_api_ids:
- api_query_ids_str = ", ".join(
- [f"'{api_id}'" for api_id in source_outflow_api_ids]
- )
- cur.execute(
- f"""
- SELECT api_query_id, associated_pattern
- FROM scada_info
- WHERE api_query_id IN ({api_query_ids_str});
- """
- )
- results = cur.fetchall()
- globals.source_outflow_region_patterns[region] = [
- associated_pattern
- for _, associated_pattern in results
- if associated_pattern
- ]
- # 获取 realtime_region_pipe_flow_and_demand_id 的 api_query_id 并查询 associated_pattern
- realtime_api_ids = globals.realtime_region_pipe_flow_and_demand_id[
- region
- ]
- if realtime_api_ids:
- api_query_ids_str = ", ".join(
- [f"'{api_id}'" for api_id in realtime_api_ids]
- )
- cur.execute(
- f"""
- SELECT api_query_id, associated_pattern
- FROM scada_info
- WHERE api_query_id IN ({api_query_ids_str});
- """
- )
- results = cur.fetchall()
- globals.realtime_region_pipe_flow_and_demand_patterns[
- region
- ] = [
- associated_pattern
- for _, associated_pattern in results
- if associated_pattern
- ]
- logging.info(
- "生成 source_outflow_region_patterns 和 realtime_region_pipe_flow_and_demand_patterns 成功。"
- )
- except psycopg.Error as e:
- logging.error(f"数据库连接或查询出错: {e}")
- except Exception as ex:
- logging.error(f"处理数据时出错: {ex}")
- return (
- globals.source_outflow_region_patterns,
- globals.realtime_region_pipe_flow_and_demand_patterns,
- )
+ for record in cur.fetchall():
+ device_type = record["device_type"]
+ element_id = record["element_id"]
+ api_query_id = record["api_query_id"]
+ mapping = target_maps.get(str(device_type).lower())
+ if mapping is not None:
+ mapping[str(element_id)] = str(api_query_id)
def get_pattern_index(cur_datetime: str) -> int:
@@ -671,20 +138,6 @@ def convert_time_format(original_time: str) -> str:
return normalized_time.replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")
-def get_history_pattern_info(project_name, pattern_name):
- """读取选定pattern的保存的历史pattern信息flow和factor"""
- flow_list = []
- factor_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"]))
- factor_list.append(float(item["factor"]))
- return flow_list, factor_list
-
-
def _apply_valve_control(
project_name: str, valve_control: dict[str, dict]
) -> None:
@@ -722,8 +175,11 @@ def run_simulation(
modify_valve_opening: dict[str, float] = None,
scheme_type: str = None,
scheme_name: str = None,
+ result_db_name: str = None,
valve_control: dict[str, dict] = None,
-) -> None:
+ scheme_username: str = "system",
+ scheme_detail: dict | None = None,
+) -> UUID | None:
"""
传入需要修改的参数,改变数据库中对应位置的值,然后计算,返回结果
:param name: 模型名称,数据库中对应的名字
@@ -823,6 +279,7 @@ def run_simulation(
reservoir_SCADA_data_dict = TimescaleInternalQueries.query_scada_by_ids_time(
device_ids=list(globals.reservoirs_id.values()),
query_time=modify_pattern_start_time,
+ db_name=name,
)
# 2.构建出新字典,形式如{'ZBBDJSCP000002': '3.1231', 'R00003': '2.7387'}
reservoir_dict = {
@@ -847,6 +304,7 @@ def run_simulation(
tank_SCADA_data_dict = TimescaleInternalQueries.query_scada_by_ids_time(
device_ids=list(globals.tanks_id.values()),
query_time=modify_pattern_start_time,
+ db_name=name,
)
tank_dict = {
key: tank_SCADA_data_dict[value] for key, value in globals.tanks_id.items()
@@ -863,6 +321,7 @@ def run_simulation(
fixed_pump_SCADA_data_dict = TimescaleInternalQueries.query_scada_by_ids_time(
device_ids=list(globals.fixed_pumps_id.values()),
query_time=modify_pattern_start_time,
+ db_name=name,
)
# print(fixed_pump_SCADA_data_dict)
fixed_pump_dict = {
@@ -887,6 +346,7 @@ def run_simulation(
TimescaleInternalQueries.query_scada_by_ids_time(
device_ids=list(globals.variable_pumps_id.values()),
query_time=modify_pattern_start_time,
+ db_name=name,
)
)
variable_pump_dict = {
@@ -907,6 +367,7 @@ def run_simulation(
demand_SCADA_data_dict = TimescaleInternalQueries.query_scada_by_ids_time(
device_ids=list(globals.demand_id.values()),
query_time=modify_pattern_start_time,
+ db_name=name,
)
demand_dict = {
key: demand_SCADA_data_dict[value]
@@ -928,219 +389,7 @@ def run_simulation(
set_pattern(name_c, cs)
# 水质、压力实时数据使用方法待补充
#############################
- if globals.source_outflow_pattern_id:
- # 基于实时的出厂流量计数据,修改出厂流量计绑定的pattern
- source_outflow_SCADA_data_dict = (
- TimescaleInternalQueries.query_scada_by_ids_time(
- device_ids=list(globals.source_outflow_pattern_id.values()),
- query_time=modify_pattern_start_time,
- )
- )
- # print(source_outflow_SCADA_data_dict)
- source_outflow_dict = {
- key: source_outflow_SCADA_data_dict[value]
- for key, value in globals.source_outflow_pattern_id.items()
- }
- # print(source_outflow_dict)
- for pattern_name in source_outflow_dict.keys():
- # print(pattern_name)
- history_source_outflow_flow_list, history_source_outflow_factor_list = (
- get_history_pattern_info(name_c, pattern_name)
- )
- history_source_outflow_flow = history_source_outflow_flow_list[modify_index]
- history_source_outflow_factor = history_source_outflow_factor_list[
- modify_index
- ]
- # print(source_outflow_dict[pattern_name])
- # print(history_source_outflow_flow)
- # print(history_source_outflow_factor)
- if source_outflow_dict[pattern_name]:
- realtime_source_outflow = float(source_outflow_dict[pattern_name])
- multiply_factor = realtime_source_outflow / history_source_outflow_flow
- # print(multiply_factor)
- pattern = get_pattern(name_c, pattern_name)
- pattern["factors"][modify_index] = (
- multiply_factor * history_source_outflow_factor
- )
- # print(pattern['factors'][modify_index])
- cs = ChangeSet()
- cs.append(pattern)
- set_pattern(name_c, cs)
- if globals.realtime_pipe_flow_pattern_id:
- # 基于实时的pipe_flow类数据,修改pipe_flow类绑定的pattern
- realtime_pipe_flow_SCADA_data_dict = (
- TimescaleInternalQueries.query_scada_by_ids_time(
- device_ids=list(globals.realtime_pipe_flow_pattern_id.values()),
- query_time=modify_pattern_start_time,
- )
- )
- realtime_pipe_flow_dict = {
- key: realtime_pipe_flow_SCADA_data_dict[value]
- for key, value in globals.realtime_pipe_flow_pattern_id.items()
- }
- for pattern_name in realtime_pipe_flow_dict.keys():
- history_pipe_flow_flow_list, history_pipe_flow_factor_list = (
- get_history_pattern_info(name_c, pattern_name)
- )
- history_pipe_flow_flow = history_pipe_flow_flow_list[modify_index]
- history_pipe_flow_factor = history_pipe_flow_factor_list[modify_index]
- if realtime_pipe_flow_dict[pattern_name]:
- realtime_pipe_flow = float(realtime_pipe_flow_dict[pattern_name])
- multiply_factor = realtime_pipe_flow / history_pipe_flow_flow
- pattern = get_pattern(name_c, pattern_name)
- pattern["factors"][modify_index] = (
- multiply_factor * history_pipe_flow_factor
- )
- cs = ChangeSet()
- cs.append(pattern)
- set_pattern(name_c, cs)
- if globals.pipe_flow_region_patterns:
- # 基于实时的pipe_flow类数据,修改pipe_flow分区流量计范围内的non_realtime的demand绑定的pattern
- temp_realtime_pipe_flow_pattern_id = {}
- # 遍历 pipe_flow_region_patterns 字典的 key
- for (
- pipe_flow_region,
- demand_patterns,
- ) in globals.pipe_flow_region_patterns.items():
- # 获取对应的实时值
- query_api_id = globals.realtime_pipe_flow_pattern_id.get(pipe_flow_region)
- temp_realtime_pipe_flow_pattern_id[pipe_flow_region] = query_api_id
- temp_realtime_pipe_flow_SCADA_data_dict = (
- TimescaleInternalQueries.query_scada_by_ids_time(
- device_ids=list(temp_realtime_pipe_flow_pattern_id.values()),
- query_time=modify_pattern_start_time,
- )
- )
- temp_realtime_pipe_flow_dict = {
- key: temp_realtime_pipe_flow_SCADA_data_dict[value]
- for key, value in temp_realtime_pipe_flow_pattern_id.items()
- }
- for pattern_name in temp_realtime_pipe_flow_dict.keys():
- temp_history_pipe_flow_flow_list, temp_history_pipe_flow_factor_list = (
- get_history_pattern_info(name_c, pattern_name)
- )
- temp_history_pipe_flow_flow = temp_history_pipe_flow_flow_list[modify_index]
- if temp_realtime_pipe_flow_dict[pattern_name]:
- temp_realtime_pipe_flow = float(
- temp_realtime_pipe_flow_dict[pattern_name]
- )
- temp_multiply_factor = (
- temp_realtime_pipe_flow / temp_history_pipe_flow_flow
- )
- temp_non_realtime_demand_pattern_list = (
- globals.pipe_flow_region_patterns[pattern_name]
- )
- for demand_pattern_name in temp_non_realtime_demand_pattern_list:
- (
- history_non_realtime_demand_flow_list,
- history_non_realtime_demand_factor_list,
- ) = get_history_pattern_info(name_c, demand_pattern_name)
- history_non_realtime_demand_factor = (
- history_non_realtime_demand_factor_list[modify_index]
- )
- pattern = get_pattern(name_c, demand_pattern_name)
- pattern["factors"][modify_index] = (
- temp_multiply_factor * history_non_realtime_demand_factor
- )
- cs = ChangeSet()
- cs.append(pattern)
- set_pattern(name_c, cs)
- if globals.source_outflow_region:
- # 根据associated_source_outflow_id进行分区,各分区用(出厂的流量计 - 实时的pipe_flow和demand)进行数据更新
- for region in globals.source_outflow_region.keys():
- temp_source_outflow_region_id = globals.source_outflow_region_id.get(
- region, []
- )
- temp_realtime_region_pipe_flow_and_demand_id = (
- globals.realtime_region_pipe_flow_and_demand_id.get(region, [])
- )
- temp_source_outflow_region_patterns = (
- globals.source_outflow_region_patterns.get(region, [])
- )
- temp_realtime_region_pipe_flow_and_demand_patterns = (
- globals.realtime_region_pipe_flow_and_demand_patterns.get(region, [])
- )
- temp_non_realtime_region_patterns = (
- globals.non_realtime_region_patterns.get(region, [])
- )
- region_source_outflow_data_dict = (
- TimescaleInternalQueries.query_scada_by_ids_time(
- device_ids=temp_source_outflow_region_id,
- query_time=modify_pattern_start_time,
- )
- )
- region_realtime_region_pipe_flow_and_demand_data_dict = (
- TimescaleInternalQueries.query_scada_by_ids_time(
- device_ids=temp_realtime_region_pipe_flow_and_demand_id,
- query_time=modify_pattern_start_time,
- )
- )
- # 2025/02/12 确保 region_source_outflow_data_dict 和
- # region_realtime_region_pipe_flow_and_demand_data_dict中的每个值都不是 None 且不为 0
- region_source_outflow_valid_values = [
- float(value)
- for value in region_source_outflow_data_dict.values()
- if value not in [None, 0]
- ]
- valid_values = [
- float(value)
- for value in region_realtime_region_pipe_flow_and_demand_data_dict.values()
- if value not in [None, 0]
- ]
- # 如果都非空,则执行 sum 操作
- if region_source_outflow_valid_values and valid_values:
- region_total_source_outflow = sum(region_source_outflow_valid_values)
- history_region_total_source_outflow = 0
- for source_outflow_pattern_name in temp_source_outflow_region_patterns:
- (
- temp_history_source_outflow_flow_list,
- temp_history_source_outflow_factor_list,
- ) = get_history_pattern_info(name_c, source_outflow_pattern_name)
- history_region_total_source_outflow += (
- temp_history_source_outflow_flow_list[modify_index]
- )
- region_total_realtime_region_pipe_flow_and_demand = sum(valid_values)
- history_region_total_realtime_region_pipe_flow_and_demand = 0
- for (
- pipe_flow_and_demand_pattern_name
- ) in temp_realtime_region_pipe_flow_and_demand_patterns:
- (
- temp_history_pipe_flow_and_demand_flow_list,
- temp_history_pipe_flow_and_demand_factor_list,
- ) = get_history_pattern_info(
- name_c, pipe_flow_and_demand_pattern_name
- )
- history_region_total_realtime_region_pipe_flow_and_demand += (
- temp_history_pipe_flow_and_demand_flow_list[modify_index]
- )
- temp_multiply_factor = (
- region_total_source_outflow
- - region_total_realtime_region_pipe_flow_and_demand
- ) / (
- history_region_total_source_outflow
- - history_region_total_realtime_region_pipe_flow_and_demand
- )
- for (
- non_realtime_region_pattern_name
- ) in temp_non_realtime_region_patterns:
- (
- history_non_realtime_region_pattern_flow_list,
- history_non_realtime_region_pattern_factor_list,
- ) = get_history_pattern_info(
- name_c, non_realtime_region_pattern_name
- )
- history_non_realtime_region_pattern_factor = (
- history_non_realtime_region_pattern_factor_list[modify_index]
- )
- pattern = get_pattern(name_c, non_realtime_region_pattern_name)
- pattern["factors"][modify_index] = (
- temp_multiply_factor
- * history_non_realtime_region_pattern_factor
- )
- cs = ChangeSet()
- cs.append(pattern)
- set_pattern(name_c, cs)
- # 根据输入的参数进行数据修改,后面修改的可以覆盖前面的,用于EXTENDED类的方案模拟
+ # 显式请求参数覆盖实时设备数据,用于扩展模拟。
# 修改清水池(reservoir)液位的pattern
if modify_reservoir_head_pattern:
for reservoir_name in modify_reservoir_head_pattern.keys():
@@ -1277,10 +526,7 @@ def run_simulation(
# 存储
starttime = time.time()
# 临时处理输入的name问题,后续需要优化,需要传递/获取iot数据库的名字
- if name.find("_") != -1:
- db_name = name.split("_")[2]
- else:
- db_name = name
+ db_name = result_db_name or name
if simulation_type.upper() == "REALTIME":
TimescaleInternalStorage.store_realtime_simulation(
node_result, link_result, modify_pattern_start_time, db_name=db_name
@@ -1289,15 +535,45 @@ def run_simulation(
result_timestep_seconds = times_info.get("report_step")
if result_timestep_seconds is None:
raise RuntimeError("run_project output missing times.report_step")
- TimescaleInternalStorage.store_scheme_simulation(
- scheme_type,
- scheme_name,
- node_result,
- link_result,
- modify_pattern_start_time,
- num_periods_result,
- result_timestep_seconds,
- db_name=db_name,
+ if not scheme_type or not scheme_name:
+ raise ValueError("extended simulation requires analysis run type and name")
+ detail = scheme_detail or {}
+ run_id = create_analysis_run(
+ name=db_name,
+ scheme_name=scheme_name,
+ scheme_type=scheme_type,
+ username=scheme_username,
+ scheme_start_time=modify_pattern_start_time,
+ scheme_detail=detail,
+ )
+ try:
+ TimescaleInternalStorage.store_analysis_simulation(
+ run_id,
+ node_result,
+ link_result,
+ modify_pattern_start_time,
+ num_periods_result,
+ result_timestep_seconds,
+ db_name=db_name,
+ )
+ except Exception:
+ try:
+ update_analysis_run(
+ db_name,
+ run_id,
+ status="failed",
+ username=scheme_username,
+ scheme_detail=detail,
+ )
+ except Exception:
+ logging.exception("failed to mark analysis run %s as failed", run_id)
+ raise
+ update_analysis_run(
+ db_name,
+ run_id,
+ status="completed",
+ username=scheme_username,
+ scheme_detail=detail,
)
endtime = time.time()
logging.info("store time: %f", endtime - starttime)
@@ -1305,82 +581,4 @@ def run_simulation(
# TimescaleInternalQueries.fill_scheme_simulation_result_to_SCADA(scheme_type=scheme_type, scheme_name=scheme_name)
print("after store result")
-
-
-if __name__ == "__main__":
- # 计算前,获取scada_info中的信息,按照设定的方法修改pg数据库
- query_corresponding_element_id_and_query_id(project_info.name)
- query_corresponding_pattern_id_and_query_id(project_info.name)
- region_result = query_non_realtime_region(project_info.name)
-
- globals.source_outflow_region_id = get_source_outflow_region_id(
- project_info.name, region_result
- )
- globals.realtime_region_pipe_flow_and_demand_id = (
- query_realtime_region_pipe_flow_and_demand_id(project_info.name, region_result)
- )
- globals.pipe_flow_region_patterns = query_pipe_flow_region_patterns(
- project_info.name
- )
-
- globals.non_realtime_region_patterns = query_non_realtime_region_patterns(
- project_info.name, region_result
- )
- (
- globals.source_outflow_region_patterns,
- globals.realtime_region_pipe_flow_and_demand_patterns,
- ) = get_realtime_region_patterns(
- project_info.name,
- globals.source_outflow_region_id,
- globals.realtime_region_pipe_flow_and_demand_id,
- )
-
- # 基础日期和时间(日期部分保持不变)
- base_date = datetime(2025, 5, 4)
-
- # 循环生成96个时间点(15分钟间隔)
- for i in range(96):
- # 计算当前时间偏移
- time_offset = timedelta(minutes=15 * i)
-
- # 生成完整时间对象
- current_time = base_date + time_offset
-
- # 格式化成ISO8601带时区格式
- iso_time = current_time.strftime("%Y-%m-%dT%H:%M:%S") + "+08:00"
-
- # 执行函数调用
- run_simulation(
- name=project_info.name,
- simulation_type="realtime",
- modify_pattern_start_time=iso_time,
- )
-
- # 打印字典内容以验证
- # print("Reservoirs ID:", globals.reservoirs_id)
- # print("Tanks ID:", globals.tanks_id)
- # print("Fixed Pumps ID:", globals.fixed_pumps_id)
- # print("Variable Pumps ID:", globals.variable_pumps_id)
- # print("Pressure ID:", globals.pressure_id)
- # print("Demand ID:", globals.demand_id)
- # print("Quality ID:", globals.quality_id)
- # print("Source Outflow Pattern ID:", globals.source_outflow_pattern_id)
- # print("Realtime Pipe Flow Pattern ID:", globals.realtime_pipe_flow_pattern_id)
- # print("Pipe Flow Region Patterns:", globals.pipe_flow_region_patterns)
- # print("Source Outflow Region:", region_result)
- # print('Source Outflow Region ID:', globals.source_outflow_region_id)
- # print('Source Outflow Region Patterns:', globals.source_outflow_region_patterns)
- # print("Non Realtime Region Patterns:", globals.non_realtime_region_patterns)
- # print("Realtime Region Pipe Flow And Demand ID:", globals.realtime_region_pipe_flow_and_demand_id)
- # print("Realtime Region Pipe Flow And Demand Patterns:", globals.realtime_region_pipe_flow_and_demand_patterns)
-
- # dump_inp(name='bb', inp="sensor_placement.inp", version='2')
- # 模拟示例1
- # run_simulation(name='bb', simulation_type="realtime", modify_pattern_start_time='2025-02-25T23:45:00+08:00')
- # 模拟示例2
- # run_simulation(name='bb', simulation_type="extended", modify_pattern_start_time='2025-03-10T12:00:00+08:00',
- # modify_total_duration=1800, scheme_type="burst_Analysis", scheme_name="scheme1")
-
- # 查询示例1:query_SCADA_ID_corresponding_info
- # result = query_SCADA_ID_corresponding_info(name='bb', SCADA_ID='P10755')
- # print(result)
+ return run_id if simulation_type.upper() == "EXTENDED" else None
diff --git a/app/services/simulation_ops.py b/app/services/simulation_ops.py
index 08b8020..8621ac7 100644
--- a/app/services/simulation_ops.py
+++ b/app/services/simulation_ops.py
@@ -44,8 +44,6 @@ def project_management(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Copying Database."
)
- # CopyProjectEx()(prj_name, new_name,
- # ['operation', 'current_operation', 'restore_operation', 'batch_operation', 'operation_table'])
copy_project(prj_name + "_template", new_name)
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
@@ -96,8 +94,6 @@ def scheduling_simulation(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Copying Database."
)
- # CopyProjectEx()(prj_name, new_name,
- # ['operation', 'current_operation', 'restore_operation', 'batch_operation', 'operation_table'])
copy_project(prj_name + "_template", new_name)
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
@@ -188,8 +184,6 @@ def daily_scheduling_simulation(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Copying Database."
)
- # CopyProjectEx()(prj_name, new_name,
- # ['operation', 'current_operation', 'restore_operation', 'batch_operation', 'operation_table'])
copy_project(prj_name + "_template", new_name)
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
diff --git a/app/services/tjnetwork.py b/app/services/tjnetwork.py
index 2400e38..272b68c 100644
--- a/app/services/tjnetwork.py
+++ b/app/services/tjnetwork.py
@@ -1,1359 +1,320 @@
+"""Application-facing water-network service boundary.
+
+The native WNDB package is organized by responsibility. This module only
+combines operations that need more than one native module and re-exports the
+small set of operations used by HTTP and algorithm services.
+"""
+
from typing import Any
-import app.native.wndb as api
-from app.native.wndb.s36_wda_cal import *
+
import app.infra.epanet as epanet
+from app.algorithms.water_demand import (
+ calculate_demand_to_network,
+ calculate_demand_to_nodes,
+ calculate_demand_to_region,
+)
+from app.infra.db.postgresql.scada_assets import (
+ get_all_scada_info,
+ get_scada_info,
+ get_scada_info_schema,
+)
+from app.native.wndb.commands.api import (
+ delete_curve_cascade as delete_curve,
+ delete_junction_cascade as delete_junction,
+ delete_pattern_cascade as delete_pattern,
+ delete_pipe_cascade as delete_pipe,
+ delete_pump_cascade as delete_pump,
+ delete_reservoir_cascade as delete_reservoir,
+ delete_tank_cascade as delete_tank,
+ delete_valve_cascade as delete_valve,
+ set_option_ex as set_option,
+ set_option_v3_ex as set_option_v3,
+)
+from app.native.wndb.core.database import ChangeSet, read_all
+from app.native.wndb.core.projects import (
+ close_project,
+ copy_project,
+ create_project,
+ delete_project,
+ have_project,
+ is_project_open,
+ list_project,
+ open_project,
+)
+from app.native.wndb.gis.backdrop import (
+ get_backdrop,
+ get_backdrop_schema,
+ set_backdrop,
+)
+from app.native.wndb.gis.coordinates import get_node_coord
+from app.native.wndb.gis.labels import (
+ add_label,
+ delete_label,
+ get_label,
+ get_label_schema,
+ set_label,
+)
+from app.native.wndb.gis.region_geometry import get_nodes_in_region
+from app.native.wndb.gis.regions import (
+ add_region,
+ delete_region,
+ get_region,
+ get_region_schema,
+ set_region,
+)
+from app.native.wndb.gis.vertices import (
+ add_vertex,
+ delete_vertex,
+ get_all_vertex_links,
+ get_all_vertices,
+ get_vertex,
+ get_vertex_schema,
+ set_vertex,
+)
+from app.native.wndb.inp.exporter import dump_inp, export_inp
+from app.native.wndb.inp.importer import convert_inp_v3_to_v2, read_inp
+from app.native.wndb.model.controls import (
+ get_control,
+ get_control_schema,
+ set_control,
+)
+from app.native.wndb.model.curves import (
+ add_curve,
+ get_curve,
+ get_curve_schema,
+ set_curve,
+)
+from app.native.wndb.model.demands import get_demand, get_demand_schema, set_demand
+from app.native.wndb.model.elements import (
+ get_curves,
+ get_element_type,
+ get_element_type_value,
+ get_link_nodes,
+ get_link_type,
+ get_links,
+ get_links_id_and_type,
+ get_major_nodes,
+ get_major_pipes,
+ get_node_links,
+ get_node_type,
+ get_nodes,
+ get_nodes_id_and_type,
+ get_patterns,
+ get_regions,
+ is_curve,
+ is_junction,
+ is_link,
+ is_node,
+ is_pattern,
+ is_pipe,
+ is_pump,
+ is_reservoir,
+ is_tank,
+ is_valve,
+)
+from app.native.wndb.model.emitters import (
+ get_emitter,
+ get_emitter_schema,
+ set_emitter,
+)
+from app.native.wndb.model.energy import (
+ get_energy,
+ get_energy_schema,
+ get_pump_energy,
+ get_pump_energy_schema,
+ set_energy,
+ set_pump_energy,
+)
+from app.native.wndb.model.junctions import (
+ add_junction,
+ get_all_junctions,
+ get_junction,
+ get_junction_schema,
+ set_junction,
+)
+from app.native.wndb.model.mixing import (
+ add_mixing,
+ delete_mixing,
+ get_mixing,
+ get_mixing_schema,
+ set_mixing,
+)
+from app.native.wndb.model.options import (
+ OPTION_DEMAND_MODEL_PDA,
+ OPTION_QUALITY_CHEMICAL,
+ get_option,
+ get_option_v3,
+ get_option_v3_schema,
+)
+from app.native.wndb.model.patterns import (
+ add_pattern,
+ get_pattern,
+ get_pattern_schema,
+ set_pattern,
+)
+from app.native.wndb.model.pipes import (
+ PIPE_STATUS_OPEN,
+ add_pipe,
+ get_all_pipes,
+ get_pipe,
+ get_pipe_schema,
+ set_pipe,
+)
+from app.native.wndb.model.pumps import (
+ add_pump,
+ get_all_pumps,
+ get_pump,
+ get_pump_schema,
+ set_pump,
+)
+from app.native.wndb.model.quality import (
+ get_quality,
+ get_quality_schema,
+ set_quality,
+)
+from app.native.wndb.model.reactions import (
+ get_pipe_reaction,
+ get_pipe_reaction_schema,
+ get_reaction,
+ get_reaction_schema,
+ get_tank_reaction,
+ get_tank_reaction_schema,
+ set_pipe_reaction,
+ set_reaction,
+ set_tank_reaction,
+)
+from app.native.wndb.model.reservoirs import (
+ add_reservoir,
+ get_all_reservoirs,
+ get_reservoir,
+ get_reservoir_schema,
+ set_reservoir,
+)
+from app.native.wndb.model.rules import get_rule, get_rule_schema, set_rule
+from app.native.wndb.model.sources import (
+ SOURCE_TYPE_SETPOINT,
+ add_source,
+ delete_source,
+ get_source,
+ get_source_schema,
+ set_source,
+)
+from app.native.wndb.model.status import (
+ get_status,
+ get_status_schema,
+ set_status,
+)
+from app.native.wndb.model.tags import get_tag, get_tag_schema, get_tags, set_tag
+from app.native.wndb.model.tanks import (
+ add_tank,
+ get_all_tanks,
+ get_tank,
+ get_tank_schema,
+ set_tank,
+)
+from app.native.wndb.model.times import get_time, get_time_schema, set_time
+from app.native.wndb.model.title import get_title, get_title_schema, set_title
+from app.native.wndb.model.valves import (
+ VALVES_TYPE_PRV,
+ add_valve,
+ get_all_valves,
+ get_valve,
+ get_valve_schema,
+ set_valve,
+)
-############################################################
-# ChangeSet
-############################################################
-
-API_ADD = api.API_ADD
-API_UPDATE = api.API_UPDATE
-API_DELETE = api.API_DELETE
-
-ChangeSet = api.ChangeSet
-
-
-############################################################
-# enum
-############################################################
-
-JUNCTION = api.JUNCTION
-RESERVOIR = api.RESERVOIR
-TANK = api.TANK
-PIPE = api.PIPE
-PUMP = api.PUMP
-VALVE = api.VALVE
-PATTERN = api.PATTERN
-CURVE = api.CURVE
-
-OVERFLOW_YES = api.OVERFLOW_YES
-OVERFLOW_NO = api.OVERFLOW_NO
-
-PIPE_STATUS_OPEN = api.PIPE_STATUS_OPEN
-PIPE_STATUS_CLOSED = api.PIPE_STATUS_CLOSED
-PIPE_STATUS_CV = api.PIPE_STATUS_CV
-
-VALVES_TYPE_PRV = api.VALVES_TYPE_PRV
-VALVES_TYPE_PSV = api.VALVES_TYPE_PSV
-VALVES_TYPE_PBV = api.VALVES_TYPE_PBV
-VALVES_TYPE_FCV = api.VALVES_TYPE_FCV
-VALVES_TYPE_TCV = api.VALVES_TYPE_TCV
-VALVES_TYPE_GPV = api.VALVES_TYPE_GPV
-
-TAG_TYPE_NODE = api.TAG_TYPE_NODE
-TAG_TYPE_LINK = api.TAG_TYPE_LINK
-
-LINK_STATUS_OPEN = api.LINK_STATUS_OPEN
-LINK_STATUS_CLOSED = api.LINK_STATUS_CLOSED
-LINK_STATUS_ACTIVE = api.LINK_STATUS_ACTIVE
-
-CURVE_TYPE_PUMP = api.CURVE_TYPE_PUMP
-CURVE_TYPE_EFFICIENCY = api.CURVE_TYPE_EFFICIENCY
-CURVE_TYPE_VOLUME = api.CURVE_TYPE_VOLUME
-CURVE_TYPE_HEADLOSS = api.CURVE_TYPE_HEADLOSS
-
-SOURCE_TYPE_CONCEN = api.SOURCE_TYPE_CONCEN
-SOURCE_TYPE_MASS = api.SOURCE_TYPE_MASS
-SOURCE_TYPE_FLOWPACED = api.SOURCE_TYPE_FLOWPACED
-SOURCE_TYPE_SETPOINT = api.SOURCE_TYPE_SETPOINT
-
-MIXING_MODEL_MIXED = api.MIXING_MODEL_MIXED
-MIXING_MODEL_2COMP = api.MIXING_MODEL_2COMP
-MIXING_MODEL_FIFO = api.MIXING_MODEL_FIFO
-MIXING_MODEL_LIFO = api.MIXING_MODEL_LIFO
-
-TIME_STATISTIC_NONE = api.TIME_STATISTIC_NONE
-TIME_STATISTIC_AVERAGED = api.TIME_STATISTIC_AVERAGED
-TIME_STATISTIC_MINIMUM = api.TIME_STATISTIC_MINIMUM
-TIME_STATISTIC_MAXIMUM = api.TIME_STATISTIC_MAXIMUM
-TIME_STATISTIC_RANGE = api.TIME_STATISTIC_RANGE
-
-OPTION_UNITS_CFS = api.OPTION_UNITS_CFS
-OPTION_UNITS_GPM = api.OPTION_UNITS_GPM
-OPTION_UNITS_MGD = api.OPTION_UNITS_MGD
-OPTION_UNITS_IMGD = api.OPTION_UNITS_IMGD
-OPTION_UNITS_AFD = api.OPTION_UNITS_AFD
-OPTION_UNITS_LPS = api.OPTION_UNITS_LPS
-OPTION_UNITS_LPM = api.OPTION_UNITS_LPM
-OPTION_UNITS_MLD = api.OPTION_UNITS_MLD
-OPTION_UNITS_CMH = api.OPTION_UNITS_CMH
-OPTION_UNITS_CMD = api.OPTION_UNITS_CMD
-
-OPTION_PRESSURE_PSI = api.OPTION_PRESSURE_PSI
-OPTION_PRESSURE_KPA = api.OPTION_PRESSURE_KPA
-OPTION_PRESSURE_METERS = api.OPTION_PRESSURE_METERS
-
-OPTION_HEADLOSS_HW = api.OPTION_HEADLOSS_HW
-OPTION_HEADLOSS_DW = api.OPTION_HEADLOSS_DW
-OPTION_HEADLOSS_CM = api.OPTION_HEADLOSS_CM
-
-OPTION_UNBALANCED_STOP = api.OPTION_UNBALANCED_STOP
-OPTION_UNBALANCED_CONTINUE = api.OPTION_UNBALANCED_CONTINUE
-
-OPTION_DEMAND_MODEL_DDA = api.OPTION_DEMAND_MODEL_DDA
-OPTION_DEMAND_MODEL_PDA = api.OPTION_DEMAND_MODEL_PDA
-
-OPTION_QUALITY_NONE = api.OPTION_QUALITY_NONE
-OPTION_QUALITY_CHEMICAL = api.OPTION_QUALITY_CHEMICAL
-OPTION_QUALITY_AGE = api.OPTION_QUALITY_AGE
-OPTION_QUALITY_TRACE = api.OPTION_QUALITY_TRACE
-
-OPTION_V3_FLOW_UNITS_CFS = api.OPTION_V3_FLOW_UNITS_CFS
-OPTION_V3_FLOW_UNITS_GPM = api.OPTION_V3_FLOW_UNITS_GPM
-OPTION_V3_FLOW_UNITS_MGD = api.OPTION_V3_FLOW_UNITS_MGD
-OPTION_V3_FLOW_UNITS_IMGD = api.OPTION_V3_FLOW_UNITS_IMGD
-OPTION_V3_FLOW_UNITS_AFD = api.OPTION_V3_FLOW_UNITS_AFD
-OPTION_V3_FLOW_UNITS_LPS = api.OPTION_V3_FLOW_UNITS_LPS
-OPTION_V3_FLOW_UNITS_LPM = api.OPTION_V3_FLOW_UNITS_LPM
-OPTION_V3_FLOW_UNITS_MLD = api.OPTION_V3_FLOW_UNITS_MLD
-OPTION_V3_FLOW_UNITS_CMH = api.OPTION_V3_FLOW_UNITS_CMH
-OPTION_V3_FLOW_UNITS_CMD = api.OPTION_V3_FLOW_UNITS_CMD
-
-OPTION_V3_PRESSURE_UNITS_PSI = api.OPTION_V3_PRESSURE_UNITS_PSI
-OPTION_V3_PRESSURE_UNITS_KPA = api.OPTION_V3_PRESSURE_UNITS_KPA
-OPTION_V3_PRESSURE_UNITS_METERS = api.OPTION_V3_PRESSURE_UNITS_METERS
-
-OPTION_V3_HEADLOSS_MODEL_HW = api.OPTION_V3_HEADLOSS_MODEL_HW
-OPTION_V3_HEADLOSS_MODEL_DW = api.OPTION_V3_HEADLOSS_MODEL_DW
-OPTION_V3_HEADLOSS_MODEL_CM = api.OPTION_V3_HEADLOSS_MODEL_CM
-
-OPTION_V3_STEP_SIZING_FULL = api.OPTION_V3_STEP_SIZING_FULL
-OPTION_V3_STEP_SIZING_RELAXATION = api.OPTION_V3_STEP_SIZING_RELAXATION
-OPTION_V3_STEP_SIZING_LINESEARCH = api.OPTION_V3_STEP_SIZING_LINESEARCH
-
-OPTION_V3_IF_UNBALANCED_STOP = api.OPTION_V3_IF_UNBALANCED_STOP
-OPTION_V3_IF_UNBALANCED_CONTINUE = api.OPTION_V3_IF_UNBALANCED_CONTINUE
-
-OPTION_V3_DEMAND_MODEL_FIXED = api.OPTION_V3_DEMAND_MODEL_FIXED
-OPTION_V3_DEMAND_MODEL_CONSTRAINED = api.OPTION_V3_DEMAND_MODEL_CONSTRAINED
-OPTION_V3_DEMAND_MODEL_POWER = api.OPTION_V3_DEMAND_MODEL_POWER
-OPTION_V3_DEMAND_MODEL_LOGISTIC = api.OPTION_V3_DEMAND_MODEL_LOGISTIC
-
-OPTION_V3_LEAKAGE_MODEL_NONE = api.OPTION_V3_LEAKAGE_MODEL_NONE
-OPTION_V3_LEAKAGE_MODEL_POWER = api.OPTION_V3_LEAKAGE_MODEL_POWER
-OPTION_V3_LEAKAGE_MODEL_FAVAD = api.OPTION_V3_LEAKAGE_MODEL_FAVAD
-
-OPTION_V3_QUALITY_MODEL_NONE = api.OPTION_V3_QUALITY_MODEL_NONE
-OPTION_V3_QUALITY_MODEL_CHEMICAL = api.OPTION_V3_QUALITY_MODEL_CHEMICAL
-OPTION_V3_QUALITY_MODEL_AGE = api.OPTION_V3_QUALITY_MODEL_AGE
-OPTION_V3_QUALITY_MODEL_TRACE = api.OPTION_V3_QUALITY_MODEL_TRACE
-
-OPTION_V3_QUALITY_UNITS_HRS = api.OPTION_V3_QUALITY_UNITS_HRS
-OPTION_V3_QUALITY_UNITS_PCNT = api.OPTION_V3_QUALITY_UNITS_PCNT
-OPTION_V3_QUALITY_UNITS_MGL = api.OPTION_V3_QUALITY_UNITS_MGL
-OPTION_V3_QUALITY_UNITS_UGL = api.OPTION_V3_QUALITY_UNITS_UGL
-
-SCADA_DEVICE_TYPE_PRESSURE = api.SCADA_DEVICE_TYPE_PRESSURE
-SCADA_DEVICE_TYPE_DEMAND = api.SCADA_DEVICE_TYPE_DEMAND
-SCADA_DEVICE_TYPE_QUALITY = api.SCADA_DEVICE_TYPE_QUALITY
-SCADA_DEVICE_TYPE_LEVEL = api.SCADA_DEVICE_TYPE_LEVEL
-SCADA_DEVICE_TYPE_FLOW = api.SCADA_DEVICE_TYPE_FLOW
-SCADA_DEVICE_TYPE_UNKNOWN = api.SCADA_DEVICE_TYPE_UNKNOWN
-
-
-SCADA_MODEL_TYPE_JUNCTION = api.SCADA_MODEL_TYPE_JUNCTION
-SCADA_MODEL_TYPE_RESERVOIR = api.SCADA_MODEL_TYPE_RESERVOIR
-SCADA_MODEL_TYPE_TANK = api.SCADA_MODEL_TYPE_TANK
-SCADA_MODEL_TYPE_PIPE = api.SCADA_MODEL_TYPE_PIPE
-SCADA_MODEL_TYPE_PUMP = api.SCADA_MODEL_TYPE_PUMP
-SCADA_MODEL_TYPE_VALVE = api.SCADA_MODEL_TYPE_VALVE
-
-
-SCADA_ELEMENT_STATUS_ONLINE = api.SCADA_ELEMENT_STATUS_ONLINE
-SCADA_ELEMENT_STATUS_OFFLINE = api.SCADA_ELEMENT_STATUS_OFFLINE
-
-
-PARTITION_TYPE_RB = api.PARTITION_TYPE_RB
-PARTITION_TYPE_KWAY = api.PARTITION_TYPE_KWAY
-
-
-############################################################
-# project
-############################################################
-
-def list_project() -> list[str]:
- return api.list_project()
-
-def have_project(name: str) -> bool:
- return api.have_project(name)
-
-def create_project(name: str) -> None:
- return api.create_project(name)
-
-def delete_project(name: str) -> None:
- return api.delete_project(name)
-
-def clean_project(excluded: list[str] = []) -> None:
- return api.clean_project(excluded)
-
-def is_project_open(name: str) -> bool:
- return api.is_project_open(name)
-
-def open_project(name: str) -> None:
- return api.open_project(name)
-
-def close_project(name: str) -> None:
- return api.close_project(name)
-
-def copy_project(source: str, new: str) -> None:
- return api.copy_project(source, new)
-
-def read_inp(name: str, inp: str, version: str = '3') -> bool:
- return api.read_inp(name, inp, version)
-
-def dump_inp(name: str, inp: str, version: str = '3') -> None:
- return api.dump_inp(name, inp, version)
-
-def import_inp(name: str, cs: ChangeSet, version: str = '3') -> bool:
- return api.import_inp(name, cs, version)
-
-def export_inp(name: str, version: str = '3') -> ChangeSet:
- return api.export_inp(name, version)
-
-#DingZQ, 2025-02-04, 返回dict[str, Any]
def run_project_return_dict(name: str) -> dict[str, Any]:
return epanet.run_project_return_dict(name, True)
-# original code
+
def run_project(name: str) -> str:
return epanet.run_project(name)
-# put in inp folder, name without extension
+
def run_inp(name: str) -> str:
return epanet.run_inp(name)
-# path is absolute path
+
def dump_output(path: str) -> str:
return epanet.dump_output(path)
-#DingZQ, 2024-12-28, convert inp v3 to v2
-def convert_inp_v3_to_v2(inp: str) -> ChangeSet:
- return api.convert_inp_v3_to_v2(inp)
-############################################################
-# operation
-############################################################
-
-def get_current_operation(name: str) -> int:
- return api.get_current_operation(name)
-
-def execute_undo(name: str, discard: bool = False) -> ChangeSet:
- return api.execute_undo(name, discard)
-
-def execute_redo(name: str) -> ChangeSet:
- return api.execute_redo(name)
-
-def list_snapshot(name: str) -> list[tuple[int, str]]:
- return api.list_snapshot(name)
-
-def have_snapshot(name: str, tag: str) -> bool:
- return api.have_snapshot(name, tag)
-
-def have_snapshot_for_operation(name: str, operation: int) -> bool:
- return api.have_snapshot_for_operation(name, operation)
-
-def have_snapshot_for_current_operation(name: str) -> bool:
- return api.have_snapshot_for_current_operation(name)
-
-def take_snapshot_for_operation(name: str, operation: int, tag: str) -> None:
- return api.take_snapshot_for_operation(name, operation, tag)
-
-def take_snapshot_for_current_operation(name: str, tag: str) -> None:
- return api.take_snapshot_for_current_operation(name, tag)
-
-# deprecated ! use take_snapshot_for_current_operation instead
-def take_snapshot(name: str, tag: str) -> None:
- return api.take_snapshot(name, tag)
-
-def update_snapshot(name: str, operation: int, tag: str) -> None:
- return api.update_snapshot(name, operation, tag)
-
-def update_snapshot_for_current_operation(name: str, tag: str) -> None:
- return api.update_snapshot_for_current_operation(name, tag)
-
-def delete_snapshot(name: str, tag: str) -> None:
- return api.delete_snapshot(name, tag)
-
-def delete_snapshot_by_operation(name: str, operation: int) -> None:
- return api.delete_snapshot_by_operation(name, operation)
-
-def get_operation_by_snapshot(name: str, tag: str) -> int | None:
- return api.get_operation_by_snapshot(name, tag)
-
-def get_snapshot_by_operation(name: str, operation: int) -> str | None:
- return api.get_snapshot_by_operation(name, operation)
-
-def pick_snapshot(name: str, tag: str, discard: bool = False) -> ChangeSet:
- return api.pick_snapshot(name, tag, discard)
-
-def pick_operation(name: str, operation: int, discard: bool = False) -> ChangeSet:
- return api.pick_operation(name, operation, discard)
-
-def sync_with_server(name: str, operation: int) -> ChangeSet:
- return api.sync_with_server(name, operation)
-
-# combine commands as one undo/redo unit
-def execute_batch_command(name: str, cs: ChangeSet) -> ChangeSet:
- return api.execute_batch_command(name, cs)
-
-# execute command one by one
-def execute_batch_commands(name: str, cs: ChangeSet) -> ChangeSet:
- return api.execute_batch_commands(name, cs)
-
-def get_restore_operation(name: str) -> int:
- return api.get_restore_operation(name)
-
-def set_restore_operation(name: str, operation: int) -> None:
- return api.set_restore_operation(name, operation)
-
-def set_restore_operation_to_current(name: str) -> None:
- return api.set_restore_operation_to_current(name)
-
-def restore(name: str, discard: bool = False) -> ChangeSet:
- return api.restore(name, discard)
-
-def read(name: str, sql: str):
- return api.read(name, sql)
-
-def try_read(name: str, sql: str):
- return api.try_read(name, sql)
-
-def read_all(name: str, sql: str):
- return api.read_all(name, sql)
-
-def write(name: str, sql: str):
- return api.write(name, sql)
-
-
-############################################################
-# extension_data
-############################################################
-
-def get_all_extension_data_keys(name: str) -> list[str]:
- return api.get_all_extension_data_keys(name)
-
-def get_all_extension_data(name: str) -> dict[str, Any]:
- return api.get_all_extension_data(name)
-
-def get_extension_data(name: str, key: str) -> str | None:
- return api.get_extension_data(name, key)
-
-def set_extension_data(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_extension_data(name, cs)
-
-
-############################################################
-# type
-############################################################
-
-def is_node(name: str, node_id: str) -> bool:
- return api.is_node(name, node_id)
-
-def is_junction(name: str, node_id: str) -> bool:
- return api.is_junction(name, node_id)
-
-def is_reservoir(name: str, node_id: str) -> bool:
- return api.is_reservoir(name, node_id)
-
-def is_tank(name: str, node_id: str) -> bool:
- return api.is_tank(name, node_id)
-
-def is_link(name: str, link_id: str) -> bool:
- return api.is_link(name, link_id)
-
-def is_pipe(name: str, link_id: str) -> bool:
- return api.is_pipe(name, link_id)
-
-def is_pump(name: str, link_id: str) -> bool:
- return api.is_pump(name, link_id)
-
-def is_valve(name: str, link_id: str) -> bool:
- return api.is_valve(name, link_id)
-
-def is_curve(name: str, curve_id: str) -> bool:
- return api.is_curve(name, curve_id)
-
-def is_pattern(name: str, pattern_id: str) -> bool:
- return api.is_pattern(name, pattern_id)
-
-# DingZQ, 2025-02-05
-def get_node_type(name: str, node_id: str) -> str:
- return api.get_node_type(name, node_id)
-
-def get_link_type(name: str, link_id: str) -> str:
- return api.get_link_type(name, link_id)
-
-def get_element_type(name: str, element_id: str) -> str:
- return api.get_element_type(name, element_id)
-
-def get_element_type_value(name: str, element_id: str) -> int:
- return api.get_element_type_value(name, element_id)
-
-def get_nodes(name: str) -> list[str]:
- return api.get_nodes(name)
-
-def get_links(name: str) -> list[str]:
- return api.get_links(name)
-
-def get_curves(name: str) -> list[str]:
- return api.get_curves(name)
-
-def get_patterns(name: str) -> list[str]:
- return api.get_patterns(name)
-
-def get_node_links(name: str, node_id: str) -> list[str]:
- return api.get_node_links(name, node_id)
-
-# DingZQ, 2025-02-05
def get_node_properties(name: str, node_id: str) -> dict[str, Any]:
- if api.is_junction(name, node_id):
- return api.get_junction(name, node_id)
- elif api.is_reservoir(name, node_id):
- return api.get_reservoir(name, node_id)
- elif api.is_tank(name, node_id):
- return api.get_tank(name, node_id)
+ if is_junction(name, node_id):
+ return get_junction(name, node_id)
+ if is_reservoir(name, node_id):
+ return get_reservoir(name, node_id)
+ if is_tank(name, node_id):
+ return get_tank(name, node_id)
+ return {}
+
def get_link_properties(name: str, link_id: str) -> dict[str, Any]:
- if api.is_pipe(name, link_id):
- return api.get_pipe(name, link_id)
- elif api.is_pump(name, link_id):
- return api.get_pump(name, link_id)
- elif api.is_valve(name, link_id):
- return api.get_valve(name, link_id)
+ if is_pipe(name, link_id):
+ return get_pipe(name, link_id)
+ if is_pump(name, link_id):
+ return get_pump(name, link_id)
+ if is_valve(name, link_id):
+ return get_valve(name, link_id)
+ return {}
-# type can be 'node' or 'link'
-def get_element_properties_with_type(name: str, type: str, element_id: str) -> dict[str, Any]:
- if type == 'node':
+
+def get_element_properties_with_type(
+ name: str, element_type: str, element_id: str
+) -> dict[str, Any]:
+ if element_type == "node":
return get_node_properties(name, element_id)
- elif type == 'link':
+ if element_type == "link":
return get_link_properties(name, element_id)
- elif type == 'scada':
+ if element_type == "scada":
return get_scada_info(name, element_id)
- else:
- return {}
-
-# DingZQ, 2025-02-05
-# element_id can be 'node' 'link' 'scada'
+ return {}
+
+
def get_element_properties(name: str, element_id: str) -> dict[str, Any]:
- if api.is_node(name, element_id):
+ if is_node(name, element_id):
return get_node_properties(name, element_id)
- elif api.is_link(name, element_id):
+ if is_link(name, element_id):
return get_link_properties(name, element_id)
- else:
- # return get_scada_element(name, element_id)
- return get_scada_info(name, element_id)
-
-############################################################
-# title 1.[TITLE]
-############################################################
+ return get_scada_info(name, element_id)
-def get_title_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_title_schema(name)
-def get_title(name: str) -> dict[str, Any]:
- return api.get_title(name)
+def get_network_node_coords(name: str) -> dict[str, dict[str, Any]]:
+ nodes = get_nodes_id_and_type(name)
+ return {
+ node_id: {**get_node_coord(name, node_id), "type": node_type}
+ for node_id, node_type in nodes.items()
+ }
-def set_title(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_title(name, cs)
+def get_major_node_coords(name: str, diameter: int) -> dict[str, dict[str, Any]]:
+ node_types = get_nodes_id_and_type(name)
+ return {
+ node_id: {**get_node_coord(name, node_id), "type": node_types[node_id]}
+ for node_id in get_major_nodes(name, diameter)
+ }
-############################################################
-# junction 2.[JUNCTIONS]
-############################################################
-def get_junction_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_junction_schema(name)
-
-def get_junction(name: str, id: str) -> dict[str, Any]:
- return api.get_junction(name, id)
-
-# DingZQ, 2025-03-29
-def get_all_junctions(name: str) -> list[dict[str, Any]]:
- return api.get_all_junctions(name)
-
-def set_junction(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_junction(name, cs)
-
-# example: add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
-def add_junction(name: str, cs: ChangeSet) -> ChangeSet:
- return api.add_junction(name, cs)
-
-def delete_junction(name: str, cs: ChangeSet) -> ChangeSet:
- return api.delete_junction_cascade(name, cs)
-
-
-############################################################
-# reservoir 3.[RESERVOIRS]
-############################################################
-
-def get_reservoir_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_reservoir_schema(name)
-
-def get_reservoir(name: str, id: str) -> dict[str, Any]:
- return api.get_reservoir(name, id)
-
-# DingZQ, 2025-03-29
-def get_all_reservoirs(name: str) -> list[dict[str, Any]]:
- return api.get_all_reservoirs(name)
-
-def set_reservoir(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_reservoir(name, cs)
-
-# example: add_reservoir(p, ChangeSet({'id': 'r0', 'x': 0.0, 'y': 10.0, 'head': 20.0}))
-def add_reservoir(name: str, cs: ChangeSet) -> ChangeSet:
- return api.add_reservoir(name, cs)
-
-def delete_reservoir(name: str, cs: ChangeSet) -> ChangeSet:
- return api.delete_reservoir_cascade(name, cs)
-
-
-############################################################
-# tank 4.[TANKS]
-############################################################
-
-def get_tank_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_tank_schema(name)
-
-def get_tank(name: str, id: str) -> dict[str, Any]:
- return api.get_tank(name, id)
-
-# DingZQ, 2025-03-29
-def get_all_tanks(name: str) -> list[dict[str, Any]]:
- return api.get_all_tanks(name)
-
-def set_tank(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_tank(name, cs)
-
-# example: add_tank(p, ChangeSet({'id': 't0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0, 'init_level': 1.0, 'min_level': 0.0, 'max_level': 2.0, 'diameter': 10.0, 'min_vol': 100.0}))
-def add_tank(name: str, cs: ChangeSet) -> ChangeSet:
- return api.add_tank(name, cs)
-
-def delete_tank(name: str, cs: ChangeSet) -> ChangeSet:
- return api.delete_tank_cascade(name, cs)
-
-
-############################################################
-# pipe 5.[PIPES]
-############################################################
-
-def get_pipe_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_pipe_schema(name)
-
-def get_pipe(name: str, id: str) -> dict[str, Any]:
- return api.get_pipe(name, id)
-
-# DingZQ, 2025-03-29
-def get_all_pipes(name: str) -> list[dict[str, Any]]:
- return api.get_all_pipes(name)
-
-def set_pipe(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_pipe(name, cs)
-
-# example: add_pipe(p, ChangeSet({'id': 'p0', 'node1': 'j1', 'node2': 'j2', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
-def add_pipe(name: str, cs: ChangeSet) -> ChangeSet:
- return api.add_pipe(name, cs)
-
-def delete_pipe(name: str, cs: ChangeSet) -> ChangeSet:
- return api.delete_pipe_cascade(name, cs)
-
-
-############################################################
-# pump 6.[PUMPS]
-############################################################
-
-def get_pump_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_pump_schema(name)
-
-def get_pump(name: str, id: str) -> dict[str, Any]:
- return api.get_pump(name, id)
-
-# DingZQ, 2025-03-29
-def get_all_pumps(name: str) -> list[dict[str, Any]]:
- return api.get_all_pumps(name)
-
-def set_pump(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_pump(name, cs)
-
-# example: add_pump(p, ChangeSet({'id': 'p0', 'node1': 'j1', 'node2': 'j2', 'power': 0}))
-def add_pump(name: str, cs: ChangeSet) -> ChangeSet:
- return api.add_pump(name, cs)
-
-def delete_pump(name: str, cs: ChangeSet) -> ChangeSet:
- return api.delete_pump_cascade(name, cs)
-
-
-############################################################
-# valve 7.[VALVES]
-############################################################
-
-def get_valve_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_valve_schema(name)
-
-def get_valve(name: str, id: str) -> dict[str, Any]:
- return api.get_valve(name, id)
-
-# DingZQ, 2025-03-29
-def get_all_valves(name: str) -> list[dict[str, Any]]:
- return api.get_all_valves(name)
-
-def set_valve(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_valve(name, cs)
-
-# example: add_valve(p, ChangeSet({'id': 'v0', 'node1': 'j1', 'node2': 'j2', 'diameter': 10.0, 'v_type': VALVES_TYPE_FCV, 'setting': 0.1, 'minor_loss': 0.5 }))
-def add_valve(name: str, cs: ChangeSet) -> ChangeSet:
- return api.add_valve(name, cs)
-
-def delete_valve(name: str, cs: ChangeSet) -> ChangeSet:
- return api.delete_valve_cascade(name, cs)
-
-
-############################################################
-# tag 8.[TAGS]
-############################################################
-
-def get_tag_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_tag_schema(name)
-
-def get_tags(name: str) -> list[dict[str, Any]]:
- return api.get_tags(name)
-
-def get_tag(name: str, t_type: str, id: str) -> dict[str, Any]:
- return api.get_tag(name, t_type, id)
-
-# example:
-# set_tag(p, ChangeSet({'t_type': TAG_TYPE_NODE, 'id': 'j1', 'tag': 'j1t' }))
-# set_tag(p, ChangeSet({'t_type': TAG_TYPE_LINK, 'id': 'p0', 'tag': 'p0t' }))
-def set_tag(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_tag(name, cs)
-
-
-############################################################
-# demand 9.[DEMANDS]
-############################################################
-
-def get_demand_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_demand_schema(name)
-
-def get_demand(name: str, junction: str) -> dict[str, Any]:
- return api.get_demand(name, junction)
-
-# example: set_demand(p, ChangeSet({'junction': 'j1', 'demands': [{'demand': 10.0, 'pattern': None, 'category': 'x'}, {'demand': 20.0, 'pattern': None, 'category': None}]}))
-def set_demand(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_demand(name, cs)
-
-
-############################################################
-# status 10.[STATUS]
-############################################################
-
-def get_status_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_status_schema(name)
-
-def get_status(name: str, link: str) -> dict[str, Any]:
- return api.get_status(name, link)
-
-# example: set_status(p, ChangeSet({'link': 'p0', 'status': LINK_STATUS_OPEN, 'setting': 10.0}))
-def set_status(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_status(name, cs)
-
-
-############################################################
-# pattern 11.[PATTERNS]
-############################################################
-
-def get_pattern_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_pattern_schema(name)
-
-def get_pattern(name: str, id: str) -> dict[str, Any]:
- return api.get_pattern(name, id)
-
-def set_pattern(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_pattern(name, cs)
-
-# example: add_pattern(p, ChangeSet({'id' : 'p0', 'factors': [1.0, 2.0, 3.0]}))
-def add_pattern(name: str, cs: ChangeSet) -> ChangeSet:
- return api.add_pattern(name, cs)
-
-def delete_pattern(name: str, cs: ChangeSet) -> ChangeSet:
- return api.delete_pattern_cascade(name, cs)
-
-
-############################################################
-# curve 12.[CURVES]
-############################################################
-
-def get_curve_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_curve_schema(name)
-
-def get_curve(name: str, id: str) -> dict[str, Any]:
- return api.get_curve(name, id)
-
-def set_curve(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_curve(name, cs)
-
-# example: add_curve(p, ChangeSet({'id' : 'c0', 'c_type' : CURVE_TYPE_PUMP, 'coords': [{'x': 1.0, 'y': 2.0}, {'x': 2.0, 'y': 1.0}]}))
-def add_curve(name: str, cs: ChangeSet) -> ChangeSet:
- return api.add_curve(name, cs)
-
-def delete_curve(name: str, cs: ChangeSet) -> ChangeSet:
- return api.delete_curve_cascade(name, cs)
-
-
-############################################################
-# control 13.[CONTROLS]
-############################################################
-
-def get_control_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_control_schema(name)
-
-def get_control(name: str) -> dict[str, Any]:
- return api.get_control(name)
-
-# example: set_control(p, ChangeSet({'control': 'x'}))
-def set_control(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_control(name, cs)
-
-
-############################################################
-# rule 14.[RULES]
-############################################################
-
-def get_rule_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_rule_schema(name)
-
-def get_rule(name: str) -> dict[str, Any]:
- return api.get_rule(name)
-
-# example: set_rule(p, ChangeSet({'rule': 'x'}))
-def set_rule(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_rule(name, cs)
-
-
-############################################################
-# energy 15.[ENERGY]
-############################################################
-
-def get_energy_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_energy_schema(name)
-
-def get_energy(name: str) -> dict[str, Any]:
- return api.get_energy(name)
-
-def set_energy(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_energy(name, cs)
-
-def get_pump_energy_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_pump_energy_schema(name)
-
-def get_pump_energy(name: str, pump: str) -> dict[str, Any]:
- return api.get_pump_energy(name, pump)
-
-def set_pump_energy(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_pump_energy(name, cs)
-
-
-############################################################
-# emitter 16.[EMITTERS]
-############################################################
-
-def get_emitter_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_emitter_schema(name)
-
-def get_emitter(name: str, junction: str) -> dict[str, Any]:
- return api.get_emitter(name, junction)
-
-# example: set_emitter(p, ChangeSet({'junction': 'j1', 'coefficient': 10.0}))
-def set_emitter(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_emitter(name, cs)
-
-
-############################################################
-# quality 17.[QUALITY]
-############################################################
-
-def get_quality_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_quality_schema(name)
-
-def get_quality(name: str, node: str) -> dict[str, Any]:
- return api.get_quality(name, node)
-
-# example: set_quality(p, ChangeSet({'node': 'j1', 'quality': 10.0}))
-def set_quality(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_quality(name, cs)
-
-
-############################################################
-# source 18.[SOURCES]
-############################################################
-
-def get_source_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_source_schema(name)
-
-def get_source(name: str, node: str) -> dict[str, Any]:
- return api.get_source(name, node)
-
-def set_source(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_source(name, cs)
-
-# example: add_source(p, ChangeSet({'node': 'j0', 's_type': SOURCE_TYPE_CONCEN, 'strength': 10.0, 'pattern': 'p0'}))
-def add_source(name: str, cs: ChangeSet) -> ChangeSet:
- return api.add_source(name, cs)
-
-def delete_source(name: str, cs: ChangeSet) -> ChangeSet:
- return api.delete_source(name, cs)
-
-
-############################################################
-# reaction 19.[REACTIONS]
-############################################################
-
-def get_reaction_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_reaction_schema(name)
-
-def get_reaction(name: str) -> dict[str, Any]:
- return api.get_reaction(name)
-
-def set_reaction(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_reaction(name, cs)
-
-def get_pipe_reaction_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_pipe_reaction_schema(name)
-
-def get_pipe_reaction(name: str, pipe: str) -> dict[str, Any]:
- return api.get_pipe_reaction(name, pipe)
-
-def set_pipe_reaction(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_pipe_reaction(name, cs)
-
-def get_tank_reaction_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_tank_reaction_schema(name)
-
-def get_tank_reaction(name: str, tank: str) -> dict[str, Any]:
- return api.get_tank_reaction(name, tank)
-
-def set_tank_reaction(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_tank_reaction(name, cs)
-
-
-############################################################
-# mixing 20.[MIXING]
-############################################################
-
-def get_mixing_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_mixing_schema(name)
-
-def get_mixing(name: str, tank: str) -> dict[str, Any]:
- return api.get_mixing(name, tank)
-
-def set_mixing(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_mixing(name, cs)
-
-# example: add_mixing(p, ChangeSet({'tank': 't0', 'model': MIXING_MODEL_MIXED, 'value': 10.0}))
-def add_mixing(name: str, cs: ChangeSet) -> ChangeSet:
- return api.add_mixing(name, cs)
-
-def delete_mixing(name: str, cs: ChangeSet) -> ChangeSet:
- return api.delete_mixing(name, cs)
-
-
-############################################################
-# time 21.[TIMES]
-############################################################
-
-def get_time_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_time_schema(name)
-
-def get_time(name: str) -> dict[str, Any]:
- return api.get_time(name)
-
-def set_time(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_time(name, cs)
-
-
-############################################################
-# report 22.[REPORT]
-############################################################
-
-# hardcode...
-
-
-############################################################
-# option 23.[OPTIONS]
-############################################################
-
-def get_option_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_option_schema(name)
-
-def get_option(name: str) -> dict[str, Any]:
- return api.get_option(name)
-
-def set_option(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_option_ex(name, cs)
-
-
-############################################################
-# option_v3 23.[EPA3][OPTIONS]
-############################################################
-
-def get_option_v3_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_option_v3_schema(name)
-
-def get_option_v3(name: str) -> dict[str, Any]:
- return api.get_option_v3(name)
-
-def set_option_v3(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_option_v3_ex(name, cs)
-
-
-############################################################
-# coord 24.[COORDINATES]
-############################################################
-
-def get_node_coord(name: str, node_id: str) -> dict[str, float]:
- return api.get_node_coord(name, node_id)
-
-# DingZQ, 2024-12-08, get all node coord
-# id, x, y, type
-def get_network_node_coords(name: str) -> dict[str, dict[str, float]]:
- nodes_id_and_type = api.get_nodes_id_and_type(name)
- result = {}
- for node_id, node_type in nodes_id_and_type.items():
- coord = api.get_node_coord(name, node_id)
- coord['type'] = node_type
- result[node_id] = coord
- return result
-
-# DingZQ 2024-12-31
-# id, x, y, type
-def get_major_node_coords(name: str, diameter: int) -> dict[str, dict[str, float]]:
- nodes_id_and_type = api.get_nodes_id_and_type(name)
- major_node_ids = api.get_major_nodes(name, diameter)
- result = {}
- for node_id in major_node_ids:
- coord = api.get_node_coord(name, node_id)
- coord['type'] = nodes_id_and_type[node_id]
- result[node_id] = coord
- return result
-
-def get_network_in_extent(name: str, x1: float, y1: float, x2: float, y2: float) -> dict[str, Any]:
- pass
-
-
-
-# DingZQ, 2024-12-08, get all links' start and end node
-# link_id:link_type:node_id1:node_id2
def get_network_link_nodes(name: str) -> list[str]:
- links_id_and_type = api.get_links_id_and_type(name)
- result = []
- for link_id, link_type in links_id_and_type.items():
- nodes = api.get_link_nodes(name, link_id)
- result.append(f"{link_id}:{link_type}:{nodes[0]}:{nodes[1]}")
- return result
-
-# DingZQ 2024-12-31
-# link_id:pipe:node_id1:node_id2
-def get_major_pipe_nodes(name: str, diameter: int) -> list[str]:
- major_pipe_ids = api.get_major_pipes(name, diameter)
- result = []
- for link_id in major_pipe_ids:
- nodes = api.get_link_nodes(name, link_id)
- result.append(f"{link_id}:pipe:{nodes[0]}:{nodes[1]}")
- return result
-
-############################################################
-# vertex 25.[VERTICES]
-############################################################
-
-def get_vertex_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_vertex_schema(name)
-
-def get_vertex(name: str, link: str) -> dict[str, Any]:
- return api.get_vertex(name, link)
-
-def set_vertex(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_vertex(name, cs)
-
-def add_vertex(name: str, cs: ChangeSet) -> ChangeSet:
- return api.add_vertex(name, cs)
-
-def delete_vertex(name: str, cs: ChangeSet) -> ChangeSet:
- return api.delete_vertex(name, cs)
-
-def get_all_vertex_links(name: str) -> list[str]:
- return api.get_all_vertex_links(name)
-
-def get_all_vertices(name: str) -> list[dict[str, Any]]:
- return api.get_all_vertices(name)
-
-
-############################################################
-# label 26.[LABELS]
-############################################################
-
-def get_label_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_label_schema(name)
-
-def get_label(name: str, x: float, y: float) -> dict[str, Any]:
- return api.get_label(name, x, y)
-
-def set_label(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_label(name, cs)
-
-def add_label(name: str, cs: ChangeSet) -> ChangeSet:
- return api.add_label(name, cs)
-
-def delete_label(name: str, cs: ChangeSet) -> ChangeSet:
- return api.delete_label(name, cs)
-
-
-############################################################
-# backdrop 27.[BACKDROP]
-############################################################
-
-def get_backdrop_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_backdrop_schema(name)
-
-def get_backdrop(name: str) -> dict[str, Any]:
- return api.get_backdrop(name)
-
-def set_backdrop(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_backdrop(name, cs)
-
-
-############################################################
-# end 28.[END]
-############################################################
-
-
-############################################################
-# scada_device 29
-############################################################
-
-def get_scada_device_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_scada_device_schema(name)
-
-def get_scada_device(name: str, id: str) -> dict[str, Any]:
- return api.get_scada_device(name, id)
-
-def set_scada_device(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_scada_device(name, cs)
-
-def add_scada_device(name: str, cs: ChangeSet) -> ChangeSet:
- return api.add_scada_device(name, cs)
-
-def delete_scada_device(name: str, cs: ChangeSet) -> ChangeSet:
- return api.delete_scada_device(name, cs)
-
-def clean_scada_device(name: str) -> ChangeSet:
- return api.clean_scada_device(name)
-
-def get_all_scada_device_ids(name: str) -> list[str]:
- return api.get_all_scada_device_ids(name)
-
-def get_all_scada_devices(name: str) -> list[dict[str, Any]]:
- return api.get_all_scada_devices(name)
-
-
-############################################################
-# scada_device_data 30
-############################################################
-
-def get_scada_device_data_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_scada_device_data_schema(name)
-
-def get_scada_device_data(name: str, device_id: str) -> dict[str, Any]:
- return api.get_scada_device_data(name, device_id)
-
-# example: set_scada_device_data(p, ChangeSet({'device_id': 'sm_device', 'data': [{ 'time': '2023-02-10 00:02:22', 'value': 100.0 }, { 'time': '2023-02-10 00:03:22', 'value': 200.0 }]}))
-# time format must be 'YYYY-MM-DD HH:MM:SS'
-def set_scada_device_data(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_scada_device_data(name, cs)
-
-# example: add_scada_device_data(p, ChangeSet({'device_id': 'sm_device', 'time': '2023-02-10 00:02:22', 'value': 100.0}))
-def add_scada_device_data(name: str, cs: ChangeSet) -> ChangeSet:
- return api.add_scada_device_data(name, cs)
-
-# example: delete_scada_device_data(p, ChangeSet({'device_id': 'sm_device', 'time': '2023-02-12 00:02:22'}))
-def delete_scada_device_data(name: str, cs: ChangeSet) -> ChangeSet:
- return api.delete_scada_device_data(name, cs)
-
-def clean_scada_device_data(name: str) -> ChangeSet:
- return api.clean_scada_device_data(name)
-
-
-############################################################
-# scada_element 31
-############################################################
-
-def get_scada_element_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_scada_element_schema(name)
-
-def get_scada_element(name: str, id: str) -> dict[str, Any]:
- return api.get_scada_element(name, id)
-
-def set_scada_element(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_scada_element(name, cs)
-
-def add_scada_element(name: str, cs: ChangeSet) -> ChangeSet:
- return api.add_scada_element(name, cs)
-
-def delete_scada_element(name: str, cs: ChangeSet) -> ChangeSet:
- return api.delete_scada_element(name, cs)
-
-def clean_scada_element(name: str) -> ChangeSet:
- return api.clean_scada_element(name)
-
-def get_all_scada_element_ids(name: str) -> list[str]:
- return api.get_all_scada_element_ids(name)
-
-def get_all_scada_elements(name: str) -> list[dict[str, Any]]:
- return api.get_all_scada_elements(name)
-
-
-############################################################
-# general_region 32
-############################################################
-
-def get_nodes_in_boundary(name: str, boundary: list[tuple[float, float]]) -> list[str]:
- return api.get_nodes_in_boundary(name, boundary)
-
-def get_nodes_in_region(name: str, region_id: str) -> list[str]:
- return api.get_nodes_in_region(name, region_id)
-
-def get_links_on_region_boundary(name: str, region_id: str) -> list[str]:
- return api.get_links_on_region_boundary(name, region_id)
-
-def calculate_convex_hull(name: str, nodes: list[str]) -> list[tuple[float, float]]:
- return api.calculate_convex_hull(name, nodes)
-
-def calculate_boundary(name: str, nodes: list[str]) -> list[tuple[float, float]]:
- return api.calculate_boundary(name, nodes)
-
-def inflate_boundary(name: str, boundary: list[tuple[float, float]], delta: float = 0.5) -> list[tuple[float, float]]:
- return api.inflate_boundary(name, boundary, delta)
-
-def inflate_region(name: str, region_id: str, delta: float = 0.5) -> list[tuple[float, float]]:
- return api.inflate_region(name, region_id, delta)
-
-def get_region_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_region_schema(name)
-
-def get_region(name: str, id: str) -> dict[str, Any]:
- return api.get_region(name, id)
-
-def set_region(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_region(name, cs)
-
-# example: add_region(p, ChangeSet({'id': 'r', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]}))
-def add_region(name: str, cs: ChangeSet) -> ChangeSet:
- return api.add_region(name, cs)
-
-def delete_region(name: str, cs: ChangeSet) -> ChangeSet:
- return api.delete_region(name, cs)
-
-
-############################################################
-# district_metering_area 33
-############################################################
-
-def calculate_district_metering_area_for_nodes(name: str, nodes: list[str], part_count: int = 1, part_type: int = PARTITION_TYPE_RB) -> list[list[str]]:
- return api.calculate_district_metering_area_for_nodes(name, nodes, part_count, part_type)
-
-def calculate_district_metering_area_for_region(name: str, region: str, part_count: int = 1, part_type: int = PARTITION_TYPE_RB) -> list[list[str]]:
- return api.calculate_district_metering_area_for_region(name, region, part_count, part_type)
-
-def calculate_district_metering_area_for_network(name: str, part_count: int = 1, part_type: int = PARTITION_TYPE_RB) -> list[list[str]]:
- return api.calculate_district_metering_area_for_network(name, part_count, part_type)
-
-def get_district_metering_area_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_district_metering_area_schema(name)
-
-def get_district_metering_area(name: str, id: str) -> dict[str, Any]:
- return api.get_district_metering_area(name, id)
-
-def set_district_metering_area(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_district_metering_area(name, cs)
-
-def add_district_metering_area(name: str, cs: ChangeSet) -> ChangeSet:
- return api.add_district_metering_area(name, cs)
-
-def delete_district_metering_area(name: str, cs: ChangeSet) -> ChangeSet:
- return api.delete_district_metering_area(name, cs)
-
-def get_all_district_metering_area_ids(name: str) -> list[str]:
- return api.get_all_district_metering_area_ids(name)
-
-def get_all_district_metering_areas(name: str) -> list[dict[str, Any]]:
- return api.get_all_district_metering_areas(name)
-
-def generate_district_metering_area(name: str, part_count: int = 1, part_type: int = PARTITION_TYPE_RB, inflate_delta: float = 0.5) -> ChangeSet:
- return api.generate_district_metering_area(name, part_count, part_type, inflate_delta)
-
-def generate_sub_district_metering_area(name: str, dma: str, part_count: int = 1, part_type: int = PARTITION_TYPE_RB, inflate_delta: float = 0.5) -> ChangeSet:
- return api.generate_sub_district_metering_area(name, dma, part_count, part_type, inflate_delta)
-
-
-############################################################
-# service_area 34
-############################################################
-
-def calculate_service_area(name: str) -> list[dict[str, list[str]]]:
- return api.calculate_service_area(name)
-
-def get_service_area_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_service_area_schema(name)
-
-def get_service_area(name: str, id: str) -> dict[str, Any]:
- return api.get_service_area(name, id)
-
-def set_service_area(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_service_area(name, cs)
-
-def add_service_area(name: str, cs: ChangeSet) -> ChangeSet:
- return api.add_service_area(name, cs)
-
-def delete_service_area(name: str, cs: ChangeSet) -> ChangeSet:
- return api.delete_service_area(name, cs)
-
-def get_all_service_area_ids(name: str) -> list[str]:
- return api.get_all_service_area_ids(name)
-
-def get_all_service_areas(name: str) -> list[dict[str, Any]]:
- return api.get_all_service_areas(name)
-
-def generate_service_area(name: str, inflate_delta: float = 0.5) -> ChangeSet:
- return api.generate_service_area(name, inflate_delta)
-
-
-############################################################
-# virtual_district 35
-############################################################
-
-def calculate_virtual_district(name: str, centers: list[str]) -> dict[str, list[Any]]:
- return api.calculate_virtual_district(name, centers)
-
-def get_virtual_district_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_virtual_district_schema(name)
-
-def get_virtual_district(name: str, id: str) -> dict[str, Any]:
- return api.get_virtual_district(name, id)
-
-def set_virtual_district(name: str, cs: ChangeSet) -> ChangeSet:
- return api.set_virtual_district(name, cs)
-
-def add_virtual_district(name: str, cs: ChangeSet) -> ChangeSet:
- return api.add_virtual_district(name, cs)
-
-def delete_virtual_district(name: str, cs: ChangeSet) -> ChangeSet:
- return api.delete_virtual_district(name, cs)
-
-def get_all_virtual_district_ids(name: str) -> list[str]:
- return api.get_all_virtual_district_ids(name)
-
-def get_all_virtual_districts(name: str) -> list[dict[str, Any]]:
- return api.get_all_virtual_districts(name)
-
-def generate_virtual_district(name: str, centers: list[str], inflate_delta: float = 0.5) -> ChangeSet:
- return api.generate_virtual_district(name, centers, inflate_delta)
-
-
-############################################################
-# water_distribution_area 36
-############################################################
-
-def calculate_demand_to_nodes(name: str, demand: float, nodes: list[str]) -> dict[str, float]:
- return api.calculate_demand_to_nodes(name, demand, nodes)
-
-# if region is general or wda => get_nodes_in_boundary
-# if region is dma, sa or vd => get stored nodes in table
-# TODO: more test
-def calculate_demand_to_region(name: str, demand: float, region: str) -> dict[str, float]:
- return api.calculate_demand_to_region(name, demand, region)
-
-def calculate_demand_to_network(name: str, demand: float) -> dict[str, float]:
- return api.calculate_demand_to_network(name, demand)
-
-
-############################################################
-# scada_info 38 | WMH
-############################################################
-
-def get_scada_info_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_scada_info_schema(name)
-
-def get_scada_info(name: str, id: str) -> dict[str, Any]:
- return api.get_scada_info(name, id)
-
-
-def get_all_scada_info(name: str) -> list[dict[str, Any]]:
- return api.get_all_scada_info(name)
-
-############################################################
-# scheme 40
-############################################################
-def get_scheme_schema(name: str) -> dict[str, dict[str, Any]]:
- return api.get_scheme_schema(name)
-
-def get_scheme(name: str, schema_name: str) -> dict[str, Any]:
- return api.get_scheme(name, schema_name)
-
-def get_all_schemes(
- name: str,
- scheme_type: str | None = None,
- query_date: Any | None = None,
-) -> list[dict[str, Any]]:
- if scheme_type is None and query_date is None:
- return api.get_all_schemes(name)
-
- from app.services.scheme_management import query_scheme_list
-
- rows = query_scheme_list(name, scheme_type=scheme_type, query_date=query_date) or []
- columns = [
- "scheme_id",
- "scheme_name",
- "scheme_type",
- "username",
- "create_time",
- "scheme_start_time",
- "scheme_detail",
+ links = get_links_id_and_type(name)
+ return [
+ f"{link_id}:{link_type}:{nodes[0]}:{nodes[1]}"
+ for link_id, link_type in links.items()
+ if (nodes := get_link_nodes(name, link_id))
]
- result = []
- for row in rows:
- item = dict(zip(columns, row, strict=False))
- detail = item.get("scheme_detail")
- if isinstance(detail, dict) and detail.get("network") not in (None, name):
- continue
- result.append(item)
- return result
-############################################################
-# pipe_risk_probability 41
-############################################################
-def get_pipe_risk_probability_now(name: str, pipe_id: str) -> dict[str, Any]:
- return api.get_pipe_risk_probability_now(name, pipe_id)
-def get_pipe_risk_probability(name: str, pipe_id: str) -> dict[str, Any]:
- return api.get_pipe_risk_probability(name, pipe_id)
+def get_major_pipe_nodes(name: str, diameter: int) -> list[str]:
+ return [
+ f"{link_id}:pipe:{nodes[0]}:{nodes[1]}"
+ for link_id in get_major_pipes(name, diameter)
+ if (nodes := get_link_nodes(name, link_id))
+ ]
-def get_pipes_risk_probability(name: str, pipe_ids: list[str]) -> list[dict[str, Any]]:
- return api.get_pipes_risk_probability(name, pipe_ids)
-def get_network_pipe_risk_probability_now(name: str) -> list[dict[str, Any]]:
- return api.get_network_pipe_risk_probability_now(name)
-
-def get_pipe_risk_probability_geometries(name: str) -> dict[str, Any]:
- return api.get_pipe_risk_probability_geometries(name)
-
-############################################################
-# sensor_placement 42
-############################################################
-def get_all_sensor_placements(name: str) -> list[dict[Any, Any]]:
- return api.get_all_sensor_placements(name)
-
-############################################################
-# burst_locate_result 43
-############################################################
-def get_all_burst_locate_results(name: str) -> list[dict[Any, Any]]:
- return api.get_all_burst_locate_results(name)
+def get_network_in_extent(
+ name: str, x1: float, y1: float, x2: float, y2: float
+) -> dict[str, Any]:
+ # Kept as the existing API placeholder; spatial filtering belongs in GIS.
+ return {}
diff --git a/contracts/manifest.json b/contracts/manifest.json
index 70f691e..920c9a4 100644
--- a/contracts/manifest.json
+++ b/contracts/manifest.json
@@ -3,7 +3,7 @@
"contracts": {
"server": {
"file": "server-v1.openapi.json",
- "sha256": "ac9b6fac185dfd999f1791cba51eb482df17a427b361963250aafa5fb1a276b4"
+ "sha256": "404a196c0177faed2aa5b46ee86430a034dfe990e0a77a43428a727748a882b6"
}
}
}
diff --git a/contracts/server-v1.openapi.json b/contracts/server-v1.openapi.json
index 5c176bc..a53537d 100644
--- a/contracts/server-v1.openapi.json
+++ b/contracts/server-v1.openapi.json
@@ -532,32 +532,6 @@
"title": "Body_post_timeseries_realtime_simulation_results",
"type": "object"
},
- "Body_post_timeseries_schemes_simulation_results": {
- "properties": {
- "link_result_list": {
- "description": "管道模拟结果列表",
- "items": {
- "type": "object"
- },
- "title": "Link Result List",
- "type": "array"
- },
- "node_result_list": {
- "description": "节点模拟结果列表",
- "items": {
- "type": "object"
- },
- "title": "Node Result List",
- "type": "array"
- }
- },
- "required": [
- "node_result_list",
- "link_result_list"
- ],
- "title": "Body_post_timeseries_schemes_simulation_results",
- "type": "object"
- },
"BurstDetectionRequestRest": {
"properties": {
"data_source": {
@@ -686,29 +660,18 @@
"description": "传感器节点列表",
"title": "Sensor Nodes"
},
- "simulation_scheme_name": {
+ "simulation_run_id": {
"anyOf": [
{
+ "format": "uuid",
"type": "string"
},
{
"type": "null"
}
],
- "description": "模拟方案名称",
- "title": "Simulation Scheme Name"
- },
- "simulation_scheme_type": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "模拟方案类型",
- "title": "Simulation Scheme Type"
+ "description": "分析模拟运行 ID",
+ "title": "Simulation Run Id"
},
"target_time": {
"anyOf": [
@@ -931,32 +894,21 @@
"type": "null"
}
],
- "description": "方案名称",
+ "description": "爆管定位运行名称",
"title": "Scheme Name"
},
- "simulation_scheme_name": {
+ "simulation_run_id": {
"anyOf": [
{
+ "format": "uuid",
"type": "string"
},
{
"type": "null"
}
],
- "description": "模拟方案名称",
- "title": "Simulation Scheme Name"
- },
- "simulation_scheme_type": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "模拟方案类型",
- "title": "Simulation Scheme Type"
+ "description": "分析模拟运行 ID",
+ "title": "Simulation Run Id"
},
"use_scada_flow": {
"default": false,
@@ -1597,11 +1549,11 @@
"title": "Page[ProjectSummaryResponse]",
"type": "object"
},
- "Page_dict_Any__Any__": {
+ "Page_SensorPlacementSchemeResponse_": {
"properties": {
"items": {
"items": {
- "type": "object"
+ "$ref": "#/components/schemas/SensorPlacementSchemeResponse"
},
"title": "Items",
"type": "array"
@@ -1625,7 +1577,7 @@
"limit",
"offset"
],
- "title": "Page[dict[Any, Any]]",
+ "title": "Page[SensorPlacementSchemeResponse]",
"type": "object"
},
"Page_dict_str__Any__": {
@@ -1659,77 +1611,6 @@
"title": "Page[dict[str, Any]]",
"type": "object"
},
- "Page_dict_str__list_str___": {
- "properties": {
- "items": {
- "items": {
- "additionalProperties": {
- "items": {
- "type": "string"
- },
- "type": "array"
- },
- "type": "object"
- },
- "title": "Items",
- "type": "array"
- },
- "limit": {
- "title": "Limit",
- "type": "integer"
- },
- "offset": {
- "title": "Offset",
- "type": "integer"
- },
- "total": {
- "title": "Total",
- "type": "integer"
- }
- },
- "required": [
- "items",
- "total",
- "limit",
- "offset"
- ],
- "title": "Page[dict[str, list[str]]]",
- "type": "object"
- },
- "Page_list_str__": {
- "properties": {
- "items": {
- "items": {
- "items": {
- "type": "string"
- },
- "type": "array"
- },
- "title": "Items",
- "type": "array"
- },
- "limit": {
- "title": "Limit",
- "type": "integer"
- },
- "offset": {
- "title": "Offset",
- "type": "integer"
- },
- "total": {
- "title": "Total",
- "type": "integer"
- }
- },
- "required": [
- "items",
- "total",
- "limit",
- "offset"
- ],
- "title": "Page[list[str]]",
- "type": "object"
- },
"Page_str_": {
"properties": {
"items": {
@@ -1761,47 +1642,6 @@
"title": "Page[str]",
"type": "object"
},
- "Page_tuple_int__str__": {
- "properties": {
- "items": {
- "items": {
- "maxItems": 2,
- "minItems": 2,
- "prefixItems": [
- {
- "type": "integer"
- },
- {
- "type": "string"
- }
- ],
- "type": "array"
- },
- "title": "Items",
- "type": "array"
- },
- "limit": {
- "title": "Limit",
- "type": "integer"
- },
- "offset": {
- "title": "Offset",
- "type": "integer"
- },
- "total": {
- "title": "Total",
- "type": "integer"
- }
- },
- "required": [
- "items",
- "total",
- "limit",
- "offset"
- ],
- "title": "Page[tuple[int, str]]",
- "type": "object"
- },
"PressureRegulationRest": {
"properties": {
"duration": {
@@ -1859,32 +1699,6 @@
"title": "PressureRegulationRest",
"type": "object"
},
- "PressureSensorPlacementRest": {
- "properties": {
- "min_diameter": {
- "default": 0,
- "description": "最小管径限制",
- "title": "Min Diameter",
- "type": "integer"
- },
- "scheme_name": {
- "description": "方案名称",
- "title": "Scheme Name",
- "type": "string"
- },
- "sensor_number": {
- "description": "传感器数量",
- "title": "Sensor Number",
- "type": "integer"
- }
- },
- "required": [
- "scheme_name",
- "sensor_number"
- ],
- "title": "PressureSensorPlacementRest",
- "type": "object"
- },
"ProblemDetails": {
"description": "RFC 9457 compatible error response used by the REST contract.",
"properties": {
@@ -2417,18 +2231,18 @@
"title": "Adjustment Status",
"type": "object"
},
- "sensor_location": {
+ "sensor_locations": {
"items": {
"type": "string"
},
"maxItems": 200,
"minItems": 1,
- "title": "Sensor Location",
+ "title": "Sensor Locations",
"type": "array"
}
},
"required": [
- "sensor_location"
+ "sensor_locations"
],
"title": "SensorPlacementExportRequest",
"type": "object"
@@ -2449,10 +2263,10 @@
"title": "Min Diameter",
"type": "integer"
},
- "scheme_name": {
- "maxLength": 32,
+ "run_name": {
+ "maxLength": 64,
"minLength": 1,
- "title": "Scheme Name",
+ "title": "Run Name",
"type": "string"
},
"sensor_count": {
@@ -2468,7 +2282,7 @@
}
},
"required": [
- "scheme_name",
+ "run_name",
"sensor_type",
"method",
"sensor_count"
@@ -2483,34 +2297,39 @@
"title": "Can Edit",
"type": "boolean"
},
- "create_time": {
+ "created_at": {
"format": "date-time",
- "title": "Create Time",
+ "title": "Created At",
"type": "string"
},
- "id": {
- "title": "Id",
- "type": "integer"
+ "created_by": {
+ "title": "Created By",
+ "type": "string"
},
"min_diameter": {
"title": "Min Diameter",
"type": "integer"
},
- "scheme_name": {
- "title": "Scheme Name",
+ "name": {
+ "title": "Name",
"type": "string"
},
- "sensor_location": {
+ "run_id": {
+ "format": "uuid",
+ "title": "Run Id",
+ "type": "string"
+ },
+ "sensor_count": {
+ "title": "Sensor Count",
+ "type": "integer"
+ },
+ "sensor_locations": {
"items": {
"type": "string"
},
- "title": "Sensor Location",
+ "title": "Sensor Locations",
"type": "array"
},
- "sensor_number": {
- "title": "Sensor Number",
- "type": "integer"
- },
"sensor_points": {
"items": {
"$ref": "#/components/schemas/SensorPointResponse"
@@ -2518,19 +2337,20 @@
"title": "Sensor Points",
"type": "array"
},
- "username": {
- "title": "Username",
+ "status": {
+ "title": "Status",
"type": "string"
}
},
"required": [
- "id",
- "scheme_name",
- "sensor_number",
+ "run_id",
+ "name",
+ "sensor_count",
"min_diameter",
- "username",
- "create_time",
- "sensor_location",
+ "created_by",
+ "created_at",
+ "status",
+ "sensor_locations",
"sensor_points"
],
"title": "SensorPlacementSchemeResponse",
@@ -2538,28 +2358,28 @@
},
"SensorPlacementUpdateRequest": {
"properties": {
- "expected_sensor_location": {
+ "expected_sensor_locations": {
"items": {
"type": "string"
},
"maxItems": 200,
"minItems": 1,
- "title": "Expected Sensor Location",
+ "title": "Expected Sensor Locations",
"type": "array"
},
- "sensor_location": {
+ "sensor_locations": {
"items": {
"type": "string"
},
"maxItems": 200,
"minItems": 1,
- "title": "Sensor Location",
+ "title": "Sensor Locations",
"type": "array"
}
},
"required": [
- "expected_sensor_location",
- "sensor_location"
+ "expected_sensor_locations",
+ "sensor_locations"
],
"title": "SensorPlacementUpdateRequest",
"type": "object"
@@ -5007,226 +4827,6 @@
]
}
},
- "/api/v1/all-extension-data-keys": {
- "get": {
- "description": "获取指定网络的所有扩展数据的键列表",
- "operationId": "get_all_extension_data_keys",
- "parameters": [
- {
- "in": "query",
- "name": "limit",
- "required": false,
- "schema": {
- "default": 100,
- "maximum": 1000,
- "minimum": 1,
- "title": "Limit",
- "type": "integer"
- }
- },
- {
- "in": "query",
- "name": "offset",
- "required": false,
- "schema": {
- "default": 0,
- "minimum": 0,
- "title": "Offset",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Page_str_"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取所有扩展数据键",
- "tags": [
- "Extension"
- ]
- }
- },
- "/api/v1/all-extension-datas": {
- "get": {
- "description": "获取指定网络的所有扩展数据",
- "operationId": "get_all_extension_datas",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "title": "Response Get All Extension Datas",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取所有扩展数据",
- "tags": [
- "Extension"
- ]
- }
- },
"/api/v1/all-scada-properties": {
"get": {
"description": "获取指定水网中所有SCADA点的属性信息",
@@ -5469,6 +5069,338 @@
]
}
},
+ "/api/v1/analysis/runs": {
+ "get": {
+ "description": "获取所有方案信息\n\n返回项目中所有方案的详细信息",
+ "operationId": "get_analysis_runs",
+ "parameters": [
+ {
+ "in": "header",
+ "name": "X-Project-Id",
+ "required": true,
+ "schema": {
+ "title": "X-Project-Id",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/JsonValue"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Authentication required"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Insufficient permission"
+ },
+ "404": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource not found"
+ },
+ "409": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource conflict"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Validation error"
+ },
+ "503": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Dependency unavailable"
+ }
+ },
+ "security": [
+ {
+ "OAuth2PasswordBearer": []
+ }
+ ],
+ "summary": "获取分析运行列表",
+ "tags": [
+ "Project Data"
+ ]
+ }
+ },
+ "/api/v1/analysis/runs/{run_id}": {
+ "get": {
+ "description": "获取所有爆管定位结果\n\n返回项目中所有的爆管定位分析结果",
+ "operationId": "get_analysis_runs_run_id",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "run_id",
+ "required": true,
+ "schema": {
+ "format": "uuid",
+ "title": "Run Id",
+ "type": "string"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Project-Id",
+ "required": true,
+ "schema": {
+ "title": "X-Project-Id",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/JsonValue"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Authentication required"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Insufficient permission"
+ },
+ "404": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource not found"
+ },
+ "409": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource conflict"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Validation error"
+ },
+ "503": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Dependency unavailable"
+ }
+ },
+ "security": [
+ {
+ "OAuth2PasswordBearer": []
+ }
+ ],
+ "summary": "获取分析运行",
+ "tags": [
+ "Project Data"
+ ]
+ }
+ },
+ "/api/v1/analysis/runs/{run_id}/results": {
+ "get": {
+ "description": "根据爆管事件ID查询爆管定位结果\n\n参数:\n burst_incident: 爆管事件的唯一标识符",
+ "operationId": "get_analysis_runs_run_id_results",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "run_id",
+ "required": true,
+ "schema": {
+ "format": "uuid",
+ "title": "Run Id",
+ "type": "string"
+ }
+ },
+ {
+ "description": "结果类型",
+ "in": "query",
+ "name": "result_type",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "结果类型",
+ "title": "Result Type"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Project-Id",
+ "required": true,
+ "schema": {
+ "title": "X-Project-Id",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/JsonValue"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Authentication required"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Insufficient permission"
+ },
+ "404": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource not found"
+ },
+ "409": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource conflict"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Validation error"
+ },
+ "503": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Dependency unavailable"
+ }
+ },
+ "security": [
+ {
+ "OAuth2PasswordBearer": []
+ }
+ ],
+ "summary": "获取分析结果",
+ "tags": [
+ "Project Data"
+ ]
+ }
+ },
"/api/v1/audit-events": {
"post": {
"operationId": "post_audit_events",
@@ -6626,125 +6558,6 @@
}
},
"/api/v1/burst-locations": {
- "get": {
- "description": "获取网络中所有爆管定位的分析结果",
- "operationId": "get_burst_locations",
- "parameters": [
- {
- "in": "query",
- "name": "limit",
- "required": false,
- "schema": {
- "default": 100,
- "maximum": 1000,
- "minimum": 1,
- "title": "Limit",
- "type": "integer"
- }
- },
- {
- "in": "query",
- "name": "offset",
- "required": false,
- "schema": {
- "default": 0,
- "minimum": 0,
- "title": "Offset",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Page_dict_Any__Any__"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取所有爆管定位结果",
- "tags": [
- "Misc"
- ]
- },
"post": {
"description": "基于压力和流量数据定位管网中的爆管位置",
"operationId": "post_burst_locations",
@@ -6854,213 +6667,6 @@
]
}
},
- "/api/v1/burst-locations/database-view": {
- "get": {
- "description": "使用连接池查询所有爆管定位结果",
- "operationId": "get_burst_locations_database_view",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取爆管定位结果",
- "tags": [
- "Project Data"
- ]
- }
- },
- "/api/v1/burst-locations/{burst_incident}": {
- "get": {
- "description": "根据爆管事件ID查询对应的爆管定位结果",
- "operationId": "get_burst_locations_burst_incident",
- "parameters": [
- {
- "description": "爆管事件ID",
- "in": "path",
- "name": "burst_incident",
- "required": true,
- "schema": {
- "description": "爆管事件ID",
- "title": "Burst Incident",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "按事件查询爆管定位结果",
- "tags": [
- "Project Data"
- ]
- }
- },
"/api/v1/contaminant-simulations": {
"post": {
"description": "对管网中的污染物扩散进行模拟,评估污染源对管网的影响范围和浓度分布。支持指定污染源位置、污染浓度和扩散模式。",
@@ -7427,105 +7033,6 @@
]
}
},
- "/api/v1/current-operation-ids": {
- "get": {
- "description": "获取网络当前的操作ID",
- "operationId": "get_current_operation_ids",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "title": "Response Get Current Operation Ids",
- "type": "integer"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取当前操作ID",
- "tags": [
- "Snapshots"
- ]
- }
- },
"/api/v1/curves": {
"delete": {
"description": "从网络中删除指定的曲线",
@@ -8826,1134 +8333,6 @@
]
}
},
- "/api/v1/district-metering-area-generation-runs": {
- "post": {
- "description": "根据参数自动生成水网的DMA分区方案",
- "operationId": "post_district_metering_area_generation_runs",
- "parameters": [
- {
- "description": "分区数量",
- "in": "query",
- "name": "part_count",
- "required": true,
- "schema": {
- "description": "分区数量",
- "exclusiveMinimum": 0,
- "title": "Part Count",
- "type": "integer"
- }
- },
- {
- "description": "分区类型",
- "in": "query",
- "name": "part_type",
- "required": true,
- "schema": {
- "description": "分区类型",
- "title": "Part Type",
- "type": "integer"
- }
- },
- {
- "description": "膨胀参数",
- "in": "query",
- "name": "inflate_delta",
- "required": true,
- "schema": {
- "description": "膨胀参数",
- "title": "Inflate Delta",
- "type": "number"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "生成DMA分区",
- "tags": [
- "Regions & DMAs"
- ]
- }
- },
- "/api/v1/district-metering-areas": {
- "delete": {
- "description": "删除指定的区域计量(DMA)",
- "operationId": "delete_district_metering_areas",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "204": {
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "删除DMA",
- "tags": [
- "Regions & DMAs"
- ]
- },
- "get": {
- "description": "获取指定水网中所有DMA的详细信息",
- "operationId": "get_district_metering_areas",
- "parameters": [
- {
- "in": "query",
- "name": "limit",
- "required": false,
- "schema": {
- "default": 100,
- "maximum": 1000,
- "minimum": 1,
- "title": "Limit",
- "type": "integer"
- }
- },
- {
- "in": "query",
- "name": "offset",
- "required": false,
- "schema": {
- "default": 0,
- "minimum": 0,
- "title": "Offset",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Page_dict_str__Any__"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取所有DMA",
- "tags": [
- "Regions & DMAs"
- ]
- },
- "patch": {
- "description": "修改指定DMA的属性信息",
- "operationId": "patch_district_metering_areas",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "设置DMA属性",
- "tags": [
- "Regions & DMAs"
- ]
- },
- "post": {
- "description": "向水网添加一个新的区域计量(DMA)",
- "operationId": "post_district_metering_areas",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "201": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "添加新DMA",
- "tags": [
- "Regions & DMAs"
- ]
- }
- },
- "/api/v1/district-metering-areas/detail": {
- "get": {
- "description": "获取指定ID的区域计量(DMA)详细信息",
- "operationId": "get_district_metering_areas_detail",
- "parameters": [
- {
- "description": "DMA ID",
- "in": "query",
- "name": "id",
- "required": true,
- "schema": {
- "description": "DMA ID",
- "title": "Id",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "title": "Response Get District Metering Areas Detail",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取DMA信息",
- "tags": [
- "Regions & DMAs"
- ]
- }
- },
- "/api/v1/district-metering-areas/for-network": {
- "post": {
- "description": "为整个水网计算区域计量(DMA)分区方案",
- "operationId": "post_district_metering_areas_for_network",
- "parameters": [
- {
- "in": "query",
- "name": "limit",
- "required": false,
- "schema": {
- "default": 100,
- "maximum": 1000,
- "minimum": 1,
- "title": "Limit",
- "type": "integer"
- }
- },
- {
- "in": "query",
- "name": "offset",
- "required": false,
- "schema": {
- "default": 0,
- "minimum": 0,
- "title": "Offset",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Page_list_str__"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "计算整网DMA分区",
- "tags": [
- "Regions & DMAs"
- ]
- }
- },
- "/api/v1/district-metering-areas/for-nodes": {
- "post": {
- "description": "为指定节点集计算区域计量(DMA)分区方案",
- "operationId": "post_district_metering_areas_for_nodes",
- "parameters": [
- {
- "in": "query",
- "name": "limit",
- "required": false,
- "schema": {
- "default": 100,
- "maximum": 1000,
- "minimum": 1,
- "title": "Limit",
- "type": "integer"
- }
- },
- {
- "in": "query",
- "name": "offset",
- "required": false,
- "schema": {
- "default": 0,
- "minimum": 0,
- "title": "Offset",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Page_list_str__"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "计算节点DMA分区",
- "tags": [
- "Regions & DMAs"
- ]
- }
- },
- "/api/v1/district-metering-areas/for-region": {
- "post": {
- "description": "为指定区域计算区域计量(DMA)分区方案",
- "operationId": "post_district_metering_areas_for_region",
- "parameters": [
- {
- "in": "query",
- "name": "limit",
- "required": false,
- "schema": {
- "default": 100,
- "maximum": 1000,
- "minimum": 1,
- "title": "Limit",
- "type": "integer"
- }
- },
- {
- "in": "query",
- "name": "offset",
- "required": false,
- "schema": {
- "default": 0,
- "minimum": 0,
- "title": "Offset",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Page_list_str__"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "计算区域内DMA分区",
- "tags": [
- "Regions & DMAs"
- ]
- }
- },
- "/api/v1/district-metering-areas/ids": {
- "get": {
- "description": "获取指定水网中所有DMA的ID列表",
- "operationId": "get_district_metering_areas_ids",
- "parameters": [
- {
- "in": "query",
- "name": "limit",
- "required": false,
- "schema": {
- "default": 100,
- "maximum": 1000,
- "minimum": 1,
- "title": "Limit",
- "type": "integer"
- }
- },
- {
- "in": "query",
- "name": "offset",
- "required": false,
- "schema": {
- "default": 0,
- "minimum": 0,
- "title": "Offset",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Page_str_"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取所有DMA ID",
- "tags": [
- "Regions & DMAs"
- ]
- }
- },
"/api/v1/element-properties": {
"get": {
"description": "获取指定元素的属性信息",
@@ -10720,219 +9099,6 @@
]
}
},
- "/api/v1/extension-datas": {
- "get": {
- "description": "获取指定网络中指定键的扩展数据值",
- "operationId": "get_extension_datas",
- "parameters": [
- {
- "description": "扩展数据键",
- "in": "query",
- "name": "key",
- "required": true,
- "schema": {
- "description": "扩展数据键",
- "title": "Key",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "title": "Response Get Extension Datas"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取指定扩展数据",
- "tags": [
- "Extension"
- ]
- },
- "patch": {
- "description": "设置指定网络中的扩展数据",
- "operationId": "patch_extension_datas",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "设置扩展数据",
- "tags": [
- "Extension"
- ]
- }
- },
"/api/v1/flushing-analyses": {
"post": {
"description": "高级版本的冲洗分析,支持按状态和设置值控制多个可选阀门,指定排污节点,并设置固定的冲洗流量。返回纯文本格式的分析结果。",
@@ -15157,202 +13323,6 @@
]
}
},
- "/api/v1/network-command-batches": {
- "post": {
- "description": "执行多个网络操作命令",
- "operationId": "post_network_command_batches",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "执行批量命令",
- "tags": [
- "Snapshots"
- ]
- }
- },
- "/api/v1/network-command-batches/compressed": {
- "post": {
- "description": "执行压缩的批量命令",
- "operationId": "post_network_command_batches_compressed",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "执行压缩批量命令",
- "tags": [
- "Snapshots"
- ]
- }
- },
"/api/v1/network-in-extents": {
"get": {
"description": "获取指定地理范围内的网络节点和管线",
@@ -16215,127 +14185,6 @@
]
}
},
- "/api/v1/network-pipe-risk-probability-nows": {
- "get": {
- "description": "获取指定网络中所有管道的当前风险概率值",
- "operationId": "get_network_pipe_risk_probability_nows",
- "parameters": [
- {
- "in": "query",
- "name": "limit",
- "required": false,
- "schema": {
- "default": 100,
- "maximum": 1000,
- "minimum": 1,
- "title": "Limit",
- "type": "integer"
- }
- },
- {
- "in": "query",
- "name": "offset",
- "required": false,
- "schema": {
- "default": 0,
- "minimum": 0,
- "title": "Offset",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Page_dict_str__Any__"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取整个网络的管道风险概率",
- "tags": [
- "Risk"
- ]
- }
- },
"/api/v1/network-schemas/backdrop": {
"get": {
"description": "获取网络中背景对象的架构定义",
@@ -16744,108 +14593,6 @@
]
}
},
- "/api/v1/network-schemas/district-metering-area": {
- "get": {
- "description": "获取指定水网的区域计量(DMA)属性架构定义",
- "operationId": "get_network_schemas_district_metering_area",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "additionalProperties": {
- "type": "object"
- },
- "title": "Response Get Network Schemas District Metering Area",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取DMA属性架构",
- "tags": [
- "Regions & DMAs"
- ]
- }
- },
"/api/v1/network-schemas/emitter": {
"get": {
"description": "获取网络中发射器对象的架构定义",
@@ -18174,7 +15921,6 @@
},
"/api/v1/network-schemas/region": {
"get": {
- "description": "获取指定水网的区域属性架构定义",
"operationId": "get_network_schemas_region",
"parameters": [
{
@@ -18378,7 +16124,6 @@
},
"/api/v1/network-schemas/scada-device": {
"get": {
- "description": "获取SCADA设备的数据架构\n\n返回SCADA设备表的字段定义和类型信息。\n\nArgs:\n network: 管网名称(或数据库名称)\n \nReturns:\n SCADA设备的字段架构信息",
"operationId": "get_network_schemas_scada_device",
"parameters": [
{
@@ -18472,417 +16217,9 @@
"OAuth2PasswordBearer": []
}
],
- "summary": "获取SCADA设备架构",
+ "summary": "获取 SCADA 设备结构",
"tags": [
- "SCADA设备"
- ]
- }
- },
- "/api/v1/network-schemas/scada-device-data": {
- "get": {
- "description": "获取SCADA设备数据的表结构\n\n返回SCADA设备数据表的字段定义和类型信息。\n\nArgs:\n network: 管网名称(或数据库名称)\n \nReturns:\n SCADA设备数据的字段架构信息",
- "operationId": "get_network_schemas_scada_device_data",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "additionalProperties": {
- "type": "object"
- },
- "title": "Response Get Network Schemas Scada Device Data",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取SCADA设备数据架构",
- "tags": [
- "SCADA设备数据"
- ]
- }
- },
- "/api/v1/network-schemas/scada-element": {
- "get": {
- "description": "获取SCADA元素映射的表结构\n\n返回SCADA元素映射表的字段定义和类型信息。\n\nArgs:\n network: 管网名称(或数据库名称)\n \nReturns:\n SCADA元素映射的字段架构信息",
- "operationId": "get_network_schemas_scada_element",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "additionalProperties": {
- "type": "object"
- },
- "title": "Response Get Network Schemas Scada Element",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取SCADA元素架构",
- "tags": [
- "SCADA元素映射"
- ]
- }
- },
- "/api/v1/network-schemas/scheme": {
- "get": {
- "description": "获取指定网络的方案模式定义",
- "operationId": "get_network_schemas_scheme",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "additionalProperties": {
- "type": "object"
- },
- "title": "Response Get Network Schemas Scheme",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取方案模式",
- "tags": [
- "Schemes"
- ]
- }
- },
- "/api/v1/network-schemas/service-area": {
- "get": {
- "description": "获取指定水网的服务区属性架构定义",
- "operationId": "get_network_schemas_service_area",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "additionalProperties": {
- "type": "object"
- },
- "title": "Response Get Network Schemas Service Area",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取服务区属性架构",
- "tags": [
- "Regions & DMAs"
+ "SCADA Metadata"
]
}
},
@@ -19600,108 +16937,6 @@
]
}
},
- "/api/v1/network-schemas/virtual-district": {
- "get": {
- "description": "获取指定水网的虚拟分区属性架构定义",
- "operationId": "get_network_schemas_virtual_district",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "additionalProperties": {
- "type": "object"
- },
- "title": "Response Get Network Schemas Virtual District",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取虚拟分区属性架构",
- "tags": [
- "Regions & DMAs"
- ]
- }
- },
"/api/v1/node-coords": {
"get": {
"description": "获取指定节点的地理坐标(X, Y)",
@@ -20505,127 +17740,6 @@
]
}
},
- "/api/v1/operations": {
- "patch": {
- "description": "选择并恢复到指定的操作",
- "operationId": "patch_operations",
- "parameters": [
- {
- "description": "操作ID",
- "in": "query",
- "name": "operation",
- "required": true,
- "schema": {
- "description": "操作ID",
- "title": "Operation",
- "type": "integer"
- }
- },
- {
- "description": "是否丢弃当前更改",
- "in": "query",
- "name": "discard",
- "required": false,
- "schema": {
- "default": false,
- "description": "是否丢弃当前更改",
- "title": "Discard",
- "type": "boolean"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "选择操作",
- "tags": [
- "Snapshots"
- ]
- }
- },
"/api/v1/outputs": {
"get": {
"description": "导出指定路径的模拟输出文件内容。参数应为绝对路径。",
@@ -22119,138 +19233,6 @@
]
}
},
- "/api/v1/pipes-risk-probabilities": {
- "get": {
- "description": "批量获取多条管道的风险概率值",
- "operationId": "get_pipes_risk_probabilities",
- "parameters": [
- {
- "description": "逗号分隔的管道ID列表",
- "in": "query",
- "name": "pipe_ids",
- "required": true,
- "schema": {
- "description": "逗号分隔的管道ID列表",
- "title": "Pipe Ids",
- "type": "string"
- }
- },
- {
- "in": "query",
- "name": "limit",
- "required": false,
- "schema": {
- "default": 100,
- "maximum": 1000,
- "minimum": 1,
- "title": "Limit",
- "type": "integer"
- }
- },
- {
- "in": "query",
- "name": "offset",
- "required": false,
- "schema": {
- "default": 0,
- "minimum": 0,
- "title": "Offset",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Page_dict_str__Any__"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "批量获取多条管道风险概率",
- "tags": [
- "Risk"
- ]
- }
- },
"/api/v1/pipes/diameter": {
"get": {
"description": "获取指定管道的管径",
@@ -23753,325 +20735,6 @@
]
}
},
- "/api/v1/pipes/risk-probability": {
- "get": {
- "description": "获取指定管道的风险概率历史数据",
- "operationId": "get_pipes_risk_probability",
- "parameters": [
- {
- "description": "管道ID",
- "in": "query",
- "name": "pipe_id",
- "required": true,
- "schema": {
- "description": "管道ID",
- "title": "Pipe Id",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "title": "Response Get Pipes Risk Probability",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取管道风险概率历史",
- "tags": [
- "Risk"
- ]
- }
- },
- "/api/v1/pipes/risk-probability-geometries": {
- "get": {
- "description": "获取指定网络中管道的风险相关几何数据",
- "operationId": "get_pipes_risk_probability_geometries",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "title": "Response Get Pipes Risk Probability Geometries",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取管道风险几何信息",
- "tags": [
- "Risk"
- ]
- }
- },
- "/api/v1/pipes/risk-probability-now": {
- "get": {
- "description": "获取指定管道当前时刻的风险概率值",
- "operationId": "get_pipes_risk_probability_now",
- "parameters": [
- {
- "description": "管道ID",
- "in": "query",
- "name": "pipe_id",
- "required": true,
- "schema": {
- "description": "管道ID",
- "title": "Pipe Id",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "title": "Response Get Pipes Risk Probability Now",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取管道当前风险概率",
- "tags": [
- "Risk"
- ]
- }
- },
"/api/v1/pipes/roughness": {
"get": {
"description": "获取指定管道的粗糙度",
@@ -24772,486 +21435,6 @@
]
}
},
- "/api/v1/pressure-sensor-placement-kmeans": {
- "post": {
- "description": "高级版本的压力传感器放置分析,通过JSON请求体提供详细参数。基于KMeans聚类算法确定最优放置位置。",
- "operationId": "post_pressure_sensor_placement_kmeans",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/PressureSensorPlacementRest",
- "description": "传感器放置分析参数"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "压力传感器放置-KMeans聚类分析(高级)",
- "tags": [
- "Simulation Control"
- ]
- }
- },
- "/api/v1/pressure-sensor-placement-kmeans-calculations": {
- "post": {
- "description": "基于KMeans聚类算法,为指定管网项目确定压力传感器的最优放置位置。此为基础版本。",
- "operationId": "post_pressure_sensor_placement_kmeans_calculations",
- "parameters": [
- {
- "description": "放置方案名称",
- "in": "query",
- "name": "scheme_name",
- "required": true,
- "schema": {
- "description": "放置方案名称",
- "title": "Scheme Name",
- "type": "string"
- }
- },
- {
- "description": "传感器数量",
- "in": "query",
- "name": "sensor_number",
- "required": true,
- "schema": {
- "description": "传感器数量",
- "title": "Sensor Number",
- "type": "integer"
- }
- },
- {
- "description": "最小管径限制(毫米)",
- "in": "query",
- "name": "min_diameter",
- "required": true,
- "schema": {
- "description": "最小管径限制(毫米)",
- "title": "Min Diameter",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "压力传感器放置-KMeans聚类分析(基础)",
- "tags": [
- "Simulation Control"
- ]
- }
- },
- "/api/v1/pressure-sensor-placement-sensitivities": {
- "post": {
- "description": "高级版本的压力传感器放置分析,通过JSON请求体提供详细参数。基于灵敏度分析方法确定最优放置位置。",
- "operationId": "post_pressure_sensor_placement_sensitivities",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/PressureSensorPlacementRest",
- "description": "传感器放置分析参数"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "压力传感器放置-灵敏度分析(高级)",
- "tags": [
- "Simulation Control"
- ]
- }
- },
- "/api/v1/pressure-sensor-placement-sensitivity-calculations": {
- "post": {
- "description": "基于灵敏度分析方法,为指定管网项目确定最优的压力传感器放置位置。此为基础版本。",
- "operationId": "post_pressure_sensor_placement_sensitivity_calculations",
- "parameters": [
- {
- "description": "放置方案名称",
- "in": "query",
- "name": "scheme_name",
- "required": true,
- "schema": {
- "description": "放置方案名称",
- "title": "Scheme Name",
- "type": "string"
- }
- },
- {
- "description": "传感器数量",
- "in": "query",
- "name": "sensor_number",
- "required": true,
- "schema": {
- "description": "传感器数量",
- "title": "Sensor Number",
- "type": "integer"
- }
- },
- {
- "description": "最小管径限制(毫米)",
- "in": "query",
- "name": "min_diameter",
- "required": true,
- "schema": {
- "description": "最小管径限制(毫米)",
- "title": "Min Diameter",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "压力传感器放置-灵敏度分析(基础)",
- "tags": [
- "Simulation Control"
- ]
- }
- },
"/api/v1/project-codes": {
"get": {
"description": "获取服务器上所有可用的供水管网项目名称列表。",
@@ -29361,107 +25544,8 @@
]
}
},
- "/api/v1/redos": {
- "post": {
- "description": "重做网络上被撤销的操作",
- "operationId": "post_redos",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "重做操作",
- "tags": [
- "Snapshots"
- ]
- }
- },
"/api/v1/regions": {
"delete": {
- "description": "删除指定的区域",
"operationId": "delete_regions",
"parameters": [
{
@@ -29549,8 +25633,125 @@
"Regions & DMAs"
]
},
+ "get": {
+ "operationId": "get_regions",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "limit",
+ "required": false,
+ "schema": {
+ "default": 100,
+ "maximum": 1000,
+ "minimum": 1,
+ "title": "Limit",
+ "type": "integer"
+ }
+ },
+ {
+ "in": "query",
+ "name": "offset",
+ "required": false,
+ "schema": {
+ "default": 0,
+ "minimum": 0,
+ "title": "Offset",
+ "type": "integer"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Project-Id",
+ "required": true,
+ "schema": {
+ "title": "X-Project-Id",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Page_dict_str__Any__"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Authentication required"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Insufficient permission"
+ },
+ "404": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource not found"
+ },
+ "409": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource conflict"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Validation error"
+ },
+ "503": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Dependency unavailable"
+ }
+ },
+ "security": [
+ {
+ "OAuth2PasswordBearer": []
+ }
+ ],
+ "summary": "获取区域列表",
+ "tags": [
+ "Regions & DMAs"
+ ]
+ },
"patch": {
- "description": "修改指定区域的属性信息",
"operationId": "patch_regions",
"parameters": [
{
@@ -29640,13 +25841,12 @@
"OAuth2PasswordBearer": []
}
],
- "summary": "设置区域属性",
+ "summary": "修改区域",
"tags": [
"Regions & DMAs"
]
},
"post": {
- "description": "向水网添加一个新的区域",
"operationId": "post_regions",
"parameters": [
{
@@ -29736,7 +25936,7 @@
"OAuth2PasswordBearer": []
}
],
- "summary": "添加新区域",
+ "summary": "添加区域",
"tags": [
"Regions & DMAs"
]
@@ -29744,16 +25944,15 @@
},
"/api/v1/regions/detail": {
"get": {
- "description": "获取指定ID的区域详细信息",
"operationId": "get_regions_detail",
"parameters": [
{
- "description": "区域ID",
+ "description": "区域 ID",
"in": "query",
"name": "id",
"required": true,
"schema": {
- "description": "区域ID",
+ "description": "区域 ID",
"title": "Id",
"type": "string"
}
@@ -29852,6 +26051,137 @@
]
}
},
+ "/api/v1/regions/nodes": {
+ "get": {
+ "operationId": "get_regions_nodes",
+ "parameters": [
+ {
+ "description": "区域 ID",
+ "in": "query",
+ "name": "id",
+ "required": true,
+ "schema": {
+ "description": "区域 ID",
+ "title": "Id",
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "limit",
+ "required": false,
+ "schema": {
+ "default": 100,
+ "maximum": 1000,
+ "minimum": 1,
+ "title": "Limit",
+ "type": "integer"
+ }
+ },
+ {
+ "in": "query",
+ "name": "offset",
+ "required": false,
+ "schema": {
+ "default": 0,
+ "minimum": 0,
+ "title": "Offset",
+ "type": "integer"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Project-Id",
+ "required": true,
+ "schema": {
+ "title": "X-Project-Id",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Page_str_"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Authentication required"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Insufficient permission"
+ },
+ "404": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource not found"
+ },
+ "409": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource conflict"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Validation error"
+ },
+ "503": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Dependency unavailable"
+ }
+ },
+ "security": [
+ {
+ "OAuth2PasswordBearer": []
+ }
+ ],
+ "summary": "获取区域节点",
+ "tags": [
+ "Regions & DMAs"
+ ]
+ }
+ },
"/api/v1/reservoirs": {
"delete": {
"description": "从指定供水网络中删除指定的水库/水源节点",
@@ -31735,212 +28065,6 @@
]
}
},
- "/api/v1/restore-operations": {
- "get": {
- "description": "获取网络的恢复操作ID",
- "operationId": "get_restore_operations",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "title": "Response Get Restore Operations",
- "type": "integer"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取恢复操作ID",
- "tags": [
- "Snapshots"
- ]
- },
- "patch": {
- "description": "设置网络的恢复操作ID",
- "operationId": "patch_restore_operations",
- "parameters": [
- {
- "description": "操作ID",
- "in": "query",
- "name": "operation",
- "required": true,
- "schema": {
- "description": "操作ID",
- "title": "Operation",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "设置恢复操作ID",
- "tags": [
- "Snapshots"
- ]
- }
- },
"/api/v1/rule-properties": {
"get": {
"description": "获取指定网络中的规则属性信息",
@@ -32238,687 +28362,8 @@
]
}
},
- "/api/v1/scada-device-cleaning-runs": {
- "post": {
- "description": "清空SCADA设备表\n\n删除指定管网中所有的SCADA设备。\n\nArgs:\n network: 管网名称(或数据库名称)\n \nReturns:\n 变更集合信息",
- "operationId": "post_scada_device_cleaning_runs",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "清空SCADA设备表",
- "tags": [
- "SCADA设备"
- ]
- }
- },
- "/api/v1/scada-device-data-cleaning-runs": {
- "post": {
- "description": "清空SCADA设备数据表\n\n删除指定管网中所有SCADA设备的数据。\n\nArgs:\n network: 管网名称(或数据库名称)\n \nReturns:\n 变更集合信息",
- "operationId": "post_scada_device_data_cleaning_runs",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "清空SCADA设备数据表",
- "tags": [
- "SCADA设备数据"
- ]
- }
- },
- "/api/v1/scada-device-datas": {
- "delete": {
- "description": "删除SCADA设备数据\n\n删除指定SCADA设备的数据记录。\n\nArgs:\n network: 管网名称(或数据库名称)\n req: 请求体,包含要删除的数据ID\n \nReturns:\n 变更集合信息",
- "operationId": "delete_scada_device_datas",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "204": {
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "删除SCADA设备数据",
- "tags": [
- "SCADA设备数据"
- ]
- },
- "patch": {
- "description": "更新SCADA设备数据\n\n修改指定SCADA设备的数据。\n\nArgs:\n network: 管网名称(或数据库名称)\n req: 请求体,包含要更新的数据\n \nReturns:\n 变更集合信息",
- "operationId": "patch_scada_device_datas",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "更新SCADA设备数据",
- "tags": [
- "SCADA设备数据"
- ]
- },
- "post": {
- "description": "添加新的SCADA设备数据\n\n为指定SCADA设备添加新的数据记录。\n\nArgs:\n network: 管网名称(或数据库名称)\n req: 请求体,包含新数据的内容\n \nReturns:\n 变更集合信息",
- "operationId": "post_scada_device_datas",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "201": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "添加SCADA设备数据",
- "tags": [
- "SCADA设备数据"
- ]
- }
- },
- "/api/v1/scada-device-datas/detail": {
- "get": {
- "description": "获取单个SCADA设备的数据\n\n查询指定设备的监测数据或配置数据。\n\nArgs:\n network: 管网名称(或数据库名称)\n device_id: SCADA设备ID\n \nReturns:\n SCADA设备数据",
- "operationId": "get_scada_device_datas_detail",
- "parameters": [
- {
- "description": "SCADA设备ID",
- "in": "query",
- "name": "device_id",
- "required": true,
- "schema": {
- "description": "SCADA设备ID",
- "title": "Device Id",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "title": "Response Get Scada Device Datas Detail",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取SCADA设备数据",
- "tags": [
- "SCADA设备数据"
- ]
- }
- },
"/api/v1/scada-devices": {
- "delete": {
- "description": "删除SCADA设备\n\n从指定管网中删除一个SCADA设备。\n\nArgs:\n network: 管网名称(或数据库名称)\n req: 请求体,包含要删除的设备ID\n \nReturns:\n 变更集合信息",
- "operationId": "delete_scada_devices",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "204": {
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "删除SCADA设备",
- "tags": [
- "SCADA设备"
- ]
- },
"get": {
- "description": "获取指定管网所有SCADA设备的完整信息\n\nArgs:\n network: 管网名称(或数据库名称)\n \nReturns:\n SCADA设备信息列表",
"operationId": "get_scada_devices",
"parameters": [
{
@@ -33031,217 +28476,24 @@
"OAuth2PasswordBearer": []
}
],
- "summary": "获取所有SCADA设备",
+ "summary": "获取 SCADA 设备列表",
"tags": [
- "SCADA设备"
- ]
- },
- "patch": {
- "description": "更新SCADA设备信息\n\n修改指定SCADA设备的属性。\n\nArgs:\n network: 管网名称(或数据库名称)\n req: 请求体,包含要更新的设备属性\n \nReturns:\n 变更集合信息",
- "operationId": "patch_scada_devices",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "更新SCADA设备",
- "tags": [
- "SCADA设备"
- ]
- },
- "post": {
- "description": "添加新的SCADA设备\n\n在指定管网中添加一个新的SCADA设备。\n\nArgs:\n network: 管网名称(或数据库名称)\n req: 请求体,包含新设备的属性\n \nReturns:\n 变更集合信息",
- "operationId": "post_scada_devices",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "201": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "添加SCADA设备",
- "tags": [
- "SCADA设备"
+ "SCADA Metadata"
]
}
},
"/api/v1/scada-devices/detail": {
"get": {
- "description": "获取单个SCADA设备的信息\n\n根据设备ID查询该设备的详细信息。\n\nArgs:\n network: 管网名称(或数据库名称)\n id: SCADA设备ID\n \nReturns:\n SCADA设备信息",
"operationId": "get_scada_devices_detail",
"parameters": [
{
- "description": "SCADA设备ID",
+ "description": "SCADA 设备 ID",
"in": "query",
- "name": "id",
+ "name": "device_id",
"required": true,
"schema": {
- "description": "SCADA设备ID",
- "title": "Id",
+ "description": "SCADA 设备 ID",
+ "title": "Device Id",
"type": "string"
}
},
@@ -33333,963 +28585,9 @@
"OAuth2PasswordBearer": []
}
],
- "summary": "获取SCADA设备",
+ "summary": "获取 SCADA 设备",
"tags": [
- "SCADA设备"
- ]
- }
- },
- "/api/v1/scada-devices/ids": {
- "get": {
- "description": "获取指定管网所有SCADA设备的ID列表\n\nArgs:\n network: 管网名称(或数据库名称)\n \nReturns:\n SCADA设备ID列表",
- "operationId": "get_scada_devices_ids",
- "parameters": [
- {
- "in": "query",
- "name": "limit",
- "required": false,
- "schema": {
- "default": 100,
- "maximum": 1000,
- "minimum": 1,
- "title": "Limit",
- "type": "integer"
- }
- },
- {
- "in": "query",
- "name": "offset",
- "required": false,
- "schema": {
- "default": 0,
- "minimum": 0,
- "title": "Offset",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Page_str_"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取所有SCADA设备ID",
- "tags": [
- "SCADA设备"
- ]
- }
- },
- "/api/v1/scada-element-cleaning-runs": {
- "post": {
- "description": "清空SCADA元素映射表\n\n删除指定管网中所有的SCADA元素映射。\n\nArgs:\n network: 管网名称(或数据库名称)\n \nReturns:\n 变更集合信息",
- "operationId": "post_scada_element_cleaning_runs",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "清空SCADA元素映射表",
- "tags": [
- "SCADA元素映射"
- ]
- }
- },
- "/api/v1/scada-elements": {
- "delete": {
- "description": "删除SCADA元素映射\n\n移除SCADA设备与管网元素的映射关系。\n\nArgs:\n network: 管网名称(或数据库名称)\n req: 请求体,包含要删除的映射ID\n \nReturns:\n 变更集合信息",
- "operationId": "delete_scada_elements",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "204": {
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "删除SCADA元素映射",
- "tags": [
- "SCADA元素映射"
- ]
- },
- "get": {
- "description": "获取指定管网所有SCADA元素映射\n\n查询所有SCADA设备与管网元素(节点/管道)的映射关系。\n\nArgs:\n network: 管网名称(或数据库名称)\n \nReturns:\n SCADA元素映射列表",
- "operationId": "get_scada_elements",
- "parameters": [
- {
- "in": "query",
- "name": "limit",
- "required": false,
- "schema": {
- "default": 100,
- "maximum": 1000,
- "minimum": 1,
- "title": "Limit",
- "type": "integer"
- }
- },
- {
- "in": "query",
- "name": "offset",
- "required": false,
- "schema": {
- "default": 0,
- "minimum": 0,
- "title": "Offset",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Page_dict_str__Any__"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取所有SCADA元素映射",
- "tags": [
- "SCADA元素映射"
- ]
- },
- "patch": {
- "description": "更新SCADA元素映射\n\n修改SCADA设备与管网元素的映射关系。\n\nArgs:\n network: 管网名称(或数据库名称)\n req: 请求体,包含要更新的映射信息\n \nReturns:\n 变更集合信息",
- "operationId": "patch_scada_elements",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "更新SCADA元素映射",
- "tags": [
- "SCADA元素映射"
- ]
- },
- "post": {
- "description": "添加新的SCADA元素映射\n\n创建SCADA设备与管网元素的新映射关系。\n\nArgs:\n network: 管网名称(或数据库名称)\n req: 请求体,包含新映射的信息\n \nReturns:\n 变更集合信息",
- "operationId": "post_scada_elements",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "201": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "添加SCADA元素映射",
- "tags": [
- "SCADA元素映射"
- ]
- }
- },
- "/api/v1/scada-elements/detail": {
- "get": {
- "description": "获取单个SCADA元素映射的信息\n\n根据ID查询特定的SCADA设备与管网元素的映射关系。\n\nArgs:\n network: 管网名称(或数据库名称)\n id: SCADA元素映射ID\n \nReturns:\n SCADA元素映射信息",
- "operationId": "get_scada_elements_detail",
- "parameters": [
- {
- "description": "SCADA元素映射ID",
- "in": "query",
- "name": "id",
- "required": true,
- "schema": {
- "description": "SCADA元素映射ID",
- "title": "Id",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "title": "Response Get Scada Elements Detail",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取单个SCADA元素映射",
- "tags": [
- "SCADA元素映射"
- ]
- }
- },
- "/api/v1/scada-info": {
- "get": {
- "description": "获取指定管网所有SCADA的信息\n\n查询该管网下所有已配置的SCADA的完整信息。\n\nArgs:\n network: 管网名称(或数据库名称)\n \nReturns:\n SCADA信息列表",
- "operationId": "get_scada_info",
- "parameters": [
- {
- "in": "query",
- "name": "limit",
- "required": false,
- "schema": {
- "default": 100,
- "maximum": 1000,
- "minimum": 1,
- "title": "Limit",
- "type": "integer"
- }
- },
- {
- "in": "query",
- "name": "offset",
- "required": false,
- "schema": {
- "default": 0,
- "minimum": 0,
- "title": "Offset",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Page_dict_str__Any__"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取所有SCADA信息",
- "tags": [
- "SCADA信息"
- ]
- }
- },
- "/api/v1/scada-info-schemas": {
- "get": {
- "description": "获取SCADA信息表的结构\n\n返回SCADA信息表的字段定义和类型信息。\n\nArgs:\n network: 管网名称(或数据库名称)\n \nReturns:\n SCADA信息的字段架构信息",
- "operationId": "get_scada_info_schemas",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "additionalProperties": {
- "type": "object"
- },
- "title": "Response Get Scada Info Schemas",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取SCADA信息架构",
- "tags": [
- "SCADA信息"
+ "SCADA Metadata"
]
}
},
@@ -34391,116 +28689,6 @@
]
}
},
- "/api/v1/scada-info/detail": {
- "get": {
- "description": "获取单个SCADA信息\n\n根据ID查询SCADA的详细配置信息。\n\nArgs:\n network: 管网名称(或数据库名称)\n id: SCADA信息ID\n \nReturns:\n SCADA信息详情",
- "operationId": "get_scada_info_detail",
- "parameters": [
- {
- "description": "SCADA信息ID",
- "in": "query",
- "name": "id",
- "required": true,
- "schema": {
- "description": "SCADA信息ID",
- "title": "Id",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "title": "Response Get Scada Info Detail",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取SCADA信息",
- "tags": [
- "SCADA信息"
- ]
- }
- },
"/api/v1/scada-properties": {
"get": {
"description": "获取指定SCADA点的属性信息",
@@ -34721,500 +28909,6 @@
]
}
},
- "/api/v1/schemes": {
- "get": {
- "description": "获取指定网络的所有方案信息",
- "operationId": "get_schemes",
- "parameters": [
- {
- "description": "方案类型;为空时返回全部类型",
- "in": "query",
- "name": "scheme_type",
- "required": false,
- "schema": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "方案类型;为空时返回全部类型",
- "title": "Scheme Type"
- }
- },
- {
- "description": "查询日期(可选)",
- "in": "query",
- "name": "query_date",
- "required": false,
- "schema": {
- "anyOf": [
- {
- "format": "date-time",
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "查询日期(可选)",
- "title": "Query Date"
- }
- },
- {
- "in": "query",
- "name": "limit",
- "required": false,
- "schema": {
- "default": 100,
- "maximum": 1000,
- "minimum": 1,
- "title": "Limit",
- "type": "integer"
- }
- },
- {
- "in": "query",
- "name": "offset",
- "required": false,
- "schema": {
- "default": 0,
- "minimum": 0,
- "title": "Offset",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Page_dict_Any__Any__"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取所有方案",
- "tags": [
- "Schemes"
- ]
- }
- },
- "/api/v1/schemes/detail": {
- "get": {
- "description": "根据名称获取指定的方案信息",
- "operationId": "get_schemes_detail",
- "parameters": [
- {
- "description": "方案名称",
- "in": "query",
- "name": "schema_name",
- "required": true,
- "schema": {
- "description": "方案名称",
- "title": "Schema Name",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "title": "Response Get Schemes Detail",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取单个方案",
- "tags": [
- "Schemes"
- ]
- }
- },
- "/api/v1/schemes/list-with-connection": {
- "get": {
- "description": "使用连接池查询所有方案信息",
- "operationId": "get_schemes_list_with_connection",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取方案列表",
- "tags": [
- "Project Data"
- ]
- }
- },
- "/api/v1/schemes/{scheme_name}": {
- "get": {
- "description": "按方案类型获取指定方案详情",
- "operationId": "get_schemes_scheme_name",
- "parameters": [
- {
- "description": "方案名称",
- "in": "path",
- "name": "scheme_name",
- "required": true,
- "schema": {
- "description": "方案名称",
- "title": "Scheme Name",
- "type": "string"
- }
- },
- {
- "description": "方案类型;为空时返回通用方案详情",
- "in": "query",
- "name": "scheme_type",
- "required": false,
- "schema": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "方案类型;为空时返回通用方案详情",
- "title": "Scheme Type"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "title": "Response Get Schemes Scheme Name",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取方案详情",
- "tags": [
- "Schemes"
- ]
- }
- },
"/api/v1/sensor-placement-candidates/{node_id}": {
"get": {
"operationId": "get_sensor_placement_candidates_node_id",
@@ -35323,9 +29017,127 @@
]
}
},
- "/api/v1/sensor-placement-optimization-runs": {
+ "/api/v1/sensor-placement-runs": {
+ "get": {
+ "operationId": "get_sensor_placement_runs",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "limit",
+ "required": false,
+ "schema": {
+ "default": 100,
+ "maximum": 1000,
+ "minimum": 1,
+ "title": "Limit",
+ "type": "integer"
+ }
+ },
+ {
+ "in": "query",
+ "name": "offset",
+ "required": false,
+ "schema": {
+ "default": 0,
+ "minimum": 0,
+ "title": "Offset",
+ "type": "integer"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Project-Id",
+ "required": true,
+ "schema": {
+ "title": "X-Project-Id",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Page_SensorPlacementSchemeResponse_"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Authentication required"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Insufficient permission"
+ },
+ "404": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource not found"
+ },
+ "409": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource conflict"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Validation error"
+ },
+ "503": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Dependency unavailable"
+ }
+ },
+ "security": [
+ {
+ "OAuth2PasswordBearer": []
+ }
+ ],
+ "summary": "获取监测点优化运行",
+ "tags": [
+ "Sensor Placement"
+ ]
+ },
"post": {
- "operationId": "post_sensor_placement_optimization_runs",
+ "operationId": "post_sensor_placement_runs",
"parameters": [
{
"in": "header",
@@ -35430,291 +29242,18 @@
]
}
},
- "/api/v1/sensor-placement-schemes": {
+ "/api/v1/sensor-placement-runs/{run_id}": {
"get": {
- "description": "获取网络中所有传感器的放置位置信息",
- "operationId": "get_sensor_placement_schemes",
- "parameters": [
- {
- "in": "query",
- "name": "limit",
- "required": false,
- "schema": {
- "default": 100,
- "maximum": 1000,
- "minimum": 1,
- "title": "Limit",
- "type": "integer"
- }
- },
- {
- "in": "query",
- "name": "offset",
- "required": false,
- "schema": {
- "default": 0,
- "minimum": 0,
- "title": "Offset",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Page_dict_Any__Any__"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取所有传感器位置",
- "tags": [
- "Misc"
- ]
- },
- "post": {
- "description": "创建新的传感器放置方案,支持灵敏度分析和KMeans聚类两种方法。根据指定的方法自动计算最优的传感器放置位置。",
- "operationId": "post_sensor_placement_schemes",
- "parameters": [
- {
- "description": "放置方案名称",
- "in": "query",
- "name": "scheme_name",
- "required": true,
- "schema": {
- "description": "放置方案名称",
- "title": "Scheme Name",
- "type": "string"
- }
- },
- {
- "description": "传感器类型",
- "in": "query",
- "name": "sensor_type",
- "required": true,
- "schema": {
- "description": "传感器类型",
- "title": "Sensor Type",
- "type": "string"
- }
- },
- {
- "description": "放置方法('sensitivity'或'kmeans')",
- "in": "query",
- "name": "method",
- "required": true,
- "schema": {
- "description": "放置方法('sensitivity'或'kmeans')",
- "title": "Method",
- "type": "string"
- }
- },
- {
- "description": "传感器数量",
- "in": "query",
- "name": "sensor_count",
- "required": true,
- "schema": {
- "description": "传感器数量",
- "title": "Sensor Count",
- "type": "integer"
- }
- },
- {
- "description": "最小管径限制(毫米),默认0",
- "in": "query",
- "name": "min_diameter",
- "required": false,
- "schema": {
- "default": 0,
- "description": "最小管径限制(毫米),默认0",
- "title": "Min Diameter",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "201": {
- "content": {
- "application/json": {
- "schema": {
- "title": "Response Post Sensor Placement Schemes",
- "type": "string"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "传感器放置方案创建",
- "tags": [
- "Simulation Control"
- ]
- }
- },
- "/api/v1/sensor-placement-schemes/{scheme_id}": {
- "get": {
- "operationId": "get_sensor_placement_schemes_scheme_id",
+ "operationId": "get_sensor_placement_runs_run_id",
"parameters": [
{
"in": "path",
- "name": "scheme_id",
+ "name": "run_id",
"required": true,
"schema": {
- "title": "Scheme Id",
- "type": "integer"
+ "format": "uuid",
+ "title": "Run Id",
+ "type": "string"
}
},
{
@@ -35810,15 +29349,16 @@
]
},
"put": {
- "operationId": "put_sensor_placement_schemes_scheme_id",
+ "operationId": "put_sensor_placement_runs_run_id",
"parameters": [
{
"in": "path",
- "name": "scheme_id",
+ "name": "run_id",
"required": true,
"schema": {
- "title": "Scheme Id",
- "type": "integer"
+ "format": "uuid",
+ "title": "Run Id",
+ "type": "string"
}
},
{
@@ -35924,17 +29464,18 @@
]
}
},
- "/api/v1/sensor-placement-schemes/{scheme_id}/exports/excel": {
+ "/api/v1/sensor-placement-runs/{run_id}/exports/excel": {
"post": {
- "operationId": "post_sensor_placement_schemes_scheme_id_exports_excel",
+ "operationId": "post_sensor_placement_runs_run_id_exports_excel",
"parameters": [
{
"in": "path",
- "name": "scheme_id",
+ "name": "run_id",
"required": true,
"schema": {
- "title": "Scheme Id",
- "type": "integer"
+ "format": "uuid",
+ "title": "Run Id",
+ "type": "string"
}
},
{
@@ -36040,748 +29581,6 @@
]
}
},
- "/api/v1/service-area-calculations": {
- "post": {
- "description": "计算指定水网的服务区分区,返回全部时间步结果",
- "operationId": "post_service_area_calculations",
- "parameters": [
- {
- "in": "query",
- "name": "limit",
- "required": false,
- "schema": {
- "default": 100,
- "maximum": 1000,
- "minimum": 1,
- "title": "Limit",
- "type": "integer"
- }
- },
- {
- "in": "query",
- "name": "offset",
- "required": false,
- "schema": {
- "default": 0,
- "minimum": 0,
- "title": "Offset",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Page_dict_str__list_str___"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "计算服务区",
- "tags": [
- "Regions & DMAs"
- ]
- }
- },
- "/api/v1/service-area-generation-runs": {
- "post": {
- "description": "根据参数自动生成水网的服务区分区",
- "operationId": "post_service_area_generation_runs",
- "parameters": [
- {
- "description": "膨胀参数",
- "in": "query",
- "name": "inflate_delta",
- "required": true,
- "schema": {
- "description": "膨胀参数",
- "title": "Inflate Delta",
- "type": "number"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "生成服务区分区",
- "tags": [
- "Regions & DMAs"
- ]
- }
- },
- "/api/v1/service-areas": {
- "delete": {
- "description": "删除指定的服务区",
- "operationId": "delete_service_areas",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "204": {
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "删除服务区",
- "tags": [
- "Regions & DMAs"
- ]
- },
- "get": {
- "description": "获取指定水网中的所有服务区信息",
- "operationId": "get_service_areas",
- "parameters": [
- {
- "in": "query",
- "name": "limit",
- "required": false,
- "schema": {
- "default": 100,
- "maximum": 1000,
- "minimum": 1,
- "title": "Limit",
- "type": "integer"
- }
- },
- {
- "in": "query",
- "name": "offset",
- "required": false,
- "schema": {
- "default": 0,
- "minimum": 0,
- "title": "Offset",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Page_dict_str__Any__"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取所有服务区",
- "tags": [
- "Regions & DMAs"
- ]
- },
- "patch": {
- "description": "修改指定服务区的属性信息",
- "operationId": "patch_service_areas",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "设置服务区属性",
- "tags": [
- "Regions & DMAs"
- ]
- },
- "post": {
- "description": "向水网添加一个新的服务区",
- "operationId": "post_service_areas",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "201": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "添加新服务区",
- "tags": [
- "Regions & DMAs"
- ]
- }
- },
- "/api/v1/service-areas/detail": {
- "get": {
- "description": "获取指定ID的服务区详细信息",
- "operationId": "get_service_areas_detail",
- "parameters": [
- {
- "description": "服务区ID",
- "in": "query",
- "name": "id",
- "required": true,
- "schema": {
- "description": "服务区ID",
- "title": "Id",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "title": "Response Get Service Areas Detail",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取服务区信息",
- "tags": [
- "Regions & DMAs"
- ]
- }
- },
"/api/v1/simulation-runs": {
"post": {
"description": "根据指定的开始时间和持续时间,手动运行水力模拟。开始时间必须是显式带时区的 ISO 8601 / RFC3339 时间。",
@@ -36895,897 +29694,6 @@
]
}
},
- "/api/v1/snapshot-for-current-operations": {
- "get": {
- "description": "检查当前操作的快照是否存在",
- "operationId": "get_snapshot_for_current_operations",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "title": "Response Get Snapshot For Current Operations",
- "type": "boolean"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "检查当前操作快照是否存在",
- "tags": [
- "Snapshots"
- ]
- },
- "post": {
- "description": "为当前操作创建快照",
- "operationId": "post_snapshot_for_current_operations",
- "parameters": [
- {
- "description": "快照标签",
- "in": "query",
- "name": "tag",
- "required": true,
- "schema": {
- "description": "快照标签",
- "title": "Tag",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "201": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "为当前操作创建快照",
- "tags": [
- "Snapshots"
- ]
- }
- },
- "/api/v1/snapshot-for-operations": {
- "get": {
- "description": "检查指定操作ID的快照是否存在",
- "operationId": "get_snapshot_for_operations",
- "parameters": [
- {
- "description": "操作ID",
- "in": "query",
- "name": "operation",
- "required": true,
- "schema": {
- "description": "操作ID",
- "title": "Operation",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "title": "Response Get Snapshot For Operations",
- "type": "boolean"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "检查操作快照是否存在",
- "tags": [
- "Snapshots"
- ]
- },
- "post": {
- "description": "为指定的操作创建快照",
- "operationId": "post_snapshot_for_operations",
- "parameters": [
- {
- "description": "操作ID",
- "in": "query",
- "name": "operation",
- "required": true,
- "schema": {
- "description": "操作ID",
- "title": "Operation",
- "type": "integer"
- }
- },
- {
- "description": "快照标签",
- "in": "query",
- "name": "tag",
- "required": true,
- "schema": {
- "description": "快照标签",
- "title": "Tag",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "201": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "为操作创建快照",
- "tags": [
- "Snapshots"
- ]
- }
- },
- "/api/v1/snapshots": {
- "get": {
- "description": "获取网络中的所有快照",
- "operationId": "get_snapshots",
- "parameters": [
- {
- "in": "query",
- "name": "limit",
- "required": false,
- "schema": {
- "default": 100,
- "maximum": 1000,
- "minimum": 1,
- "title": "Limit",
- "type": "integer"
- }
- },
- {
- "in": "query",
- "name": "offset",
- "required": false,
- "schema": {
- "default": 0,
- "minimum": 0,
- "title": "Offset",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Page_tuple_int__str__"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取快照列表",
- "tags": [
- "Snapshots"
- ]
- },
- "patch": {
- "description": "选择并恢复到指定的快照",
- "operationId": "patch_snapshots",
- "parameters": [
- {
- "description": "快照标签",
- "in": "query",
- "name": "tag",
- "required": true,
- "schema": {
- "description": "快照标签",
- "title": "Tag",
- "type": "string"
- }
- },
- {
- "description": "是否丢弃当前更改",
- "in": "query",
- "name": "discard",
- "required": false,
- "schema": {
- "default": false,
- "description": "是否丢弃当前更改",
- "title": "Discard",
- "type": "boolean"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "选择快照",
- "tags": [
- "Snapshots"
- ]
- },
- "post": {
- "description": "为网络创建一个快照",
- "operationId": "post_snapshots",
- "parameters": [
- {
- "description": "快照标签",
- "in": "query",
- "name": "tag",
- "required": true,
- "schema": {
- "description": "快照标签",
- "title": "Tag",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "201": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "创建快照",
- "tags": [
- "Snapshots"
- ]
- }
- },
- "/api/v1/snapshots/existence": {
- "get": {
- "description": "检查指定标签的快照是否存在",
- "operationId": "get_snapshots_existence",
- "parameters": [
- {
- "description": "快照标签",
- "in": "query",
- "name": "tag",
- "required": true,
- "schema": {
- "description": "快照标签",
- "title": "Tag",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "title": "Response Get Snapshots Existence",
- "type": "boolean"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "检查快照是否存在",
- "tags": [
- "Snapshots"
- ]
- }
- },
"/api/v1/sources": {
"delete": {
"description": "从网络中删除指定节点的水源",
@@ -38511,149 +30419,6 @@
]
}
},
- "/api/v1/sub-district-metering-areas": {
- "post": {
- "description": "为指定DMA生成子DMA分区",
- "operationId": "post_sub_district_metering_areas",
- "parameters": [
- {
- "description": "DMA ID",
- "in": "query",
- "name": "dma",
- "required": true,
- "schema": {
- "description": "DMA ID",
- "title": "Dma",
- "type": "string"
- }
- },
- {
- "description": "分区数量",
- "in": "query",
- "name": "part_count",
- "required": true,
- "schema": {
- "description": "分区数量",
- "exclusiveMinimum": 0,
- "title": "Part Count",
- "type": "integer"
- }
- },
- {
- "description": "分区类型",
- "in": "query",
- "name": "part_type",
- "required": true,
- "schema": {
- "description": "分区类型",
- "title": "Part Type",
- "type": "integer"
- }
- },
- {
- "description": "膨胀参数",
- "in": "query",
- "name": "inflate_delta",
- "required": true,
- "schema": {
- "description": "膨胀参数",
- "title": "Inflate Delta",
- "type": "number"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "生成DMA子分区",
- "tags": [
- "Regions & DMAs"
- ]
- }
- },
"/api/v1/tags": {
"get": {
"description": "获取指定水网中的所有标签信息",
@@ -42624,6 +34389,552 @@
]
}
},
+ "/api/v1/timeseries/analysis/runs/{run_id}/links/{link_id}": {
+ "get": {
+ "operationId": "get_timeseries_analysis_runs_run_id_links_link_id",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "run_id",
+ "required": true,
+ "schema": {
+ "format": "uuid",
+ "title": "Run Id",
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "link_id",
+ "required": true,
+ "schema": {
+ "title": "Link Id",
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "start_time",
+ "required": true,
+ "schema": {
+ "format": "date-time",
+ "title": "Start Time",
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "end_time",
+ "required": true,
+ "schema": {
+ "format": "date-time",
+ "title": "End Time",
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "field",
+ "required": true,
+ "schema": {
+ "title": "Field",
+ "type": "string"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Project-Id",
+ "required": true,
+ "schema": {
+ "title": "X-Project-Id",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/JsonValue"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Authentication required"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Insufficient permission"
+ },
+ "404": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource not found"
+ },
+ "409": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource conflict"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Validation error"
+ },
+ "503": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Dependency unavailable"
+ }
+ },
+ "security": [
+ {
+ "OAuth2PasswordBearer": []
+ }
+ ],
+ "summary": "Get Analysis Link Series",
+ "tags": [
+ "TimescaleDB - Analysis"
+ ]
+ }
+ },
+ "/api/v1/timeseries/analysis/runs/{run_id}/nodes/{node_id}": {
+ "get": {
+ "operationId": "get_timeseries_analysis_runs_run_id_nodes_node_id",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "run_id",
+ "required": true,
+ "schema": {
+ "format": "uuid",
+ "title": "Run Id",
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "node_id",
+ "required": true,
+ "schema": {
+ "title": "Node Id",
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "start_time",
+ "required": true,
+ "schema": {
+ "format": "date-time",
+ "title": "Start Time",
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "end_time",
+ "required": true,
+ "schema": {
+ "format": "date-time",
+ "title": "End Time",
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "field",
+ "required": true,
+ "schema": {
+ "title": "Field",
+ "type": "string"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Project-Id",
+ "required": true,
+ "schema": {
+ "title": "X-Project-Id",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/JsonValue"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Authentication required"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Insufficient permission"
+ },
+ "404": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource not found"
+ },
+ "409": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource conflict"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Validation error"
+ },
+ "503": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Dependency unavailable"
+ }
+ },
+ "security": [
+ {
+ "OAuth2PasswordBearer": []
+ }
+ ],
+ "summary": "Get Analysis Node Series",
+ "tags": [
+ "TimescaleDB - Analysis"
+ ]
+ }
+ },
+ "/api/v1/timeseries/analysis/runs/{run_id}/results": {
+ "post": {
+ "operationId": "post_timeseries_analysis_runs_run_id_results",
+ "parameters": [
+ {
+ "description": "分析运行 ID",
+ "in": "path",
+ "name": "run_id",
+ "required": true,
+ "schema": {
+ "description": "分析运行 ID",
+ "format": "uuid",
+ "title": "Run Id",
+ "type": "string"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Project-Id",
+ "required": true,
+ "schema": {
+ "title": "X-Project-Id",
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Payload",
+ "type": "object"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "201": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/JsonValue"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Authentication required"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Insufficient permission"
+ },
+ "404": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource not found"
+ },
+ "409": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource conflict"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Validation error"
+ },
+ "503": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Dependency unavailable"
+ }
+ },
+ "security": [
+ {
+ "OAuth2PasswordBearer": []
+ }
+ ],
+ "summary": "Store Analysis Results",
+ "tags": [
+ "TimescaleDB - Analysis"
+ ]
+ }
+ },
+ "/api/v1/timeseries/analysis/runs/{run_id}/values": {
+ "get": {
+ "operationId": "get_timeseries_analysis_runs_run_id_values",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "run_id",
+ "required": true,
+ "schema": {
+ "format": "uuid",
+ "title": "Run Id",
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "result_time",
+ "required": true,
+ "schema": {
+ "format": "date-time",
+ "title": "Result Time",
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "element_type",
+ "required": true,
+ "schema": {
+ "pattern": "^(node|link)$",
+ "title": "Element Type",
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "field",
+ "required": true,
+ "schema": {
+ "title": "Field",
+ "type": "string"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Project-Id",
+ "required": true,
+ "schema": {
+ "title": "X-Project-Id",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/JsonValue"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Authentication required"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Insufficient permission"
+ },
+ "404": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource not found"
+ },
+ "409": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Resource conflict"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Validation error"
+ },
+ "503": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ },
+ "description": "Dependency unavailable"
+ }
+ },
+ "security": [
+ {
+ "OAuth2PasswordBearer": []
+ }
+ ],
+ "summary": "Get Analysis Values At Time",
+ "tags": [
+ "TimescaleDB - Analysis"
+ ]
+ }
+ },
"/api/v1/timeseries/realtime/links": {
"delete": {
"description": "按时间范围删除实时管道数据。start_time 和 end_time 必须显式带时区;允许传 UTC+8,服务端按请求中的绝对时间删除对应 UTC 数据。",
@@ -44632,1751 +36943,6 @@
]
}
},
- "/api/v1/timeseries/schemes/links": {
- "delete": {
- "description": "删除指定方案和时间范围内的管道数据\n\n删除在指定方案和时间范围内的所有管道模拟数据。\n\nArgs:\n scheme_type: 方案类型\n scheme_name: 方案名称\n start_time: 删除开始时间\n end_time: 删除结束时间\n\nReturns:\n 删除结果信息",
- "operationId": "delete_timeseries_schemes_links",
- "parameters": [
- {
- "description": "方案类型",
- "in": "query",
- "name": "scheme_type",
- "required": true,
- "schema": {
- "description": "方案类型",
- "title": "Scheme Type",
- "type": "string"
- }
- },
- {
- "description": "方案名称",
- "in": "query",
- "name": "scheme_name",
- "required": true,
- "schema": {
- "description": "方案名称",
- "title": "Scheme Name",
- "type": "string"
- }
- },
- {
- "description": "删除开始时间",
- "in": "query",
- "name": "start_time",
- "required": true,
- "schema": {
- "description": "删除开始时间",
- "format": "date-time",
- "title": "Start Time",
- "type": "string"
- }
- },
- {
- "description": "删除结束时间",
- "in": "query",
- "name": "end_time",
- "required": true,
- "schema": {
- "description": "删除结束时间",
- "format": "date-time",
- "title": "End Time",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "204": {
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "删除方案管道数据",
- "tags": [
- "TimescaleDB - Scheme"
- ]
- },
- "get": {
- "description": "查询指定方案和时间范围内的管道数据\n\n根据方案和时间范围查询管道的模拟值。\n\nArgs:\n scheme_type: 方案类型\n scheme_name: 方案名称\n start_time: 查询开始时间\n end_time: 查询结束时间\n\nReturns:\n 方案管道数据列表",
- "operationId": "get_timeseries_schemes_links",
- "parameters": [
- {
- "description": "方案类型",
- "in": "query",
- "name": "scheme_type",
- "required": true,
- "schema": {
- "description": "方案类型",
- "title": "Scheme Type",
- "type": "string"
- }
- },
- {
- "description": "方案名称",
- "in": "query",
- "name": "scheme_name",
- "required": true,
- "schema": {
- "description": "方案名称",
- "title": "Scheme Name",
- "type": "string"
- }
- },
- {
- "description": "查询开始时间",
- "in": "query",
- "name": "start_time",
- "required": true,
- "schema": {
- "description": "查询开始时间",
- "format": "date-time",
- "title": "Start Time",
- "type": "string"
- }
- },
- {
- "description": "查询结束时间",
- "in": "query",
- "name": "end_time",
- "required": true,
- "schema": {
- "description": "查询结束时间",
- "format": "date-time",
- "title": "End Time",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "查询方案管道数据",
- "tags": [
- "TimescaleDB - Scheme"
- ]
- }
- },
- "/api/v1/timeseries/schemes/links/batches": {
- "post": {
- "description": "批量插入方案管道数据\n\n将特定方案的管道模拟数据批量插入时间序列数据库。\n\nArgs:\n data: 方案管道数据列表\n\nReturns:\n 插入成功的记录数",
- "operationId": "post_timeseries_schemes_links_batches",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "description": "方案管道数据列表",
- "items": {
- "type": "object"
- },
- "title": "Data",
- "type": "array"
- }
- }
- },
- "required": true
- },
- "responses": {
- "201": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "批量插入方案管道数据",
- "tags": [
- "TimescaleDB - Scheme"
- ]
- }
- },
- "/api/v1/timeseries/schemes/links/{link_id}/field": {
- "get": {
- "description": "查询指定方案管道的特定字段数据\n\n查询特定方案中指定管道在时间范围内的特定字段值。\n\nArgs:\n link_id: 管道ID\n scheme_type: 方案类型\n scheme_name: 方案名称\n start_time: 查询开始时间\n end_time: 查询结束时间\n field: 字段名称\n\nReturns:\n 字段数据列表\n\nRaises:\n HTTPException: 当查询参数无效时返回400错误",
- "operationId": "get_timeseries_schemes_links_link_id_field",
- "parameters": [
- {
- "description": "管道ID",
- "in": "path",
- "name": "link_id",
- "required": true,
- "schema": {
- "description": "管道ID",
- "title": "Link Id",
- "type": "string"
- }
- },
- {
- "description": "方案类型",
- "in": "query",
- "name": "scheme_type",
- "required": true,
- "schema": {
- "description": "方案类型",
- "title": "Scheme Type",
- "type": "string"
- }
- },
- {
- "description": "方案名称",
- "in": "query",
- "name": "scheme_name",
- "required": true,
- "schema": {
- "description": "方案名称",
- "title": "Scheme Name",
- "type": "string"
- }
- },
- {
- "description": "查询开始时间",
- "in": "query",
- "name": "start_time",
- "required": true,
- "schema": {
- "description": "查询开始时间",
- "format": "date-time",
- "title": "Start Time",
- "type": "string"
- }
- },
- {
- "description": "查询结束时间",
- "in": "query",
- "name": "end_time",
- "required": true,
- "schema": {
- "description": "查询结束时间",
- "format": "date-time",
- "title": "End Time",
- "type": "string"
- }
- },
- {
- "description": "要查询的字段名称",
- "in": "query",
- "name": "field",
- "required": true,
- "schema": {
- "description": "要查询的字段名称",
- "title": "Field",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "查询方案管道字段数据",
- "tags": [
- "TimescaleDB - Scheme"
- ]
- },
- "patch": {
- "description": "更新指定方案管道的字段值\n\n更新特定方案中指定管道在某个时间的字段数据。\n\nArgs:\n link_id: 管道ID\n scheme_type: 方案类型\n scheme_name: 方案名称\n time: 数据时间戳\n field: 字段名称\n value: 字段新值\n\nReturns:\n 更新结果信息\n\nRaises:\n HTTPException: 当字段不存在或更新失败时返回400错误",
- "operationId": "patch_timeseries_schemes_links_link_id_field",
- "parameters": [
- {
- "description": "管道ID",
- "in": "path",
- "name": "link_id",
- "required": true,
- "schema": {
- "description": "管道ID",
- "title": "Link Id",
- "type": "string"
- }
- },
- {
- "description": "方案类型",
- "in": "query",
- "name": "scheme_type",
- "required": true,
- "schema": {
- "description": "方案类型",
- "title": "Scheme Type",
- "type": "string"
- }
- },
- {
- "description": "方案名称",
- "in": "query",
- "name": "scheme_name",
- "required": true,
- "schema": {
- "description": "方案名称",
- "title": "Scheme Name",
- "type": "string"
- }
- },
- {
- "description": "更新数据的时间戳",
- "in": "query",
- "name": "time",
- "required": true,
- "schema": {
- "description": "更新数据的时间戳",
- "format": "date-time",
- "title": "Time",
- "type": "string"
- }
- },
- {
- "description": "要更新的字段名称",
- "in": "query",
- "name": "field",
- "required": true,
- "schema": {
- "description": "要更新的字段名称",
- "title": "Field",
- "type": "string"
- }
- },
- {
- "description": "更新的字段值",
- "in": "query",
- "name": "value",
- "required": true,
- "schema": {
- "description": "更新的字段值",
- "title": "Value",
- "type": "number"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "更新方案管道字段",
- "tags": [
- "TimescaleDB - Scheme"
- ]
- }
- },
- "/api/v1/timeseries/schemes/nodes": {
- "delete": {
- "description": "删除指定方案和时间范围内的节点数据\n\n删除在指定方案和时间范围内的所有节点模拟数据。\n\nArgs:\n scheme_type: 方案类型\n scheme_name: 方案名称\n start_time: 删除开始时间\n end_time: 删除结束时间\n\nReturns:\n 删除结果信息",
- "operationId": "delete_timeseries_schemes_nodes",
- "parameters": [
- {
- "description": "方案类型",
- "in": "query",
- "name": "scheme_type",
- "required": true,
- "schema": {
- "description": "方案类型",
- "title": "Scheme Type",
- "type": "string"
- }
- },
- {
- "description": "方案名称",
- "in": "query",
- "name": "scheme_name",
- "required": true,
- "schema": {
- "description": "方案名称",
- "title": "Scheme Name",
- "type": "string"
- }
- },
- {
- "description": "删除开始时间",
- "in": "query",
- "name": "start_time",
- "required": true,
- "schema": {
- "description": "删除开始时间",
- "format": "date-time",
- "title": "Start Time",
- "type": "string"
- }
- },
- {
- "description": "删除结束时间",
- "in": "query",
- "name": "end_time",
- "required": true,
- "schema": {
- "description": "删除结束时间",
- "format": "date-time",
- "title": "End Time",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "204": {
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "删除方案节点数据",
- "tags": [
- "TimescaleDB - Scheme"
- ]
- }
- },
- "/api/v1/timeseries/schemes/nodes/batches": {
- "post": {
- "description": "批量插入方案节点数据\n\n将特定方案的节点模拟数据批量插入时间序列数据库。\n\nArgs:\n data: 方案节点数据列表\n\nReturns:\n 插入成功的记录数",
- "operationId": "post_timeseries_schemes_nodes_batches",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "description": "方案节点数据列表",
- "items": {
- "type": "object"
- },
- "title": "Data",
- "type": "array"
- }
- }
- },
- "required": true
- },
- "responses": {
- "201": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "批量插入方案节点数据",
- "tags": [
- "TimescaleDB - Scheme"
- ]
- }
- },
- "/api/v1/timeseries/schemes/nodes/{node_id}/field": {
- "get": {
- "description": "查询指定方案节点的特定字段数据\n\n查询特定方案中指定节点在时间范围内的特定字段值。\n\nArgs:\n node_id: 节点ID\n scheme_type: 方案类型\n scheme_name: 方案名称\n start_time: 查询开始时间\n end_time: 查询结束时间\n field: 字段名称\n\nReturns:\n 字段数据列表\n\nRaises:\n HTTPException: 当查询参数无效时返回400错误",
- "operationId": "get_timeseries_schemes_nodes_node_id_field",
- "parameters": [
- {
- "description": "节点ID",
- "in": "path",
- "name": "node_id",
- "required": true,
- "schema": {
- "description": "节点ID",
- "title": "Node Id",
- "type": "string"
- }
- },
- {
- "description": "方案类型",
- "in": "query",
- "name": "scheme_type",
- "required": true,
- "schema": {
- "description": "方案类型",
- "title": "Scheme Type",
- "type": "string"
- }
- },
- {
- "description": "方案名称",
- "in": "query",
- "name": "scheme_name",
- "required": true,
- "schema": {
- "description": "方案名称",
- "title": "Scheme Name",
- "type": "string"
- }
- },
- {
- "description": "查询开始时间",
- "in": "query",
- "name": "start_time",
- "required": true,
- "schema": {
- "description": "查询开始时间",
- "format": "date-time",
- "title": "Start Time",
- "type": "string"
- }
- },
- {
- "description": "查询结束时间",
- "in": "query",
- "name": "end_time",
- "required": true,
- "schema": {
- "description": "查询结束时间",
- "format": "date-time",
- "title": "End Time",
- "type": "string"
- }
- },
- {
- "description": "要查询的字段名称",
- "in": "query",
- "name": "field",
- "required": true,
- "schema": {
- "description": "要查询的字段名称",
- "title": "Field",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "查询方案节点字段数据",
- "tags": [
- "TimescaleDB - Scheme"
- ]
- },
- "patch": {
- "description": "更新指定方案节点的字段值\n\n更新特定方案中指定节点在某个时间的字段数据。\n\nArgs:\n node_id: 节点ID\n scheme_type: 方案类型\n scheme_name: 方案名称\n time: 数据时间戳\n field: 字段名称\n value: 字段新值\n\nReturns:\n 更新结果信息\n\nRaises:\n HTTPException: 当字段不存在或更新失败时返回400错误",
- "operationId": "patch_timeseries_schemes_nodes_node_id_field",
- "parameters": [
- {
- "description": "节点ID",
- "in": "path",
- "name": "node_id",
- "required": true,
- "schema": {
- "description": "节点ID",
- "title": "Node Id",
- "type": "string"
- }
- },
- {
- "description": "方案类型",
- "in": "query",
- "name": "scheme_type",
- "required": true,
- "schema": {
- "description": "方案类型",
- "title": "Scheme Type",
- "type": "string"
- }
- },
- {
- "description": "方案名称",
- "in": "query",
- "name": "scheme_name",
- "required": true,
- "schema": {
- "description": "方案名称",
- "title": "Scheme Name",
- "type": "string"
- }
- },
- {
- "description": "更新数据的时间戳",
- "in": "query",
- "name": "time",
- "required": true,
- "schema": {
- "description": "更新数据的时间戳",
- "format": "date-time",
- "title": "Time",
- "type": "string"
- }
- },
- {
- "description": "要更新的字段名称",
- "in": "query",
- "name": "field",
- "required": true,
- "schema": {
- "description": "要更新的字段名称",
- "title": "Field",
- "type": "string"
- }
- },
- {
- "description": "更新的字段值",
- "in": "query",
- "name": "value",
- "required": true,
- "schema": {
- "description": "更新的字段值",
- "title": "Value",
- "type": "number"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "更新方案节点字段",
- "tags": [
- "TimescaleDB - Scheme"
- ]
- }
- },
- "/api/v1/timeseries/schemes/records": {
- "get": {
- "description": "按指定方案、时间和属性查询所有方案数据\n\n查询在特定方案和时间点,所有指定类型元素的特定属性值。\n\nArgs:\n scheme_type: 方案类型\n scheme_name: 方案名称\n query_time: 查询时间\n type: 元素类型(pipe或junction)\n property: 属性名称\n\nReturns:\n 查询结果列表\n\nRaises:\n HTTPException: 当查询参数无效时返回400错误",
- "operationId": "get_timeseries_schemes_records",
- "parameters": [
- {
- "description": "方案类型",
- "in": "query",
- "name": "scheme_type",
- "required": true,
- "schema": {
- "description": "方案类型",
- "title": "Scheme Type",
- "type": "string"
- }
- },
- {
- "description": "方案名称",
- "in": "query",
- "name": "scheme_name",
- "required": true,
- "schema": {
- "description": "方案名称",
- "title": "Scheme Name",
- "type": "string"
- }
- },
- {
- "description": "查询时间",
- "in": "query",
- "name": "query_time",
- "required": true,
- "schema": {
- "description": "查询时间",
- "title": "Query Time",
- "type": "string"
- }
- },
- {
- "description": "元素类型,pipe(管道)或 junction(节点)",
- "in": "query",
- "name": "type",
- "required": true,
- "schema": {
- "description": "元素类型,pipe(管道)或 junction(节点)",
- "title": "Type",
- "type": "string"
- }
- },
- {
- "description": "要查询的属性名称",
- "in": "query",
- "name": "property",
- "required": true,
- "schema": {
- "description": "要查询的属性名称",
- "title": "Property",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "按方案、时间和属性查询数据",
- "tags": [
- "TimescaleDB - Scheme"
- ]
- }
- },
- "/api/v1/timeseries/schemes/simulation-results": {
- "get": {
- "description": "按指定ID和时间查询方案模拟结果\n\n查询特定方案中的元素在某一时间点的模拟数据。\n\nArgs:\n scheme_type: 方案类型\n scheme_name: 方案名称\n id: 元素ID\n type: 元素类型(pipe或junction)\n query_time: 查询时间\n\nReturns:\n 模拟结果数据\n\nRaises:\n HTTPException: 当查询参数无效时返回400错误",
- "operationId": "get_timeseries_schemes_simulation_results",
- "parameters": [
- {
- "description": "方案类型",
- "in": "query",
- "name": "scheme_type",
- "required": true,
- "schema": {
- "description": "方案类型",
- "title": "Scheme Type",
- "type": "string"
- }
- },
- {
- "description": "方案名称",
- "in": "query",
- "name": "scheme_name",
- "required": true,
- "schema": {
- "description": "方案名称",
- "title": "Scheme Name",
- "type": "string"
- }
- },
- {
- "description": "元素ID(管道ID或节点ID)",
- "in": "query",
- "name": "id",
- "required": true,
- "schema": {
- "description": "元素ID(管道ID或节点ID)",
- "title": "Id",
- "type": "string"
- }
- },
- {
- "description": "元素类型,pipe(管道)或 junction(节点)",
- "in": "query",
- "name": "type",
- "required": true,
- "schema": {
- "description": "元素类型,pipe(管道)或 junction(节点)",
- "title": "Type",
- "type": "string"
- }
- },
- {
- "description": "查询时间",
- "in": "query",
- "name": "query_time",
- "required": true,
- "schema": {
- "description": "查询时间",
- "title": "Query Time",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "按ID和时间查询方案模拟数据",
- "tags": [
- "TimescaleDB - Scheme"
- ]
- },
- "post": {
- "description": "存储方案模拟结果到时间序列数据库\n\n将特定方案的节点和管道模拟计算结果批量存储到TimescaleDB数据库。\n\nArgs:\n scheme_type: 方案类型\n scheme_name: 方案名称\n node_result_list: 节点模拟结果列表\n link_result_list: 管道模拟结果列表\n result_start_time: 模拟结果对应的起始时间\n\nReturns:\n 存储结果信息",
- "operationId": "post_timeseries_schemes_simulation_results",
- "parameters": [
- {
- "description": "方案类型",
- "in": "query",
- "name": "scheme_type",
- "required": true,
- "schema": {
- "description": "方案类型",
- "title": "Scheme Type",
- "type": "string"
- }
- },
- {
- "description": "方案名称",
- "in": "query",
- "name": "scheme_name",
- "required": true,
- "schema": {
- "description": "方案名称",
- "title": "Scheme Name",
- "type": "string"
- }
- },
- {
- "description": "模拟结果开始时间",
- "in": "query",
- "name": "result_start_time",
- "required": true,
- "schema": {
- "description": "模拟结果开始时间",
- "title": "Result Start Time",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Body_post_timeseries_schemes_simulation_results"
- }
- }
- },
- "required": true
- },
- "responses": {
- "201": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "存储方案模拟结果",
- "tags": [
- "TimescaleDB - Scheme"
- ]
- }
- },
"/api/v1/timeseries/views/element-scada-readings": {
"get": {
"description": "获取link/node关联的SCADA监测值\n\n根据传入的link/node id,匹配SCADA信息,\n如果存在关联的SCADA device_id,获取实际的监测数据。\n\nArgs:\n element_id: 管网元素ID\n start_time: 查询开始时间\n end_time: 查询结束时间\n use_cleaned: 是否使用清洗后的数据,默认为False使用原始数据\n timescale_conn: TimescaleDB连接\n postgres_conn: PostgreSQL连接\n \nReturns:\n 管网元素关联的SCADA监测数据\n \nRaises:\n HTTPException: 当查询参数无效时返回400错误,未找到关联数据返回404错误",
@@ -46524,7 +37090,7 @@
},
"/api/v1/timeseries/views/element-simulations": {
"get": {
- "description": "获取link/node模拟值\n\n根据传入的featureInfos,找到关联的link/node,\n并根据对应的type,查询对应的模拟数据。支持查询实时或方案数据。\n\nArgs:\n start_time: 查询开始时间\n end_time: 查询结束时间\n feature_infos: 格式为 \"element_id1:type1,element_id2:type2\"\n 例如: \"P1:pipe,J1:junction\"\n scheme_type: 方案类型,若为空则查询实时数据\n scheme_name: 方案名称,若为空则查询实时数据\n timescale_conn: TimescaleDB连接\n \nReturns:\n 管网元素的模拟数据\n \nRaises:\n HTTPException: 当feature_infos为空返回400错误,未找到数据返回404错误,其他错误返回400错误",
+ "description": "获取link/node模拟值\n\n根据传入的featureInfos,找到关联的link/node,\n并根据对应的type,查询对应的模拟数据。支持查询实时或分析运行数据。\n\nArgs:\n start_time: 查询开始时间\n end_time: 查询结束时间\n feature_infos: 格式为 \"element_id1:type1,element_id2:type2\"\n 例如: \"P1:pipe,J1:junction\"\n run_id: 分析运行 ID,若为空则查询实时数据\n timescale_conn: TimescaleDB连接\n \nReturns:\n 管网元素的模拟数据\n \nRaises:\n HTTPException: 当feature_infos为空返回400错误,未找到数据返回404错误,其他错误返回400错误",
"operationId": "get_timeseries_views_element_simulations",
"parameters": [
{
@@ -46563,25 +37129,22 @@
}
},
{
- "description": "方案类型,若为空则查询实时数据",
+ "description": "分析运行 ID;为空时查询实时数据",
"in": "query",
- "name": "scheme_type",
+ "name": "run_id",
"required": false,
"schema": {
- "description": "方案类型,若为空则查询实时数据",
- "title": "Scheme Type",
- "type": "string"
- }
- },
- {
- "description": "方案名称,若为空则查询实时数据",
- "in": "query",
- "name": "scheme_name",
- "required": false,
- "schema": {
- "description": "方案名称,若为空则查询实时数据",
- "title": "Scheme Name",
- "type": "string"
+ "anyOf": [
+ {
+ "format": "uuid",
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "分析运行 ID;为空时查询实时数据",
+ "title": "Run Id"
}
},
{
@@ -46679,7 +37242,7 @@
},
"/api/v1/timeseries/views/scada-simulations": {
"get": {
- "description": "获取SCADA关联的link/node模拟值\n\n根据传入的SCADA device_ids,找到关联的link/node,\n并根据对应的type,查询对应的模拟数据。支持查询实时或方案数据。\n\nArgs:\n start_time: 查询开始时间\n end_time: 查询结束时间\n device_ids: SCADA设备ID列表,用逗号分隔\n scheme_type: 方案类型,若为空则查询实时数据\n scheme_name: 方案名称,若为空则查询实时数据\n timescale_conn: TimescaleDB连接\n postgres_conn: PostgreSQL连接\n \nReturns:\n SCADA关联的模拟数据\n \nRaises:\n HTTPException: 当查询参数无效时返回400错误,未找到数据时返回404错误",
+ "description": "获取SCADA关联的link/node模拟值\n\n根据传入的SCADA device_ids,找到关联的link/node,\n并根据对应的type,查询对应的模拟数据。支持查询实时或分析运行数据。\n\nArgs:\n start_time: 查询开始时间\n end_time: 查询结束时间\n device_ids: SCADA设备ID列表,用逗号分隔\n run_id: 分析运行 ID,若为空则查询实时数据\n timescale_conn: TimescaleDB连接\n postgres_conn: PostgreSQL连接\n \nReturns:\n SCADA关联的模拟数据\n \nRaises:\n HTTPException: 当查询参数无效时返回400错误,未找到数据时返回404错误",
"operationId": "get_timeseries_views_scada_simulations",
"parameters": [
{
@@ -46718,25 +37281,22 @@
}
},
{
- "description": "方案类型,若为空则查询实时数据",
+ "description": "分析运行 ID;为空时查询实时数据",
"in": "query",
- "name": "scheme_type",
+ "name": "run_id",
"required": false,
"schema": {
- "description": "方案类型,若为空则查询实时数据",
- "title": "Scheme Type",
- "type": "string"
- }
- },
- {
- "description": "方案名称,若为空则查询实时数据",
- "in": "query",
- "name": "scheme_name",
- "required": false,
- "schema": {
- "description": "方案名称,若为空则查询实时数据",
- "title": "Scheme Name",
- "type": "string"
+ "anyOf": [
+ {
+ "format": "uuid",
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "分析运行 ID;为空时查询实时数据",
+ "title": "Run Id"
}
},
{
@@ -47129,104 +37689,6 @@
]
}
},
- "/api/v1/undos": {
- "post": {
- "description": "撤销网络上最后的一个操作",
- "operationId": "post_undos",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "撤销操作",
- "tags": [
- "Snapshots"
- ]
- }
- },
"/api/v1/valve-closure-analyses": {
"post": {
"description": "高级版本的阀门关闭分析,支持同时关闭多个阀门,并在指定持续时间内进行模拟。返回纯文本格式的分析结果。",
@@ -49522,744 +39984,6 @@
]
}
},
- "/api/v1/virtual-district-calculations": {
- "post": {
- "description": "根据指定的压力监测节点作为中心节点计算虚拟分区方案",
- "operationId": "post_virtual_district_calculations",
- "parameters": [
- {
- "description": "压力监测节点ID列表",
- "in": "query",
- "name": "centers",
- "required": true,
- "schema": {
- "description": "压力监测节点ID列表",
- "items": {
- "type": "string"
- },
- "title": "Centers",
- "type": "array"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "additionalProperties": {
- "items": {},
- "type": "array"
- },
- "title": "Response Post Virtual District Calculations",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "计算虚拟分区",
- "tags": [
- "Regions & DMAs"
- ]
- }
- },
- "/api/v1/virtual-district-generation-runs": {
- "post": {
- "description": "根据参数自动生成虚拟分区方案",
- "operationId": "post_virtual_district_generation_runs",
- "parameters": [
- {
- "description": "膨胀参数",
- "in": "query",
- "name": "inflate_delta",
- "required": true,
- "schema": {
- "description": "膨胀参数",
- "title": "Inflate Delta",
- "type": "number"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "生成虚拟分区",
- "tags": [
- "Regions & DMAs"
- ]
- }
- },
- "/api/v1/virtual-districts": {
- "delete": {
- "description": "删除指定的虚拟分区",
- "operationId": "delete_virtual_districts",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "204": {
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "删除虚拟分区",
- "tags": [
- "Regions & DMAs"
- ]
- },
- "get": {
- "description": "获取指定水网中的所有虚拟分区信息",
- "operationId": "get_virtual_districts",
- "parameters": [
- {
- "in": "query",
- "name": "limit",
- "required": false,
- "schema": {
- "default": 100,
- "maximum": 1000,
- "minimum": 1,
- "title": "Limit",
- "type": "integer"
- }
- },
- {
- "in": "query",
- "name": "offset",
- "required": false,
- "schema": {
- "default": 0,
- "minimum": 0,
- "title": "Offset",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Page_dict_str__Any__"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取所有虚拟分区",
- "tags": [
- "Regions & DMAs"
- ]
- },
- "patch": {
- "description": "修改指定虚拟分区的属性信息",
- "operationId": "patch_virtual_districts",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "设置虚拟分区属性",
- "tags": [
- "Regions & DMAs"
- ]
- },
- "post": {
- "description": "向水网添加一个新的虚拟分区",
- "operationId": "post_virtual_districts",
- "parameters": [
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "201": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "添加新虚拟分区",
- "tags": [
- "Regions & DMAs"
- ]
- }
- },
- "/api/v1/virtual-districts/detail": {
- "get": {
- "description": "获取指定ID的虚拟分区详细信息",
- "operationId": "get_virtual_districts_detail",
- "parameters": [
- {
- "description": "虚拟分区ID",
- "in": "query",
- "name": "id",
- "required": true,
- "schema": {
- "description": "虚拟分区ID",
- "title": "Id",
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "title": "Response Get Virtual Districts Detail",
- "type": "object"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "获取虚拟分区信息",
- "tags": [
- "Regions & DMAs"
- ]
- }
- },
"/api/v1/visual-elements": {
"delete": {
"description": "从网络中删除指定的图形元素",
@@ -51002,115 +40726,6 @@
"Web Search"
]
}
- },
- "/api/v1/with-servers": {
- "post": {
- "description": "将网络与服务器同步到指定操作",
- "operationId": "post_with_servers",
- "parameters": [
- {
- "description": "目标操作ID",
- "in": "query",
- "name": "operation",
- "required": true,
- "schema": {
- "description": "目标操作ID",
- "title": "Operation",
- "type": "integer"
- }
- },
- {
- "in": "header",
- "name": "X-Project-Id",
- "required": true,
- "schema": {
- "title": "X-Project-Id",
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/JsonValue"
- }
- }
- },
- "description": "Successful Response"
- },
- "401": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Authentication required"
- },
- "403": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Insufficient permission"
- },
- "404": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource not found"
- },
- "409": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Resource conflict"
- },
- "422": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Validation error"
- },
- "503": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProblemDetails"
- }
- }
- },
- "description": "Dependency unavailable"
- }
- },
- "security": [
- {
- "OAuth2PasswordBearer": []
- }
- ],
- "summary": "与服务器同步",
- "tags": [
- "Snapshots"
- ]
- }
}
}
}
diff --git a/resources/db_v2/DATABASE_ARCHITECTURE.md b/resources/db_v2/DATABASE_ARCHITECTURE.md
new file mode 100644
index 0000000..5bb3e35
--- /dev/null
+++ b/resources/db_v2/DATABASE_ARCHITECTURE.md
@@ -0,0 +1,454 @@
+# TJWater 数据库改造说明与当前结构
+
+> 本文记录 2026-08-25 的数据库实际状态。结构、约束、行数、TimescaleDB chunk 和策略均直接读取数据库,不以仓库中的 SQL 脚本为依据。文中不包含主机、端口、账号、密码或 DSN。
+
+## 改造范围与当前状态
+
+本次改造保留原 `tjwater` 业务库和时序库,新建 `tjwater_next` 作为隔离验证环境。元数据库仍为 `system_hub`,新项目通过 `biz_data` 和 `iot_data` 两条路由分别关联新业务库与新时序库。项目完成迁移和联调后已切换为 `active`,原数据库没有被覆盖,仍可用于对照和回退。
+
+已经完成的数据库修改包括:
+
+- 新建并迁移 `tjwater_next` 业务库,将旧 `public` 中混合存放的管网、GIS、SCADA 配置和分析数据按领域拆分。
+- 新建并迁移 `tjwater_next` 时序库,将 SCADA、实时计算和分析计算结果分开存放。
+- `realtime` 采用冷热数据策略,72 小时后的 chunk 自动转为有序列存。
+- `analysis` 按 `stored_at` 分区,入库满 24 小时的 chunk 自动转为有序列存。
+- GIS 查询层改用物化视图,当前 7 张物化视图均已填充。
+- GeoServer 已建立 `tjwater_next` 工作空间和同名数据存储,从业务库 `gis` schema 发布 7 个图层。GeoWebCache 的服务端与客户端缓存有效期均为 300 秒。
+- 旧库中的 `operation`、`current_operation`、`batch_operation`、`operation_table`、`restore_operation` 和 `snapshot_operation` 没有进入新业务库。
+- `system_hub.public` 补充了项目数据库外键、数据库路由约束、连接池约束、必要的非空约束,以及 5 张表和 44 个字段的中文数据库注释。
+- 用户角色和项目角色仍是可扩展字符串,没有增加枚举检查约束。
+- `audit_logs.user_id` 和 `audit_logs.project_id` 仍为逻辑关联,没有增加外键。
+- 业务库 48 个表或物化视图、192 个字段,以及时序库 7 张表、47 个字段均已写入中文数据库注释。
+- 后端已对接新 schema。WNDB、PostgreSQL 管理连接和同步 TimescaleDB 访问均使用有界连接池,闲置项目按最近使用顺序回收,实时覆盖写入使用单一事务。
+- 后端批量元素查询读取 GIS 物化视图,模型增删改和 INP 导入提交后执行并发刷新;批量事务只刷新一次。
+- `pattern_values`、`pattern_flow_samples`、`curve_points`、`demands` 和 `link_vertices` 的顺序号按所属父对象编号,主键已改为父对象 ID 与 `sequence_no` 的复合键。
+
+`tjwater_next` 当前为 `active`。排水项目 `lingang` 已迁入 `system_hub.public`,供水和排水后端共用同一套项目、成员、数据库路由和审计表。
+
+## 数据库总体关系
+
+```mermaid
+flowchart LR
+ subgraph META["system_hub 元数据库"]
+ MP["public
统一元数据
5 个项目"]
+ end
+
+ subgraph OLD["原数据库,保持不变"]
+ OB["tjwater 业务库
public"]
+ OT["tjwater 时序库
scada / realtime / scheme"]
+ end
+
+ subgraph NEXT["隔离验证数据库"]
+ NB["tjwater_next 业务库
network / gis / asset / analysis"]
+ NT["tjwater_next 时序库
scada / realtime / analysis"]
+ end
+
+ GS["GeoServer
tjwater_next 工作空间"]
+ WEB["供水前端
tjwater_next 图层配置"]
+
+ MP -->|"tjwater 的 biz_data"| OB
+ MP -->|"tjwater 的 iot_data"| OT
+ MP -->|"tjwater_next 的 biz_data"| NB
+ MP -->|"tjwater_next 的 iot_data"| NT
+ MP -->|"lingang 的两条数据库路由"| DRAIN["排水项目数据库"]
+ NB -->|"gis 物化视图"| GS -->|"WFS / WMTS"| WEB
+```
+
+一个项目对应一个业务数据库和一个时序数据库。项目本地表不保存 `project_id`,项目边界由元数据库路由和数据库连接共同确定。
+
+## 元数据库 system_hub
+
+### public:当前主元数据
+
+`public` 当前有 5 个项目、1 个用户、5 条成员关系和 10 条数据库路由。5 个项目均配置了一条 `biz_data` 和一条 `iot_data` 路由。审计日志数量会随接口请求持续增加,不在文档中固化行数。
+
+| 表 | 用途 | 主要字段 |
+| --- | --- | --- |
+| `users` | Keycloak 用户身份快照和系统授权状态 | `id`、`keycloak_id`、`username`、`email`、`role`、`is_active`、`is_superuser`、`attributes`、时间字段 |
+| `projects` | 项目基本信息和地图配置 | `id`、`name`、`code`、`gs_workspace`、`map_extent`、`map_config`、`status`、时间字段 |
+| `user_project_membership` | 用户和项目的成员关系 | `id`、`user_id`、`project_id`、`project_role` |
+| `project_databases` | 项目业务库和时序库路由 | `id`、`project_id`、`db_role`、`db_type`、`dsn_encrypted`、`pool_min_size`、`pool_max_size` |
+| `audit_logs` | 独立保留的操作审计 | `id`、`user_id`、`project_id`、`action`、资源字段、请求字段、`response_status`、`timestamp` |
+
+```mermaid
+erDiagram
+ USERS {
+ uuid id PK
+ uuid keycloak_id UK
+ varchar username UK
+ varchar email UK
+ varchar role
+ boolean is_active
+ boolean is_superuser
+ }
+
+ PROJECTS {
+ uuid id PK
+ varchar code UK
+ varchar name
+ varchar gs_workspace UK
+ jsonb map_extent
+ jsonb map_config
+ varchar status
+ }
+
+ USER_PROJECT_MEMBERSHIP {
+ uuid id PK
+ uuid user_id FK
+ uuid project_id FK
+ varchar project_role
+ }
+
+ PROJECT_DATABASES {
+ uuid id PK
+ uuid project_id FK
+ varchar db_role
+ varchar db_type
+ text dsn_encrypted
+ int pool_min_size
+ int pool_max_size
+ }
+
+ AUDIT_LOGS {
+ uuid id PK
+ uuid user_id
+ uuid project_id
+ varchar action
+ timestamptz timestamp
+ }
+
+ USERS ||--o{ USER_PROJECT_MEMBERSHIP : "参与项目"
+ PROJECTS ||--o{ USER_PROJECT_MEMBERSHIP : "包含成员"
+ PROJECTS ||--o{ PROJECT_DATABASES : "配置路由"
+```
+
+数据库强制执行以下关系和约束:
+
+- 成员表的 `user_id`、`project_id` 分别引用用户和项目,删除用户或项目时级联删除成员关系。
+- `project_databases.project_id` 引用项目,删除项目时级联删除数据库路由。
+- 同一项目的 `db_role` 唯一。
+- `biz_data` 必须使用 `postgresql`,`iot_data` 必须使用 `timescaledb`。
+- `pool_min_size` 不小于 1,`pool_max_size` 不小于 `pool_min_size`。
+- 项目状态限定为 `active`、`inactive` 或 `archived`。
+- 审计表中的用户和项目 ID 不设置外键,删除业务对象不会连带删除历史日志。
+
+### 排水元数据合并
+
+排水后端原先使用独立的 `hub` schema,其中有 1 个 `lingang` 项目、2 条数据库路由和 203 条审计记录,没有用户或成员关系。项目 UUID 保持不变,两条路由转换为 `public.project_databases` 使用的 Fernet 加密格式,连接池参数由 `pool_size + max_overflow` 映射为 `pool_min_size + pool_max_size`。原审计记录已迁入 `public.audit_logs`。
+
+排水后端已改用 `public.projects`、`public.project_databases`、`public.user_project_membership`、`public.users` 和 `public.audit_logs`。供水和排水服务读取同一份项目状态、Keycloak 身份、项目权限及数据库路由。原 `hub` schema 已在迁移校验和连接测试通过后删除。
+
+## 原业务库 tjwater
+
+原业务库将大部分业务对象放在 `public`,当前有 68 张表、9 张视图和 2 张物化视图。管网模型、GIS、SCADA 配置、分析结果、方案数据、临时表和操作记录混在同一命名空间中。
+
+其中还安装了 `postgis_tiger_geocoder` 和 `postgis_topology`,因此存在 `tiger` 和 `topology` 扩展 schema。新业务库只保留当前实际使用的 PostGIS 能力,没有继续安装这两个扩展。
+
+## 相对原数据库的结构变化
+
+以下对比以当前仍保留的原业务库 `tjwater`、原时序库 `tjwater` 与新库 `tjwater_next` 的实际对象为准。对象名称的对应关系表示业务实体或数据职责的迁移方向,不表示所有字段均一对一复制。
+
+### 归并和调整
+
+| 原库对象或职责 | 新库对象或职责 | 调整内容 |
+| --- | --- | --- |
+| `_node`、`junctions`、`reservoirs`、`tanks` | `network.nodes` 及节点类型子表 | 节点统一由主表管理,类型专有字段保留在共享主键子表。 |
+| `_link`、`pipes`、`pumps`、`valves` | `network.links` 及连接类型子表 | 连接统一记录端点和类型,管道、泵、阀门参数移入对应子表。 |
+| `coordinates`、`vertices` | `gis.node_geometries`、`gis.link_vertices` | 管网几何从模型参数中拆出,分别保存节点位置和连接折点。 |
+| 旧的 GIS 视图和物化视图 | `gis` 下 7 张物化视图 | 前端查询层统一为节点、连接和设备的物化视图,底层仍读取 `network`、`gis`、`asset` 的规范化表。 |
+| SCADA 设备配置表 | `asset.scada_devices` | 设备配置集中到资产域,并以外键关联一个节点或一条连接。当前已迁入 118 台设备。 |
+| `scheme_list` 及方案结果相关表 | `analysis.runs`、`analysis.results` | 运行批次与非时序摘要结果留在业务库,逐时逐元素结果迁入时序库。当前已迁入 124 次运行和 17 条非时序结果。 |
+| `scada.scada_data` | `scada.measurements` | SCADA 测量值迁入新时序库并按设备和时间保存。 |
+| `realtime.node_simulation`、`realtime.link_simulation` | `realtime.node_results`、`realtime.link_results` | 实时仿真结果保留为节点和连接两类时序数据。 |
+| `scheme.node_simulation`、`scheme.link_simulation` | `analysis.node_results`、`analysis.link_results` | 方案或分析的元素时序结果以 `run_id` 区分批次,和业务库中的 `analysis.runs` 形成逻辑关联。 |
+
+### 已删减且未迁入的对象
+
+- 云端操作记录相关的 `operation`、`current_operation`、`batch_operation`、`operation_table`、`restore_operation`、`snapshot_operation` 未进入新业务库。该设计不再作为业务数据模型的一部分。
+- 临时处理表 `temp_link_1`、`temp_link_2`、`temp_node`、`temp_region`、`temp_vd_topology` 未迁入。它们属于历史处理过程的中间对象,不应成为长期库结构。
+- 原库中的 `_node`、`_link`、`_pattern`、`_curve`、`_region` 等内部或过渡表不再单独存在。新库以明确的领域表和外键关系表达同一类数据。
+- 原库的 `tiger`、`topology` 扩展 schema 未在新库安装,`tjwater_next` 仅保留 PostGIS 及其 `public` 系统对象。
+
+### 尚未完整承接的范围
+
+原库有 `region`、`region_dma`、`region_sa`、`region_vd`、`region_wda` 五张区域细分表。检查时这些表均无数据,因此当前新库只建立了通用的 `gis.regions`、`gis.region_nodes`,没有为 DMA、分区计量、分区调度或用水分区固化专用结构。
+
+这不是删除已有业务数据,而是暂缓固化尚未使用的模型。后续确认 DMA 和 `VA` 的业务含义后,再决定是在 `gis` 中增加区域类型专有表,还是放入独立的业务 schema。当前库中没有名为 `va` 的表;若该名称指阀门,则对应 `network.valves`,若指 `region_vd`,则属于上述尚未迁入的区域细分模型。
+
+### 新增的结构能力
+
+- 业务库由单一 `public` 命名空间拆为 `network`、`gis`、`asset`、`analysis` 四个领域 schema,降低模型、空间数据、设备配置和分析记录之间的耦合。
+- `gis` 增加了面向前端的 7 张物化视图,以及 `gis.refresh_all_materialized_views` 刷新过程。后端在模型批量修改和 INP 导入提交后统一刷新,不使用数据库触发器或定时任务。
+- 新时序库增加 `migration` schema,用于保留迁移过程的设置和日志,不与业务时序数据混放。
+- `realtime` 两张 hypertable 已启用 72 小时后的列存压缩策略,`analysis` 两张 hypertable 按入库时间执行 24 小时冷热转换。`scada` 保持独立行存。
+- 元数据库的 `public.project_databases` 增加项目外键、数据库角色和类型约束,以及连接池上下限约束,用于保证每个项目的业务库和时序库路由有效。
+
+## 新业务库 tjwater_next
+
+新业务库使用 PostgreSQL 和 PostGIS。业务对象分布在 4 个 schema 中,`public` 只保留 PostGIS 提供的系统对象。
+
+| Schema | 普通表 | 物化视图 | 用途 |
+| --- | ---: | ---: | --- |
+| `network` | 32 | 0 | 供水管网模型、仿真参数、规则和曲线 |
+| `gis` | 6 | 7 | 原始空间数据、区域关系和前端查询层 |
+| `asset` | 1 | 0 | SCADA 设备配置及其管网关联 |
+| `analysis` | 2 | 0 | 分析运行记录和非时序结果 |
+
+当前迁移数据包含 87,907 个节点、91,054 条连接、118 个 SCADA 设备、124 次分析运行和 17 条非时序分析结果。其中有 87,894 个普通节点、13 个水源、91,052 条管道和 2 个阀门。
+
+### network:管网模型
+
+`network.nodes` 和 `network.links` 是节点、连接的统一主表。具体类型通过共享主键的一对一子表扩展:
+
+- 节点:`junctions`、`reservoirs`、`tanks`。
+- 连接:`pipes`、`pumps`、`valves`。
+- 模式和曲线:`patterns`、`pattern_values`、`pattern_flow_samples`、`curves`、`curve_points`。
+- 节点附属数据:`demands`、`emitters`、`sources`、`initial_quality`、`tank_mixing`、`node_tags`。
+- 连接附属数据:`link_initial_settings`、`link_tags`、`pump_energy_settings`、`pipe_reaction_coefficients`、`tank_reaction_coefficients`。
+- 模型配置:`controls`、`rules`、`simulation_settings`、`time_settings`、`report_settings`、`energy_settings`、`reaction_settings`、`model_titles`。
+
+```mermaid
+erDiagram
+ NODES {
+ text id PK
+ text node_type
+ }
+ JUNCTIONS {
+ text node_id PK,FK
+ float elevation
+ }
+ RESERVOIRS {
+ text node_id PK,FK
+ float head
+ text pattern_id FK
+ }
+ TANKS {
+ text node_id PK,FK
+ float elevation
+ float initial_level
+ text volume_curve_id FK
+ }
+ LINKS {
+ text id PK
+ text link_type
+ text start_node_id FK
+ text end_node_id FK
+ }
+ PIPES {
+ text link_id PK,FK
+ float length
+ float diameter
+ text status
+ }
+ PUMPS {
+ text link_id PK,FK
+ float power
+ text head_curve_id FK
+ text pattern_id FK
+ }
+ VALVES {
+ text link_id PK,FK
+ float diameter
+ text valve_type
+ text setting
+ }
+ PATTERNS {
+ text id PK
+ }
+ CURVES {
+ text id PK
+ text curve_type
+ }
+
+ NODES ||--o| JUNCTIONS : "节点类型"
+ NODES ||--o| RESERVOIRS : "节点类型"
+ NODES ||--o| TANKS : "节点类型"
+ NODES ||--o{ LINKS : "起点"
+ NODES ||--o{ LINKS : "终点"
+ LINKS ||--o| PIPES : "连接类型"
+ LINKS ||--o| PUMPS : "连接类型"
+ LINKS ||--o| VALVES : "连接类型"
+ PATTERNS ||--o{ RESERVOIRS : "水位模式"
+ PATTERNS ||--o{ PUMPS : "运行模式"
+ CURVES ||--o{ PUMPS : "扬程曲线"
+ CURVES ||--o{ TANKS : "容积曲线"
+```
+
+删除节点或连接时,对应类型子表、GIS 几何、标签和关联参数按外键规则同步删除。连接的起点和终点必须引用已有节点,且不能是同一个节点。
+
+`pattern_values`、`pattern_flow_samples` 和 `curve_points` 的 `sequence_no` 分别表示同一模式或曲线内部的顺序,`demands.sequence_no` 表示同一节点下多条需水记录的顺序。因此,这四张明细表使用父对象 ID 与 `sequence_no` 组成主键,不要求顺序号在全表唯一。该约束与 WNDB 的局部编号、INP 导入和按父对象排序查询一致。
+
+### gis:空间数据和查询层
+
+基础空间表包括:
+
+| 表 | 用途 | 主要关系 |
+| --- | --- | --- |
+| `node_geometries` | 节点点位 | `node_id` 引用 `network.nodes` |
+| `link_vertices` | 连接中间折点 | `link_id` 引用 `network.links` |
+| `labels` | 地图标注 | 可选关联节点 |
+| `backdrops` | 模型背景配置 | 单条配置记录 |
+| `regions` | 区域边界 | 保存区域类型和面几何 |
+| `region_nodes` | 区域和节点的多对多关系 | 同时引用区域和节点 |
+
+`link_vertices` 使用 `(link_id, sequence_no)` 复合主键。折点顺序只在同一条连接内部有效,不同连接可以从相同的顺序号开始编号。
+
+前端查询使用 7 张物化视图:`junctions`、`reservoirs`、`tanks`、`pipes`、`pumps`、`valves` 和 `scada_devices`。这些视图把模型属性和几何合并,避免前端与 GeoServer 每次重复执行跨表计算。当前 7 张视图均已填充,并为元素 ID 建立唯一索引,为几何建立 GiST 索引。
+
+原始模型几何继续使用项目坐标系 `EPSG:900914`。点要素物化视图中的 `x`、`y` 仍保存该坐标系下的模型坐标,供 WNDB 和 INP 读写使用;提供给 GeoServer 的 `geom` 统一转换为 `EPSG:3857`。管道发布为线,水泵和阀门使用连接线中点发布为点,SCADA 设备按设备自有位置、关联节点位置或关联连接中点依次取值。当前数据库中的 7 个 `geom` 字段均已核对为 `EPSG:3857`。
+
+数据库中的 `gis.refresh_all_materialized_views(boolean)` 存储过程统一刷新全部物化视图。后端批量读取普通节点、水源、水箱、管道、水泵、阀门和 SCADA 设备时直接查询这些视图;单条模型增删改提交后立即刷新,批量修改和 INP 导入成功提交后只刷新一次。实际库中一次全量并发刷新约 6 秒。视图已有数据和唯一索引,因此刷新期间查询仍可继续。
+
+```mermaid
+flowchart LR
+ N["network 节点和连接"] --> G["gis 原始几何"]
+ A["asset.scada_devices"] --> G
+ N --> MV["gis 物化视图"]
+ G --> MV
+ A --> MV
+ MV --> GS["GeoServer tjwater_next"]
+ GS --> WFS["WFS 要素查询"]
+ GS --> GWC["GeoWebCache WMTS"]
+ WFS --> F["供水前端"]
+ GWC --> F
+ R["refresh_all_materialized_views"] --> MV
+```
+
+### GeoServer 与前端图层
+
+`system_hub.public.projects` 中的 `tjwater_next` 项目已配置 `gs_workspace=tjwater_next`,当前状态为 `active`。GeoServer 的 `tjwater_next` 数据存储连接同名业务库并限定到 `gis` schema,图层名称直接采用物化视图名称。7 个图层使用相同的项目管网发布边界,空图层和视口内没有要素的瓦片会返回空 MVT,不会产生越界错误。
+
+| 前端数据源 | GeoServer 图层 | 几何 | 当前要素数 |
+| --- | --- | --- | ---: |
+| `junctions` | `tjwater_next:junctions` | Point,`EPSG:3857` | 87,894 |
+| `reservoirs` | `tjwater_next:reservoirs` | Point,`EPSG:3857` | 13 |
+| `tanks` | `tjwater_next:tanks` | Point,`EPSG:3857` | 0 |
+| `pipes` | `tjwater_next:pipes` | LineString,`EPSG:3857` | 91,052 |
+| `pumps` | `tjwater_next:pumps` | Point,`EPSG:3857` | 0 |
+| `valves` | `tjwater_next:valves` | Point,`EPSG:3857` | 2 |
+| `scada` | `tjwater_next:scada_devices` | Point,`EPSG:3857` | 118 |
+
+供水前端的 `refactor/tjwater-next-integration` 分支已切换到这些图层和新字段名。地图切片走 `WebMercatorQuad` 的 MVT,定位和详情查询走 WFS。旧 `geo_pipes_mat`、`geo_junctions_mat` 和其他 `geo_*` 图层名不再作为新前端的兼容别名。
+
+方案查询统一读取 `/api/v1/analysis/runs`,详情和时序结果按 UUID `run_id` 关联。时间轴读取 `/api/v1/timeseries/analysis/runs/{run_id}/values`,爆管定位的模拟数据源也传递 `simulation_run_id`。监测点优化使用 `/api/v1/sensor-placement-runs`。前端不再调用旧的 `/schemes`、`/timeseries/schemes` 和 `/sensor-placement-schemes` 接口。
+
+模型修改提交后,后端先调用 `gis.refresh_all_materialized_views(boolean)` 刷新数据库查询层。GeoWebCache 不会感知 PostgreSQL 物化视图刷新,因此 7 个新图层配置了 300 秒的服务端和客户端缓存有效期,前端最迟在 5 分钟后读到新瓦片。部署或批量迁移完成后仍可执行一次图层缓存清空,避免等待已有瓦片自然过期。
+
+### asset:SCADA 设备配置
+
+`asset.scada_devices` 保存设备类型、采集接口标识、传输模式、频率、可靠性和可选几何。每台设备必须关联一个节点或一条连接,不能同时关联两者。设备的历史测量值不放在业务库,保存在时序库 `scada.measurements`。
+
+### analysis:分析运行和非时序结果
+
+`analysis.runs` 表示一次实际执行,保存运行名称、类型、创建人、开始时间、状态和参数。当前没有单独的 `scenarios` 模板表。
+
+每次执行都会创建新的 `run_id`,名称和业务时间相同也不会覆盖旧结果。扩展仿真开始写结果前,业务库先记录 `running`;时序库写入完成后更新为 `completed`,写入失败则保留为 `failed`。业务库和时序库据此共用同一个执行标识。
+
+`analysis.results` 保存不适合放入时序表的结果摘要或结构化业务结果。每条结果必须属于一个运行,可以关联一个节点、一条连接,或者不关联任何管网元素。元素级时序结果保存在时序库,某次分析只有摘要结果、只有元素时序结果或两者都有都是合法状态。
+
+```mermaid
+erDiagram
+ RUNS {
+ uuid run_id PK
+ text name
+ text run_type
+ text created_by
+ timestamptz started_at
+ text status
+ jsonb parameters
+ }
+ RESULTS {
+ uuid result_id PK
+ uuid run_id FK
+ text result_type
+ text node_id FK
+ text link_id FK
+ jsonb payload
+ }
+ RUNS ||--o{ RESULTS : "产生可选结果"
+```
+
+### 业务表如何扩展
+
+当前没有预建空的 `business` schema。管网模型放在 `network`,空间区域和 DMA 放在 `gis`,设备配置放在 `asset`,运行和结果放在 `analysis`。后续出现工单、巡检、告警或资产养护等明确业务实体时,再按稳定的业务边界增加独立 schema;不把新业务表重新堆回 `public`,也不为尚未确定的角色或流程提前建表。
+
+## 原时序库 tjwater
+
+原时序库运行 TimescaleDB 2.21.3,共有 5 张 hypertable:
+
+- `scada.scada_data`
+- `realtime.node_simulation`
+- `realtime.link_simulation`
+- `scheme.node_simulation`
+- `scheme.link_simulation`
+
+这些表均为行存,没有启用压缩策略。方案结果使用 `scheme_name` 关联业务库中的方案记录。
+
+## 新时序库 tjwater_next
+
+新时序库仍运行 TimescaleDB 2.21.3。业务时序数据分为 `scada`、`realtime` 和 `analysis`,迁移过程状态单独放在 `migration`。
+
+| 表 | 主键 | Chunk 间隔 | 当前行数 | 当前占用 |
+| --- | --- | --- | ---: | ---: |
+| `scada.measurements` | `(time, device_id)` | 7 天 | 1,249,602 | 约 0.18 GiB |
+| `realtime.node_results` | `(time, node_id)` | 1 天 | 159,111,670 | 约 1.89 GiB |
+| `realtime.link_results` | `(time, link_id)` | 1 天 | 164,807,740 | 约 2.18 GiB |
+| `analysis.node_results` | `(stored_at, time, run_id, node_id)` | 1 天 | 21,800,936 | 约 2.93 GiB |
+| `analysis.link_results` | `(stored_at, time, run_id, link_id)` | 1 天 | 22,581,392 | 约 4.82 GiB |
+
+### scada
+
+`scada.measurements` 保存设备测量值和清洗值。`device_id` 与业务库的 `asset.scada_devices.device_id` 是跨数据库逻辑关联,数据库不能建立物理外键。
+
+时序表当前有 118 个设备标识,与业务库的 118 台设备一一对应。迁移后曾发现设备标识 `11`、`12`、`13`、`14` 没有对应的 SCADA 点位配置;确认属于无效孤立数据后,已从新时序库删除其 5,376 条记录。原 `tjwater` 时序库保持不变,必要时仍可追溯迁移前数据。
+
+### realtime
+
+`realtime.node_results` 和 `realtime.link_results` 保存当前实时计算窗口。主键保证同一时刻、同一元素只能有一条记录。相同时间窗口由后端先删除、再通过 `COPY` 批量插入;节点和连接两次替换位于同一个最外层事务,其中任一步失败都会整体回滚。
+
+两张表当前各有 19 个 chunk,均已转为列存,因为现有数据都早于 72 小时热窗口。数据库每小时执行一次策略检查,将 72 小时以前的 chunk 转为有序列存。节点结果按 `node_id, time DESC` 排序,连接结果按 `link_id, time DESC` 排序。新写入的数据使用 1 天 chunk,并在 72 小时内保持行存。
+
+### analysis
+
+`analysis.node_results` 和 `analysis.link_results` 保存每次分析运行的元素级时间序列,通过 `run_id` 与业务库 `analysis.runs` 逻辑关联。`time` 是仿真业务时刻,继续用于曲线和时间范围查询;`stored_at` 是该批结果的入库完成时间,作为 hypertable 的分区时间。
+
+两张表按 1 天创建 chunk,最近 24 小时保持热数据。超过 24 小时的 chunk 会由压缩策略转为列存冷数据,策略按 `run_id` 与元素 ID 分段、按仿真 `time DESC` 排序。重新计算相同历史时间段时,新结果写入当前的 `stored_at` 热分区,不需要改写历史冷分区。当前各有 20 个 chunk,均因入库时间超过 24 小时而完成压缩。
+
+迁移期间曾保留按仿真时间分区的 `*_time_partitioned_legacy` 表用于回退校验。清理前已核对 46 个 `run_id`:节点 21,800,936 行、连接 22,581,392 行的总数、逐运行数量、时间范围和全字段校验值均一致。两张旧表随后删除,释放约 12.16 GiB;原 `tjwater` 时序库仍保留,不受此次清理影响。
+
+### migration
+
+`migration.copy_log` 记录每个来源表、迁移时间窗口和源行数,当前有 150 条记录。`migration.settings` 保存迁移截止时间等运行参数,当前有 1 条记录。它们是迁移审计和断点信息,不是业务数据,也不是仍待处理的中间结果。正式数据迁移已经完成。
+
+## 后端连接与事务
+
+元数据库通过 SQLAlchemy 异步连接池访问;项目请求按 `system_hub.public.project_databases` 路由到业务库和时序库。异步业务查询和异步时序查询由项目级动态池管理,原生 WNDB 同步访问使用按数据库缓存的 `psycopg_pool.ConnectionPool`,数据库创建、复制和删除使用独立的 PostgreSQL 管理池,同步 TimescaleDB 访问也使用按数据库缓存的连接池。应用目录中已没有直接调用 `psycopg.connect` 的业务代码。
+
+WNDB 批量修改和 INP 数据导入在同一条池连接和同一事务中执行,提交后再刷新 GIS 物化视图。实时节点和连接结果也在一个事务中执行先删后写,同一结果时间使用事务级锁避免并发覆盖竞态;分析结果按 `run_id` 加事务级锁,防止同一运行被并发写入两次。
+
+自动化真实数据库测试分别执行 64 次业务库和 64 次时序库并发借用,查询结果一致,连接均能归还池中。嵌套 WNDB 写入和分析运行生命周期测试会在外层强制回滚,数据库没有残留记录。`DatabaseCommand` 的 pattern 新增、修改、删除也在同一池化事务中完成,并验证了五张明细表的复合主键、级联解除需求模式关联、结果变更和整体回滚。实时覆盖测试确认第二批数据替换第一批数据,外层回滚后测试记录为 0。
+
+## 跨数据库逻辑关系
+
+PostgreSQL 不能对另一个数据库中的表建立外键。业务库与时序库之间通过稳定 ID 保持逻辑一致性,完整性由迁移校验和后端事务负责。
+
+```mermaid
+flowchart LR
+ P["system_hub.public.projects"] -->|"biz_data 路由"| B["项目业务库"]
+ P -->|"iot_data 路由"| T["项目时序库"]
+
+ BN["network.nodes.id"] -. "node_id" .-> RN["realtime.node_results"]
+ BN -. "node_id" .-> AN["analysis.node_results"]
+ BL["network.links.id"] -. "link_id" .-> RL["realtime.link_results"]
+ BL -. "link_id" .-> AL["analysis.link_results"]
+ SD["asset.scada_devices.device_id"] -. "device_id" .-> SM["scada.measurements"]
+ AR["analysis.runs.run_id"] -. "run_id" .-> AN
+ AR -. "run_id" .-> AL
+```
+
+实线表示元数据库保存的项目路由,虚线表示跨数据库逻辑关联。每个项目使用独立业务库和时序库,不需要在项目本地表中重复保存 `project_id`。
+
+## 仍需处理的事项
+
+- 当前没有修改 SCADA 设备配置的后端接口。以后增加这类写接口时,也要在提交后调用统一的物化视图刷新函数。
+- 用户角色和项目角色还未定型,数据库只要求字段非空,不限制具体取值。
diff --git a/resources/db_v2/WNDB_STRUCTURE.md b/resources/db_v2/WNDB_STRUCTURE.md
new file mode 100644
index 0000000..80a8771
--- /dev/null
+++ b/resources/db_v2/WNDB_STRUCTURE.md
@@ -0,0 +1,168 @@
+# WNDB 文件结构说明
+
+本文记录 2026-08-25 完成的 WNDB 目录与命令结构重构。检查对象是当前代码,不以旧 SQL 脚本或历史目录为依据。
+
+## 结论
+
+当前结构适合继续维护。WNDB 已按连接基础设施、管网模型、GIS、INP 和命令执行分组,原来的编号文件名、根目录聚合门面和星号导入已经移除。WDA、SCADA 资产查询和测压点选址也已离开底层模型目录。
+
+本次调整了代码文件、导入关系、命令分派和 WNDB 内部命令对象,没有改变 HTTP 接口。历史撤销日志已从数据库中移除,内部接口不再保留无效的兼容字段。真实库回归时发现五张明细表错误地把局部顺序号设成全局主键,已在 `tjwater_next` 中改为父对象 ID 与 `sequence_no` 的复合主键。
+
+## 当前目录
+
+```text
+app/native/wndb/
+├── __init__.py
+├── core/
+│ ├── connection.py
+│ ├── database.py
+│ └── projects.py
+├── model/
+│ ├── elements.py
+│ ├── junctions.py
+│ ├── reservoirs.py
+│ ├── tanks.py
+│ ├── pipes.py
+│ ├── pumps.py
+│ ├── valves.py
+│ ├── patterns.py
+│ ├── curves.py
+│ ├── options.py
+│ └── 其他 EPANET 模型模块
+├── gis/
+│ ├── coordinates.py
+│ ├── vertices.py
+│ ├── labels.py
+│ ├── backdrop.py
+│ ├── regions.py
+│ └── region_geometry.py
+├── inp/
+│ ├── sections.py
+│ ├── importer.py
+│ └── exporter.py
+└── commands/
+ ├── api.py
+ ├── cascade.py
+ └── executor.py
+```
+
+目录内共有 47 个 Python 文件,约 7,100 行。`app/native/wndb/__init__.py` 只保留包说明,不再统一导出所有函数。调用方需要从具体职责模块导入,依赖来源可以直接从文件头确认。
+
+## 各目录的职责
+
+### core:连接、事务和数据库基础能力
+
+`connection.py` 管理项目连接池、管理连接池和项目事务上下文。连接池按路由后的 DSN 复用,并限制缓存规模。
+
+`database.py` 提供 `ChangeSet`、`DatabaseCommand`、参数化查询和物化视图刷新。`DatabaseCommand` 只保存待执行的 SQL 和执行成功后返回给调用方的变更列表,不再生成或保存撤销 SQL。模型直接修改时按需刷新视图;批量命令在外层事务提交后只刷新一次。物化视图保留模型坐标 `x`、`y`,同时将供 GeoServer 使用的 `geom` 转换为 `EPSG:3857`,WNDB 查询不会把发布坐标误当成模型坐标。
+
+`projects.py` 只负责项目数据库的创建、复制、打开、关闭和删除,不再混入模型查询。
+
+### model:管网模型和仿真配置
+
+`model` 按业务实体命名,不再使用 `s2_junctions.py` 这类 INP 章节编号。节点、连接、模式、曲线、需求、规则和仿真设置都能从文件名直接定位。
+
+每个实体模块保留三类紧密相关的函数:读取实体、生成并执行实体变更、转换该实体对应的一行或一段 INP 内容。完整文件的读取顺序、事务和项目生命周期由 `inp` 目录负责。因此,实体级编解码仍靠近实体定义,跨章节编排已经集中。
+
+`elements.py` 保存节点、连接、模式、曲线和区域的通用类型判断及拓扑查询。它不再承担业务算法。
+
+### gis:空间数据和区域几何
+
+`coordinates.py`、`vertices.py`、`labels.py` 和 `backdrop.py` 对应原始 GIS 数据。`regions.py` 负责区域持久化,`region_geometry.py` 负责边界、凸包、膨胀和区域内元素查询。
+
+管网实体修改会调用坐标 SQL 辅助函数,区域几何也会读取管网拓扑。这里存在明确的模型与 GIS 协作,但没有模块导入环。现阶段继续拆出抽象接口只会增加层级,没有实际收益。
+
+### inp:文件级导入导出
+
+`sections.py` 只保存 INP 章节名称和输出顺序。旧文件中混放的 `s1_title`、`s2_junction` 等命令类型常量已经移除。
+
+`importer.py` 负责文件分段、导入顺序、项目事务、版本转换和导入后的物化视图刷新。`exporter.py` 负责按 EPANET 版本组织各章节并写出文件或 `ChangeSet`。
+
+### commands:批量修改和级联关系
+
+`api.py` 为级联删除和选项同步补齐命令元数据。`cascade.py` 将删除节点、连接、模式和曲线的请求展开为完整的关联修改。`executor.py` 在一个项目事务中执行展开后的命令。
+
+元素命令分派已经由类型到处理函数的显式注册表实现。新增元素时,只需把受支持的新增、修改或删除处理函数登记到对应注册表。没有处理函数的命令保持空操作,行为与改造前一致。
+
+### 命令执行与事务
+
+```mermaid
+flowchart LR
+ REQUEST[ChangeSet 请求]
+ EXPAND[展开级联修改]
+ BUILD[实体模块生成 DatabaseCommand]
+ EXECUTE[执行 SQL]
+ RESULT[返回 ChangeSet]
+ COMMIT[提交项目事务]
+ REFRESH[刷新物化视图]
+
+ REQUEST --> EXPAND --> BUILD --> EXECUTE --> RESULT
+ EXECUTE --> COMMIT --> REFRESH
+```
+
+实体模块根据请求生成 `DatabaseCommand`,其中 `sql` 是要执行的语句,`changes` 是成功后的变更结果。批量命令先在同一个项目事务中展开级联关系,再依次执行 SQL;任何一步失败都会回滚整个事务。事务提交后统一刷新物化视图,避免一次批量修改触发多次刷新。直接调用单个实体修改时,如果当前没有外层项目事务,则由 `execute_command` 完成提交并按影响范围刷新视图。
+
+旧实现中的 `DbChangeSet` 同时保存执行和撤销两套 SQL、两套变更结果,但数据库已经不再提供 operation 或 snapshot 撤销日志,这些字段没有消费者。当前代码已经删除 `undo_sql`、`undo_cs` 及各实体模块中的撤销 SQL 构造,也删除了只为撤销结果读取旧记录的查询和辅助方法。局部修改仍会读取一次当前记录,用于补齐请求中未提供的字段;这类读取属于更新语义,不是撤销机制。
+
+## WNDB 之外的业务代码
+
+以下代码不再放在 `app/native/wndb`:
+
+| 职责 | 当前路径 | 原因 |
+| --- | --- | --- |
+| 用水量分配计算 | `app/algorithms/water_demand/` | 属于业务算法,WNDB 只提供节点、需求和区域查询 |
+| SCADA 资产查询 | `app/infra/db/postgresql/scada_assets.py` | 对应 `asset.scada_devices` 的 PostgreSQL 仓储 |
+| 测压点选址结果 | `app/infra/db/postgresql/sensor_placement.py` | 对应 `analysis.runs` 和 `analysis.results` 的仓储 |
+| 服务组合入口 | `app/services/tjnetwork.py` | 组合 WNDB、EPANET 和业务仓储,供接口与算法层调用 |
+
+`tjnetwork.py` 从约 995 行缩减到约 320 行。它不再通过 WNDB 根包获得全部函数,只显式导入当前服务使用的能力。过时的 `scripts/test_tjnetwork.py` 依赖已移除的 operation、snapshot、DMA 和旧 SCADA API,已经一并删除。
+
+## 依赖方向
+
+```mermaid
+flowchart TD
+ API[HTTP 接口与业务服务]
+ SERVICE[services.tjnetwork]
+ ALGORITHM[业务算法]
+ REPOSITORY[PostgreSQL 业务仓储]
+ COMMANDS[wndb.commands]
+ INP[wndb.inp]
+ MODEL[wndb.model]
+ GIS[wndb.gis]
+ CORE[wndb.core]
+ DB[(项目业务数据库)]
+
+ API --> SERVICE
+ API --> ALGORITHM
+ API --> REPOSITORY
+ SERVICE --> COMMANDS
+ SERVICE --> INP
+ SERVICE --> MODEL
+ SERVICE --> GIS
+ SERVICE --> REPOSITORY
+ COMMANDS --> MODEL
+ COMMANDS --> GIS
+ COMMANDS --> CORE
+ INP --> MODEL
+ INP --> GIS
+ INP --> CORE
+ MODEL --> CORE
+ GIS --> CORE
+ MODEL --> GIS
+ GIS --> MODEL
+ CORE --> DB
+ REPOSITORY --> DB
+```
+
+WNDB 根包不再作为依赖汇聚点。上层若只需要管道查询,应直接依赖 `model.pipes`;需要区域几何时依赖 `gis.region_geometry`;需要项目连接时依赖 `core.connection`。
+
+## 仍需留意的文件规模
+
+`importer.py` 和 `region_geometry.py` 行数较多,但函数仍围绕单一职责。只有在继续增加 INP 格式或区域算法时,才需要分别拆出版本转换器或边界算法模块。目前没有必要为了控制文件行数继续分层。
+
+## 验证结果
+
+- 本地 conda 环境全量测试:239 项通过,10 项按条件跳过。
+- Docker 镜像构建成功,镜像内全量测试结果一致。
+- `tjwater_next` 真实数据库测试:8 项通过,覆盖业务库和时序库并发借用、嵌套事务回滚、分析运行生命周期、恶意标识符转义、五张明细表的复合主键,以及 WNDB pattern 增删改、级联解除需求关联和整体回滚。
+- Python 编译、未使用导入扫描、撤销字段残留扫描和 `git diff --check` 均通过。
diff --git a/scripts/online_Analysis.py b/scripts/online_Analysis.py
index 72b2dba..82e9dda 100644
--- a/scripts/online_Analysis.py
+++ b/scripts/online_Analysis.py
@@ -30,7 +30,6 @@ from app.services.tjnetwork import (
set_source,
set_time,
)
-from app.native.wndb.project import copy_project
from app.algorithms.simulation.runner import run_simulation_ex, from_clock_to_seconds_2
from math import sqrt, pi
from app.infra.epanet.epanet import Output
diff --git a/scripts/test_tjnetwork.py b/scripts/test_tjnetwork.py
deleted file mode 100644
index 908ec13..0000000
--- a/scripts/test_tjnetwork.py
+++ /dev/null
@@ -1,6869 +0,0 @@
-import pytest
-import random
-from app.services.tjnetwork import (
- API_ADD,
- API_DELETE,
- API_UPDATE,
- CURVE,
- CURVE_TYPE_EFFICIENCY,
- CURVE_TYPE_PUMP,
- ChangeSet,
- JUNCTION,
- LINK_STATUS_OPEN,
- MIXING_MODEL_2COMP,
- MIXING_MODEL_MIXED,
- OPTION_DEMAND_MODEL_DDA,
- OPTION_DEMAND_MODEL_PDA,
- OPTION_HEADLOSS_DW,
- OPTION_HEADLOSS_HW,
- OPTION_PRESSURE_KPA,
- OPTION_PRESSURE_METERS,
- OPTION_PRESSURE_PSI,
- OPTION_QUALITY_NONE,
- OPTION_QUALITY_TRACE,
- OPTION_UNBALANCED_CONTINUE,
- OPTION_UNBALANCED_STOP,
- OPTION_UNITS_GPM,
- OPTION_UNITS_LPS,
- OPTION_V3_DEMAND_MODEL_CONSTRAINED,
- OPTION_V3_DEMAND_MODEL_FIXED,
- OPTION_V3_DEMAND_MODEL_LOGISTIC,
- OPTION_V3_DEMAND_MODEL_POWER,
- OPTION_V3_FLOW_UNITS_GPM,
- OPTION_V3_FLOW_UNITS_LPS,
- OPTION_V3_HEADLOSS_MODEL_DW,
- OPTION_V3_HEADLOSS_MODEL_HW,
- OPTION_V3_IF_UNBALANCED_CONTINUE,
- OPTION_V3_IF_UNBALANCED_STOP,
- OPTION_V3_LEAKAGE_MODEL_NONE,
- OPTION_V3_LEAKAGE_MODEL_POWER,
- OPTION_V3_PRESSURE_UNITS_KPA,
- OPTION_V3_PRESSURE_UNITS_METERS,
- OPTION_V3_PRESSURE_UNITS_PSI,
- OPTION_V3_QUALITY_MODEL_CHEMICAL,
- OPTION_V3_QUALITY_MODEL_NONE,
- OPTION_V3_QUALITY_MODEL_TRACE,
- OPTION_V3_QUALITY_UNITS_HRS,
- OPTION_V3_QUALITY_UNITS_MGL,
- OPTION_V3_STEP_SIZING_FULL,
- OPTION_V3_STEP_SIZING_RELAXATION,
- OVERFLOW_NO,
- OVERFLOW_YES,
- PATTERN,
- PIPE,
- PIPE_STATUS_CLOSED,
- PIPE_STATUS_OPEN,
- PUMP,
- RESERVOIR,
- SCADA_DEVICE_TYPE_FLOW,
- SCADA_DEVICE_TYPE_PRESSURE,
- SCADA_ELEMENT_STATUS_OFFLINE,
- SCADA_ELEMENT_STATUS_ONLINE,
- SCADA_MODEL_TYPE_JUNCTION,
- SCADA_MODEL_TYPE_PIPE,
- SOURCE_TYPE_CONCEN,
- SOURCE_TYPE_FLOWPACED,
- TAG_TYPE_LINK,
- TAG_TYPE_NODE,
- TANK,
- TIME_STATISTIC_AVERAGED,
- TIME_STATISTIC_NONE,
- VALVE,
- VALVES_TYPE_FCV,
- VALVES_TYPE_GPV,
- add_curve,
- add_district_metering_area,
- add_junction,
- add_label,
- add_mixing,
- add_pattern,
- add_pipe,
- add_pump,
- add_region,
- add_reservoir,
- add_scada_device,
- add_scada_device_data,
- add_scada_element,
- add_service_area,
- add_source,
- add_tank,
- add_valve,
- add_virtual_district,
- calculate_boundary,
- calculate_convex_hull,
- calculate_demand_to_network,
- calculate_demand_to_nodes,
- calculate_demand_to_region,
- calculate_district_metering_area_for_network,
- calculate_district_metering_area_for_nodes,
- calculate_district_metering_area_for_region,
- calculate_service_area,
- calculate_virtual_district,
- clean_scada_device,
- clean_scada_device_data,
- clean_scada_element,
- close_project,
- create_project,
- delete_curve,
- delete_district_metering_area,
- delete_junction,
- delete_label,
- delete_mixing,
- delete_pattern,
- delete_pipe,
- delete_project,
- delete_pump,
- delete_region,
- delete_reservoir,
- delete_scada_device,
- delete_scada_device_data,
- delete_scada_element,
- delete_service_area,
- delete_source,
- delete_tank,
- delete_valve,
- delete_virtual_district,
- execute_batch_command,
- execute_batch_commands,
- execute_redo,
- execute_undo,
- generate_district_metering_area,
- generate_service_area,
- generate_sub_district_metering_area,
- generate_virtual_district,
- get_all_district_metering_area_ids,
- get_all_extension_data,
- get_all_extension_data_keys,
- get_all_scada_device_ids,
- get_all_scada_element_ids,
- get_all_service_area_ids,
- get_all_service_areas,
- get_all_virtual_district_ids,
- get_all_virtual_districts,
- get_backdrop,
- get_control,
- get_current_operation,
- get_curve,
- get_demand,
- get_district_metering_area,
- get_emitter,
- get_energy,
- get_extension_data,
- get_junction,
- get_label,
- get_links,
- get_links_on_region_boundary,
- get_mixing,
- get_node_links,
- get_nodes,
- get_nodes_in_boundary,
- get_nodes_in_region,
- get_operation_by_snapshot,
- get_option,
- get_option_v3,
- get_pattern,
- get_pipe,
- get_pipe_reaction,
- get_pump,
- get_pump_energy,
- get_quality,
- get_reaction,
- get_region,
- get_reservoir,
- get_restore_operation,
- get_rule,
- get_scada_device,
- get_scada_device_data,
- get_scada_element,
- get_service_area,
- get_snapshot_by_operation,
- get_source,
- get_status,
- get_tag,
- get_tags,
- get_tank,
- get_tank_reaction,
- get_time,
- get_title,
- get_valve,
- get_vertex,
- get_virtual_district,
- have_project,
- inflate_boundary,
- inflate_region,
- is_curve,
- is_junction,
- is_link,
- is_node,
- is_pattern,
- is_pipe,
- is_project_open,
- is_pump,
- is_reservoir,
- is_tank,
- is_valve,
- open_project,
- pick_operation,
- pick_snapshot,
- read_all,
- read_inp,
- set_backdrop,
- set_control,
- set_curve,
- set_demand,
- set_district_metering_area,
- set_emitter,
- set_energy,
- set_extension_data,
- set_junction,
- set_label,
- set_mixing,
- set_option,
- set_option_v3,
- set_pattern,
- set_pipe,
- set_pipe_reaction,
- set_pump,
- set_pump_energy,
- set_quality,
- set_reaction,
- set_region,
- set_reservoir,
- set_rule,
- set_scada_device,
- set_scada_device_data,
- set_scada_element,
- set_service_area,
- set_source,
- set_status,
- set_tag,
- set_tank,
- set_tank_reaction,
- set_time,
- set_title,
- set_valve,
- set_vertex,
- set_virtual_district,
- sync_with_server,
- take_snapshot,
- update_snapshot_for_current_operation,
- write,
-)
-
-class TestApi:
- def enter(self, p):
- if is_project_open(p):
- close_project(p)
-
- if have_project(p):
- delete_project(p)
-
- create_project(p)
- open_project(p)
-
-
- def leave(self, p):
- close_project(p)
- delete_project(p)
-
-
- # encoding
-
-
- def test_utf8(self):
- p = 'test_utf8'
- self.enter(p)
-
- write(p, f"create table {p} (a varchar(32))")
- write(p, f"insert into {p} values ('你好')")
- result = read_all(p, f"select * from {p}")
- assert result == [{'a': '你好'}]
-
- self.leave(p)
-
-
- # project
-
-
- def test_project(self):
- p = 'test_project'
-
- assert not have_project(p)
- assert not is_project_open(p)
-
- create_project(p)
-
- assert have_project(p)
- assert not is_project_open(p)
-
- open_project(p)
-
- assert have_project(p)
- assert is_project_open(p)
-
- close_project(p)
-
- assert have_project(p)
- assert not is_project_open(p)
-
- delete_project(p)
-
- assert not have_project(p)
- assert not is_project_open(p)
-
-
- def test_project_name(self):
- p = 'test_PROJECT_name'
-
- assert not have_project(p)
- assert not is_project_open(p)
-
- create_project(p)
-
- assert have_project(p)
- assert not is_project_open(p)
-
- open_project(p)
-
- assert have_project(p)
- assert is_project_open(p)
-
- close_project(p)
-
- assert have_project(p)
- assert not is_project_open(p)
-
- delete_project(p)
-
- assert not have_project(p)
- assert not is_project_open(p)
-
-
- # operation
-
-
- def test_snapshot(self):
- p = "test_snapshot"
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j3', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j4', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
-
- op_4 = get_current_operation(p)
- assert op_4 == 4
- take_snapshot(p, 'x')
- assert get_snapshot_by_operation(p, 4) == 'x'
- assert get_operation_by_snapshot(p, 'x') == 4
- update_snapshot_for_current_operation(p, 'y')
- assert get_snapshot_by_operation(p, 4) == 'y'
-
- execute_undo(p)
- execute_undo(p)
- add_junction(p, ChangeSet({'id': 'j5', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j6', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
-
- op_6 = get_current_operation(p)
- assert op_6 == 6
- take_snapshot(p, 'xx')
- assert get_snapshot_by_operation(p, 6) == 'xx'
- assert get_operation_by_snapshot(p, 'xx') == 6
-
- cs = sync_with_server(p, op_4).operations
- cs[0]['operation'] = API_DELETE
- cs[0]['id'] = 'j4'
- cs[1]['operation'] = API_DELETE
- cs[1]['id'] = 'j3'
- cs[2]['operation'] = API_ADD
- cs[2]['id'] = 'j5'
- cs[3]['operation'] = API_ADD
- cs[3]['id'] = 'j6'
-
- cs = pick_snapshot(p, 'y').operations
- cs[0]['operation'] = 'delete'
- cs[0]['id'] = 'j6'
- cs[1]['operation'] = 'delete'
- cs[1]['id'] = 'j5'
- cs[2]['operation'] = 'add'
- cs[2]['id'] = 'j3'
- cs[3]['operation'] = 'add'
- cs[3]['id'] = 'j4'
-
- assert get_nodes(p) == ['j1', 'j2', 'j3', 'j4']
-
- self.leave(p)
-
-
- def test_batch_commands(self):
- p = 'test_batch_commands'
- self.enter(p)
-
- cs = ChangeSet()
- cs.add({'type': JUNCTION, 'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0})
- cs.add({'type': JUNCTION, 'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0})
- cs.add({'type': JUNCTION, 'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}) # fail
-
- cs = execute_batch_commands(p, cs)
- assert len(cs.operations) == 2
-
- cs = ChangeSet()
- cs.delete({'type': JUNCTION, 'id': 'j1'})
- cs.delete({'type': JUNCTION, 'id': 'j2'})
-
- cs = execute_batch_commands(p, cs)
- assert len(cs.operations) == 2
-
- cs = execute_undo(p)
- assert len(cs.operations) == 1
-
- self.leave(p)
-
-
- def test_batch_command(self):
- p = 'test_batch_command'
- self.enter(p)
-
- cs = ChangeSet()
- cs.add({'type': JUNCTION, 'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0})
- cs.add({'type': JUNCTION, 'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0})
- cs.add({'type': JUNCTION, 'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}) # fail
-
- cs = execute_batch_command(p, cs)
- assert len(cs.operations) == 2
-
- assert get_current_operation(p) == 1
-
- cs = ChangeSet()
- cs.delete({'type': JUNCTION, 'id': 'j1'})
- cs.delete({'type': JUNCTION, 'id': 'j2'})
-
- cs = execute_batch_command(p, cs)
-
- assert get_current_operation(p) == 2
-
- cs = execute_undo(p)
- assert get_current_operation(p) == 1
-
- cs = execute_undo(p)
- assert get_current_operation(p) == 0
-
- self.leave(p)
-
-
- # extension_data
-
-
- def test_extension_data(self):
- p = 'test_extension_data'
- self.enter(p)
-
- assert get_all_extension_data_keys(p) == []
- assert get_all_extension_data(p) == {}
- assert get_extension_data(p, '') == None
-
- set_extension_data(p, ChangeSet({'key': 'key', 'value': None}))
- assert get_extension_data(p, 'key') == None
-
- set_extension_data(p, ChangeSet({'key': 'key', 'value': ''}))
- assert get_extension_data(p, 'key') == ''
-
- set_extension_data(p, ChangeSet({'key': 'key', 'value': 'value'}))
- assert get_extension_data(p, 'key') == 'value'
-
- set_extension_data(p, ChangeSet({'key': 'key', 'value': 'val'}))
- assert get_extension_data(p, 'key') == 'val'
-
- set_extension_data(p, ChangeSet({'key': 'key1', 'value': 'val1'}))
- assert get_extension_data(p, 'key1') == 'val1'
-
- assert get_all_extension_data_keys(p) == ['key', 'key1']
- assert get_all_extension_data(p) == {'key': 'val', 'key1': 'val1'}
-
- self.leave(p)
-
-
- def test_extension_data_op(self):
- p = 'test_extension_data_op'
- self.enter(p)
-
- cs = set_extension_data(p, ChangeSet({'key': 'key', 'value': ''})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'extension_data'
- assert cs['key'] == 'key'
- assert cs['value'] == ''
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'extension_data'
- assert cs['key'] == 'key'
- assert cs['value'] == None
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'extension_data'
- assert cs['key'] == 'key'
- assert cs['value'] == ''
-
- cs = set_extension_data(p, ChangeSet({'key': 'key', 'value': 'value'})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'extension_data'
- assert cs['key'] == 'key'
- assert cs['value'] == 'value'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'extension_data'
- assert cs['key'] == 'key'
- assert cs['value'] == ''
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'extension_data'
- assert cs['key'] == 'key'
- assert cs['value'] == 'value'
-
- self.leave(p)
-
-
- # complex test
-
-
- def test_delete_node_link_then_restore(self):
- p = 'test_remove_node_link_then_restore'
- read_inp(p, f'./inp/net3.inp', '2')
-
- open_project(p)
-
- nodes = []
- links = []
-
- nodes.append('131')
- links += get_node_links(p, nodes[-1])
- delete_junction(p, ChangeSet({'id': '131'}))
-
- links.append('137')
- delete_pipe(p, ChangeSet({'id': '137'}))
-
- nodes.append('129')
- links += get_node_links(p, nodes[-1])
- delete_junction(p, ChangeSet({'id': '129'}))
-
- nodes.append('127')
- links += get_node_links(p, nodes[-1])
- delete_junction(p, ChangeSet({'id': '127'}))
-
- links.append('135')
- delete_pipe(p, ChangeSet({'id': '135'}))
-
- links.append('135')
- delete_pipe(p, ChangeSet({'id': '133'}))
-
- nodes.append('20')
- links += get_node_links(p, nodes[-1])
- delete_junction(p, ChangeSet({'id': '20'}))
-
- nodes.append('3')
- links += get_node_links(p, nodes[-1])
- delete_tank(p, ChangeSet({'id': '3'}))
-
- for node in nodes:
- assert is_node(p, node) == False
- for link in links:
- assert is_link(p, link) == False
-
- op = get_restore_operation(p)
- pick_operation(p, op)
-
- for node in nodes:
- assert is_node(p, node)
- for link in links:
- assert is_link(p, link)
-
- self.leave(p)
-
-
- def test_delete_nodes_then_restore(self):
- p = 'test_delete_nodes_then_restore'
- read_inp(p, f'./inp/net3.inp', '2')
-
- open_project(p)
-
- nodes = get_nodes(p)
- links = get_links(p)
-
- for _ in range(10):
- random.shuffle(nodes)
- for node in nodes:
- if is_junction(p, node):
- delete_junction(p, ChangeSet({'id': node}))
- if is_reservoir(p, node):
- delete_reservoir(p, ChangeSet({'id': node}))
- if is_tank(p, node):
- delete_tank(p, ChangeSet({'id': node}))
-
- for node in nodes:
- assert is_node(p, node) == False
- for link in links:
- assert is_link(p, link) == False
-
- assert get_nodes(p) == []
- assert get_links(p) == []
-
- op = get_restore_operation(p)
- pick_operation(p, op)
-
- for node in nodes:
- assert is_node(p, node)
- for link in links:
- assert is_link(p, link)
-
- self.leave(p)
-
-
- def test_delete_links_then_restore(self):
- p = 'test_delete_links_then_restore'
- read_inp(p, f'./inp/net3.inp', '2')
-
- open_project(p)
-
- nodes = get_nodes(p)
- links = get_links(p)
-
- for _ in range(10):
- random.shuffle(links)
- for link in links:
- if is_pipe(p, link):
- delete_pipe(p, ChangeSet({'id': link}))
- if is_pump(p, link):
- delete_pump(p, ChangeSet({'id': link}))
- if is_valve(p, link):
- delete_valve(p, ChangeSet({'id': link}))
-
- for node in nodes:
- assert is_node(p, node)
- for link in links:
- assert is_link(p, link) == False
-
- assert get_links(p) == []
-
- op = get_restore_operation(p)
- pick_operation(p, op)
-
- for node in nodes:
- assert is_node(p, node)
- for link in links:
- assert is_link(p, link)
-
- self.leave(p)
-
-
- def test_delete_nodes_then_restore_commands(self):
- p = 'test_delete_nodes_then_restore_commands'
- read_inp(p, f'./inp/net3.inp', '2')
-
- open_project(p)
-
- nodes = get_nodes(p)
- links = get_links(p)
-
- for _ in range(10):
- random.shuffle(nodes)
-
- batch = ChangeSet()
- for node in nodes:
- if is_junction(p, node):
- batch.delete({'type' : 'junction', 'id': node })
- if is_reservoir(p, node):
- batch.delete({'type' : 'reservoir', 'id': node })
- if is_tank(p, node):
- batch.delete({'type' : 'tank', 'id': node })
- execute_batch_commands(p, batch)
-
- for node in nodes:
- assert is_node(p, node) == False
- for link in links:
- assert is_link(p, link) == False
-
- assert get_nodes(p) == []
- assert get_links(p) == []
-
- op = get_restore_operation(p)
- pick_operation(p, op)
-
- for node in nodes:
- assert is_node(p, node)
- for link in links:
- assert is_link(p, link)
-
- self.leave(p)
-
-
- def test_delete_links_then_restore_commands(self):
- p = 'test_delete_links_then_restore_commands'
- read_inp(p, f'./inp/net3.inp', '2')
-
- open_project(p)
-
- nodes = get_nodes(p)
- links = get_links(p)
-
- for _ in range(10):
- random.shuffle(links)
-
- batch = ChangeSet()
- for link in links:
- if is_pipe(p, link):
- batch.delete({'type' : 'pipe', 'id': link })
- if is_pump(p, link):
- batch.delete({'type' : 'pump', 'id': link })
- if is_valve(p, link):
- batch.delete({'type' : 'valve', 'id': link })
- execute_batch_commands(p, batch)
-
- for node in nodes:
- assert is_node(p, node)
- for link in links:
- assert is_link(p, link) == False
-
- assert get_links(p) == []
-
- op = get_restore_operation(p)
- pick_operation(p, op)
-
- for node in nodes:
- assert is_node(p, node)
- for link in links:
- assert is_link(p, link)
-
- self.leave(p)
-
-
- def test_delete_nodes_then_restore_command(self):
- p = 'test_delete_nodes_then_restore_commands'
- read_inp(p, f'./inp/net3.inp', '2')
-
- open_project(p)
-
- nodes = get_nodes(p)
- links = get_links(p)
-
- for _ in range(10):
- random.shuffle(nodes)
-
- batch = ChangeSet()
- for node in nodes:
- if is_junction(p, node):
- batch.delete({'type' : 'junction', 'id': node })
- if is_reservoir(p, node):
- batch.delete({'type' : 'reservoir', 'id': node })
- if is_tank(p, node):
- batch.delete({'type' : 'tank', 'id': node })
- execute_batch_command(p, batch)
-
- for node in nodes:
- assert is_node(p, node) == False
- for link in links:
- assert is_link(p, link) == False
-
- assert get_nodes(p) == []
- assert get_links(p) == []
-
- op = get_restore_operation(p)
- pick_operation(p, op)
-
- for node in nodes:
- assert is_node(p, node)
- for link in links:
- assert is_link(p, link)
-
- self.leave(p)
-
-
- def test_delete_links_then_restore_command(self):
- p = 'test_delete_links_then_restore_commands'
- read_inp(p, f'./inp/net3.inp', '2')
-
- open_project(p)
-
- nodes = get_nodes(p)
- links = get_links(p)
-
- for _ in range(10):
- random.shuffle(links)
-
- batch = ChangeSet()
- for link in links:
- if is_pipe(p, link):
- batch.delete({'type' : 'pipe', 'id': link })
- if is_pump(p, link):
- batch.delete({'type' : 'pump', 'id': link })
- if is_valve(p, link):
- batch.delete({'type' : 'valve', 'id': link })
- execute_batch_command(p, batch)
-
- for node in nodes:
- assert is_node(p, node)
- for link in links:
- assert is_link(p, link) == False
-
- assert get_links(p) == []
-
- op = get_restore_operation(p)
- pick_operation(p, op)
-
- for node in nodes:
- assert is_node(p, node)
- for link in links:
- assert is_link(p, link)
-
- self.leave(p)
-
-
- def test_delete_nodes_links_then_restore_v2(self):
- p = 'test_delete_nodes_links_then_restore_v2'
- read_inp(p, f'./inp/net3.inp', '2')
-
- open_project(p)
-
- nls : list[tuple[str, str]] = []
-
- nodes = get_nodes(p)
- for node in nodes:
- nls.append(('node', node))
-
- links = get_links(p)
- for link in links:
- nls.append(('link', link))
-
- for _ in range(10):
- random.shuffle(nls)
- for nl in nls:
- if nl[0] == 'node':
- node = nl[1]
- if is_junction(p, node):
- delete_junction(p, ChangeSet({'id': node}))
- if is_reservoir(p, node):
- delete_reservoir(p, ChangeSet({'id': node}))
- if is_tank(p, node):
- delete_tank(p, ChangeSet({'id': node}))
- else:
- link = nl[1]
- if is_pipe(p, link):
- delete_pipe(p, ChangeSet({'id': link}))
- if is_pump(p, link):
- delete_pump(p, ChangeSet({'id': link}))
- if is_valve(p, link):
- delete_valve(p, ChangeSet({'id': link}))
-
- for node in nodes:
- assert is_node(p, node) == False
- for link in links:
- assert is_link(p, link) == False
-
- assert get_nodes(p) == []
- assert get_links(p) == []
-
- op = get_restore_operation(p)
- pick_operation(p, op)
-
- for node in nodes:
- assert is_node(p, node)
- for link in links:
- assert is_link(p, link)
-
- self.leave(p)
-
-
- def test_delete_nodes_links_then_restore_v3(self):
- p = 'test_delete_nodes_links_then_restore_v3'
- read_inp(p, f'./inp/net3.inp', '3')
-
- open_project(p)
-
- nls : list[tuple[str, str]] = []
-
- nodes = get_nodes(p)
- for node in nodes:
- nls.append(('node', node))
-
- links = get_links(p)
- for link in links:
- nls.append(('link', link))
-
- for _ in range(10):
- random.shuffle(nls)
- for nl in nls:
- if nl[0] == 'node':
- node = nl[1]
- if is_junction(p, node):
- delete_junction(p, ChangeSet({'id': node}))
- if is_reservoir(p, node):
- delete_reservoir(p, ChangeSet({'id': node}))
- if is_tank(p, node):
- delete_tank(p, ChangeSet({'id': node}))
- else:
- link = nl[1]
- if is_pipe(p, link):
- delete_pipe(p, ChangeSet({'id': link}))
- if is_pump(p, link):
- delete_pump(p, ChangeSet({'id': link}))
- if is_valve(p, link):
- delete_valve(p, ChangeSet({'id': link}))
-
- for node in nodes:
- assert is_node(p, node) == False
- for link in links:
- assert is_link(p, link) == False
-
- assert get_nodes(p) == []
- assert get_links(p) == []
-
- op = get_restore_operation(p)
- pick_operation(p, op)
-
- for node in nodes:
- assert is_node(p, node)
- for link in links:
- assert is_link(p, link)
-
- self.leave(p)
-
-
- # 1 title
-
-
- def test_title(self):
- p = 'test_title'
- self.enter(p)
-
- assert get_title(p)['value'] == ''
-
- change = set_title(p, ChangeSet({'value': 'title'})).operations[0]
- assert change['operation'] == 'update'
- assert change['type'] == 'title'
- assert get_title(p)['value'] == 'title'
-
- set_title(p, ChangeSet({'value': 'test'}))
- assert get_title(p)['value'] == 'test'
-
- self.leave(p)
-
-
- def test_title_op(self):
- p = 'test_title_op'
- self.enter(p)
-
- assert get_title(p)['value'] == ''
-
- cs = set_title(p, ChangeSet({'value': 'title'})).operations[0]
- assert cs['operation'] == 'update'
- assert cs['type'] == 'title'
- assert cs['value'] == 'title'
- assert get_title(p)['value'] == 'title'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'update'
- assert cs['type'] == 'title'
- assert cs['value'] == ''
- assert get_title(p)['value'] == ''
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == 'update'
- assert cs['type'] == 'title'
- assert cs['value'] == 'title'
- assert get_title(p)['value'] == 'title'
-
- cs = execute_undo(p, True).operations[0]
- assert cs['operation'] == 'update'
- assert cs['type'] == 'title'
- assert cs['value'] == ''
- assert get_title(p)['value'] == ''
-
- cs = execute_redo(p)
- assert len(cs.operations) == 0
- assert get_title(p)['value'] == ''
-
- self.leave(p)
-
-
- # 2 junction
-
-
- def test_junction(self):
- p = 'test_junction'
- self.enter(p)
-
- assert get_junction(p, 'j0') == {}
-
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- j0 = get_junction(p, 'j0')
- assert j0['x'] == 0.0
- assert j0['y'] == 10.0
- assert j0['elevation'] == 20.0
- assert j0['links'] == []
-
- set_junction(p, ChangeSet({'id': 'j0', 'x': 100.0, 'y': 200.0}))
- j0 = get_junction(p, 'j0')
- assert j0['x'] == 100.0
- assert j0['y'] == 200.0
-
- set_junction(p, ChangeSet({'id': 'j0', 'elevation': 100.0}))
- j0 = get_junction(p, 'j0')
- assert j0['elevation'] == 100.0
-
- # TODO: pattern
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- nodes = get_nodes(p)
- assert len(nodes) == 2
- assert nodes[0] == 'j0'
- assert nodes[1] == 'j1'
- assert is_junction(p, 'j0')
- assert is_junction(p, 'j1')
-
- delete_junction(p, ChangeSet({'id': 'j1'}))
- nodes = get_nodes(p)
- assert len(nodes) == 1
- assert nodes[0] == 'j0'
-
- delete_junction(p, ChangeSet({'id': 'j0'}))
- nodes = get_nodes(p)
- assert len(nodes) == 0
-
- assert get_junction(p, 'j0') == {}
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j3', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_pipe(p, ChangeSet({'id': 'p1', 'node1': 'j1', 'node2': 'j2', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
- add_pump(p, ChangeSet({'id': 'p2', 'node1': 'j1', 'node2': 'j2', 'power': 0.0}))
- add_valve(p, ChangeSet({'id': 'v1', 'node1': 'j2', 'node2': 'j3', 'diameter': 10.0, 'v_type': VALVES_TYPE_FCV, 'setting': 0.1, 'minor_loss': 0.5 }))
- assert get_junction(p, 'j1')['links'] == ['p1', 'p2']
- assert get_junction(p, 'j2')['links'] == ['p1', 'p2', 'v1']
- assert get_junction(p, 'j3')['links'] == ['v1']
-
- self.leave(p)
-
-
- def test_junction_op(self):
- p = 'test_junction_op'
- self.enter(p)
-
- cs = add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0})).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == JUNCTION
- assert cs['id'] == 'j0'
- assert cs['x'] == 0.0
- assert cs['y'] == 10.0
- assert cs['elevation'] == 20.0
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == JUNCTION
- assert cs['id'] == 'j0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == JUNCTION
- assert cs['id'] == 'j0'
- assert cs['x'] == 0.0
- assert cs['y'] == 10.0
- assert cs['elevation'] == 20.0
-
- cs = execute_undo(p, True).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == JUNCTION
- assert cs['id'] == 'j0'
-
- cs = execute_redo(p)
- assert len(cs.operations) == 0
-
- nodes = get_nodes(p)
- assert len(nodes) == 0
-
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0, 'demand': 100.0}))
-
- nodes = get_nodes(p)
- assert len(nodes) == 1
-
- cs = delete_junction(p, ChangeSet({'id': 'j0'})).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == JUNCTION
- assert cs['id'] == 'j0'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == JUNCTION
- assert cs['id'] == 'j0'
- assert cs['x'] == 0.0
- assert cs['y'] == 10.0
- assert cs['elevation'] == 20.0
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == JUNCTION
- assert cs['id'] == 'j0'
-
- cs = execute_undo(p, True).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == JUNCTION
- assert cs['id'] == 'j0'
- assert cs['x'] == 0.0
- assert cs['y'] == 10.0
- assert cs['elevation'] == 20.0
-
- cs = execute_redo(p)
- assert len(cs.operations) == 0
-
- nodes = get_nodes(p)
- assert len(nodes) == 1
-
- cs = set_junction(p, ChangeSet({'id': 'j0', 'x': 100.0, 'y': 200.0})).operations[0]
- assert cs['operation'] == 'update'
- assert cs['type'] == JUNCTION
- assert cs['id'] == 'j0'
- assert cs['x'] == 100.0
- assert cs['y'] == 200.0
- assert cs['elevation'] == 20.0
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'update'
- assert cs['type'] == JUNCTION
- assert cs['id'] == 'j0'
- assert cs['x'] == 0.0
- assert cs['y'] == 10.0
- assert cs['elevation'] == 20.0
-
- self.leave(p)
-
-
- def test_junction_del(self):
- p = 'test_junction_del'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_pipe(p, ChangeSet({'id': 'p0', 'node1': 'j0', 'node2': 'j1', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
- set_tag(p, ChangeSet({'t_type': TAG_TYPE_NODE, 'id': 'j0', 'tag': 'j0t' }))
- set_demand(p, ChangeSet({'junction': 'j0', 'demands': [{'demand': 10.0, 'pattern': None, 'category': 'x'}, {'demand': 20.0, 'pattern': None, 'category': None}]}))
- set_emitter(p, ChangeSet({'junction': 'j0', 'coefficient': 10.0}))
- set_quality(p, ChangeSet({'node': 'j0', 'quality': 10.0}))
- add_source(p, ChangeSet({'node': 'j0', 's_type': SOURCE_TYPE_CONCEN, 'strength': 10.0, 'pattern': None }))
- add_label(p, ChangeSet({'x': 0.0, 'y': 0.0, 'label': 'x', 'node': 'j0'}))
- assert is_junction(p, 'j0')
- assert is_junction(p, 'j1')
- assert is_pipe(p, 'p0')
- assert get_tag(p, TAG_TYPE_NODE, 'j0')['tag'] == 'j0t'
- assert get_demand(p, 'j0')['demands'] == [{'demand': 10.0, 'pattern': None, 'category': 'x'}, {'demand': 20.0, 'pattern': None, 'category': None}]
- assert get_emitter(p, 'j0')['coefficient'] == 10.0
- assert get_quality(p, 'j0')['quality'] == 10.0
- assert get_source(p, 'j0')['s_type'] == SOURCE_TYPE_CONCEN
- assert get_label(p, 0.0, 0.0)['node'] == 'j0'
-
- delete_junction(p, ChangeSet({'id': 'j0'}))
- assert is_junction(p, 'j0') == False
- assert is_junction(p, 'j1')
- assert is_pipe(p, 'p0') == False
- assert get_tag(p, TAG_TYPE_NODE, 'j0')['tag'] == None
- assert get_demand(p, 'j1')['demands'] == []
- assert get_emitter(p, 'j0')['coefficient'] == None
- assert get_quality(p, 'j0')['quality'] == None
- assert get_source(p, 'j0') == {}
- assert get_label(p, 0.0, 0.0)['node'] == None
-
- execute_undo(p)
- assert is_junction(p, 'j0')
- assert is_junction(p, 'j1')
- assert is_pipe(p, 'p0')
- assert get_tag(p, TAG_TYPE_NODE, 'j0')['tag'] == 'j0t'
- assert get_demand(p, 'j0')['demands'] == [{'demand': 10.0, 'pattern': None, 'category': 'x'}, {'demand': 20.0, 'pattern': None, 'category': None}]
- assert get_emitter(p, 'j0')['coefficient'] == 10.0
- assert get_quality(p, 'j0')['quality'] == 10.0
- assert get_source(p, 'j0')['s_type'] == SOURCE_TYPE_CONCEN
- assert get_label(p, 0.0, 0.0)['node'] == 'j0'
-
- execute_redo(p)
- assert is_junction(p, 'j0') == False
- assert is_junction(p, 'j1')
- assert is_pipe(p, 'p0') == False
- assert get_tag(p, TAG_TYPE_NODE, 'j0')['tag'] == None
- assert get_demand(p, 'j1')['demands'] == []
- assert get_emitter(p, 'j0')['coefficient'] == None
- assert get_quality(p, 'j0')['quality'] == None
- assert get_source(p, 'j0') == {}
- assert get_label(p, 0.0, 0.0)['node'] == None
-
- self.leave(p)
-
-
- # 3 reservoir
-
-
- def test_reservoir(self):
- p = 'test_reservoir'
- self.enter(p)
-
- assert get_reservoir(p, 'r0') == {}
-
- add_reservoir(p, ChangeSet({'id': 'r0', 'x': 0.0, 'y': 10.0, 'head': 20.0}))
- r0 = get_reservoir(p, 'r0')
- assert r0['x'] == 0.0
- assert r0['y'] == 10.0
- assert r0['head'] == 20.0
- assert r0['pattern'] == None
- assert r0['links'] == []
-
- set_reservoir(p, ChangeSet({'id': 'r0', 'x': 100.0, 'y': 200.0}))
- r0 = get_reservoir(p, 'r0')
- assert r0['x'] == 100.0
- assert r0['y'] == 200.0
-
- set_reservoir(p, ChangeSet({'id': 'r0', 'head': 100.0}))
- r0 = get_reservoir(p, 'r0')
- assert r0['head'] == 100.0
-
- # TODO: pattern
-
- add_reservoir(p, ChangeSet({'id': 'r1', 'x': 0.0, 'y': 10.0, 'head': 20.0}))
- nodes = get_nodes(p)
- assert len(nodes) == 2
- assert nodes[0] == 'r0'
- assert nodes[1] == 'r1'
- assert is_reservoir(p, 'r0')
- assert is_reservoir(p, 'r1')
-
- delete_reservoir(p, ChangeSet({'id': 'r1'}))
- nodes = get_nodes(p)
- assert len(nodes) == 1
- assert nodes[0] == 'r0'
-
- delete_reservoir(p, ChangeSet({'id': 'r0'}))
- nodes = get_nodes(p)
- assert len(nodes) == 0
-
- assert get_reservoir(p, 'r0') == {}
-
- add_reservoir(p, ChangeSet({'id': 'r1', 'x': 0.0, 'y': 10.0, 'head': 20.0}))
- add_reservoir(p, ChangeSet({'id': 'r2', 'x': 0.0, 'y': 10.0, 'head': 20.0}))
- add_reservoir(p, ChangeSet({'id': 'r3', 'x': 0.0, 'y': 10.0, 'head': 20.0}))
- add_pipe(p, ChangeSet({'id': 'p1', 'node1': 'r1', 'node2': 'r2', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
- add_pump(p, ChangeSet({'id': 'p2', 'node1': 'r1', 'node2': 'r2', 'power': 0.0}))
- add_valve(p, ChangeSet({'id': 'v1', 'node1': 'r2', 'node2': 'r3', 'diameter': 10.0, 'v_type': VALVES_TYPE_FCV, 'setting': 0.1, 'minor_loss': 0.5 }))
- assert get_reservoir(p, 'r1')['links'] == ['p1', 'p2']
- assert get_reservoir(p, 'r2')['links'] == ['p1', 'p2', 'v1']
- assert get_reservoir(p, 'r3')['links'] == ['v1']
-
- self.leave(p)
-
-
- def test_reservoir_op(self):
- p = 'test_reservoir_op'
- self.enter(p)
-
- cs = add_reservoir(p, ChangeSet({'id': 'r0', 'x': 0.0, 'y': 10.0, 'head': 20.0})).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == RESERVOIR
- assert cs['id'] == 'r0'
- assert cs['x'] == 0.0
- assert cs['y'] == 10.0
- assert cs['head'] == 20.0
- assert cs['pattern'] == None
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == RESERVOIR
- assert cs['id'] == 'r0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == RESERVOIR
- assert cs['id'] == 'r0'
- assert cs['x'] == 0.0
- assert cs['y'] == 10.0
- assert cs['head'] == 20.0
- assert cs['pattern'] == None
-
- cs = execute_undo(p, True).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == RESERVOIR
- assert cs['id'] == 'r0'
-
- cs = execute_redo(p)
- assert len(cs.operations) == 0
-
- nodes = get_nodes(p)
- assert len(nodes) == 0
-
- add_reservoir(p, ChangeSet({'id': 'r0', 'x': 0.0, 'y': 10.0, 'head': 20.0}))
-
- nodes = get_nodes(p)
- assert len(nodes) == 1
-
- cs = delete_reservoir(p, ChangeSet({'id': 'r0'})).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == RESERVOIR
- assert cs['id'] == 'r0'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == RESERVOIR
- assert cs['id'] == 'r0'
- assert cs['x'] == 0.0
- assert cs['y'] == 10.0
- assert cs['head'] == 20.0
- assert cs['pattern'] == None
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == RESERVOIR
- assert cs['id'] == 'r0'
-
- cs = execute_undo(p, True).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == RESERVOIR
- assert cs['id'] == 'r0'
- assert cs['x'] == 0.0
- assert cs['y'] == 10.0
- assert cs['head'] == 20.0
- assert cs['pattern'] == None
-
- cs = execute_redo(p)
- assert len(cs.operations) == 0
-
- nodes = get_nodes(p)
- assert len(nodes) == 1
-
- cs = set_reservoir(p, ChangeSet({'id': 'r0', 'x': 100.0, 'y': 200.0})).operations[0]
- assert cs['operation'] == 'update'
- assert cs['type'] == RESERVOIR
- assert cs['id'] == 'r0'
- assert cs['x'] == 100.0
- assert cs['y'] == 200.0
- assert cs['head'] == 20.0
- assert cs['pattern'] == None
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'update'
- assert cs['type'] == RESERVOIR
- assert cs['id'] == 'r0'
- assert cs['x'] == 0.0
- assert cs['y'] == 10.0
- assert cs['head'] == 20.0
- assert cs['pattern'] == None
-
- self.leave(p)
-
-
- def test_reservoir_del(self):
- p = 'test_reservoir_del'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_reservoir(p, ChangeSet({'id': 'r0', 'x': 0.0, 'y': 10.0, 'head': 20.0}))
- add_pipe(p, ChangeSet({'id': 'p0', 'node1': 'j0', 'node2': 'r0', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
- assert is_junction(p, 'j0')
- assert is_reservoir(p, 'r0')
- assert is_pipe(p, 'p0')
-
- delete_reservoir(p, ChangeSet({'id': 'r0'}))
- assert is_junction(p, 'j0')
- assert is_reservoir(p, 'r0') == False
- assert is_pipe(p, 'p0') == False
-
- execute_undo(p)
- assert is_junction(p, 'j0')
- assert is_reservoir(p, 'r0')
- assert is_pipe(p, 'p0')
-
- execute_redo(p)
- assert is_junction(p, 'j0')
- assert is_reservoir(p, 'r0') == False
- assert is_pipe(p, 'p0') == False
-
- self.leave(p)
-
-
- # 4 tank
-
-
- def test_tank(self):
- p = 'test_tank'
- self.enter(p)
-
- assert get_tank(p, 't0') == {}
-
- add_tank(p, ChangeSet({'id': 't0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0, 'init_level': 1.0, 'min_level': 0.0, 'max_level': 2.0, 'diameter': 10.0, 'min_vol': 100.0, 'vol_curve': None, 'overflow': OVERFLOW_NO}))
- t0 = get_tank(p, 't0')
- assert t0['x'] == 0.0
- assert t0['y'] == 10.0
- assert t0['elevation'] == 20.0
- assert t0['init_level'] == 1.0
- assert t0['min_level'] == 0.0
- assert t0['max_level'] == 2.0
- assert t0['diameter'] == 10.0
- assert t0['min_vol'] == 100.0
- assert t0['vol_curve'] == None
- assert t0['overflow'] == OVERFLOW_NO
- assert t0['links'] == []
-
- set_tank(p, ChangeSet({'id': 't0', 'x': 100.0, 'y': 200.0}))
- t0 = get_tank(p, 't0')
- assert t0['x'] == 100.0
- assert t0['y'] == 200.0
-
- set_tank(p, ChangeSet({'id': 't0', 'elevation': 100.0}))
- t0 = get_tank(p, 't0')
- assert t0['elevation'] == 100.0
-
- set_tank(p, ChangeSet({'id': 't0', 'init_level': 100.0, 'min_level': 50.0, 'max_level': 200.0}))
- t0 = get_tank(p, 't0')
- assert t0['init_level'] == 100.0
- assert t0['min_level'] == 50.0
- assert t0['max_level'] == 200.0
-
- set_tank(p, ChangeSet({'id': 't0', 'diameter': 100.0}))
- t0 = get_tank(p, 't0')
- assert t0['diameter'] == 100.0
-
- set_tank(p, ChangeSet({'id': 't0', 'min_vol': 200.0}))
- t0 = get_tank(p, 't0')
- assert t0['min_vol'] == 200.0
-
- # TODO: vol_curve
-
- set_tank(p, ChangeSet({'id': 't0', 'overflow': OVERFLOW_YES}))
- t0 = get_tank(p, 't0')
- assert t0['overflow'] == OVERFLOW_YES
-
- add_tank(p, ChangeSet({'id': 't1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0, 'init_level': 1.0, 'min_level': 0.0, 'max_level': 2.0, 'diameter': 10.0, 'min_vol': 100.0, 'vol_curve': None, 'overflow': OVERFLOW_NO}))
- nodes = get_nodes(p)
- assert len(nodes) == 2
- assert nodes[0] == 't0'
- assert nodes[1] == 't1'
- assert is_tank(p, 't0')
- assert is_tank(p, 't1')
-
- delete_tank(p, ChangeSet({'id': 't1'}))
- nodes = get_nodes(p)
- assert len(nodes) == 1
- assert nodes[0] == 't0'
-
- delete_tank(p, ChangeSet({'id': 't0'}))
- nodes = get_nodes(p)
- assert len(nodes) == 0
-
- assert get_tank(p, 't0') == {}
-
- add_tank(p, ChangeSet({'id': 't1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0, 'init_level': 1.0, 'min_level': 0.0, 'max_level': 2.0, 'diameter': 10.0, 'min_vol': 100.0, 'vol_curve': None, 'overflow': OVERFLOW_NO}))
- add_tank(p, ChangeSet({'id': 't2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0, 'init_level': 1.0, 'min_level': 0.0, 'max_level': 2.0, 'diameter': 10.0, 'min_vol': 100.0, 'vol_curve': None, 'overflow': OVERFLOW_NO}))
- add_tank(p, ChangeSet({'id': 't3', 'x': 0.0, 'y': 10.0, 'elevation': 20.0, 'init_level': 1.0, 'min_level': 0.0, 'max_level': 2.0, 'diameter': 10.0, 'min_vol': 100.0, 'vol_curve': None, 'overflow': OVERFLOW_NO}))
- add_pipe(p, ChangeSet({'id': 'p1', 'node1': 't1', 'node2': 't2', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
- add_pump(p, ChangeSet({'id': 'p2', 'node1': 't1', 'node2': 't2', 'power': 0.0}))
- add_valve(p, ChangeSet({'id': 'v1', 'node1': 't2', 'node2': 't3', 'diameter': 10.0, 'v_type': VALVES_TYPE_FCV, 'setting': 0.1, 'minor_loss': 0.5 }))
- assert get_tank(p, 't1')['links'] == ['p1', 'p2']
- assert get_tank(p, 't2')['links'] == ['p1', 'p2', 'v1']
- assert get_tank(p, 't3')['links'] == ['v1']
-
- self.leave(p)
-
-
- def test_tank_op(self):
- p = 'test_tank_op'
- self.enter(p)
-
- cs = add_tank(p, ChangeSet({'id': 't0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0, 'init_level': 1.0, 'min_level': 0.0, 'max_level': 2.0, 'diameter': 10.0, 'min_vol': 100.0, 'vol_curve': None, 'overflow': OVERFLOW_NO})).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == TANK
- assert cs['id'] == 't0'
- assert cs['x'] == 0.0
- assert cs['y'] == 10.0
- assert cs['elevation'] == 20.0
- assert cs['init_level'] == 1.0
- assert cs['min_level'] == 0.0
- assert cs['max_level'] == 2.0
- assert cs['diameter'] == 10.0
- assert cs['min_vol'] == 100.0
- assert cs['vol_curve'] == None
- assert cs['overflow'] == OVERFLOW_NO
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == TANK
- assert cs['id'] == 't0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == TANK
- assert cs['id'] == 't0'
- assert cs['x'] == 0.0
- assert cs['y'] == 10.0
- assert cs['elevation'] == 20.0
- assert cs['init_level'] == 1.0
- assert cs['min_level'] == 0.0
- assert cs['max_level'] == 2.0
- assert cs['diameter'] == 10.0
- assert cs['min_vol'] == 100.0
- assert cs['vol_curve'] == None
- assert cs['overflow'] == OVERFLOW_NO
-
- cs = execute_undo(p, True).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == TANK
- assert cs['id'] == 't0'
-
- cs = execute_redo(p)
- assert len(cs.operations) == 0
-
- nodes = get_nodes(p)
- assert len(nodes) == 0
-
- add_tank(p, ChangeSet({'id': 't0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0, 'init_level': 1.0, 'min_level': 0.0, 'max_level': 2.0, 'diameter': 10.0, 'min_vol': 100.0, 'vol_curve': None, 'overflow': OVERFLOW_NO}))
-
- nodes = get_nodes(p)
- assert len(nodes) == 1
-
- cs = delete_tank(p, ChangeSet({'id': 't0'})).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == TANK
- assert cs['id'] == 't0'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == TANK
- assert cs['id'] == 't0'
- assert cs['x'] == 0.0
- assert cs['y'] == 10.0
- assert cs['elevation'] == 20.0
- assert cs['init_level'] == 1.0
- assert cs['min_level'] == 0.0
- assert cs['max_level'] == 2.0
- assert cs['diameter'] == 10.0
- assert cs['min_vol'] == 100.0
- assert cs['vol_curve'] == None
- assert cs['overflow'] == OVERFLOW_NO
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == TANK
- assert cs['id'] == 't0'
-
- cs = execute_undo(p, True).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == TANK
- assert cs['id'] == 't0'
- assert cs['x'] == 0.0
- assert cs['y'] == 10.0
- assert cs['elevation'] == 20.0
- assert cs['init_level'] == 1.0
- assert cs['min_level'] == 0.0
- assert cs['max_level'] == 2.0
- assert cs['diameter'] == 10.0
- assert cs['min_vol'] == 100.0
- assert cs['vol_curve'] == None
- assert cs['overflow'] == OVERFLOW_NO
-
- cs = execute_redo(p)
- assert len(cs.operations) == 0
-
- nodes = get_nodes(p)
- assert len(nodes) == 1
-
- cs = set_tank(p, ChangeSet({'id': 't0', 'x': 100.0, 'y': 200.0})).operations[0]
- assert cs['operation'] == 'update'
- assert cs['type'] == TANK
- assert cs['id'] == 't0'
- assert cs['x'] == 100.0
- assert cs['y'] == 200.0
- assert cs['elevation'] == 20.0
- assert cs['init_level'] == 1.0
- assert cs['min_level'] == 0.0
- assert cs['max_level'] == 2.0
- assert cs['diameter'] == 10.0
- assert cs['min_vol'] == 100.0
- assert cs['vol_curve'] == None
- assert cs['overflow'] == OVERFLOW_NO
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'update'
- assert cs['type'] == TANK
- assert cs['id'] == 't0'
- assert cs['x'] == 0.0
- assert cs['y'] == 10.0
- assert cs['elevation'] == 20.0
- assert cs['init_level'] == 1.0
- assert cs['min_level'] == 0.0
- assert cs['max_level'] == 2.0
- assert cs['diameter'] == 10.0
- assert cs['min_vol'] == 100.0
- assert cs['vol_curve'] == None
- assert cs['overflow'] == OVERFLOW_NO
-
- self.leave(p)
-
-
- def test_tank_del(self):
- p = 'test_tank_del'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_tank(p, ChangeSet({'id': 't0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0, 'init_level': 1.0, 'min_level': 0.0, 'max_level': 2.0, 'diameter': 10.0, 'min_vol': 100.0, 'vol_curve': None, 'overflow': OVERFLOW_NO}))
- add_pipe(p, ChangeSet({'id': 'p0', 'node1': 'j0', 'node2': 't0', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
- set_tank_reaction(p, ChangeSet({'tank': 't0', 'value': 10.0}))
- assert is_junction(p, 'j0')
- assert is_tank(p, 't0')
- assert is_pipe(p, 'p0')
- assert get_tank_reaction(p, 't0')['value'] == 10.0
-
- delete_tank(p, ChangeSet({'id': 't0'}))
- assert is_junction(p, 'j0')
- assert is_tank(p, 't0') == False
- assert is_pipe(p, 'p0') == False
- assert get_tank_reaction(p, 't0')['value'] == None
-
- execute_undo(p)
- assert is_junction(p, 'j0')
- assert is_tank(p, 't0')
- assert is_pipe(p, 'p0')
- assert get_tank_reaction(p, 't0')['value'] == 10.0
-
- execute_redo(p)
- assert is_junction(p, 'j0')
- assert is_tank(p, 't0') == False
- assert is_pipe(p, 'p0') == False
- assert get_tank_reaction(p, 't0')['value'] == None
-
- self.leave(p)
-
-
- # 5 pipe
-
-
- def test_pipe(self):
- p = 'test_pipe'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j3', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j4', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- assert is_junction(p, 'j1')
- assert is_junction(p, 'j2')
- assert is_junction(p, 'j3')
- assert is_junction(p, 'j4')
-
- assert get_pipe(p, 'p0') == {}
-
- add_pipe(p, ChangeSet({'id': 'p0', 'node1': 'j1', 'node2': 'j2', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
- p0 = get_pipe(p, 'p0')
- assert p0['node1'] == 'j1'
- assert p0['node2'] == 'j2'
- assert p0['length'] == 100.0
- assert p0['diameter'] == 10.0
- assert p0['roughness'] == 0.1
- assert p0['minor_loss'] == 0.5
- assert p0['status'] == PIPE_STATUS_OPEN
-
- set_pipe(p, ChangeSet({'id': 'p0', 'node1': 'j3', 'node2': 'j4'}))
- p0 = get_pipe(p, 'p0')
- assert p0['node1'] == 'j3'
- assert p0['node2'] == 'j4'
-
- set_pipe(p, ChangeSet({'id': 'p0', 'length': 200.0}))
- p0 = get_pipe(p, 'p0')
- assert p0['length'] == 200.0
-
- set_pipe(p, ChangeSet({'id': 'p0', 'diameter': 100.0}))
- p0 = get_pipe(p, 'p0')
- assert p0['diameter'] == 100.0
-
- set_pipe(p, ChangeSet({'id': 'p0', 'roughness': 0.2}))
- p0 = get_pipe(p, 'p0')
- assert p0['roughness'] == 0.2
-
- set_pipe(p, ChangeSet({'id': 'p0', 'minor_loss': 0.1}))
- p0 = get_pipe(p, 'p0')
- assert p0['minor_loss'] == 0.1
-
- set_pipe(p, ChangeSet({'id': 'p0', 'status': PIPE_STATUS_CLOSED}))
- p0 = get_pipe(p, 'p0')
- assert p0['status'] == PIPE_STATUS_CLOSED
-
- add_pipe(p, ChangeSet({'id': 'p1', 'node1': 'j1', 'node2': 'j2', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
- links = get_links(p)
- assert len(links) == 2
- assert links[0] == 'p0'
- assert links[1] == 'p1'
- assert is_pipe(p, 'p0')
- assert is_pipe(p, 'p1')
-
- delete_pipe(p, ChangeSet({'id': 'p1'}))
- links = get_links(p)
- assert len(links) == 1
- assert links[0] == 'p0'
-
- delete_pipe(p, ChangeSet({'id': 'p0'}))
- links = get_links(p)
- assert len(links) == 0
-
- assert get_pipe(p, 'p0') == {}
-
- self.leave(p)
-
-
- def test_pipe_op(self):
- p = 'test_pipe_op'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j3', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j4', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- assert is_junction(p, 'j1')
- assert is_junction(p, 'j2')
- assert is_junction(p, 'j3')
- assert is_junction(p, 'j4')
-
- cs = add_pipe(p, ChangeSet({'id': 'p0', 'node1': 'j1', 'node2': 'j2', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN })).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == PIPE
- assert cs['id'] == 'p0'
- assert cs['node1'] == 'j1'
- assert cs['node2'] == 'j2'
- assert cs['length'] == 100.0
- assert cs['diameter'] == 10.0
- assert cs['roughness'] == 0.1
- assert cs['minor_loss'] == 0.5
- assert cs['status'] == PIPE_STATUS_OPEN
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == PIPE
- assert cs['id'] == 'p0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == PIPE
- assert cs['id'] == 'p0'
- assert cs['node1'] == 'j1'
- assert cs['node2'] == 'j2'
- assert cs['length'] == 100.0
- assert cs['diameter'] == 10.0
- assert cs['roughness'] == 0.1
- assert cs['minor_loss'] == 0.5
- assert cs['status'] == PIPE_STATUS_OPEN
-
- cs = execute_undo(p, True).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == PIPE
- assert cs['id'] == 'p0'
-
- cs = execute_redo(p)
- assert len(cs.operations) == 0
-
- links = get_links(p)
- assert len(links) == 0
-
- add_pipe(p, ChangeSet({'id': 'p0', 'node1': 'j1', 'node2': 'j2', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
-
- links = get_links(p)
- assert len(links) == 1
-
- cs = delete_pipe(p, ChangeSet({'id': 'p0'})).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == PIPE
- assert cs['id'] == 'p0'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == PIPE
- assert cs['id'] == 'p0'
- assert cs['node1'] == 'j1'
- assert cs['node2'] == 'j2'
- assert cs['length'] == 100.0
- assert cs['diameter'] == 10.0
- assert cs['roughness'] == 0.1
- assert cs['minor_loss'] == 0.5
- assert cs['status'] == PIPE_STATUS_OPEN
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == PIPE
- assert cs['id'] == 'p0'
-
- cs = execute_undo(p, True).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == PIPE
- assert cs['id'] == 'p0'
- assert cs['node1'] == 'j1'
- assert cs['node2'] == 'j2'
- assert cs['length'] == 100.0
- assert cs['diameter'] == 10.0
- assert cs['roughness'] == 0.1
- assert cs['minor_loss'] == 0.5
- assert cs['status'] == PIPE_STATUS_OPEN
-
- cs = execute_redo(p)
- assert len(cs.operations) == 0
-
- links = get_links(p)
- assert len(links) == 1
-
- cs = set_pipe(p, ChangeSet({'id': 'p0', 'node1': 'j3', 'node2': 'j4'})).operations[0]
- assert cs['operation'] == 'update'
- assert cs['type'] == PIPE
- assert cs['id'] == 'p0'
- assert cs['node1'] == 'j3'
- assert cs['node2'] == 'j4'
- assert cs['length'] == 100.0
- assert cs['diameter'] == 10.0
- assert cs['roughness'] == 0.1
- assert cs['minor_loss'] == 0.5
- assert cs['status'] == PIPE_STATUS_OPEN
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'update'
- assert cs['type'] == PIPE
- assert cs['id'] == 'p0'
- assert cs['node1'] == 'j1'
- assert cs['node2'] == 'j2'
- assert cs['length'] == 100.0
- assert cs['diameter'] == 10.0
- assert cs['roughness'] == 0.1
- assert cs['minor_loss'] == 0.5
- assert cs['status'] == PIPE_STATUS_OPEN
-
- self.leave(p)
-
-
- def test_pipe_del(self):
- p = 'test_pipe_del'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_pipe(p, ChangeSet({'id': 'p0', 'node1': 'j0', 'node2': 'j1', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
- set_tag(p, ChangeSet({'t_type': TAG_TYPE_LINK, 'id': 'p0', 'tag': 'p0t' }))
- set_status(p, ChangeSet({'link': 'p0', 'status': LINK_STATUS_OPEN, 'setting': 10.0}))
- set_pipe_reaction(p, ChangeSet({'pipe': 'p0', 'bulk': 10.0, 'wall': 20.0}))
- set_vertex(p, ChangeSet({'link' : 'p0', 'coords': [{'x': 1.0, 'y': 2.0}, {'x': 2.0, 'y': 1.0}]}))
- assert is_pipe(p, 'p0')
- assert get_tag(p, TAG_TYPE_LINK, 'p0')['tag'] == 'p0t'
- assert get_status(p, 'p0')['status'] == LINK_STATUS_OPEN
- assert get_status(p, 'p0')['setting'] == 10.0
- assert get_pipe_reaction(p, 'p0')['bulk'] == 10.0
- assert get_pipe_reaction(p, 'p0')['wall'] == 20.0
- assert get_vertex(p, 'p0')['coords'] == [{'x': 1.0, 'y': 2.0}, {'x': 2.0, 'y': 1.0}]
-
- delete_pipe(p, ChangeSet({'id': 'p0'}))
- assert is_pipe(p, 'p0') == False
- assert get_tag(p, TAG_TYPE_LINK, 'p0')['tag'] == None
- assert get_status(p, 'p0')['status'] == None
- assert get_status(p, 'p0')['setting'] == None
- assert get_pipe_reaction(p, 'p0')['bulk'] == None
- assert get_pipe_reaction(p, 'p0')['wall'] == None
- assert get_vertex(p, 'p0')['coords'] == []
-
- execute_undo(p)
- assert is_pipe(p, 'p0')
- assert get_tag(p, TAG_TYPE_LINK, 'p0')['tag'] == 'p0t'
- assert get_status(p, 'p0')['status'] == LINK_STATUS_OPEN
- assert get_status(p, 'p0')['setting'] == 10.0
- assert get_pipe_reaction(p, 'p0')['bulk'] == 10.0
- assert get_pipe_reaction(p, 'p0')['wall'] == 20.0
- assert get_vertex(p, 'p0')['coords'] == [{'x': 1.0, 'y': 2.0}, {'x': 2.0, 'y': 1.0}]
-
- execute_redo(p)
- assert is_pipe(p, 'p0') == False
- assert get_tag(p, TAG_TYPE_LINK, 'p0')['tag'] == None
- assert get_status(p, 'p0')['status'] == None
- assert get_status(p, 'p0')['setting'] == None
- assert get_pipe_reaction(p, 'p0')['bulk'] == None
- assert get_pipe_reaction(p, 'p0')['wall'] == None
- assert get_vertex(p, 'p0')['coords'] == []
-
- self.leave(p)
-
-
- # 6 pump
-
-
- def test_pump(self):
- p = 'test_pump'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j3', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j4', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- assert is_junction(p, 'j1')
- assert is_junction(p, 'j2')
- assert is_junction(p, 'j3')
- assert is_junction(p, 'j4')
-
- assert get_pump(p, 'p0') == {}
-
- add_pump(p, ChangeSet({'id': 'p0', 'node1': 'j1', 'node2': 'j2', 'power': 0.0}))
- p0 = get_pump(p, 'p0')
- assert p0['node1'] == 'j1'
- assert p0['node2'] == 'j2'
- assert p0['power'] == 0.0
- assert p0['head'] == None
- assert p0['speed'] == None
- assert p0['pattern'] == None
-
- set_pump(p, ChangeSet({'id': 'p0', 'node1': 'j3', 'node2': 'j4'}))
- p0 = get_pump(p, 'p0')
- assert p0['node1'] == 'j3'
- assert p0['node2'] == 'j4'
-
- set_pump(p, ChangeSet({'id': 'p0', 'power': 100.0}))
- p0 = get_pump(p, 'p0')
- assert p0['power'] == 100.0
-
- add_pump(p, ChangeSet({'id': 'p1', 'node1': 'j1', 'node2': 'j2', 'power': 0.0}))
- links = get_links(p)
- assert len(links) == 2
- assert links[0] == 'p0'
- assert links[1] == 'p1'
- assert is_pump(p, 'p0')
- assert is_pump(p, 'p1')
-
- delete_pump(p, ChangeSet({'id': 'p1'}))
- links = get_links(p)
- assert len(links) == 1
- assert links[0] == 'p0'
-
- delete_pump(p, ChangeSet({'id': 'p0'}))
- links = get_links(p)
- assert len(links) == 0
-
- assert get_pump(p, 'p0') == {}
-
- self.leave(p)
-
-
- def test_pump_op(self):
- p = 'test_pump_op'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j3', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j4', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- assert is_junction(p, 'j1')
- assert is_junction(p, 'j2')
- assert is_junction(p, 'j3')
- assert is_junction(p, 'j4')
-
- cs = add_pump(p, ChangeSet({'id': 'p0', 'node1': 'j1', 'node2': 'j2', 'power': 0.0})).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == PUMP
- assert cs['id'] == 'p0'
- assert cs['node1'] == 'j1'
- assert cs['node2'] == 'j2'
- assert cs['power'] == 0.0
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == PUMP
- assert cs['id'] == 'p0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == PUMP
- assert cs['id'] == 'p0'
- assert cs['node1'] == 'j1'
- assert cs['node2'] == 'j2'
-
- cs = execute_undo(p, True).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == PUMP
- assert cs['id'] == 'p0'
-
- cs = execute_redo(p)
- assert len(cs.operations) == 0
-
- links = get_links(p)
- assert len(links) == 0
-
- add_pump(p, ChangeSet({'id': 'p0', 'node1': 'j1', 'node2': 'j2', 'power': 0.0}))
-
- links = get_links(p)
- assert len(links) == 1
-
- cs = delete_pump(p, ChangeSet({'id': 'p0'})).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == PUMP
- assert cs['id'] == 'p0'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == PUMP
- assert cs['id'] == 'p0'
- assert cs['node1'] == 'j1'
- assert cs['node2'] == 'j2'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == PUMP
- assert cs['id'] == 'p0'
-
- cs = execute_undo(p, True).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == PUMP
- assert cs['id'] == 'p0'
- assert cs['node1'] == 'j1'
- assert cs['node2'] == 'j2'
-
- cs = execute_redo(p)
- assert len(cs.operations) == 0
-
- links = get_links(p)
- assert len(links) == 1
-
- cs = set_pump(p, ChangeSet({'id': 'p0', 'node1': 'j3', 'node2': 'j4'})).operations[0]
- assert cs['operation'] == 'update'
- assert cs['type'] == PUMP
- assert cs['id'] == 'p0'
- assert cs['node1'] == 'j3'
- assert cs['node2'] == 'j4'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'update'
- assert cs['type'] == PUMP
- assert cs['id'] == 'p0'
- assert cs['node1'] == 'j1'
- assert cs['node2'] == 'j2'
-
- cs = set_pump(p, ChangeSet({'id': 'p0', 'power': 100.0})).operations[0]
- assert cs['operation'] == 'update'
- assert cs['type'] == PUMP
- assert cs['id'] == 'p0'
- assert cs['power'] == 100.0
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'update'
- assert cs['type'] == PUMP
- assert cs['id'] == 'p0'
- assert cs['power'] == 0.0
-
- self.leave(p)
-
-
- def test_pump_del(self):
- p = 'test_pump_del'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_pump(p, ChangeSet({'id': 'p0', 'node1': 'j0', 'node2': 'j1', 'power': 0.0}))
- set_tag(p, ChangeSet({'t_type': TAG_TYPE_LINK, 'id': 'p0', 'tag': 'p0t' }))
- set_status(p, ChangeSet({'link': 'p0', 'status': LINK_STATUS_OPEN, 'setting': 10.0}))
- set_pump_energy(p, ChangeSet({'pump' : 'p0', 'price': 1.0}))
- set_vertex(p, ChangeSet({'link' : 'p0', 'coords': [{'x': 1.0, 'y': 2.0}, {'x': 2.0, 'y': 1.0}]}))
- assert is_pump(p, 'p0')
- assert get_tag(p, TAG_TYPE_LINK, 'p0')['tag'] == 'p0t'
- assert get_status(p, 'p0')['status'] == LINK_STATUS_OPEN
- assert get_status(p, 'p0')['setting'] == 10.0
- assert get_pump_energy(p, 'p0')['price'] == 1.0
- assert get_vertex(p, 'p0')['coords'] == [{'x': 1.0, 'y': 2.0}, {'x': 2.0, 'y': 1.0}]
-
- delete_pump(p, ChangeSet({'id': 'p0'}))
- assert is_pump(p, 'p0') == False
- assert get_tag(p, TAG_TYPE_LINK, 'p0')['tag'] == None
- assert get_status(p, 'p0')['status'] == None
- assert get_status(p, 'p0')['setting'] == None
- assert get_pump_energy(p, 'p0')['price'] == None
- assert get_vertex(p, 'p0')['coords'] == []
-
- execute_undo(p)
- assert is_pump(p, 'p0')
- assert get_tag(p, TAG_TYPE_LINK, 'p0')['tag'] == 'p0t'
- assert get_status(p, 'p0')['status'] == LINK_STATUS_OPEN
- assert get_status(p, 'p0')['setting'] == 10.0
- assert get_pump_energy(p, 'p0')['price'] == 1.0
- assert get_vertex(p, 'p0')['coords'] == [{'x': 1.0, 'y': 2.0}, {'x': 2.0, 'y': 1.0}]
-
- execute_redo(p)
- assert is_pump(p, 'p0') == False
- assert get_tag(p, TAG_TYPE_LINK, 'p0')['tag'] == None
- assert get_status(p, 'p0')['status'] == None
- assert get_status(p, 'p0')['setting'] == None
- assert get_pump_energy(p, 'p0')['price'] == None
- assert get_vertex(p, 'p0')['coords'] == []
-
- self.leave(p)
-
-
- # 7 valve
-
-
- def test_valve(self):
- p = 'test_valve'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j3', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j4', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- assert is_junction(p, 'j1')
- assert is_junction(p, 'j2')
- assert is_junction(p, 'j3')
- assert is_junction(p, 'j4')
-
- assert get_valve(p, 'v0') == {}
-
- add_valve(p, ChangeSet({'id': 'v0', 'node1': 'j1', 'node2': 'j2', 'diameter': 10.0, 'v_type': VALVES_TYPE_FCV, 'setting': '0.1', 'minor_loss': 0.5 }))
- v0 = get_valve(p, 'v0')
- assert v0['node1'] == 'j1'
- assert v0['node2'] == 'j2'
- assert v0['diameter'] == 10.0
- assert v0['v_type'] == VALVES_TYPE_FCV
- assert v0['setting'] == '0.1'
- assert v0['minor_loss'] == 0.5
-
- set_valve(p, ChangeSet({'id': 'v0', 'node1': 'j3', 'node2': 'j4'}))
- v0 = get_valve(p, 'v0')
- assert v0['node1'] == 'j3'
- assert v0['node2'] == 'j4'
-
- set_valve(p, ChangeSet({'id': 'v0', 'diameter': 100.0}))
- v0 = get_valve(p, 'v0')
- assert v0['diameter'] == 100.0
-
- set_valve(p, ChangeSet({'id': 'v0', 'v_type': VALVES_TYPE_GPV}))
- v0 = get_valve(p, 'v0')
- assert v0['v_type'] == VALVES_TYPE_GPV
-
- set_valve(p, ChangeSet({'id': 'v0', 'setting': '0.2'}))
- v0 = get_valve(p, 'v0')
- assert v0['setting'] == '0.2'
-
- set_valve(p, ChangeSet({'id': 'v0', 'minor_loss': 0.1}))
- v0 = get_valve(p, 'v0')
- assert v0['minor_loss'] == 0.1
-
- add_valve(p, ChangeSet({'id': 'v1', 'node1': 'j1', 'node2': 'j2', 'diameter': 10.0, 'v_type': VALVES_TYPE_FCV, 'setting': '0.1', 'minor_loss': 0.5 }))
- links = get_links(p)
- assert len(links) == 2
- assert links[0] == 'v0'
- assert links[1] == 'v1'
- assert is_valve(p, 'v0')
- assert is_valve(p, 'v1')
-
- delete_valve(p, ChangeSet({'id': 'v1'}))
- links = get_links(p)
- assert len(links) == 1
- assert links[0] == 'v0'
-
- delete_valve(p, ChangeSet({'id': 'v0'}))
- links = get_links(p)
- assert len(links) == 0
-
- assert get_valve(p, 'v0') == {}
-
- self.leave(p)
-
-
- def test_valve_op(self):
- p = 'test_valve_op'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j3', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j4', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- assert is_junction(p, 'j1')
- assert is_junction(p, 'j2')
- assert is_junction(p, 'j3')
- assert is_junction(p, 'j4')
-
- cs = add_valve(p, ChangeSet({'id': 'v0', 'node1': 'j1', 'node2': 'j2', 'diameter': 10.0, 'v_type': VALVES_TYPE_FCV, 'setting': '0.1', 'minor_loss': 0.5 })).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == VALVE
- assert cs['id'] == 'v0'
- assert cs['node1'] == 'j1'
- assert cs['node2'] == 'j2'
- assert cs['diameter'] == 10.0
- assert cs['v_type'] == VALVES_TYPE_FCV
- assert cs['setting'] == '0.1'
- assert cs['minor_loss'] == 0.5
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == VALVE
- assert cs['id'] == 'v0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == VALVE
- assert cs['id'] == 'v0'
- assert cs['node1'] == 'j1'
- assert cs['node2'] == 'j2'
- assert cs['diameter'] == 10.0
- assert cs['v_type'] == VALVES_TYPE_FCV
- assert cs['setting'] == '0.1'
- assert cs['minor_loss'] == 0.5
-
- cs = execute_undo(p, True).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == VALVE
- assert cs['id'] == 'v0'
-
- cs = execute_redo(p)
- assert len(cs.operations) == 0
-
- links = get_links(p)
- assert len(links) == 0
-
- add_valve(p, ChangeSet({'id': 'v0', 'node1': 'j1', 'node2': 'j2', 'diameter': 10.0, 'v_type': VALVES_TYPE_FCV, 'setting': '0.1', 'minor_loss': 0.5 }))
-
- links = get_links(p)
- assert len(links) == 1
-
- cs = delete_valve(p, ChangeSet({'id': 'v0'})).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == VALVE
- assert cs['id'] == 'v0'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == VALVE
- assert cs['id'] == 'v0'
- assert cs['node1'] == 'j1'
- assert cs['node2'] == 'j2'
- assert cs['diameter'] == 10.0
- assert cs['v_type'] == VALVES_TYPE_FCV
- assert cs['setting'] == '0.1'
- assert cs['minor_loss'] == 0.5
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == 'delete'
- assert cs['type'] == VALVE
- assert cs['id'] == 'v0'
-
- cs = execute_undo(p, True).operations[0]
- assert cs['operation'] == 'add'
- assert cs['type'] == VALVE
- assert cs['id'] == 'v0'
- assert cs['node1'] == 'j1'
- assert cs['node2'] == 'j2'
- assert cs['diameter'] == 10.0
- assert cs['v_type'] == VALVES_TYPE_FCV
- assert cs['setting'] == '0.1'
- assert cs['minor_loss'] == 0.5
-
- cs = execute_redo(p)
- assert len(cs.operations) == 0
-
- links = get_links(p)
- assert len(links) == 1
-
- cs = set_valve(p, ChangeSet({'id': 'v0', 'node1': 'j3', 'node2': 'j4'})).operations[0]
- assert cs['operation'] == 'update'
- assert cs['type'] == VALVE
- assert cs['id'] == 'v0'
- assert cs['node1'] == 'j3'
- assert cs['node2'] == 'j4'
- assert cs['diameter'] == 10.0
- assert cs['v_type'] == VALVES_TYPE_FCV
- assert cs['setting'] == '0.1'
- assert cs['minor_loss'] == 0.5
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == 'update'
- assert cs['type'] == VALVE
- assert cs['id'] == 'v0'
- assert cs['node1'] == 'j1'
- assert cs['node2'] == 'j2'
- assert cs['diameter'] == 10.0
- assert cs['v_type'] == VALVES_TYPE_FCV
- assert cs['setting'] == '0.1'
- assert cs['minor_loss'] == 0.5
-
- self.leave(p)
-
-
- def test_valve_del(self):
- p = 'test_valve_del'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_valve(p, ChangeSet({'id': 'v0', 'node1': 'j0', 'node2': 'j1', 'diameter': 10.0, 'v_type': VALVES_TYPE_FCV, 'setting': '0.1', 'minor_loss': 0.5 }))
- set_tag(p, ChangeSet({'t_type': TAG_TYPE_LINK, 'id': 'v0', 'tag': 'v0t' }))
- set_status(p, ChangeSet({'link': 'v0', 'status': LINK_STATUS_OPEN, 'setting': 10.0}))
- set_vertex(p, ChangeSet({'link' : 'v0', 'coords': [{'x': 1.0, 'y': 2.0}, {'x': 2.0, 'y': 1.0}]}))
- assert is_valve(p, 'v0')
- assert get_tag(p, TAG_TYPE_LINK, 'v0')['tag'] == 'v0t'
- assert get_status(p, 'v0')['status'] == LINK_STATUS_OPEN
- assert get_status(p, 'v0')['setting'] == 10.0
- assert get_vertex(p, 'v0')['coords'] == [{'x': 1.0, 'y': 2.0}, {'x': 2.0, 'y': 1.0}]
-
- delete_valve(p, ChangeSet({'id': 'v0'}))
- assert is_valve(p, 'v0') == False
- assert get_tag(p, TAG_TYPE_LINK, 'v0')['tag'] == None
- assert get_status(p, 'v0')['status'] == None
- assert get_status(p, 'v0')['setting'] == None
- assert get_vertex(p, 'v0')['coords'] == []
-
- execute_undo(p)
- assert is_valve(p, 'v0')
- assert get_tag(p, TAG_TYPE_LINK, 'v0')['tag'] == 'v0t'
- assert get_status(p, 'v0')['status'] == LINK_STATUS_OPEN
- assert get_status(p, 'v0')['setting'] == 10.0
- assert get_vertex(p, 'v0')['coords'] == [{'x': 1.0, 'y': 2.0}, {'x': 2.0, 'y': 1.0}]
-
- execute_redo(p)
- assert is_valve(p, 'v0') == False
- assert get_tag(p, TAG_TYPE_LINK, 'v0')['tag'] == None
- assert get_status(p, 'v0')['status'] == None
- assert get_status(p, 'v0')['setting'] == None
- assert get_vertex(p, 'v0')['coords'] == []
-
- self.leave(p)
-
-
- # 8 tag
-
-
- def test_tag(self):
- p = 'test_tag'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- assert is_junction(p, 'j1')
- assert is_junction(p, 'j2')
-
- add_pipe(p, ChangeSet({'id': 'p0', 'node1': 'j1', 'node2': 'j2', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
- assert is_pipe(p, 'p0')
-
- t = get_tag(p, TAG_TYPE_NODE, 'j1')
- assert t['tag'] == None
- set_tag(p, ChangeSet({'t_type': TAG_TYPE_NODE, 'id': 'j1', 'tag': 'j1t' }))
- t = get_tag(p, TAG_TYPE_NODE, 'j1')
- assert t['tag'] == 'j1t'
-
- tags = get_tags(p)
- assert len(tags) == 1
- assert tags[0]['t_type'] == TAG_TYPE_NODE
- assert tags[0]['id'] == 'j1'
- assert tags[0]['tag'] == 'j1t'
-
- set_tag(p, ChangeSet({'t_type': TAG_TYPE_NODE, 'id': 'j1', 'tag': None }))
- t = get_tag(p, TAG_TYPE_NODE, 'j1')
- assert t['tag'] == None
-
- tags = get_tags(p)
- assert len(tags) == 0
-
- t = get_tag(p, TAG_TYPE_NODE, 'j2')
- assert t['tag'] == None
- set_tag(p, ChangeSet({'t_type': TAG_TYPE_NODE, 'id': 'j2', 'tag': 'j2t' }))
- t = get_tag(p, TAG_TYPE_NODE, 'j2')
- assert t['tag'] == 'j2t'
-
- t = get_tag(p, TAG_TYPE_LINK, 'p0')
- assert t['tag'] == None
- set_tag(p, ChangeSet({'t_type': TAG_TYPE_LINK, 'id': 'p0', 'tag': 'p0t' }))
- t = get_tag(p, TAG_TYPE_LINK, 'p0')
- assert t['tag'] == 'p0t'
-
- tags = get_tags(p)
- assert len(tags) == 2
- assert tags[0]['t_type'] == TAG_TYPE_NODE
- assert tags[0]['id'] == 'j2'
- assert tags[0]['tag'] == 'j2t'
- assert tags[1]['t_type'] == TAG_TYPE_LINK
- assert tags[1]['id'] == 'p0'
- assert tags[1]['tag'] == 'p0t'
-
- self.leave(p)
-
-
- def test_tag_op(self):
- p = 'test_tag_op'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- assert is_junction(p, 'j1')
- assert is_junction(p, 'j2')
-
- add_pipe(p, ChangeSet({'id': 'p0', 'node1': 'j1', 'node2': 'j2', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
- assert is_pipe(p, 'p0')
-
- cs = set_tag(p, ChangeSet({'t_type': TAG_TYPE_NODE, 'id': 'j1', 'tag': 'j1t' })).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'tag'
- assert cs['t_type'] == TAG_TYPE_NODE
- assert cs['id'] == 'j1'
- assert cs['tag'] == 'j1t'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'tag'
- assert cs['t_type'] == TAG_TYPE_NODE
- assert cs['id'] == 'j1'
- assert cs['tag'] == None
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'tag'
- assert cs['t_type'] == TAG_TYPE_NODE
- assert cs['id'] == 'j1'
- assert cs['tag'] == 'j1t'
-
- cs = set_tag(p, ChangeSet({'t_type': TAG_TYPE_LINK, 'id': 'p0', 'tag': 'p0t' })).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'tag'
- assert cs['t_type'] == TAG_TYPE_LINK
- assert cs['id'] == 'p0'
- assert cs['tag'] == 'p0t'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'tag'
- assert cs['t_type'] == TAG_TYPE_LINK
- assert cs['id'] == 'p0'
- assert cs['tag'] == None
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'tag'
- assert cs['t_type'] == TAG_TYPE_LINK
- assert cs['id'] == 'p0'
- assert cs['tag'] == 'p0t'
-
- self.leave(p)
-
-
- # 9 demand
-
-
- def test_demand(self):
- p = 'test_demand'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- assert is_junction(p, 'j1')
-
- d = get_demand(p, 'j1')
- assert d['junction'] == 'j1'
- assert d['demands'] == []
-
- set_demand(p, ChangeSet({'junction': 'j1', 'demands': [{'demand': 10.0, 'pattern': None, 'category': 'x'},
- {'demand': 20.0, 'pattern': None, 'category': None}]}))
-
- d = get_demand(p, 'j1')
- assert d['junction'] == 'j1'
- ds = d['demands']
- assert len(ds) == 2
- assert ds[0]['demand'] == 10.0
- assert ds[0]['pattern'] == None
- assert ds[0]['category'] == 'x'
- assert ds[1]['demand'] == 20.0
- assert ds[1]['pattern'] == None
- assert ds[1]['category'] == None
-
- set_demand(p, ChangeSet({'junction': 'j1', 'demands': []}))
-
- d = get_demand(p, 'j1')
- assert d['junction'] == 'j1'
- assert d['demands'] == []
-
- self.leave(p)
-
-
- def test_demand_op(self):
- p = 'test_demand_op'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- assert is_junction(p, 'j1')
-
- result = set_demand(p, ChangeSet({'junction': 'j1', 'demands': [{'demand': 10.0, 'pattern': None, 'category': 'x'},
- {'demand': 20.0, 'pattern': None, 'category': None}]}))
- cs = result.operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'demand'
- assert cs['junction'] == 'j1'
- ds = cs['demands']
- assert len(ds) == 2
- assert ds[0]['demand'] == 10.0
- assert ds[0]['pattern'] == None
- assert ds[0]['category'] == 'x'
- assert ds[1]['demand'] == 20.0
- assert ds[1]['pattern'] == None
- assert ds[1]['category'] == None
-
- result = execute_undo(p)
- cs = result.operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'demand'
- assert cs['junction'] == 'j1'
- assert len(cs['demands']) == 0
-
- result = execute_redo(p)
- cs = result.operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'demand'
- assert cs['junction'] == 'j1'
- ds = cs['demands']
- assert len(ds) == 2
- assert ds[0]['demand'] == 10.0
- assert ds[0]['pattern'] == None
- assert ds[0]['category'] == 'x'
- assert ds[1]['demand'] == 20.0
- assert ds[1]['pattern'] == None
- assert ds[1]['category'] == None
-
- self.leave(p)
-
-
- # 10 status
-
-
- def test_status(self):
- p = 'test_status'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- assert is_junction(p, 'j1')
- assert is_junction(p, 'j2')
-
- add_pipe(p, ChangeSet({'id': 'p0', 'node1': 'j1', 'node2': 'j2', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
- assert is_pipe(p, 'p0')
-
- s = get_status(p, 'p0')
- assert s['link'] == 'p0'
- assert s['status'] == None
- assert s['setting'] == None
-
- set_status(p, ChangeSet({'link': 'p0', 'status': LINK_STATUS_OPEN, 'setting': 10.0}))
- s = get_status(p, 'p0')
- assert s['link'] == 'p0'
- assert s['status'] == LINK_STATUS_OPEN
- assert s['setting'] == 10.0
-
- set_status(p, ChangeSet({'link': 'p0', 'status': None, 'setting': None}))
- s = get_status(p, 'p0')
- assert s['link'] == 'p0'
- assert s['status'] == None
- assert s['setting'] == None
-
- self.leave(p)
-
-
- def test_status_op(self):
- p = 'test_status_op'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- assert is_junction(p, 'j1')
- assert is_junction(p, 'j2')
-
- add_pipe(p, ChangeSet({'id': 'p0', 'node1': 'j1', 'node2': 'j2', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
- assert is_pipe(p, 'p0')
-
- s = get_status(p, 'p0')
- assert s['link'] == 'p0'
- assert s['status'] == None
- assert s['setting'] == None
-
- cs = set_status(p, ChangeSet({'link': 'p0', 'status': LINK_STATUS_OPEN, 'setting': 10.0})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'status'
- assert cs['link'] == 'p0'
- assert cs['status'] == LINK_STATUS_OPEN
- assert cs['setting'] == 10.0
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'status'
- assert cs['link'] == 'p0'
- assert cs['status'] == None
- assert cs['setting'] == None
-
- s = get_status(p, 'p0')
- assert s['link'] == 'p0'
- assert s['status'] == None
- assert s['setting'] == None
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'status'
- assert cs['link'] == 'p0'
- assert cs['status'] == LINK_STATUS_OPEN
- assert cs['setting'] == 10.0
-
- self.leave(p)
-
-
- # 11 pattern
-
-
- def test_pattern(self):
- p = 'test_pattern'
- self.enter(p)
-
- assert is_pattern(p, 'p0') == False
-
- assert get_pattern(p, 'p0') == {}
-
- add_pattern(p, ChangeSet({'id' : 'p0', 'factors': [1.0, 2.0, 3.0]}))
-
- assert is_pattern(p, 'p0')
- p0 = get_pattern(p, 'p0')
- assert p0['id'] == 'p0'
- assert p0['factors'] == [1.0, 2.0, 3.0]
-
- set_pattern(p, ChangeSet({'id' : 'p0', 'factors': [1.0, 2.0]}))
-
- assert is_pattern(p, 'p0')
- p0 = get_pattern(p, 'p0')
- assert p0['id'] == 'p0'
- assert p0['factors'] == [1.0, 2.0]
-
- set_pattern(p, ChangeSet({'id' : 'p0', 'factors': []}))
-
- assert is_pattern(p, 'p0')
- p0 = get_pattern(p, 'p0')
- assert p0['id'] == 'p0'
- assert p0['factors'] == []
-
- delete_pattern(p, ChangeSet({'id' : 'p0'}))
- assert is_pattern(p, 'p0') == False
-
- assert get_pattern(p, 'p0') == {}
-
- self.leave(p)
-
-
- def test_pattern_op(self):
- p = 'test_pattern_op'
- self.enter(p)
-
- cs = add_pattern(p, ChangeSet({'id' : 'p0', 'factors': [1.0, 2.0, 3.0]})).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == PATTERN
- assert cs['id'] == 'p0'
- assert cs['factors'] == [1.0, 2.0, 3.0]
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == PATTERN
- assert cs['id'] == 'p0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == PATTERN
- assert cs['id'] == 'p0'
- assert cs['factors'] == [1.0, 2.0, 3.0]
-
- cs = set_pattern(p, ChangeSet({'id' : 'p0', 'factors': [1.0, 2.0]})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == PATTERN
- assert cs['id'] == 'p0'
- assert cs['factors'] == [1.0, 2.0]
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == PATTERN
- assert cs['id'] == 'p0'
- assert cs['factors'] == [1.0, 2.0, 3.0]
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == PATTERN
- assert cs['id'] == 'p0'
- assert cs['factors'] == [1.0, 2.0]
-
- cs = set_pattern(p, ChangeSet({'id' : 'p0', 'factors': []})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == PATTERN
- assert cs['id'] == 'p0'
- assert cs['factors'] == []
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == PATTERN
- assert cs['id'] == 'p0'
- assert cs['factors'] == [1.0, 2.0]
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == PATTERN
- assert cs['id'] == 'p0'
- assert cs['factors'] == []
-
- cs = delete_pattern(p, ChangeSet({'id' : 'p0'})).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == PATTERN
- assert cs['id'] == 'p0'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == PATTERN
- assert cs['id'] == 'p0'
- assert cs['factors'] == []
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == PATTERN
- assert cs['id'] == 'p0'
-
- self.leave(p)
-
-
- def test_pattern_del(self):
- p = 'test_pattern_del'
- self.enter(p)
-
- add_pattern(p, ChangeSet({'id' : 'p0', 'factors': [1.0, 2.0, 3.0]}))
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_reservoir(p, ChangeSet({'id': 'r0', 'x': 0.0, 'y': 10.0, 'head': 20.0, 'pattern': 'p0'}))
- add_pump(p, ChangeSet({'id': 'pump0', 'node1': 'j0', 'node2': 'r0', 'power': 0.0, 'pattern': 'p0'}))
- set_demand(p, ChangeSet({'junction': 'j0', 'demands': [{'demand': 10.0, 'pattern': 'p0', 'category': 'x'}, {'demand': 20.0, 'pattern': 'p0', 'category': None}]}))
- set_pump_energy(p, ChangeSet({'pump' : 'pump0', 'pattern': 'p0'}))
- add_source(p, ChangeSet({'node': 'j0', 's_type': SOURCE_TYPE_CONCEN, 'strength': 10.0, 'pattern': 'p0' }))
- assert is_pattern(p, 'p0')
- assert get_reservoir(p, 'r0')['pattern'] == 'p0'
- assert get_pump(p, 'pump0')['pattern'] == 'p0'
- assert get_demand(p, 'j0')['demands'] == [{'demand': 10.0, 'pattern': 'p0', 'category': 'x'}, {'demand': 20.0, 'pattern': 'p0', 'category': None}]
- assert get_pump_energy(p, 'pump0')['pattern'] == 'p0'
- assert get_source(p, 'j0')['pattern'] == 'p0'
-
- delete_pattern(p, ChangeSet({'id': 'p0'}))
- assert is_pattern(p, 'p0') == False
- assert get_reservoir(p, 'r0')['pattern'] == None
- assert get_pump(p, 'pump0')['pattern'] == None
- assert get_demand(p, 'j0')['demands'] == [{'demand': 10.0, 'pattern': None, 'category': 'x'}, {'demand': 20.0, 'pattern': None, 'category': None}]
- assert get_pump_energy(p, 'pump0')['pattern'] == None
- assert get_source(p, 'j0')['pattern'] == None
-
- execute_undo(p)
- assert is_pattern(p, 'p0')
- assert get_reservoir(p, 'r0')['pattern'] == 'p0'
- assert get_pump(p, 'pump0')['pattern'] == 'p0'
- assert get_demand(p, 'j0')['demands'] == [{'demand': 10.0, 'pattern': 'p0', 'category': 'x'}, {'demand': 20.0, 'pattern': 'p0', 'category': None}]
- assert get_pump_energy(p, 'pump0')['pattern'] == 'p0'
- assert get_source(p, 'j0')['pattern'] == 'p0'
-
- execute_redo(p)
- assert is_pattern(p, 'p0') == False
- assert get_reservoir(p, 'r0')['pattern'] == None
- assert get_pump(p, 'pump0')['pattern'] == None
- assert get_demand(p, 'j0')['demands'] == [{'demand': 10.0, 'pattern': None, 'category': 'x'}, {'demand': 20.0, 'pattern': None, 'category': None}]
- assert get_pump_energy(p, 'pump0')['pattern'] == None
- assert get_source(p, 'j0')['pattern'] == None
-
- self.leave(p)
-
-
- # 12 curve
-
-
- def test_curve(self):
- p = 'test_curve'
- self.enter(p)
-
- assert is_curve(p, 'c0') == False
-
- assert get_curve(p, 'c0') == {}
-
- add_curve(p, ChangeSet({'id' : 'c0', 'c_type' : CURVE_TYPE_PUMP, 'coords': [{'x': 1.0, 'y': 2.0}, {'x': 2.0, 'y': 1.0}]}))
-
- assert is_curve(p, 'c0')
- c0 = get_curve(p, 'c0')
- assert c0['id'] == 'c0'
- assert c0['c_type'] == CURVE_TYPE_PUMP
- xys = c0['coords']
- assert len(xys) == 2
- assert xys[0]['x'] == 1.0
- assert xys[0]['y'] == 2.0
- assert xys[1]['x'] == 2.0
- assert xys[1]['y'] == 1.0
-
- set_curve(p, ChangeSet({'id' : 'c0', 'c_type': CURVE_TYPE_EFFICIENCY}))
-
- c0 = get_curve(p, 'c0')
- assert c0['id'] == 'c0'
- assert c0['c_type'] == CURVE_TYPE_EFFICIENCY
- xys = c0['coords']
- assert len(xys) == 2
- assert xys[0]['x'] == 1.0
- assert xys[0]['y'] == 2.0
- assert xys[1]['x'] == 2.0
- assert xys[1]['y'] == 1.0
-
- set_curve(p, ChangeSet({'id' : 'c0', 'coords': []}))
-
- c0 = get_curve(p, 'c0')
- assert c0['id'] == 'c0'
- assert c0['c_type'] == CURVE_TYPE_EFFICIENCY
- assert c0['coords'] == []
-
- delete_curve(p, ChangeSet({'id' : 'c0'}))
-
- assert is_curve(p, 'c0') == False
-
- assert get_curve(p, 'c0') == {}
-
- self.leave(p)
-
-
- def test_curve_op(self):
- p = 'test_curve_op'
- self.enter(p)
-
- cs = add_curve(p, ChangeSet({'id' : 'c0', 'c_type' : CURVE_TYPE_PUMP, 'coords': [{'x': 1.0, 'y': 2.0}, {'x': 2.0, 'y': 1.0}]})).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == CURVE
- assert cs['id'] == 'c0'
- assert cs['c_type'] == CURVE_TYPE_PUMP
- xys = cs['coords']
- assert len(xys) == 2
- assert xys[0]['x'] == 1.0
- assert xys[0]['y'] == 2.0
- assert xys[1]['x'] == 2.0
- assert xys[1]['y'] == 1.0
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == CURVE
- assert cs['id'] == 'c0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == CURVE
- assert cs['id'] == 'c0'
- assert cs['c_type'] == CURVE_TYPE_PUMP
- xys = cs['coords']
- assert len(xys) == 2
- assert xys[0]['x'] == 1.0
- assert xys[0]['y'] == 2.0
- assert xys[1]['x'] == 2.0
- assert xys[1]['y'] == 1.0
-
- cs = set_curve(p, ChangeSet({'id' : 'c0', 'c_type': CURVE_TYPE_EFFICIENCY})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == CURVE
- assert cs['id'] == 'c0'
- assert cs['c_type'] == CURVE_TYPE_EFFICIENCY
- xys = cs['coords']
- assert len(xys) == 2
- assert xys[0]['x'] == 1.0
- assert xys[0]['y'] == 2.0
- assert xys[1]['x'] == 2.0
- assert xys[1]['y'] == 1.0
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == CURVE
- assert cs['id'] == 'c0'
- assert cs['c_type'] == CURVE_TYPE_PUMP
- xys = cs['coords']
- assert len(xys) == 2
- assert xys[0]['x'] == 1.0
- assert xys[0]['y'] == 2.0
- assert xys[1]['x'] == 2.0
- assert xys[1]['y'] == 1.0
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == CURVE
- assert cs['id'] == 'c0'
- assert cs['c_type'] == CURVE_TYPE_EFFICIENCY
- xys = cs['coords']
- assert len(xys) == 2
- assert xys[0]['x'] == 1.0
- assert xys[0]['y'] == 2.0
- assert xys[1]['x'] == 2.0
- assert xys[1]['y'] == 1.0
-
- cs = set_curve(p, ChangeSet({'id' : 'c0', 'coords': []})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == CURVE
- assert cs['id'] == 'c0'
- assert cs['c_type'] == CURVE_TYPE_EFFICIENCY
- assert cs['coords'] == []
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == CURVE
- assert cs['id'] == 'c0'
- assert cs['c_type'] == CURVE_TYPE_EFFICIENCY
- xys = cs['coords']
- assert len(xys) == 2
- assert xys[0]['x'] == 1.0
- assert xys[0]['y'] == 2.0
- assert xys[1]['x'] == 2.0
- assert xys[1]['y'] == 1.0
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == CURVE
- assert cs['id'] == 'c0'
- assert cs['c_type'] == CURVE_TYPE_EFFICIENCY
- assert cs['coords'] == []
-
- self.leave(p)
-
-
- def test_curve_del(self):
- p = 'test_curve_del'
- self.enter(p)
-
- add_curve(p, ChangeSet({'id' : 'c0', 'c_type' : CURVE_TYPE_PUMP, 'coords': [{'x': 1.0, 'y': 2.0}, {'x': 2.0, 'y': 1.0}]}))
- add_tank(p, ChangeSet({'id': 't0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0, 'init_level': 1.0, 'min_level': 0.0, 'max_level': 2.0, 'diameter': 10.0, 'min_vol': 100.0, 'vol_curve': 'c0', 'overflow': OVERFLOW_NO}))
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0, 'pattern': None}))
- add_reservoir(p, ChangeSet({'id': 'r0', 'x': 0.0, 'y': 10.0, 'head': 20.0, 'pattern': None}))
- add_pump(p, ChangeSet({'id': 'p0', 'node1': 'j0', 'node2': 'r0', 'head': 'c0'}))
- set_pump_energy(p, ChangeSet({'pump' : 'p0', 'effic': 'c0'}))
- assert is_curve(p, 'c0')
- assert get_tank(p, 't0')['vol_curve'] == 'c0'
- assert get_pump(p, 'p0')['head'] == 'c0'
- assert get_pump_energy(p, 'p0')['effic'] == 'c0'
-
- delete_curve(p, ChangeSet({'id': 'c0'}))
- assert is_curve(p, 'c0') == False
- assert get_tank(p, 't0')['vol_curve'] == None
- assert get_pump(p, 'p0')['head'] == None
- assert get_pump_energy(p, 'p0')['effic'] == None
-
- execute_undo(p)
- assert is_curve(p, 'c0')
- assert get_tank(p, 't0')['vol_curve'] == 'c0'
- assert get_pump(p, 'p0')['head'] == 'c0'
- assert get_pump_energy(p, 'p0')['effic'] == 'c0'
-
- execute_redo(p)
- assert is_curve(p, 'c0') == False
- assert get_tank(p, 't0')['vol_curve'] == None
- assert get_pump(p, 'p0')['head'] == None
- assert get_pump_energy(p, 'p0')['effic'] == None
-
- self.leave(p)
-
-
- # 13 control
-
-
- def test_control(self):
- p = 'test_control'
- self.enter(p)
-
- assert get_control(p)['controls'] == []
-
- set_control(p, ChangeSet({'controls': ['x']}))
- assert get_control(p)['controls'] == ['x']
-
- self.leave(p)
-
-
- def test_control_op(self):
- p = 'test_control_op'
- self.enter(p)
-
- cs = set_control(p, ChangeSet({'controls': ['x']})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'control'
- assert cs['controls'] == ['x']
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'control'
- assert cs['controls'] == []
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'control'
- assert cs['controls'] == ['x']
-
- self.leave(p)
-
-
- # 14 rule
-
-
- def test_rule(self):
- p = 'test_rule'
- self.enter(p)
-
- assert get_rule(p)['rules'] == []
-
- set_rule(p, ChangeSet({'rules': ['x']}))
- assert get_rule(p)['rules'] == ['x']
-
- self.leave(p)
-
-
- def test_rule_op(self):
- p = 'test_rule_op'
- self.enter(p)
-
- cs = set_rule(p, ChangeSet({'rules': ['x']})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'rule'
- assert cs['rules'] == ['x']
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'rule'
- assert cs['rules'] == []
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'rule'
- assert cs['rules'] == ['x']
-
- self.leave(p)
-
-
- # 15 energy
-
-
- def test_energy(self):
- p = 'test_energy'
- self.enter(p)
-
- ge = get_energy(p)
- assert ge['GLOBAL PRICE'] == '0'
- assert ge['GLOBAL PATTERN'] == ''
- assert ge['GLOBAL EFFIC'] == '75'
- assert ge['DEMAND CHARGE'] == '0'
-
- set_energy(p, ChangeSet({ 'GLOBAL PRICE' : '10' }))
- ge = get_energy(p)
- assert ge['GLOBAL PRICE'] == '10'
- assert ge['GLOBAL PATTERN'] == ''
- assert ge['GLOBAL EFFIC'] == '75'
- assert ge['DEMAND CHARGE'] == '0'
-
- add_pattern(p, ChangeSet({'id' : 'p0', 'factors': [1.0, 2.0, 3.0]}))
- set_energy(p, ChangeSet({ 'GLOBAL PATTERN' : 'p0' }))
- ge = get_energy(p)
- assert ge['GLOBAL PRICE'] == '10'
- assert ge['GLOBAL PATTERN'] == 'p0'
- assert ge['GLOBAL EFFIC'] == '75'
- assert ge['DEMAND CHARGE'] == '0'
-
- set_energy(p, ChangeSet({ 'GLOBAL EFFIC' : '0' }))
- ge = get_energy(p)
- assert ge['GLOBAL PRICE'] == '10'
- assert ge['GLOBAL PATTERN'] == 'p0'
- assert ge['GLOBAL EFFIC'] == '0'
- assert ge['DEMAND CHARGE'] == '0'
-
- set_energy(p, ChangeSet({ 'DEMAND CHARGE' : '10' }))
- ge = get_energy(p)
- assert ge['GLOBAL PRICE'] == '10'
- assert ge['GLOBAL PATTERN'] == 'p0'
- assert ge['GLOBAL EFFIC'] == '0'
- assert ge['DEMAND CHARGE'] == '10'
-
- self.leave(p)
-
-
- def test_energy_op(self):
- p = 'test_energy_op'
- self.enter(p)
-
- add_pattern(p, ChangeSet({'id' : 'p0', 'factors': [1.0, 2.0, 3.0]}))
-
- cs = set_energy(p, ChangeSet({ 'GLOBAL PRICE' : '10' })).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'energy'
- assert cs['GLOBAL PRICE'] == '10'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'energy'
- assert cs['GLOBAL PRICE'] == '0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'energy'
- assert cs['GLOBAL PRICE'] == '10'
-
- cs = set_energy(p, ChangeSet({ 'GLOBAL PATTERN' : 'p0' })).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'energy'
- assert cs['GLOBAL PATTERN'] == 'p0'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'energy'
- assert cs['GLOBAL PATTERN'] == ''
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'energy'
- assert cs['GLOBAL PATTERN'] == 'p0'
-
- cs = set_energy(p, ChangeSet({ 'GLOBAL EFFIC' : '0' })).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'energy'
- assert cs['GLOBAL EFFIC'] == '0'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'energy'
- assert cs['GLOBAL EFFIC'] == '75'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'energy'
- assert cs['GLOBAL EFFIC'] == '0'
-
- cs = set_energy(p, ChangeSet({ 'DEMAND CHARGE' : '10' })).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'energy'
- assert cs['DEMAND CHARGE'] == '10'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'energy'
- assert cs['DEMAND CHARGE'] == '0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'energy'
- assert cs['DEMAND CHARGE'] == '10'
-
- self.leave(p)
-
-
- def test_pump_energy(self):
- p = 'test_pump_energy'
- self.enter(p)
-
- ge = get_pump_energy(p, 'p0')
- assert ge['pump'] == 'p0'
- assert ge['price'] == None
- assert ge['pattern'] == None
- assert ge['effic'] == None
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_pump(p, ChangeSet({'id': 'p0', 'node1': 'j1', 'node2': 'j2', 'power': 0.0}))
-
- set_pump_energy(p, ChangeSet({'pump' : 'p0'}))
- ge = get_pump_energy(p, 'p0')
- assert ge['pump'] == 'p0'
- assert ge['price'] == None
- assert ge['pattern'] == None
- assert ge['effic'] == None
-
- set_pump_energy(p, ChangeSet({'pump' : 'p0', 'price': 0.0}))
- ge = get_pump_energy(p, 'p0')
- assert ge['pump'] == 'p0'
- assert ge['price'] == 0.0
- assert ge['pattern'] == None
- assert ge['effic'] == None
-
- add_pattern(p, ChangeSet({'id' : 'pa0', 'factors': [1.0, 2.0, 3.0]}))
- set_pump_energy(p, ChangeSet({'pump' : 'p0', 'pattern': 'pa0'}))
- ge = get_pump_energy(p, 'p0')
- assert ge['pump'] == 'p0'
- assert ge['price'] == 0.0
- assert ge['pattern'] == 'pa0'
- assert ge['effic'] == None
-
- add_curve(p, ChangeSet({'id' : 'c0', 'c_type' : CURVE_TYPE_PUMP, 'coords': [{'x': 1.0, 'y': 2.0}, {'x': 2.0, 'y': 1.0}]}))
- set_pump_energy(p, ChangeSet({'pump' : 'p0', 'effic': 'c0'}))
- ge = get_pump_energy(p, 'p0')
- assert ge['pump'] == 'p0'
- assert ge['price'] == 0.0
- assert ge['pattern'] == 'pa0'
- assert ge['effic'] == 'c0'
-
- self.leave(p)
-
-
- def test_pump_energy_op(self):
- p = 'test_pump_energy_op'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_pump(p, ChangeSet({'id': 'p0', 'node1': 'j1', 'node2': 'j2', 'power': 0.0}))
- add_pattern(p, ChangeSet({'id' : 'pa0', 'factors': [1.0, 2.0, 3.0]}))
- add_curve(p, ChangeSet({'id' : 'c0', 'c_type' : CURVE_TYPE_PUMP, 'coords': [{'x': 1.0, 'y': 2.0}, {'x': 2.0, 'y': 1.0}]}))
-
- cs = set_pump_energy(p, ChangeSet({'pump' : 'p0'})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pump_energy'
- assert cs['pump'] == 'p0'
- assert cs['price'] == None
- assert cs['pattern'] == None
- assert cs['effic'] == None
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pump_energy'
- assert cs['pump'] == 'p0'
- assert cs['price'] == None
- assert cs['pattern'] == None
- assert cs['effic'] == None
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pump_energy'
- assert cs['pump'] == 'p0'
- assert cs['price'] == None
- assert cs['pattern'] == None
- assert cs['effic'] == None
-
- cs = set_pump_energy(p, ChangeSet({'pump' : 'p0', 'price': 0.0})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pump_energy'
- assert cs['pump'] == 'p0'
- assert cs['price'] == 0.0
- assert cs['pattern'] == None
- assert cs['effic'] == None
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pump_energy'
- assert cs['pump'] == 'p0'
- assert cs['price'] == None
- assert cs['pattern'] == None
- assert cs['effic'] == None
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pump_energy'
- assert cs['pump'] == 'p0'
- assert cs['price'] == 0.0
- assert cs['pattern'] == None
- assert cs['effic'] == None
-
- cs = set_pump_energy(p, ChangeSet({'pump' : 'p0', 'pattern': 'pa0'})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pump_energy'
- assert cs['pump'] == 'p0'
- assert cs['price'] == 0.0
- assert cs['pattern'] == 'pa0'
- assert cs['effic'] == None
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pump_energy'
- assert cs['pump'] == 'p0'
- assert cs['price'] == 0.0
- assert cs['pattern'] == None
- assert cs['effic'] == None
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pump_energy'
- assert cs['pump'] == 'p0'
- assert cs['price'] == 0.0
- assert cs['pattern'] == 'pa0'
- assert cs['effic'] == None
-
- cs = set_pump_energy(p, ChangeSet({'pump' : 'p0', 'effic': 'c0'})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pump_energy'
- assert cs['pump'] == 'p0'
- assert cs['price'] == 0.0
- assert cs['pattern'] == 'pa0'
- assert cs['effic'] == 'c0'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pump_energy'
- assert cs['pump'] == 'p0'
- assert cs['price'] == 0.0
- assert cs['pattern'] == 'pa0'
- assert cs['effic'] == None
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pump_energy'
- assert cs['pump'] == 'p0'
- assert cs['price'] == 0.0
- assert cs['pattern'] == 'pa0'
- assert cs['effic'] == 'c0'
-
- cs = set_pump_energy(p, ChangeSet({'pump' : 'p0', 'price': None, 'pattern': None, 'effic': None})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pump_energy'
- assert cs['pump'] == 'p0'
- assert cs['price'] == None
- assert cs['pattern'] == None
- assert cs['effic'] == None
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pump_energy'
- assert cs['pump'] == 'p0'
- assert cs['price'] == 0.0
- assert cs['pattern'] == 'pa0'
- assert cs['effic'] == 'c0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pump_energy'
- assert cs['pump'] == 'p0'
- assert cs['price'] == None
- assert cs['pattern'] == None
- assert cs['effic'] == None
-
- cs = set_pump_energy(p, ChangeSet({'pump' : 'p0', 'price': 0.0, 'pattern': 'pa0', 'effic': 'c0'})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pump_energy'
- assert cs['pump'] == 'p0'
- assert cs['price'] == 0.0
- assert cs['pattern'] == 'pa0'
- assert cs['effic'] == 'c0'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pump_energy'
- assert cs['pump'] == 'p0'
- assert cs['price'] == None
- assert cs['pattern'] == None
- assert cs['effic'] == None
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pump_energy'
- assert cs['pump'] == 'p0'
- assert cs['price'] == 0.0
- assert cs['pattern'] == 'pa0'
- assert cs['effic'] == 'c0'
-
- self.leave(p)
-
-
- # 16 emitter
-
-
- def test_emitter(self):
- p = 'test_emitter'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- assert is_junction(p, 'j1')
-
- e = get_emitter(p, 'j1')
- assert e['junction'] == 'j1'
- assert e['coefficient'] == None
-
- set_emitter(p, ChangeSet({'junction': 'j1', 'coefficient': 10.0}))
-
- e = get_emitter(p, 'j1')
- assert e['junction'] == 'j1'
- assert e['coefficient'] == 10.0
-
- set_emitter(p, ChangeSet({'junction': 'j1', 'coefficient': None}))
-
- e = get_emitter(p, 'j1')
- assert e['junction'] == 'j1'
- assert e['coefficient'] == None
-
- self.leave(p)
-
-
- def test_emitter_op(self):
- p = 'test_emitter_op'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- assert is_junction(p, 'j1')
-
- cs = set_emitter(p, ChangeSet({'junction': 'j1', 'coefficient': 10.0})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'emitter'
- assert cs['junction'] == 'j1'
- assert cs['coefficient'] == 10.0
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'emitter'
- assert cs['junction'] == 'j1'
- assert cs['coefficient'] == None
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'emitter'
- assert cs['junction'] == 'j1'
- assert cs['coefficient'] == 10.0
-
- self.leave(p)
-
-
- # 17 quality
-
-
- def test_quality(self):
- p = 'test_quality'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- assert is_junction(p, 'j1')
-
- e = get_quality(p, 'j1')
- assert e['node'] == 'j1'
- assert e['quality'] == None
-
- set_quality(p, ChangeSet({'node': 'j1', 'quality': 10.0}))
-
- e = get_quality(p, 'j1')
- assert e['node'] == 'j1'
- assert e['quality'] == 10.0
-
- set_quality(p, ChangeSet({'node': 'j1', 'quality': None}))
-
- e = get_quality(p, 'j1')
- assert e['node'] == 'j1'
- assert e['quality'] == None
-
- self.leave(p)
-
-
- def test_quality_op(self):
- p = 'test_quality_op'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- assert is_junction(p, 'j1')
-
- cs = set_quality(p, ChangeSet({'node': 'j1', 'quality': 10.0})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'quality'
- assert cs['node'] == 'j1'
- assert cs['quality'] == 10.0
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'quality'
- assert cs['node'] == 'j1'
- assert cs['quality'] == None
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'quality'
- assert cs['node'] == 'j1'
- assert cs['quality'] == 10.0
-
- self.leave(p)
-
-
- # 18 source
-
-
- def test_source(self):
- p = 'test_source'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_pattern(p, ChangeSet({'id' : 'p0', 'factors': [1.0, 2.0, 3.0]}))
- add_pattern(p, ChangeSet({'id' : 'p1', 'factors': [1.0, 2.0, 3.0]}))
-
- assert get_source(p, 'j0') == {}
-
- add_source(p, ChangeSet({'node': 'j0', 's_type': SOURCE_TYPE_CONCEN, 'strength': 10.0, 'pattern': 'p0'}))
- s = get_source(p, 'j0')
- assert s['node'] == 'j0'
- assert s['s_type'] == SOURCE_TYPE_CONCEN
- assert s['strength'] == 10.0
- assert s['pattern'] == 'p0'
-
- set_source(p, ChangeSet({'node': 'j0', 's_type': SOURCE_TYPE_FLOWPACED}))
- s = get_source(p, 'j0')
- assert s['node'] == 'j0'
- assert s['s_type'] == SOURCE_TYPE_FLOWPACED
- assert s['strength'] == 10.0
- assert s['pattern'] == 'p0'
-
- set_source(p, ChangeSet({'node': 'j0', 'strength': 20.0}))
- s = get_source(p, 'j0')
- assert s['node'] == 'j0'
- assert s['s_type'] == SOURCE_TYPE_FLOWPACED
- assert s['strength'] == 20.0
- assert s['pattern'] == 'p0'
-
- set_source(p, ChangeSet({'node': 'j0', 'pattern': 'p1'}))
- s = get_source(p, 'j0')
- assert s['node'] == 'j0'
- assert s['s_type'] == SOURCE_TYPE_FLOWPACED
- assert s['strength'] == 20.0
- assert s['pattern'] == 'p1'
-
- delete_source(p, ChangeSet({'node': 'j0'}))
-
- assert get_source(p, 'j0') == {}
-
- self.leave(p)
-
-
- def test_source_op(self):
- p = 'test_source_op'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_pattern(p, ChangeSet({'id' : 'p0', 'factors': [1.0, 2.0, 3.0]}))
- add_pattern(p, ChangeSet({'id' : 'p1', 'factors': [1.0, 2.0, 3.0]}))
-
- cs = add_source(p, ChangeSet({'node': 'j0', 's_type': SOURCE_TYPE_CONCEN, 'strength': 10.0, 'pattern': 'p0'})).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'source'
- assert cs['node'] == 'j0'
- assert cs['s_type'] == SOURCE_TYPE_CONCEN
- assert cs['strength'] == 10.0
- assert cs['pattern'] == 'p0'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'source'
- assert cs['node'] == 'j0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'source'
- assert cs['node'] == 'j0'
- assert cs['s_type'] == SOURCE_TYPE_CONCEN
- assert cs['strength'] == 10.0
- assert cs['pattern'] == 'p0'
-
- cs = set_source(p, ChangeSet({'node': 'j0', 's_type': SOURCE_TYPE_FLOWPACED})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'source'
- assert cs['node'] == 'j0'
- assert cs['s_type'] == SOURCE_TYPE_FLOWPACED
- assert cs['strength'] == 10.0
- assert cs['pattern'] == 'p0'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'source'
- assert cs['node'] == 'j0'
- assert cs['s_type'] == SOURCE_TYPE_CONCEN
- assert cs['strength'] == 10.0
- assert cs['pattern'] == 'p0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'source'
- assert cs['node'] == 'j0'
- assert cs['s_type'] == SOURCE_TYPE_FLOWPACED
- assert cs['strength'] == 10.0
- assert cs['pattern'] == 'p0'
-
- cs = set_source(p, ChangeSet({'node': 'j0', 'strength': 20.0})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'source'
- assert cs['node'] == 'j0'
- assert cs['s_type'] == SOURCE_TYPE_FLOWPACED
- assert cs['strength'] == 20.0
- assert cs['pattern'] == 'p0'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'source'
- assert cs['node'] == 'j0'
- assert cs['s_type'] == SOURCE_TYPE_FLOWPACED
- assert cs['strength'] == 10.0
- assert cs['pattern'] == 'p0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'source'
- assert cs['node'] == 'j0'
- assert cs['s_type'] == SOURCE_TYPE_FLOWPACED
- assert cs['strength'] == 20.0
- assert cs['pattern'] == 'p0'
-
- cs = set_source(p, ChangeSet({'node': 'j0', 'pattern': 'p1'})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'source'
- assert cs['node'] == 'j0'
- assert cs['s_type'] == SOURCE_TYPE_FLOWPACED
- assert cs['strength'] == 20.0
- assert cs['pattern'] == 'p1'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'source'
- assert cs['node'] == 'j0'
- assert cs['s_type'] == SOURCE_TYPE_FLOWPACED
- assert cs['strength'] == 20.0
- assert cs['pattern'] == 'p0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'source'
- assert cs['node'] == 'j0'
- assert cs['s_type'] == SOURCE_TYPE_FLOWPACED
- assert cs['strength'] == 20.0
- assert cs['pattern'] == 'p1'
-
- cs = delete_source(p, ChangeSet({'node': 'j0'})).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'source'
- assert cs['node'] == 'j0'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'source'
- assert cs['node'] == 'j0'
- assert cs['s_type'] == SOURCE_TYPE_FLOWPACED
- assert cs['strength'] == 20.0
- assert cs['pattern'] == 'p1'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'source'
- assert cs['node'] == 'j0'
-
- self.leave(p)
-
-
- # 19 reaction
-
-
- def test_reaction(self):
- p = 'test_reaction'
- self.enter(p)
-
- gr = get_reaction(p)
- assert gr['ORDER BULK'] == '1'
- assert gr['ORDER WALL'] == '1'
- assert gr['ORDER TANK'] == '1'
- assert gr['GLOBAL BULK'] == '0'
- assert gr['GLOBAL WALL'] == '0'
- assert gr['LIMITING POTENTIAL'] == '0'
- assert gr['ROUGHNESS CORRELATION'] == '0'
-
- set_reaction(p, ChangeSet({ 'ORDER BULK' : '10' }))
- gr = get_reaction(p)
- assert gr['ORDER BULK'] == '10'
- assert gr['ORDER WALL'] == '1'
- assert gr['ORDER TANK'] == '1'
- assert gr['GLOBAL BULK'] == '0'
- assert gr['GLOBAL WALL'] == '0'
- assert gr['LIMITING POTENTIAL'] == '0'
- assert gr['ROUGHNESS CORRELATION'] == '0'
-
- set_reaction(p, ChangeSet({ 'ORDER BULK' : '1' }))
- gr = get_reaction(p)
- assert gr['ORDER BULK'] == '1'
- assert gr['ORDER WALL'] == '1'
- assert gr['ORDER TANK'] == '1'
- assert gr['GLOBAL BULK'] == '0'
- assert gr['GLOBAL WALL'] == '0'
- assert gr['LIMITING POTENTIAL'] == '0'
- assert gr['ROUGHNESS CORRELATION'] == '0'
-
- self.leave(p)
-
-
- def test_reaction_op(self):
- p = 'test_reaction_op'
- self.enter(p)
-
- cs = set_reaction(p, ChangeSet({ 'ORDER BULK' : '10' })).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'reaction'
- assert cs['ORDER BULK'] == '10'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'reaction'
- assert cs['ORDER BULK'] == '1'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'reaction'
- assert cs['ORDER BULK'] == '10'
-
- self.leave(p)
-
-
- def test_pipe_reaction(self):
- p = 'test_pipe_reaction'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_pipe(p, ChangeSet({'id': 'p0', 'node1': 'j1', 'node2': 'j2', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
-
- pp = get_pipe_reaction(p, 'p0')
- assert pp['pipe'] == 'p0'
- assert pp['bulk'] == None
- assert pp['wall'] == None
-
- set_pipe_reaction(p, ChangeSet({'pipe': 'p0', 'bulk': 10.0, 'wall': 20.0}))
- pp = get_pipe_reaction(p, 'p0')
- assert pp['pipe'] == 'p0'
- assert pp['bulk'] == 10.0
- assert pp['wall'] == 20.0
-
- set_pipe_reaction(p, ChangeSet({'pipe': 'p0', 'bulk': None, 'wall': None}))
- pp = get_pipe_reaction(p, 'p0')
- assert pp['pipe'] == 'p0'
- assert pp['bulk'] == None
- assert pp['wall'] == None
-
- self.leave(p)
-
-
- def test_pipe_reaction_op(self):
- p = 'test_pipe_reaction_op'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_pipe(p, ChangeSet({'id': 'p0', 'node1': 'j1', 'node2': 'j2', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
-
- cs = set_pipe_reaction(p, ChangeSet({'pipe': 'p0', 'bulk': 10.0, 'wall': 20.0})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pipe_reaction'
- assert cs['pipe'] == 'p0'
- assert cs['bulk'] == 10.0
- assert cs['wall'] == 20.0
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pipe_reaction'
- assert cs['pipe'] == 'p0'
- assert cs['bulk'] == None
- assert cs['wall'] == None
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'pipe_reaction'
- assert cs['pipe'] == 'p0'
- assert cs['bulk'] == 10.0
- assert cs['wall'] == 20.0
-
- self.leave(p)
-
-
- def test_tank_reaction(self):
- p = 'test_tank_reaction'
- self.enter(p)
-
- add_tank(p, ChangeSet({'id': 't0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0, 'init_level': 1.0, 'min_level': 0.0, 'max_level': 2.0, 'diameter': 10.0, 'min_vol': 100.0, 'vol_curve': None, 'overflow': OVERFLOW_NO}))
-
- pt = get_tank_reaction(p, 't0')
- assert pt['tank'] == 't0'
- assert pt['value'] == None
-
- set_tank_reaction(p, ChangeSet({'tank': 't0', 'value': 10.0}))
- pt = get_tank_reaction(p, 't0')
- assert pt['tank'] == 't0'
- assert pt['value'] == 10.0
-
- set_tank_reaction(p, ChangeSet({'tank': 't0', 'value': None}))
- pt = get_tank_reaction(p, 't0')
- assert pt['tank'] == 't0'
- assert pt['value'] == None
-
- self.leave(p)
-
-
- def test_tank_reaction_op(self):
- p = 'test_tank_reaction_op'
- self.enter(p)
-
- add_tank(p, ChangeSet({'id': 't0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0, 'init_level': 1.0, 'min_level': 0.0, 'max_level': 2.0, 'diameter': 10.0, 'min_vol': 100.0, 'vol_curve': None, 'overflow': OVERFLOW_NO}))
-
- cs = set_tank_reaction(p, ChangeSet({'tank': 't0', 'value': 10.0})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'tank_reaction'
- assert cs['tank'] == 't0'
- assert cs['value'] == 10.0
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'tank_reaction'
- assert cs['tank'] == 't0'
- assert cs['value'] == None
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'tank_reaction'
- assert cs['tank'] == 't0'
- assert cs['value'] == 10.0
-
- self.leave(p)
-
-
- # 20 mixing
-
-
- def test_mixing(self):
- p = 'test_mixing'
- self.enter(p)
-
- add_tank(p, ChangeSet({'id': 't0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0, 'init_level': 1.0, 'min_level': 0.0, 'max_level': 2.0, 'diameter': 10.0, 'min_vol': 100.0, 'vol_curve': None, 'overflow': OVERFLOW_NO}))
-
- assert get_mixing(p, 't0') == {}
-
- add_mixing(p, ChangeSet({'tank': 't0', 'model': MIXING_MODEL_MIXED, 'value': 10.0}))
- m = get_mixing(p,'t0')
- assert m['tank'] == 't0'
- assert m['model'] == MIXING_MODEL_MIXED
- assert m['value'] == 10.0
-
- set_mixing(p, ChangeSet({'tank': 't0', 'model': MIXING_MODEL_2COMP}))
- m = get_mixing(p,'t0')
- assert m['tank'] == 't0'
- assert m['model'] == MIXING_MODEL_2COMP
- assert m['value'] == 10.0
-
- set_mixing(p, ChangeSet({'tank': 't0', 'value': 20.0}))
- m = get_mixing(p,'t0')
- assert m['tank'] == 't0'
- assert m['model'] == MIXING_MODEL_2COMP
- assert m['value'] == 20.0
-
- delete_mixing(p, ChangeSet({'tank': 't0'}))
-
- assert get_mixing(p, 't0') == {}
-
- self.leave(p)
-
-
- def test_mixing_op(self):
- p = 'test_mixing_op'
- self.enter(p)
-
- add_tank(p, ChangeSet({'id': 't0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0, 'init_level': 1.0, 'min_level': 0.0, 'max_level': 2.0, 'diameter': 10.0, 'min_vol': 100.0, 'vol_curve': None, 'overflow': OVERFLOW_NO}))
-
- cs = add_mixing(p, ChangeSet({'tank': 't0', 'model': MIXING_MODEL_MIXED, 'value': 10.0})).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'mixing'
- assert cs['tank'] == 't0'
- assert cs['model'] == MIXING_MODEL_MIXED
- assert cs['value'] == 10.0
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'mixing'
- assert cs['tank'] == 't0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'mixing'
- assert cs['tank'] == 't0'
- assert cs['model'] == MIXING_MODEL_MIXED
- assert cs['value'] == 10.0
-
- cs = set_mixing(p, ChangeSet({'tank': 't0', 'model': MIXING_MODEL_2COMP})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'mixing'
- assert cs['tank'] == 't0'
- assert cs['model'] == MIXING_MODEL_2COMP
- assert cs['value'] == 10.0
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'mixing'
- assert cs['tank'] == 't0'
- assert cs['model'] == MIXING_MODEL_MIXED
- assert cs['value'] == 10.0
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'mixing'
- assert cs['tank'] == 't0'
- assert cs['model'] == MIXING_MODEL_2COMP
- assert cs['value'] == 10.0
-
- cs = set_mixing(p, ChangeSet({'tank': 't0', 'value': 20.0})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'mixing'
- assert cs['tank'] == 't0'
- assert cs['model'] == MIXING_MODEL_2COMP
- assert cs['value'] == 20.0
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'mixing'
- assert cs['tank'] == 't0'
- assert cs['model'] == MIXING_MODEL_2COMP
- assert cs['value'] == 10.0
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'mixing'
- assert cs['tank'] == 't0'
- assert cs['model'] == MIXING_MODEL_2COMP
- assert cs['value'] == 20.0
-
- cs = delete_mixing(p, ChangeSet({'tank': 't0'})).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'mixing'
- assert cs['tank'] == 't0'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'mixing'
- assert cs['tank'] == 't0'
- assert cs['model'] == MIXING_MODEL_2COMP
- assert cs['value'] == 20.0
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'mixing'
- assert cs['tank'] == 't0'
-
- self.leave(p)
-
-
- # 21 time
-
-
- def test_time(self):
- p = 'test_time'
- self.enter(p)
-
- t = get_time(p)
- assert t['DURATION'] == '0:00'
- assert t['HYDRAULIC TIMESTEP'] == '1:00'
- assert t['QUALITY TIMESTEP'] == '0:05'
- assert t['RULE TIMESTEP'] == '0:05'
- assert t['PATTERN TIMESTEP'] == '1:00'
- assert t['PATTERN START'] == '0:00'
- assert t['REPORT TIMESTEP'] == '1:00'
- assert t['REPORT START'] == '0:00'
- assert t['START CLOCKTIME'] == '12:00 AM'
- assert t['STATISTIC'] == TIME_STATISTIC_NONE
-
- t['STATISTIC'] = TIME_STATISTIC_AVERAGED
- set_time(p, ChangeSet(t))
-
- t = get_time(p)
- assert t['DURATION'] == '0:00'
- assert t['HYDRAULIC TIMESTEP'] == '1:00'
- assert t['QUALITY TIMESTEP'] == '0:05'
- assert t['RULE TIMESTEP'] == '0:05'
- assert t['PATTERN TIMESTEP'] == '1:00'
- assert t['PATTERN START'] == '0:00'
- assert t['REPORT TIMESTEP'] == '1:00'
- assert t['REPORT START'] == '0:00'
- assert t['START CLOCKTIME'] == '12:00 AM'
- assert t['STATISTIC'] == TIME_STATISTIC_AVERAGED
-
- self.leave(p)
-
-
- def test_time_op(self):
- p = 'test_time_op'
- self.enter(p)
-
- t = get_time(p)
- assert t['DURATION'] == '0:00'
- assert t['HYDRAULIC TIMESTEP'] == '1:00'
- assert t['QUALITY TIMESTEP'] == '0:05'
- assert t['RULE TIMESTEP'] == '0:05'
- assert t['PATTERN TIMESTEP'] == '1:00'
- assert t['PATTERN START'] == '0:00'
- assert t['REPORT TIMESTEP'] == '1:00'
- assert t['REPORT START'] == '0:00'
- assert t['START CLOCKTIME'] == '12:00 AM'
- assert t['STATISTIC'] == TIME_STATISTIC_NONE
-
- t['STATISTIC'] = TIME_STATISTIC_AVERAGED
- cs = set_time(p, ChangeSet(t)).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'time'
- assert cs['DURATION'] == '0:00'
- assert cs['HYDRAULIC TIMESTEP'] == '1:00'
- assert cs['QUALITY TIMESTEP'] == '0:05'
- assert cs['RULE TIMESTEP'] == '0:05'
- assert cs['PATTERN TIMESTEP'] == '1:00'
- assert cs['PATTERN START'] == '0:00'
- assert cs['REPORT TIMESTEP'] == '1:00'
- assert cs['REPORT START'] == '0:00'
- assert cs['START CLOCKTIME'] == '12:00 AM'
- assert cs['STATISTIC'] == TIME_STATISTIC_AVERAGED
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'time'
- assert cs['DURATION'] == '0:00'
- assert cs['HYDRAULIC TIMESTEP'] == '1:00'
- assert cs['QUALITY TIMESTEP'] == '0:05'
- assert cs['RULE TIMESTEP'] == '0:05'
- assert cs['PATTERN TIMESTEP'] == '1:00'
- assert cs['PATTERN START'] == '0:00'
- assert cs['REPORT TIMESTEP'] == '1:00'
- assert cs['REPORT START'] == '0:00'
- assert cs['START CLOCKTIME'] == '12:00 AM'
- assert cs['STATISTIC'] == TIME_STATISTIC_NONE
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'time'
- assert cs['DURATION'] == '0:00'
- assert cs['HYDRAULIC TIMESTEP'] == '1:00'
- assert cs['QUALITY TIMESTEP'] == '0:05'
- assert cs['RULE TIMESTEP'] == '0:05'
- assert cs['PATTERN TIMESTEP'] == '1:00'
- assert cs['PATTERN START'] == '0:00'
- assert cs['REPORT TIMESTEP'] == '1:00'
- assert cs['REPORT START'] == '0:00'
- assert cs['START CLOCKTIME'] == '12:00 AM'
- assert cs['STATISTIC'] == TIME_STATISTIC_AVERAGED
-
- self.leave(p)
-
-
- # 22 option
-
-
- def test_option(self):
- p = 'test_option'
- self.enter(p)
-
- o = get_option(p)
- assert o['UNITS'] == OPTION_UNITS_LPS
- assert o['PRESSURE'] == OPTION_PRESSURE_METERS
- assert o['HEADLOSS'] == OPTION_HEADLOSS_HW
- assert o['QUALITY'] == OPTION_QUALITY_NONE
- assert o['UNBALANCED'] == OPTION_UNBALANCED_STOP
- assert o['PATTERN'] == '1'
- assert o['DEMAND MODEL'] == OPTION_DEMAND_MODEL_DDA
- assert o['DEMAND MULTIPLIER'] == '1.0'
- assert o['EMITTER EXPONENT'] == '0.5'
- assert o['VISCOSITY'] == '1.0'
- assert o['DIFFUSIVITY'] == '1.0'
- assert o['SPECIFIC GRAVITY'] == '1.0'
- assert o['TRIALS'] == '40'
- assert o['ACCURACY'] == '0.001'
- assert o['HEADERROR'] == '0.0001'
- assert o['FLOWCHANGE'] == '0.0001'
- assert o['MINIMUM PRESSURE'] == '0.0001'
- assert o['REQUIRED PRESSURE'] == '20.0'
- assert o['PRESSURE EXPONENT'] == '0.5'
- assert o['TOLERANCE'] == '0.01'
- assert o['HTOL'] == '0.0005'
- assert o['QTOL'] == '0.0001'
- assert o['RQTOL'] == '0.0000001'
- assert o['CHECKFREQ'] == '2'
- assert o['MAXCHECK'] == '10'
- assert o['DAMPLIMIT'] == '0'
-
- o3 = get_option_v3(p)
- assert o3['FLOW_UNITS'] == OPTION_V3_FLOW_UNITS_LPS
- assert o3['PRESSURE_UNITS'] == OPTION_V3_PRESSURE_UNITS_METERS
- assert o3['HEADLOSS_MODEL'] == OPTION_V3_HEADLOSS_MODEL_HW
- assert o3['SPECIFIC_GRAVITY'] == '1.0'
- assert o3['SPECIFIC_VISCOSITY'] == '1.0'
- assert o3['MAXIMUM_TRIALS'] == '40'
- assert o3['HEAD_TOLERANCE'] == '0.0005'
- assert o3['FLOW_TOLERANCE'] == '0.0001'
- assert o3['FLOW_CHANGE_LIMIT'] == '0.0001'
- assert o3['RELATIVE_ACCURACY'] == '0.001'
- assert o3['TIME_WEIGHT'] == '0.0'
- assert o3['STEP_SIZING'] == OPTION_V3_STEP_SIZING_FULL
- assert o3['IF_UNBALANCED'] == OPTION_V3_IF_UNBALANCED_STOP
- assert o3['DEMAND_MODEL'] == OPTION_V3_DEMAND_MODEL_FIXED
- assert o3['DEMAND_PATTERN'] == '1'
- assert o3['DEMAND_MULTIPLIER'] == '1.0'
- assert o3['MINIMUM_PRESSURE'] == '0.0001'
- assert o3['SERVICE_PRESSURE'] == '20.0'
- assert o3['PRESSURE_EXPONENT'] == '0.5'
- assert o3['LEAKAGE_MODEL'] == OPTION_V3_LEAKAGE_MODEL_NONE
- assert o3['LEAKAGE_COEFF1'] == '0.0'
- assert o3['LEAKAGE_COEFF2'] == '0.0'
- assert o3['EMITTER_EXPONENT'] == '0.5'
- assert o3['QUALITY_MODEL'] == OPTION_V3_QUALITY_MODEL_NONE
- assert o3['QUALITY_NAME'] == OPTION_V3_QUALITY_MODEL_CHEMICAL
- assert o3['QUALITY_UNITS'] == OPTION_V3_QUALITY_UNITS_MGL
- assert o3['TRACE_NODE'] == ''
- assert o3['SPECIFIC_DIFFUSIVITY'] == '1.0'
- assert o3['QUALITY_TOLERANCE'] == '0.01'
-
- o['UNITS'] = OPTION_UNITS_LPS
- o['PRESSURE'] = OPTION_PRESSURE_KPA
- o['HEADLOSS'] = OPTION_HEADLOSS_DW
- o['QUALITY'] = f'{OPTION_QUALITY_TRACE} 1'
- o['UNBALANCED'] = OPTION_UNBALANCED_CONTINUE
- o['PATTERN'] = '2'
- o['DEMAND MODEL'] = OPTION_DEMAND_MODEL_PDA
- o['DEMAND MULTIPLIER'] = '2.0'
- o['EMITTER EXPONENT'] = '1.5'
- o['VISCOSITY'] = '2.0'
- o['DIFFUSIVITY'] = '2.0'
- o['SPECIFIC GRAVITY'] = '2.0'
- o['TRIALS'] = '50'
- o['ACCURACY'] = '0.0001'
- o['HEADERROR'] = '0.01'
- o['FLOWCHANGE'] = '0.01'
- o['MINIMUM PRESSURE'] = '0.01'
- o['REQUIRED PRESSURE'] = '0.01'
- o['PRESSURE EXPONENT'] = '0.05'
- o['TOLERANCE'] = '0.001'
- o['HTOL'] = '0.005'
- o['QTOL'] = '0.001'
- o['RQTOL'] = '0.000001'
- o['CHECKFREQ'] = '1'
- o['MAXCHECK'] = '15'
- o['DAMPLIMIT'] = '1'
-
- set_option(p, ChangeSet(o))
-
- o = get_option(p)
- assert o['UNITS'] == OPTION_UNITS_LPS
- assert o['PRESSURE'] == OPTION_PRESSURE_KPA
- assert o['HEADLOSS'] == OPTION_HEADLOSS_DW
- assert o['QUALITY'] == f'{OPTION_QUALITY_TRACE} 1'
- assert o['UNBALANCED'] == OPTION_UNBALANCED_CONTINUE
- assert o['PATTERN'] == '2'
- assert o['DEMAND MODEL'] == OPTION_DEMAND_MODEL_PDA
- assert o['DEMAND MULTIPLIER'] == '2.0'
- assert o['EMITTER EXPONENT'] == '1.5'
- assert o['VISCOSITY'] == '2.0'
- assert o['DIFFUSIVITY'] == '2.0'
- assert o['SPECIFIC GRAVITY'] == '2.0'
- assert o['TRIALS'] == '50'
- assert o['ACCURACY'] == '0.0001'
- assert o['HEADERROR'] == '0.01'
- assert o['FLOWCHANGE'] == '0.01'
- assert o['MINIMUM PRESSURE'] == '0.01'
- assert o['REQUIRED PRESSURE'] == '0.01'
- assert o['PRESSURE EXPONENT'] == '0.05'
- assert o['TOLERANCE'] == '0.001'
- assert o['HTOL'] == '0.005'
- assert o['QTOL'] == '0.001'
- assert o['RQTOL'] == '0.000001'
- assert o['CHECKFREQ'] == '1'
- assert o['MAXCHECK'] == '15'
- assert o['DAMPLIMIT'] == '1'
-
- o3 = get_option_v3(p)
- assert o3['FLOW_UNITS'] == OPTION_V3_FLOW_UNITS_LPS
- assert o3['PRESSURE_UNITS'] == OPTION_V3_PRESSURE_UNITS_KPA
- assert o3['HEADLOSS_MODEL'] == OPTION_V3_HEADLOSS_MODEL_DW
- assert o3['SPECIFIC_GRAVITY'] == '2.0'
- assert o3['SPECIFIC_VISCOSITY'] == '2.0'
- assert o3['MAXIMUM_TRIALS'] == '50'
- assert o3['HEAD_TOLERANCE'] == '0.005'
- assert o3['FLOW_TOLERANCE'] == '0.001'
- assert o3['FLOW_CHANGE_LIMIT'] == '0.01'
- assert o3['RELATIVE_ACCURACY'] == '0.0001'
- assert o3['TIME_WEIGHT'] == '0.0'
- assert o3['STEP_SIZING'] == OPTION_V3_STEP_SIZING_FULL
- assert o3['IF_UNBALANCED'] == OPTION_V3_IF_UNBALANCED_CONTINUE
- assert o3['DEMAND_MODEL'] == OPTION_V3_DEMAND_MODEL_POWER
- assert o3['DEMAND_PATTERN'] == '2'
- assert o3['DEMAND_MULTIPLIER'] == '2.0'
- assert o3['MINIMUM_PRESSURE'] == '0.01'
- assert o3['SERVICE_PRESSURE'] == '0.01'
- assert o3['PRESSURE_EXPONENT'] == '0.05'
- assert o3['LEAKAGE_MODEL'] == OPTION_V3_LEAKAGE_MODEL_NONE
- assert o3['LEAKAGE_COEFF1'] == '0.0'
- assert o3['LEAKAGE_COEFF2'] == '0.0'
- assert o3['EMITTER_EXPONENT'] == '1.5'
- assert o3['QUALITY_MODEL'] == OPTION_V3_QUALITY_MODEL_TRACE
- assert o3['QUALITY_NAME'] == OPTION_V3_QUALITY_MODEL_CHEMICAL
- assert o3['QUALITY_UNITS'] == OPTION_V3_QUALITY_UNITS_MGL
- assert o3['TRACE_NODE'] == '1'
- assert o3['SPECIFIC_DIFFUSIVITY'] == '2.0'
- assert o3['QUALITY_TOLERANCE'] == '0.001'
-
- o3['FLOW_UNITS'] = OPTION_V3_FLOW_UNITS_GPM
- o3['PRESSURE_UNITS'] = OPTION_V3_PRESSURE_UNITS_PSI
- o3['HEADLOSS_MODEL'] = OPTION_V3_HEADLOSS_MODEL_HW
- o3['SPECIFIC_GRAVITY'] = '1.0'
- o3['SPECIFIC_VISCOSITY'] = '1.0'
- o3['MAXIMUM_TRIALS'] = '40'
- o3['HEAD_TOLERANCE'] = '0.0005'
- o3['FLOW_TOLERANCE'] = '0.0001'
- o3['FLOW_CHANGE_LIMIT'] = '0.0'
- o3['RELATIVE_ACCURACY'] = '0.001'
- o3['TIME_WEIGHT'] = '0.0'
- o3['STEP_SIZING'] = OPTION_V3_STEP_SIZING_RELAXATION
- o3['IF_UNBALANCED'] = OPTION_V3_IF_UNBALANCED_STOP
- o3['DEMAND_MODEL'] = OPTION_V3_DEMAND_MODEL_FIXED
- o3['DEMAND_PATTERN'] = '1'
- o3['DEMAND_MULTIPLIER'] = '1.0'
- o3['MINIMUM_PRESSURE'] = '0.0'
- o3['SERVICE_PRESSURE'] = '0.1'
- o3['PRESSURE_EXPONENT'] = '0.5'
- o3['LEAKAGE_MODEL'] = OPTION_V3_LEAKAGE_MODEL_POWER
- o3['LEAKAGE_COEFF1'] = '1.0'
- o3['LEAKAGE_COEFF2'] = '2.0'
- o3['EMITTER_EXPONENT'] = '0.5'
- o3['QUALITY_MODEL'] = OPTION_V3_QUALITY_MODEL_TRACE
- o3['QUALITY_NAME'] = OPTION_V3_QUALITY_MODEL_NONE
- o3['QUALITY_UNITS'] = OPTION_V3_QUALITY_UNITS_HRS
- o3['TRACE_NODE'] = '2'
- o3['SPECIFIC_DIFFUSIVITY'] = '1.0'
- o3['QUALITY_TOLERANCE'] = '0.01'
-
- set_option_v3(p, ChangeSet(o3))
-
- o3 = get_option_v3(p)
- assert o3['FLOW_UNITS'] == OPTION_V3_FLOW_UNITS_GPM
- assert o3['PRESSURE_UNITS'] == OPTION_V3_PRESSURE_UNITS_PSI
- assert o3['HEADLOSS_MODEL'] == OPTION_V3_HEADLOSS_MODEL_HW
- assert o3['SPECIFIC_GRAVITY'] == '1.0'
- assert o3['SPECIFIC_VISCOSITY'] == '1.0'
- assert o3['MAXIMUM_TRIALS'] == '40'
- assert o3['HEAD_TOLERANCE'] == '0.0005'
- assert o3['FLOW_TOLERANCE'] == '0.0001'
- assert o3['FLOW_CHANGE_LIMIT'] == '0.0'
- assert o3['RELATIVE_ACCURACY'] == '0.001'
- assert o3['TIME_WEIGHT'] == '0.0'
- assert o3['STEP_SIZING'] == OPTION_V3_STEP_SIZING_RELAXATION
- assert o3['IF_UNBALANCED'] == OPTION_V3_IF_UNBALANCED_STOP
- assert o3['DEMAND_MODEL'] == OPTION_V3_DEMAND_MODEL_FIXED
- assert o3['DEMAND_PATTERN'] == '1'
- assert o3['DEMAND_MULTIPLIER'] == '1.0'
- assert o3['MINIMUM_PRESSURE'] == '0.0'
- assert o3['SERVICE_PRESSURE'] == '0.1'
- assert o3['PRESSURE_EXPONENT'] == '0.5'
- assert o3['LEAKAGE_MODEL'] == OPTION_V3_LEAKAGE_MODEL_POWER
- assert o3['LEAKAGE_COEFF1'] == '1.0'
- assert o3['LEAKAGE_COEFF2'] == '2.0'
- assert o3['EMITTER_EXPONENT'] == '0.5'
- assert o3['QUALITY_MODEL'] == OPTION_V3_QUALITY_MODEL_TRACE
- assert o3['QUALITY_NAME'] == OPTION_V3_QUALITY_MODEL_NONE
- assert o3['QUALITY_UNITS'] == OPTION_V3_QUALITY_UNITS_HRS
- assert o3['TRACE_NODE'] == '2'
- assert o3['SPECIFIC_DIFFUSIVITY'] == '1.0'
- assert o3['QUALITY_TOLERANCE'] == '0.01'
-
- o = get_option(p)
- assert o['UNITS'] == OPTION_UNITS_GPM
- assert o['PRESSURE'] == OPTION_PRESSURE_PSI
- assert o['HEADLOSS'] == OPTION_HEADLOSS_HW
- assert o['QUALITY'] == f'{OPTION_QUALITY_TRACE} 2'
- assert o['UNBALANCED'] == OPTION_UNBALANCED_STOP
- assert o['PATTERN'] == '1'
- assert o['DEMAND MODEL'] == OPTION_DEMAND_MODEL_DDA
- assert o['DEMAND MULTIPLIER'] == '1.0'
- assert o['EMITTER EXPONENT'] == '0.5'
- assert o['VISCOSITY'] == '1.0'
- assert o['DIFFUSIVITY'] == '1.0'
- assert o['SPECIFIC GRAVITY'] == '1.0'
- assert o['TRIALS'] == '40'
- assert o['ACCURACY'] == '0.001'
- assert o['HEADERROR'] == '0.01'
- assert o['FLOWCHANGE'] == '0.0'
- assert o['MINIMUM PRESSURE'] == '0.0'
- assert o['REQUIRED PRESSURE'] == '0.1'
- assert o['PRESSURE EXPONENT'] == '0.5'
- assert o['TOLERANCE'] == '0.01'
- assert o['HTOL'] == '0.0005'
- assert o['QTOL'] == '0.0001'
- assert o['RQTOL'] == '0.000001'
- assert o['CHECKFREQ'] == '1'
- assert o['MAXCHECK'] == '15'
- assert o['DAMPLIMIT'] == '1'
-
- o3['DEMAND_MODEL'] = OPTION_V3_DEMAND_MODEL_CONSTRAINED
- set_option_v3(p, ChangeSet(o3))
- assert o3['DEMAND_MODEL'] == OPTION_V3_DEMAND_MODEL_CONSTRAINED
- o = get_option(p)
- assert o['DEMAND MODEL'] == OPTION_DEMAND_MODEL_PDA
-
- o3['DEMAND_MODEL'] = OPTION_V3_DEMAND_MODEL_LOGISTIC
- set_option_v3(p, ChangeSet(o3))
- assert o3['DEMAND_MODEL'] == OPTION_V3_DEMAND_MODEL_LOGISTIC
- o = get_option(p)
- assert o['DEMAND MODEL'] == OPTION_DEMAND_MODEL_PDA
-
- self.leave(p)
-
-
- def test_option_op(self):
- p = 'test_option_op'
- self.enter(p)
-
- o = get_option(p)
- assert o['UNITS'] == OPTION_UNITS_LPS
- assert o['PRESSURE'] == OPTION_PRESSURE_METERS
- assert o['HEADLOSS'] == OPTION_HEADLOSS_HW
- assert o['QUALITY'] == OPTION_QUALITY_NONE
- assert o['UNBALANCED'] == OPTION_UNBALANCED_STOP
- assert o['PATTERN'] == '1'
- assert o['DEMAND MODEL'] == OPTION_DEMAND_MODEL_DDA
- assert o['DEMAND MULTIPLIER'] == '1.0'
- assert o['EMITTER EXPONENT'] == '0.5'
- assert o['VISCOSITY'] == '1.0'
- assert o['DIFFUSIVITY'] == '1.0'
- assert o['SPECIFIC GRAVITY'] == '1.0'
- assert o['TRIALS'] == '40'
- assert o['ACCURACY'] == '0.001'
- assert o['HEADERROR'] == '0.0001'
- assert o['FLOWCHANGE'] == '0.0001'
- assert o['MINIMUM PRESSURE'] == '0.0001'
- assert o['REQUIRED PRESSURE'] == '20.0'
- assert o['PRESSURE EXPONENT'] == '0.5'
- assert o['TOLERANCE'] == '0.01'
- assert o['HTOL'] == '0.0005'
- assert o['QTOL'] == '0.0001'
- assert o['RQTOL'] == '0.0000001'
- assert o['CHECKFREQ'] == '2'
- assert o['MAXCHECK'] == '10'
- assert o['DAMPLIMIT'] == '0'
-
- o3 = get_option_v3(p)
- assert o3['FLOW_UNITS'] == OPTION_V3_FLOW_UNITS_LPS
- assert o3['PRESSURE_UNITS'] == OPTION_V3_PRESSURE_UNITS_METERS
- assert o3['HEADLOSS_MODEL'] == OPTION_V3_HEADLOSS_MODEL_HW
- assert o3['SPECIFIC_GRAVITY'] == '1.0'
- assert o3['SPECIFIC_VISCOSITY'] == '1.0'
- assert o3['MAXIMUM_TRIALS'] == '40'
- assert o3['HEAD_TOLERANCE'] == '0.0005'
- assert o3['FLOW_TOLERANCE'] == '0.0001'
- assert o3['FLOW_CHANGE_LIMIT'] == '0.0001'
- assert o3['RELATIVE_ACCURACY'] == '0.001'
- assert o3['TIME_WEIGHT'] == '0.0'
- assert o3['STEP_SIZING'] == OPTION_V3_STEP_SIZING_FULL
- assert o3['IF_UNBALANCED'] == OPTION_V3_IF_UNBALANCED_STOP
- assert o3['DEMAND_MODEL'] == OPTION_V3_DEMAND_MODEL_FIXED
- assert o3['DEMAND_PATTERN'] == '1'
- assert o3['DEMAND_MULTIPLIER'] == '1.0'
- assert o3['MINIMUM_PRESSURE'] == '0.0001'
- assert o3['SERVICE_PRESSURE'] == '20.0'
- assert o3['PRESSURE_EXPONENT'] == '0.5'
- assert o3['LEAKAGE_MODEL'] == OPTION_V3_LEAKAGE_MODEL_NONE
- assert o3['LEAKAGE_COEFF1'] == '0.0'
- assert o3['LEAKAGE_COEFF2'] == '0.0'
- assert o3['EMITTER_EXPONENT'] == '0.5'
- assert o3['QUALITY_MODEL'] == OPTION_V3_QUALITY_MODEL_NONE
- assert o3['QUALITY_NAME'] == OPTION_V3_QUALITY_MODEL_CHEMICAL
- assert o3['QUALITY_UNITS'] == OPTION_V3_QUALITY_UNITS_MGL
- assert o3['TRACE_NODE'] == ''
- assert o3['SPECIFIC_DIFFUSIVITY'] == '1.0'
- assert o3['QUALITY_TOLERANCE'] == '0.01'
-
- o['UNITS'] = OPTION_UNITS_LPS
- o['PRESSURE'] = OPTION_PRESSURE_KPA
- o['HEADLOSS'] = OPTION_HEADLOSS_DW
- o['QUALITY'] = f'{OPTION_QUALITY_TRACE} 1'
- o['UNBALANCED'] = OPTION_UNBALANCED_CONTINUE
- o['PATTERN'] = '2'
- o['DEMAND MODEL'] = OPTION_DEMAND_MODEL_PDA
- o['DEMAND MULTIPLIER'] = '2.0'
- o['EMITTER EXPONENT'] = '1.5'
- o['VISCOSITY'] = '2.0'
- o['DIFFUSIVITY'] = '2.0'
- o['SPECIFIC GRAVITY'] = '2.0'
- o['TRIALS'] = '50'
- o['ACCURACY'] = '0.0001'
- o['HEADERROR'] = '0.01'
- o['FLOWCHANGE'] = '0.01'
- o['MINIMUM PRESSURE'] = '0.01'
- o['REQUIRED PRESSURE'] = '0.01'
- o['PRESSURE EXPONENT'] = '0.05'
- o['TOLERANCE'] = '0.001'
- o['HTOL'] = '0.005'
- o['QTOL'] = '0.001'
- o['RQTOL'] = '0.000001'
- o['CHECKFREQ'] = '1'
- o['MAXCHECK'] = '15'
- o['DAMPLIMIT'] = '1'
-
- css = set_option(p, ChangeSet(o)).operations
- cs = css[0]
- assert cs['UNITS'] == OPTION_UNITS_LPS
- assert cs['PRESSURE'] == OPTION_PRESSURE_KPA
- assert cs['HEADLOSS'] == OPTION_HEADLOSS_DW
- assert cs['QUALITY'] == f'{OPTION_QUALITY_TRACE} 1'
- assert cs['UNBALANCED'] == OPTION_UNBALANCED_CONTINUE
- assert cs['PATTERN'] == '2'
- assert cs['DEMAND MODEL'] == OPTION_DEMAND_MODEL_PDA
- assert cs['DEMAND MULTIPLIER'] == '2.0'
- assert cs['EMITTER EXPONENT'] == '1.5'
- assert cs['VISCOSITY'] == '2.0'
- assert cs['DIFFUSIVITY'] == '2.0'
- assert cs['SPECIFIC GRAVITY'] == '2.0'
- assert cs['TRIALS'] == '50'
- assert cs['ACCURACY'] == '0.0001'
- assert cs['HEADERROR'] == '0.01'
- assert cs['FLOWCHANGE'] == '0.01'
- assert cs['MINIMUM PRESSURE'] == '0.01'
- assert cs['REQUIRED PRESSURE'] == '0.01'
- assert cs['PRESSURE EXPONENT'] == '0.05'
- assert cs['TOLERANCE'] == '0.001'
- assert cs['HTOL'] == '0.005'
- assert cs['QTOL'] == '0.001'
- assert cs['RQTOL'] == '0.000001'
- assert cs['CHECKFREQ'] == '1'
- assert cs['MAXCHECK'] == '15'
- assert cs['DAMPLIMIT'] == '1'
- cs = css[1]
- assert cs['FLOW_UNITS'] == OPTION_V3_FLOW_UNITS_LPS
- assert cs['PRESSURE_UNITS'] == OPTION_V3_PRESSURE_UNITS_KPA
- assert cs['HEADLOSS_MODEL'] == OPTION_V3_HEADLOSS_MODEL_DW
- assert cs['SPECIFIC_GRAVITY'] == '2.0'
- assert cs['SPECIFIC_VISCOSITY'] == '2.0'
- assert cs['MAXIMUM_TRIALS'] == '50'
- assert cs['HEAD_TOLERANCE'] == '0.005'
- assert cs['FLOW_TOLERANCE'] == '0.001'
- assert cs['FLOW_CHANGE_LIMIT'] == '0.01'
- assert cs['RELATIVE_ACCURACY'] == '0.0001'
- assert cs['IF_UNBALANCED'] == OPTION_V3_IF_UNBALANCED_CONTINUE
- assert cs['DEMAND_MODEL'] == OPTION_V3_DEMAND_MODEL_POWER
- assert cs['DEMAND_PATTERN'] == '2'
- assert cs['DEMAND_MULTIPLIER'] == '2.0'
- assert cs['MINIMUM_PRESSURE'] == '0.01'
- assert cs['SERVICE_PRESSURE'] == '0.01'
- assert cs['PRESSURE_EXPONENT'] == '0.05'
- assert cs['EMITTER_EXPONENT'] == '1.5'
- assert cs['QUALITY_MODEL'] == OPTION_V3_QUALITY_MODEL_TRACE
- assert cs['TRACE_NODE'] == '1'
- assert cs['SPECIFIC_DIFFUSIVITY'] == '2.0'
- assert cs['QUALITY_TOLERANCE'] == '0.001'
-
- css = execute_undo(p).operations
- cs = css[0]
- assert cs['FLOW_UNITS'] == OPTION_V3_FLOW_UNITS_LPS
- assert cs['PRESSURE_UNITS'] == OPTION_V3_PRESSURE_UNITS_METERS
- assert cs['HEADLOSS_MODEL'] == OPTION_V3_HEADLOSS_MODEL_HW
- assert cs['SPECIFIC_GRAVITY'] == '1.0'
- assert cs['SPECIFIC_VISCOSITY'] == '1.0'
- assert cs['MAXIMUM_TRIALS'] == '40'
- assert cs['HEAD_TOLERANCE'] == '0.0005'
- assert cs['FLOW_TOLERANCE'] == '0.0001'
- assert cs['FLOW_CHANGE_LIMIT'] == '0.0001'
- assert cs['RELATIVE_ACCURACY'] == '0.001'
- assert cs['IF_UNBALANCED'] == OPTION_V3_IF_UNBALANCED_STOP
- assert cs['DEMAND_MODEL'] == OPTION_V3_DEMAND_MODEL_FIXED
- assert cs['DEMAND_PATTERN'] == '1'
- assert cs['DEMAND_MULTIPLIER'] == '1.0'
- assert cs['MINIMUM_PRESSURE'] == '0.0001'
- assert cs['SERVICE_PRESSURE'] == '20.0'
- assert cs['PRESSURE_EXPONENT'] == '0.5'
- assert cs['EMITTER_EXPONENT'] == '0.5'
- assert cs['QUALITY_MODEL'] == OPTION_V3_QUALITY_MODEL_NONE
- assert cs['TRACE_NODE'] == ''
- assert cs['SPECIFIC_DIFFUSIVITY'] == '1.0'
- assert cs['QUALITY_TOLERANCE'] == '0.01'
- cs = css[1]
- assert cs['UNITS'] == OPTION_UNITS_LPS
- assert cs['PRESSURE'] == OPTION_PRESSURE_METERS
- assert cs['HEADLOSS'] == OPTION_HEADLOSS_HW
- assert cs['QUALITY'] == OPTION_QUALITY_NONE
- assert cs['UNBALANCED'] == OPTION_UNBALANCED_STOP
- assert cs['PATTERN'] == '1'
- assert cs['DEMAND MODEL'] == OPTION_DEMAND_MODEL_DDA
- assert cs['DEMAND MULTIPLIER'] == '1.0'
- assert cs['EMITTER EXPONENT'] == '0.5'
- assert cs['VISCOSITY'] == '1.0'
- assert cs['DIFFUSIVITY'] == '1.0'
- assert cs['SPECIFIC GRAVITY'] == '1.0'
- assert cs['TRIALS'] == '40'
- assert cs['ACCURACY'] == '0.001'
- assert cs['HEADERROR'] == '0.0001'
- assert cs['FLOWCHANGE'] == '0.0001'
- assert cs['MINIMUM PRESSURE'] == '0.0001'
- assert cs['REQUIRED PRESSURE'] == '20.0'
- assert cs['PRESSURE EXPONENT'] == '0.5'
- assert cs['TOLERANCE'] == '0.01'
- assert cs['HTOL'] == '0.0005'
- assert cs['QTOL'] == '0.0001'
- assert cs['RQTOL'] == '0.0000001'
- assert cs['CHECKFREQ'] == '2'
- assert cs['MAXCHECK'] == '10'
- assert cs['DAMPLIMIT'] == '0'
-
- css = execute_redo(p).operations
- cs = css[0]
- assert cs['UNITS'] == OPTION_UNITS_LPS
- assert cs['PRESSURE'] == OPTION_PRESSURE_KPA
- assert cs['HEADLOSS'] == OPTION_HEADLOSS_DW
- assert cs['QUALITY'] == f'{OPTION_QUALITY_TRACE} 1'
- assert cs['UNBALANCED'] == OPTION_UNBALANCED_CONTINUE
- assert cs['PATTERN'] == '2'
- assert cs['DEMAND MODEL'] == OPTION_DEMAND_MODEL_PDA
- assert cs['DEMAND MULTIPLIER'] == '2.0'
- assert cs['EMITTER EXPONENT'] == '1.5'
- assert cs['VISCOSITY'] == '2.0'
- assert cs['DIFFUSIVITY'] == '2.0'
- assert cs['SPECIFIC GRAVITY'] == '2.0'
- assert cs['TRIALS'] == '50'
- assert cs['ACCURACY'] == '0.0001'
- assert cs['HEADERROR'] == '0.01'
- assert cs['FLOWCHANGE'] == '0.01'
- assert cs['MINIMUM PRESSURE'] == '0.01'
- assert cs['REQUIRED PRESSURE'] == '0.01'
- assert cs['PRESSURE EXPONENT'] == '0.05'
- assert cs['TOLERANCE'] == '0.001'
- assert cs['HTOL'] == '0.005'
- assert cs['QTOL'] == '0.001'
- assert cs['RQTOL'] == '0.000001'
- assert cs['CHECKFREQ'] == '1'
- assert cs['MAXCHECK'] == '15'
- assert cs['DAMPLIMIT'] == '1'
- cs = css[1]
- assert cs['FLOW_UNITS'] == OPTION_V3_FLOW_UNITS_LPS
- assert cs['PRESSURE_UNITS'] == OPTION_V3_PRESSURE_UNITS_KPA
- assert cs['HEADLOSS_MODEL'] == OPTION_V3_HEADLOSS_MODEL_DW
- assert cs['SPECIFIC_GRAVITY'] == '2.0'
- assert cs['SPECIFIC_VISCOSITY'] == '2.0'
- assert cs['MAXIMUM_TRIALS'] == '50'
- assert cs['HEAD_TOLERANCE'] == '0.005'
- assert cs['FLOW_TOLERANCE'] == '0.001'
- assert cs['FLOW_CHANGE_LIMIT'] == '0.01'
- assert cs['RELATIVE_ACCURACY'] == '0.0001'
- assert cs['IF_UNBALANCED'] == OPTION_V3_IF_UNBALANCED_CONTINUE
- assert cs['DEMAND_MODEL'] == OPTION_V3_DEMAND_MODEL_POWER
- assert cs['DEMAND_PATTERN'] == '2'
- assert cs['DEMAND_MULTIPLIER'] == '2.0'
- assert cs['MINIMUM_PRESSURE'] == '0.01'
- assert cs['SERVICE_PRESSURE'] == '0.01'
- assert cs['PRESSURE_EXPONENT'] == '0.05'
- assert cs['EMITTER_EXPONENT'] == '1.5'
- assert cs['QUALITY_MODEL'] == OPTION_V3_QUALITY_MODEL_TRACE
- assert cs['TRACE_NODE'] == '1'
- assert cs['SPECIFIC_DIFFUSIVITY'] == '2.0'
- assert cs['QUALITY_TOLERANCE'] == '0.001'
-
- o3['FLOW_UNITS'] = OPTION_V3_FLOW_UNITS_GPM
- o3['PRESSURE_UNITS'] = OPTION_V3_PRESSURE_UNITS_PSI
- o3['HEADLOSS_MODEL'] = OPTION_V3_HEADLOSS_MODEL_HW
- o3['SPECIFIC_GRAVITY'] = '1.0'
- o3['SPECIFIC_VISCOSITY'] = '1.0'
- o3['MAXIMUM_TRIALS'] = '40'
- o3['HEAD_TOLERANCE'] = '0.0005'
- o3['FLOW_TOLERANCE'] = '0.0001'
- o3['FLOW_CHANGE_LIMIT'] = '0.0'
- o3['RELATIVE_ACCURACY'] = '0.001'
- o3['TIME_WEIGHT'] = '0.0'
- o3['STEP_SIZING'] = OPTION_V3_STEP_SIZING_RELAXATION
- o3['IF_UNBALANCED'] = OPTION_V3_IF_UNBALANCED_STOP
- o3['DEMAND_MODEL'] = OPTION_V3_DEMAND_MODEL_FIXED
- o3['DEMAND_PATTERN'] = '1'
- o3['DEMAND_MULTIPLIER'] = '1.0'
- o3['MINIMUM_PRESSURE'] = '0.0'
- o3['SERVICE_PRESSURE'] = '0.1'
- o3['PRESSURE_EXPONENT'] = '0.5'
- o3['LEAKAGE_MODEL'] = OPTION_V3_LEAKAGE_MODEL_POWER
- o3['LEAKAGE_COEFF1'] = '1.0'
- o3['LEAKAGE_COEFF2'] = '2.0'
- o3['EMITTER_EXPONENT'] = '0.5'
- o3['QUALITY_MODEL'] = OPTION_V3_QUALITY_MODEL_TRACE
- o3['QUALITY_NAME'] = OPTION_V3_QUALITY_MODEL_NONE
- o3['QUALITY_UNITS'] = OPTION_V3_QUALITY_UNITS_HRS
- o3['TRACE_NODE'] = '2'
- o3['SPECIFIC_DIFFUSIVITY'] = '1.0'
- o3['QUALITY_TOLERANCE'] = '0.01'
-
- css = set_option_v3(p, ChangeSet(o3)).operations
- cs = css[0]
- assert cs['FLOW_UNITS'] == OPTION_V3_FLOW_UNITS_GPM
- assert cs['PRESSURE_UNITS'] == OPTION_V3_PRESSURE_UNITS_PSI
- assert cs['HEADLOSS_MODEL'] == OPTION_V3_HEADLOSS_MODEL_HW
- assert cs['SPECIFIC_GRAVITY'] == '1.0'
- assert cs['SPECIFIC_VISCOSITY'] == '1.0'
- assert cs['MAXIMUM_TRIALS'] == '40'
- assert cs['HEAD_TOLERANCE'] == '0.0005'
- assert cs['FLOW_TOLERANCE'] == '0.0001'
- assert cs['FLOW_CHANGE_LIMIT'] == '0.0'
- assert cs['RELATIVE_ACCURACY'] == '0.001'
- assert cs['TIME_WEIGHT'] == '0.0'
- assert cs['STEP_SIZING'] == OPTION_V3_STEP_SIZING_RELAXATION
- assert cs['IF_UNBALANCED'] == OPTION_V3_IF_UNBALANCED_STOP
- assert cs['DEMAND_MODEL'] == OPTION_V3_DEMAND_MODEL_FIXED
- assert cs['DEMAND_PATTERN'] == '1'
- assert cs['DEMAND_MULTIPLIER'] == '1.0'
- assert cs['MINIMUM_PRESSURE'] == '0.0'
- assert cs['SERVICE_PRESSURE'] == '0.1'
- assert cs['PRESSURE_EXPONENT'] == '0.5'
- assert cs['LEAKAGE_MODEL'] == OPTION_V3_LEAKAGE_MODEL_POWER
- assert cs['LEAKAGE_COEFF1'] == '1.0'
- assert cs['LEAKAGE_COEFF2'] == '2.0'
- assert cs['EMITTER_EXPONENT'] == '0.5'
- assert cs['QUALITY_MODEL'] == OPTION_V3_QUALITY_MODEL_TRACE
- assert cs['QUALITY_NAME'] == OPTION_V3_QUALITY_MODEL_NONE
- assert cs['QUALITY_UNITS'] == OPTION_V3_QUALITY_UNITS_HRS
- assert cs['TRACE_NODE'] == '2'
- assert cs['SPECIFIC_DIFFUSIVITY'] == '1.0'
- assert cs['QUALITY_TOLERANCE'] == '0.01'
- cs = css[1]
- assert cs['UNITS'] == OPTION_UNITS_GPM
- assert cs['PRESSURE'] == OPTION_PRESSURE_PSI
- assert cs['HEADLOSS'] == OPTION_HEADLOSS_HW
- assert cs['QUALITY'] == f'{OPTION_QUALITY_TRACE} 2'
- assert cs['UNBALANCED'] == OPTION_UNBALANCED_STOP
- assert cs['PATTERN'] == '1'
- assert cs['DEMAND MODEL'] == OPTION_DEMAND_MODEL_DDA
- assert cs['DEMAND MULTIPLIER'] == '1.0'
- assert cs['EMITTER EXPONENT'] == '0.5'
- assert cs['VISCOSITY'] == '1.0'
- assert cs['DIFFUSIVITY'] == '1.0'
- assert cs['SPECIFIC GRAVITY'] == '1.0'
- assert cs['TRIALS'] == '40'
- assert cs['ACCURACY'] == '0.001'
- assert cs['FLOWCHANGE'] == '0.0'
- assert cs['MINIMUM PRESSURE'] == '0.0'
- assert cs['REQUIRED PRESSURE'] == '0.1'
- assert cs['PRESSURE EXPONENT'] == '0.5'
- assert cs['TOLERANCE'] == '0.01'
- assert cs['HTOL'] == '0.0005'
- assert cs['QTOL'] == '0.0001'
-
- css = execute_undo(p).operations
- cs = css[0]
- assert cs['UNITS'] == OPTION_UNITS_LPS
- assert cs['PRESSURE'] == OPTION_PRESSURE_KPA
- assert cs['HEADLOSS'] == OPTION_HEADLOSS_DW
- assert cs['QUALITY'] == f'{OPTION_QUALITY_TRACE} 1'
- assert cs['UNBALANCED'] == OPTION_UNBALANCED_CONTINUE
- assert cs['PATTERN'] == '2'
- assert cs['DEMAND MODEL'] == OPTION_DEMAND_MODEL_PDA
- assert cs['DEMAND MULTIPLIER'] == '2.0'
- assert cs['EMITTER EXPONENT'] == '1.5'
- assert cs['VISCOSITY'] == '2.0'
- assert cs['DIFFUSIVITY'] == '2.0'
- assert cs['SPECIFIC GRAVITY'] == '2.0'
- assert cs['TRIALS'] == '50'
- assert cs['ACCURACY'] == '0.0001'
- assert cs['FLOWCHANGE'] == '0.01'
- assert cs['MINIMUM PRESSURE'] == '0.01'
- assert cs['REQUIRED PRESSURE'] == '0.01'
- assert cs['PRESSURE EXPONENT'] == '0.05'
- assert cs['TOLERANCE'] == '0.001'
- assert cs['HTOL'] == '0.005'
- assert cs['QTOL'] == '0.001'
- cs = css[1]
- o3['FLOW_UNITS'] = OPTION_V3_FLOW_UNITS_GPM
- o3['PRESSURE_UNITS'] = OPTION_V3_PRESSURE_UNITS_PSI
- o3['HEADLOSS_MODEL'] = OPTION_V3_HEADLOSS_MODEL_HW
- o3['SPECIFIC_GRAVITY'] = '1.0'
- o3['SPECIFIC_VISCOSITY'] = '1.0'
- o3['MAXIMUM_TRIALS'] = '40'
- o3['HEAD_TOLERANCE'] = '0.0005'
- o3['FLOW_TOLERANCE'] = '0.0001'
- o3['FLOW_CHANGE_LIMIT'] = '0.0'
- o3['RELATIVE_ACCURACY'] = '0.001'
- o3['TIME_WEIGHT'] = '0.0'
- o3['STEP_SIZING'] = OPTION_V3_STEP_SIZING_RELAXATION
- o3['IF_UNBALANCED'] = OPTION_V3_IF_UNBALANCED_STOP
- o3['DEMAND_MODEL'] = OPTION_V3_DEMAND_MODEL_FIXED
- o3['DEMAND_PATTERN'] = '1'
- o3['DEMAND_MULTIPLIER'] = '1.0'
- o3['MINIMUM_PRESSURE'] = '0.0'
- o3['SERVICE_PRESSURE'] = '0.1'
- o3['PRESSURE_EXPONENT'] = '0.5'
- o3['LEAKAGE_MODEL'] = OPTION_V3_LEAKAGE_MODEL_POWER
- o3['LEAKAGE_COEFF1'] = '1.0'
- o3['LEAKAGE_COEFF2'] = '2.0'
- o3['EMITTER_EXPONENT'] = '0.5'
- o3['QUALITY_MODEL'] = OPTION_V3_QUALITY_MODEL_TRACE
- o3['QUALITY_NAME'] = OPTION_V3_QUALITY_MODEL_NONE
- o3['QUALITY_UNITS'] = OPTION_V3_QUALITY_UNITS_HRS
- o3['TRACE_NODE'] = '2'
- o3['SPECIFIC_DIFFUSIVITY'] = '1.0'
- o3['QUALITY_TOLERANCE'] = '0.01'
-
- css = execute_redo(p).operations
- cs = css[0]
- assert cs['FLOW_UNITS'] == OPTION_V3_FLOW_UNITS_GPM
- assert cs['PRESSURE_UNITS'] == OPTION_V3_PRESSURE_UNITS_PSI
- assert cs['HEADLOSS_MODEL'] == OPTION_V3_HEADLOSS_MODEL_HW
- assert cs['SPECIFIC_GRAVITY'] == '1.0'
- assert cs['SPECIFIC_VISCOSITY'] == '1.0'
- assert cs['MAXIMUM_TRIALS'] == '40'
- assert cs['HEAD_TOLERANCE'] == '0.0005'
- assert cs['FLOW_TOLERANCE'] == '0.0001'
- assert cs['FLOW_CHANGE_LIMIT'] == '0.0'
- assert cs['RELATIVE_ACCURACY'] == '0.001'
- assert cs['TIME_WEIGHT'] == '0.0'
- assert cs['STEP_SIZING'] == OPTION_V3_STEP_SIZING_RELAXATION
- assert cs['IF_UNBALANCED'] == OPTION_V3_IF_UNBALANCED_STOP
- assert cs['DEMAND_MODEL'] == OPTION_V3_DEMAND_MODEL_FIXED
- assert cs['DEMAND_PATTERN'] == '1'
- assert cs['DEMAND_MULTIPLIER'] == '1.0'
- assert cs['MINIMUM_PRESSURE'] == '0.0'
- assert cs['SERVICE_PRESSURE'] == '0.1'
- assert cs['PRESSURE_EXPONENT'] == '0.5'
- assert cs['LEAKAGE_MODEL'] == OPTION_V3_LEAKAGE_MODEL_POWER
- assert cs['LEAKAGE_COEFF1'] == '1.0'
- assert cs['LEAKAGE_COEFF2'] == '2.0'
- assert cs['EMITTER_EXPONENT'] == '0.5'
- assert cs['QUALITY_MODEL'] == OPTION_V3_QUALITY_MODEL_TRACE
- assert cs['QUALITY_NAME'] == OPTION_V3_QUALITY_MODEL_NONE
- assert cs['QUALITY_UNITS'] == OPTION_V3_QUALITY_UNITS_HRS
- assert cs['TRACE_NODE'] == '2'
- assert cs['SPECIFIC_DIFFUSIVITY'] == '1.0'
- assert cs['QUALITY_TOLERANCE'] == '0.01'
- cs = css[1]
- assert cs['UNITS'] == OPTION_UNITS_GPM
- assert cs['PRESSURE'] == OPTION_PRESSURE_PSI
- assert cs['HEADLOSS'] == OPTION_HEADLOSS_HW
- assert cs['QUALITY'] == f'{OPTION_QUALITY_TRACE} 2'
- assert cs['UNBALANCED'] == OPTION_UNBALANCED_STOP
- assert cs['PATTERN'] == '1'
- assert cs['DEMAND MODEL'] == OPTION_DEMAND_MODEL_DDA
- assert cs['DEMAND MULTIPLIER'] == '1.0'
- assert cs['EMITTER EXPONENT'] == '0.5'
- assert cs['VISCOSITY'] == '1.0'
- assert cs['DIFFUSIVITY'] == '1.0'
- assert cs['SPECIFIC GRAVITY'] == '1.0'
- assert cs['TRIALS'] == '40'
- assert cs['ACCURACY'] == '0.001'
- assert cs['FLOWCHANGE'] == '0.0'
- assert cs['MINIMUM PRESSURE'] == '0.0'
- assert cs['REQUIRED PRESSURE'] == '0.1'
- assert cs['PRESSURE EXPONENT'] == '0.5'
- assert cs['TOLERANCE'] == '0.01'
- assert cs['HTOL'] == '0.0005'
- assert cs['QTOL'] == '0.0001'
-
- o3 = get_option_v3(p)
-
- o3['DEMAND_MODEL'] = OPTION_V3_DEMAND_MODEL_CONSTRAINED
-
- css = set_option_v3(p, ChangeSet(o3)).operations
- cs = css[0]
- assert cs['DEMAND_MODEL'] == OPTION_V3_DEMAND_MODEL_CONSTRAINED
- cs = css[1]
- assert cs['DEMAND MODEL'] == OPTION_DEMAND_MODEL_PDA
-
- css = execute_undo(p).operations
- cs = css[0]
- assert cs['DEMAND MODEL'] == OPTION_DEMAND_MODEL_DDA
- cs = css[1]
- assert cs['DEMAND_MODEL'] == OPTION_V3_DEMAND_MODEL_FIXED
-
- css = execute_redo(p).operations
- cs = css[0]
- assert cs['DEMAND_MODEL'] == OPTION_V3_DEMAND_MODEL_CONSTRAINED
- cs = css[1]
- assert cs['DEMAND MODEL'] == OPTION_DEMAND_MODEL_PDA
-
- o3 = get_option_v3(p)
-
- o3['DEMAND_MODEL'] = OPTION_V3_DEMAND_MODEL_LOGISTIC
-
- css = set_option_v3(p, ChangeSet(o3)).operations
- cs = css[0]
- assert cs['DEMAND_MODEL'] == OPTION_V3_DEMAND_MODEL_LOGISTIC
- cs = css[1]
- assert cs['DEMAND MODEL'] == OPTION_DEMAND_MODEL_PDA
-
- css = execute_undo(p).operations
- cs = css[0]
- assert cs['DEMAND MODEL'] == OPTION_DEMAND_MODEL_PDA
- cs = css[1]
- assert cs['DEMAND_MODEL'] == OPTION_V3_DEMAND_MODEL_CONSTRAINED
-
- css = execute_redo(p).operations
- cs = css[0]
- assert cs['DEMAND_MODEL'] == OPTION_V3_DEMAND_MODEL_LOGISTIC
- cs = css[1]
- assert cs['DEMAND MODEL'] == OPTION_DEMAND_MODEL_PDA
-
- self.leave(p)
-
-
- # 25 vertex
-
-
- def test_vertex(self):
- p = 'test_vertex'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_pipe(p, ChangeSet({'id': 'p0', 'node1': 'j1', 'node2': 'j2', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
-
- v = get_vertex(p, 'p0')
- assert v['link'] == 'p0'
- assert v['coords'] == []
-
- set_vertex(p, ChangeSet({'link' : 'p0', 'coords': [{'x': 1.0, 'y': 2.0}, {'x': 2.0, 'y': 1.0}]}))
-
- v = get_vertex(p, 'p0')
- xys = v['coords']
- assert len(xys) == 2
- assert xys[0]['x'] == 1.0
- assert xys[0]['y'] == 2.0
- assert xys[1]['x'] == 2.0
- assert xys[1]['y'] == 1.0
-
- set_vertex(p, ChangeSet({'link' : 'p0', 'coords': []}))
-
- v = get_vertex(p, 'p0')
- assert v['link'] == 'p0'
- assert v['coords'] == []
-
- self.leave(p)
-
-
- def test_vertex_op(self):
- p = 'test_vertex_op'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_pipe(p, ChangeSet({'id': 'p0', 'node1': 'j1', 'node2': 'j2', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
-
- cs = set_vertex(p, ChangeSet({'link' : 'p0', 'coords': [{'x': 1.0, 'y': 2.0}, {'x': 2.0, 'y': 1.0}]})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'vertex'
- assert cs['link'] == 'p0'
- xys = cs['coords']
- assert len(xys) == 2
- assert xys[0]['x'] == 1.0
- assert xys[0]['y'] == 2.0
- assert xys[1]['x'] == 2.0
- assert xys[1]['y'] == 1.0
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'vertex'
- assert cs['link'] == 'p0'
- assert cs['coords'] == []
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'vertex'
- assert cs['link'] == 'p0'
- xys = cs['coords']
- assert len(xys) == 2
- assert xys[0]['x'] == 1.0
- assert xys[0]['y'] == 2.0
- assert xys[1]['x'] == 2.0
- assert xys[1]['y'] == 1.0
-
- self.leave(p)
-
-
- # 26 label
-
-
- def test_label(self):
- p = 'test_label'
- self.enter(p)
-
- l = get_label(p, 0.0, 0.0)
- assert l['x'] == 0.0
- assert l['y'] == 0.0
- assert l['label'] == None
- assert l['node'] == None
-
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_label(p, ChangeSet({'x': 0.0, 'y': 0.0, 'label': 'x', 'node': 'j0'}))
- l = get_label(p, 0.0, 0.0)
- assert l['x'] == 0.0
- assert l['y'] == 0.0
- assert l['label'] == 'x'
- assert l['node'] == 'j0'
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 1.0, 'y': 20.0, 'elevation': 20.0}))
- set_label(p, ChangeSet({'x': 0.0, 'y': 0.0, 'label': 'xxx', 'node': 'j1'}))
- l = get_label(p, 0.0, 0.0)
- assert l['x'] == 0.0
- assert l['y'] == 0.0
- assert l['label'] == 'xxx'
- assert l['node'] == 'j1'
-
- delete_label(p, ChangeSet({'x': 0.0, 'y': 0.0}))
- l = get_label(p, 0.0, 0.0)
- assert l['x'] == 0.0
- assert l['y'] == 0.0
- assert l['label'] == None
- assert l['node'] == None
-
- self.leave(p)
-
-
- def test_label_op(self):
- p = 'test_label_op'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j1', 'x': 1.0, 'y': 20.0, 'elevation': 20.0}))
-
- cs = add_label(p, ChangeSet({'x': 0.0, 'y': 0.0, 'label': 'x', 'node': 'j0'})).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'label'
- assert cs['x'] == 0.0
- assert cs['y'] == 0.0
- assert cs['label'] == 'x'
- assert cs['node'] == 'j0'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'label'
- assert cs['x'] == 0.0
- assert cs['y'] == 0.0
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'label'
- assert cs['x'] == 0.0
- assert cs['y'] == 0.0
- assert cs['label'] == 'x'
- assert cs['node'] == 'j0'
-
- cs = set_label(p, ChangeSet({'x': 0.0, 'y': 0.0, 'label': 'xxx', 'node': 'j1'})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'label'
- assert cs['x'] == 0.0
- assert cs['y'] == 0.0
- assert cs['label'] == 'xxx'
- assert cs['node'] == 'j1'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'label'
- assert cs['x'] == 0.0
- assert cs['y'] == 0.0
- assert cs['label'] == 'x'
- assert cs['node'] == 'j0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'label'
- assert cs['x'] == 0.0
- assert cs['y'] == 0.0
- assert cs['label'] == 'xxx'
- assert cs['node'] == 'j1'
-
- cs = delete_label(p, ChangeSet({'x': 0.0, 'y': 0.0})).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'label'
- assert cs['x'] == 0.0
- assert cs['y'] == 0.0
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'label'
- assert cs['x'] == 0.0
- assert cs['y'] == 0.0
- assert cs['label'] == 'xxx'
- assert cs['node'] == 'j1'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'label'
- assert cs['x'] == 0.0
- assert cs['y'] == 0.0
-
- self.leave(p)
-
-
- # 27 backdrop
-
-
- def test_backdrop(self):
- p = 'test_backdrop'
- self.enter(p)
-
- assert get_backdrop(p)['content'] == ''
-
- set_backdrop(p, ChangeSet({'content': 'x'}))
- assert get_backdrop(p)['content'] == 'x'
-
- self.leave(p)
-
-
- def test_backdrop_op(self):
- p = 'test_backdrop_op'
- self.enter(p)
-
- cs = set_backdrop(p, ChangeSet({'content': 'x'})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'backdrop'
- assert cs['content'] == 'x'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'backdrop'
- assert cs['content'] == ''
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'backdrop'
- assert cs['content'] == 'x'
-
- self.leave(p)
-
-
- # 28 end
-
-
- # 29 scada_device
-
-
- def test_scada_device(self):
- p = 'test_scada_device'
- self.enter(p)
-
- assert get_all_scada_device_ids(p) == []
-
- d = get_scada_device(p, 'd0')
- assert d == {}
-
- add_scada_device(p, ChangeSet({'id': 'd0'}))
- d = get_scada_device(p, 'd0')
- assert d['id'] == 'd0'
- assert d['name'] == None
- assert d['address'] == None
- assert d['sd_type'] == None
-
- set_scada_device(p, ChangeSet({'id': 'd0', 'name': 'device0', 'address': 'x', 'sd_type': SCADA_DEVICE_TYPE_FLOW}))
- d = get_scada_device(p, 'd0')
- assert d['id'] == 'd0'
- assert d['name'] == 'device0'
- assert d['address'] == 'x'
- assert d['sd_type'] == SCADA_DEVICE_TYPE_FLOW
-
- add_scada_device(p, ChangeSet({'id': 'd1', 'name': 'device1', 'address': 'x', 'sd_type': SCADA_DEVICE_TYPE_PRESSURE}))
- d = get_scada_device(p, 'd1')
- assert d['id'] == 'd1'
- assert d['name'] == 'device1'
- assert d['address'] == 'x'
- assert d['sd_type'] == SCADA_DEVICE_TYPE_PRESSURE
-
- devices = get_all_scada_device_ids(p)
- assert len(devices) == 2
- assert devices[0] == 'd0'
- assert devices[1] == 'd1'
-
- delete_scada_device(p, ChangeSet({'id': 'd0'}))
- d = get_scada_device(p, 'd0')
- assert d == {}
-
- delete_scada_device(p, ChangeSet({'id': 'd1'}))
- d = get_scada_device(p, 'd1')
- assert d == {}
-
- assert get_all_scada_device_ids(p) == []
-
- add_scada_device(p, ChangeSet({'id': 'd0'}))
- d = get_scada_device(p, 'd0')
- assert d['id'] == 'd0'
-
- add_scada_device(p, ChangeSet({'id': 'd1'}))
- d = get_scada_device(p, 'd1')
- assert d['id'] == 'd1'
-
- devices = get_all_scada_device_ids(p)
- assert len(devices) == 2
- assert devices[0] == 'd0'
- assert devices[1] == 'd1'
-
- clean_scada_device(p)
- d = get_scada_device(p, 'd0')
- assert d == {}
- d = get_scada_device(p, 'd1')
- assert d == {}
-
- assert get_all_scada_device_ids(p) == []
-
- self.leave(p)
-
-
- def test_scada_device_op(self):
- p = 'test_scada_device_op'
-
- self.enter(p)
-
- cs = add_scada_device(p, ChangeSet({'id': 'd0'})).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd0'
- assert cs['name'] == None
- assert cs['address'] == None
- assert cs['sd_type'] == None
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd0'
- assert cs['name'] == None
- assert cs['address'] == None
- assert cs['sd_type'] == None
-
- cs = set_scada_device(p, ChangeSet({'id': 'd0', 'name': 'device0', 'address': 'x', 'sd_type': SCADA_DEVICE_TYPE_FLOW})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd0'
- assert cs['name'] == 'device0'
- assert cs['address'] == 'x'
- assert cs['sd_type'] == SCADA_DEVICE_TYPE_FLOW
-
- cs = add_scada_device(p, ChangeSet({'id': 'd1', 'name': 'device1', 'address': 'x', 'sd_type': SCADA_DEVICE_TYPE_PRESSURE})).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd1'
- assert cs['name'] == 'device1'
- assert cs['address'] == 'x'
- assert cs['sd_type'] == SCADA_DEVICE_TYPE_PRESSURE
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd1'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd1'
- assert cs['name'] == 'device1'
- assert cs['address'] == 'x'
- assert cs['sd_type'] == SCADA_DEVICE_TYPE_PRESSURE
-
- cs = delete_scada_device(p, ChangeSet({'id': 'd0'})).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd0'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd0'
- assert cs['name'] == 'device0'
- assert cs['address'] == 'x'
- assert cs['sd_type'] == SCADA_DEVICE_TYPE_FLOW
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd0'
-
- cs = delete_scada_device(p, ChangeSet({'id': 'd1'})).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd1'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd1'
- assert cs['name'] == 'device1'
- assert cs['address'] == 'x'
- assert cs['sd_type'] == SCADA_DEVICE_TYPE_PRESSURE
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd1'
-
- cs = add_scada_device(p, ChangeSet({'id': 'd0'})).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd0'
- assert cs['name'] == None
- assert cs['address'] == None
- assert cs['sd_type'] == None
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd0'
- assert cs['name'] == None
- assert cs['address'] == None
- assert cs['sd_type'] == None
-
- cs = add_scada_device(p, ChangeSet({'id': 'd1'})).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd1'
- assert cs['name'] == None
- assert cs['address'] == None
- assert cs['sd_type'] == None
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd1'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd1'
- assert cs['name'] == None
- assert cs['address'] == None
- assert cs['sd_type'] == None
-
- css = clean_scada_device(p).operations
- assert len(css) == 2
- cs = css[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd0'
- cs = css[1]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd1'
-
- css = execute_undo(p).operations
- assert len(css) == 2
- cs = css[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd1'
- assert cs['name'] == None
- assert cs['address'] == None
- assert cs['sd_type'] == None
- cs = css[1]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd0'
- assert cs['name'] == None
- assert cs['address'] == None
- assert cs['sd_type'] == None
-
- css = execute_redo(p).operations
- assert len(css) == 2
- cs = css[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd0'
- cs = css[1]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_device'
- assert cs['id'] == 'd1'
-
- self.leave(p)
-
-
- # 30 scada_device_data
-
-
- def test_scada_device_data(self):
- p = 'test_scada_device_data'
- self.enter(p)
-
- add_scada_device(p, ChangeSet({'id': 'sd'}))
-
- sa = get_scada_device_data(p, 'sd')
- assert sa['device_id'] == 'sd'
- assert sa['data'] == []
-
- set_scada_device_data(p, ChangeSet({'device_id': 'sd', 'data': [{ 'time': '2023-02-10 00:02:22', 'value': 100.0 }]}))
- sa = get_scada_device_data(p, 'sd')
- assert sa['device_id'] == 'sd'
- assert len(sa['data']) == 1
- assert sa['data'][0]['time'] == '2023-02-10 00:02:22'
- assert sa['data'][0]['value'] == 100.0
-
- set_scada_device_data(p, ChangeSet({'device_id': 'sd', 'data': [{ 'time': '2023-02-10 00:02:22', 'value': 100.0 }, { 'time': '2023-02-10 00:03:22', 'value': 200.0 }]}))
- sa = get_scada_device_data(p, 'sd')
- assert sa['device_id'] == 'sd'
- assert len(sa['data']) == 2
- assert sa['data'][0]['time'] == '2023-02-10 00:02:22'
- assert sa['data'][0]['value'] == 100.0
- assert sa['data'][1]['time'] == '2023-02-10 00:03:22'
- assert sa['data'][1]['value'] == 200.0
-
- add_scada_device_data(p, ChangeSet({'device_id': 'sd', 'time': '2023-02-10 00:02:22', 'value': 100.0}))
- sa = get_scada_device_data(p, 'sd')
- assert sa['device_id'] == 'sd'
- assert len(sa['data']) == 2
- assert sa['data'][0]['time'] == '2023-02-10 00:02:22'
- assert sa['data'][0]['value'] == 100.0
- assert sa['data'][1]['time'] == '2023-02-10 00:03:22'
- assert sa['data'][1]['value'] == 200.0
-
- add_scada_device_data(p, ChangeSet({'device_id': 'sd', 'time': '2023-02-11 00:02:22', 'value': 100.0}))
- sa = get_scada_device_data(p, 'sd')
- assert sa['device_id'] == 'sd'
- assert len(sa['data']) == 3
- assert sa['data'][0]['time'] == '2023-02-10 00:02:22'
- assert sa['data'][0]['value'] == 100.0
- assert sa['data'][1]['time'] == '2023-02-10 00:03:22'
- assert sa['data'][1]['value'] == 200.0
- assert sa['data'][2]['time'] == '2023-02-11 00:02:22'
- assert sa['data'][2]['value'] == 100.0
-
- delete_scada_device_data(p, ChangeSet({'device_id': 'sd', 'time': '2023-02-12 00:02:22'}))
- sa = get_scada_device_data(p, 'sd')
- assert sa['device_id'] == 'sd'
- assert len(sa['data']) == 3
- assert sa['data'][0]['time'] == '2023-02-10 00:02:22'
- assert sa['data'][0]['value'] == 100.0
- assert sa['data'][1]['time'] == '2023-02-10 00:03:22'
- assert sa['data'][1]['value'] == 200.0
- assert sa['data'][2]['time'] == '2023-02-11 00:02:22'
- assert sa['data'][2]['value'] == 100.0
-
- delete_scada_device_data(p, ChangeSet({'device_id': 'sd', 'time': '2023-02-11 00:02:22'}))
- sa = get_scada_device_data(p, 'sd')
- assert sa['device_id'] == 'sd'
- assert len(sa['data']) == 2
- assert sa['data'][0]['time'] == '2023-02-10 00:02:22'
- assert sa['data'][0]['value'] == 100.0
- assert sa['data'][1]['time'] == '2023-02-10 00:03:22'
- assert sa['data'][1]['value'] == 200.0
-
- set_scada_device_data(p, ChangeSet({'device_id': 'sd', 'data': []}))
- sa = get_scada_device_data(p, 'sd')
- assert sa['device_id'] == 'sd'
- assert sa['data'] == []
-
- add_scada_device(p, ChangeSet({'id': 'sd0'}))
- add_scada_device_data(p, ChangeSet({'device_id': 'sd0', 'time': '2023-02-10 00:02:22', 'value': 100.0}))
- add_scada_device_data(p, ChangeSet({'device_id': 'sd0', 'time': '2023-02-11 00:02:22', 'value': 200.0}))
- add_scada_device_data(p, ChangeSet({'device_id': 'sd0', 'time': '2023-02-12 00:02:22', 'value': 300.0}))
-
- add_scada_device(p, ChangeSet({'id': 'sd1'}))
- set_scada_device_data(p, ChangeSet({'device_id': 'sd1', 'data': [{'time': '2023-02-10 00:02:22', 'value': 100.0}, {'time': '2023-02-11 00:02:22', 'value': 200.0}, {'time': '2023-02-12 00:02:22', 'value': 300.0}]}))
-
- clean_scada_device_data(p)
- sa = get_scada_device_data(p, 'sd0')
- assert sa['device_id'] == 'sd0'
- assert sa['data'] == []
- sa = get_scada_device_data(p, 'sd1')
- assert sa['device_id'] == 'sd1'
- assert sa['data'] == []
-
- self.leave(p)
-
-
- def test_scada_device_data_op(self):
- p = 'test_scada_device_data_op'
- self.enter(p)
-
- add_scada_device(p, ChangeSet({'id': 'sd'}))
-
- cs = set_scada_device_data(p, ChangeSet({'device_id': 'sd', 'data': [{ 'time': '2023-02-10 00:02:22', 'value': 100.0 }]})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd'
- assert len(cs['data']) == 1
- assert cs['data'][0]['time'] == '2023-02-10 00:02:22'
- assert cs['data'][0]['value'] == 100.0
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd'
- assert cs['data'] == []
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd'
- assert len(cs['data']) == 1
- assert cs['data'][0]['time'] == '2023-02-10 00:02:22'
- assert cs['data'][0]['value'] == 100.0
-
- cs = set_scada_device_data(p, ChangeSet({'device_id': 'sd', 'data': [{ 'time': '2023-02-10 00:02:22', 'value': 100.0 }, { 'time': '2023-02-10 00:03:22', 'value': 200.0 }]})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd'
- assert len(cs['data']) == 2
- assert cs['data'][0]['time'] == '2023-02-10 00:02:22'
- assert cs['data'][0]['value'] == 100.0
- assert cs['data'][1]['time'] == '2023-02-10 00:03:22'
- assert cs['data'][1]['value'] == 200.0
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd'
- assert len(cs['data']) == 1
- assert cs['data'][0]['time'] == '2023-02-10 00:02:22'
- assert cs['data'][0]['value'] == 100.0
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd'
- assert len(cs['data']) == 2
- assert cs['data'][0]['time'] == '2023-02-10 00:02:22'
- assert cs['data'][0]['value'] == 100.0
- assert cs['data'][1]['time'] == '2023-02-10 00:03:22'
- assert cs['data'][1]['value'] == 200.0
-
- cs = add_scada_device_data(p, ChangeSet({'device_id': 'sd', 'time': '2023-02-10 00:02:22', 'value': 100.0}))
- assert len(cs.operations) == 0
-
- cs = add_scada_device_data(p, ChangeSet({'device_id': 'sd', 'time': '2023-02-11 00:02:22', 'value': 100.0})).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd'
- assert cs['time'] == '2023-02-11 00:02:22'
- assert cs['value'] == 100.0
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd'
- assert cs['time'] == '2023-02-11 00:02:22'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd'
- assert cs['time'] == '2023-02-11 00:02:22'
- assert cs['value'] == 100.0
-
- cs = delete_scada_device_data(p, ChangeSet({'device_id': 'sd', 'time': '2023-02-12 00:02:22'}))
- assert len(cs.operations) == 0
-
- cs = delete_scada_device_data(p, ChangeSet({'device_id': 'sd', 'time': '2023-02-11 00:02:22'})).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd'
- assert cs['time'] == '2023-02-11 00:02:22'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd'
- assert cs['time'] == '2023-02-11 00:02:22'
- assert cs['value'] == 100.0
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd'
- assert cs['time'] == '2023-02-11 00:02:22'
-
- cs = set_scada_device_data(p, ChangeSet({'device_id': 'sd', 'data': []})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd'
- assert cs['data'] == []
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd'
- assert len(cs['data']) == 2
- assert cs['data'][0]['time'] == '2023-02-10 00:02:22'
- assert cs['data'][0]['value'] == 100.0
- assert cs['data'][1]['time'] == '2023-02-10 00:03:22'
- assert cs['data'][1]['value'] == 200.0
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd'
- assert cs['data'] == []
-
- add_scada_device(p, ChangeSet({'id': 'sd0'}))
- add_scada_device_data(p, ChangeSet({'device_id': 'sd0', 'time': '2023-02-10 00:02:22', 'value': 100.0}))
- add_scada_device_data(p, ChangeSet({'device_id': 'sd0', 'time': '2023-02-11 00:02:22', 'value': 200.0}))
- add_scada_device_data(p, ChangeSet({'device_id': 'sd0', 'time': '2023-02-12 00:02:22', 'value': 300.0}))
-
- add_scada_device(p, ChangeSet({'id': 'sd1'}))
- set_scada_device_data(p, ChangeSet({'device_id': 'sd1', 'data': [{'time': '2023-02-10 00:02:22', 'value': 100.0}, {'time': '2023-02-11 00:02:22', 'value': 200.0}, {'time': '2023-02-12 00:02:22', 'value': 300.0}]}))
-
- css = clean_scada_device_data(p).operations
- cs = css[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd0'
- assert cs['data'] == []
- cs = css[1]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd1'
- assert cs['data'] == []
-
- css = execute_undo(p).operations
- cs = css[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd1'
- assert cs['data'][0] == {'time': '2023-02-10 00:02:22', 'value': 100.0}
- assert cs['data'][1] == {'time': '2023-02-11 00:02:22', 'value': 200.0}
- assert cs['data'][2] == {'time': '2023-02-12 00:02:22', 'value': 300.0}
- cs = css[1]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd0'
- assert cs['data'][0] == {'time': '2023-02-10 00:02:22', 'value': 100.0}
- assert cs['data'][1] == {'time': '2023-02-11 00:02:22', 'value': 200.0}
- assert cs['data'][2] == {'time': '2023-02-12 00:02:22', 'value': 300.0}
-
- css = execute_redo(p).operations
- cs = css[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd0'
- assert cs['data'] == []
- cs = css[1]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_device_data'
- assert cs['device_id'] == 'sd1'
- assert cs['data'] == []
-
- self.leave(p)
-
-
- # 31 scada_element
-
-
- def test_scada_element(self):
- p = 'test_scada_element'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_pipe(p, ChangeSet({'id': 'p1', 'node1': 'j1', 'node2': 'j2', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
- add_scada_device(p, ChangeSet({'id': 'sd0', 'name': 'device0', 'address': 'x0', 'sd_type': SCADA_DEVICE_TYPE_PRESSURE}))
- add_scada_device(p, ChangeSet({'id': 'sd1', 'name': 'device1', 'address': 'x1', 'sd_type': SCADA_DEVICE_TYPE_FLOW}))
-
- assert get_all_scada_element_ids(p) == []
-
- add_scada_element(p, ChangeSet({'id': 'sm0', 'x': 0.0, 'y': 1.0, 'device_id': 'sd0', 'model_id': 'j0', 'model_type': SCADA_MODEL_TYPE_JUNCTION, 'status': SCADA_ELEMENT_STATUS_OFFLINE}))
- sm = get_scada_element(p, 'sm0')
- assert sm == {}
- add_scada_element(p, ChangeSet({'id': 'sm0', 'x': 0.0, 'y': 1.0, 'device_id': 'sd0', 'model_id': 'j0', 'status': SCADA_ELEMENT_STATUS_OFFLINE}))
- sm = get_scada_element(p, 'sm0')
- assert sm == {}
- add_scada_element(p, ChangeSet({'id': 'sm0', 'x': 0.0, 'y': 1.0, 'device_id': 'sd0', 'model_type': SCADA_MODEL_TYPE_JUNCTION, 'status': SCADA_ELEMENT_STATUS_OFFLINE}))
- sm = get_scada_element(p, 'sm0')
- assert sm == {}
- add_scada_element(p, ChangeSet({'id': 'sm0', 'x': 0.0, 'y': 1.0, 'device_id': 'sd0', 'model_type': SCADA_MODEL_TYPE_PIPE, 'status': SCADA_ELEMENT_STATUS_OFFLINE}))
- sm = get_scada_element(p, 'sm0')
- assert sm == {}
-
- add_scada_element(p, ChangeSet({'id': 'sm0', 'x': 0.0, 'y': 1.0, 'device_id': 'sd0', 'model_id': 'j1', 'model_type': SCADA_MODEL_TYPE_JUNCTION, 'status': SCADA_ELEMENT_STATUS_OFFLINE}))
- sm = get_scada_element(p, 'sm0')
- assert sm['id'] == 'sm0'
- assert sm['x'] == 0.0
- assert sm['y'] == 1.0
- assert sm['device_id'] == 'sd0'
- assert sm['model_id'] == 'j1'
- assert sm['model_type'] == SCADA_MODEL_TYPE_JUNCTION
- assert sm['status'] == SCADA_ELEMENT_STATUS_OFFLINE
-
- add_scada_element(p, ChangeSet({'id': 'sm1', 'x': 1.0, 'y': 2.0, 'device_id': 'sd1', 'model_id': 'p0', 'model_type': SCADA_MODEL_TYPE_PIPE, 'status': SCADA_ELEMENT_STATUS_ONLINE}))
- sm = get_scada_element(p, 'sm1')
- assert sm == {}
- add_scada_element(p, ChangeSet({'id': 'sm1', 'x': 1.0, 'y': 2.0, 'device_id': 'sd1', 'model_id': 'p0', 'status': SCADA_ELEMENT_STATUS_ONLINE}))
- sm = get_scada_element(p, 'sm1')
- assert sm == {}
- add_scada_element(p, ChangeSet({'id': 'sm1', 'x': 1.0, 'y': 2.0, 'device_id': 'sd1', 'model_type': SCADA_MODEL_TYPE_PIPE, 'status': SCADA_ELEMENT_STATUS_ONLINE}))
- sm = get_scada_element(p, 'sm1')
- assert sm == {}
- add_scada_element(p, ChangeSet({'id': 'sm1', 'x': 1.0, 'y': 2.0, 'device_id': 'sd1', 'model_id': 'p0', 'model_type': SCADA_MODEL_TYPE_JUNCTION, 'status': SCADA_ELEMENT_STATUS_ONLINE}))
- sm = get_scada_element(p, 'sm1')
- assert sm == {}
-
- add_scada_element(p, ChangeSet({'id': 'sm1', 'x': 1.0, 'y': 2.0, 'device_id': 'sd1', 'model_id': 'p1', 'model_type': SCADA_MODEL_TYPE_PIPE, 'status': SCADA_ELEMENT_STATUS_ONLINE}))
- sm = get_scada_element(p, 'sm1')
- assert sm['id'] == 'sm1'
- assert sm['x'] == 1.0
- assert sm['y'] == 2.0
- assert sm['device_id'] == 'sd1'
- assert sm['model_id'] == 'p1'
- assert sm['model_type'] == SCADA_MODEL_TYPE_PIPE
- assert sm['status'] == SCADA_ELEMENT_STATUS_ONLINE
-
- elements = get_all_scada_element_ids(p)
- assert len(elements) == 2
- assert elements[0] == 'sm0'
- assert elements[1] == 'sm1'
-
- set_scada_element(p, ChangeSet({'id': 'sm0', 'x': 1.0, 'y': 2.0, 'device_id': 'sd1', 'model_id': 'p1', 'model_type': SCADA_MODEL_TYPE_PIPE, 'status': SCADA_ELEMENT_STATUS_ONLINE}))
- sm = get_scada_element(p, 'sm0')
- assert sm['id'] == 'sm0'
- assert sm['x'] == 1.0
- assert sm['y'] == 2.0
- assert sm['device_id'] == 'sd1'
- assert sm['model_id'] == 'p1'
- assert sm['model_type'] == SCADA_MODEL_TYPE_PIPE
- assert sm['status'] == SCADA_ELEMENT_STATUS_ONLINE
-
- set_scada_element(p, ChangeSet({'id': 'sm0', 'x': 1.0, 'y': 2.0, 'device_id': 'sd1', 'model_id': 'p1', 'model_type': SCADA_MODEL_TYPE_JUNCTION, 'status': SCADA_ELEMENT_STATUS_ONLINE}))
- sm = get_scada_element(p, 'sm0')
- assert sm['id'] == 'sm0'
- assert sm['x'] == 1.0
- assert sm['y'] == 2.0
- assert sm['device_id'] == 'sd1'
- assert sm['model_id'] == 'p1'
- assert sm['model_type'] == SCADA_MODEL_TYPE_PIPE
- assert sm['status'] == SCADA_ELEMENT_STATUS_ONLINE
-
- set_scada_element(p, ChangeSet({'id': 'sm1', 'x': 0.0, 'y': 1.0, 'device_id': 'sd0', 'model_id': 'j1', 'model_type': SCADA_MODEL_TYPE_JUNCTION, 'status': SCADA_ELEMENT_STATUS_OFFLINE}))
- sm = get_scada_element(p, 'sm1')
- assert sm['id'] == 'sm1'
- assert sm['x'] == 0.0
- assert sm['y'] == 1.0
- assert sm['device_id'] == 'sd0'
- assert sm['model_id'] == 'j1'
- assert sm['model_type'] == SCADA_MODEL_TYPE_JUNCTION
- assert sm['status'] == SCADA_ELEMENT_STATUS_OFFLINE
-
- set_scada_element(p, ChangeSet({'id': 'sm1', 'x': 0.0, 'y': 1.0, 'device_id': 'sd0', 'model_id': 'j1', 'model_type': SCADA_MODEL_TYPE_PIPE, 'status': SCADA_ELEMENT_STATUS_OFFLINE}))
- sm = get_scada_element(p, 'sm1')
- assert sm['id'] == 'sm1'
- assert sm['x'] == 0.0
- assert sm['y'] == 1.0
- assert sm['device_id'] == 'sd0'
- assert sm['model_id'] == 'j1'
- assert sm['model_type'] == SCADA_MODEL_TYPE_JUNCTION
- assert sm['status'] == SCADA_ELEMENT_STATUS_OFFLINE
-
- delete_scada_element(p, ChangeSet({'id': 'sm0'}))
- sm = get_scada_element(p, 'sm0')
- assert sm == {}
-
- delete_scada_element(p, ChangeSet({'id': 'sm1'}))
- sm = get_scada_element(p, 'sm1')
- assert sm == {}
-
- assert get_all_scada_element_ids(p) == []
-
- add_scada_element(p, ChangeSet({'id': 'sm0', 'x': 0.0, 'y': 1.0, 'device_id': 'sd0', 'model_id': 'j1', 'model_type': SCADA_MODEL_TYPE_JUNCTION, 'status': SCADA_ELEMENT_STATUS_OFFLINE}))
- sm = get_scada_element(p, 'sm0')
- assert sm['id'] == 'sm0'
- assert sm['x'] == 0.0
- assert sm['y'] == 1.0
- assert sm['device_id'] == 'sd0'
- assert sm['model_id'] == 'j1'
- assert sm['model_type'] == SCADA_MODEL_TYPE_JUNCTION
- assert sm['status'] == SCADA_ELEMENT_STATUS_OFFLINE
-
- add_scada_element(p, ChangeSet({'id': 'sm1', 'x': 1.0, 'y': 2.0, 'device_id': 'sd1', 'model_id': 'p1', 'model_type': SCADA_MODEL_TYPE_PIPE, 'status': SCADA_ELEMENT_STATUS_ONLINE}))
- sm = get_scada_element(p, 'sm1')
- assert sm['id'] == 'sm1'
- assert sm['x'] == 1.0
- assert sm['y'] == 2.0
- assert sm['device_id'] == 'sd1'
- assert sm['model_id'] == 'p1'
- assert sm['model_type'] == SCADA_MODEL_TYPE_PIPE
- assert sm['status'] == SCADA_ELEMENT_STATUS_ONLINE
-
- elements = get_all_scada_element_ids(p)
- assert len(elements) == 2
- assert elements[0] == 'sm0'
- assert elements[1] == 'sm1'
-
- clean_scada_element(p)
- sm = get_scada_element(p, 'sm0')
- assert sm == {}
- sm = get_scada_element(p, 'sm1')
- assert sm == {}
-
- assert get_all_scada_element_ids(p) == []
-
- self.leave(p)
-
-
- def test_scada_element_op(self):
- p = 'test_scada_element_op'
- self.enter(p)
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_junction(p, ChangeSet({'id': 'j2', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- add_pipe(p, ChangeSet({'id': 'p1', 'node1': 'j1', 'node2': 'j2', 'length': 100.0, 'diameter': 10.0, 'roughness': 0.1, 'minor_loss': 0.5, 'status': PIPE_STATUS_OPEN }))
- add_scada_device(p, ChangeSet({'id': 'sd0', 'name': 'device0', 'address': 'x0', 'sd_type': SCADA_DEVICE_TYPE_PRESSURE}))
- add_scada_device(p, ChangeSet({'id': 'sd1', 'name': 'device1', 'address': 'x1', 'sd_type': SCADA_DEVICE_TYPE_FLOW}))
-
- css = add_scada_element(p, ChangeSet({'id': 'sm0', 'x': 0.0, 'y': 1.0, 'device_id': 'sd0', 'model_id': 'j0', 'model_type': SCADA_MODEL_TYPE_JUNCTION, 'status': SCADA_ELEMENT_STATUS_OFFLINE})).operations
- assert len(css) == 0
- cs = add_scada_element(p, ChangeSet({'id': 'sm0', 'x': 0.0, 'y': 1.0, 'device_id': 'sd0', 'model_id': 'j1', 'model_type': SCADA_MODEL_TYPE_JUNCTION, 'status': SCADA_ELEMENT_STATUS_OFFLINE})).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm0'
- assert cs['x'] == 0.0
- assert cs['y'] == 1.0
- assert cs['device_id'] == 'sd0'
- assert cs['model_id'] == 'j1'
- assert cs['model_type'] == SCADA_MODEL_TYPE_JUNCTION
- assert cs['status'] == SCADA_ELEMENT_STATUS_OFFLINE
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm0'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm0'
- assert cs['x'] == 0.0
- assert cs['y'] == 1.0
- assert cs['device_id'] == 'sd0'
- assert cs['model_id'] == 'j1'
- assert cs['model_type'] == SCADA_MODEL_TYPE_JUNCTION
- assert cs['status'] == SCADA_ELEMENT_STATUS_OFFLINE
-
- css = add_scada_element(p, ChangeSet({'id': 'sm1', 'x': 1.0, 'y': 2.0, 'device_id': 'sd1', 'model_id': 'p0', 'model_type': SCADA_MODEL_TYPE_PIPE, 'status': SCADA_ELEMENT_STATUS_ONLINE})).operations
- assert len(css) == 0
- cs = add_scada_element(p, ChangeSet({'id': 'sm1', 'x': 1.0, 'y': 2.0, 'device_id': 'sd1', 'model_id': 'p1', 'model_type': SCADA_MODEL_TYPE_PIPE, 'status': SCADA_ELEMENT_STATUS_ONLINE})).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm1'
- assert cs['x'] == 1.0
- assert cs['y'] == 2.0
- assert cs['device_id'] == 'sd1'
- assert cs['model_id'] == 'p1'
- assert cs['model_type'] == SCADA_MODEL_TYPE_PIPE
- assert cs['status'] == SCADA_ELEMENT_STATUS_ONLINE
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm1'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm1'
- assert cs['x'] == 1.0
- assert cs['y'] == 2.0
- assert cs['device_id'] == 'sd1'
- assert cs['model_id'] == 'p1'
- assert cs['model_type'] == SCADA_MODEL_TYPE_PIPE
- assert cs['status'] == SCADA_ELEMENT_STATUS_ONLINE
-
- cs = set_scada_element(p, ChangeSet({'id': 'sm0', 'x': 1.0, 'y': 2.0, 'device_id': 'sd1', 'model_id': 'p1', 'model_type': SCADA_MODEL_TYPE_PIPE, 'status': SCADA_ELEMENT_STATUS_ONLINE})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm0'
- assert cs['x'] == 1.0
- assert cs['y'] == 2.0
- assert cs['device_id'] == 'sd1'
- assert cs['model_id'] == 'p1'
- assert cs['model_type'] == SCADA_MODEL_TYPE_PIPE
- assert cs['status'] == SCADA_ELEMENT_STATUS_ONLINE
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm0'
- assert cs['x'] == 0.0
- assert cs['y'] == 1.0
- assert cs['device_id'] == 'sd0'
- assert cs['model_id'] == 'j1'
- assert cs['model_type'] == SCADA_MODEL_TYPE_JUNCTION
- assert cs['status'] == SCADA_ELEMENT_STATUS_OFFLINE
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm0'
- assert cs['x'] == 1.0
- assert cs['y'] == 2.0
- assert cs['device_id'] == 'sd1'
- assert cs['model_id'] == 'p1'
- assert cs['model_type'] == SCADA_MODEL_TYPE_PIPE
- assert cs['status'] == SCADA_ELEMENT_STATUS_ONLINE
-
- cs = set_scada_element(p, ChangeSet({'id': 'sm1', 'x': 0.0, 'y': 1.0, 'device_id': 'sd0', 'model_id': 'j1', 'model_type': SCADA_MODEL_TYPE_JUNCTION, 'status': SCADA_ELEMENT_STATUS_OFFLINE})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm1'
- assert cs['x'] == 0.0
- assert cs['y'] == 1.0
- assert cs['device_id'] == 'sd0'
- assert cs['model_id'] == 'j1'
- assert cs['model_type'] == SCADA_MODEL_TYPE_JUNCTION
- assert cs['status'] == SCADA_ELEMENT_STATUS_OFFLINE
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm1'
- assert cs['x'] == 1.0
- assert cs['y'] == 2.0
- assert cs['device_id'] == 'sd1'
- assert cs['model_id'] == 'p1'
- assert cs['model_type'] == SCADA_MODEL_TYPE_PIPE
- assert cs['status'] == SCADA_ELEMENT_STATUS_ONLINE
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm1'
- assert cs['x'] == 0.0
- assert cs['y'] == 1.0
- assert cs['device_id'] == 'sd0'
- assert cs['model_id'] == 'j1'
- assert cs['model_type'] == SCADA_MODEL_TYPE_JUNCTION
- assert cs['status'] == SCADA_ELEMENT_STATUS_OFFLINE
-
- cs = delete_scada_element(p, ChangeSet({'id': 'sm0'})).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm0'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm0'
- assert cs['x'] == 1.0
- assert cs['y'] == 2.0
- assert cs['device_id'] == 'sd1'
- assert cs['model_id'] == 'p1'
- assert cs['model_type'] == SCADA_MODEL_TYPE_PIPE
- assert cs['status'] == SCADA_ELEMENT_STATUS_ONLINE
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm0'
-
- cs = delete_scada_element(p, ChangeSet({'id': 'sm1'})).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm1'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm1'
- assert cs['x'] == 0.0
- assert cs['y'] == 1.0
- assert cs['device_id'] == 'sd0'
- assert cs['model_id'] == 'j1'
- assert cs['model_type'] == SCADA_MODEL_TYPE_JUNCTION
- assert cs['status'] == SCADA_ELEMENT_STATUS_OFFLINE
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm1'
-
- cs = add_scada_element(p, ChangeSet({'id': 'sm0', 'x': 0.0, 'y': 1.0, 'device_id': 'sd0', 'model_id': 'j1', 'model_type': SCADA_MODEL_TYPE_JUNCTION, 'status': SCADA_ELEMENT_STATUS_OFFLINE})).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm0'
- assert cs['x'] == 0.0
- assert cs['y'] == 1.0
- assert cs['device_id'] == 'sd0'
- assert cs['model_id'] == 'j1'
- assert cs['model_type'] == SCADA_MODEL_TYPE_JUNCTION
- assert cs['status'] == SCADA_ELEMENT_STATUS_OFFLINE
-
- cs = add_scada_element(p, ChangeSet({'id': 'sm1', 'x': 1.0, 'y': 2.0, 'device_id': 'sd1', 'model_id': 'p1', 'model_type': SCADA_MODEL_TYPE_PIPE, 'status': SCADA_ELEMENT_STATUS_ONLINE})).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm1'
- assert cs['x'] == 1.0
- assert cs['y'] == 2.0
- assert cs['device_id'] == 'sd1'
- assert cs['model_id'] == 'p1'
- assert cs['model_type'] == SCADA_MODEL_TYPE_PIPE
- assert cs['status'] == SCADA_ELEMENT_STATUS_ONLINE
-
- css = clean_scada_element(p).operations
- cs = css[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm0'
- cs = css[1]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm1'
-
- css = execute_undo(p).operations
- cs = css[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm1'
- assert cs['x'] == 1.0
- assert cs['y'] == 2.0
- assert cs['device_id'] == 'sd1'
- assert cs['model_id'] == 'p1'
- assert cs['model_type'] == SCADA_MODEL_TYPE_PIPE
- assert cs['status'] == SCADA_ELEMENT_STATUS_ONLINE
- cs = css[1]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm0'
- assert cs['x'] == 0.0
- assert cs['y'] == 1.0
- assert cs['device_id'] == 'sd0'
- assert cs['model_id'] == 'j1'
- assert cs['model_type'] == SCADA_MODEL_TYPE_JUNCTION
- assert cs['status'] == SCADA_ELEMENT_STATUS_OFFLINE
-
- css = execute_redo(p).operations
- cs = css[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm0'
- cs = css[1]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'scada_element'
- assert cs['id'] == 'sm1'
-
- self.leave(p)
-
-
- # 32 region_util
-
-
- def test_get_nodes_in_boundary(self):
- p = 'test_get_nodes_in_boundary'
- read_inp(p, f'./inp/net3.inp', '3')
- open_project(p)
-
- vds = calculate_virtual_district(p, ['107', '139', '267', '211'])['virtual_districts']
- boundary = calculate_boundary(p, vds[0]['nodes'])
- boundary = inflate_boundary(p, boundary)
- nodes = get_nodes_in_boundary(p, boundary)
- assert nodes == ['10', '101', '103', '105', '107', '109', '111', '115', '117', '119', '120', '257', '259', '261', '263', 'Lake']
-
- self.leave(p)
-
-
- def test_get_nodes_in_region(self):
- p = 'test_get_nodes_in_region'
- read_inp(p, f'./inp/net3.inp', '3')
- open_project(p)
-
- vds = calculate_virtual_district(p, ['107', '139', '267', '211'])['virtual_districts']
- boundary = calculate_boundary(p, vds[0]['nodes'])
- boundary = inflate_boundary(p, boundary)
-
- add_region(p, ChangeSet({'id': 'r1', 'boundary': boundary}))
-
- nodes = get_nodes_in_region(p, 'r1')
- assert nodes == ['10', '101', '103', '105', '107', '109', '111', '115', '117', '119', '120', '257', '259', '261', '263', 'Lake']
-
- add_district_metering_area(p, ChangeSet({'id': 'r2', 'boundary': boundary, 'nodes': ['10', '101', '103']}))
- nodes = get_nodes_in_region(p, 'r2')
- assert nodes == ['10', '101', '103']
-
- self.leave(p)
-
-
- def test_get_links_on_boundary(self):
- p = 'get_links_on_boundary'
- read_inp(p, f'./inp/net3.inp', '3')
- open_project(p)
-
- add_region(p, ChangeSet({'id': 'r', 'boundary': [(24.614,13.087), (24.835,11.069), (26.144,10.747), (27.290,11.543), (25.726,12.987), (24.614,13.087)]}))
- links = get_links_on_region_boundary(p, 'r')
- assert links == ['183', '185', '229', '313', '315']
-
- self.leave(p)
-
-
- def test_calculate_convex_hull(self):
- p = 'test_calculate_convex_hull'
- read_inp(p, f'./inp/net3.inp', '3')
- open_project(p)
-
- nodes = ['10', '101', '103', '105', '107', '109', '111', '115', '117', '119', '120', '257', '259', '261', '263', 'Lake']
- ch = calculate_convex_hull(p, nodes)
- assert ch == [(20.21, 17.53), (12.96, 21.31), (8.0, 27.53), (9.0, 27.85), (23.7, 22.76), (20.21, 17.53)]
-
- self.leave(p)
-
-
- def test_calculate_boundary(self):
- p = 'test_calculate_boundary'
- read_inp(p, f'./inp/net3.inp', '3')
- open_project(p)
-
- nodes = ['10', '101', '103', '105', '107', '109', '111', '115', '117', '119', '120', '257', '259', '261', '263', 'Lake']
- b = calculate_boundary(p, nodes)
- assert b == [(23.7, 22.76), (22.08, 23.1), (21.17, 23.32), (20.8, 23.4), (20.32, 21.57), (16.97, 21.28), (13.81, 22.94), (9.0, 27.85), (8.0, 27.53), (9.0, 27.85), (13.81, 22.94), (12.96, 21.31), (17.64, 18.92), (20.21, 17.53), (20.98, 19.18), (21.69, 21.28), (22.08, 23.1), (23.7, 22.76)]
-
- self.leave(p)
-
-
- def test_inflate_boundary(self):
- p = 'test_inflate_boundary'
- read_inp(p, f'./inp/net3.inp', '3')
- open_project(p)
-
- nodes = ['10', '101', '103', '105', '107', '109', '111', '115', '117', '119', '120', '257', '259', '261', '263', 'Lake']
- b = calculate_boundary(p, nodes)
- b = inflate_boundary(p, b)
- assert b == [(20.57, 17.12), (21.44, 18.98), (21.45, 19.01), (22.17, 21.13), (22.18, 21.16), (22.46, 22.5), (24.09, 22.17), (24.29, 23.150000000000002), (22.19, 23.580000000000002), (22.2, 23.59), (21.28, 23.81), (20.71, 23.93), (20.37, 23.72), (19.92, 22.03), (17.06, 21.79), (14.120000000000001, 23.330000000000002), (9.26, 28.3), (8.98, 28.37), (7.37, 27.85), (7.68, 26.900000000000002), (8.85, 27.27), (13.19, 22.84), (12.42, 21.36), (12.55, 20.96), (17.41, 18.47), (20.16, 16.990000000000002), (20.57, 17.12)]
-
- self.leave(p)
-
-
- def test_inflate_region(self):
- p = 'test_inflate_region'
- read_inp(p, f'./inp/net3.inp', '3')
- open_project(p)
-
- vds = calculate_virtual_district(p, ['107', '139', '267', '211'])['virtual_districts']
- boundary = calculate_boundary(p, vds[0]['nodes'])
- add_region(p, ChangeSet({'id': 'r', 'boundary': boundary}))
- b = inflate_region(p, 'r')
- assert b == [(20.57, 17.12), (21.44, 18.98), (21.45, 19.01), (22.17, 21.13), (22.18, 21.16), (22.46, 22.5), (24.09, 22.17), (24.29, 23.150000000000002), (22.19, 23.580000000000002), (22.2, 23.59), (21.28, 23.81), (20.71, 23.93), (20.37, 23.72), (19.92, 22.03), (17.06, 21.79), (14.120000000000001, 23.330000000000002), (9.26, 28.3), (8.98, 28.37), (7.37, 27.85), (7.68, 26.900000000000002), (8.85, 27.27), (13.19, 22.84), (12.42, 21.36), (12.55, 20.96), (17.41, 18.47), (20.16, 16.990000000000002), (20.57, 17.12)]
-
- self.leave(p)
-
-
- # 32 region
-
-
- def test_region(self):
- p = 'test_region'
- self.enter(p)
-
- r = get_region(p, 'r')
- assert r == {}
-
- add_region(p, ChangeSet({'id': 'r', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]}))
- r = get_region(p, 'r')
- assert r == { 'id': 'r', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)] }
-
- set_region(p, ChangeSet({'id': 'r', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 2.0), (0.0, 0.0)]}))
- r = get_region(p, 'r')
- assert r == { 'id': 'r', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 2.0), (0.0, 0.0)] }
-
- delete_region(p, ChangeSet({'id': 'r'}))
- r = get_region(p, 'r')
- assert r == {}
-
- self.leave(p)
-
-
- def test_region_op(self):
- p = 'test_region_op'
- self.enter(p)
-
- cs = add_region(p, ChangeSet({'id': 'r', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]})).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'region'
- assert cs['id'] == 'r'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'region'
- assert cs['id'] == 'r'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'region'
- assert cs['id'] == 'r'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
-
- cs = set_region(p, ChangeSet({'id': 'r', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 2.0), (0.0, 0.0)]})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'region'
- assert cs['id'] == 'r'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 2.0), (0.0, 0.0)]
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'region'
- assert cs['id'] == 'r'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'region'
- assert cs['id'] == 'r'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 2.0), (0.0, 0.0)]
-
- cs = delete_region(p, ChangeSet({'id': 'r'})).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'region'
- assert cs['id'] == 'r'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'region'
- assert cs['id'] == 'r'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 2.0), (0.0, 0.0)]
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'region'
- assert cs['id'] == 'r'
-
- self.leave(p)
-
-
- # 33 district_metering_area
-
-
- def test_calculate_district_metering_area_for_nodes(self):
- p = 'test_calculate_district_metering_area_for_nodes'
- read_inp(p, f'./inp/net3.inp', '3')
- open_project(p)
-
- dmas = calculate_district_metering_area_for_nodes(p, get_nodes(p), 3)
- assert len(dmas) == 3
- assert dmas[0] == ['117', '119', '120', '121', '123', '125', '127', '129', '131', '139', '141', '143', '145', '147', '149', '15', '151', '153', '157', '159', '161', '195', '20', '257', '259', '261', '263', '3', '60', '601', '61', 'River']
- assert dmas[1] == ['1', '163', '164', '166', '167', '169', '171', '173', '177', '179', '181', '183', '184', '185', '187', '189', '191', '193', '199', '201', '203', '204', '205', '207', '265', '267', '269', '271', '273', '275', '35', '40']
- assert dmas[2] == ['10', '101', '103', '105', '107', '109', '111', '113', '115', '197', '2', '206', '208', '209', '211', '213', '215', '217', '219', '225', '229', '231', '237', '239', '241', '243', '247', '249', '251', '253', '255', '50', 'Lake']
-
- self.leave(p)
-
-
- def test_calculate_district_metering_area_for_region(self):
- p = 'test_calculate_district_metering_area_for_region'
- read_inp(p, f'./inp/net3.inp', '3')
- open_project(p)
-
- add_region(p, ChangeSet({'id': 'r', 'boundary': [(-10000.0, -10000.0), (10000.0, -10000.0), (10000.0, 10000.0), (-10000.0, 10000.0), (-10000.0, -10000.0)]}))
-
- nodes = get_nodes_in_region(p, 'r')
- assert len(nodes) == 97
-
- dmas = calculate_district_metering_area_for_region(p, 'r', 3)
- assert dmas[0] == ['15', '20', '60', '601', '61', '117', '119', '120', '121', '123', '125', '127', '129', '131', '139', '141', '143', '145', '147', '149', '151', '153', '157', '159', '161', '195', '257', '259', '261', '263', 'River', '3']
- assert dmas[1] == ['50', '171', '173', '184', '199', '201', '203', '205', '206', '207', '208', '209', '211', '213', '215', '217', '219', '225', '229', '231', '237', '239', '241', '243', '247', '249', '251', '253', '255', '273', '275', '2']
- assert dmas[2] == ['10', '35', '40', '101', '103', '105', '107', '109', '111', '113', '115', '163', '164', '166', '167', '169', '177', '179', '181', '183', '185', '187', '189', '191', '193', '197', '204', '265', '267', '269', '271', 'Lake', '1']
-
- self.leave(p)
-
-
- def test_calculate_district_metering_area_for_network(self):
- p = 'test_calculate_district_metering_area_for_region'
- read_inp(p, f'./inp/net3.inp', '3')
- open_project(p)
-
- dmas = calculate_district_metering_area_for_network(p, 3)
- assert len(dmas) == 3
- assert dmas[0] == ['117', '119', '120', '121', '123', '125', '127', '129', '131', '139', '141', '143', '145', '147', '149', '15', '151', '153', '157', '159', '161', '195', '20', '257', '259', '261', '263', '3', '60', '601', '61', 'River']
- assert dmas[1] == ['1', '163', '164', '166', '167', '169', '171', '173', '177', '179', '181', '183', '184', '185', '187', '189', '191', '193', '199', '201', '203', '204', '205', '207', '265', '267', '269', '271', '273', '275', '35', '40']
- assert dmas[2] == ['10', '101', '103', '105', '107', '109', '111', '113', '115', '197', '2', '206', '208', '209', '211', '213', '215', '217', '219', '225', '229', '231', '237', '239', '241', '243', '247', '249', '251', '253', '255', '50', 'Lake']
-
- self.leave(p)
-
-
- def test_district_metering_area(self):
- p = 'test_district_metering_area'
- self.enter(p)
-
- dma = get_district_metering_area(p, 'dma')
- assert dma == {}
-
- add_district_metering_area(p, ChangeSet({'id': 'dma', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]}))
- dma = get_district_metering_area(p, 'dma')
- assert dma == {}
-
- add_district_metering_area(p, ChangeSet({'id': 'dma', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'parent': '1'}))
- dma = get_district_metering_area(p, 'dma')
- assert dma == {}
-
- add_district_metering_area(p, ChangeSet({'id': 'dma', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]}))
- dma = get_district_metering_area(p, 'dma')
- assert dma['id'] == 'dma'
- assert dma['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert dma['parent'] == None
- assert dma['nodes'] == []
- assert dma['level'] == 1
-
- set_district_metering_area(p, ChangeSet({'id': 'dma', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'parent': '1'}))
- dma = get_district_metering_area(p, 'dma')
- assert dma['id'] == 'dma'
- assert dma['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert dma['parent'] == None
- assert dma['nodes'] == []
- assert dma['level'] == 1
-
- add_district_metering_area(p, ChangeSet({'id': 'd0', 'boundary': [(0.0, 0.0), (1.0, 0.0), (2.0, 2.0), (0.0, 0.0)]}))
- set_district_metering_area(p, ChangeSet({'id': 'dma', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'parent': 'd0'}))
- dma = get_district_metering_area(p, 'dma')
- assert dma['id'] == 'dma'
- assert dma['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert dma['parent'] == 'd0'
- assert dma['nodes'] == []
- assert dma['level'] == 2
-
- set_district_metering_area(p, ChangeSet({'id': 'dma', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'nodes': ['1']}))
- dma = get_district_metering_area(p, 'dma')
- assert dma['id'] == 'dma'
- assert dma['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert dma['parent'] == 'd0'
- assert dma['nodes'] == []
- assert dma['level'] == 2
-
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- set_district_metering_area(p, ChangeSet({'id': 'dma', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'nodes': ['j0']}))
- dma = get_district_metering_area(p, 'dma')
- assert dma['id'] == 'dma'
- assert dma['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert dma['parent'] == 'd0'
- assert dma['nodes'] == ['j0']
- assert dma['level'] == 2
-
- delete_district_metering_area(p, ChangeSet({'id': 'dma'}))
- dma = get_district_metering_area(p, 'dma')
- assert dma == {}
-
- add_district_metering_area(p, ChangeSet({'id': 'dma', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'parent': 'd0', 'nodes': ['j0']}))
- dma = get_district_metering_area(p, 'dma')
- assert dma['id'] == 'dma'
- assert dma['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert dma['parent'] == 'd0'
- assert dma['nodes'] == ['j0']
- assert dma['level'] == 2
-
- delete_district_metering_area(p, ChangeSet({'id': 'd0'}))
- dma = get_district_metering_area(p, 'd0')
- assert dma != {}
-
- delete_district_metering_area(p, ChangeSet({'id': 'dma'}))
- dma = get_district_metering_area(p, 'dma')
- assert dma == {}
-
- delete_district_metering_area(p, ChangeSet({'id': 'd0'}))
- dma = get_district_metering_area(p, 'd0')
- assert dma == {}
-
- self.leave(p)
-
-
- def test_district_metering_area_op(self):
- p = 'test_district_metering_area_op'
- self.enter(p)
-
- cs = add_district_metering_area(p, ChangeSet({'id': 'dma', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]})).operations
- assert len(cs) == 0
-
- cs = add_district_metering_area(p, ChangeSet({'id': 'dma', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'parent': '1'})).operations
- assert len(cs) == 0
-
- cs = add_district_metering_area(p, ChangeSet({'id': 'dma', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]})).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'district_metering_area'
- assert cs['id'] == 'dma'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['parent'] == None
- assert cs['nodes'] == []
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'district_metering_area'
- assert cs['id'] == 'dma'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'district_metering_area'
- assert cs['id'] == 'dma'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['parent'] == None
- assert cs['nodes'] == []
-
- cs = set_district_metering_area(p, ChangeSet({'id': 'dma', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'parent': '1'})).operations
- assert len(cs) == 0
-
- add_district_metering_area(p, ChangeSet({'id': 'd0', 'boundary': [(0.0, 0.0), (1.0, 0.0), (2.0, 2.0), (0.0, 0.0)]}))
- cs = set_district_metering_area(p, ChangeSet({'id': 'dma', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'parent': 'd0'})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'district_metering_area'
- assert cs['id'] == 'dma'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['parent'] == 'd0'
- assert cs['nodes'] == []
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'district_metering_area'
- assert cs['id'] == 'dma'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['parent'] == None
- assert cs['nodes'] == []
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'district_metering_area'
- assert cs['id'] == 'dma'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['parent'] == 'd0'
- assert cs['nodes'] == []
-
- cs = set_district_metering_area(p, ChangeSet({'id': 'dma', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'nodes': ['1']})).operations
- assert len(cs) == 0
-
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
- cs = set_district_metering_area(p, ChangeSet({'id': 'dma', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'nodes': ['j0']})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'district_metering_area'
- assert cs['id'] == 'dma'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['parent'] == 'd0'
- assert cs['nodes'] == ['j0']
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'district_metering_area'
- assert cs['id'] == 'dma'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['parent'] == 'd0'
- assert cs['nodes'] == []
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'district_metering_area'
- assert cs['id'] == 'dma'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['parent'] == 'd0'
- assert cs['nodes'] == ['j0']
-
- cs = delete_district_metering_area(p, ChangeSet({'id': 'xxx'})).operations
- assert len(cs) == 0
-
- cs = delete_district_metering_area(p, ChangeSet({'id': 'dma'})).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'district_metering_area'
- assert cs['id'] == 'dma'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'district_metering_area'
- assert cs['id'] == 'dma'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['parent'] == 'd0'
- assert cs['nodes'] == ['j0']
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'district_metering_area'
- assert cs['id'] == 'dma'
-
- cs = add_district_metering_area(p, ChangeSet({'id': 'dma', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'parent': 'd0', 'nodes': ['j0']})).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'district_metering_area'
- assert cs['id'] == 'dma'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['parent'] == 'd0'
- assert cs['nodes'] == ['j0']
-
- cs = delete_district_metering_area(p, ChangeSet({'id': 'd0'})).operations
- assert len(cs) == 0
-
- cs = delete_district_metering_area(p, ChangeSet({'id': 'dma'})).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'district_metering_area'
- assert cs['id'] == 'dma'
-
- cs = delete_district_metering_area(p, ChangeSet({'id': 'dma'})).operations
- assert len(cs) == 0
-
- cs = delete_district_metering_area(p, ChangeSet({'id': 'd0'})).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'district_metering_area'
- assert cs['id'] == 'd0'
-
- self.leave(p)
-
-
- def test_district_metering_area_gen(self):
- p = 'test_district_metering_area_gen'
- read_inp(p, f'./inp/net3.inp', '3')
- open_project(p)
-
- cs = generate_district_metering_area(p, 3).operations
- assert len(cs) == 3
- assert cs[0]['operation'] == API_ADD
- assert cs[0]['type'] == 'district_metering_area'
- assert cs[0]['id'] == 'DMA_1_1'
- assert cs[1]['operation'] == API_ADD
- assert cs[1]['type'] == 'district_metering_area'
- assert cs[1]['id'] == 'DMA_1_2'
- assert cs[2]['operation'] == API_ADD
- assert cs[2]['type'] == 'district_metering_area'
- assert cs[2]['id'] == 'DMA_1_3'
-
- dmas = get_all_district_metering_area_ids(p)
- assert len(dmas) == 3
- assert dmas[0] == 'DMA_1_1'
- assert dmas[1] == 'DMA_1_2'
- assert dmas[2] == 'DMA_1_3'
-
- cs = generate_district_metering_area(p, 3).operations
- assert len(cs) == 6
- assert cs[0]['operation'] == API_DELETE
- assert cs[0]['type'] == 'district_metering_area'
- assert cs[0]['id'] == 'DMA_1_1'
- assert cs[1]['operation'] == API_DELETE
- assert cs[1]['type'] == 'district_metering_area'
- assert cs[1]['id'] == 'DMA_1_2'
- assert cs[2]['operation'] == API_DELETE
- assert cs[2]['type'] == 'district_metering_area'
- assert cs[2]['id'] == 'DMA_1_3'
- assert cs[3]['operation'] == API_ADD
- assert cs[3]['type'] == 'district_metering_area'
- assert cs[3]['id'] == 'DMA_1_1'
- assert cs[4]['operation'] == API_ADD
- assert cs[4]['type'] == 'district_metering_area'
- assert cs[4]['id'] == 'DMA_1_2'
- assert cs[5]['operation'] == API_ADD
- assert cs[5]['type'] == 'district_metering_area'
- assert cs[5]['id'] == 'DMA_1_3'
-
- dmas = get_all_district_metering_area_ids(p)
- assert len(dmas) == 3
- assert dmas[0] == 'DMA_1_1'
- assert dmas[1] == 'DMA_1_2'
- assert dmas[2] == 'DMA_1_3'
-
- cs = generate_sub_district_metering_area(p, 'DMA_1_1', 2).operations
- assert len(cs) == 2
- assert cs[0]['operation'] == API_ADD
- assert cs[0]['type'] == 'district_metering_area'
- assert cs[0]['id'] == 'DMA_[DMA_1_1]_2_1'
- assert cs[1]['operation'] == API_ADD
- assert cs[1]['type'] == 'district_metering_area'
- assert cs[1]['id'] == 'DMA_[DMA_1_1]_2_2'
-
- cs = generate_sub_district_metering_area(p, 'DMA_1_2', 3).operations
- assert len(cs) == 3
- assert cs[0]['operation'] == API_ADD
- assert cs[0]['type'] == 'district_metering_area'
- assert cs[0]['id'] == 'DMA_[DMA_1_2]_2_1'
- assert cs[1]['operation'] == API_ADD
- assert cs[1]['type'] == 'district_metering_area'
- assert cs[1]['id'] == 'DMA_[DMA_1_2]_2_2'
- assert cs[2]['operation'] == API_ADD
- assert cs[2]['type'] == 'district_metering_area'
- assert cs[2]['id'] == 'DMA_[DMA_1_2]_2_3'
-
- cs = generate_sub_district_metering_area(p, 'DMA_1_3', 2).operations
- assert len(cs) == 2
- assert cs[0]['operation'] == API_ADD
- assert cs[0]['type'] == 'district_metering_area'
- assert cs[0]['id'] == 'DMA_[DMA_1_3]_2_1'
- assert cs[1]['operation'] == API_ADD
- assert cs[1]['type'] == 'district_metering_area'
- assert cs[1]['id'] == 'DMA_[DMA_1_3]_2_2'
-
- dmas = get_all_district_metering_area_ids(p)
- assert len(dmas) == 10
-
- cs = generate_district_metering_area(p, 3).operations
- assert len(cs) == 13
-
- dmas = get_all_district_metering_area_ids(p)
- assert len(dmas) == 3
-
- self.leave(p)
-
-
- # 34 service_area
-
-
- def test_calculate_service_area(self):
- p = 'test_calculate_service_area'
- read_inp(p, f'./inp/net3.inp', '3')
- open_project(p)
-
- sass = calculate_service_area(p)
- assert len(sass) == 25
-
- assert sass[0]['River'] == ['River', '60', '61', '123', '601']
- assert sass[0]['3'] == ['121', '120', '119', '117', '257', '151', '157', '115', '259', '261', '149', '159', '111', '113', '263', '147', '161', '197', '193', '105', '145', '163', '195', '191', '267', '107', '141', '164', '265', '187', '189', '143', '166', '169', '204', '15', '167', '171', '269', '173', '271', '199', '201', '203', '3', '20', '127', '125', '129', '153', '131', '139']
- assert sass[0]['1'] == ['185', '184', '205', '273', '1', '40', '179', '177', '183', '181', '35']
- assert sass[0]['2'] == ['207', '275', '2', '50', '255', '247', '253', '251', '241', '249', '239', '243', '237', '211', '229', '209', '213', '231', '208', '215', '206', '217', '219', '225']
-
- assert sass[1]['River'] == ['River', '60', '61', '123', '601']
- assert sass[1]['3'] == ['121', '120', '119', '117', '257', '151', '157', '115', '259', '261', '149', '159', '111', '113', '263', '147', '161', '197', '193', '145', '163', '195', '191', '141', '164', '265', '187', '143', '166', '169', '267', '204', '15', '167', '171', '269', '173', '199', '201', '203', '3', '20', '127', '125', '129', '153', '131', '139']
- assert sass[1]['Lake'] == ['105', '107', 'Lake', '10', '101', '103', '109']
- assert sass[1]['1'] == ['189', '185', '271', '184', '205', '273', '1', '40', '179', '177', '183', '181', '35']
- assert sass[1]['2'] == ['207', '275', '2', '50', '255', '247', '253', '251', '241', '249', '239', '243', '237', '211', '229', '209', '213', '231', '208', '215', '206', '217', '219', '225']
-
- self.leave(p)
-
-
- def test_service_area(self):
- p = 'test_service_area'
- self.enter(p)
-
- sa = get_service_area(p, 'sa')
- assert sa == {}
-
- add_service_area(p, ChangeSet({'id': 'sa', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]}))
- sa = get_service_area(p, 'sa')
- assert sa == {}
-
- add_service_area(p, ChangeSet({'id': 'sa', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'source': 'j0'}))
- sa = get_service_area(p, 'sa')
- assert sa == {}
-
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
-
- add_service_area(p, ChangeSet({'id': 'sa', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'source': 'j0'}))
- sa = get_service_area(p, 'sa')
- assert sa == {}
-
- add_service_area(p, ChangeSet({'id': 'sa', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'source': 'j0', 'nodes' : ['x']}))
- sa = get_service_area(p, 'sa')
- assert sa == {}
-
- add_service_area(p, ChangeSet({'id': 'sa', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'source': 'j0', 'time_index' : 0, 'nodes': ['j0']}))
- sa = get_service_area(p, 'sa')
- assert sa['id'] == 'sa'
- assert sa['time_index'] == 0
- assert sa['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert sa['source'] == 'j0'
- assert sa['nodes'] == ['j0']
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
-
- set_service_area(p, ChangeSet({'id': 'sa', 'source': 'j1', 'time_index' : 1, 'nodes': ['j1']}))
- sa = get_service_area(p, 'sa')
- assert sa['id'] == 'sa'
- assert sa['time_index'] == 1
- assert sa['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert sa['source'] == 'j1'
- assert sa['nodes'] == ['j1']
-
- assert get_all_service_area_ids(p) == ['sa']
- sas = get_all_service_areas(p)
- assert len(sas) == 1
- sa = sas[0]
- assert sa['id'] == 'sa'
- assert sa['time_index'] == 1
- assert sa['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert sa['source'] == 'j1'
- assert sa['nodes'] == ['j1']
-
- delete_service_area(p, ChangeSet({'id': 'sa'}))
- sa = get_service_area(p, 'sa')
- assert sa == {}
-
- self.leave(p)
-
-
- def test_service_area_op(self):
- p = 'test_service_area_op'
- self.enter(p)
-
- cs = add_service_area(p, ChangeSet({'id': 'sa', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]})).operations
- assert len(cs) == 0
-
- cs = add_service_area(p, ChangeSet({'id': 'sa', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'source': 'j0'})).operations
- assert len(cs) == 0
-
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
-
- cs = add_service_area(p, ChangeSet({'id': 'sa', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'source': 'j0'})).operations
- assert len(cs) == 0
-
- cs = add_service_area(p, ChangeSet({'id': 'sa', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'source': 'j0', 'nodes' : ['x']})).operations
- assert len(cs) == 0
-
- cs = add_service_area(p, ChangeSet({'id': 'sa', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'source': 'j0', 'time_index' : 0, 'nodes': ['j0']})).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'service_area'
- assert cs['id'] == 'sa'
- assert cs['time_index'] == 0
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['source'] == 'j0'
- assert cs['nodes'] == ['j0']
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'service_area'
- assert cs['id'] == 'sa'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'service_area'
- assert cs['id'] == 'sa'
- assert cs['time_index'] == 0
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['source'] == 'j0'
- assert cs['nodes'] == ['j0']
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
-
- cs = set_service_area(p, ChangeSet({'id': 'sa', 'source': 'j1', 'time_index' : 1, 'nodes': ['j1']})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'service_area'
- assert cs['id'] == 'sa'
- assert cs['time_index'] == 1
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['source'] == 'j1'
- assert cs['nodes'] == ['j1']
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'service_area'
- assert cs['id'] == 'sa'
- assert cs['time_index'] == 0
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['source'] == 'j0'
- assert cs['nodes'] == ['j0']
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'service_area'
- assert cs['id'] == 'sa'
- assert cs['time_index'] == 1
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['source'] == 'j1'
- assert cs['nodes'] == ['j1']
-
- cs = delete_service_area(p, ChangeSet({'id': 'sa'})).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'service_area'
- assert cs['id'] == 'sa'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'service_area'
- assert cs['id'] == 'sa'
- assert cs['time_index'] == 1
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['source'] == 'j1'
- assert cs['nodes'] == ['j1']
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'service_area'
- assert cs['id'] == 'sa'
-
- self.leave(p)
-
-
- def test_service_area_gen(self):
- p = 'test_service_area_gen'
- read_inp(p, f'./inp/net3.inp', '3')
- open_project(p)
-
- cs = generate_service_area(p).operations
- assert len(cs) == 78
-
- assert len(get_all_service_area_ids(p)) == 78
- assert len(get_all_service_areas(p)) == 78
-
- cs = generate_service_area(p).operations
- assert len(cs) == 78 * 2
-
- assert len(get_all_service_area_ids(p)) == 78
- assert len(get_all_service_areas(p)) == 78
-
- self.leave(p)
-
-
- # 35 virtual_district
-
-
- def test_calculate_virtual_district(self):
- p = 'test_calculate_virtual_district'
- read_inp(p, f'./inp/net3.inp', '3')
- open_project(p)
-
- result = calculate_virtual_district(p, ['107', '139', '267', '211'])
- assert result['isolated_nodes'] == []
- vds = result['virtual_districts']
- assert len(vds) == 4
- assert vds[0]['center'] == '107'
- assert vds[0]['nodes'] == ['10', '101', '103', '105', '107', '109', '111', '115', '117', '119', '120', '257', '259', '261', '263', 'Lake']
- assert vds[1]['center'] == '139'
- assert vds[1]['nodes'] == ['15', '20', '60', '601', '61', '121', '123', '125', '127', '129', '131', '139', '141', '143', '145', '147', '149', '151', '153', 'River', '3']
- assert vds[2]['center'] == '267'
- assert vds[2]['nodes'] == ['35', '40', '113', '157', '159', '161', '163', '164', '166', '167', '169', '171', '173', '177', '179', '181', '183', '184', '185', '187', '189', '191', '193', '195', '197', '204', '265', '267', '269', '271', '1']
- assert vds[3]['center'] == '211'
- assert vds[3]['nodes'] == ['50', '199', '201', '203', '205', '206', '207', '208', '209', '211', '213', '215', '217', '219', '225', '229', '231', '237', '239', '241', '243', '247', '249', '251', '253', '255', '273', '275', '2']
-
- self.leave(p)
-
-
- def test_virtual_district(self):
- p = 'test_virtual_district'
- self.enter(p)
-
- vd = get_virtual_district(p, 'vd')
- assert vd == {}
-
- add_virtual_district(p, ChangeSet({'id': 'vd', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]}))
- vd = get_virtual_district(p, 'vd')
- assert vd == {}
-
- add_virtual_district(p, ChangeSet({'id': 'vd', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'center': 'j0'}))
- vd = get_virtual_district(p, 'vd')
- assert vd == {}
-
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
-
- add_virtual_district(p, ChangeSet({'id': 'vd', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'center': 'j0', 'nodes' : ['x']}))
- vd = get_virtual_district(p, 'vd')
- assert vd == {}
-
- add_virtual_district(p, ChangeSet({'id': 'vd', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'center': 'j0', 'nodes': ['j0']}))
- vd = get_virtual_district(p, 'vd')
- assert vd['id'] == 'vd'
- assert vd['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert vd['center'] == 'j0'
- assert vd['nodes'] == ['j0']
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
-
- set_virtual_district(p, ChangeSet({'id': 'vd', 'center': 'j1', 'nodes': ['j1']}))
- vd = get_virtual_district(p, 'vd')
- assert vd['id'] == 'vd'
- assert vd['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert vd['center'] == 'j1'
- assert vd['nodes'] == ['j1']
-
- assert get_all_virtual_district_ids(p) == ['vd']
- vds = get_all_virtual_districts(p)
- assert len(vds) == 1
- vd = vds[0]
- assert vd['id'] == 'vd'
- assert vd['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert vd['center'] == 'j1'
- assert vd['nodes'] == ['j1']
-
- delete_virtual_district(p, ChangeSet({'id': 'vd'}))
- vd = get_virtual_district(p, 'vd')
- assert vd == {}
-
- self.leave(p)
-
-
- def test_virtual_district_op(self):
- p = 'test_virtual_district_op'
- self.enter(p)
-
- cs = add_virtual_district(p, ChangeSet({'id': 'vd', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]})).operations
- assert len(cs) == 0
-
- cs = add_virtual_district(p, ChangeSet({'id': 'vd', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'center': 'j0'})).operations
- assert len(cs) == 0
-
- add_junction(p, ChangeSet({'id': 'j0', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
-
- cs = add_virtual_district(p, ChangeSet({'id': 'vd', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'center': 'j0', 'nodes' : ['x']})).operations
- assert len(cs) == 0
-
- cs = add_virtual_district(p, ChangeSet({'id': 'vd', 'boundary': [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)], 'center': 'j0', 'nodes': ['j0']})).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'virtual_district'
- assert cs['id'] == 'vd'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['center'] == 'j0'
- assert cs['nodes'] == ['j0']
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'virtual_district'
- assert cs['id'] == 'vd'
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'virtual_district'
- assert cs['id'] == 'vd'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['center'] == 'j0'
- assert cs['nodes'] == ['j0']
-
- add_junction(p, ChangeSet({'id': 'j1', 'x': 0.0, 'y': 10.0, 'elevation': 20.0}))
-
- cs = set_virtual_district(p, ChangeSet({'id': 'vd', 'center': 'j1', 'nodes': ['j1']})).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'virtual_district'
- assert cs['id'] == 'vd'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['center'] == 'j1'
- assert cs['nodes'] == ['j1']
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'virtual_district'
- assert cs['id'] == 'vd'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['center'] == 'j0'
- assert cs['nodes'] == ['j0']
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_UPDATE
- assert cs['type'] == 'virtual_district'
- assert cs['id'] == 'vd'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['center'] == 'j1'
- assert cs['nodes'] == ['j1']
-
- cs = delete_virtual_district(p, ChangeSet({'id': 'vd'})).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'virtual_district'
- assert cs['id'] == 'vd'
-
- cs = execute_undo(p).operations[0]
- assert cs['operation'] == API_ADD
- assert cs['type'] == 'virtual_district'
- assert cs['id'] == 'vd'
- assert cs['boundary'] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
- assert cs['center'] == 'j1'
- assert cs['nodes'] == ['j1']
-
- cs = execute_redo(p).operations[0]
- assert cs['operation'] == API_DELETE
- assert cs['type'] == 'virtual_district'
- assert cs['id'] == 'vd'
-
- self.leave(p)
-
-
- def test_virtual_district_gen(self):
- p = 'test_virtual_district_gen'
- read_inp(p, f'./inp/net3.inp', '3')
- open_project(p)
-
- cs = generate_virtual_district(p, ['107', '139', '267', '211']).operations
- assert len(cs) == 4
-
- assert len(get_all_virtual_district_ids(p)) == 4
- assert len(get_all_virtual_districts(p)) == 4
-
- cs = generate_virtual_district(p, ['107', '139', '267', '211']).operations
- assert len(cs) == 8
-
- assert len(get_all_virtual_district_ids(p)) == 4
- assert len(get_all_virtual_districts(p)) == 4
-
- self.leave(p)
-
-
- # 36 water_distribution
-
-
- def test_calculate_demand_to_nodes(self):
- p = 'test_calculate_demand_to_nodes'
- read_inp(p, f'./inp/net3.inp', '3')
- open_project(p)
-
- vds = calculate_virtual_district(p, ['107', '139', '267', '211'])['virtual_districts']
- result = calculate_demand_to_nodes(p, 100.0, vds[0]['nodes'])
- assert result == {'10': 17.357291284684024, '101': 22.112211221122113, '103': 6.466202175773133, '105': 8.232489915658233, '107': 4.180418041804181, '109': 7.260726072607261, '111': 3.862608483070529, '115': 6.466202175773133, '117': 5.738907224055739, '119': 0.892311453367559, '120': 3.9665077618873, '257': 2.9275149737195942, '259': 2.1391027991688056, '261': 2.9275149737195942, '263': 5.469991443588803}
-
- self.leave(p)
-
-
- def test_calculate_demand_to_region(self):
- p = 'test_calculate_demand_to_region'
- read_inp(p, f'./inp/net3.inp', '3')
- open_project(p)
-
- vds = calculate_virtual_district(p, ['107', '139', '267', '211'])['virtual_districts']
- nodes = vds[0]['nodes']
- boundary = calculate_boundary(p, nodes)
- boundary = inflate_boundary(p, boundary, 0.1)
- add_region(p, ChangeSet({'id': 'r', 'boundary': boundary}))
-
- result = calculate_demand_to_region(p, 100.0, 'r')
- assert result == {'10': 17.357291284684024, '101': 22.112211221122113, '103': 6.466202175773133, '105': 8.232489915658233, '107': 4.180418041804181, '109': 7.260726072607261, '111': 3.862608483070529, '115': 6.466202175773133, '117': 5.738907224055739, '119': 0.892311453367559, '120': 3.9665077618873, '257': 2.9275149737195942, '259': 2.1391027991688056, '261': 2.9275149737195942, '263': 5.469991443588803}
-
- self.leave(p)
-
-
- def test_calculate_demand_to_network(self):
- p = 'test_calculate_demand_to_network'
- read_inp(p, f'./inp/net3.inp', '3')
- open_project(p)
-
- result = calculate_demand_to_network(p, 100.0)
- assert result == {'10': 3.2914286561977604, '101': 4.1930946753955975, '103': 1.226173069808884, '105': 1.5611107041895715, '107': 0.7927243664927, '109': 1.3768370575925843, '111': 1.7685634258302052, '113': 1.237762607330707, '115': 1.689754570681808, '117': 1.0882575732991893, '119': 1.9169095061095407, '120': 1.2273320235610663, '121': 1.5020040628282736, '123': 10.894165270513714, '125': 2.0235332513103135, '127': 1.1415694458995753, '129': 2.34804030192136, '131': 1.5020040628282738, '139': 1.1125956020950176, '141': 1.6132636230377755, '143': 0.7069617888312091, '145': 1.323525184992198, '147': 0.713915511344303, '149': 0.4404024258292778, '15': 0.3824547382201623, '151': 1.3096177399660103, '153': 1.3281610000009272, '157': 1.1566358446779454, '159': 1.1380925846430283, '161': 1.0963702495644652, '163': 0.4125875357769024, '164': 0.14834608027933568, '166': 0.11357746771386638, '167': 0.01390744502618772, '169': 0.5947750656199615, '171': 0.482124760907841, '173': 0.9387525392676711, '177': 0.01390744502618772, '179': 0.3314607731241407, '181': 0.07417304013966784, '183': 0.4798068534034764, '184': 1.0731679954457753, '185': 0.44849192301951035, '187': 0.7324355923041763, '189': 0.8636523361262574, '191': 0.9804748743462343, '193': 0.859943684119274, '195': 0.6165633961609889, '197': 0.9132555567196603, '199': 1.241239468587254, '20': 0.2049030233858324, '201': 0.24338028795828512, '203': 0.02781489005237544, '204': 0.3302786402969147, '205': 1.4776660340324455, '206': 0.2225191204190035, '207': 0.713915511344303, '208': 0.32334809685886445, '209': 0.48560162216438785, '211': 0.9920644118680574, '213': 1.7326358595125533, '215': 1.3779960113447665, '217': 1.2215372548001548, '219': 0.4751710383947471, '225': 0.3615935706808807, '229': 1.1473642146604868, '231': 0.45430987085546554, '237': 0.7834527364752415, '239': 0.22599598167555046, '241': 0.6211992111697181, '243': 0.5099396509602164, '247': 0.4276539345552724, '249': 0.4380845183249132, '251': 0.5910664136129782, '253': 0.2549698254801082, '255': 1.0465352382206259, '257': 0.5551388472953265, '259': 0.4056338132638085, '261': 0.5551388472953265, '263': 1.0372636082031674, '265': 0.7811348289708769, '267': 0.9225271867371188, '269': 0.5984837176269449, '271': 0.5354366335082272, '273': 0.8344467015712631, '275': 0.9178913717283894, '35': 0.00695372251309386, '40': 0.29877827731259954, '50': 0.23735372844693708, '60': 0.28556620453772114, '601': 0.000463581500872924, '61': 10.546710935609457}
- self.leave(p)
-
-
-if __name__ == '__main__':
- pytest.main()
diff --git a/tests/api/test_openapi_contract.py b/tests/api/test_openapi_contract.py
index bfd18d2..848b291 100644
--- a/tests/api/test_openapi_contract.py
+++ b/tests/api/test_openapi_contract.py
@@ -10,12 +10,10 @@ from fastapi import APIRouter, FastAPI, Query
from fastapi.routing import APIRoute
from fastapi.testclient import TestClient
-from app.api.v1.endpoints import schemes as schemes_endpoint
from app.api.v1.endpoints import simulation as simulation_endpoint
from app.api.pagination import PaginatedList
from app.api.v1.rest_router import api_router, build_rest_router
from app.api.v1.router import api_router as source_api_router
-from app.auth.metadata_dependencies import get_current_metadata_user
from app.auth.project_dependencies import (
ProjectContext,
get_project_business_routing,
@@ -173,12 +171,6 @@ def test_rest_contract_uses_header_project_context() -> None:
assert "network" not in schema.get("properties", {})
assert "network_name" not in schema.get("properties", {})
- placement_schema = document["components"]["schemas"][
- "PressureSensorPlacementRest"
- ]
- assert "name" not in placement_schema["properties"]
- assert "username" not in placement_schema["properties"]
-
assert "/api/v1/burst-analysis" not in document["paths"]
assert "/api/v1/getpipeproperties/" not in document["paths"]
@@ -294,54 +286,10 @@ def test_sensor_placement_excel_export_is_post() -> None:
if isinstance(route, APIRoute)
}
assert methods_by_path[
- "/sensor-placement-schemes/{scheme_id}/exports/excel"
+ "/sensor-placement-runs/{run_id}/exports/excel"
] == {"POST"}
-def test_rest_runtime_consumes_injected_project_context(monkeypatch) -> None:
- captured: dict[str, object] = {}
-
- def fake_get_all_schemes(network, scheme_type=None, query_date=None):
- captured.update(
- network=network,
- scheme_type=scheme_type,
- query_date=query_date,
- business_dsn=get_project_pgconn_string(network),
- )
- return [{"scheme_name": "burst_case", "scheme_type": scheme_type}]
-
- monkeypatch.setattr(
- schemes_endpoint,
- "get_all_schemes",
- fake_get_all_schemes,
- )
- app = FastAPI(redirect_slashes=False)
- app.include_router(api_router, prefix="/api/v1")
- project_context = ProjectContext(
- project_id=uuid4(),
- project_code="fengyang",
- user_id=uuid4(),
- project_role="viewer",
- )
- _override_project_routing(app, project_context)
-
- response = TestClient(app, raise_server_exceptions=False).get(
- "/api/v1/schemes",
- params={"scheme_type": "burst_analysis"},
- )
-
- assert response.status_code == 200
- assert captured == {
- "network": "fengyang",
- "scheme_type": "burst_analysis",
- "query_date": None,
- "business_dsn": "postgresql://user:password@biz/fengyang",
- }
- assert response.json()["items"] == [
- {"scheme_name": "burst_case", "scheme_type": "burst_analysis"}
- ]
-
-
def test_rest_runtime_wraps_handler_paginated_list() -> None:
source_router = APIRouter()
@@ -370,51 +318,6 @@ def test_rest_runtime_wraps_handler_paginated_list() -> None:
}
-def test_sensor_placement_body_uses_authenticated_project_and_user(
- monkeypatch,
-) -> None:
- captured: dict[str, object] = {}
-
- def fake_pressure_sensor_placement_kmeans(**kwargs):
- captured.update(kwargs)
-
- monkeypatch.setattr(
- simulation_endpoint,
- "pressure_sensor_placement_kmeans",
- fake_pressure_sensor_placement_kmeans,
- )
- app = FastAPI(redirect_slashes=False)
- app.include_router(api_router, prefix="/api/v1")
- project_context = ProjectContext(
- project_id=uuid4(),
- project_code="project_a",
- user_id=uuid4(),
- project_role="member",
- )
- _override_project_routing(app, project_context)
- app.dependency_overrides[get_current_metadata_user] = lambda: type(
- "User", (), {"username": "alice"}
- )()
-
- response = TestClient(app, raise_server_exceptions=False).post(
- "/api/v1/pressure-sensor-placement-kmeans",
- json={
- "scheme_name": "placement_01",
- "sensor_number": 5,
- "min_diameter": 100,
- },
- )
-
- assert response.status_code == 200
- assert captured == {
- "name": "project_a",
- "scheme_name": "placement_01",
- "sensor_number": 5,
- "min_diameter": 100,
- "username": "alice",
- }
-
-
def test_rest_runtime_json_encodes_untyped_datetime_response() -> None:
source_router = APIRouter()
diff --git a/tests/api/test_project_endpoints.py b/tests/api/test_project_endpoints.py
index 31efb35..b4d1834 100644
--- a/tests/api/test_project_endpoints.py
+++ b/tests/api/test_project_endpoints.py
@@ -42,11 +42,10 @@ def _load_project_module(monkeypatch):
"read_inp": lambda network, inp: True,
"dump_inp": lambda network, inp: True,
"get_all_vertices": lambda network: [],
- "get_all_scada_elements": lambda network: [],
+ "get_all_scada_info": lambda network: [],
"get_all_district_metering_areas": lambda network: [],
"get_all_service_areas": lambda network: [],
"get_all_virtual_districts": lambda network: [],
- "get_extension_data": lambda network, key: None,
"convert_inp_v3_to_v2": lambda inp: DummyChangeSet({"inp": inp}),
},
)
@@ -55,16 +54,6 @@ def _load_project_module(monkeypatch):
"app.auth.project_dependencies",
{"get_metadata_repository": lambda: None},
)
- install_stub(
- monkeypatch,
- "app.infra.db.postgresql.database",
- {"get_database_instance": lambda network: None},
- )
- install_stub(
- monkeypatch,
- "app.infra.db.timescaledb.database",
- {"get_database_instance": lambda network: None},
- )
return load_module_from_path(
"tests_project_endpoints_module",
"app/api/v1/endpoints/project.py",
@@ -109,16 +98,12 @@ def test_project_info_returns_project_workspace(monkeypatch):
assert "geoserver" not in payload
-def test_open_project_returns_network_even_when_db_connection_fails(monkeypatch):
+def test_open_project_uses_unified_wndb_connection_path(monkeypatch):
module = _load_project_module(monkeypatch)
called = []
monkeypatch.setattr(module, "open_project", lambda network: called.append(network))
- async def failing_get_pg_db(network):
- raise RuntimeError("db down")
-
- monkeypatch.setattr(module, "get_pg_db", failing_get_pg_db)
client = TestClient(build_test_app(module.router, "/api/v1"))
response = client.post("/api/v1/projects/current", params={"network": "demo"})
diff --git a/tests/api/test_regions_endpoints.py b/tests/api/test_regions_endpoints.py
index a1d0898..41b3cca 100644
--- a/tests/api/test_regions_endpoints.py
+++ b/tests/api/test_regions_endpoints.py
@@ -1,5 +1,3 @@
-from typing import Any
-
from fastapi.testclient import TestClient
from tests.conftest import build_test_app, install_stub, load_module_from_path
@@ -7,16 +5,7 @@ from tests.conftest import build_test_app, install_stub, load_module_from_path
class DummyChangeSet:
def __init__(self, operations=None):
- if operations is None:
- self.operations = []
- elif isinstance(operations, dict):
- self.operations = [operations]
- else:
- self.operations = operations
-
-
-def _noop(*args, **kwargs):
- return None
+ self.operations = [operations] if isinstance(operations, dict) else operations or []
def _load_regions_module(monkeypatch):
@@ -25,41 +14,18 @@ def _load_regions_module(monkeypatch):
monkeypatch,
"app.services.tjnetwork",
{
- "Any": Any,
"ChangeSet": DummyChangeSet,
- "add_district_metering_area": _noop,
- "add_region": _noop,
- "add_service_area": _noop,
- "add_virtual_district": _noop,
- "calculate_district_metering_area_for_network": lambda *args, **kwargs: [],
- "calculate_district_metering_area_for_nodes": lambda *args, **kwargs: [],
- "calculate_district_metering_area_for_region": lambda *args, **kwargs: [],
- "calculate_service_area": lambda network: [],
- "calculate_virtual_district": lambda *args, **kwargs: {},
- "delete_district_metering_area": _noop,
- "delete_region": _noop,
- "delete_service_area": _noop,
- "delete_virtual_district": _noop,
- "generate_district_metering_area": _noop,
- "generate_service_area": _noop,
- "generate_sub_district_metering_area": _noop,
- "generate_virtual_district": _noop,
- "get_all_district_metering_area_ids": lambda network: [],
- "get_all_district_metering_areas": lambda network: [],
- "get_all_service_areas": lambda network: [],
- "get_all_virtual_districts": lambda network: [],
- "get_district_metering_area": lambda network, area_id: {},
- "get_district_metering_area_schema": lambda network: {},
- "get_region": lambda network, region_id: {},
- "get_region_schema": lambda network: {},
- "get_service_area": lambda network, area_id: {},
- "get_service_area_schema": lambda network: {},
- "get_virtual_district": lambda network, area_id: {},
- "get_virtual_district_schema": lambda network: {},
- "set_district_metering_area": _noop,
- "set_region": _noop,
- "set_service_area": _noop,
- "set_virtual_district": _noop,
+ "add_region": lambda network, cs: cs,
+ "delete_region": lambda network, cs: cs,
+ "get_nodes_in_region": lambda network, region_id: ["J1"],
+ "get_region": lambda network, region_id: {
+ "id": region_id,
+ "region_type": "DMA",
+ "boundary": [[0, 0], [1, 0], [0, 0]],
+ },
+ "get_region_schema": lambda network: {"id": {"type": "str"}},
+ "get_regions": lambda network: ["DMA-1"],
+ "set_region": lambda network, cs: cs,
},
)
return load_module_from_path(
@@ -68,87 +34,47 @@ def _load_regions_module(monkeypatch):
)
-def test_removed_routes_are_absent_and_return_404(monkeypatch):
+def test_regions_are_exposed_as_one_generic_resource(monkeypatch):
module = _load_regions_module(monkeypatch)
client = TestClient(build_test_app(module.router, "/api/v1"))
- openapi = client.get("/openapi.json").json()
-
- assert "/api/v1/calculateregion/" not in openapi["paths"]
- assert "/api/v1/getallregions/" not in openapi["paths"]
- assert "/api/v1/generateregion/" not in openapi["paths"]
- assert "/api/v1/calculatedistrictmeteringarea/" not in openapi["paths"]
- assert client.get("/api/v1/calculateregion/", params={"network": "demo", "time_index": 0}).status_code == 404
- assert client.get("/api/v1/calculatedistrictmeteringarea/", params={"network": "demo"}).status_code == 404
-
-
-def test_calculate_service_area_contract_uses_only_network(monkeypatch):
- module = _load_regions_module(monkeypatch)
- calls = []
- monkeypatch.setattr(
- module,
- "calculate_service_area",
- lambda network: calls.append(network) or [{"source-1": ["n1", "n2"]}],
- )
- client = TestClient(build_test_app(module.router, "/api/v1"))
-
- response = client.post(
- "/api/v1/service-area-calculations",
- params={"network": "demo", "time_index": 5},
- )
- schema = client.get("/openapi.json").json()
+ response = client.get("/api/v1/regions", params={"network": "demo"})
assert response.status_code == 200
- assert response.json() == [{"source-1": ["n1", "n2"]}]
- assert calls == ["demo"]
- parameter_names = [
- item["name"]
- for item in schema["paths"]["/api/v1/service-area-calculations"]["post"]["parameters"]
- ]
- assert parameter_names == ["network"]
+ assert response.json()[0]["region_type"] == "DMA"
-def test_add_district_metering_area_converts_boundary_to_tuples(monkeypatch):
+def test_add_region_converts_boundary_to_tuples(monkeypatch):
module = _load_regions_module(monkeypatch)
captured = {}
- def fake_add(network, change_set):
- captured["network"] = network
- captured["boundary"] = change_set.operations[0]["boundary"]
- return {"ok": True}
+ def add(network, changeset):
+ captured["operation"] = changeset.operations[0]
+ return changeset
- monkeypatch.setattr(module, "add_district_metering_area", fake_add)
+ monkeypatch.setattr(module, "add_region", add)
client = TestClient(build_test_app(module.router, "/api/v1"))
-
response = client.post(
- "/api/v1/district-metering-areas",
+ "/api/v1/regions",
params={"network": "demo"},
- json={"id": "dma-1", "boundary": [[1, 2], [3, 4], [1, 2]]},
+ json={
+ "id": "DMA-1",
+ "region_type": "DMA",
+ "boundary": [[0, 0], [1, 0], [0, 0]],
+ },
)
assert response.status_code == 200
- assert captured == {
- "network": "demo",
- "boundary": [(1, 2), (3, 4), (1, 2)],
- }
+ assert captured["operation"]["boundary"] == [(0, 0), (1, 0), (0, 0)]
-def test_generate_virtual_district_reads_centers_from_body(monkeypatch):
+def test_region_nodes_use_generic_region_id(monkeypatch):
module = _load_regions_module(monkeypatch)
- captured = {}
-
- def fake_generate(network, centers, inflate_delta):
- captured["args"] = (network, centers, inflate_delta)
- return {"generated": True}
-
- monkeypatch.setattr(module, "generate_virtual_district", fake_generate)
client = TestClient(build_test_app(module.router, "/api/v1"))
- response = client.post(
- "/api/v1/virtual-district-generation-runs",
- params={"network": "demo", "inflate_delta": 0.75},
- json={"centers": ["J1", "J2"]},
+ response = client.get(
+ "/api/v1/regions/nodes", params={"network": "demo", "id": "DMA-1"}
)
assert response.status_code == 200
- assert captured["args"] == ("demo", ["J1", "J2"], 0.75)
+ assert response.json() == ["J1"]
diff --git a/tests/api/test_schemes_endpoints.py b/tests/api/test_schemes_endpoints.py
deleted file mode 100644
index 319a5ad..0000000
--- a/tests/api/test_schemes_endpoints.py
+++ /dev/null
@@ -1,102 +0,0 @@
-from datetime import date
-
-from fastapi import FastAPI
-from fastapi.testclient import TestClient
-
-from app.api.v1.endpoints import schemes as schemes_endpoint
-
-
-def _build_client() -> TestClient:
- app = FastAPI()
- app.include_router(schemes_endpoint.router, prefix="/api/v1")
- return TestClient(app)
-
-
-def test_get_schemes_forwards_optional_scheme_type(monkeypatch):
- captured = {}
-
- def fake_get_all_schemes(network, scheme_type=None, query_date=None):
- captured["network"] = network
- captured["scheme_type"] = scheme_type
- captured["query_date"] = query_date
- return [
- {
- "scheme_id": 1,
- "scheme_name": "burst_case",
- "scheme_type": scheme_type,
- }
- ]
-
- monkeypatch.setattr(schemes_endpoint, "get_all_schemes", fake_get_all_schemes)
-
- response = _build_client().get(
- "/api/v1/schemes",
- params={"network": "demo", "scheme_type": "burst_analysis"},
- )
-
- assert response.status_code == 200
- assert captured == {
- "network": "demo",
- "scheme_type": "burst_analysis",
- "query_date": None,
- }
- assert response.json()[0]["scheme_type"] == "burst_analysis"
-
-
-def test_get_schemes_forwards_query_date(monkeypatch):
- captured = {}
-
- def fake_get_all_schemes(network, scheme_type=None, query_date=None):
- captured["network"] = network
- captured["scheme_type"] = scheme_type
- captured["query_date"] = query_date
- return []
-
- monkeypatch.setattr(schemes_endpoint, "get_all_schemes", fake_get_all_schemes)
-
- response = _build_client().get(
- "/api/v1/schemes",
- params={
- "network": "demo",
- "scheme_type": "dma_leak_identification",
- "query_date": "2026-01-02T00:00:00+08:00",
- },
- )
-
- assert response.status_code == 200
- assert captured == {
- "network": "demo",
- "scheme_type": "dma_leak_identification",
- "query_date": date(2026, 1, 2),
- }
-
-
-def test_get_scheme_detail_forwards_scheme_type(monkeypatch):
- captured = {}
-
- def fake_query_scheme_detail(name, scheme_name, scheme_type=None):
- captured["name"] = name
- captured["scheme_name"] = scheme_name
- captured["scheme_type"] = scheme_type
- return {
- "scheme_name": scheme_name,
- "scheme_type": scheme_type,
- "rows": [{"Area": "1", "LeakageFlow_m3_per_s": 0.1}],
- }
-
- monkeypatch.setattr(
- schemes_endpoint, "query_scheme_detail", fake_query_scheme_detail
- )
-
- response = _build_client().get(
- "/api/v1/schemes/dma_001",
- params={"network": "demo", "scheme_type": "dma_leak_identification"},
- )
-
- assert response.status_code == 200
- assert captured == {
- "name": "demo",
- "scheme_name": "dma_001",
- "scheme_type": "dma_leak_identification",
- }
- assert response.json()["scheme_name"] == "dma_001"
diff --git a/tests/api/test_sensor_placement_endpoints.py b/tests/api/test_sensor_placement_endpoints.py
index 73b42ef..f4bc828 100644
--- a/tests/api/test_sensor_placement_endpoints.py
+++ b/tests/api/test_sensor_placement_endpoints.py
@@ -1,424 +1,124 @@
from datetime import datetime, timezone
from io import BytesIO
from types import SimpleNamespace
+from uuid import uuid4
-import pytest
from fastapi.testclient import TestClient
-from tests.conftest import build_test_app, install_stub, load_module_from_path
+from app.api.v1.endpoints import sensor_placement as endpoint
+from tests.conftest import build_test_app
-class NotFoundError(LookupError):
- pass
+RUN_ID = uuid4()
-class ValidationError(ValueError):
- pass
-
-
-class ConflictError(RuntimeError):
- pass
-
-
-def _scheme(**overrides):
+def _run(**overrides):
value = {
- "id": 7,
- "scheme_name": "北区测压点",
- "sensor_number": 2,
+ "run_id": RUN_ID,
+ "name": "北区测压点",
+ "sensor_count": 1,
"min_diameter": 300,
- "username": "alice",
- "create_time": datetime(2026, 7, 30, 8, 0, tzinfo=timezone.utc),
- "sensor_location": ["J1", "J2"],
+ "created_by": "alice",
+ "created_at": datetime(2026, 8, 24, tzinfo=timezone.utc),
+ "status": "completed",
+ "sensor_locations": ["J1"],
"sensor_points": [
{
"node_id": "J1",
"max_pipe_diameter": 400.0,
- "project_x": 13500000.0,
- "project_y": 3600000.0,
- "map_x": 13500000.0,
- "map_y": 3600000.0,
+ "project_x": 1.0,
+ "project_y": 2.0,
+ "map_x": 3.0,
+ "map_y": 4.0,
"longitude": 121.0,
"latitude": 31.0,
- "elevation": 4.5,
- },
- {
- "node_id": "J2",
- "max_pipe_diameter": 300.0,
- "project_x": 13500100.0,
- "project_y": 3600100.0,
- "map_x": 13500100.0,
- "map_y": 3600100.0,
- "longitude": 121.001,
- "latitude": 31.001,
"elevation": 5.0,
- },
+ }
],
}
value.update(overrides)
return value
-def _load_module(monkeypatch):
- install_stub(monkeypatch, "app.algorithms", package=True)
- install_stub(
- monkeypatch,
- "app.algorithms.sensor",
- {
- "pressure_sensor_placement_kmeans": lambda **kwargs: {"id": 7},
- "pressure_sensor_placement_sensitivity": lambda **kwargs: {"id": 7},
- },
+def _client(project_role="member", username="alice", role="user"):
+ app = build_test_app(endpoint.router, "/api/v1")
+ app.dependency_overrides[endpoint.get_project_context] = lambda: SimpleNamespace(
+ project_code="tjwater", project_role=project_role
)
- install_stub(monkeypatch, "app.auth", package=True)
-
- async def current_user():
- return SimpleNamespace(
- username="alice",
- role="user",
- is_superuser=False,
- )
-
- install_stub(
- monkeypatch,
- "app.auth.metadata_dependencies",
- {"get_current_metadata_user": current_user},
- )
-
- class ProjectContext:
- def __init__(self, project_code: str, project_role: str = "member"):
- self.project_code = project_code
- self.project_role = project_role
-
- async def project_context():
- return ProjectContext("tjwater")
-
- install_stub(
- monkeypatch,
- "app.auth.project_dependencies",
- {
- "ProjectContext": ProjectContext,
- "get_project_context": project_context,
- },
- )
- install_stub(monkeypatch, "app.services", package=True)
- install_stub(
- monkeypatch,
- "app.services.sensor_placement",
- {
- "SensorPlacementConflictError": ConflictError,
- "SensorPlacementNotFoundError": NotFoundError,
- "SensorPlacementValidationError": ValidationError,
- "build_sensor_placement_workbook": lambda **kwargs: BytesIO(b"xlsx"),
- "can_edit_sensor_placement": (
- lambda user, scheme: user.username == scheme["username"]
- or user.role == "admin"
- or user.is_superuser
- ),
- "get_sensor_placement_scheme": lambda network, scheme_id: _scheme(
- id=scheme_id
- ),
- "get_sensor_placement_candidate": (
- lambda network, node_id: _scheme()["sensor_points"][0]
- ),
- "update_sensor_placement_scheme": (
- lambda network, scheme_id, **kwargs: _scheme(
- id=scheme_id,
- sensor_location=kwargs["sensor_location"],
- sensor_number=len(kwargs["sensor_location"]),
- )
- ),
- },
- )
- return load_module_from_path(
- "tests_sensor_placement_endpoints_module",
- "app/api/v1/endpoints/sensor_placement.py",
- )
-
-
-def _client(module, user=None, project_role="member"):
- app = build_test_app(module.router, "/api/v1")
- if user is None:
- user = SimpleNamespace(
- username="alice",
- role="user",
- is_superuser=False,
- )
- app.dependency_overrides[module.get_current_metadata_user] = lambda: user
- app.dependency_overrides[module.get_project_context] = lambda: (
- module.ProjectContext("tjwater", project_role)
+ app.dependency_overrides[endpoint.get_current_metadata_user] = lambda: SimpleNamespace(
+ username=username, role=role, is_superuser=False
)
return TestClient(app)
-def test_optimize_returns_created_scheme(monkeypatch):
- module = _load_module(monkeypatch)
+def test_optimize_returns_analysis_run(monkeypatch):
captured = {}
+ monkeypatch.setattr(
+ endpoint,
+ "pressure_sensor_placement_kmeans",
+ lambda **kwargs: captured.update(kwargs) or {"run_id": RUN_ID},
+ )
+ monkeypatch.setattr(endpoint, "get_sensor_placement_run", lambda *_: _run())
- def optimize(**kwargs):
- captured.update(kwargs)
- return {"id": 7}
-
- monkeypatch.setattr(module, "pressure_sensor_placement_kmeans", optimize)
- response = _client(module).post(
- "/api/v1/sensor-placement-optimization-runs",
+ response = _client().post(
+ "/api/v1/sensor-placement-runs",
json={
"network": "tjwater",
- "scheme_name": "北区测压点",
+ "run_name": "北区测压点",
"sensor_type": "pressure",
"method": "kmeans",
- "sensor_count": 2,
+ "sensor_count": 1,
"min_diameter": 300,
},
)
assert response.status_code == 200
- assert response.json()["sensor_location"] == ["J1", "J2"]
+ assert response.json()["run_id"] == str(RUN_ID)
assert captured["username"] == "alice"
-def test_get_candidate_returns_maximum_incident_pipe_diameter(monkeypatch):
- module = _load_module(monkeypatch)
-
- response = _client(module).get(
- "/api/v1/sensor-placement-candidates/J1",
- )
-
- assert response.status_code == 200
- assert response.json()["node_id"] == "J1"
- assert response.json()["max_pipe_diameter"] == 400.0
-
-
-def test_optimize_rejects_unsupported_sensor_type(monkeypatch):
- module = _load_module(monkeypatch)
- response = _client(module).post(
- "/api/v1/sensor-placement-optimization-runs",
+def test_optimize_rejects_project_mismatch(monkeypatch):
+ response = _client().post(
+ "/api/v1/sensor-placement-runs",
json={
- "network": "tjwater",
- "scheme_name": "北区测流点",
- "sensor_type": "flow",
- "method": "kmeans",
- "sensor_count": 2,
- "min_diameter": 300,
- },
- )
-
- assert response.status_code == 422
-
-
-def test_optimize_rejects_network_outside_project_context(monkeypatch):
- module = _load_module(monkeypatch)
- response = _client(module).post(
- "/api/v1/sensor-placement-optimization-runs",
- json={
- "network": "other_project",
- "scheme_name": "越权方案",
+ "network": "other",
+ "run_name": "越权运行",
"sensor_type": "pressure",
"method": "kmeans",
- "sensor_count": 2,
- "min_diameter": 300,
+ "sensor_count": 1,
},
)
assert response.status_code == 403
-def test_optimize_rejects_network_path_traversal(monkeypatch):
- module = _load_module(monkeypatch)
- response = _client(module).post(
- "/api/v1/sensor-placement-optimization-runs",
+def test_viewer_cannot_update_run(monkeypatch):
+ monkeypatch.setattr(endpoint, "get_sensor_placement_run", lambda *_: _run())
+
+ response = _client(project_role="viewer").put(
+ f"/api/v1/sensor-placement-runs/{RUN_ID}",
+ params={"network": "tjwater"},
json={
- "network": "../other_project",
- "scheme_name": "非法路径",
- "sensor_type": "pressure",
- "method": "kmeans",
- "sensor_count": 2,
- "min_diameter": 300,
- },
- )
-
- assert response.status_code == 422
-
-
-def test_optimize_rejects_unbounded_sensor_count(monkeypatch):
- module = _load_module(monkeypatch)
- response = _client(module).post(
- "/api/v1/sensor-placement-optimization-runs",
- json={
- "network": "tjwater",
- "scheme_name": "超大方案",
- "sensor_type": "pressure",
- "method": "kmeans",
- "sensor_count": 201,
- "min_diameter": 300,
- },
- )
-
- assert response.status_code == 422
-
-
-def test_optimize_rejects_viewer_project_role(monkeypatch):
- module = _load_module(monkeypatch)
- response = _client(module, project_role="viewer").post(
- "/api/v1/sensor-placement-optimization-runs",
- json={
- "network": "tjwater",
- "scheme_name": "只读成员方案",
- "sensor_type": "pressure",
- "method": "kmeans",
- "sensor_count": 2,
- "min_diameter": 300,
+ "expected_sensor_locations": ["J1"],
+ "sensor_locations": ["J2"],
},
)
assert response.status_code == 403
-@pytest.mark.parametrize(
- "project_role",
- ["owner", "admin", "modeler", "dispatcher", "auditor"],
-)
-def test_legacy_project_roles_cannot_optimize(monkeypatch, project_role):
- module = _load_module(monkeypatch)
- response = _client(module, project_role=project_role).post(
- "/api/v1/sensor-placement-optimization-runs",
- json={
- "network": "tjwater",
- "scheme_name": f"{project_role}方案",
- "sensor_type": "pressure",
- "method": "kmeans",
- "sensor_count": 2,
- "min_diameter": 300,
- },
+def test_export_returns_xlsx(monkeypatch):
+ monkeypatch.setattr(endpoint, "get_sensor_placement_run", lambda *_: _run())
+ monkeypatch.setattr(
+ endpoint,
+ "build_sensor_placement_workbook",
+ lambda **kwargs: BytesIO(b"xlsx"),
)
- assert response.status_code == 403
-
-
-def test_optimize_maps_running_project_job_to_409(monkeypatch):
- module = _load_module(monkeypatch)
-
- def conflict(**kwargs):
- raise ConflictError("当前项目已有监测点优化任务正在运行,请稍后重试")
-
- monkeypatch.setattr(module, "pressure_sensor_placement_kmeans", conflict)
- response = _client(module).post(
- "/api/v1/sensor-placement-optimization-runs",
- json={
- "network": "tjwater",
- "scheme_name": "并发方案",
- "sensor_type": "pressure",
- "method": "kmeans",
- "sensor_count": 2,
- "min_diameter": 300,
- },
- )
-
- assert response.status_code == 409
-
-
-def test_viewer_reads_scheme_as_non_editable(monkeypatch):
- module = _load_module(monkeypatch)
- response = _client(module, project_role="viewer").get(
- "/api/v1/sensor-placement-schemes/7",
+ response = _client().post(
+ f"/api/v1/sensor-placement-runs/{RUN_ID}/exports/excel",
params={"network": "tjwater"},
- )
-
- assert response.status_code == 200
- assert response.json()["can_edit"] is False
-
-
-def test_update_rejects_non_owner(monkeypatch):
- module = _load_module(monkeypatch)
- response = _client(
- module,
- SimpleNamespace(username="bob", role="user", is_superuser=False),
- ).put(
- "/api/v1/sensor-placement-schemes/7",
- params={"network": "tjwater"},
- json={
- "expected_sensor_location": ["J1", "J2"],
- "sensor_location": ["J1", "J3"],
- },
- )
-
- assert response.status_code == 403
-
-
-def test_update_rejects_owner_with_viewer_project_role(monkeypatch):
- module = _load_module(monkeypatch)
- response = _client(module, project_role="viewer").put(
- "/api/v1/sensor-placement-schemes/7",
- params={"network": "tjwater"},
- json={
- "expected_sensor_location": ["J1", "J2"],
- "sensor_location": ["J1", "J3"],
- },
- )
-
- assert response.status_code == 403
-
-
-def test_admin_can_overwrite_scheme(monkeypatch):
- module = _load_module(monkeypatch)
- response = _client(
- module,
- SimpleNamespace(username="ops", role="admin", is_superuser=False),
- ).put(
- "/api/v1/sensor-placement-schemes/7",
- params={"network": "tjwater"},
- json={
- "expected_sensor_location": ["J1", "J2"],
- "sensor_location": ["J1", "J3"],
- },
- )
-
- assert response.status_code == 200
- assert response.json()["sensor_number"] == 2
- assert response.json()["sensor_location"] == ["J1", "J3"]
-
-
-def test_update_maps_concurrent_change_to_409(monkeypatch):
- module = _load_module(monkeypatch)
-
- def conflict(*args, **kwargs):
- raise ConflictError("方案已被其他用户修改,请重新加载")
-
- monkeypatch.setattr(module, "update_sensor_placement_scheme", conflict)
- response = _client(module).put(
- "/api/v1/sensor-placement-schemes/7",
- params={"network": "tjwater"},
- json={
- "expected_sensor_location": ["J1", "J2"],
- "sensor_location": ["J1", "J3"],
- },
- )
-
- assert response.status_code == 409
- assert "重新加载" in response.json()["detail"]
-
-
-def test_update_rejects_duplicate_nodes_before_service(monkeypatch):
- module = _load_module(monkeypatch)
- response = _client(module).put(
- "/api/v1/sensor-placement-schemes/7",
- params={"network": "tjwater"},
- json={
- "expected_sensor_location": ["J1", "J2"],
- "sensor_location": ["J1", "J1"],
- },
- )
-
- assert response.status_code == 422
-
-
-def test_export_returns_xlsx_download(monkeypatch):
- module = _load_module(monkeypatch)
- response = _client(module).post(
- "/api/v1/sensor-placement-schemes/7/exports/excel",
- params={"network": "tjwater"},
- json={
- "sensor_location": ["J1", "J2"],
- "adjustment_status": {"J1": "original", "J2": "replaced"},
- },
+ json={"sensor_locations": ["J1"], "adjustment_status": {}},
)
assert response.status_code == 200
@@ -426,4 +126,3 @@ def test_export_returns_xlsx_download(monkeypatch):
assert response.headers["content-type"].startswith(
"application/vnd.openxmlformats-officedocument"
)
- assert "filename*=UTF-8" in response.headers["content-disposition"]
diff --git a/tests/integration/test_database_pooling_live.py b/tests/integration/test_database_pooling_live.py
new file mode 100644
index 0000000..e5f3818
--- /dev/null
+++ b/tests/integration/test_database_pooling_live.py
@@ -0,0 +1,238 @@
+import os
+from concurrent.futures import ThreadPoolExecutor
+from uuid import uuid4
+
+import pytest
+
+from app.infra.db.timescaledb.sync_pool import timescale_connection
+from app.native.wndb.commands.api import delete_pattern_cascade
+from app.native.wndb.core.connection import project_connection, project_transaction
+from app.native.wndb.core.database import ChangeSet, g_delete_prefix, write
+from app.native.wndb.model import demands, junctions, patterns
+from app.services.scheme_management import create_analysis_run, update_analysis_run
+
+
+pytestmark = pytest.mark.skipif(
+ os.getenv("RUN_DB_INTEGRATION") != "1",
+ reason="set RUN_DB_INTEGRATION=1 to test configured PostgreSQL databases",
+)
+
+PROJECT = os.getenv("DB_INTEGRATION_PROJECT", "tjwater_next")
+
+
+def _read_business_database(_: int) -> str:
+ with project_connection(PROJECT) as conn, conn.cursor() as cur:
+ cur.execute("select current_database()")
+ return str(cur.fetchone()["current_database"])
+
+
+def _read_timeseries_database(_: int) -> str:
+ with timescale_connection(PROJECT) as conn, conn.cursor() as cur:
+ cur.execute("select current_database()")
+ return str(cur.fetchone()["current_database"])
+
+
+def test_business_pool_handles_concurrent_borrows() -> None:
+ with ThreadPoolExecutor(max_workers=16) as executor:
+ names = list(executor.map(_read_business_database, range(64)))
+
+ assert names == [PROJECT] * 64
+
+
+def test_timeseries_pool_handles_concurrent_borrows() -> None:
+ with ThreadPoolExecutor(max_workers=16) as executor:
+ names = list(executor.map(_read_timeseries_database, range(64)))
+
+ assert names == [PROJECT] * 64
+
+
+def test_nested_wndb_writes_roll_back_as_one_transaction() -> None:
+ with pytest.raises(RuntimeError, match="force rollback"):
+ with project_transaction(PROJECT) as conn:
+ with conn.cursor() as cur:
+ cur.execute(
+ "create temporary table wndb_pool_rollback_probe (value integer) on commit drop"
+ )
+ cur.execute("insert into wndb_pool_rollback_probe values (1)")
+ with project_connection(PROJECT) as nested:
+ assert nested is conn
+ raise RuntimeError("force rollback")
+
+ with project_connection(PROJECT) as conn, conn.cursor() as cur:
+ cur.execute("select to_regclass('pg_temp.wndb_pool_rollback_probe')")
+ assert cur.fetchone()["to_regclass"] is None
+
+
+def test_analysis_run_lifecycle_uses_one_execution_id() -> None:
+ run_id = None
+ with pytest.raises(RuntimeError, match="force rollback"):
+ with project_transaction(PROJECT) as conn:
+ run_id = create_analysis_run(
+ PROJECT,
+ "integration-lifecycle-probe",
+ "integration_test",
+ "pytest",
+ "2026-08-24T00:00:00Z",
+ {"temporary": True},
+ )
+ update_analysis_run(
+ PROJECT,
+ run_id,
+ status="completed",
+ username="pytest",
+ scheme_detail={"temporary": True},
+ )
+ with conn.cursor() as cur:
+ cur.execute(
+ "select status from analysis.runs where run_id = %s", (run_id,)
+ )
+ assert cur.fetchone()["status"] == "completed"
+ raise RuntimeError("force rollback")
+
+ with project_connection(PROJECT) as conn, conn.cursor() as cur:
+ cur.execute("select count(*) as count from analysis.runs where run_id = %s", (run_id,))
+ assert cur.fetchone()["count"] == 0
+
+
+def test_legacy_wndb_batch_treats_malicious_id_as_data() -> None:
+ malicious = "integration'); DROP SCHEMA network CASCADE; --"
+ command = patterns._add_pattern(
+ PROJECT,
+ ChangeSet({"id": malicious, "factors": [1.0]}),
+ )
+
+ with pytest.raises(RuntimeError, match="force rollback"):
+ with project_transaction(PROJECT) as conn:
+ write(PROJECT, command.sql)
+ with conn.cursor() as cur:
+ cur.execute("select count(*) as count from network.patterns where id = %s", (malicious,))
+ assert cur.fetchone()["count"] == 1
+ cur.execute("select to_regnamespace('network') as namespace")
+ assert cur.fetchone()["namespace"] is not None
+ raise RuntimeError("force rollback")
+
+
+def test_wndb_database_command_pattern_crud_uses_pooled_transaction() -> None:
+ pattern_id = f"integration-command-{uuid4()}"
+
+ with pytest.raises(RuntimeError, match="force rollback"):
+ with project_transaction(PROJECT) as conn:
+ added = patterns.add_pattern(
+ PROJECT,
+ ChangeSet({"id": pattern_id, "factors": [1.0, 1.1]}),
+ )
+ updated = patterns.set_pattern(
+ PROJECT,
+ ChangeSet({"id": pattern_id, "factors": [0.8, 1.2, 1.0]}),
+ )
+
+ with conn.cursor() as cur:
+ cur.execute(
+ "select factor from network.pattern_values "
+ "where pattern_id = %s order by sequence_no",
+ (pattern_id,),
+ )
+ assert [float(row["factor"]) for row in cur.fetchall()] == [
+ 0.8,
+ 1.2,
+ 1.0,
+ ]
+
+ deleted = patterns.delete_pattern(
+ PROJECT,
+ ChangeSet({"id": pattern_id}),
+ )
+ with conn.cursor() as cur:
+ cur.execute(
+ "select count(*) as count from network.patterns where id = %s",
+ (pattern_id,),
+ )
+ assert cur.fetchone()["count"] == 0
+
+ assert added.operations[0]["operation"] == "add"
+ assert updated.operations[0]["operation"] == "update"
+ assert deleted.operations == [
+ {"operation": "delete", "type": "pattern", "id": pattern_id}
+ ]
+ raise RuntimeError("force rollback")
+
+ with project_connection(PROJECT) as conn, conn.cursor() as cur:
+ cur.execute(
+ "select count(*) as count from network.patterns where id = %s",
+ (pattern_id,),
+ )
+ assert cur.fetchone()["count"] == 0
+
+
+def test_wndb_ordered_detail_tables_use_parent_scoped_primary_keys() -> None:
+ expected = {
+ "gis.link_vertices": "PRIMARY KEY (link_id, sequence_no)",
+ "network.curve_points": "PRIMARY KEY (curve_id, sequence_no)",
+ "network.demands": "PRIMARY KEY (junction_id, sequence_no)",
+ "network.pattern_flow_samples": "PRIMARY KEY (pattern_id, sequence_no)",
+ "network.pattern_values": "PRIMARY KEY (pattern_id, sequence_no)",
+ }
+
+ with project_connection(PROJECT) as conn, conn.cursor() as cur:
+ cur.execute(
+ "select conrelid::regclass::text as table_name, "
+ "pg_get_constraintdef(oid) as definition "
+ "from pg_constraint "
+ "where conname = any(%s) order by 1",
+ ([f"{table.rsplit('.', 1)[1]}_pkey" for table in expected],),
+ )
+ actual = {row["table_name"]: row["definition"] for row in cur.fetchall()}
+
+ assert actual == expected
+
+
+def test_wndb_pattern_cascade_unsets_dependent_demand_atomically() -> None:
+ suffix = uuid4()
+ junction_id = f"integration-junction-{suffix}"
+ pattern_id = f"integration-cascade-{suffix}"
+
+ with pytest.raises(RuntimeError, match="force rollback"):
+ with project_transaction(PROJECT):
+ junctions.add_junction(
+ PROJECT,
+ ChangeSet(
+ {"id": junction_id, "x": 0.0, "y": 0.0, "elevation": 1.0}
+ ),
+ )
+ patterns.add_pattern(
+ PROJECT,
+ ChangeSet({"id": pattern_id, "factors": [1.0]}),
+ )
+ demands.set_demand(
+ PROJECT,
+ ChangeSet(
+ {
+ "junction": junction_id,
+ "demands": [
+ {
+ "demand": 1.0,
+ "pattern": pattern_id,
+ "category": "integration",
+ }
+ ],
+ }
+ ),
+ )
+
+ result = delete_pattern_cascade(
+ PROJECT,
+ ChangeSet(g_delete_prefix | {"id": pattern_id}),
+ )
+
+ assert patterns.get_pattern(PROJECT, pattern_id) == {}
+ assert demands.get_demand(PROJECT, junction_id)["demands"] == [
+ {"demand": 1.0, "pattern": None, "category": "integration"}
+ ]
+ assert result.operations[-1] == {
+ "operation": "delete",
+ "type": "pattern",
+ "id": pattern_id,
+ }
+ raise RuntimeError("force rollback")
+
+ assert junctions.get_junction(PROJECT, junction_id) == {}
diff --git a/tests/unit/test_analysis_results_repository.py b/tests/unit/test_analysis_results_repository.py
new file mode 100644
index 0000000..48f8afc
--- /dev/null
+++ b/tests/unit/test_analysis_results_repository.py
@@ -0,0 +1,91 @@
+import asyncio
+from datetime import datetime, timezone
+from uuid import uuid4
+
+import pytest
+
+from app.infra.db.timescaledb.repositories.analysis import AnalysisResultsRepository
+
+
+class _FakeCursor:
+ def __init__(self):
+ self.calls = []
+
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, exc_type, exc, tb):
+ return False
+
+ async def execute(self, query, params):
+ self.calls.append((str(query), params))
+
+ async def fetchall(self):
+ return []
+
+
+class _FakeConnection:
+ def __init__(self):
+ self.cursor_instance = _FakeCursor()
+
+ def cursor(self):
+ return self.cursor_instance
+
+
+def test_prepare_simulation_rows_uses_run_timestep():
+ nodes, links = AnalysisResultsRepository.prepare_simulation_rows(
+ [{"node": "J1", "result": [{"pressure": 1.0}, {"pressure": 2.0}]}],
+ [{"link": "P1", "result": [{"flow": 3.0}, {"flow": 4.0}]}],
+ "2026-08-24T08:00:00+08:00",
+ num_periods=2,
+ result_timestep_seconds=900,
+ )
+
+ assert [row["time"] for row in nodes] == [
+ datetime(2026, 8, 24, 0, 0, tzinfo=timezone.utc),
+ datetime(2026, 8, 24, 0, 15, tzinfo=timezone.utc),
+ ]
+ assert nodes[0]["node_id"] == "J1"
+ assert links[1]["link_id"] == "P1"
+
+
+def test_node_series_rejects_unknown_field():
+ with pytest.raises(ValueError, match="invalid node result field"):
+ asyncio.run(
+ AnalysisResultsRepository.get_node_series(
+ _FakeConnection(),
+ uuid4(),
+ "J1",
+ datetime.now(timezone.utc),
+ datetime.now(timezone.utc),
+ "unknown",
+ )
+ )
+
+
+def test_node_series_filters_by_run_and_node():
+ conn = _FakeConnection()
+ run_id = uuid4()
+ start = datetime(2026, 8, 24, tzinfo=timezone.utc)
+ end = datetime(2026, 8, 25, tzinfo=timezone.utc)
+
+ asyncio.run(
+ AnalysisResultsRepository.get_node_series(
+ conn, run_id, "J1", start, end, "pressure"
+ )
+ )
+
+ query, params = conn.cursor_instance.calls[0]
+ assert "analysis.node_results" in query
+ assert params == (run_id, "J1", start, end)
+
+
+def test_analysis_store_locks_run_before_empty_check():
+ cursor = _FakeCursor()
+ run_id = uuid4()
+
+ asyncio.run(AnalysisResultsRepository._lock_run(cursor, run_id))
+
+ query, params = cursor.calls[0]
+ assert "pg_advisory_xact_lock" in query
+ assert params == (run_id,)
diff --git a/tests/unit/test_analysis_simulation.py b/tests/unit/test_analysis_simulation.py
new file mode 100644
index 0000000..d9a16ab
--- /dev/null
+++ b/tests/unit/test_analysis_simulation.py
@@ -0,0 +1,167 @@
+import inspect
+import json
+from uuid import uuid4
+
+
+def test_run_simulation_exposes_explicit_valve_control():
+ from app.services import simulation
+
+ assert "valve_control" in inspect.signature(simulation.run_simulation).parameters
+
+
+def test_apply_valve_control_matches_runner_semantics(monkeypatch):
+ from app.services import simulation
+
+ updates: dict[str, dict] = {}
+ monkeypatch.setattr(
+ simulation,
+ "get_status",
+ lambda project_name, valve_name: {
+ "link": valve_name,
+ "status": "OPEN",
+ "setting": 1.0,
+ },
+ )
+ monkeypatch.setattr(
+ simulation,
+ "set_status",
+ lambda project_name, changeset: updates.update(
+ {changeset.operations[0]["link"]: changeset.operations[0].copy()}
+ ),
+ )
+
+ simulation._apply_valve_control(
+ "demo",
+ {
+ "V-status": {"status": "ACTIVE"},
+ "V-setting": {"setting": 2.5},
+ "V-closed": {"status": "ACTIVE", "setting": 9.0, "k": 0},
+ "V-k": {"status": "ACTIVE", "setting": 9.0, "k": 0.5},
+ },
+ )
+
+ assert updates["V-status"]["status"] == "ACTIVE"
+ assert updates["V-setting"]["setting"] == 2.5
+ assert updates["V-closed"]["status"] == "CLOSED"
+ assert updates["V-k"]["setting"] == 0.1036 * pow(0.5, -3.105)
+
+
+def test_extended_simulation_stores_results_by_run_id(monkeypatch):
+ from app.services import simulation
+
+ run_id = uuid4()
+ storage_calls: list[tuple] = []
+ monkeypatch.setattr(simulation, "open_project", lambda name: None)
+ monkeypatch.setattr(
+ simulation,
+ "get_time",
+ lambda name: {
+ "HYDRAULIC TIMESTEP": "00:15:00",
+ "REPORT TIMESTEP": "1:00",
+ "DURATION": "0:00",
+ "PATTERN START": "0:00",
+ },
+ )
+ monkeypatch.setattr(simulation, "set_time", lambda name, changeset: None)
+ monkeypatch.setattr(
+ simulation,
+ "run_project",
+ lambda name: json.dumps(
+ {
+ "output": {
+ "times": {"num_periods": 2, "report_step": 900},
+ "node_results": [{"node": "J1", "result": [{}, {}]}],
+ "link_results": [{"link": "P1", "result": [{}, {}]}],
+ }
+ }
+ ),
+ )
+ lifecycle_calls: list[tuple] = []
+ monkeypatch.setattr(simulation, "create_analysis_run", lambda **kwargs: run_id)
+ monkeypatch.setattr(
+ simulation,
+ "update_analysis_run",
+ lambda *args, **kwargs: lifecycle_calls.append((args, kwargs)),
+ )
+ monkeypatch.setattr(
+ simulation.TimescaleInternalStorage,
+ "store_analysis_simulation",
+ staticmethod(lambda *args, **kwargs: storage_calls.append((args, kwargs))),
+ )
+
+ returned_run_id = simulation.run_simulation(
+ name="demo",
+ simulation_type="extended",
+ modify_pattern_start_time="2026-07-16T00:00:00+08:00",
+ modify_total_duration=900,
+ scheme_type="burst_analysis",
+ scheme_name="case",
+ )
+
+ args, kwargs = storage_calls[0]
+ assert args[0] == run_id
+ assert args[4:] == (2, 900)
+ assert kwargs["db_name"] == "demo"
+ assert returned_run_id == run_id
+ assert lifecycle_calls[-1][1]["status"] == "completed"
+
+
+def test_extended_simulation_marks_run_failed_when_result_storage_fails(monkeypatch):
+ from app.services import simulation
+
+ run_id = uuid4()
+ lifecycle_calls: list[tuple] = []
+ monkeypatch.setattr(simulation, "open_project", lambda name: None)
+ monkeypatch.setattr(
+ simulation,
+ "get_time",
+ lambda name: {
+ "HYDRAULIC TIMESTEP": "00:15:00",
+ "REPORT TIMESTEP": "1:00",
+ "DURATION": "0:00",
+ "PATTERN START": "0:00",
+ },
+ )
+ monkeypatch.setattr(simulation, "set_time", lambda name, changeset: None)
+ monkeypatch.setattr(
+ simulation,
+ "run_project",
+ lambda name: json.dumps(
+ {
+ "output": {
+ "times": {"num_periods": 1, "report_step": 900},
+ "node_results": [{"node": "J1", "result": [{}]}],
+ "link_results": [{"link": "P1", "result": [{}]}],
+ }
+ }
+ ),
+ )
+ monkeypatch.setattr(simulation, "create_analysis_run", lambda **kwargs: run_id)
+ monkeypatch.setattr(
+ simulation,
+ "update_analysis_run",
+ lambda *args, **kwargs: lifecycle_calls.append((args, kwargs)),
+ )
+
+ def fail_storage(*args, **kwargs):
+ raise RuntimeError("timescale write failed")
+
+ monkeypatch.setattr(
+ simulation.TimescaleInternalStorage,
+ "store_analysis_simulation",
+ staticmethod(fail_storage),
+ )
+
+ import pytest
+
+ with pytest.raises(RuntimeError, match="timescale write failed"):
+ simulation.run_simulation(
+ name="demo",
+ simulation_type="extended",
+ modify_pattern_start_time="2026-07-16T00:00:00+08:00",
+ modify_total_duration=900,
+ scheme_type="burst_analysis",
+ scheme_name="case",
+ )
+
+ assert [call[1]["status"] for call in lifecycle_calls] == ["failed"]
diff --git a/tests/unit/test_burst_location_service.py b/tests/unit/test_burst_location_service.py
index c80f813..99f7ac0 100644
--- a/tests/unit/test_burst_location_service.py
+++ b/tests/unit/test_burst_location_service.py
@@ -1,723 +1,101 @@
-import importlib.util
-import sys
-import types
-from datetime import datetime, timedelta, timezone
-from pathlib import Path
+from datetime import datetime, timezone
+from uuid import uuid4
import pytest
-
-def _load_burst_location_module():
- module_path = (
- Path(__file__).resolve().parents[2] / "app" / "services" / "burst_location.py"
- )
-
- missing = object()
- previous_modules = {}
-
- def install_module(name: str, module: types.ModuleType) -> None:
- previous_modules.setdefault(name, sys.modules.get(name, missing))
- sys.modules[name] = module
-
- def ensure_package(name: str) -> types.ModuleType:
- module = sys.modules.get(name)
- if module is None:
- module = types.ModuleType(name)
- module.__path__ = []
- install_module(name, module)
- return module
-
- for package_name in [
- "app",
- "app.algorithms",
- "app.infra",
- "app.infra.db",
- "app.infra.db.timescaledb",
- "app.services",
- ]:
- ensure_package(package_name)
-
- time_api_module = types.ModuleType("app.services.time_api")
- time_api_module.parse_utc_time = (
- lambda value, field_name="datetime": (
- value.astimezone(timezone.utc)
- if isinstance(value, datetime) and value.tzinfo is not None
- else datetime.fromisoformat(value).astimezone(timezone.utc)
- )
- )
- time_api_module.extract_date = (
- lambda value, field_name="date": (
- value.date()
- if isinstance(value, datetime)
- else datetime.fromisoformat(value).date()
- )
- )
- time_api_module.utc_now = lambda: datetime.now(timezone.utc)
- install_module("app.services.time_api", time_api_module)
-
- algorithms_module = types.ModuleType("app.algorithms.burst_location")
- algorithms_module.run_burst_location = lambda **kwargs: {}
- install_module("app.algorithms.burst_location", algorithms_module)
-
- internal_queries_module = types.ModuleType(
- "app.infra.db.timescaledb.internal_queries"
- )
-
- class DummyInternalQueries:
- @staticmethod
- def query_scada_by_ids_timerange(**kwargs):
- return {}
-
- @staticmethod
- def query_scheme_simulation_by_ids_timerange(**kwargs):
- return {}
-
- @staticmethod
- def query_realtime_simulation_by_ids_timerange(**kwargs):
- return {}
-
- internal_queries_module.InternalQueries = DummyInternalQueries
- install_module(
- "app.infra.db.timescaledb.internal_queries", internal_queries_module
- )
-
- scheme_management_module = types.ModuleType("app.services.scheme_management")
- scheme_management_module.query_burst_location_scheme_detail = lambda *args, **kwargs: {}
- scheme_management_module.query_burst_location_schemes = lambda *args, **kwargs: []
- scheme_management_module.query_scheme_list = lambda *args, **kwargs: []
- scheme_management_module.scheme_name_exists = lambda *args, **kwargs: False
- scheme_management_module.store_scheme_info = lambda *args, **kwargs: None
- install_module("app.services.scheme_management", scheme_management_module)
-
- tjnetwork_module = types.ModuleType("app.services.tjnetwork")
- tjnetwork_module.dump_inp = lambda *args, **kwargs: None
- tjnetwork_module.get_all_scada_info = lambda *args, **kwargs: []
- install_module("app.services.tjnetwork", tjnetwork_module)
-
- module_name = "tests_burst_location_under_test"
- spec = importlib.util.spec_from_file_location(module_name, module_path)
- module = importlib.util.module_from_spec(spec)
- assert spec and spec.loader
- try:
- spec.loader.exec_module(module)
- finally:
- for name, previous in reversed(previous_modules.items()):
- if previous is missing:
- sys.modules.pop(name, None)
- else:
- sys.modules[name] = previous
- return module
+from app.services import burst_location
-def test_run_burst_location_uses_single_timerange_with_burst_source_split(monkeypatch, tmp_path):
- module = _load_burst_location_module()
+START = datetime(2026, 8, 1, tzinfo=timezone.utc)
+END = datetime(2026, 8, 2, tzinfo=timezone.utc)
+
+
+def test_analysis_simulation_query_is_keyed_by_run_id(monkeypatch):
+ run_id = uuid4()
captured = {}
- scheme_calls = []
- realtime_calls = []
- monkeypatch.setattr(
- module,
- "get_all_scada_info",
- lambda network: [
- {
- "type": "pressure",
- "associated_element_id": "J1",
- "api_query_id": "pressure-query",
- },
- {
- "type": "pipe_flow",
- "associated_element_id": "P1",
- "api_query_id": "pipe-flow-query",
- },
- {
- "type": "demand",
- "associated_element_id": "J2",
- "api_query_id": "demand-query",
- },
- ],
- )
- monkeypatch.setattr(module, "_prepare_burst_inp", lambda network: str(tmp_path / "fake.inp"))
-
- def fake_run_burst_location(**kwargs):
+ def fake_query(**kwargs):
captured.update(kwargs)
- return {
- "located_pipe": "Pipe-001",
- "simulation_times": 3,
- "similarity_mode": "combined",
- }
-
- monkeypatch.setattr(module, "run_burst_location", fake_run_burst_location)
-
- def _build_series(start_time: str, values: list[float]) -> list[dict]:
- base_time = datetime.fromisoformat(start_time)
- return [
- {
- "time": (base_time + timedelta(minutes=15 * index)).isoformat(),
- "value": value,
- }
- for index, value in enumerate(values)
- ]
-
- def fake_scheme_query(**kwargs):
- scheme_calls.append(kwargs)
- start_hour = datetime.fromisoformat(kwargs["start_time"]).astimezone(
- timezone(timedelta(hours=8))
- ).hour
- if kwargs["element_type"] == "node" and kwargs["field"] == "pressure":
- values = [12.0, 14.0, 16.0, 18.0] if start_hour == 8 else [8.0, 10.0, 12.0, 14.0]
- return {"J1": _build_series(kwargs["start_time"], values)}
- if kwargs["element_type"] == "link" and kwargs["field"] == "flow":
- values = [5.0, 7.0, 9.0, 11.0] if start_hour == 8 else [2.0, 4.0, 6.0, 8.0]
- return {"P1": _build_series(kwargs["start_time"], values)}
- if kwargs["element_type"] == "node" and kwargs["field"] == "actual_demand":
- values = [3.0, 5.0, 7.0, 9.0] if start_hour == 8 else [1.0, 3.0, 5.0, 7.0]
- return {"J2": _build_series(kwargs["start_time"], values)}
- raise AssertionError(f"Unexpected scheme query: {kwargs}")
-
- def fake_realtime_query(**kwargs):
- realtime_calls.append(kwargs)
- if kwargs["element_type"] == "node" and kwargs["field"] == "pressure":
- return {"J1": _build_series(kwargs["start_time"], [8.0, 10.0, 12.0, 14.0])}
- if kwargs["element_type"] == "link" and kwargs["field"] == "flow":
- return {"P1": _build_series(kwargs["start_time"], [2.0, 4.0, 6.0, 8.0])}
- if kwargs["element_type"] == "node" and kwargs["field"] == "actual_demand":
- return {"J2": _build_series(kwargs["start_time"], [1.0, 3.0, 5.0, 7.0])}
- raise AssertionError(f"Unexpected realtime query: {kwargs}")
+ return {"J1": []}
monkeypatch.setattr(
- module.InternalQueries,
- "query_scheme_simulation_by_ids_timerange",
- staticmethod(fake_scheme_query),
- )
- monkeypatch.setattr(
- module.InternalQueries,
- "query_realtime_simulation_by_ids_timerange",
- staticmethod(fake_realtime_query),
- )
- monkeypatch.setattr(
- module,
- "query_scheme_list",
- lambda name, scheme_type=None: [
- (
- 1,
- "BurstSchemeA",
- "burst_analysis",
- "testuser",
- None,
- None,
- {"burst_ID": ["Pipe-009", "Pipe-010"]},
- )
- ],
+ burst_location.InternalQueries,
+ "query_analysis_simulation_by_ids_timerange",
+ staticmethod(fake_query),
)
- result = module.run_burst_location_by_network(
- network="tjwater",
- username="testuser",
- data_source="simulation",
- simulation_scheme_name="BurstSchemeA",
- simulation_scheme_type="burst_analysis",
- burst_leakage=10.0,
- scada_burst_start=datetime(2025, 1, 1, 8, 0, 0, tzinfo=timezone(timedelta(hours=8))),
- scada_burst_end=datetime(2025, 1, 1, 9, 0, 0, tzinfo=timezone(timedelta(hours=8))),
- use_scada_flow=True,
+ result = burst_location._query_simulation_values(
+ network="demo",
+ element_ids=["J1"],
+ element_type="node",
+ field="pressure",
+ start_dt=START,
+ end_dt=END,
+ simulation_source="analysis",
+ simulation_run_id=run_id,
)
- assert result["observed_source"] == "simulation_scheme_burst_realtime_normal_timerange"
- assert result["simulation_scheme"] == {
- "name": "BurstSchemeA",
- "type": "burst_analysis",
- "burst_ids": ["Pipe-009", "Pipe-010"],
- }
- assert result["pressure_samples"] == {"burst": 4, "normal": 4}
- assert result["flow_samples"] == {"burst": 4, "normal": 4}
- assert captured["visualize_partition"] is False
- assert list(captured["burst_pressure"].index) == ["J1"]
- assert captured["burst_pressure"]["J1"] == pytest.approx(15.0)
- assert captured["normal_pressure"]["J1"] == pytest.approx(11.0)
- assert captured["burst_flow"]["J2"] == pytest.approx(6.0)
- assert captured["burst_flow"]["P1"] == pytest.approx(8.0)
- assert captured["normal_flow"]["J2"] == pytest.approx(4.0)
- assert captured["normal_flow"]["P1"] == pytest.approx(5.0)
- assert all(call["scheme_name"] == "BurstSchemeA" for call in scheme_calls)
- assert len(scheme_calls) == 3
- assert any(call["element_type"] == "node" and call["field"] == "pressure" for call in scheme_calls)
- assert any(call["element_type"] == "link" and call["field"] == "flow" for call in scheme_calls)
- assert any(call["element_type"] == "node" and call["field"] == "actual_demand" for call in scheme_calls)
- assert len(realtime_calls) == 3
- assert any(call["element_type"] == "node" and call["field"] == "pressure" for call in realtime_calls)
- assert any(call["element_type"] == "link" and call["field"] == "flow" for call in realtime_calls)
- assert any(call["element_type"] == "node" and call["field"] == "actual_demand" for call in realtime_calls)
- assert {call["start_time"] for call in scheme_calls + realtime_calls} == {
- "2025-01-01T00:00:00+00:00"
- }
- assert {call["end_time"] for call in scheme_calls + realtime_calls} == {
- "2025-01-01T01:00:00+00:00"
- }
- assert result["scada_window"] == {
- "burst_start": "2025-01-01T00:00:00+00:00",
- "burst_end": "2025-01-01T01:00:00+00:00",
- "normal_start": "2025-01-01T00:00:00+00:00",
- "normal_end": "2025-01-01T01:00:00+00:00",
- }
+ assert result == {"J1": []}
+ assert captured["run_id"] == run_id
+ assert "scheme_name" not in captured
+ assert "scheme_type" not in captured
-def test_run_burst_location_requires_simulation_scheme_name(monkeypatch, tmp_path):
- module = _load_burst_location_module()
- monkeypatch.setattr(
- module,
- "get_all_scada_info",
- lambda network: [
- {
- "type": "pressure",
- "associated_element_id": "J1",
- "api_query_id": "pressure-query",
- }
- ],
- )
- monkeypatch.setattr(module, "_prepare_burst_inp", lambda network: str(tmp_path / "fake.inp"))
- monkeypatch.setattr(module, "run_burst_location", lambda **kwargs: {})
-
- with pytest.raises(ValueError, match="simulation_scheme_name"):
- module.run_burst_location_by_network(
- network="tjwater",
- username="testuser",
- data_source="simulation",
- burst_leakage=1.0,
- scada_burst_start=datetime(2025, 1, 1, 8, 0, 0, tzinfo=timezone(timedelta(hours=8))),
- scada_burst_end=datetime(2025, 1, 1, 9, 0, 0, tzinfo=timezone(timedelta(hours=8))),
+def test_analysis_simulation_query_requires_run_id():
+ with pytest.raises(ValueError, match="simulation_run_id"):
+ burst_location._query_simulation_values(
+ network="demo",
+ element_ids=["J1"],
+ element_type="node",
+ field="pressure",
+ start_dt=START,
+ end_dt=END,
+ simulation_source="analysis",
+ simulation_run_id=None,
)
-def test_build_observed_series_from_simulation_normalizes_result_ids(monkeypatch):
- module = _load_burst_location_module()
- query_calls = []
-
+def test_scada_mapping_uses_canonical_asset_fields(monkeypatch):
monkeypatch.setattr(
- module,
+ burst_location,
"get_all_scada_info",
lambda network: [
{
- "type": "pressure",
- "associated_element_id": " 100026 ",
- "api_query_id": " pressure-query ",
- }
- ],
- )
-
- def fake_scheme_query(**kwargs):
- query_calls.append(kwargs)
- return {
- 100026: [
- {"time": kwargs["start_time"], "value": 10.0},
- {"time": kwargs["end_time"], "value": 14.0},
- ]
- }
-
- monkeypatch.setattr(
- module.InternalQueries,
- "query_scheme_simulation_by_ids_timerange",
- staticmethod(fake_scheme_query),
- )
-
- series, sample_count = module._build_observed_series_from_simulation(
- network="tjwater",
- sensor_ids=["100026"],
- start_dt=datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
- end_dt=datetime(2025, 1, 1, 1, 0, 0, tzinfo=timezone.utc),
- data_type="pressure",
- series_name="burst_pressure",
- simulation_source="scheme",
- simulation_scheme_name="BurstSchemeA",
- simulation_scheme_type="burst_analysis",
- )
-
- assert query_calls[0]["element_ids"] == ["100026"]
- assert sample_count == 2
- assert series["100026"] == pytest.approx(12.0)
-
-
-def test_build_observed_series_from_scada_uses_chinese_error_label(monkeypatch):
- module = _load_burst_location_module()
-
- monkeypatch.setattr(
- module,
- "get_all_scada_info",
- lambda network: [
- {
- "type": "pressure",
- "associated_element_id": "100026",
- "api_query_id": "pressure-query",
- }
- ],
- )
- monkeypatch.setattr(
- module.InternalQueries,
- "query_scada_by_ids_timerange",
- staticmethod(lambda **kwargs: {"pressure-query": []}),
- )
-
- with pytest.raises(ValueError) as exc_info:
- module._build_observed_series_from_scada(
- network="tjwater",
- sensor_ids=["100026"],
- start_dt=datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
- end_dt=datetime(2025, 1, 1, 1, 0, 0, tzinfo=timezone.utc),
- data_type="pressure",
- series_name="burst_pressure",
- )
-
- message = str(exc_info.value)
- assert "爆管压力数据 在时间窗内无有效数据: 100026" in message
- assert "burst_pressure" not in message
-
-
-def test_build_observed_series_from_scada_skips_missing_sensor_values(monkeypatch):
- module = _load_burst_location_module()
-
- monkeypatch.setattr(
- module,
- "get_all_scada_info",
- lambda network: [
- {"type": "pressure", "associated_element_id": "J1", "api_query_id": "q1"},
- {"type": "pressure", "associated_element_id": "J2", "api_query_id": "q2"},
- {"type": "pressure", "associated_element_id": "J3", "api_query_id": "q3"},
- ],
- )
- monkeypatch.setattr(
- module.InternalQueries,
- "query_scada_by_ids_timerange",
- staticmethod(
- lambda **kwargs: {
- "q1": [
- {"time": kwargs["start_time"], "value": 10.0},
- {"time": kwargs["end_time"], "value": 12.0},
- ],
- "q2": [],
- "q3": [
- {"time": kwargs["start_time"], "value": None},
- {"time": kwargs["end_time"], "value": 18.0},
- ],
- }
- ),
- )
-
- series, sample_count = module._build_observed_series_from_scada(
- network="tjwater",
- sensor_ids=["J1", "J2", "J3"],
- start_dt=datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
- end_dt=datetime(2025, 1, 1, 1, 0, 0, tzinfo=timezone.utc),
- data_type="pressure",
- series_name="burst_pressure",
- )
-
- assert list(series.index) == ["J1", "J3"]
- assert series["J1"] == pytest.approx(11.0)
- assert series["J3"] == pytest.approx(18.0)
- assert sample_count == 1
-
-
-def test_run_burst_location_monitoring_uses_scada_for_burst_and_normal(
- monkeypatch, tmp_path
-):
- module = _load_burst_location_module()
- captured = {}
- scada_calls = []
- realtime_calls = []
-
- monkeypatch.setattr(
- module,
- "get_all_scada_info",
- lambda network: [
- {
- "type": "pressure",
- "associated_element_id": "J1",
- "api_query_id": "pressure-query",
- }
- ],
- )
- monkeypatch.setattr(module, "_prepare_burst_inp", lambda network: str(tmp_path / "fake.inp"))
- monkeypatch.setattr(
- module,
- "run_burst_location",
- lambda **kwargs: captured.update(kwargs) or {"located_pipe": "Pipe-001"},
- )
-
- def fake_scada_query(**kwargs):
- scada_calls.append(kwargs)
- start_hour = datetime.fromisoformat(kwargs["start_time"]).astimezone(
- timezone(timedelta(hours=8))
- ).hour
- values = [20.0, 22.0] if start_hour == 8 else [10.0, 12.0]
- return {
- "pressure-query": [
- {"time": kwargs["start_time"], "value": values[0]},
- {"time": kwargs["end_time"], "value": values[1]},
- ]
- }
-
- def fake_realtime_query(**kwargs):
- realtime_calls.append(kwargs)
- return {
- "J1": [
- {"time": kwargs["start_time"], "value": 10.0},
- {"time": kwargs["end_time"], "value": 12.0},
- ]
- }
-
- monkeypatch.setattr(
- module.InternalQueries,
- "query_scada_by_ids_timerange",
- staticmethod(fake_scada_query),
- )
- monkeypatch.setattr(
- module.InternalQueries,
- "query_realtime_simulation_by_ids_timerange",
- staticmethod(fake_realtime_query),
- )
-
- result = module.run_burst_location_by_network(
- network="tjwater",
- username="testuser",
- data_source="monitoring",
- burst_leakage=1.0,
- scada_burst_start=datetime(2025, 1, 1, 8, 0, 0, tzinfo=timezone(timedelta(hours=8))),
- scada_burst_end=datetime(2025, 1, 1, 9, 0, 0, tzinfo=timezone(timedelta(hours=8))),
- scada_normal_start=datetime(2025, 1, 1, 7, 0, 0, tzinfo=timezone(timedelta(hours=8))),
- scada_normal_end=datetime(2025, 1, 1, 8, 0, 0, tzinfo=timezone(timedelta(hours=8))),
- )
-
- assert result["observed_source"] == "scada_burst_scada_normal_timerange"
- assert len(scada_calls) == 2
- assert len(realtime_calls) == 0
- assert captured["burst_pressure"]["J1"] == pytest.approx(21.0)
- assert captured["normal_pressure"]["J1"] == pytest.approx(11.0)
- assert result["scada_window"] == {
- "burst_start": "2025-01-01T00:00:00+00:00",
- "burst_end": "2025-01-01T01:00:00+00:00",
- "normal_start": "2024-12-31T23:00:00+00:00",
- "normal_end": "2025-01-01T00:00:00+00:00",
- }
-
-
-def test_run_burst_location_monitoring_defaults_normal_window_to_previous_day(
- monkeypatch, tmp_path
-):
- module = _load_burst_location_module()
- captured = {}
- scada_calls = []
-
- monkeypatch.setattr(
- module,
- "get_all_scada_info",
- lambda network: [
- {
- "type": "pressure",
- "associated_element_id": "J1",
- "api_query_id": "pressure-query",
- }
- ],
- )
- monkeypatch.setattr(module, "_prepare_burst_inp", lambda network: str(tmp_path / "fake.inp"))
- monkeypatch.setattr(
- module,
- "run_burst_location",
- lambda **kwargs: captured.update(kwargs) or {"located_pipe": "Pipe-001"},
- )
-
- def fake_scada_query(**kwargs):
- scada_calls.append(kwargs)
- start_time = datetime.fromisoformat(kwargs["start_time"])
- values = (
- [20.0, 22.0]
- if start_time.date().isoformat() == "2025-01-01"
- else [10.0, 12.0]
- )
- return {
- "pressure-query": [
- {"time": kwargs["start_time"], "value": values[0]},
- {"time": kwargs["end_time"], "value": values[1]},
- ]
- }
-
- monkeypatch.setattr(
- module.InternalQueries,
- "query_scada_by_ids_timerange",
- staticmethod(fake_scada_query),
- )
- monkeypatch.setattr(
- module.InternalQueries,
- "query_realtime_simulation_by_ids_timerange",
- staticmethod(lambda **kwargs: pytest.fail("monitoring mode must not query realtime simulation")),
- )
-
- result = module.run_burst_location_by_network(
- network="tjwater",
- username="testuser",
- data_source="monitoring",
- burst_leakage=1.0,
- scada_burst_start=datetime(2025, 1, 1, 8, 0, 0, tzinfo=timezone(timedelta(hours=8))),
- scada_burst_end=datetime(2025, 1, 1, 9, 0, 0, tzinfo=timezone(timedelta(hours=8))),
- )
-
- assert result["observed_source"] == "scada_burst_scada_normal_timerange"
- assert len(scada_calls) == 2
- assert datetime.fromisoformat(scada_calls[1]["start_time"]) == (
- datetime.fromisoformat(scada_calls[0]["start_time"]) - timedelta(days=1)
- )
- assert datetime.fromisoformat(scada_calls[1]["end_time"]) == (
- datetime.fromisoformat(scada_calls[0]["end_time"]) - timedelta(days=1)
- )
- assert captured["burst_pressure"]["J1"] == pytest.approx(21.0)
- assert captured["normal_pressure"]["J1"] == pytest.approx(11.0)
- assert result["scada_window"] == {
- "burst_start": "2025-01-01T00:00:00+00:00",
- "burst_end": "2025-01-01T01:00:00+00:00",
- "normal_start": "2024-12-31T00:00:00+00:00",
- "normal_end": "2024-12-31T01:00:00+00:00",
- }
-
-
-def test_run_burst_location_monitoring_flow_uses_previous_day_normal_window(
- monkeypatch, tmp_path
-):
- module = _load_burst_location_module()
- captured = {}
- scada_calls = []
-
- monkeypatch.setattr(
- module,
- "get_all_scada_info",
- lambda network: [
- {
- "type": "pressure",
- "associated_element_id": "J1",
- "api_query_id": "pressure-query",
+ "device_id": "pressure-1",
+ "device_type": "pressure",
+ "node_id": "J1",
+ "link_id": None,
+ "api_query_id": "q-pressure",
},
{
- "type": "pipe_flow",
- "associated_element_id": "P1",
- "api_query_id": "flow-query",
+ "device_id": "flow-1",
+ "device_type": "pipe_flow",
+ "node_id": None,
+ "link_id": "P1",
+ "api_query_id": "q-flow",
},
],
)
- monkeypatch.setattr(module, "_prepare_burst_inp", lambda network: str(tmp_path / "fake.inp"))
+
+ assert burst_location._build_scada_mapping("demo", "pressure") == {
+ "J1": "q-pressure"
+ }
+ assert burst_location._build_scada_mapping("demo", "flow") == {
+ "P1": "q-flow"
+ }
+
+
+def test_burst_ids_are_read_from_analysis_run(monkeypatch):
+ run_id = uuid4()
monkeypatch.setattr(
- module,
- "run_burst_location",
- lambda **kwargs: captured.update(kwargs) or {"located_pipe": "Pipe-001"},
+ burst_location,
+ "get_analysis_run",
+ lambda network, value: {
+ "run_id": value,
+ "parameters": {"burst_ID": ["P1", "P2"]},
+ },
)
- def fake_scada_query(**kwargs):
- scada_calls.append(kwargs)
- is_burst_day = (
- datetime.fromisoformat(kwargs["start_time"]).date().isoformat()
- == "2025-01-01"
- )
- if kwargs["device_ids"] == ["pressure-query"]:
- values = [20.0, 22.0] if is_burst_day else [10.0, 12.0]
- query_id = "pressure-query"
- else:
- values = [7.0, 9.0] if is_burst_day else [3.0, 5.0]
- query_id = "flow-query"
- return {
- query_id: [
- {"time": kwargs["start_time"], "value": values[0]},
- {"time": kwargs["end_time"], "value": values[1]},
- ]
- }
-
- monkeypatch.setattr(
- module.InternalQueries,
- "query_scada_by_ids_timerange",
- staticmethod(fake_scada_query),
- )
-
- result = module.run_burst_location_by_network(
- network="tjwater",
- username="testuser",
- data_source="monitoring",
- burst_leakage=1.0,
- scada_burst_start=datetime(2025, 1, 1, 8, 0, 0, tzinfo=timezone(timedelta(hours=8))),
- scada_burst_end=datetime(2025, 1, 1, 9, 0, 0, tzinfo=timezone(timedelta(hours=8))),
- use_scada_flow=True,
- )
-
- assert result["observed_source"] == "scada_burst_scada_normal_timerange"
- assert len(scada_calls) == 4
- for burst_call, normal_call in [
- (scada_calls[0], scada_calls[1]),
- (scada_calls[2], scada_calls[3]),
- ]:
- assert datetime.fromisoformat(normal_call["start_time"]) == (
- datetime.fromisoformat(burst_call["start_time"]) - timedelta(days=1)
- )
- assert datetime.fromisoformat(normal_call["end_time"]) == (
- datetime.fromisoformat(burst_call["end_time"]) - timedelta(days=1)
- )
- assert captured["burst_pressure"]["J1"] == pytest.approx(21.0)
- assert captured["normal_pressure"]["J1"] == pytest.approx(11.0)
- assert captured["burst_flow"]["P1"] == pytest.approx(8.0)
- assert captured["normal_flow"]["P1"] == pytest.approx(4.0)
-
-
-def test_run_burst_location_monitoring_aligns_partial_scada_data(
- monkeypatch, tmp_path
-):
- module = _load_burst_location_module()
- captured = {}
-
- monkeypatch.setattr(
- module,
- "get_all_scada_info",
- lambda network: [
- {"type": "pressure", "associated_element_id": "J1", "api_query_id": "q1"},
- {"type": "pressure", "associated_element_id": "J2", "api_query_id": "q2"},
- {"type": "pressure", "associated_element_id": "J3", "api_query_id": "q3"},
- ],
- )
- monkeypatch.setattr(module, "_prepare_burst_inp", lambda network: str(tmp_path / "fake.inp"))
- monkeypatch.setattr(
- module,
- "run_burst_location",
- lambda **kwargs: captured.update(kwargs) or {"located_pipe": "Pipe-001"},
- )
-
- def fake_scada_query(**kwargs):
- start_hour = datetime.fromisoformat(kwargs["start_time"]).astimezone(
- timezone(timedelta(hours=8))
- ).hour
- if start_hour == 8:
- return {
- "q1": [{"time": kwargs["start_time"], "value": 20.0}],
- "q2": [{"time": kwargs["start_time"], "value": 30.0}],
- "q3": [],
- }
- return {
- "q1": [{"time": kwargs["start_time"], "value": 10.0}],
- "q2": [],
- "q3": [{"time": kwargs["start_time"], "value": 12.0}],
- }
-
- monkeypatch.setattr(
- module.InternalQueries,
- "query_scada_by_ids_timerange",
- staticmethod(fake_scada_query),
- )
-
- result = module.run_burst_location_by_network(
- network="tjwater",
- username="testuser",
- data_source="monitoring",
- burst_leakage=1.0,
- scada_burst_start=datetime(2025, 1, 1, 8, 0, 0, tzinfo=timezone(timedelta(hours=8))),
- scada_burst_end=datetime(2025, 1, 1, 9, 0, 0, tzinfo=timezone(timedelta(hours=8))),
- scada_normal_start=datetime(2025, 1, 1, 7, 0, 0, tzinfo=timezone(timedelta(hours=8))),
- scada_normal_end=datetime(2025, 1, 1, 8, 0, 0, tzinfo=timezone(timedelta(hours=8))),
- )
-
- assert result["pressure_scada_ids"] == ["J1"]
- assert captured["pressure_scada_ids"] == ["J1"]
- assert list(captured["burst_pressure"].index) == ["J1"]
- assert list(captured["normal_pressure"].index) == ["J1"]
- assert captured["burst_pressure"]["J1"] == pytest.approx(20.0)
- assert captured["normal_pressure"]["J1"] == pytest.approx(10.0)
+ assert burst_location._get_simulation_run_burst_ids(
+ network="demo", run_id=run_id
+ ) == ["P1", "P2"]
diff --git a/tests/unit/test_postgres_scada_repository.py b/tests/unit/test_postgres_scada_repository.py
index d1f4cd7..a0ed677 100644
--- a/tests/unit/test_postgres_scada_repository.py
+++ b/tests/unit/test_postgres_scada_repository.py
@@ -19,15 +19,16 @@ class _FakeCursor:
async def fetchall(self):
return [
{
- "id": " 25470001 ",
- "type": " PRESSURE ",
- "associated_element_id": " J1 ",
+ "device_id": " 25470001 ",
+ "device_type": " PRESSURE ",
+ "node_id": " J1 ",
+ "link_id": None,
"api_query_id": "query-1",
"transmission_mode": "realtime",
"transmission_frequency": None,
- "reliability": "0.95",
- "x_coor": "117.1",
- "y_coor": "32.9",
+ "reliability": "95",
+ "x": "117.1",
+ "y": "32.9",
}
]
@@ -47,16 +48,18 @@ def test_get_scadas_normalizes_id_and_type():
assert result == [
{
- "id": "25470001",
- "type": "pressure",
- "associated_element_id": "J1",
+ "device_id": "25470001",
+ "device_type": "pressure",
+ "node_id": "J1",
+ "link_id": None,
"api_query_id": "query-1",
"transmission_mode": "realtime",
"transmission_frequency": None,
- "reliability": 0.95,
+ "reliability": 95,
"x": 117.1,
"y": 32.9,
}
]
- assert "associated_element_id" in conn.cursor_instance.query
- assert "FROM public.scada_info" in conn.cursor_instance.query
+ assert "node_id" in conn.cursor_instance.query
+ assert "link_id" in conn.cursor_instance.query
+ assert "FROM gis.scada_devices" in conn.cursor_instance.query
diff --git a/tests/unit/test_postgresql_analysis_repository.py b/tests/unit/test_postgresql_analysis_repository.py
new file mode 100644
index 0000000..110ec89
--- /dev/null
+++ b/tests/unit/test_postgresql_analysis_repository.py
@@ -0,0 +1,54 @@
+import asyncio
+from uuid import uuid4
+
+from app.infra.db.postgresql.analysis import AnalysisRepository
+
+
+class _FakeCursor:
+ def __init__(self):
+ self.calls = []
+
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, exc_type, exc, tb):
+ return False
+
+ async def execute(self, query, params):
+ self.calls.append((str(query), params))
+
+ async def fetchall(self):
+ return []
+
+
+class _FakeConnection:
+ def __init__(self):
+ self.cursor_instance = _FakeCursor()
+
+ def cursor(self):
+ return self.cursor_instance
+
+
+def test_list_results_uses_typed_comparison_when_result_type_is_present():
+ conn = _FakeConnection()
+ run_id = uuid4()
+
+ asyncio.run(
+ AnalysisRepository.list_results(conn, run_id, "leakage_identification")
+ )
+
+ query, params = conn.cursor_instance.calls[0]
+ assert "result_type = %s" in query
+ assert "%s IS NULL" not in query
+ assert params == (run_id, "leakage_identification")
+
+
+def test_list_results_omits_result_type_filter_when_not_requested():
+ conn = _FakeConnection()
+ run_id = uuid4()
+
+ asyncio.run(AnalysisRepository.list_results(conn, run_id))
+
+ query, params = conn.cursor_instance.calls[0]
+ assert "result_type = %s" not in query
+ assert params == (run_id,)
diff --git a/tests/unit/test_project_scada_metadata.py b/tests/unit/test_project_scada_metadata.py
index b977838..e1eecf5 100644
--- a/tests/unit/test_project_scada_metadata.py
+++ b/tests/unit/test_project_scada_metadata.py
@@ -1,15 +1,17 @@
import asyncio
from datetime import datetime, timezone
from unittest.mock import AsyncMock
+from uuid import uuid4
from app.api.v1.endpoints import project_data
from app.infra.db.timescaledb import composite_queries
PROJECT_SCADA = {
- "id": "fengyang-pressure-1",
- "type": "pressure",
- "associated_element_id": "J1",
+ "device_id": "fengyang-pressure-1",
+ "device_type": "pressure",
+ "node_id": "J1",
+ "link_id": None,
"api_query_id": "query-1",
"transmission_mode": "realtime",
"transmission_frequency": None,
@@ -42,13 +44,13 @@ def test_realtime_scada_simulation_uses_current_project_metadata(monkeypatch):
composite_queries.CompositeQueries.get_scada_associated_realtime_simulation_data(
object(),
object(),
- [PROJECT_SCADA["id"]],
+ [PROJECT_SCADA["device_id"]],
START_TIME,
END_TIME,
)
)
- assert result[PROJECT_SCADA["id"]][0]["scada_id"] == PROJECT_SCADA["id"]
+ assert result[PROJECT_SCADA["device_id"]][0]["scada_id"] == PROJECT_SCADA["device_id"]
assert query_mock.await_count == 1
assert query_mock.await_args.args[1:] == (
START_TIME,
@@ -58,36 +60,35 @@ def test_realtime_scada_simulation_uses_current_project_metadata(monkeypatch):
)
-def test_scheme_scada_simulation_uses_current_project_metadata(monkeypatch):
+def test_analysis_scada_simulation_uses_current_project_metadata(monkeypatch):
_patch_project_scadas(monkeypatch)
query_mock = AsyncMock(return_value=[{"time": START_TIME, "value": 26.5}])
monkeypatch.setattr(
- composite_queries.SchemeRepository,
- "get_node_field_by_scheme_and_time_range",
+ composite_queries.AnalysisResultsRepository,
+ "get_node_series",
query_mock,
)
result = asyncio.run(
- composite_queries.CompositeQueries.get_scada_associated_scheme_simulation_data(
+ composite_queries.CompositeQueries.get_scada_associated_analysis_simulation_data(
object(),
object(),
- [PROJECT_SCADA["id"]],
+ [PROJECT_SCADA["device_id"]],
START_TIME,
END_TIME,
- "baseline",
- "scheme-1",
+ uuid4(),
)
)
- assert result[PROJECT_SCADA["id"]][0]["scada_id"] == PROJECT_SCADA["id"]
- assert query_mock.await_args.args[5:] == ("J1", "pressure")
+ assert result[PROJECT_SCADA["device_id"]][0]["scada_id"] == PROJECT_SCADA["device_id"]
+ assert query_mock.await_args.args[2:] == ("J1", START_TIME, END_TIME, "pressure")
def test_element_scada_query_uses_current_project_metadata(monkeypatch):
_patch_project_scadas(monkeypatch)
query_mock = AsyncMock(
return_value={
- PROJECT_SCADA["id"]: [{"time": START_TIME, "value": 26.5}]
+ PROJECT_SCADA["device_id"]: [{"time": START_TIME, "value": 26.5}]
}
)
monkeypatch.setattr(
diff --git a/tests/unit/test_realtime_repository.py b/tests/unit/test_realtime_repository.py
index 9559840..f692f94 100644
--- a/tests/unit/test_realtime_repository.py
+++ b/tests/unit/test_realtime_repository.py
@@ -1,4 +1,5 @@
import asyncio
+from contextlib import contextmanager
from datetime import datetime, timezone
from app.infra.db.timescaledb.repositories.realtime import RealtimeRepository
@@ -65,3 +66,38 @@ def test_get_nodes_by_time_range_normalizes_inputs_to_utc():
datetime(2026, 6, 1, 0, 0, tzinfo=timezone.utc),
datetime(2026, 6, 1, 1, 0, tzinfo=timezone.utc),
)
+
+
+class _SyncTransactionConnection:
+ def __init__(self):
+ self.transactions = 0
+
+ @contextmanager
+ def transaction(self):
+ self.transactions += 1
+ yield
+
+
+def test_realtime_node_and_link_replacement_share_outer_transaction(monkeypatch):
+ conn = _SyncTransactionConnection()
+ calls: list[str] = []
+ monkeypatch.setattr(
+ RealtimeRepository,
+ "insert_nodes_batch_sync",
+ lambda _conn, _data: calls.append("nodes"),
+ )
+ monkeypatch.setattr(
+ RealtimeRepository,
+ "insert_links_batch_sync",
+ lambda _conn, _data: calls.append("links"),
+ )
+
+ RealtimeRepository.store_realtime_simulation_result_sync(
+ conn,
+ [{"node": "N1", "result": [{"pressure": 1.0}]}],
+ [{"link": "L1", "result": [{"flow": 2.0}]}],
+ "2026-06-01T00:00:00Z",
+ )
+
+ assert conn.transactions == 1
+ assert calls == ["nodes", "links"]
diff --git a/tests/unit/test_scada_cleaning.py b/tests/unit/test_scada_cleaning.py
index 6a37145..10268fa 100644
--- a/tests/unit/test_scada_cleaning.py
+++ b/tests/unit/test_scada_cleaning.py
@@ -17,7 +17,7 @@ def test_clean_scada_uses_current_project_metadata(monkeypatch):
composite_queries.ScadaInfoRepository,
"get_scadas",
AsyncMock(
- return_value=[{"id": "fengyang-pressure-1", "type": "pressure"}]
+ return_value=[{"device_id": "fengyang-pressure-1", "device_type": "pressure"}]
),
)
monkeypatch.setattr(
@@ -66,7 +66,7 @@ def test_clean_scada_rejects_devices_missing_from_project_metadata(monkeypatch):
monkeypatch.setattr(
composite_queries.ScadaInfoRepository,
"get_scadas",
- AsyncMock(return_value=[{"id": "other-device", "type": "pressure"}]),
+ AsyncMock(return_value=[{"device_id": "other-device", "device_type": "pressure"}]),
)
query_mock = AsyncMock()
monkeypatch.setattr(
@@ -94,7 +94,7 @@ def test_clean_scada_rejects_zero_database_updates(monkeypatch):
composite_queries.ScadaInfoRepository,
"get_scadas",
AsyncMock(
- return_value=[{"id": "fengyang-pressure-1", "type": "pressure"}]
+ return_value=[{"device_id": "fengyang-pressure-1", "device_type": "pressure"}]
),
)
monkeypatch.setattr(
@@ -141,7 +141,7 @@ def test_clean_scada_propagates_write_failures(monkeypatch):
composite_queries.ScadaInfoRepository,
"get_scadas",
AsyncMock(
- return_value=[{"id": "fengyang-pressure-1", "type": "pressure"}]
+ return_value=[{"device_id": "fengyang-pressure-1", "device_type": "pressure"}]
),
)
monkeypatch.setattr(
diff --git a/tests/unit/test_scada_repository.py b/tests/unit/test_scada_repository.py
index c8a01e3..36f9077 100644
--- a/tests/unit/test_scada_repository.py
+++ b/tests/unit/test_scada_repository.py
@@ -65,8 +65,8 @@ def test_update_scada_field_inserts_when_update_hits_no_rows():
)
assert len(conn.cursor_instance.calls) == 2
- assert "UPDATE scada.scada_data SET" in conn.cursor_instance.calls[0][0]
- assert "INSERT INTO scada.scada_data" in conn.cursor_instance.calls[1][0]
+ assert "UPDATE scada.measurements SET" in conn.cursor_instance.calls[0][0]
+ assert "INSERT INTO scada.measurements" in conn.cursor_instance.calls[1][0]
def test_update_scada_field_skips_insert_when_update_succeeds():
@@ -85,4 +85,4 @@ def test_update_scada_field_skips_insert_when_update_succeeds():
)
assert len(conn.cursor_instance.calls) == 1
- assert "UPDATE scada.scada_data SET" in conn.cursor_instance.calls[0][0]
+ assert "UPDATE scada.measurements SET" in conn.cursor_instance.calls[0][0]
diff --git a/tests/unit/test_scheme_list_filter.py b/tests/unit/test_scheme_list_filter.py
deleted file mode 100644
index 8631396..0000000
--- a/tests/unit/test_scheme_list_filter.py
+++ /dev/null
@@ -1,135 +0,0 @@
-from app.services import scheme_management, tjnetwork
-
-
-class _FakeCursor:
- def __init__(self):
- self.calls = []
-
- def __enter__(self):
- return self
-
- def __exit__(self, *_exc_info):
- return False
-
- def execute(self, statement, params=None):
- self.calls.append((str(statement), params))
-
- def fetchall(self):
- return []
-
-
-class _FakeConnection:
- def __init__(self, cursor):
- self._cursor = cursor
-
- def __enter__(self):
- return self
-
- def __exit__(self, *_exc_info):
- return False
-
- def cursor(self):
- return self._cursor
-
-
-def test_query_scheme_list_pushes_scheme_type_into_sql(monkeypatch):
- cursor = _FakeCursor()
- monkeypatch.setattr(
- scheme_management,
- "get_project_pgconn_string",
- lambda db_name=None: "postgres://test",
- )
- monkeypatch.setattr(
- scheme_management.psycopg, "connect", lambda _conn_string: _FakeConnection(cursor)
- )
-
- assert scheme_management.query_scheme_list("demo", scheme_type="burst_analysis") == []
-
- statement, params = cursor.calls[0]
- assert "WHERE scheme_type = %s" in statement
- assert params == ("burst_analysis",)
-
-
-def test_get_all_schemes_filters_central_scheme_list_by_type(monkeypatch):
- captured = {}
-
- def fake_query_scheme_list(name, scheme_type=None, query_date=None):
- captured["name"] = name
- captured["scheme_type"] = scheme_type
- captured["query_date"] = query_date
- return [
- (
- 7,
- "burst_case",
- "burst_analysis",
- "alice",
- "2026-01-01T00:00:00+08:00",
- "2026-01-01T01:00:00+08:00",
- {"burst_ID": ["P1"]},
- )
- ]
-
- monkeypatch.setattr(
- scheme_management, "query_scheme_list", fake_query_scheme_list
- )
-
- result = tjnetwork.get_all_schemes("demo", scheme_type="burst_analysis")
-
- assert captured == {
- "name": "demo",
- "scheme_type": "burst_analysis",
- "query_date": None,
- }
- assert result == [
- {
- "scheme_id": 7,
- "scheme_name": "burst_case",
- "scheme_type": "burst_analysis",
- "username": "alice",
- "create_time": "2026-01-01T00:00:00+08:00",
- "scheme_start_time": "2026-01-01T01:00:00+08:00",
- "scheme_detail": {"burst_ID": ["P1"]},
- }
- ]
-
-
-def test_query_scheme_detail_rejects_wrong_specialized_type(monkeypatch):
- monkeypatch.setattr(
- scheme_management,
- "query_burst_detection_scheme_detail",
- lambda name, scheme_name: {
- "scheme_name": scheme_name,
- "scheme_type": "burst_analysis",
- "network": name,
- },
- )
-
- assert (
- scheme_management.query_scheme_detail(
- "demo",
- "same_name",
- scheme_type="burst_detection",
- )
- == {}
- )
-
-
-def test_query_scheme_detail_rejects_wrong_network(monkeypatch):
- monkeypatch.setattr(
- scheme_management,
- "query_burst_location_scheme_detail",
- lambda name, scheme_name: {
- "scheme_name": scheme_name,
- "scheme_type": "burst_location",
- "network": "other_network",
- },
- )
-
- assert (
- scheme_management.query_scheme_detail(
- "demo",
- "same_name",
- scheme_type="burst_location",
- )
- == {}
- )
diff --git a/tests/unit/test_scheme_management_lifecycle.py b/tests/unit/test_scheme_management_lifecycle.py
new file mode 100644
index 0000000..da2ed87
--- /dev/null
+++ b/tests/unit/test_scheme_management_lifecycle.py
@@ -0,0 +1,62 @@
+from unittest.mock import MagicMock
+
+from app.services import scheme_management
+
+
+def _mock_connection(monkeypatch):
+ cursor = MagicMock()
+ cursor.rowcount = 1
+ connection = MagicMock()
+ connection.cursor.return_value.__enter__.return_value = cursor
+ context = MagicMock()
+ context.__enter__.return_value = connection
+ monkeypatch.setattr(scheme_management, "project_connection", lambda _name: context)
+ return cursor
+
+
+def test_repeated_analysis_names_create_distinct_run_ids(monkeypatch) -> None:
+ cursor = _mock_connection(monkeypatch)
+ arguments = {
+ "name": "tjwater_next",
+ "scheme_name": "same-window",
+ "scheme_type": "burst_analysis",
+ "username": "alice",
+ "scheme_start_time": "2026-08-24T00:00:00Z",
+ "scheme_detail": {},
+ }
+
+ first = scheme_management.create_analysis_run(**arguments)
+ second = scheme_management.create_analysis_run(**arguments)
+
+ assert first != second
+ assert cursor.execute.call_count == 2
+ assert all(
+ "insert into analysis.runs" in call.args[0]
+ for call in cursor.execute.call_args_list
+ )
+
+
+def test_update_analysis_run_targets_execution_id(monkeypatch) -> None:
+ cursor = _mock_connection(monkeypatch)
+ run_id = scheme_management.create_analysis_run(
+ name="tjwater_next",
+ scheme_name="run",
+ scheme_type="burst_analysis",
+ username="alice",
+ scheme_start_time="2026-08-24T00:00:00Z",
+ scheme_detail={},
+ )
+ cursor.reset_mock()
+
+ scheme_management.update_analysis_run(
+ "tjwater_next",
+ run_id,
+ status="completed",
+ username="alice",
+ scheme_detail={"window": "24h"},
+ )
+
+ statement, params = cursor.execute.call_args.args
+ assert "where run_id = %s" in statement
+ assert params[-1] == run_id
+ assert params[1] == "completed"
diff --git a/tests/unit/test_scheme_simulation_timestep.py b/tests/unit/test_scheme_simulation_timestep.py
deleted file mode 100644
index 8057b0a..0000000
--- a/tests/unit/test_scheme_simulation_timestep.py
+++ /dev/null
@@ -1,219 +0,0 @@
-import inspect
-import json
-from datetime import timedelta
-
-import pytest
-
-from app.infra.db.timescaledb.repositories.scheme import SchemeRepository
-from app.services.time_api import parse_utc_time
-
-
-def test_run_simulation_exposes_explicit_valve_control():
- from app.services import simulation
-
- parameters = inspect.signature(simulation.run_simulation).parameters
-
- assert "valve_control" in parameters
-
-
-def test_apply_valve_control_matches_run_simulation_ex_semantics(monkeypatch):
- from app.services import simulation
-
- updates: dict[str, dict] = {}
-
- monkeypatch.setattr(
- simulation,
- "get_status",
- lambda project_name, valve_name: {
- "link": valve_name,
- "status": "OPEN",
- "setting": 1.0,
- },
- )
- monkeypatch.setattr(
- simulation,
- "set_status",
- lambda project_name, changeset: updates.update(
- {
- changeset.operations[0]["link"]: changeset.operations[0].copy()
- }
- ),
- )
-
- simulation._apply_valve_control(
- "demo",
- {
- "V-status": {"status": "ACTIVE"},
- "V-setting": {"setting": 2.5},
- "V-closed": {"status": "ACTIVE", "setting": 9.0, "k": 0},
- "V-k": {"status": "ACTIVE", "setting": 9.0, "k": 0.5},
- },
- )
-
- assert updates["V-status"] == {
- "link": "V-status",
- "status": "ACTIVE",
- "setting": 1.0,
- }
- assert updates["V-setting"] == {
- "link": "V-setting",
- "status": "OPEN",
- "setting": 2.5,
- }
- assert updates["V-closed"] == {
- "link": "V-closed",
- "status": "CLOSED",
- "setting": 9.0,
- }
- assert updates["V-k"] == {
- "link": "V-k",
- "status": "ACTIVE",
- "setting": 0.1036 * pow(0.5, -3.105),
- }
-
-
-def _node_result(periods: int) -> list[dict]:
- return [
- {
- "node": "J1",
- "result": [
- {"demand": index, "head": index, "pressure": index, "quality": index}
- for index in range(periods)
- ],
- }
- ]
-
-
-def _link_result(periods: int) -> list[dict]:
- return [
- {
- "link": "P1",
- "result": [
- {
- "flow": index,
- "friction": index,
- "headloss": index,
- "quality": index,
- "reaction": index,
- "setting": index,
- "status": index,
- "velocity": index,
- }
- for index in range(periods)
- ],
- }
- ]
-
-
-def test_store_scheme_simulation_uses_15_minute_report_step(monkeypatch):
- inserted: dict[str, list[dict]] = {}
-
- monkeypatch.setattr(
- SchemeRepository,
- "insert_nodes_batch_sync",
- staticmethod(lambda conn, data: inserted.setdefault("nodes", data)),
- )
- monkeypatch.setattr(
- SchemeRepository,
- "insert_links_batch_sync",
- staticmethod(lambda conn, data: inserted.setdefault("links", data)),
- )
-
- SchemeRepository.store_scheme_simulation_result_sync(
- conn=object(),
- scheme_type="burst_analysis",
- scheme_name="five_hour_case",
- node_result_list=_node_result(21),
- link_result_list=_link_result(21),
- result_start_time="2026-07-16T00:00:00Z",
- num_periods=21,
- result_timestep_seconds=900,
- )
-
- start_time = parse_utc_time("2026-07-16T00:00:00Z")
- assert len(inserted["nodes"]) == 21
- assert inserted["nodes"][0]["time"] == start_time
- assert inserted["nodes"][-1]["time"] == start_time + timedelta(hours=5)
- assert inserted["links"][-1]["time"] == start_time + timedelta(hours=5)
-
-
-def test_store_scheme_simulation_uses_hourly_report_step(monkeypatch):
- inserted: dict[str, list[dict]] = {}
-
- monkeypatch.setattr(
- SchemeRepository,
- "insert_nodes_batch_sync",
- staticmethod(lambda conn, data: inserted.setdefault("nodes", data)),
- )
- monkeypatch.setattr(
- SchemeRepository,
- "insert_links_batch_sync",
- staticmethod(lambda conn, data: inserted.setdefault("links", data)),
- )
-
- SchemeRepository.store_scheme_simulation_result_sync(
- conn=object(),
- scheme_type="burst_analysis",
- scheme_name="hourly_case",
- node_result_list=_node_result(6),
- link_result_list=_link_result(6),
- result_start_time="2026-07-16T00:00:00Z",
- num_periods=6,
- result_timestep_seconds=3600,
- )
-
- start_time = parse_utc_time("2026-07-16T00:00:00Z")
- assert [item["time"] for item in inserted["nodes"]] == [
- start_time + timedelta(hours=index) for index in range(6)
- ]
-
-
-def test_run_simulation_passes_report_step_for_extended_scheme(monkeypatch):
- import app.services.simulation as simulation
-
- time_updates: list[dict] = []
- storage_calls: list[tuple] = []
-
- monkeypatch.setattr(simulation, "open_project", lambda name: None)
- monkeypatch.setattr(
- simulation,
- "get_time",
- lambda name: {
- "HYDRAULIC TIMESTEP": "00:15:00",
- "REPORT TIMESTEP": "1:00",
- "DURATION": "0:00",
- "PATTERN START": "0:00",
- },
- )
- monkeypatch.setattr(
- simulation,
- "set_time",
- lambda name, changeset: time_updates.append(changeset.operations[0]),
- )
- monkeypatch.setattr(simulation, "run_project", lambda name: json.dumps({
- "simulation_result": "successful",
- "output": {
- "times": {"num_periods": 21, "report_step": 900},
- "node_results": _node_result(21),
- "link_results": _link_result(21),
- },
- }))
- monkeypatch.setattr(
- simulation.TimescaleInternalStorage,
- "store_scheme_simulation",
- staticmethod(lambda *args, **kwargs: storage_calls.append((args, kwargs))),
- )
-
- simulation.run_simulation(
- name="fengyang",
- simulation_type="extended",
- modify_pattern_start_time="2026-07-16T00:00:00+08:00",
- modify_total_duration=18000,
- scheme_type="burst_analysis",
- scheme_name="five_hour_case",
- )
-
- assert time_updates[0]["DURATION"] == "05:00:00"
- assert time_updates[0]["REPORT TIMESTEP"] == "1:00"
- assert storage_calls[0][0][5] == 21
- assert storage_calls[0][0][6] == 900
diff --git a/tests/unit/test_sensor_placement_service.py b/tests/unit/test_sensor_placement_service.py
index e14e06b..01884d6 100644
--- a/tests/unit/test_sensor_placement_service.py
+++ b/tests/unit/test_sensor_placement_service.py
@@ -1,10 +1,11 @@
from datetime import datetime, timezone
from unittest.mock import MagicMock
+from uuid import uuid4
import pytest
from openpyxl import load_workbook
-from app.native.wndb import s42_sensor_placement
+from app.infra.db.postgresql import sensor_placement as sensor_placement_repository
from app.services import sensor_placement
@@ -15,180 +16,121 @@ def _mock_project_cursor(monkeypatch):
connection_context = MagicMock()
connection_context.__enter__.return_value = connection
monkeypatch.setattr(
- s42_sensor_placement,
+ sensor_placement_repository,
"project_connection",
lambda _network: connection_context,
)
return cursor
-def test_build_workbook_contains_engineering_columns(monkeypatch):
- monkeypatch.setattr(
- sensor_placement,
- "_sensor_points",
- lambda network, locations: [
- {
- "node_id": "J1",
- "max_pipe_diameter": 400.0,
- "project_x": 13500000.0,
- "project_y": 3600000.0,
- "map_x": 13500000.0,
- "map_y": 3600000.0,
- "longitude": 121.0,
- "latitude": 31.0,
- "elevation": 4.5,
- }
- ],
- )
- scheme = {
- "id": 7,
- "scheme_name": "北区测压点",
- "sensor_number": 2,
+def _run() -> dict:
+ return {
+ "run_id": uuid4(),
+ "name": "北区测压点",
+ "sensor_count": 2,
"min_diameter": 300,
- "username": "alice",
- "create_time": datetime(2026, 7, 30, tzinfo=timezone.utc),
- "sensor_location": ["J1", "J2"],
+ "created_by": "alice",
+ "created_at": datetime(2026, 7, 30, tzinfo=timezone.utc),
+ "status": "completed",
+ "sensor_locations": ["J1", "J2"],
}
+
+def _point() -> dict:
+ return {
+ "node_id": "J1",
+ "max_pipe_diameter": 400.0,
+ "project_x": 3038.94,
+ "project_y": -34446.59,
+ "map_x": 13525191.530279,
+ "map_y": 3622984.760237,
+ "longitude": 121.498863,
+ "latitude": 30.924784,
+ "elevation": 4.5,
+ }
+
+
+def test_build_workbook_uses_analysis_run_metadata(monkeypatch):
+ monkeypatch.setattr(sensor_placement, "_sensor_points", lambda *_: [_point()])
output = sensor_placement.build_sensor_placement_workbook(
network="tjwater",
- scheme=scheme,
+ scheme=_run(),
sensor_location=["J1"],
adjustment_status={"J1": "replaced"},
)
workbook = load_workbook(output)
assert workbook.sheetnames == ["方案信息", "监测点清单"]
- headers = [cell.value for cell in workbook["监测点清单"][1]]
- assert headers == [
- "序号",
- "节点 ID",
- "经度",
- "纬度",
- "工程 X",
- "工程 Y",
- "地图 X",
- "地图 Y",
- "高程",
- "调整状态",
- ]
assert workbook["监测点清单"]["J2"].value == "替换"
assert workbook["方案信息"]["B8"].value == "未保存草稿"
-def test_candidate_keeps_engineering_coordinates_and_transforms_map_coordinates(
- monkeypatch,
-):
+def test_candidate_transforms_map_coordinates(monkeypatch):
+ row = _point().copy()
+ row.pop("longitude")
+ row.pop("latitude")
monkeypatch.setattr(
- sensor_placement.wndb,
+ sensor_placement.sensor_placement_repository,
"get_sensor_placement_nodes",
- lambda network, node_ids: [
- {
- "node_id": "J1",
- "max_pipe_diameter": 400.0,
- "project_x": 3038.94,
- "project_y": -34446.59,
- "map_x": 13525191.530279,
- "map_y": 3622984.760237,
- "elevation": 4.5,
- }
- ],
+ lambda network, node_ids: [row],
)
point = sensor_placement.get_sensor_placement_candidate("tjwater", "J1")
assert point["project_x"] == 3038.94
- assert point["project_y"] == -34446.59
- assert point["max_pipe_diameter"] == 400.0
assert point["longitude"] == pytest.approx(121.498863, abs=1e-6)
- assert point["latitude"] == pytest.approx(30.924784, abs=1e-6)
def test_update_validates_nodes_before_write(monkeypatch):
monkeypatch.setattr(
- sensor_placement.wndb,
+ sensor_placement.sensor_placement_repository,
"get_sensor_placement_nodes",
lambda network, node_ids: [],
)
- try:
- sensor_placement.update_sensor_placement_scheme(
+ with pytest.raises(sensor_placement.SensorPlacementValidationError, match="missing"):
+ sensor_placement.update_sensor_placement_run(
"tjwater",
- 7,
- expected_sensor_location=["J1"],
- sensor_location=["missing"],
+ uuid4(),
+ expected_sensor_locations=["J1"],
+ sensor_locations=["missing"],
)
- except sensor_placement.SensorPlacementValidationError as exc:
- assert "missing" in str(exc)
- else:
- raise AssertionError("expected invalid node to be rejected")
-def test_sensor_nodes_use_materialized_web_mercator_geometry(monkeypatch):
+def test_sensor_nodes_query_uses_new_network_and_gis_schemas(monkeypatch):
cursor = _mock_project_cursor(monkeypatch)
cursor.fetchall.return_value = []
- s42_sensor_placement.get_sensor_placement_nodes("tjwater", ["J1"])
+ sensor_placement_repository.get_sensor_placement_nodes("tjwater", ["J1"])
query = cursor.execute.call_args.args[0]
- assert "geo_junctions_mat" in query
- assert "ST_X(c.coord)" in query
- assert "ST_Y(c.coord)" in query
- assert "ST_X(gj.geom)" in query
- assert "ST_Y(gj.geom)" in query
- assert "MAX(diameter) AS max_pipe_diameter" in query
+ assert "network.pipes" in query
+ assert "network.links" in query
+ assert "gis.node_geometries" in query
+ assert "ST_Transform(g.geom, 3857)" in query
assert cursor.execute.call_args.args[1] == (["J1"], ["J1"], ["J1"])
-def test_workbook_escapes_formula_in_scheme_metadata(monkeypatch):
- monkeypatch.setattr(
- sensor_placement,
- "_sensor_points",
- lambda network, locations: [
- {
- "node_id": "J1",
- "max_pipe_diameter": 400.0,
- "project_x": 3038.94,
- "project_y": -34446.59,
- "map_x": 13525191.53,
- "map_y": 3622984.76,
- "longitude": 121.49,
- "latitude": 30.92,
- "elevation": 4.5,
- }
- ],
- )
- output = sensor_placement.build_sensor_placement_workbook(
- network="tjwater",
- scheme={
- "scheme_name": "=1+1",
- "sensor_location": ["J1"],
- "min_diameter": 300,
- "username": "alice",
- "create_time": datetime(2026, 7, 30, tzinfo=timezone.utc),
- },
- sensor_location=["J1"],
- adjustment_status={},
- )
-
- workbook = load_workbook(output, data_only=False)
- assert workbook["方案信息"]["B2"].value == "'=1+1"
- assert workbook["方案信息"]["B2"].data_type == "s"
-
-
-def test_create_sensor_placement_returns_inserted_record(monkeypatch):
+def test_create_sensor_placement_writes_run_and_result_atomically(monkeypatch):
cursor = _mock_project_cursor(monkeypatch)
- cursor.fetchone.return_value = {"id": 7, "sensor_location": ["J1", "J2"]}
+ created_at = datetime(2026, 8, 24, tzinfo=timezone.utc)
+ cursor.fetchone.return_value = {
+ "run_id": uuid4(),
+ "name": "北区测压点",
+ "created_by": "alice",
+ "created_at": created_at,
+ "status": "completed",
+ }
- created = s42_sensor_placement.create_sensor_placement(
+ created = sensor_placement_repository.create_sensor_placement(
"tjwater",
- scheme_name="北区测压点",
+ run_name="北区测压点",
min_diameter=300,
- username="alice",
- sensor_location=["J1", "J2"],
+ created_by="alice",
+ sensor_locations=["J1", "J2"],
)
- assert created["id"] == 7
- query, parameters = cursor.execute.call_args.args
- assert "INSERT INTO sensor_placement" in query
- assert parameters == ("北区测压点", 2, 300, "alice", ["J1", "J2"])
+ assert created["sensor_count"] == 2
+ statements = [call.args[0] for call in cursor.execute.call_args_list]
+ assert "INSERT INTO analysis.runs" in statements[0]
+ assert "INSERT INTO analysis.results" in statements[1]
diff --git a/tests/unit/test_timescale_sync_pool.py b/tests/unit/test_timescale_sync_pool.py
new file mode 100644
index 0000000..a3f345c
--- /dev/null
+++ b/tests/unit/test_timescale_sync_pool.py
@@ -0,0 +1,152 @@
+from contextlib import contextmanager
+
+import pytest
+
+from app.infra.db.timescaledb import sync_pool
+
+
+class _FakePool:
+ def __init__(self, *, conninfo, **_kwargs):
+ self.conninfo = conninfo
+ self.closed = False
+ self.borrowed = 0
+ self.returned = 0
+
+ def close(self):
+ self.closed = True
+
+ @contextmanager
+ def connection(self):
+ self.borrowed += 1
+ try:
+ yield object()
+ finally:
+ self.returned += 1
+
+
+@pytest.fixture(autouse=True)
+def clear_pools():
+ sync_pool._pools.clear()
+ sync_pool._pool_conninfo.clear()
+ sync_pool._pool_borrows.clear()
+ yield
+ sync_pool._pools.clear()
+ sync_pool._pool_conninfo.clear()
+ sync_pool._pool_borrows.clear()
+
+
+def test_pool_reuses_same_routed_timescale_dsn(monkeypatch):
+ monkeypatch.setattr(sync_pool, "ConnectionPool", _FakePool)
+ monkeypatch.setattr(
+ sync_pool,
+ "get_project_timescale_pgconn_string",
+ lambda *, db_name: f"dbname={db_name}",
+ )
+
+ first = sync_pool.get_timescale_pool("tjwater_next")
+ second = sync_pool.get_timescale_pool("tjwater_next")
+
+ assert first is second
+
+
+def test_pool_rebuilds_after_routing_change(monkeypatch):
+ monkeypatch.setattr(sync_pool, "ConnectionPool", _FakePool)
+ dsn = {"value": "host=old dbname=tjwater_next"}
+ monkeypatch.setattr(
+ sync_pool,
+ "get_project_timescale_pgconn_string",
+ lambda *, db_name: dsn["value"],
+ )
+
+ old = sync_pool.get_timescale_pool("tjwater_next")
+ dsn["value"] = "host=new dbname=tjwater_next"
+ new = sync_pool.get_timescale_pool("tjwater_next")
+
+ assert old.closed is True
+ assert new is not old
+
+
+def test_connection_is_returned_to_pool(monkeypatch):
+ monkeypatch.setattr(sync_pool, "ConnectionPool", _FakePool)
+ monkeypatch.setattr(
+ sync_pool,
+ "get_project_timescale_pgconn_string",
+ lambda *, db_name: f"dbname={db_name}",
+ )
+
+ pool = sync_pool.get_timescale_pool("tjwater_next")
+ with sync_pool.timescale_connection("tjwater_next"):
+ assert pool.borrowed == 1
+ assert pool.returned == 0
+ assert pool.returned == 1
+
+
+def test_close_removes_pool(monkeypatch):
+ monkeypatch.setattr(sync_pool, "ConnectionPool", _FakePool)
+ monkeypatch.setattr(
+ sync_pool,
+ "get_project_timescale_pgconn_string",
+ lambda *, db_name: f"dbname={db_name}",
+ )
+
+ pool = sync_pool.get_timescale_pool("tjwater_next")
+ sync_pool.close_timescale_pool("tjwater_next")
+
+ assert pool.closed is True
+ assert "tjwater_next" not in sync_pool._pools
+
+
+def test_close_all_removes_every_pool(monkeypatch):
+ monkeypatch.setattr(sync_pool, "ConnectionPool", _FakePool)
+ monkeypatch.setattr(
+ sync_pool,
+ "get_project_timescale_pgconn_string",
+ lambda *, db_name: f"dbname={db_name}",
+ )
+
+ first = sync_pool.get_timescale_pool("first")
+ second = sync_pool.get_timescale_pool("second")
+ sync_pool.close_all_timescale_pools()
+
+ assert first.closed is True
+ assert second.closed is True
+ assert sync_pool._pools == {}
+
+
+def test_pool_cache_evicts_least_recently_used_idle_pool(monkeypatch):
+ monkeypatch.setattr(sync_pool, "ConnectionPool", _FakePool)
+ monkeypatch.setattr(sync_pool.settings, "PROJECT_TS_CACHE_SIZE", 2)
+ monkeypatch.setattr(
+ sync_pool,
+ "get_project_timescale_pgconn_string",
+ lambda *, db_name: f"dbname={db_name}",
+ )
+
+ first = sync_pool.get_timescale_pool("first")
+ second = sync_pool.get_timescale_pool("second")
+ sync_pool.get_timescale_pool("first")
+ third = sync_pool.get_timescale_pool("third")
+
+ assert list(sync_pool._pools) == ["first", "third"]
+ assert second.closed is True
+ assert first.closed is False
+ assert third.closed is False
+
+
+def test_pool_cache_does_not_evict_active_pool(monkeypatch):
+ monkeypatch.setattr(sync_pool, "ConnectionPool", _FakePool)
+ monkeypatch.setattr(sync_pool.settings, "PROJECT_TS_CACHE_SIZE", 1)
+ monkeypatch.setattr(
+ sync_pool,
+ "get_project_timescale_pgconn_string",
+ lambda *, db_name: f"dbname={db_name}",
+ )
+
+ with sync_pool.timescale_connection("active"):
+ active = sync_pool._pools["active"]
+ sync_pool.get_timescale_pool("new")
+ assert active.closed is False
+ assert set(sync_pool._pools) == {"active", "new"}
+
+ assert list(sync_pool._pools) == ["new"]
+ assert active.closed is True
diff --git a/tests/unit/test_wndb_batch_transactions.py b/tests/unit/test_wndb_batch_transactions.py
new file mode 100644
index 0000000..b192276
--- /dev/null
+++ b/tests/unit/test_wndb_batch_transactions.py
@@ -0,0 +1,83 @@
+from contextlib import contextmanager
+
+import pytest
+
+from app.native.wndb.commands import executor
+from app.native.wndb.core.database import ChangeSet
+
+
+def test_batch_commits_before_materialized_view_refresh(monkeypatch) -> None:
+ events: list[str] = []
+
+ @contextmanager
+ def fake_transaction(_name: str):
+ events.append("transaction-enter")
+ yield object()
+ events.append("transaction-exit")
+
+ monkeypatch.setattr(executor, "project_transaction", fake_transaction)
+ monkeypatch.setattr(
+ executor,
+ "expand_command",
+ lambda _name, change_set: change_set,
+ )
+ monkeypatch.setattr(
+ executor,
+ "_execute_update_command",
+ lambda _name, _change_set: events.append("write") or ChangeSet(),
+ )
+ monkeypatch.setattr(
+ executor,
+ "refresh_materialized_views",
+ lambda _name: events.append("refresh"),
+ )
+
+ executor.execute_batch_commands(
+ "project_a",
+ ChangeSet({"operation": "update", "type": "junction", "id": "J1"}),
+ )
+
+ assert events == [
+ "transaction-enter",
+ "write",
+ "transaction-exit",
+ "refresh",
+ ]
+
+
+def test_failed_batch_does_not_refresh_materialized_views(monkeypatch) -> None:
+ events: list[str] = []
+
+ @contextmanager
+ def fake_transaction(_name: str):
+ events.append("transaction-enter")
+ try:
+ yield object()
+ finally:
+ events.append("transaction-exit")
+
+ monkeypatch.setattr(executor, "project_transaction", fake_transaction)
+ monkeypatch.setattr(
+ executor,
+ "expand_command",
+ lambda _name, change_set: change_set,
+ )
+
+ def fail_write(_name, _change_set):
+ events.append("write")
+ raise RuntimeError("write failed")
+
+ monkeypatch.setattr(executor, "_execute_update_command", fail_write)
+ monkeypatch.setattr(
+ executor,
+ "refresh_materialized_views",
+ lambda _name: events.append("refresh"),
+ )
+
+ with pytest.raises(RuntimeError, match="write failed"):
+ executor.execute_batch_commands(
+ "project_a",
+ ChangeSet({"operation": "update", "type": "junction", "id": "J1"}),
+ )
+
+ assert events == ["transaction-enter", "write", "transaction-exit"]
diff --git a/tests/unit/test_wndb_connection.py b/tests/unit/test_wndb_connection.py
index 0db0002..a61c9ad 100644
--- a/tests/unit/test_wndb_connection.py
+++ b/tests/unit/test_wndb_connection.py
@@ -1,145 +1,228 @@
+from contextlib import contextmanager
+
import pytest
-from app.native.wndb import connection
-from app.native.wndb import database
-from app.native.wndb import project
+from app.native.wndb.core import connection
-class _FakeCursor:
- def __init__(self, connection):
- self.connection = connection
+class _FakePool:
+ def __init__(self, *, conninfo, **_kwargs):
+ self.conninfo = conninfo
+ self.closed = False
+ self.borrowed = 0
+ self.returned = 0
- def __enter__(self):
- return self
+ def close(self):
+ self.closed = True
- def __exit__(self, exc_type, exc, tb):
- return False
-
- def execute(self, sql):
- self.connection.executed.append(sql)
- if self.connection.fail_ping and sql == "SELECT 1":
- raise connection.pg.OperationalError("server closed the connection")
-
- def fetchall(self):
- return self.connection.rows
+ @contextmanager
+ def connection(self):
+ self.borrowed += 1
+ try:
+ yield _FakeConnection()
+ finally:
+ self.returned += 1
class _FakeConnection:
- def __init__(self, rows=None, *, closed=False, fail_ping=False):
- self.rows = list(rows or [])
- self.closed = closed
- self.fail_ping = fail_ping
- self.executed = []
- self.close_calls = 0
+ def __init__(self):
+ self.transactions = 0
- def cursor(self, row_factory=None):
- if self.closed:
- raise RuntimeError("the connection is closed")
- return _FakeCursor(self)
-
- def close(self):
- self.close_calls += 1
- self.closed = True
+ @contextmanager
+ def transaction(self):
+ self.transactions += 1
+ yield
@pytest.fixture(autouse=True)
-def clear_native_connections():
- connection.g_conn_dict.clear()
- connection.g_conninfo_dict.clear()
- connection._project_locks.clear()
+def clear_pools():
+ connection._pools.clear()
+ connection._pool_conninfo.clear()
+ connection._pool_borrows.clear()
+ connection._admin_pools.clear()
+ connection._admin_pool_borrows.clear()
yield
- connection.g_conn_dict.clear()
- connection.g_conninfo_dict.clear()
- connection._project_locks.clear()
+ connection._pools.clear()
+ connection._pool_conninfo.clear()
+ connection._pool_borrows.clear()
+ connection._admin_pools.clear()
+ connection._admin_pool_borrows.clear()
-def test_is_project_open_drops_closed_cached_connection():
- connection.g_conn_dict["fengyang"] = _FakeConnection(closed=True)
+def test_project_pool_is_reused_for_same_routed_dsn(monkeypatch):
+ monkeypatch.setattr(connection, "ConnectionPool", _FakePool)
+ monkeypatch.setattr(connection, "get_project_pgconn_string", lambda *, db_name: f"dbname={db_name}")
- assert project.is_project_open("fengyang") is False
- assert "fengyang" not in connection.g_conn_dict
+ first = connection.get_project_pool("fengyang")
+ second = connection.get_project_pool("fengyang")
+
+ assert first is second
+ assert first.conninfo == "dbname=fengyang"
-def test_open_connection_reuses_healthy_cached_connection(monkeypatch):
- cached = _FakeConnection()
- connection.g_conn_dict["fengyang"] = cached
- connection.g_conninfo_dict["fengyang"] = "dbname=fengyang"
- monkeypatch.setattr(
- connection, "get_project_pgconn_string", lambda db_name: f"dbname={db_name}"
- )
+def test_project_pool_rebuilds_when_routed_dsn_changes(monkeypatch):
+ monkeypatch.setattr(connection, "ConnectionPool", _FakePool)
+ dsn = {"value": "host=old dbname=fengyang"}
+ monkeypatch.setattr(connection, "get_project_pgconn_string", lambda *, db_name: dsn["value"])
- def fail_connect(*, conninfo, autocommit):
- raise AssertionError("cached connection should be reused")
+ old = connection.get_project_pool("fengyang")
+ dsn["value"] = "host=new dbname=fengyang"
+ new = connection.get_project_pool("fengyang")
- monkeypatch.setattr(connection.pg, "connect", fail_connect)
-
- assert connection.open_connection("fengyang") is cached
- assert cached.executed == ["SELECT 1"]
+ assert old.closed is True
+ assert new is not old
+ assert new.conninfo == "host=new dbname=fengyang"
-def test_read_all_reopens_closed_cached_connection(monkeypatch):
- stale = _FakeConnection(closed=True)
- fresh = _FakeConnection(rows=[{"key": "DURATION", "value": "01:00:00"}])
- connection.g_conn_dict["fengyang"] = stale
+def test_project_connection_returns_connection_to_pool(monkeypatch):
+ monkeypatch.setattr(connection, "ConnectionPool", _FakePool)
+ monkeypatch.setattr(connection, "get_project_pgconn_string", lambda *, db_name: f"dbname={db_name}")
- opened = []
-
- def fake_connect(*, conninfo, autocommit):
- opened.append((conninfo, autocommit))
- return fresh
-
- monkeypatch.setattr(connection.pg, "connect", fake_connect)
- monkeypatch.setattr(
- connection, "get_project_pgconn_string", lambda db_name: f"dbname={db_name}"
- )
-
- rows = database.read_all("fengyang", "select * from times")
-
- assert rows == [{"key": "DURATION", "value": "01:00:00"}]
- assert opened == [("dbname=fengyang", True)]
- assert connection.g_conn_dict["fengyang"] is fresh
- assert fresh.executed == ["select * from times"]
+ pool = connection.get_project_pool("fengyang")
+ with connection.project_connection("fengyang"):
+ assert pool.borrowed == 1
+ assert pool.returned == 0
+ assert pool.returned == 1
-def test_read_all_reopens_cached_connection_when_health_check_fails(monkeypatch):
- stale = _FakeConnection(fail_ping=True)
- fresh = _FakeConnection(rows=[{"scheme_name": "base"}])
- connection.g_conn_dict["fengyang"] = stale
- connection.g_conninfo_dict["fengyang"] = "dbname=fengyang"
-
- opened = []
-
- def fake_connect(*, conninfo, autocommit):
- opened.append((conninfo, autocommit))
- return fresh
-
- monkeypatch.setattr(connection.pg, "connect", fake_connect)
- monkeypatch.setattr(
- connection, "get_project_pgconn_string", lambda db_name: f"dbname={db_name}"
- )
-
- rows = database.read_all("fengyang", "select * from scheme_list")
-
- assert rows == [{"scheme_name": "base"}]
- assert stale.executed == ["SELECT 1"]
- assert stale.close_calls == 1
- assert opened == [("dbname=fengyang", True)]
- assert connection.g_conn_dict["fengyang"] is fresh
- assert fresh.executed == ["select * from scheme_list"]
-
-
-def test_open_connection_replaces_cache_when_project_dsn_changes(monkeypatch):
- cached = _FakeConnection()
- fresh = _FakeConnection()
- connection.g_conn_dict["fengyang"] = cached
- connection.g_conninfo_dict["fengyang"] = "host=old dbname=fengyang"
+def test_project_transaction_reuses_one_pooled_connection(monkeypatch):
+ monkeypatch.setattr(connection, "ConnectionPool", _FakePool)
monkeypatch.setattr(
connection,
"get_project_pgconn_string",
- lambda db_name: f"host=new dbname={db_name}",
+ lambda *, db_name: f"dbname={db_name}",
)
- monkeypatch.setattr(connection.pg, "connect", lambda **_kwargs: fresh)
- assert connection.open_connection("fengyang") is fresh
- assert cached.close_calls == 1
- assert connection.g_conninfo_dict["fengyang"] == "host=new dbname=fengyang"
+ pool = connection.get_project_pool("fengyang")
+ with connection.project_transaction("fengyang") as transaction_conn:
+ with connection.project_connection("fengyang") as nested_conn:
+ assert nested_conn is transaction_conn
+ assert transaction_conn.transactions == 1
+
+ assert pool.borrowed == 1
+ assert pool.returned == 1
+
+
+def test_close_project_pool_removes_and_closes_pool(monkeypatch):
+ monkeypatch.setattr(connection, "ConnectionPool", _FakePool)
+ monkeypatch.setattr(connection, "get_project_pgconn_string", lambda *, db_name: f"dbname={db_name}")
+
+ pool = connection.get_project_pool("fengyang")
+ connection.close_project_pool("fengyang")
+
+ assert pool.closed is True
+ assert "fengyang" not in connection._pools
+
+
+def test_admin_connection_is_pooled(monkeypatch):
+ monkeypatch.setattr(connection, "ConnectionPool", _FakePool)
+ monkeypatch.setattr(
+ connection,
+ "get_project_pgconn_string",
+ lambda *, db_name: f"dbname={db_name}",
+ )
+
+ first = connection.get_admin_pool()
+ second = connection.get_admin_pool()
+ with connection.admin_connection():
+ pass
+
+ assert first is second
+ assert first.conninfo == "dbname=postgres"
+ assert first.borrowed == 1
+ assert first.returned == 1
+
+
+def test_close_all_closes_project_and_admin_pools(monkeypatch):
+ monkeypatch.setattr(connection, "ConnectionPool", _FakePool)
+ monkeypatch.setattr(
+ connection,
+ "get_project_pgconn_string",
+ lambda *, db_name: f"dbname={db_name}",
+ )
+
+ project_pool = connection.get_project_pool("fengyang")
+ admin_pool = connection.get_admin_pool()
+ connection.close_all_project_pools()
+
+ assert project_pool.closed is True
+ assert admin_pool.closed is True
+ assert connection._pools == {}
+ assert connection._admin_pools == {}
+
+
+def test_admin_pools_are_isolated_by_routed_host(monkeypatch):
+ monkeypatch.setattr(connection, "ConnectionPool", _FakePool)
+ route = {"host": "one"}
+ monkeypatch.setattr(
+ connection,
+ "get_project_pgconn_string",
+ lambda *, db_name: f"host={route['host']} dbname={db_name}",
+ )
+
+ first = connection.get_admin_pool()
+ route["host"] = "two"
+ second = connection.get_admin_pool()
+
+ assert first is not second
+ assert first.closed is False
+ assert second.closed is False
+
+
+def test_project_pool_cache_evicts_least_recently_used_idle_pool(monkeypatch):
+ monkeypatch.setattr(connection, "ConnectionPool", _FakePool)
+ monkeypatch.setattr(connection.settings, "PROJECT_PG_CACHE_SIZE", 2)
+ monkeypatch.setattr(
+ connection,
+ "get_project_pgconn_string",
+ lambda *, db_name: f"dbname={db_name}",
+ )
+
+ first = connection.get_project_pool("first")
+ second = connection.get_project_pool("second")
+ connection.get_project_pool("first")
+ third = connection.get_project_pool("third")
+
+ assert list(connection._pools) == ["first", "third"]
+ assert second.closed is True
+ assert first.closed is False
+ assert third.closed is False
+
+
+def test_project_pool_cache_does_not_evict_active_pool(monkeypatch):
+ monkeypatch.setattr(connection, "ConnectionPool", _FakePool)
+ monkeypatch.setattr(connection.settings, "PROJECT_PG_CACHE_SIZE", 1)
+ monkeypatch.setattr(
+ connection,
+ "get_project_pgconn_string",
+ lambda *, db_name: f"dbname={db_name}",
+ )
+
+ with connection.project_connection("active"):
+ active = connection._pools["active"]
+ connection.get_project_pool("new")
+ assert active.closed is False
+ assert set(connection._pools) == {"active", "new"}
+
+ assert list(connection._pools) == ["new"]
+ assert active.closed is True
+
+
+def test_route_health_check_does_not_close_active_pool(monkeypatch):
+ monkeypatch.setattr(connection, "ConnectionPool", _FakePool)
+ route = {"value": "host=old dbname=project"}
+ monkeypatch.setattr(
+ connection,
+ "get_project_pgconn_string",
+ lambda *, db_name: route["value"],
+ )
+
+ with connection.project_connection("project"):
+ pool = connection._pools["project"]
+ route["value"] = "host=new dbname=project"
+ assert connection.is_project_pool_open("project") is False
+ assert pool.closed is False
+
+ replacement = connection.get_project_pool("project")
+ assert pool.closed is True
+ assert replacement is not pool
diff --git a/tests/unit/test_wndb_materialized_refresh.py b/tests/unit/test_wndb_materialized_refresh.py
new file mode 100644
index 0000000..8943b42
--- /dev/null
+++ b/tests/unit/test_wndb_materialized_refresh.py
@@ -0,0 +1,94 @@
+from app.native.wndb.core import database
+
+
+def _command(statement: str) -> database.DatabaseCommand:
+ return database.DatabaseCommand(statement, [])
+
+
+def test_database_command_has_no_removed_undo_state() -> None:
+ changes = [{"operation": "update", "type": "title", "value": "new"}]
+
+ command = database.DatabaseCommand("SELECT 1", changes)
+
+ assert vars(command) == {"sql": "SELECT 1", "changes": changes}
+ assert not hasattr(command, "undo_sql")
+ assert not hasattr(command, "undo_cs")
+
+
+def test_direct_model_write_refreshes_materialized_views(monkeypatch) -> None:
+ events: list[str] = []
+ monkeypatch.setattr(
+ database,
+ "write",
+ lambda _name, _statement: events.append("write"),
+ )
+ monkeypatch.setattr(
+ database,
+ "is_project_transaction_active",
+ lambda _name: False,
+ )
+ monkeypatch.setattr(
+ database,
+ "refresh_materialized_views",
+ lambda _name: events.append("refresh"),
+ )
+
+ result = database.execute_command(
+ "project_a",
+ database.DatabaseCommand(
+ "UPDATE network.junctions SET elevation = 1",
+ [{"operation": "update", "type": "junction", "id": "J-1"}],
+ ),
+ )
+
+ assert events == ["write", "refresh"]
+ assert result.operations == [
+ {"operation": "update", "type": "junction", "id": "J-1"}
+ ]
+
+
+def test_batch_model_write_defers_materialized_view_refresh(monkeypatch) -> None:
+ events: list[str] = []
+ monkeypatch.setattr(
+ database,
+ "write",
+ lambda _name, _statement: events.append("write"),
+ )
+ monkeypatch.setattr(
+ database,
+ "is_project_transaction_active",
+ lambda _name: True,
+ )
+ monkeypatch.setattr(
+ database,
+ "refresh_materialized_views",
+ lambda _name: events.append("refresh"),
+ )
+
+ database.execute_command(
+ "project_a",
+ _command("UPDATE network.junctions SET elevation = 1"),
+ )
+
+ assert events == ["write"]
+
+
+def test_non_gis_model_write_does_not_refresh_materialized_views(monkeypatch) -> None:
+ events: list[str] = []
+ monkeypatch.setattr(
+ database,
+ "write",
+ lambda _name, _statement: events.append("write"),
+ )
+ monkeypatch.setattr(
+ database,
+ "refresh_materialized_views",
+ lambda _name: events.append("refresh"),
+ )
+
+ database.execute_command(
+ "project_a",
+ _command("UPDATE network.time_settings SET value = '01:00'"),
+ )
+
+ assert events == ["write"]
diff --git a/tests/unit/test_wndb_query_safety.py b/tests/unit/test_wndb_query_safety.py
index 6162871..0dc9d7a 100644
--- a/tests/unit/test_wndb_query_safety.py
+++ b/tests/unit/test_wndb_query_safety.py
@@ -1,4 +1,9 @@
-from app.native.wndb import s2_junctions
+import ast
+from pathlib import Path
+
+from app.infra.db.postgresql import scada_assets
+from app.native.wndb.core.database import ChangeSet, sql_literal
+from app.native.wndb.model import controls, junctions, patterns
def test_get_junction_binds_untrusted_identifier(monkeypatch) -> None:
@@ -9,13 +14,104 @@ def test_get_junction_binds_untrusted_identifier(monkeypatch) -> None:
calls.append((name, statement, params))
return None
- monkeypatch.setattr(s2_junctions, "try_read", fake_try_read)
+ monkeypatch.setattr(junctions, "try_read", fake_try_read)
- assert s2_junctions.get_junction("project_a", malicious_id) == {}
- assert calls == [
- (
- "project_a",
- "select * from junctions where id = %s",
- (malicious_id,),
- )
- ]
+ assert junctions.get_junction("project_a", malicious_id) == {}
+ assert len(calls) == 1
+ name, statement, params = calls[0]
+ assert name == "project_a"
+ assert "network.junctions" in statement
+ assert "WHERE n.id = %s" in statement
+ assert malicious_id not in statement
+ assert params == (malicious_id,)
+
+
+def test_get_all_junctions_reads_materialized_view(monkeypatch) -> None:
+ statements: list[str] = []
+
+ def fake_read_all(_name, statement):
+ statements.append(statement)
+ return []
+
+ monkeypatch.setattr(junctions, "read_all", fake_read_all)
+ monkeypatch.setattr(junctions, "get_all_node_links", lambda _name: {})
+
+ assert junctions.get_all_junctions("project_a") == []
+ assert "FROM gis.junctions" in statements[0]
+
+
+def test_get_all_scada_info_reads_materialized_view(monkeypatch) -> None:
+ statements: list[str] = []
+
+ def fake_read_all(_name, statement):
+ statements.append(statement)
+ return []
+
+ monkeypatch.setattr(scada_assets, "read_all", fake_read_all)
+
+ assert scada_assets.get_all_scada_info("project_a") == []
+ assert "FROM gis.scada_devices" in statements[0]
+
+
+def test_sql_literal_keeps_attacker_text_inside_one_postgres_literal() -> None:
+ malicious = "x'); DROP SCHEMA network CASCADE; --"
+
+ assert sql_literal("O'Brien") == "'O''Brien'"
+ assert sql_literal(malicious) == "'x''); DROP SCHEMA network CASCADE; --'"
+ assert sql_literal(None) == "NULL"
+
+
+def test_pattern_command_quotes_malicious_identifier() -> None:
+ malicious = "x'); DROP SCHEMA network CASCADE; --"
+ change = ChangeSet({"id": malicious, "factors": [1.0]})
+
+ command = patterns._add_pattern("project_a", change).sql
+
+ assert f"values ({sql_literal(malicious)})" in command
+ assert "values ('x'); DROP SCHEMA" not in command
+
+
+def test_inp_control_quotes_embedded_apostrophe() -> None:
+ command = controls.inp_in_control("LINK P-1 STATUS 'OPEN'; DELETE")
+
+ assert "''OPEN''" in command
+ assert "STATUS 'OPEN'; DELETE')" not in command
+
+
+def test_wndb_sql_fstrings_do_not_quote_formatted_values_directly() -> None:
+ root = Path(__file__).resolve().parents[2] / "app" / "native" / "wndb"
+ violations: list[str] = []
+ for path in root.rglob("*.py"):
+ tree = ast.parse(path.read_text(encoding="utf-8"))
+ for node in ast.walk(tree):
+ if not isinstance(node, ast.JoinedStr):
+ continue
+ static_text = "".join(
+ value.value
+ for value in node.values
+ if isinstance(value, ast.Constant) and isinstance(value.value, str)
+ ).lower()
+ if not any(
+ keyword in static_text
+ for keyword in ("select ", "insert into", "update ", "delete from")
+ ):
+ continue
+ for index, value in enumerate(node.values):
+ if not isinstance(value, ast.FormattedValue):
+ continue
+ before = node.values[index - 1] if index else None
+ after = node.values[index + 1] if index + 1 < len(node.values) else None
+ left_quote = (
+ isinstance(before, ast.Constant)
+ and isinstance(before.value, str)
+ and before.value.endswith("'")
+ )
+ right_quote = (
+ isinstance(after, ast.Constant)
+ and isinstance(after.value, str)
+ and after.value.startswith("'")
+ )
+ if left_quote and right_quote:
+ violations.append(f"{path.name}:{node.lineno}")
+
+ assert violations == []