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:
@@ -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 (可选)
|
||||
# ============================================
|
||||
|
||||
@@ -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
@@ -7,4 +7,4 @@ build/
|
||||
*.dump
|
||||
.vscode/
|
||||
app/algorithms/health/model/my_survival_forest_model_quxi.joblib
|
||||
inp/
|
||||
/inp/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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']))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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,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,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,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,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,
|
||||
|
||||
@@ -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,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,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,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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)}",
|
||||
)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
@@ -1,6 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from datetime import datetime
|
||||
from psycopg import AsyncConnection
|
||||
from uuid import UUID
|
||||
|
||||
from app.infra.db.timescaledb.composite_queries import CompositeQueries
|
||||
from .dependencies import get_timescale_connection, get_postgres_connection
|
||||
@@ -13,8 +14,7 @@ async def get_scada_associated_simulation_data(
|
||||
start_time: datetime = Query(..., description="查询开始时间"),
|
||||
end_time: datetime = Query(..., description="查询结束时间"),
|
||||
device_ids: str = Query(..., description="SCADA设备ID列表,逗号分隔"),
|
||||
scheme_type: str = Query(None, description="方案类型,若为空则查询实时数据"),
|
||||
scheme_name: str = Query(None, description="方案名称,若为空则查询实时数据"),
|
||||
run_id: UUID | None = Query(None, description="分析运行 ID;为空时查询实时数据"),
|
||||
timescale_conn: AsyncConnection = Depends(get_timescale_connection),
|
||||
postgres_conn: AsyncConnection = Depends(get_postgres_connection),
|
||||
):
|
||||
@@ -22,14 +22,13 @@ async def get_scada_associated_simulation_data(
|
||||
获取SCADA关联的link/node模拟值
|
||||
|
||||
根据传入的SCADA device_ids,找到关联的link/node,
|
||||
并根据对应的type,查询对应的模拟数据。支持查询实时或方案数据。
|
||||
并根据对应的type,查询对应的模拟数据。支持查询实时或分析运行数据。
|
||||
|
||||
Args:
|
||||
start_time: 查询开始时间
|
||||
end_time: 查询结束时间
|
||||
device_ids: SCADA设备ID列表,用逗号分隔
|
||||
scheme_type: 方案类型,若为空则查询实时数据
|
||||
scheme_name: 方案名称,若为空则查询实时数据
|
||||
run_id: 分析运行 ID,若为空则查询实时数据
|
||||
timescale_conn: TimescaleDB连接
|
||||
postgres_conn: PostgreSQL连接
|
||||
|
||||
@@ -46,15 +45,14 @@ async def get_scada_associated_simulation_data(
|
||||
else []
|
||||
)
|
||||
|
||||
if scheme_type and scheme_name:
|
||||
result = await CompositeQueries.get_scada_associated_scheme_simulation_data(
|
||||
if run_id is not None:
|
||||
result = await CompositeQueries.get_scada_associated_analysis_simulation_data(
|
||||
timescale_conn,
|
||||
postgres_conn,
|
||||
device_ids_list,
|
||||
start_time,
|
||||
end_time,
|
||||
scheme_type,
|
||||
scheme_name,
|
||||
run_id,
|
||||
)
|
||||
else:
|
||||
result = (
|
||||
@@ -80,23 +78,21 @@ async def get_feature_simulation_data(
|
||||
feature_infos: str = Query(
|
||||
..., description="特征信息,格式: id1:type1,id2:type2,type为pipe(管道)或junction(节点)"
|
||||
),
|
||||
scheme_type: str = Query(None, description="方案类型,若为空则查询实时数据"),
|
||||
scheme_name: str = Query(None, description="方案名称,若为空则查询实时数据"),
|
||||
run_id: UUID | None = Query(None, description="分析运行 ID;为空时查询实时数据"),
|
||||
timescale_conn: AsyncConnection = Depends(get_timescale_connection),
|
||||
):
|
||||
"""
|
||||
获取link/node模拟值
|
||||
|
||||
根据传入的featureInfos,找到关联的link/node,
|
||||
并根据对应的type,查询对应的模拟数据。支持查询实时或方案数据。
|
||||
并根据对应的type,查询对应的模拟数据。支持查询实时或分析运行数据。
|
||||
|
||||
Args:
|
||||
start_time: 查询开始时间
|
||||
end_time: 查询结束时间
|
||||
feature_infos: 格式为 "element_id1:type1,element_id2:type2"
|
||||
例如: "P1:pipe,J1:junction"
|
||||
scheme_type: 方案类型,若为空则查询实时数据
|
||||
scheme_name: 方案名称,若为空则查询实时数据
|
||||
run_id: 分析运行 ID,若为空则查询实时数据
|
||||
timescale_conn: TimescaleDB连接
|
||||
|
||||
Returns:
|
||||
@@ -119,14 +115,13 @@ async def get_feature_simulation_data(
|
||||
if not feature_infos_list:
|
||||
raise HTTPException(status_code=400, detail="feature_infos cannot be empty")
|
||||
|
||||
if scheme_type and scheme_name:
|
||||
result = await CompositeQueries.get_scheme_simulation_data(
|
||||
if run_id is not None:
|
||||
result = await CompositeQueries.get_analysis_simulation_data(
|
||||
timescale_conn,
|
||||
feature_infos_list,
|
||||
start_time,
|
||||
end_time,
|
||||
scheme_type,
|
||||
scheme_name,
|
||||
run_id,
|
||||
)
|
||||
else:
|
||||
result = await CompositeQueries.get_realtime_simulation_data(
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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.")
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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")
|
||||
]
|
||||
@@ -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
|
||||
@@ -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 +1 @@
|
||||
from .database import *
|
||||
from .composite_queries import CompositeQueries
|
||||
from .composite_queries import CompositeQueries
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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.")
|
||||
@@ -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'")
|
||||
@@ -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()
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -0,0 +1 @@
|
||||
"""Model command rewriting, cascade handling, and execution."""
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
"""WNDB connection, transaction, and project lifecycle infrastructure."""
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -0,0 +1 @@
|
||||
"""GIS persistence and network geometry operations."""
|
||||
@@ -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']))
|
||||
@@ -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})
|
||||
@@ -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]
|
||||
@@ -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']:
|
||||
@@ -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})
|
||||
@@ -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
Reference in New Issue
Block a user