refactor(db)!: adopt project-routed pooled databases

Reorganize WNDB by responsibility and remove legacy scheme endpoints.\n\nRoute analysis and time-series access through project pools, preserve transactional realtime replacement, and refresh GIS materialized views after writes.\n\nAdd database architecture documentation, live pooling coverage, API contract updates, and executable container verification.\n\nBREAKING CHANGE: legacy scheme APIs and flat app.native.wndb module imports are removed.
This commit is contained in:
2026-08-25 18:35:05 +08:00
parent fdbcc5c033
commit fa188af0b1
181 changed files with 8446 additions and 33546 deletions
+9
View File
@@ -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 (可选)
# ============================================
+5 -6
View File
@@ -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:
+1 -1
View File
@@ -7,4 +7,4 @@ build/
*.dump
.vscode/
app/algorithms/health/model/my_survival_forest_model_quxi.joblib
inp/
/inp/
+5
View File
@@ -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
+14 -14
View File
@@ -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,
)
+6 -2
View File
@@ -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']))
+11 -40
View File
@@ -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)
+19
View File
@@ -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",
]
@@ -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
return t_demands
+2 -2
View File
@@ -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(
+3 -3
View File
@@ -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(
@@ -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,
@@ -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,
@@ -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,
@@ -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,
+2 -3
View File
@@ -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(
@@ -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,
-104
View File
@@ -1,104 +0,0 @@
from typing import List, Any
from fastapi import APIRouter, Request, HTTPException, Query, Body
from app.services.tjnetwork import (
ChangeSet,
get_all_extension_data_keys,
get_all_extension_data,
get_extension_data,
set_extension_data
)
router = APIRouter()
@router.get(
"/all-extension-data-keys",
summary="获取所有扩展数据键",
description="获取指定网络的所有扩展数据的键列表"
)
async def get_all_extension_data_keys_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> list[str]:
"""
获取所有扩展数据键。
返回指定网络中所有可用的扩展数据键。
Args:
network: 管网名称(或数据库名称)
Returns:
扩展数据键列表
"""
return get_all_extension_data_keys(network)
@router.get(
"/all-extension-datas",
summary="获取所有扩展数据",
description="获取指定网络的所有扩展数据"
)
async def get_all_extension_data_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> dict[str, Any]:
"""
获取所有扩展数据。
返回指定网络的所有扩展数据及其值。
Args:
network: 管网名称(或数据库名称)
Returns:
扩展数据字典
"""
return get_all_extension_data(network)
@router.get(
"/extension-datas",
summary="获取指定扩展数据",
description="获取指定网络中指定键的扩展数据值"
)
async def get_extension_data_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
key: str = Query(..., description="扩展数据键")
) -> str | None:
"""
获取指定扩展数据。
返回指定网络中指定键对应的扩展数据值。
Args:
network: 管网名称(或数据库名称)
key: 扩展数据键
Returns:
扩展数据值,如果不存在返回None
"""
return get_extension_data(network, key)
@router.patch(
"/extension-datas",
response_model=None,
summary="设置扩展数据",
description="设置指定网络中的扩展数据"
)
async def set_extension_data_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""
设置扩展数据。
在指定网络中设置扩展数据,并返回变更集信息。
Args:
network: 管网名称(或数据库名称)
req: 包含扩展数据的请求体
Returns:
变更集信息
"""
props = await req.json()
print(props)
cs = set_extension_data(network, ChangeSet(props))
print(cs.operations[0])
return cs
-62
View File
@@ -1,62 +0,0 @@
from typing import Any
from fastapi import APIRouter, Query
from fastapi.responses import JSONResponse
from fastapi import status
from pydantic import BaseModel
from app.services.tjnetwork import (
get_all_sensor_placements,
get_all_burst_locate_results,
)
router = APIRouter()
async def fastapi_get_json():
"""
获取JSON示例
返回示例JSON格式的响应
"""
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={
"code": 400,
"message": "this is message",
"data": 123,
},
)
@router.get("/sensor-placement-schemes", summary="获取所有传感器位置", description="获取网络中所有传感器的放置位置信息")
async def fastapi_get_all_sensor_placements(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[Any, Any]]:
"""
获取所有传感器位置
返回网络中所有传感器的放置位置及其配置信息
"""
return get_all_sensor_placements(network)
@router.get("/burst-locations", summary="获取所有爆管定位结果", description="获取网络中所有爆管定位的分析结果")
async def fastapi_get_all_burst_locate_results(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[Any, Any]]:
"""
获取所有爆管定位结果
返回网络中所有的爆管定位分析结果
"""
return get_all_burst_locate_results(network)
class Item(BaseModel):
"""测试数据模型"""
str_info: str
async def fastapi_test_dict(data: Item) -> dict[str, str]:
"""
测试字典处理
接收Item模型,返回其字典格式
"""
item = data.dict()
return item
-1
View File
@@ -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,
-1
View File
@@ -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,
@@ -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,
-1
View File
@@ -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,
-1
View File
@@ -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,
+41 -502
View File
@@ -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()))
@@ -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,
-1
View File
@@ -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,
-1
View File
@@ -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,
-1
View File
@@ -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,
+4 -68
View File
@@ -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)
+25 -19
View File
@@ -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)}",
)
-127
View File
@@ -1,127 +0,0 @@
from typing import Any, List, Dict
from fastapi import APIRouter, Query, Path
from app.services.tjnetwork import (
get_pipe_risk_probability_now,
get_pipe_risk_probability,
get_pipes_risk_probability,
get_network_pipe_risk_probability_now,
get_pipe_risk_probability_geometries,
)
router = APIRouter()
@router.get(
"/pipes/risk-probability-now",
summary="获取管道当前风险概率",
description="获取指定管道当前时刻的风险概率值"
)
async def fastapi_get_pipe_risk_probability_now(
network: str = Query(..., description="管网名称(或数据库名称)"),
pipe_id: str = Query(..., description="管道ID")
) -> dict[str, Any]:
"""
获取管道当前风险概率
查询指定管道在当前时刻的风险概率值
Args:
network: 管网名称或数据库名称
pipe_id: 管道ID
Returns:
包含风险概率信息的字典
"""
return get_pipe_risk_probability_now(network, pipe_id)
@router.get(
"/pipes/risk-probability",
summary="获取管道风险概率历史",
description="获取指定管道的风险概率历史数据"
)
async def fastapi_get_pipe_risk_probability(
network: str = Query(..., description="管网名称(或数据库名称)"),
pipe_id: str = Query(..., description="管道ID")
) -> dict[str, Any]:
"""
获取管道风险概率历史
查询指定管道的历史风险概率数据
Args:
network: 管网名称或数据库名称
pipe_id: 管道ID
Returns:
包含风险概率历史的字典
"""
return get_pipe_risk_probability(network, pipe_id)
@router.get(
"/pipes-risk-probabilities",
summary="批量获取多条管道风险概率",
description="批量获取多条管道的风险概率值"
)
async def fastapi_get_pipes_risk_probability(
network: str = Query(..., description="管网名称(或数据库名称)"),
pipe_ids: str = Query(..., description="逗号分隔的管道ID列表")
) -> list[dict[str, Any]]:
"""
批量获取多条管道风险概率
查询多条指定管道的风险概率值
Args:
network: 管网名称或数据库名称
pipe_ids: 逗号分隔的管道ID列表例如pipe1,pipe2,pipe3
Returns:
包含多条管道风险概率的列表
"""
pipeids = pipe_ids.split(",")
return get_pipes_risk_probability(network, pipeids)
@router.get(
"/network-pipe-risk-probability-nows",
summary="获取整个网络的管道风险概率",
description="获取指定网络中所有管道的当前风险概率值"
)
async def fastapi_get_network_pipe_risk_probability_now(
network: str = Query(..., description="管网名称(或数据库名称)"),
) -> list[dict[str, Any]]:
"""
获取整个网络的管道风险概率
查询指定网络中所有管道在当前时刻的风险概率值
Args:
network: 管网名称或数据库名称
Returns:
包含网络内所有管道风险概率的列表
"""
return get_network_pipe_risk_probability_now(network)
@router.get(
"/pipes/risk-probability-geometries",
summary="获取管道风险几何信息",
description="获取指定网络中管道的风险相关几何数据"
)
async def fastapi_get_pipe_risk_probability_geometries(
network: str = Query(..., description="管网名称(或数据库名称)")
) -> dict[str, Any]:
"""
获取管道风险几何信息
查询指定网络中管道的地理和风险相关的几何数据
Args:
network: 管网名称或数据库名称
Returns:
包含几何信息和风险数据的字典
"""
return get_pipe_risk_probability_geometries(network)
+18 -509
View File
@@ -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)
-60
View File
@@ -1,60 +0,0 @@
from datetime import datetime
from fastapi import APIRouter, HTTPException, Path, Query
from typing import Any
from app.services.tjnetwork import get_scheme_schema, get_scheme, get_all_schemes
from app.services.scheme_management import query_scheme_detail
from app.services.time_api import extract_date
router = APIRouter()
@router.get("/network-schemas/scheme", summary="获取方案模式", description="获取指定网络的方案模式定义")
async def fastapi_get_scheme_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[Any, Any]]:
"""
获取方案模式定义
返回指定网络的方案模式结构定义
"""
return get_scheme_schema(network)
@router.get("/schemes/detail", summary="获取单个方案", description="根据名称获取指定的方案信息")
async def fastapi_get_scheme(network: str = Query(..., description="管网名称(或数据库名称)"), schema_name: str = Query(..., description="方案名称")) -> dict[Any, Any]:
"""
获取单个方案详情
返回指定网络中指定名称的方案详细信息
"""
return get_scheme(network, schema_name)
@router.get("/schemes", summary="获取所有方案", description="获取指定网络的所有方案信息")
async def fastapi_get_all_schemes(
network: str = Query(..., description="管网名称(或数据库名称)"),
scheme_type: str | None = Query(None, description="方案类型;为空时返回全部类型"),
query_date: datetime | None = Query(None, description="查询日期(可选)"),
) -> list[dict[Any, Any]]:
"""
获取所有方案列表
返回指定网络中所有可用的方案
"""
parsed_date = (
extract_date(query_date, field_name="query_date")
if query_date is not None
else None
)
return get_all_schemes(network, scheme_type=scheme_type, query_date=parsed_date)
@router.get("/schemes/{scheme_name}", summary="获取方案详情", description="按方案类型获取指定方案详情")
async def fastapi_get_scheme_detail(
scheme_name: str = Path(..., description="方案名称"),
network: str = Query(..., description="管网名称(或数据库名称)"),
scheme_type: str | None = Query(None, description="方案类型;为空时返回通用方案详情"),
) -> dict[Any, Any]:
result = query_scheme_detail(
name=network,
scheme_name=scheme_name,
scheme_type=scheme_type,
)
if not result:
raise HTTPException(status_code=404, detail=f"Scheme {scheme_name} not found")
return result
+52 -36
View File
@@ -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,
-185
View File
@@ -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"]
-199
View File
@@ -1,199 +0,0 @@
from fastapi import APIRouter, Depends, Request, Query
from app.auth.permissions import SIMULATION_RUN, require_permission
from app.services.tjnetwork import (
ChangeSet,
get_current_operation,
execute_undo,
execute_redo,
list_snapshot,
have_snapshot,
have_snapshot_for_operation,
have_snapshot_for_current_operation,
take_snapshot_for_operation,
take_snapshot_for_current_operation,
take_snapshot,
pick_snapshot,
pick_operation,
sync_with_server,
execute_batch_commands,
execute_batch_command,
get_restore_operation,
set_restore_operation,
)
router = APIRouter()
@router.get("/current-operation-ids", summary="获取当前操作ID", description="获取网络当前的操作ID")
async def get_current_operation_id_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> int:
"""
获取当前操作ID
返回网络当前正在执行的操作ID
"""
return get_current_operation(network)
@router.post("/undos", summary="撤销操作", description="撤销网络上最后的一个操作")
async def undo_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")):
"""
撤销操作
撤销网络上最近执行的一个操作
"""
return execute_undo(network)
@router.post("/redos", summary="重做操作", description="重做网络上被撤销的操作")
async def redo_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")):
"""
重做操作
重做网络上被撤销的操作
"""
return execute_redo(network)
@router.get("/snapshots", summary="获取快照列表", description="获取网络中的所有快照")
async def list_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[tuple[int, str]]:
"""
获取快照列表
返回网络中所有可用的快照及其信息
"""
return list_snapshot(network)
@router.get("/snapshots/existence", summary="检查快照是否存在", description="检查指定标签的快照是否存在")
async def have_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签")) -> bool:
"""
检查快照是否存在
返回指定标签的快照是否存在
"""
return have_snapshot(network, tag)
@router.get("/snapshot-for-operations", summary="检查操作快照是否存在", description="检查指定操作ID的快照是否存在")
async def have_snapshot_for_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), operation: int = Query(..., description="操作ID")) -> bool:
"""
检查操作快照是否存在
返回指定操作ID的快照是否存在
"""
return have_snapshot_for_operation(network, operation)
@router.get("/snapshot-for-current-operations", summary="检查当前操作快照是否存在", description="检查当前操作的快照是否存在")
async def have_snapshot_for_current_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> bool:
"""
检查当前操作快照是否存在
返回当前操作的快照是否存在
"""
return have_snapshot_for_current_operation(network)
@router.post("/snapshot-for-operations", summary="为操作创建快照", description="为指定的操作创建快照")
async def take_snapshot_for_operation_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
operation: int = Query(..., description="操作ID"),
tag: str = Query(..., description="快照标签")
) -> None:
"""
为操作创建快照
为指定操作创建一个带标签的快照
"""
return take_snapshot_for_operation(network, operation, tag)
@router.post("/snapshot-for-current-operations", summary="为当前操作创建快照", description="为当前操作创建快照")
async def take_snapshot_for_current_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签")) -> None:
"""
为当前操作创建快照
为网络当前操作创建一个快照
"""
return take_snapshot_for_current_operation(network, tag)
@router.post("/snapshots", summary="创建快照", description="为网络创建一个快照")
async def take_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签")) -> None:
"""
创建快照
为网络创建一个带标签的快照
"""
return take_snapshot(network, tag)
@router.patch("/snapshots", summary="选择快照", description="选择并恢复到指定的快照", response_model=None)
async def pick_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签"), discard: bool = Query(False, description="是否丢弃当前更改")) -> ChangeSet:
"""
选择快照
选择并恢复到指定的快照
"""
return pick_snapshot(network, tag, discard)
@router.patch("/operations", summary="选择操作", description="选择并恢复到指定的操作", response_model=None)
async def pick_operation_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
operation: int = Query(..., description="操作ID"),
discard: bool = Query(False, description="是否丢弃当前更改")
) -> ChangeSet:
"""
选择操作
选择并恢复到指定的操作
"""
return pick_operation(network, operation, discard)
@router.post("/with-servers", summary="与服务器同步", description="将网络与服务器同步到指定操作", response_model=None)
async def sync_with_server_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
operation: int = Query(..., description="目标操作ID"),
_=Depends(require_permission(SIMULATION_RUN)),
) -> ChangeSet:
"""
与服务器同步
将网络与服务器同步到指定的操作
"""
return sync_with_server(network, operation)
@router.post("/network-command-batches", summary="执行批量命令", description="执行多个网络操作命令", response_model=None)
async def execute_batch_commands_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), req: Request = None) -> ChangeSet:
"""
执行批量命令
在网络上执行多个操作命令
"""
jo_root = await req.json()
cs: ChangeSet = ChangeSet()
cs.operations = jo_root["operations"]
rcs = execute_batch_commands(network, cs)
return rcs
@router.post("/network-command-batches/compressed", summary="执行压缩批量命令", description="执行压缩的批量命令", response_model=None)
async def execute_compressed_batch_commands_endpoint(
network: str = Query(..., description="管网名称(或数据库名称)"),
req: Request = None
) -> ChangeSet:
"""
执行压缩批量命令
执行压缩格式的批量命令
"""
jo_root = await req.json()
cs: ChangeSet = ChangeSet()
cs.operations = jo_root["operations"]
return execute_batch_command(network, cs)
@router.get("/restore-operations", summary="获取恢复操作ID", description="获取网络的恢复操作ID")
async def get_restore_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> int:
"""
获取恢复操作ID
返回网络的恢复操作ID
"""
return get_restore_operation(network)
@router.patch("/restore-operations", summary="设置恢复操作ID", description="设置网络的恢复操作ID")
async def set_restore_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), operation: int = Query(..., description="操作ID")) -> None:
"""
设置恢复操作ID
设置网络的恢复操作ID
"""
return set_restore_operation(network, operation)
@@ -0,0 +1,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
+13 -18
View File
@@ -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:type2type为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(
-391
View File
@@ -1,391 +0,0 @@
from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
from typing import List
from datetime import datetime
from psycopg import AsyncConnection
from app.infra.db.timescaledb.repositories.scheme import SchemeRepository
from .dependencies import get_timescale_connection
router = APIRouter()
@router.post("/timeseries/schemes/links/batches", status_code=201, summary="批量插入方案管道数据")
async def insert_scheme_links(
data: List[dict] = Body(..., description="方案管道数据列表"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
批量插入方案管道数据
将特定方案的管道模拟数据批量插入时间序列数据库
Args:
data: 方案管道数据列表
Returns:
插入成功的记录数
"""
await SchemeRepository.insert_links_batch(conn, data)
return {"message": f"Inserted {len(data)} records"}
@router.get("/timeseries/schemes/links", summary="查询方案管道数据")
async def get_scheme_links(
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
start_time: datetime = Query(..., description="查询开始时间"),
end_time: datetime = Query(..., description="查询结束时间"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
查询指定方案和时间范围内的管道数据
根据方案和时间范围查询管道的模拟值
Args:
scheme_type: 方案类型
scheme_name: 方案名称
start_time: 查询开始时间
end_time: 查询结束时间
Returns:
方案管道数据列表
"""
return await SchemeRepository.get_links_by_scheme_and_time_range(
conn, scheme_type, scheme_name, start_time, end_time
)
@router.get("/timeseries/schemes/links/{link_id}/field", summary="查询方案管道字段数据")
async def get_scheme_link_field(
link_id: str = Path(..., description="管道ID"),
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
start_time: datetime = Query(..., description="查询开始时间"),
end_time: datetime = Query(..., description="查询结束时间"),
field: str = Query(..., description="要查询的字段名称"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
查询指定方案管道的特定字段数据
查询特定方案中指定管道在时间范围内的特定字段值
Args:
link_id: 管道ID
scheme_type: 方案类型
scheme_name: 方案名称
start_time: 查询开始时间
end_time: 查询结束时间
field: 字段名称
Returns:
字段数据列表
Raises:
HTTPException: 当查询参数无效时返回400错误
"""
try:
return await SchemeRepository.get_link_field_by_scheme_and_time_range(
conn, scheme_type, scheme_name, start_time, end_time, link_id, field
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.patch("/timeseries/schemes/links/{link_id}/field", summary="更新方案管道字段")
async def update_scheme_link_field(
link_id: str = Path(..., description="管道ID"),
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
time: datetime = Query(..., description="更新数据的时间戳"),
field: str = Query(..., description="要更新的字段名称"),
value: float = Query(..., description="更新的字段值"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
更新指定方案管道的字段值
更新特定方案中指定管道在某个时间的字段数据
Args:
link_id: 管道ID
scheme_type: 方案类型
scheme_name: 方案名称
time: 数据时间戳
field: 字段名称
value: 字段新值
Returns:
更新结果信息
Raises:
HTTPException: 当字段不存在或更新失败时返回400错误
"""
try:
await SchemeRepository.update_link_field(
conn, time, scheme_type, scheme_name, link_id, field, value
)
return {"message": "Updated successfully"}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/timeseries/schemes/links", summary="删除方案管道数据")
async def delete_scheme_links(
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
start_time: datetime = Query(..., description="删除开始时间"),
end_time: datetime = Query(..., description="删除结束时间"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
删除指定方案和时间范围内的管道数据
删除在指定方案和时间范围内的所有管道模拟数据
Args:
scheme_type: 方案类型
scheme_name: 方案名称
start_time: 删除开始时间
end_time: 删除结束时间
Returns:
删除结果信息
"""
await SchemeRepository.delete_links_by_scheme_and_time_range(
conn, scheme_type, scheme_name, start_time, end_time
)
return {"message": "Deleted successfully"}
@router.post("/timeseries/schemes/nodes/batches", status_code=201, summary="批量插入方案节点数据")
async def insert_scheme_nodes(
data: List[dict] = Body(..., description="方案节点数据列表"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
批量插入方案节点数据
将特定方案的节点模拟数据批量插入时间序列数据库
Args:
data: 方案节点数据列表
Returns:
插入成功的记录数
"""
await SchemeRepository.insert_nodes_batch(conn, data)
return {"message": f"Inserted {len(data)} records"}
@router.get("/timeseries/schemes/nodes/{node_id}/field", summary="查询方案节点字段数据")
async def get_scheme_node_field(
node_id: str = Path(..., description="节点ID"),
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
start_time: datetime = Query(..., description="查询开始时间"),
end_time: datetime = Query(..., description="查询结束时间"),
field: str = Query(..., description="要查询的字段名称"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
查询指定方案节点的特定字段数据
查询特定方案中指定节点在时间范围内的特定字段值
Args:
node_id: 节点ID
scheme_type: 方案类型
scheme_name: 方案名称
start_time: 查询开始时间
end_time: 查询结束时间
field: 字段名称
Returns:
字段数据列表
Raises:
HTTPException: 当查询参数无效时返回400错误
"""
try:
return await SchemeRepository.get_node_field_by_scheme_and_time_range(
conn, scheme_type, scheme_name, start_time, end_time, node_id, field
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.patch("/timeseries/schemes/nodes/{node_id}/field", summary="更新方案节点字段")
async def update_scheme_node_field(
node_id: str = Path(..., description="节点ID"),
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
time: datetime = Query(..., description="更新数据的时间戳"),
field: str = Query(..., description="要更新的字段名称"),
value: float = Query(..., description="更新的字段值"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
更新指定方案节点的字段值
更新特定方案中指定节点在某个时间的字段数据
Args:
node_id: 节点ID
scheme_type: 方案类型
scheme_name: 方案名称
time: 数据时间戳
field: 字段名称
value: 字段新值
Returns:
更新结果信息
Raises:
HTTPException: 当字段不存在或更新失败时返回400错误
"""
try:
await SchemeRepository.update_node_field(
conn, time, scheme_type, scheme_name, node_id, field, value
)
return {"message": "Updated successfully"}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/timeseries/schemes/nodes", summary="删除方案节点数据")
async def delete_scheme_nodes(
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
start_time: datetime = Query(..., description="删除开始时间"),
end_time: datetime = Query(..., description="删除结束时间"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
删除指定方案和时间范围内的节点数据
删除在指定方案和时间范围内的所有节点模拟数据
Args:
scheme_type: 方案类型
scheme_name: 方案名称
start_time: 删除开始时间
end_time: 删除结束时间
Returns:
删除结果信息
"""
await SchemeRepository.delete_nodes_by_scheme_and_time_range(
conn, scheme_type, scheme_name, start_time, end_time
)
return {"message": "Deleted successfully"}
@router.post("/timeseries/schemes/simulation-results", status_code=201, summary="存储方案模拟结果")
async def store_scheme_simulation_result(
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
node_result_list: List[dict] = Body(..., description="节点模拟结果列表"),
link_result_list: List[dict] = Body(..., description="管道模拟结果列表"),
result_start_time: str = Query(..., description="模拟结果开始时间"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
存储方案模拟结果到时间序列数据库
将特定方案的节点和管道模拟计算结果批量存储到TimescaleDB数据库
Args:
scheme_type: 方案类型
scheme_name: 方案名称
node_result_list: 节点模拟结果列表
link_result_list: 管道模拟结果列表
result_start_time: 模拟结果对应的起始时间
Returns:
存储结果信息
"""
await SchemeRepository.store_scheme_simulation_result(
conn,
scheme_type,
scheme_name,
node_result_list,
link_result_list,
result_start_time,
)
return {"message": "Scheme simulation results stored successfully"}
@router.get(
"/timeseries/schemes/records", summary="按方案、时间和属性查询数据"
)
async def query_scheme_records_by_scheme_time_property(
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
query_time: str = Query(..., description="查询时间"),
type: str = Query(..., description="元素类型,pipe(管道)或 junction(节点)"),
property: str = Query(..., description="要查询的属性名称"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
按指定方案时间和属性查询所有方案数据
查询在特定方案和时间点所有指定类型元素的特定属性值
Args:
scheme_type: 方案类型
scheme_name: 方案名称
query_time: 查询时间
type: 元素类型pipe或junction
property: 属性名称
Returns:
查询结果列表
Raises:
HTTPException: 当查询参数无效时返回400错误
"""
try:
results = await SchemeRepository.query_all_record_by_scheme_time_property(
conn, scheme_type, scheme_name, query_time, type, property
)
return {"results": results}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/timeseries/schemes/simulation-results", summary="按ID和时间查询方案模拟数据")
async def query_scheme_simulation_by_id_time(
scheme_type: str = Query(..., description="方案类型"),
scheme_name: str = Query(..., description="方案名称"),
id: str = Query(..., description="元素ID(管道ID或节点ID"),
type: str = Query(..., description="元素类型,pipe(管道)或 junction(节点)"),
query_time: str = Query(..., description="查询时间"),
conn: AsyncConnection = Depends(get_timescale_connection),
):
"""
按指定ID和时间查询方案模拟结果
查询特定方案中的元素在某一时间点的模拟数据
Args:
scheme_type: 方案类型
scheme_name: 方案名称
id: 元素ID
type: 元素类型pipe或junction
query_time: 查询时间
Returns:
模拟结果数据
Raises:
HTTPException: 当查询参数无效时返回400错误
"""
try:
result = await SchemeRepository.query_scheme_simulation_result_by_id_time(
conn, scheme_type, scheme_name, id, type, query_time
)
return {"result": result}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
+7 -35
View File
@@ -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],
)
+2 -1
View File
@@ -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
+14 -12
View File
@@ -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
+52
View File
@@ -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()
-123
View File
@@ -1,123 +0,0 @@
import logging
from contextlib import asynccontextmanager
from typing import AsyncGenerator, Dict, Optional
import psycopg_pool
from psycopg.rows import dict_row
from app.infra.db.project_routing import get_project_pgconn_string
# Configure logging
logger = logging.getLogger(__name__)
class Database:
def __init__(self, db_name=None):
self.pool = None
self.db_name = db_name
self.conninfo = None
def init_pool(self, db_name=None):
"""Initialize the connection pool."""
# Use provided db_name, or the one from constructor, or default from config
target_db_name = db_name or self.db_name
# Get connection string, handling default case where target_db_name might be None
if target_db_name:
conn_string = get_project_pgconn_string(db_name=target_db_name)
else:
conn_string = get_project_pgconn_string()
self.conninfo = conn_string
try:
self.pool = psycopg_pool.AsyncConnectionPool(
conninfo=conn_string,
min_size=5,
max_size=20,
open=False, # Don't open immediately, wait for startup
kwargs={"row_factory": dict_row}, # Return rows as dictionaries
)
logger.info(f"PostgreSQL connection pool initialized for database: {target_db_name or 'default'}")
except Exception as e:
logger.error(f"Failed to initialize postgresql connection pool: {e}")
raise
async def open(self):
if self.pool:
await self.pool.open()
async def close(self):
"""Close the connection pool."""
if self.pool:
await self.pool.close()
logger.info("PostgreSQL connection pool closed.")
@asynccontextmanager
async def get_connection(self) -> AsyncGenerator:
"""Get a connection from the pool."""
if not self.pool:
raise Exception("Database pool is not initialized.")
async with self.pool.connection() as conn:
yield conn
# 默认数据库实例
db = Database()
# 缓存不同数据库的实例 - 避免重复创建连接池
_database_instances: Dict[str, Database] = {}
def create_database_instance(db_name):
"""Create a new Database instance for a specific database."""
return Database(db_name=db_name)
async def get_database_instance(db_name: Optional[str] = None) -> Database:
"""Get or create a database instance for the specified database name."""
if not db_name:
return db # 返回默认数据库实例
expected_conninfo = get_project_pgconn_string(db_name=db_name)
existing = _database_instances.get(db_name)
if existing is not None and existing.conninfo != expected_conninfo:
await existing.close()
del _database_instances[db_name]
if db_name not in _database_instances:
# 创建新的数据库实例
instance = create_database_instance(db_name)
instance.init_pool()
await instance.open()
_database_instances[db_name] = instance
logger.info(f"Created new database instance for: {db_name}")
return _database_instances[db_name]
async def get_db_connection():
"""Dependency for FastAPI to get a database connection."""
async with db.get_connection() as conn:
yield conn
async def get_database_connection(db_name: Optional[str] = None):
"""
FastAPI dependency to get database connection with optional database name.
使用方法: conn: AsyncConnection = Depends(lambda: get_database_connection("your_db_name"))
或在路由函数中: conn: AsyncConnection = Depends(get_database_connection)
"""
instance = await get_database_instance(db_name)
async with instance.get_connection() as conn:
yield conn
async def cleanup_database_instances():
"""Clean up all database instances (call this on application shutdown)."""
for db_name, instance in _database_instances.items():
await instance.close()
logger.info(f"Closed database instance for: {db_name}")
_database_instances.clear()
# 关闭默认数据库
await db.close()
logger.info("All database instances cleaned up.")
+20 -15
View File
@@ -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
]
+66
View File
@@ -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")
]
-104
View File
@@ -1,104 +0,0 @@
from typing import List, Optional, Any
from psycopg import AsyncConnection
class SchemeRepository:
@staticmethod
async def get_schemes(conn: AsyncConnection) -> List[dict]:
"""
查询pg数据库中, scheme_list 的所有记录
:param conn: 异步数据库连接
:return: 包含所有记录的列表, 每条记录为一个字典
"""
async with conn.cursor() as cur:
await cur.execute(
"""
SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
FROM public.scheme_list
"""
)
records = await cur.fetchall()
scheme_list = []
for record in records:
scheme_list.append(
{
"scheme_id": record["scheme_id"],
"scheme_name": record["scheme_name"],
"scheme_type": record["scheme_type"],
"username": record["username"],
"create_time": record["create_time"],
"scheme_start_time": record["scheme_start_time"],
"scheme_detail": record["scheme_detail"],
}
)
return scheme_list
@staticmethod
async def get_burst_locate_results(conn: AsyncConnection) -> List[dict]:
"""
查询pg数据库中, burst_locate_result 的所有记录
:param conn: 异步数据库连接
:return: 包含所有记录的列表, 每条记录为一个字典
"""
async with conn.cursor() as cur:
await cur.execute(
"""
SELECT id, type, burst_incident, leakage, detect_time, locate_result
FROM public.burst_locate_result
"""
)
records = await cur.fetchall()
results = []
for record in records:
results.append(
{
"id": record["id"],
"type": record["type"],
"burst_incident": record["burst_incident"],
"leakage": record["leakage"],
"detect_time": record["detect_time"],
"locate_result": record["locate_result"],
}
)
return results
@staticmethod
async def get_burst_locate_result_by_incident(
conn: AsyncConnection, burst_incident: str
) -> List[dict]:
"""
根据 burst_incident 查询爆管定位结果
:param conn: 异步数据库连接
:param burst_incident: 爆管事件标识
:return: 包含匹配记录的列表
"""
async with conn.cursor() as cur:
await cur.execute(
"""
SELECT id, type, burst_incident, leakage, detect_time, locate_result
FROM public.burst_locate_result
WHERE burst_incident = %s
""",
(burst_incident,),
)
records = await cur.fetchall()
results = []
for record in records:
results.append(
{
"id": record["id"],
"type": record["type"],
"burst_incident": record["burst_incident"],
"leakage": record["leakage"],
"detect_time": record["detect_time"],
"locate_result": record["locate_result"],
}
)
return results
+189
View File
@@ -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)
+1 -2
View File
@@ -1,2 +1 @@
from .database import *
from .composite_queries import CompositeQueries
from .composite_queries import CompositeQueries
+38 -56
View File
@@ -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}
-132
View File
@@ -1,132 +0,0 @@
import logging
from contextlib import asynccontextmanager
from typing import AsyncGenerator, Dict, Optional
import psycopg_pool
from psycopg.rows import dict_row
from app.infra.db.project_routing import get_project_timescale_pgconn_string
# Configure logging
logger = logging.getLogger(__name__)
class Database:
def __init__(self, db_name=None):
self.pool = None
self.db_name = db_name
self.conninfo = None
def init_pool(self, db_name=None):
"""Initialize the connection pool."""
# Use provided db_name, or the one from constructor, or default from config
target_db_name = db_name or self.db_name
# Get connection string, handling default case where target_db_name might be None
if target_db_name:
conn_string = get_project_timescale_pgconn_string(db_name=target_db_name)
else:
conn_string = get_project_timescale_pgconn_string()
self.conninfo = conn_string
try:
self.pool = psycopg_pool.AsyncConnectionPool(
conninfo=conn_string,
min_size=5,
max_size=20,
open=False, # Don't open immediately, wait for startup
kwargs={"row_factory": dict_row}, # Return rows as dictionaries
)
logger.info(
f"TimescaleDB connection pool initialized for database: {target_db_name or 'default'}"
)
except Exception as e:
logger.error(f"Failed to initialize TimescaleDB connection pool: {e}")
raise
async def open(self):
if self.pool:
await self.pool.open()
async def close(self):
"""Close the connection pool."""
if self.pool:
await self.pool.close()
logger.info("TimescaleDB connection pool closed.")
def get_pgconn_string(self, db_name=None):
"""Get the TimescaleDB connection string."""
target_db_name = db_name or self.db_name
if target_db_name:
return get_project_timescale_pgconn_string(db_name=target_db_name)
return get_project_timescale_pgconn_string()
@asynccontextmanager
async def get_connection(self) -> AsyncGenerator:
"""Get a connection from the pool."""
if not self.pool:
raise Exception("Database pool is not initialized.")
async with self.pool.connection() as conn:
yield conn
# 默认数据库实例
db = Database()
# 缓存不同数据库的实例 - 避免重复创建连接池
_database_instances: Dict[str, Database] = {}
def create_database_instance(db_name):
"""Create a new Database instance for a specific database."""
return Database(db_name=db_name)
async def get_database_instance(db_name: Optional[str] = None) -> Database:
"""Get or create a database instance for the specified database name."""
if not db_name:
return db # 返回默认数据库实例
expected_conninfo = get_project_timescale_pgconn_string(db_name=db_name)
existing = _database_instances.get(db_name)
if existing is not None and existing.conninfo != expected_conninfo:
await existing.close()
del _database_instances[db_name]
if db_name not in _database_instances:
# 创建新的数据库实例
instance = create_database_instance(db_name)
instance.init_pool()
await instance.open()
_database_instances[db_name] = instance
logger.info(f"Created new database instance for: {db_name}")
return _database_instances[db_name]
async def get_db_connection():
"""Dependency for FastAPI to get a database connection."""
async with db.get_connection() as conn:
yield conn
async def get_database_connection(db_name: Optional[str] = None):
"""
FastAPI dependency to get database connection with optional database name.
使用方法: conn: AsyncConnection = Depends(lambda: get_database_connection("your_db_name"))
或在路由函数中: conn: AsyncConnection = Depends(get_database_connection)
"""
instance = await get_database_instance(db_name)
async with instance.get_connection() as conn:
yield conn
async def cleanup_database_instances():
"""Clean up all database instances (call this on application shutdown)."""
for db_name, instance in _database_instances.items():
await instance.close()
logger.info(f"Closed database instance for: {db_name}")
_database_instances.clear()
# 关闭默认数据库
await db.close()
logger.info("All database instances cleaned up.")
+41 -80
View File
@@ -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",
@@ -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"),
)
)
@@ -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(
@@ -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),
)
@@ -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'")
+95
View File
@@ -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()
+6 -6
View File
@@ -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")
+4 -15
View File
@@ -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")
+4 -478
View File
@@ -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
-53
View File
@@ -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)
-238
View File
@@ -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
-380
View File
@@ -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
-45
View File
@@ -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))
+1
View File
@@ -0,0 +1 @@
"""Model command rewriting, cascade handling, and execution."""
+65
View File
@@ -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"
)
+244
View File
@@ -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,
}
+155
View File
@@ -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)
-90
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
"""WNDB connection, transaction, and project lifecycle infrastructure."""
+224
View File
@@ -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()
+143
View File
@@ -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)
+107
View File
@@ -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)
-368
View File
@@ -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)
-62
View File
@@ -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))
+1
View File
@@ -0,0 +1 @@
"""GIS persistence and network geometry operations."""
+44
View File
@@ -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')
@@ -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']
@@ -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})
@@ -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']))
+127
View File
@@ -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])});"
@@ -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})
+1
View File
@@ -0,0 +1 @@
"""EPANET INP import, export, and section mapping."""
@@ -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 *
@@ -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)
@@ -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]
LABELS, BACKDROP, END]
+1
View File
@@ -0,0 +1 @@
"""Water-network model persistence grouped by domain entity."""
@@ -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]:
@@ -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']
@@ -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']:
+282
View File
@@ -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"]
@@ -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})
+220
View File
@@ -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
@@ -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']
@@ -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})
@@ -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()
@@ -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
@@ -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']
@@ -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']

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