Compare commits
17
Commits
latest
...
v2026.09.11.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90b02057bc | ||
|
|
10a7a66a41 | ||
|
|
bd857ea5e1 | ||
|
|
8942541759 | ||
|
|
5966d039de | ||
|
|
9b095c7439 | ||
|
|
b74799a39d | ||
|
|
fa188af0b1 | ||
|
|
fdbcc5c033 | ||
|
|
d63d7ef1b6 | ||
|
|
6b09662de6 | ||
|
|
b21eaffe40 | ||
|
|
8853877fcd | ||
|
|
2581631b51 | ||
|
|
c250e97b87 | ||
|
|
69a7d53aff | ||
|
|
e4975b7be3 |
+6
-3
@@ -11,9 +11,12 @@ dist/
|
|||||||
package/
|
package/
|
||||||
temp/
|
temp/
|
||||||
data/
|
data/
|
||||||
# db_inp/
|
db_inp/
|
||||||
inp/
|
inp/
|
||||||
# .env
|
.env
|
||||||
|
.env.*
|
||||||
|
logs/
|
||||||
|
coverage/
|
||||||
*.pyc
|
*.pyc
|
||||||
*.dump
|
*.dump
|
||||||
app/algorithms/health/model/my_survival_forest_model_quxi.joblib
|
app/algorithms/pipe_health_prediction/model/my_survival_forest_model_quxi.joblib
|
||||||
|
|||||||
+28
-1
@@ -1,6 +1,6 @@
|
|||||||
# TJWater Server 环境变量配置模板
|
# TJWater Server 环境变量配置模板
|
||||||
# 复制此文件为 .env 并填写实际值
|
# 复制此文件为 .env 并填写实际值
|
||||||
# CI/CD: 将生产 .env 的完整内容保存为 Gitea 仓库密钥 TJWATER_SERVER_ENV。
|
# CI/CD: 生产环境变量由 Dev 主机的受控 backend.env 注入,不要将完整 .env 保存为 Gitea 仓库密钥。
|
||||||
ENVIRONMENT="production"
|
ENVIRONMENT="production"
|
||||||
NETWORK_NAME="tjwater"
|
NETWORK_NAME="tjwater"
|
||||||
# ============================================
|
# ============================================
|
||||||
@@ -39,6 +39,33 @@ METADATA_DB_PORT="5432"
|
|||||||
METADATA_DB_USER="tjwater"
|
METADATA_DB_USER="tjwater"
|
||||||
METADATA_DB_PASSWORD="password"
|
METADATA_DB_PASSWORD="password"
|
||||||
|
|
||||||
|
# Per-project synchronous connection pools
|
||||||
|
PROJECT_PG_CACHE_SIZE="16"
|
||||||
|
PROJECT_TS_CACHE_SIZE="16"
|
||||||
|
PROJECT_PG_POOL_MIN_SIZE="0"
|
||||||
|
PROJECT_PG_POOL_SIZE="4"
|
||||||
|
PROJECT_PG_MAX_OVERFLOW="2"
|
||||||
|
PROJECT_TS_POOL_MIN_SIZE="0"
|
||||||
|
PROJECT_TS_POOL_MAX_SIZE="4"
|
||||||
|
WNDB_SCHEMA_TEMPLATE_DB_NAME="tjwater_v2_schema_template"
|
||||||
|
TIMESCALEDB_SCHEMA_TEMPLATE_DB_NAME="tjwater_v2_timescale_template"
|
||||||
|
WNDB_TEMP_DB_MAX_COUNT="8"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# GeoServer 项目供应
|
||||||
|
# ============================================
|
||||||
|
GEOSERVER_URL="http://localhost:8080/geoserver"
|
||||||
|
GEOSERVER_USERNAME="admin"
|
||||||
|
GEOSERVER_PASSWORD="password"
|
||||||
|
# 留空时分别复用 DB_HOST / DB_PORT / DB_USER / DB_PASSWORD。
|
||||||
|
# GeoServer 在容器内运行时,GEOSERVER_DB_HOST 通常应填写数据库服务名。
|
||||||
|
GEOSERVER_DB_HOST=""
|
||||||
|
GEOSERVER_DB_PORT=""
|
||||||
|
GEOSERVER_DB_USER=""
|
||||||
|
GEOSERVER_DB_PASSWORD=""
|
||||||
|
# 30 天客户端缓存,单位为秒。
|
||||||
|
GEOSERVER_CLIENT_CACHE_SECONDS="2592000"
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Keycloak JWT (可选)
|
# Keycloak JWT (可选)
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|||||||
+11
-252
@@ -1,263 +1,22 @@
|
|||||||
name: Server CI/CD
|
name: Server CI/CD v2
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "v*"
|
||||||
- "latest"
|
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker-image:
|
build-test-publish-and-deploy:
|
||||||
runs-on: ubuntu-22.04
|
uses: OrgTJWater/ci-templates/.gitea/workflows/container-cd.yml@68c8a9855391baa31d31523674f5812cd24ec604
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: https://gitea.waternetwork.cn/actions/checkout@v4
|
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
image_name: gitea.waternetwork.cn/orgtjwater/tjwater-backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
- name: Normalize image metadata
|
build_context: .
|
||||||
env:
|
test_target: test
|
||||||
RAW_REGISTRY_HOST: ${{ vars.REGISTRY_HOST }}
|
deploy_service: backend
|
||||||
RAW_REPOSITORY: ${{ github.repository }}
|
deploy_host: 192.168.1.114
|
||||||
RAW_REF_NAME: ${{ github.ref_name }}
|
secrets:
|
||||||
run: |
|
|
||||||
RAW_REGISTRY_HOST="$(printf '%s' "${RAW_REGISTRY_HOST}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
||||||
|
|
||||||
if [ -z "${RAW_REGISTRY_HOST}" ]; then
|
|
||||||
echo "Missing required repository variable: REGISTRY_HOST"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
REGISTRY_HOST="${RAW_REGISTRY_HOST#http://}"
|
|
||||||
REGISTRY_HOST="${REGISTRY_HOST#https://}"
|
|
||||||
REGISTRY_HOST="${REGISTRY_HOST%/}"
|
|
||||||
|
|
||||||
if [ -z "${REGISTRY_HOST}" ]; then
|
|
||||||
echo "Repository variable REGISTRY_HOST resolves to an empty host"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
REPOSITORY_PATH="${RAW_REPOSITORY#/}"
|
|
||||||
IMAGE_OWNER="${REPOSITORY_PATH%%/*}"
|
|
||||||
IMAGE_REPOSITORY_PATH="$(printf '%s' "${IMAGE_OWNER}/tjwater-backend" | tr '[:upper:]' '[:lower:]')"
|
|
||||||
IMAGE_NAME="${REGISTRY_HOST}/${IMAGE_REPOSITORY_PATH}"
|
|
||||||
IMAGE_TAG="${RAW_REF_NAME}"
|
|
||||||
{
|
|
||||||
echo "REGISTRY_HOST=${REGISTRY_HOST}"
|
|
||||||
echo "REPOSITORY_PATH=${REPOSITORY_PATH}"
|
|
||||||
echo "IMAGE_REPOSITORY_PATH=${IMAGE_REPOSITORY_PATH}"
|
|
||||||
echo "IMAGE_NAME=${IMAGE_NAME}"
|
|
||||||
echo "IMAGE_TAG=${IMAGE_TAG}"
|
|
||||||
echo "IMAGE_REF=${IMAGE_NAME}:${IMAGE_TAG}"
|
|
||||||
} >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
- name: Login to Gitea Container Registry
|
|
||||||
env:
|
|
||||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
GITEA_SERVER_URL: ${{ github.server_url }}
|
DEV_DEPLOY_SSH_KEY: ${{ secrets.DEV_DEPLOY_SSH_KEY }}
|
||||||
run: |
|
|
||||||
if [ -z "${REGISTRY_HOST:-}" ]; then
|
|
||||||
echo "Missing resolved environment value: REGISTRY_HOST"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${REGISTRY_USERNAME}" ]; then
|
|
||||||
echo "Missing required repository secret: REGISTRY_USERNAME"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${REGISTRY_PASSWORD}" ]; then
|
|
||||||
echo "Missing required repository secret: REGISTRY_PASSWORD"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Registry username: ${REGISTRY_USERNAME}"
|
|
||||||
echo "Image target: ${IMAGE_REF}"
|
|
||||||
|
|
||||||
API_SERVER_URL="${GITEA_SERVER_URL%/}"
|
|
||||||
api_user="$(curl -fsS \
|
|
||||||
-H "Authorization: token ${REGISTRY_PASSWORD}" \
|
|
||||||
"${API_SERVER_URL}/api/v1/user" \
|
|
||||||
| sed -n 's/.*"login"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
|
|
||||||
| head -n 1 || true)"
|
|
||||||
|
|
||||||
if [ -n "${api_user}" ]; then
|
|
||||||
echo "Registry token resolves to Gitea user: ${api_user}"
|
|
||||||
else
|
|
||||||
echo "Could not resolve Gitea user from REGISTRY_PASSWORD token; docker login may still use a password or a token without API access."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Logging into registry host: ${REGISTRY_HOST}"
|
|
||||||
echo "${REGISTRY_PASSWORD}" | docker login "$REGISTRY_HOST" \
|
|
||||||
--username "${REGISTRY_USERNAME}" \
|
|
||||||
--password-stdin
|
|
||||||
|
|
||||||
- name: Materialize runtime env file
|
|
||||||
env:
|
|
||||||
TJWATER_SERVER_ENV: ${{ secrets.TJWATER_SERVER_ENV }}
|
|
||||||
run: |
|
|
||||||
if [ -z "${TJWATER_SERVER_ENV}" ]; then
|
|
||||||
echo "Missing required repository secret: TJWATER_SERVER_ENV"
|
|
||||||
echo "Store the backend .env file content as a multiline Gitea repository secret named TJWATER_SERVER_ENV."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s\n' "${TJWATER_SERVER_ENV}" > .env
|
|
||||||
chmod 600 .env
|
|
||||||
|
|
||||||
required_env_keys=(
|
|
||||||
ENVIRONMENT
|
|
||||||
NETWORK_NAME
|
|
||||||
DB_NAME
|
|
||||||
DB_HOST
|
|
||||||
DB_PORT
|
|
||||||
DB_USER
|
|
||||||
DB_PASSWORD
|
|
||||||
TIMESCALEDB_DB_NAME
|
|
||||||
TIMESCALEDB_DB_HOST
|
|
||||||
TIMESCALEDB_DB_PORT
|
|
||||||
TIMESCALEDB_DB_USER
|
|
||||||
TIMESCALEDB_DB_PASSWORD
|
|
||||||
METADATA_DB_NAME
|
|
||||||
METADATA_DB_HOST
|
|
||||||
METADATA_DB_PORT
|
|
||||||
METADATA_DB_USER
|
|
||||||
METADATA_DB_PASSWORD
|
|
||||||
DATABASE_ENCRYPTION_KEY
|
|
||||||
)
|
|
||||||
|
|
||||||
missing_keys=()
|
|
||||||
for key in "${required_env_keys[@]}"; do
|
|
||||||
if ! grep -Eq "^[[:space:]]*(export[[:space:]]+)?${key}[[:space:]]*=" .env; then
|
|
||||||
missing_keys+=("$key")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "${#missing_keys[@]}" -gt 0 ]; then
|
|
||||||
echo "TJWATER_SERVER_ENV is missing required keys: ${missing_keys[*]}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Validate workspace
|
|
||||||
run: |
|
|
||||||
if [ ! -f ./Dockerfile ]; then
|
|
||||||
echo "Dockerfile not found in workspace. Repository checkout may have failed or produced an unexpected workspace."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build and Push Image
|
|
||||||
run: |
|
|
||||||
if [ -z "${IMAGE_NAME:-}" ] || [ -z "${IMAGE_TAG:-}" ]; then
|
|
||||||
echo "Missing resolved image metadata: IMAGE_NAME or IMAGE_TAG"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
push_with_retry() {
|
|
||||||
image_ref="$1"
|
|
||||||
attempt=1
|
|
||||||
max_attempts=3
|
|
||||||
|
|
||||||
while [ "$attempt" -le "$max_attempts" ]; do
|
|
||||||
if docker push "$image_ref"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$attempt" -eq "$max_attempts" ]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Push failed for $image_ref (attempt $attempt/$max_attempts); retrying in 10s..."
|
|
||||||
attempt=$((attempt + 1))
|
|
||||||
sleep 10
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ "${IMAGE_TAG}" = "latest" ]; then
|
|
||||||
docker build \
|
|
||||||
-f ./Dockerfile \
|
|
||||||
-t "${IMAGE_NAME}:latest" \
|
|
||||||
.
|
|
||||||
push_with_retry "${IMAGE_NAME}:latest"
|
|
||||||
else
|
|
||||||
docker build \
|
|
||||||
-f ./Dockerfile \
|
|
||||||
-t "${IMAGE_NAME}:${IMAGE_TAG}" \
|
|
||||||
-t "${IMAGE_NAME}:latest" \
|
|
||||||
.
|
|
||||||
push_with_retry "${IMAGE_NAME}:${IMAGE_TAG}"
|
|
||||||
push_with_retry "${IMAGE_NAME}:latest"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Notify Deploy Server
|
|
||||||
run: |
|
|
||||||
post_deploy_webhook() {
|
|
||||||
label="$1"
|
|
||||||
payload="$2"
|
|
||||||
webhook_url="${{ vars.DEPLOY_WEBHOOK_URL }}"
|
|
||||||
token="${{ secrets.DEPLOY_WEBHOOK_TOKEN }}"
|
|
||||||
|
|
||||||
webhook_url=$(echo "$webhook_url" | xargs)
|
|
||||||
|
|
||||||
if [ -z "$webhook_url" ]; then
|
|
||||||
echo "Missing required repository variable: DEPLOY_WEBHOOK_URL"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$token" ]; then
|
|
||||||
echo "Missing required repository secret: DEPLOY_WEBHOOK_TOKEN"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[$label] Calling webhook: $webhook_url"
|
|
||||||
|
|
||||||
http_code=$(curl -sS -D /tmp/deploy_headers.txt -o /tmp/deploy_response.txt -w "%{http_code}" -X POST "$webhook_url" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Bearer $token" \
|
|
||||||
-d "$payload")
|
|
||||||
|
|
||||||
echo "[$label] webhook HTTP status: ${http_code}"
|
|
||||||
if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[$label] response headers:"
|
|
||||||
cat /tmp/deploy_headers.txt
|
|
||||||
echo "[$label] response body:"
|
|
||||||
cat /tmp/deploy_response.txt
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
PRIMARY_PAYLOAD="{\"image\":\"${IMAGE_REF}\",\"tag\":\"${IMAGE_TAG}\",\"repo\":\"${REPOSITORY_PATH}\"}"
|
|
||||||
FALLBACK_PAYLOAD="{\"image\":\"${IMAGE_REF}\",\"tag\":\"${IMAGE_TAG}\",\"repo\":\"${IMAGE_REPOSITORY_PATH}\"}"
|
|
||||||
|
|
||||||
echo "Deploy webhook target: ${{ vars.DEPLOY_WEBHOOK_URL }}"
|
|
||||||
echo "Deploy payload(primary): image=${IMAGE_REF}, tag=${IMAGE_TAG}, repo=${REPOSITORY_PATH}"
|
|
||||||
if post_deploy_webhook "primary" "$PRIMARY_PAYLOAD"; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Primary webhook request failed, retrying with lowercase repo path..."
|
|
||||||
echo "Deploy payload(fallback): image=${IMAGE_REF}, tag=${IMAGE_TAG}, repo=${IMAGE_REPOSITORY_PATH}"
|
|
||||||
if post_deploy_webhook "fallback" "$FALLBACK_PAYLOAD"; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Deploy webhook failed after primary and fallback attempts."
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
deploy-fallback-log:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
needs: docker-image
|
|
||||||
if: failure()
|
|
||||||
steps:
|
|
||||||
- name: Deployment not triggered
|
|
||||||
run: echo "Image build/push failed, deployment webhook was not called."
|
|
||||||
|
|||||||
+2
-2
@@ -6,5 +6,5 @@ build/
|
|||||||
.env
|
.env
|
||||||
*.dump
|
*.dump
|
||||||
.vscode/
|
.vscode/
|
||||||
app/algorithms/health/model/my_survival_forest_model_quxi.joblib
|
app/algorithms/pipe_health_prediction/model/my_survival_forest_model_quxi.joblib
|
||||||
inp/
|
/inp/
|
||||||
|
|||||||
@@ -35,4 +35,4 @@ Pull requests should describe the behavior change, list verification commands, m
|
|||||||
|
|
||||||
## Security & Configuration Tips
|
## Security & Configuration Tips
|
||||||
|
|
||||||
Do not commit `.env`, database dumps, generated caches, or local project data. Use `.env.example` as the configuration template. Secrets for CI/CD belong in Gitea repository secrets such as `REGISTRY_USERNAME`, `REGISTRY_PASSWORD`, and deploy webhook credentials.
|
Do not commit `.env`, database dumps, generated caches, or local project data. Use `.env.example` as the configuration template. CI/CD only uses Gitea repository secrets `REGISTRY_USERNAME`, `REGISTRY_PASSWORD`, and `DEV_DEPLOY_SSH_KEY`; production application settings are injected on the Dev host.
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ These route groups expose many command-style concatenated paths. They should not
|
|||||||
- Network object CRUD: `addjunction`, `getjunctionelevation`, `setpipediameter`, `getvalvesetting`, and similar junction/pipe/pump/tank/reservoir/valve routes
|
- Network object CRUD: `addjunction`, `getjunctionelevation`, `setpipediameter`, `getvalvesetting`, and similar junction/pipe/pump/tank/reservoir/valve routes
|
||||||
- Region/DMA/VD commands: `calculatedistrictmeteringareaforregion`, `getdistrictmeteringarea`, `generatevirtualdistrict`, and related routes
|
- Region/DMA/VD commands: `calculatedistrictmeteringareaforregion`, `getdistrictmeteringarea`, `generatevirtualdistrict`, and related routes
|
||||||
- SCADA native CRUD: `getscadadevice`, `setscadadevicedata`, `cleanscadaelement`, and related routes
|
- SCADA native CRUD: `getscadadevice`, `setscadadevicedata`, `cleanscadaelement`, and related routes
|
||||||
- Snapshot/cache utilities: `takesnapshotforoperation`, `syncwithserver`, `clearrediskey`, `queryredis`
|
- Snapshot/synchronization utilities: `takesnapshotforoperation`, `syncwithserver`
|
||||||
- Advanced simulation endpoints with underscore paths: `pressure_regulation`, `daily_scheduling_analysis`, `network_update`, `pressure_sensor_placement_kmeans`
|
- Advanced simulation endpoints with underscore paths: `pressure_regulation`, `daily_scheduling_analysis`, `network_update`, `pressure_sensor_placement_kmeans`
|
||||||
|
|
||||||
### Direct Cleanup Candidates
|
### Direct Cleanup Candidates
|
||||||
|
|||||||
+18
-7
@@ -1,4 +1,4 @@
|
|||||||
FROM condaforge/miniforge3:latest
|
FROM condaforge/miniforge3:latest AS base
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -14,18 +14,29 @@ COPY requirements.txt .
|
|||||||
RUN pip install --no-cache-dir uv
|
RUN pip install --no-cache-dir uv
|
||||||
RUN uv pip install --system --no-cache-dir -r requirements.txt
|
RUN uv pip install --system --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# 将代码放入子目录 'app',将数据放入子目录 'db_inp'
|
# 本地数据目录和环境变量在运行时通过 Compose 挂载或注入,
|
||||||
# 这样临时文件默认会生成在 /app 下,而代码在 /app/app 下,实现了分离
|
# 不应进入镜像构建上下文。
|
||||||
COPY app ./app
|
COPY app ./app
|
||||||
RUN python -c "from pathlib import Path; from zipfile import ZipFile; model_dir = Path('app/algorithms/health/model'); zip_path = model_dir / 'my_survival_forest_model_quxi.zip'; joblib_name = 'my_survival_forest_model_quxi.joblib'; joblib_path = model_dir / joblib_name; assert zip_path.exists(), f'Model archive not found: {zip_path}'; archive = ZipFile(zip_path); archive.extract(joblib_name, model_dir); archive.close(); assert joblib_path.exists(), f'Model file not extracted: {joblib_path}'" && \
|
COPY contracts ./contracts
|
||||||
rm -f app/algorithms/health/model/my_survival_forest_model_quxi.zip
|
COPY infra ./infra
|
||||||
# COPY db_inp ./db_inp
|
COPY resources ./resources
|
||||||
COPY .env .
|
RUN python -c "from pathlib import Path; from zipfile import ZipFile; model_dir = Path('app/algorithms/pipe_health_prediction/model'); zip_path = model_dir / 'my_survival_forest_model_quxi.zip'; joblib_name = 'my_survival_forest_model_quxi.joblib'; joblib_path = model_dir / joblib_name; assert zip_path.exists(), f'Model archive not found: {zip_path}'; archive = ZipFile(zip_path); archive.extract(joblib_name, model_dir); archive.close(); assert joblib_path.exists(), f'Model file not extracted: {joblib_path}'" && \
|
||||||
|
rm -f app/algorithms/pipe_health_prediction/model/my_survival_forest_model_quxi.zip
|
||||||
RUN mkdir -p db_inp temp data inp
|
RUN mkdir -p db_inp temp data inp
|
||||||
|
|
||||||
# 设置 PYTHONPATH 以便 uvicorn 找到 app 模块
|
# 设置 PYTHONPATH 以便 uvicorn 找到 app 模块
|
||||||
ENV PYTHONPATH=/app
|
ENV PYTHONPATH=/app
|
||||||
|
|
||||||
|
FROM base AS test
|
||||||
|
COPY scripts ./scripts
|
||||||
|
COPY tests ./tests
|
||||||
|
RUN python -m compileall -q app && \
|
||||||
|
python -c "import app.main" && \
|
||||||
|
python scripts/check_openapi.py && \
|
||||||
|
pytest -q tests
|
||||||
|
|
||||||
|
FROM base AS runner
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
- Python 3.12
|
- Python 3.12
|
||||||
- FastAPI / Uvicorn
|
- FastAPI / Uvicorn
|
||||||
- Pydantic / SQLAlchemy / psycopg
|
- Pydantic / SQLAlchemy / psycopg
|
||||||
- Redis、PostgreSQL、PostGIS、TimescaleDB
|
- PostgreSQL、PostGIS、TimescaleDB
|
||||||
- WNTR、EPANET、Cython、科学计算与空间分析依赖
|
- WNTR、EPANET、Cython、科学计算与空间分析依赖
|
||||||
- pytest
|
- pytest
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ app/api/ HTTP API 路由
|
|||||||
app/auth/ 认证和权限上下文
|
app/auth/ 认证和权限上下文
|
||||||
app/core/ 配置、日志和基础设施初始化
|
app/core/ 配置、日志和基础设施初始化
|
||||||
app/domain/ 领域模型和 Pydantic schema
|
app/domain/ 领域模型和 Pydantic schema
|
||||||
app/infra/ 数据库、缓存、EPANET 和外部集成
|
app/infra/ 数据库、EPANET 和外部集成
|
||||||
app/services/ 业务服务编排
|
app/services/ 业务服务编排
|
||||||
app/algorithms/ 管网算法、模拟、爆管、漏损、清洗和健康分析
|
app/algorithms/ 管网算法、模拟、爆管、漏损、清洗和健康分析
|
||||||
app/native/ 本地管网数据读写与转换
|
app/native/ 本地管网数据读写与转换
|
||||||
@@ -65,6 +65,28 @@ docker compose -f infra/docker/docker-compose.yml config
|
|||||||
- 优先复用现有 FastAPI/service/repository 边界。
|
- 优先复用现有 FastAPI/service/repository 边界。
|
||||||
- 不要把临时数据、数据库 dump、日志或本地运行产物纳入提交。
|
- 不要把临时数据、数据库 dump、日志或本地运行产物纳入提交。
|
||||||
|
|
||||||
|
## 项目数据库路由
|
||||||
|
|
||||||
|
项目级 REST 请求通过 `X-Project-Id` 解析元数据中的数据库配置:
|
||||||
|
|
||||||
|
- `biz_data` DSN 用于管网业务数据。每个物理业务库使用同名 `_template` 数据库,通过逻辑订阅只同步 `network` schema;模拟临时库从该项目模板克隆。`WNDB_SCHEMA_TEMPLATE_DB_NAME`(当前为 `tjwater_v2_schema_template`)仅用于创建空业务库和 INP 导入暂存库。
|
||||||
|
- `iot_data` DSN 用于 TimescaleDB,始终使用元数据配置的完整 DSN,不再从项目代码推导数据库名。
|
||||||
|
- 元数据、业务库和 TimescaleDB 可以部署在同一主机,也可以分别部署。
|
||||||
|
|
||||||
|
完整新建供水项目使用 `POST /api/v1/admin/project-provisions`,以
|
||||||
|
`multipart/form-data` 同时提交 `name`、小写 `code`、可选的
|
||||||
|
`description`、`gs_workspace`、`map_zoom` 和 INP `file`。工作流会按顺序完成:
|
||||||
|
|
||||||
|
1. EPANET 校验 INP,并从空结构模板创建业务库、导入模型;
|
||||||
|
2. 创建同名 `_template`,复制 32 张 `network` 表并建立逻辑订阅;
|
||||||
|
3. 从 `TIMESCALEDB_SCHEMA_TEMPLATE_DB_NAME` 创建空时序库;
|
||||||
|
4. 创建 GeoServer 工作空间、PostGIS 数据存储和 7 个 GIS 图层,将客户端缓存设为 `GEOSERVER_CLIENT_CACHE_SECONDS`;
|
||||||
|
5. 最后在一个元数据事务中写入项目、两条加密数据库路由和创建者成员关系,并将项目设为 `active`。
|
||||||
|
|
||||||
|
基础设施任一步失败时按 GeoServer、时序库、管网模板、业务库的逆序清理;元数据提交失败也执行同样清理。旧 `POST /admin/projects` 仅保留给已经由外部流程创建好的资源登记使用,并已标记为 deprecated。
|
||||||
|
|
||||||
|
使用模板复制或临时方案库的模拟功能时,`biz_data` 账号必须具备数据库创建和删除权限;只有显式删除项目时才会终止该项目的现有数据库会话,普通复制不会主动中断复制源会话。
|
||||||
|
|
||||||
## 测试与发布
|
## 测试与发布
|
||||||
|
|
||||||
提交前根据改动范围运行最小有效测试:
|
提交前根据改动范围运行最小有效测试:
|
||||||
|
|||||||
@@ -1,45 +1,5 @@
|
|||||||
"""Algorithm package with side-effect-free, lazy compatibility exports."""
|
"""Pure water-network calculation packages.
|
||||||
|
|
||||||
from importlib import import_module
|
Application workflows belong in :mod:`app.services`; database and external
|
||||||
from typing import Any
|
system access belongs in :mod:`app.infra` or :mod:`app.native`.
|
||||||
|
"""
|
||||||
|
|
||||||
_EXPORT_MODULES = {
|
|
||||||
"flow_data_clean": "app.algorithms.cleaning",
|
|
||||||
"pressure_data_clean": "app.algorithms.cleaning",
|
|
||||||
"pressure_sensor_placement_sensitivity": "app.algorithms.sensor",
|
|
||||||
"pressure_sensor_placement_kmeans": "app.algorithms.sensor",
|
|
||||||
"valve_isolation_analysis": "app.algorithms.isolation.valve",
|
|
||||||
"LeakageIdentifier": "app.algorithms.leakage",
|
|
||||||
"PipelineHealthAnalyzer": "app.algorithms.health",
|
|
||||||
"run_burst_location": "app.algorithms.burst_location",
|
|
||||||
**{
|
|
||||||
name: "app.algorithms.simulation.scenarios"
|
|
||||||
for name in (
|
|
||||||
"convert_to_local_unit",
|
|
||||||
"burst_analysis",
|
|
||||||
"valve_close_analysis",
|
|
||||||
"flushing_analysis",
|
|
||||||
"contaminant_simulation",
|
|
||||||
"age_analysis",
|
|
||||||
"pressure_regulation",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
__all__ = list(_EXPORT_MODULES)
|
|
||||||
|
|
||||||
|
|
||||||
def __getattr__(name: str) -> Any:
|
|
||||||
try:
|
|
||||||
module_name = _EXPORT_MODULES[name]
|
|
||||||
except KeyError as exc:
|
|
||||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from exc
|
|
||||||
|
|
||||||
value = getattr(import_module(module_name), name)
|
|
||||||
globals()[name] = value
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def __dir__() -> list[str]:
|
|
||||||
return sorted({*globals(), *__all__})
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
from app.algorithms.burst_detection.burst_detector import BurstDetector
|
from app.algorithms.burst_detection.pressure_anomaly import PressureAnomalyDetector
|
||||||
|
|
||||||
__all__ = ["BurstDetector"]
|
__all__ = ["PressureAnomalyDetector"]
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ PressureDataInput = (
|
|||||||
IGNORED_OBSERVATION_COLUMNS = {"time", "timestamp", "datetime", "date"}
|
IGNORED_OBSERVATION_COLUMNS = {"time", "timestamp", "datetime", "date"}
|
||||||
|
|
||||||
|
|
||||||
class BurstDetector:
|
class PressureAnomalyDetector:
|
||||||
"""FFT + IsolationForest based burst detection for daily aligned pressure data."""
|
"""FFT + IsolationForest based burst detection for daily aligned pressure data."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .pipeline import run_burst_location
|
||||||
|
|
||||||
|
__all__ = ["run_burst_location"]
|
||||||
+3
-4
@@ -11,13 +11,13 @@ import networkx as nx
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from .leak_simulator import cal_signature_pipe_multi_pf
|
from .leak_signature import cal_signature_pipe_multi_pf
|
||||||
from .network_partitioner import (
|
from .topology_partitioning import (
|
||||||
cal_group_num,
|
cal_group_num,
|
||||||
metis_grouping_pipe_weight,
|
metis_grouping_pipe_weight,
|
||||||
visualize_metis_partition,
|
visualize_metis_partition,
|
||||||
)
|
)
|
||||||
from .similarity_calculator import (
|
from .similarity_metrics import (
|
||||||
adjust_ratio,
|
adjust_ratio,
|
||||||
cal_similarity_all_multi_new_sq_improve_double_lzr,
|
cal_similarity_all_multi_new_sq_improve_double_lzr,
|
||||||
decode_mode,
|
decode_mode,
|
||||||
@@ -769,4 +769,3 @@ def DN_search_multi_simple_add_flow_count_new(
|
|||||||
final_candidates_csv,
|
final_candidates_csv,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
+4
-108
@@ -1,5 +1,3 @@
|
|||||||
import argparse
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
from multiprocessing import cpu_count
|
from multiprocessing import cpu_count
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -7,12 +5,12 @@ from typing import Any, Iterable
|
|||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from app.algorithms.burst_location import leak_simulator
|
from app.algorithms.burst_localization import leak_signature
|
||||||
|
|
||||||
from .burst_locator import (
|
from .candidate_ranking import (
|
||||||
DN_search_multi_simple_add_flow_count_new,
|
DN_search_multi_simple_add_flow_count_new,
|
||||||
)
|
)
|
||||||
from .network_model import (
|
from .topology_model import (
|
||||||
_build_node_pipe_maps,
|
_build_node_pipe_maps,
|
||||||
cal_node_coordinate,
|
cal_node_coordinate,
|
||||||
construct_graph,
|
construct_graph,
|
||||||
@@ -26,35 +24,6 @@ DEFAULT_N_WORKERS = max(1, min(cpu_count() - 1, 4))
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _read_id_list_json(path):
|
|
||||||
if path is None:
|
|
||||||
return None
|
|
||||||
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
||||||
if isinstance(data, list):
|
|
||||||
return [str(item) for item in data]
|
|
||||||
if isinstance(data, dict):
|
|
||||||
if "ids" in data and isinstance(data["ids"], list):
|
|
||||||
return [str(item) for item in data["ids"]]
|
|
||||||
raise ValueError(f"ID JSON must be list or dict with key 'ids': {path}")
|
|
||||||
raise ValueError(f"Unsupported ID JSON format: {path}")
|
|
||||||
|
|
||||||
|
|
||||||
def _read_series_csv(path):
|
|
||||||
if path is None:
|
|
||||||
return None
|
|
||||||
df = pd.read_csv(path)
|
|
||||||
if df.shape[1] < 2:
|
|
||||||
raise ValueError(f"CSV must contain at least two columns (id,value): {path}")
|
|
||||||
if {"id", "value"}.issubset(df.columns):
|
|
||||||
id_col, value_col = "id", "value"
|
|
||||||
else:
|
|
||||||
id_col, value_col = df.columns[0], df.columns[1]
|
|
||||||
series = pd.Series(
|
|
||||||
df[value_col].values, index=df[id_col].astype(str).values, dtype=float
|
|
||||||
)
|
|
||||||
return series
|
|
||||||
|
|
||||||
|
|
||||||
def _align_scada_series(
|
def _align_scada_series(
|
||||||
series: pd.Series, ids: Iterable[str], series_name: str
|
series: pd.Series, ids: Iterable[str], series_name: str
|
||||||
) -> pd.Series:
|
) -> pd.Series:
|
||||||
@@ -159,7 +128,7 @@ def run_burst_location(
|
|||||||
pipe_diameter,
|
pipe_diameter,
|
||||||
) = read_inf_inp(wn)
|
) = read_inf_inp(wn)
|
||||||
|
|
||||||
candidate_pipe, _ = leak_simulator.cal_possible_pipe(
|
candidate_pipe, _ = leak_signature.cal_possible_pipe(
|
||||||
burst_leakage, all_pipe, pipe_diameter
|
burst_leakage, all_pipe, pipe_diameter
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -267,76 +236,3 @@ def run_burst_location(
|
|||||||
"final_candidates_csv": final_candidates_csv,
|
"final_candidates_csv": final_candidates_csv,
|
||||||
"stage_timing_seconds": stage_timing,
|
"stage_timing_seconds": stage_timing,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _parse_args():
|
|
||||||
parser = argparse.ArgumentParser(description="爆管定位主函数入口")
|
|
||||||
parser.add_argument("--wn-inp", required=True, help="EPANET inp 文件路径")
|
|
||||||
parser.add_argument(
|
|
||||||
"--pressure-ids-json", required=True, help="压力SCADA ID列表 JSON 文件"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--flow-ids-json", default=None, help="(可选)流量SCADA ID列表 JSON 文件"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--burst-pressure-csv", required=True, help="爆管时压力 CSV(id,value)"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--normal-pressure-csv", required=True, help="正常时压力 CSV(id,value)"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--burst-flow-csv", default=None, help="(可选)爆管时流量 CSV(id,value)"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--normal-flow-csv", default=None, help="(可选)正常时流量 CSV(id,value)"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--burst-leakage", type=float, required=True, help="爆管漏损流量"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--min-dpressure",
|
|
||||||
type=float,
|
|
||||||
default=2.0,
|
|
||||||
help="(可选)最小压降阈值,默认 2.0",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--basic-pressure",
|
|
||||||
type=float,
|
|
||||||
default=10.0,
|
|
||||||
help="(可选)基础服务压力,默认 10.0",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--n-workers",
|
|
||||||
type=int,
|
|
||||||
default=DEFAULT_N_WORKERS,
|
|
||||||
help="(可选)特征中心模拟进程数,默认 max(1, min(cpu_count()-1, 4))",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--final-candidates-csv-path",
|
|
||||||
default="temp/burst_location/final_round_candidates.csv",
|
|
||||||
help="(可选)最后一轮候选管道明细 CSV 输出路径",
|
|
||||||
)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
args = _parse_args()
|
|
||||||
result = run_burst_location(
|
|
||||||
wn_inp_path=args.wn_inp,
|
|
||||||
pressure_scada_ids=_read_id_list_json(args.pressure_ids_json),
|
|
||||||
burst_pressure=_read_series_csv(args.burst_pressure_csv),
|
|
||||||
normal_pressure=_read_series_csv(args.normal_pressure_csv),
|
|
||||||
burst_leakage=args.burst_leakage,
|
|
||||||
flow_scada_ids=_read_id_list_json(args.flow_ids_json),
|
|
||||||
burst_flow=_read_series_csv(args.burst_flow_csv),
|
|
||||||
normal_flow=_read_series_csv(args.normal_flow_csv),
|
|
||||||
min_dpressure=args.min_dpressure,
|
|
||||||
basic_pressure=args.basic_pressure,
|
|
||||||
n_workers=args.n_workers,
|
|
||||||
final_candidates_csv_path=args.final_candidates_csv_path,
|
|
||||||
)
|
|
||||||
print(json.dumps(result, ensure_ascii=False))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
+1
-2
@@ -6,7 +6,7 @@ import random
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from .leak_simulator import simple_add_leak, simple_recover_wn, simple_simulation_pf
|
from .leak_signature import simple_add_leak, simple_recover_wn, simple_simulation_pf
|
||||||
|
|
||||||
|
|
||||||
def add_noise_pd(data, noise_type, noise_para):
|
def add_noise_pd(data, noise_type, noise_para):
|
||||||
@@ -195,4 +195,3 @@ def change_para_of_wn(wn, pipe_roughness_change):
|
|||||||
pipe.roughness = pipe_roughness_change[pipe_name]
|
pipe.roughness = pipe_roughness_change[pipe_name]
|
||||||
return wn
|
return wn
|
||||||
|
|
||||||
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .burst_location import run_burst_location
|
|
||||||
|
|
||||||
__all__ = ["run_burst_location"]
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
from app.algorithms.cleaning import flow as _flow_module
|
|
||||||
from app.algorithms.cleaning import pressure as _pressure_module
|
|
||||||
|
|
||||||
|
|
||||||
############################################################
|
|
||||||
# 流量监测数据清洗 ***卡尔曼滤波法***
|
|
||||||
############################################################
|
|
||||||
# 2025/08/21 hxyan
|
|
||||||
|
|
||||||
|
|
||||||
def flow_data_clean(input_csv_file: str) -> str:
|
|
||||||
"""
|
|
||||||
读取 input_csv_path 中的每列时间序列,使用一维 Kalman 滤波平滑并用预测值替换基于 3σ 检测出的异常点。
|
|
||||||
保存输出为:<input_filename>_cleaned.xlsx(与输入同目录),并返回输出文件的绝对路径。如有同名文件存在,则覆盖。
|
|
||||||
:param: input_csv_file: 输入的 CSV 文件明或路径
|
|
||||||
:return: 输出文件的绝对路径
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 提供的 input_csv_path 绝对路径,以下为 默认脚本目录下同名 CSV 文件,构建绝对路径,可根据情况修改
|
|
||||||
# 使用 algorithms 根目录保持与原 data_cleaning.py 一致的行为
|
|
||||||
script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
input_csv_path = os.path.join(script_dir, input_csv_file)
|
|
||||||
|
|
||||||
# 检查文件是否存在
|
|
||||||
if not os.path.exists(input_csv_path):
|
|
||||||
raise FileNotFoundError(f"指定的文件不存在: {input_csv_path}")
|
|
||||||
# 调用 clean_flow_data_kf 函数进行数据清洗
|
|
||||||
out_xlsx_path = _flow_module.clean_flow_data_kf(input_csv_path)
|
|
||||||
print("清洗后的数据已保存到:", out_xlsx_path)
|
|
||||||
|
|
||||||
|
|
||||||
############################################################
|
|
||||||
# 压力监测数据清洗 ***kmean++法***
|
|
||||||
############################################################
|
|
||||||
# 2025/08/21 hxyan
|
|
||||||
|
|
||||||
|
|
||||||
def pressure_data_clean(input_csv_file: str) -> str:
|
|
||||||
"""
|
|
||||||
读取 input_csv_path 中的每列时间序列,使用Kmean++清洗数据。
|
|
||||||
保存输出为:<input_filename>_cleaned.xlsx(与输入同目录),并返回输出文件的绝对路径。如有同名文件存在,则覆盖。
|
|
||||||
原始数据在 sheet 'raw_pressure_data',处理后数据在 sheet 'cleaned_pressusre_data'。
|
|
||||||
:param input_csv_path: 输入的 CSV 文件路径
|
|
||||||
:return: 输出文件的绝对路径
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 提供的 input_csv_path 绝对路径,以下为 默认脚本目录下同名 CSV 文件,构建绝对路径,可根据情况修改
|
|
||||||
# 使用 algorithms 根目录保持与原 data_cleaning.py 一致的行为
|
|
||||||
script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
input_csv_path = os.path.join(script_dir, input_csv_file)
|
|
||||||
|
|
||||||
# 检查文件是否存在
|
|
||||||
if not os.path.exists(input_csv_path):
|
|
||||||
raise FileNotFoundError(f"指定的文件不存在: {input_csv_path}")
|
|
||||||
# 调用 clean_pressure_data_km 函数进行数据清洗
|
|
||||||
out_xlsx_path = _pressure_module.clean_pressure_data_km(input_csv_path)
|
|
||||||
print("清洗后的数据已保存到:", out_xlsx_path)
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""Demand allocation calculations."""
|
||||||
|
|
||||||
|
from .pipe_length_weighted import allocate_demand_by_pipe_length
|
||||||
|
|
||||||
|
__all__ = ["allocate_demand_by_pipe_length"]
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"""Pipe-length-weighted demand allocation.
|
||||||
|
|
||||||
|
This module deliberately accepts plain topology data and performs no database
|
||||||
|
or file access. Application services are responsible for loading topology.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
|
||||||
|
def allocate_demand_by_pipe_length(
|
||||||
|
demand: float,
|
||||||
|
topology_nodes: Mapping[str, Mapping[str, Any]],
|
||||||
|
topology_links: Mapping[str, Mapping[str, Any]],
|
||||||
|
) -> dict[str, float]:
|
||||||
|
"""Allocate total demand to junctions by half of each incident link length."""
|
||||||
|
|
||||||
|
if not topology_nodes or not topology_links or demand == 0.0:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
total_link_length = sum(
|
||||||
|
abs(float(link["length"])) for link in topology_links.values()
|
||||||
|
)
|
||||||
|
if total_link_length <= 0.0:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
demand_per_length = demand / total_link_length
|
||||||
|
result: dict[str, float] = {}
|
||||||
|
for node_id, node in topology_nodes.items():
|
||||||
|
if node["type"] != "junction":
|
||||||
|
continue
|
||||||
|
incident_length = sum(
|
||||||
|
abs(float(topology_links[link_id]["length"]))
|
||||||
|
for link_id in node["links"]
|
||||||
|
)
|
||||||
|
result[node_id] = incident_length * demand_per_length * 0.5
|
||||||
|
return result
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from app.algorithms.dma_leakage_estimation.genetic_optimizer import DmaLeakageOptimizer
|
||||||
|
|
||||||
|
__all__ = ["DmaLeakageOptimizer"]
|
||||||
+3
-49
@@ -3,7 +3,6 @@ import numpy as np
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import argparse
|
|
||||||
from multiprocessing import Pool, cpu_count
|
from multiprocessing import Pool, cpu_count
|
||||||
from typing import Any, List, Dict, Union
|
from typing import Any, List, Dict, Union
|
||||||
|
|
||||||
@@ -70,7 +69,7 @@ def _worker_init(
|
|||||||
|
|
||||||
def _worker_evaluate(raw_ratios: np.ndarray) -> float:
|
def _worker_evaluate(raw_ratios: np.ndarray) -> float:
|
||||||
d = _worker_data
|
d = _worker_data
|
||||||
effective_ratio_map = LeakageIdentifier._effective_area_ratios(
|
effective_ratio_map = DmaLeakageOptimizer._effective_area_ratios(
|
||||||
raw_ratios,
|
raw_ratios,
|
||||||
d["area_ids"],
|
d["area_ids"],
|
||||||
d["nodes_by_area"],
|
d["nodes_by_area"],
|
||||||
@@ -121,7 +120,7 @@ def _worker_evaluate(raw_ratios: np.ndarray) -> float:
|
|||||||
_cleanup_temp_files(prefix)
|
_cleanup_temp_files(prefix)
|
||||||
|
|
||||||
|
|
||||||
class LeakageIdentifier:
|
class DmaLeakageOptimizer:
|
||||||
FLOW_UNIT_TO_M3S = {
|
FLOW_UNIT_TO_M3S = {
|
||||||
"m3/s": 1.0,
|
"m3/s": 1.0,
|
||||||
"m³/s": 1.0,
|
"m³/s": 1.0,
|
||||||
@@ -543,7 +542,7 @@ class LeakageProblem(Problem):
|
|||||||
leak_ratios = x
|
leak_ratios = x
|
||||||
|
|
||||||
# 将漏损分布归一化
|
# 将漏损分布归一化
|
||||||
effective_ratio_map = LeakageIdentifier._effective_area_ratios(
|
effective_ratio_map = DmaLeakageOptimizer._effective_area_ratios(
|
||||||
leak_ratios,
|
leak_ratios,
|
||||||
self.area_ids,
|
self.area_ids,
|
||||||
self.nodes_by_area,
|
self.nodes_by_area,
|
||||||
@@ -608,48 +607,3 @@ class LeakageProblem(Problem):
|
|||||||
self._pool.close()
|
self._pool.close()
|
||||||
self._pool.join()
|
self._pool.join()
|
||||||
self._pool = None
|
self._pool = None
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
parser = argparse.ArgumentParser(description="漏损区域识别")
|
|
||||||
parser.add_argument("--inp", required=True, help=".inp 文件路径")
|
|
||||||
parser.add_argument("--map", help="节点-区域映射 CSV 路径")
|
|
||||||
parser.add_argument("--scada", help="SCADA 压力 CSV 路径 (观测数据)")
|
|
||||||
parser.add_argument("--sensors", help="传感器节点 ID 列表 (逗号分隔)")
|
|
||||||
parser.add_argument("--output", default="Results", help="输出目录")
|
|
||||||
|
|
||||||
parser.add_argument("--pop_size", type=int, default=50, help="种群大小")
|
|
||||||
parser.add_argument("--max_gen", type=int, default=100, help="最大代数")
|
|
||||||
parser.add_argument("--duration", type=float, default=24, help="模拟时长(小时)")
|
|
||||||
parser.add_argument("--q_sum", type=float, default=0.241, help="总漏损流量")
|
|
||||||
parser.add_argument(
|
|
||||||
"--q_sum_unit",
|
|
||||||
default="m3/s",
|
|
||||||
choices=list(LeakageIdentifier.FLOW_UNIT_TO_M3S.keys()),
|
|
||||||
help="q_sum 输入单位(建议与现场习惯一致,内部统一换算为 m3/s)",
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if not args.map or not args.scada or not args.sensors:
|
|
||||||
parser.error("--map、--scada、--sensors 为必填")
|
|
||||||
|
|
||||||
q_sum_m3s = LeakageIdentifier._flow_to_m3s(args.q_sum, args.q_sum_unit)
|
|
||||||
|
|
||||||
sensors = [sensor.strip() for sensor in args.sensors.split(",") if sensor.strip()]
|
|
||||||
identifier = LeakageIdentifier(
|
|
||||||
args.inp, sensors, args.map, duration=args.duration, q_sum=q_sum_m3s
|
|
||||||
)
|
|
||||||
|
|
||||||
identifier.run_identification(
|
|
||||||
args.scada,
|
|
||||||
args.output,
|
|
||||||
pop_size=args.pop_size,
|
|
||||||
max_gen=args.max_gen,
|
|
||||||
output_flow_unit=args.q_sum_unit,
|
|
||||||
)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
"""Pure topology partitioning used by DMA leakage estimation."""
|
||||||
|
|
||||||
|
import math
|
||||||
|
from collections import deque
|
||||||
|
from typing import Any, Iterable, Mapping
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def build_dma_partitions(
|
||||||
|
sensor_nodes: list[str],
|
||||||
|
node_coords: Mapping[str, Mapping[str, Any]],
|
||||||
|
link_entries: Iterable[str],
|
||||||
|
dma_count: int | None,
|
||||||
|
) -> tuple[dict[str, str], list[dict[str, Any]]]:
|
||||||
|
"""Assign every topology node to a sensor-seeded virtual DMA."""
|
||||||
|
|
||||||
|
all_nodes = list(node_coords)
|
||||||
|
if not all_nodes:
|
||||||
|
raise ValueError("管网中未获取到可分区节点。")
|
||||||
|
available_sensors = [node for node in sensor_nodes if node in node_coords]
|
||||||
|
if not available_sensors:
|
||||||
|
raise ValueError("无可用压力传感器,无法生成虚拟分区。")
|
||||||
|
|
||||||
|
area_count = _resolve_dma_count(dma_count, available_sensors, all_nodes)
|
||||||
|
sensor_area_map = _cluster_sensors_to_areas(
|
||||||
|
available_sensors, node_coords, area_count
|
||||||
|
)
|
||||||
|
adjacency = _build_adjacency(link_entries, all_nodes)
|
||||||
|
distance_by_sensor = {
|
||||||
|
sensor: _bfs_distances(adjacency, sensor) for sensor in available_sensors
|
||||||
|
}
|
||||||
|
|
||||||
|
assignment_count = {sensor: 0 for sensor in available_sensors}
|
||||||
|
area_map: dict[str, str] = {}
|
||||||
|
for node_id in sorted(all_nodes):
|
||||||
|
sensor = _choose_sensor_for_node(
|
||||||
|
node_id,
|
||||||
|
available_sensors,
|
||||||
|
node_coords,
|
||||||
|
distance_by_sensor,
|
||||||
|
assignment_count,
|
||||||
|
)
|
||||||
|
assignment_count[sensor] += 1
|
||||||
|
area_map[node_id] = sensor_area_map[sensor]
|
||||||
|
|
||||||
|
return area_map, _build_area_meta(area_map, sensor_area_map)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_dma_count(
|
||||||
|
dma_count: int | None, sensor_nodes: list[str], all_nodes: list[str]
|
||||||
|
) -> int:
|
||||||
|
if dma_count is None:
|
||||||
|
return min(len(sensor_nodes), len(all_nodes))
|
||||||
|
if dma_count <= 0:
|
||||||
|
raise ValueError("dma_count 必须大于 0。")
|
||||||
|
if dma_count > len(all_nodes):
|
||||||
|
raise ValueError("dma_count 不能大于可分区节点数量。")
|
||||||
|
if dma_count > len(sensor_nodes):
|
||||||
|
raise ValueError("dma_count 不能大于可用传感器数量。")
|
||||||
|
return dma_count
|
||||||
|
|
||||||
|
|
||||||
|
def _cluster_sensors_to_areas(
|
||||||
|
sensor_nodes: list[str],
|
||||||
|
node_coords: Mapping[str, Mapping[str, Any]],
|
||||||
|
area_count: int,
|
||||||
|
) -> dict[str, str]:
|
||||||
|
if area_count >= len(sensor_nodes):
|
||||||
|
return {sensor: str(index + 1) for index, sensor in enumerate(sensor_nodes)}
|
||||||
|
|
||||||
|
points = np.array(
|
||||||
|
[
|
||||||
|
[float(node_coords[sensor]["x"]), float(node_coords[sensor]["y"])]
|
||||||
|
for sensor in sensor_nodes
|
||||||
|
],
|
||||||
|
dtype=float,
|
||||||
|
)
|
||||||
|
centers = points[:area_count].copy()
|
||||||
|
labels = np.full(points.shape[0], -1, dtype=int)
|
||||||
|
for _ in range(20):
|
||||||
|
distances_squared = (
|
||||||
|
(points[:, None, :] - centers[None, :, :]) ** 2
|
||||||
|
).sum(axis=2)
|
||||||
|
next_labels = distances_squared.argmin(axis=1)
|
||||||
|
if np.array_equal(labels, next_labels):
|
||||||
|
break
|
||||||
|
labels = next_labels
|
||||||
|
for index in range(area_count):
|
||||||
|
cluster_points = points[labels == index]
|
||||||
|
if cluster_points.size > 0:
|
||||||
|
centers[index] = cluster_points.mean(axis=0)
|
||||||
|
labels = _restore_empty_area_labels(labels, points, centers, area_count)
|
||||||
|
return {
|
||||||
|
sensor: str(int(labels[index]) + 1)
|
||||||
|
for index, sensor in enumerate(sensor_nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _restore_empty_area_labels(
|
||||||
|
labels: np.ndarray,
|
||||||
|
points: np.ndarray,
|
||||||
|
centers: np.ndarray,
|
||||||
|
area_count: int,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Keep every requested area represented when coordinates are degenerate."""
|
||||||
|
|
||||||
|
labels = labels.copy()
|
||||||
|
for missing_area in sorted(set(range(area_count)) - set(labels.tolist())):
|
||||||
|
area_sizes = {
|
||||||
|
area: int(np.count_nonzero(labels == area)) for area in range(area_count)
|
||||||
|
}
|
||||||
|
donor_area = max(
|
||||||
|
(area for area, size in area_sizes.items() if size > 1),
|
||||||
|
key=lambda area: (area_sizes[area], -area),
|
||||||
|
)
|
||||||
|
donor_indices = np.flatnonzero(labels == donor_area)
|
||||||
|
replacement_index = max(
|
||||||
|
(int(index) for index in donor_indices),
|
||||||
|
key=lambda index: (
|
||||||
|
float(np.sum((points[index] - centers[donor_area]) ** 2)),
|
||||||
|
index,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
labels[replacement_index] = missing_area
|
||||||
|
centers[missing_area] = points[replacement_index]
|
||||||
|
return labels
|
||||||
|
|
||||||
|
|
||||||
|
def _build_adjacency(
|
||||||
|
link_entries: Iterable[str], all_nodes: list[str]
|
||||||
|
) -> dict[str, set[str]]:
|
||||||
|
adjacency: dict[str, set[str]] = {node: set() for node in all_nodes}
|
||||||
|
for link in link_entries:
|
||||||
|
parts = str(link).split(":")
|
||||||
|
if len(parts) < 4:
|
||||||
|
continue
|
||||||
|
node1, node2 = parts[-2], parts[-1]
|
||||||
|
if node1 in adjacency and node2 in adjacency:
|
||||||
|
adjacency[node1].add(node2)
|
||||||
|
adjacency[node2].add(node1)
|
||||||
|
return adjacency
|
||||||
|
|
||||||
|
|
||||||
|
def _bfs_distances(adjacency: Mapping[str, set[str]], start: str) -> dict[str, int]:
|
||||||
|
distances = {start: 0}
|
||||||
|
queue: deque[str] = deque([start])
|
||||||
|
while queue:
|
||||||
|
node = queue.popleft()
|
||||||
|
for neighbor in adjacency.get(node, set()):
|
||||||
|
if neighbor in distances:
|
||||||
|
continue
|
||||||
|
distances[neighbor] = distances[node] + 1
|
||||||
|
queue.append(neighbor)
|
||||||
|
return distances
|
||||||
|
|
||||||
|
|
||||||
|
def _choose_sensor_for_node(
|
||||||
|
node_id: str,
|
||||||
|
sensors: list[str],
|
||||||
|
node_coords: Mapping[str, Mapping[str, Any]],
|
||||||
|
distance_by_sensor: Mapping[str, Mapping[str, int]],
|
||||||
|
assignment_count: Mapping[str, int],
|
||||||
|
) -> str:
|
||||||
|
min_distance: int | None = None
|
||||||
|
candidates: list[str] = []
|
||||||
|
for sensor in sensors:
|
||||||
|
distance = distance_by_sensor.get(sensor, {}).get(node_id)
|
||||||
|
if distance is None:
|
||||||
|
continue
|
||||||
|
if min_distance is None or distance < min_distance:
|
||||||
|
min_distance = distance
|
||||||
|
candidates = [sensor]
|
||||||
|
elif distance == min_distance:
|
||||||
|
candidates.append(sensor)
|
||||||
|
if not candidates:
|
||||||
|
node_coord = node_coords[node_id]
|
||||||
|
return min(
|
||||||
|
sensors,
|
||||||
|
key=lambda sensor: math.hypot(
|
||||||
|
float(node_coord["x"]) - float(node_coords[sensor]["x"]),
|
||||||
|
float(node_coord["y"]) - float(node_coords[sensor]["y"]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return min(candidates, key=lambda sensor: (assignment_count[sensor], sensor))
|
||||||
|
|
||||||
|
|
||||||
|
def _build_area_meta(
|
||||||
|
area_map: Mapping[str, str], sensor_area_map: Mapping[str, str]
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
nodes_by_area: dict[str, list[str]] = {}
|
||||||
|
for node_id, area_id in area_map.items():
|
||||||
|
nodes_by_area.setdefault(area_id, []).append(node_id)
|
||||||
|
sensors_by_area: dict[str, list[str]] = {}
|
||||||
|
for sensor, area_id in sensor_area_map.items():
|
||||||
|
sensors_by_area.setdefault(area_id, []).append(sensor)
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"area_id": area_id,
|
||||||
|
"sensor_nodes": sorted(sensors_by_area.get(area_id, [])),
|
||||||
|
"node_ids": sorted(nodes_by_area[area_id]),
|
||||||
|
"node_count": len(nodes_by_area[area_id]),
|
||||||
|
}
|
||||||
|
for area_id in sorted(nodes_by_area, key=int)
|
||||||
|
]
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from app.algorithms.health.analyzer import PipelineHealthAnalyzer
|
|
||||||
|
|
||||||
__all__ = ["PipelineHealthAnalyzer"]
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from app.algorithms.isolation.valve import valve_isolation_analysis
|
|
||||||
|
|
||||||
__all__ = ["valve_isolation_analysis"]
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
from collections import defaultdict, deque
|
|
||||||
from functools import lru_cache
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from app.services.tjnetwork import (
|
|
||||||
get_network_link_nodes,
|
|
||||||
is_node,
|
|
||||||
get_link_properties,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
VALVE_LINK_TYPE = "valve"
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_link_entry(link_entry: str) -> tuple[str, str, str, str]:
|
|
||||||
parts = link_entry.split(":", 3)
|
|
||||||
if len(parts) != 4:
|
|
||||||
raise ValueError(f"Invalid link entry format: {link_entry}")
|
|
||||||
return parts[0], parts[1], parts[2], parts[3]
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=16)
|
|
||||||
def _get_network_topology(network: str):
|
|
||||||
"""
|
|
||||||
解析并缓存网络拓扑,大幅减少重复的 API 调用和字符串解析开销。
|
|
||||||
返回:
|
|
||||||
- pipe_adj: 永久连通的管道/泵邻接表 (dict[str, set])
|
|
||||||
- all_valves: 所有阀门字典 {id: (n1, n2)}
|
|
||||||
- link_lookup: 链路快速查表 {id: (n1, n2, type)} 用于快速定位事故点
|
|
||||||
- node_set: 所有已知节点集合
|
|
||||||
"""
|
|
||||||
pipe_adj = defaultdict(set)
|
|
||||||
all_valves = {}
|
|
||||||
link_lookup = {}
|
|
||||||
node_set = set()
|
|
||||||
|
|
||||||
# 此处假设 get_network_link_nodes 获取全网数据
|
|
||||||
for link_entry in get_network_link_nodes(network):
|
|
||||||
link_id, link_type, node1, node2 = _parse_link_entry(link_entry)
|
|
||||||
link_type_name = str(link_type).lower()
|
|
||||||
|
|
||||||
link_lookup[link_id] = (node1, node2, link_type_name)
|
|
||||||
node_set.add(node1)
|
|
||||||
node_set.add(node2)
|
|
||||||
|
|
||||||
if link_type_name == VALVE_LINK_TYPE:
|
|
||||||
all_valves[link_id] = (node1, node2)
|
|
||||||
else:
|
|
||||||
# 只有非阀门(管道/泵)才进入永久连通图
|
|
||||||
pipe_adj[node1].add(node2)
|
|
||||||
pipe_adj[node2].add(node1)
|
|
||||||
|
|
||||||
return pipe_adj, all_valves, link_lookup, node_set
|
|
||||||
|
|
||||||
|
|
||||||
def valve_isolation_analysis(
|
|
||||||
network: str, accident_elements: str | list[str], disabled_valves: list[str] = None
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
关阀搜索/分析:基于拓扑结构确定事故隔离所需关阀。
|
|
||||||
:param network: 模型名称
|
|
||||||
:param accident_elements: 事故点(节点或管道/泵/阀门ID),可以是单个ID字符串或ID列表
|
|
||||||
:param disabled_valves: 故障/无法关闭的阀门ID列表
|
|
||||||
:return: dict,包含受影响节点、必须关闭阀门、可选阀门等信息
|
|
||||||
"""
|
|
||||||
if disabled_valves is None:
|
|
||||||
disabled_valves_set = set()
|
|
||||||
else:
|
|
||||||
disabled_valves_set = set(disabled_valves)
|
|
||||||
|
|
||||||
if isinstance(accident_elements, str):
|
|
||||||
target_elements = [accident_elements]
|
|
||||||
else:
|
|
||||||
target_elements = accident_elements
|
|
||||||
|
|
||||||
# 1. 获取缓存拓扑 (极快,无 IO)
|
|
||||||
pipe_adj, all_valves, link_lookup, node_set = _get_network_topology(network)
|
|
||||||
|
|
||||||
# 2. 确定起点,优先查表避免 API 调用
|
|
||||||
start_nodes = set()
|
|
||||||
for element in target_elements:
|
|
||||||
if element in node_set:
|
|
||||||
start_nodes.add(element)
|
|
||||||
elif element in link_lookup:
|
|
||||||
n1, n2, _ = link_lookup[element]
|
|
||||||
start_nodes.add(n1)
|
|
||||||
start_nodes.add(n2)
|
|
||||||
else:
|
|
||||||
# 仅当缓存中没找到时(极少见),才回退到慢速 API
|
|
||||||
if is_node(network, element):
|
|
||||||
start_nodes.add(element)
|
|
||||||
else:
|
|
||||||
props = get_link_properties(network, element)
|
|
||||||
n1, n2 = props.get("node1"), props.get("node2")
|
|
||||||
if n1 and n2:
|
|
||||||
start_nodes.add(n1)
|
|
||||||
start_nodes.add(n2)
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"Accident element {element} invalid or missing endpoints"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 3. 处理故障阀门 (构建临时增量图)
|
|
||||||
# 我们不修改 cached pipe_adj,而是建立一个 extra_adj
|
|
||||||
extra_adj = defaultdict(list)
|
|
||||||
boundary_valves = {} # 当前有效的边界阀门
|
|
||||||
|
|
||||||
for vid, (n1, n2) in all_valves.items():
|
|
||||||
if vid in disabled_valves_set:
|
|
||||||
# 故障阀门:视为连通管道
|
|
||||||
extra_adj[n1].append(n2)
|
|
||||||
extra_adj[n2].append(n1)
|
|
||||||
else:
|
|
||||||
# 正常阀门:视为潜在边界
|
|
||||||
boundary_valves[vid] = (n1, n2)
|
|
||||||
|
|
||||||
# 4. BFS 搜索 (叠加 pipe_adj 和 extra_adj)
|
|
||||||
affected_nodes: set[str] = set()
|
|
||||||
queue = deque(start_nodes)
|
|
||||||
while queue:
|
|
||||||
node = queue.popleft()
|
|
||||||
if node in affected_nodes:
|
|
||||||
continue
|
|
||||||
affected_nodes.add(node)
|
|
||||||
|
|
||||||
# 遍历永久管道邻居
|
|
||||||
if node in pipe_adj:
|
|
||||||
for neighbor in pipe_adj[node]:
|
|
||||||
if neighbor not in affected_nodes:
|
|
||||||
queue.append(neighbor)
|
|
||||||
|
|
||||||
# 遍历故障阀门带来的额外邻居
|
|
||||||
if node in extra_adj:
|
|
||||||
for neighbor in extra_adj[node]:
|
|
||||||
if neighbor not in affected_nodes:
|
|
||||||
queue.append(neighbor)
|
|
||||||
|
|
||||||
# 5. 结果聚合
|
|
||||||
must_close_valves: list[str] = []
|
|
||||||
optional_valves: list[str] = []
|
|
||||||
|
|
||||||
for valve_id, (n1, n2) in boundary_valves.items():
|
|
||||||
in_n1 = n1 in affected_nodes
|
|
||||||
in_n2 = n2 in affected_nodes
|
|
||||||
if in_n1 and in_n2:
|
|
||||||
optional_valves.append(valve_id)
|
|
||||||
elif in_n1 or in_n2:
|
|
||||||
must_close_valves.append(valve_id)
|
|
||||||
|
|
||||||
must_close_valves.sort()
|
|
||||||
optional_valves.sort()
|
|
||||||
isolatable = bool(must_close_valves)
|
|
||||||
|
|
||||||
result = {
|
|
||||||
"accident_elements": target_elements,
|
|
||||||
"disabled_valves": disabled_valves,
|
|
||||||
"affected_nodes": sorted(affected_nodes) if isolatable else [],
|
|
||||||
"affected_node_count": len(affected_nodes),
|
|
||||||
"must_close_valves": must_close_valves,
|
|
||||||
"optional_valves": optional_valves,
|
|
||||||
"isolatable": isolatable,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(target_elements) == 1:
|
|
||||||
result["accident_element"] = target_elements[0]
|
|
||||||
|
|
||||||
return result
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from app.algorithms.leakage.identifier import LeakageIdentifier
|
|
||||||
|
|
||||||
__all__ = ["LeakageIdentifier"]
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from app.algorithms.pipe_health_prediction.survival_predictor import (
|
||||||
|
PipeHealthSurvivalPredictor,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = ["PipeHealthSurvivalPredictor"]
|
||||||
+8
-18
@@ -4,7 +4,7 @@ import pandas as pd
|
|||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
|
||||||
class PipelineHealthAnalyzer:
|
class PipeHealthSurvivalPredictor:
|
||||||
"""
|
"""
|
||||||
管道健康分析器类,使用随机生存森林模型预测管道的生存概率。
|
管道健康分析器类,使用随机生存森林模型预测管道的生存概率。
|
||||||
|
|
||||||
@@ -28,11 +28,6 @@ class PipelineHealthAnalyzer:
|
|||||||
"model",
|
"model",
|
||||||
"my_survival_forest_model_quxi.joblib",
|
"my_survival_forest_model_quxi.joblib",
|
||||||
)
|
)
|
||||||
# 确保 model 目录存在
|
|
||||||
model_dir = os.path.dirname(model_path)
|
|
||||||
if model_dir and not os.path.exists(model_dir):
|
|
||||||
os.makedirs(model_dir, exist_ok=True)
|
|
||||||
|
|
||||||
if not os.path.exists(model_path):
|
if not os.path.exists(model_path):
|
||||||
raise FileNotFoundError(f"模型文件未找到: {model_path}")
|
raise FileNotFoundError(f"模型文件未找到: {model_path}")
|
||||||
|
|
||||||
@@ -102,7 +97,7 @@ class PipelineHealthAnalyzer:
|
|||||||
|
|
||||||
# 调用说明示例
|
# 调用说明示例
|
||||||
"""
|
"""
|
||||||
在其他项目中使用PipelineHealthAnalyzer类的步骤:
|
在其他项目中使用 PipeHealthSurvivalPredictor 类的步骤:
|
||||||
|
|
||||||
1. 安装依赖(在requirements.txt中添加):
|
1. 安装依赖(在requirements.txt中添加):
|
||||||
joblib==1.5.0
|
joblib==1.5.0
|
||||||
@@ -112,34 +107,29 @@ class PipelineHealthAnalyzer:
|
|||||||
matplotlib==3.9.4
|
matplotlib==3.9.4
|
||||||
|
|
||||||
2. 导入类:
|
2. 导入类:
|
||||||
from pipeline_health_analyzer import PipelineHealthAnalyzer
|
from survival_predictor import PipeHealthSurvivalPredictor
|
||||||
|
|
||||||
3. 初始化分析器(替换为实际模型路径):
|
3. 初始化分析器(替换为实际模型路径):
|
||||||
analyzer = PipelineHealthAnalyzer(model_path='path/to/my_survival_forest_model3-10.joblib')
|
predictor = PipeHealthSurvivalPredictor(model_path='path/to/model.joblib')
|
||||||
|
|
||||||
4. 准备数据(pandas DataFrame,包含9个特征列):
|
4. 准备数据(pandas DataFrame,包含4个特征列):
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
data = pd.DataFrame({
|
data = pd.DataFrame({
|
||||||
'Material': [1, 2], # 示例数据
|
'Material': [1, 2], # 示例数据
|
||||||
'Diameter': [100, 150],
|
'Diameter': [100, 150],
|
||||||
'Flow Velocity': [1.5, 2.0],
|
'Flow Velocity': [1.5, 2.0],
|
||||||
'Pressure': [50, 60],
|
'Pressure': [50, 60]
|
||||||
'Temperature': [20, 25],
|
|
||||||
'Precipitation': [0.1, 0.2],
|
|
||||||
'Location': [1, 2],
|
|
||||||
'Structural Defects': [0, 1],
|
|
||||||
'Functional Defects': [0, 0]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
5. 进行预测:
|
5. 进行预测:
|
||||||
survival_funcs = analyzer.predict_survival(data)
|
survival_funcs = predictor.predict_survival(data)
|
||||||
|
|
||||||
6. 查看结果(每个样本的生存概率随时间变化):
|
6. 查看结果(每个样本的生存概率随时间变化):
|
||||||
for i, sf in enumerate(survival_funcs):
|
for i, sf in enumerate(survival_funcs):
|
||||||
print(f"样本 {i+1}: 时间点: {sf.x[:5]}..., 生存概率: {sf.y[:5]}...")
|
print(f"样本 {i+1}: 时间点: {sf.x[:5]}..., 生存概率: {sf.y[:5]}...")
|
||||||
|
|
||||||
7. 可视化(可选):
|
7. 可视化(可选):
|
||||||
analyzer.plot_survival(survival_funcs, save_path='survival_plot.png')
|
predictor.plot_survival(survival_funcs, save_path='survival_plot.png')
|
||||||
|
|
||||||
注意:
|
注意:
|
||||||
- 数据格式必须匹配特征列表,特征值为数值型。
|
- 数据格式必须匹配特征列表,特征值为数值型。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Pressure sensor placement calculation implementations."""
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
import sklearn.cluster
|
||||||
|
import wntr
|
||||||
|
|
||||||
|
|
||||||
|
class KMeansPlacement:
|
||||||
|
def __init__(self, wn, num_monitors: int, min_diameter_mm: float):
|
||||||
|
self.cluster_num = num_monitors
|
||||||
|
self.wn = wn
|
||||||
|
self.monitor_nodes: list[str] = []
|
||||||
|
self.coords: list[tuple[float, float]] = []
|
||||||
|
self.candidate_nodes: list[str] = []
|
||||||
|
self.min_diameter_mm = min_diameter_mm
|
||||||
|
|
||||||
|
def get_junctions_coordinates(self) -> None:
|
||||||
|
eligible_nodes: set[str] = set()
|
||||||
|
junction_names = set(self.wn.junction_name_list)
|
||||||
|
for pipe_name in self.wn.pipe_name_list:
|
||||||
|
pipe = self.wn.get_link(pipe_name)
|
||||||
|
if float(pipe.diameter) * 1000 < self.min_diameter_mm:
|
||||||
|
continue
|
||||||
|
eligible_nodes.update(
|
||||||
|
node_id
|
||||||
|
for node_id in (pipe.start_node_name, pipe.end_node_name)
|
||||||
|
if node_id in junction_names
|
||||||
|
)
|
||||||
|
|
||||||
|
for junction_name in self.wn.junction_name_list:
|
||||||
|
if junction_name not in eligible_nodes:
|
||||||
|
continue
|
||||||
|
junction = self.wn.get_node(junction_name)
|
||||||
|
self.candidate_nodes.append(junction_name)
|
||||||
|
self.coords.append(junction.coordinates)
|
||||||
|
|
||||||
|
def select_monitoring_points(self) -> list[str]:
|
||||||
|
if not self.coords:
|
||||||
|
self.get_junctions_coordinates()
|
||||||
|
if self.cluster_num <= 0:
|
||||||
|
raise ValueError("sensor_count must be greater than zero")
|
||||||
|
if self.cluster_num > len(self.candidate_nodes):
|
||||||
|
raise ValueError("符合最小管径条件的候选节点数量少于请求的监测点数量")
|
||||||
|
coords = np.array(self.coords)
|
||||||
|
coordinate_span = coords.max(axis=0) - coords.min(axis=0)
|
||||||
|
coordinate_span[coordinate_span == 0] = 1.0
|
||||||
|
coords_normalized = (coords - coords.min(axis=0)) / coordinate_span
|
||||||
|
kmeans = sklearn.cluster.KMeans(n_clusters=self.cluster_num, random_state=42)
|
||||||
|
kmeans.fit(coords_normalized)
|
||||||
|
|
||||||
|
selected_indices: set[int] = set()
|
||||||
|
for cluster_index, center in enumerate(kmeans.cluster_centers_):
|
||||||
|
cluster_indices = np.flatnonzero(kmeans.labels_ == cluster_index)
|
||||||
|
available_indices = [
|
||||||
|
int(index)
|
||||||
|
for index in cluster_indices
|
||||||
|
if int(index) not in selected_indices
|
||||||
|
]
|
||||||
|
if not available_indices:
|
||||||
|
available_indices = [
|
||||||
|
index
|
||||||
|
for index in range(len(self.candidate_nodes))
|
||||||
|
if index not in selected_indices
|
||||||
|
]
|
||||||
|
nearest_index = min(
|
||||||
|
available_indices,
|
||||||
|
key=lambda index: (
|
||||||
|
float(np.sum((coords_normalized[index] - center) ** 2)),
|
||||||
|
index,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
selected_indices.add(nearest_index)
|
||||||
|
nearest_node = self.candidate_nodes[nearest_index]
|
||||||
|
self.monitor_nodes.append(nearest_node)
|
||||||
|
|
||||||
|
return self.monitor_nodes
|
||||||
|
|
||||||
|
def visualize_network(self) -> None:
|
||||||
|
"""Visualize network with monitoring points."""
|
||||||
|
wntr.graphics.plot_network(
|
||||||
|
self.wn,
|
||||||
|
node_attribute=self.monitor_nodes,
|
||||||
|
node_size=30,
|
||||||
|
title="Optimal sensor",
|
||||||
|
)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def optimize_sensor_placement(
|
||||||
|
network_model: wntr.network.WaterNetworkModel,
|
||||||
|
sensor_count: int,
|
||||||
|
min_diameter_mm: float,
|
||||||
|
) -> list[str]:
|
||||||
|
"""Select sensor nodes from an already loaded network model."""
|
||||||
|
|
||||||
|
placement = KMeansPlacement(network_model, sensor_count, min_diameter_mm)
|
||||||
|
return placement.select_monitoring_points()
|
||||||
-11
@@ -903,14 +903,3 @@ def optimize_sensor_placement_from_inp(
|
|||||||
sensor_num=sensor_num,
|
sensor_num=sensor_num,
|
||||||
min_diameter=min_diameter,
|
min_diameter=min_diameter,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_ID(name: str, sensor_num: int, min_diameter: int) -> list[str]:
|
|
||||||
"""Compatibility entry point used by the sensor placement service."""
|
|
||||||
|
|
||||||
inp_path = Path("db_inp") / f"{name}.db.inp"
|
|
||||||
return optimize_sensor_placement_from_inp(
|
|
||||||
inp_path,
|
|
||||||
sensor_num=sensor_num,
|
|
||||||
min_diameter=min_diameter,
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
"""SCADA time-series cleaning algorithms."""
|
||||||
|
|
||||||
|
from .flow_series import clean_flow_data_df_kf
|
||||||
|
from .pressure_series import clean_pressure_data_df_km
|
||||||
|
|
||||||
|
__all__ = ["clean_flow_data_df_kf", "clean_pressure_data_df_km"]
|
||||||
@@ -142,11 +142,13 @@ def clean_flow_data_kf(
|
|||||||
return os.path.abspath(output_path)
|
return os.path.abspath(output_path)
|
||||||
|
|
||||||
|
|
||||||
def clean_flow_data_df_kf(data: pd.DataFrame, show_plot: bool = False) -> dict:
|
def clean_flow_data_df_kf(
|
||||||
|
data: pd.DataFrame, show_plot: bool = False
|
||||||
|
) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
接收一个 DataFrame 数据结构,使用一维 Kalman 滤波平滑并用预测值替换基于 IQR 检测出的异常点。
|
接收一个 DataFrame 数据结构,使用一维 Kalman 滤波平滑并用预测值替换基于 IQR 检测出的异常点。
|
||||||
区分合理的0值(流量转换)和异常的0值(连续多个0或孤立0)。
|
区分合理的0值(流量转换)和异常的0值(连续多个0或孤立0)。
|
||||||
返回完整的清洗后的字典数据结构。
|
返回完整的清洗后 DataFrame。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data: 输入 DataFrame(可包含 time 列)
|
data: 输入 DataFrame(可包含 time 列)
|
||||||
@@ -305,42 +307,3 @@ def clean_flow_data_df_kf(data: pd.DataFrame, show_plot: bool = False) -> dict:
|
|||||||
|
|
||||||
# 返回完整的修复后字典
|
# 返回完整的修复后字典
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
# # 测试
|
|
||||||
# if __name__ == "__main__":
|
|
||||||
# # 默认:脚本目录下同名 CSV 文件
|
|
||||||
# script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
# default_csv = os.path.join(script_dir, "pipe_flow_data_to_clean2.0.csv")
|
|
||||||
# out = clean_flow_data_kf(default_csv)
|
|
||||||
# print("清洗后的数据已保存到:", out)
|
|
||||||
|
|
||||||
# 测试 clean_flow_data_dict 函数
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import random
|
|
||||||
|
|
||||||
# 读取 szh_flow_scada.csv 文件
|
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
csv_path = os.path.join(script_dir, "szh_flow_scada.csv")
|
|
||||||
data = pd.read_csv(csv_path, header=0, index_col=None, encoding="utf-8")
|
|
||||||
|
|
||||||
# 排除 Time 列,随机选择 5 列
|
|
||||||
columns_to_exclude = ["Time"]
|
|
||||||
available_columns = [col for col in data.columns if col not in columns_to_exclude]
|
|
||||||
selected_columns = random.sample(available_columns, 1)
|
|
||||||
|
|
||||||
# 将选中的列转换为字典
|
|
||||||
data_dict = {col: data[col].tolist() for col in selected_columns}
|
|
||||||
|
|
||||||
print("选中的列:", selected_columns)
|
|
||||||
print("原始数据长度:", len(data_dict[selected_columns[0]]))
|
|
||||||
|
|
||||||
# 调用函数进行清洗
|
|
||||||
cleaned_dict = clean_flow_data_df_kf(data_dict, show_plot=True)
|
|
||||||
# 将清洗后的字典写回 CSV
|
|
||||||
out_csv = os.path.join(script_dir, f"{selected_columns[0]}_clean.csv")
|
|
||||||
pd.DataFrame(cleaned_dict).to_csv(out_csv, index=False, encoding="utf-8-sig")
|
|
||||||
print("已保存清洗结果到:", out_csv)
|
|
||||||
print("清洗后的字典键:", list(cleaned_dict.keys()))
|
|
||||||
print("清洗后的数据长度:", len(cleaned_dict[selected_columns[0]]))
|
|
||||||
print("测试完成:函数运行正常")
|
|
||||||
-36
@@ -541,39 +541,3 @@ def clean_pressure_data_df_km(data: pd.DataFrame, show_plot: bool = False) -> pd
|
|||||||
plt.show()
|
plt.show()
|
||||||
|
|
||||||
return data_repaired
|
return data_repaired
|
||||||
|
|
||||||
|
|
||||||
# 测试
|
|
||||||
# if __name__ == "__main__":
|
|
||||||
# # 默认使用脚本目录下的 pressure_raw_data.csv
|
|
||||||
# script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
# default_csv = os.path.join(script_dir, "pressure_raw_data.csv")
|
|
||||||
# out_path = clean_pressure_data_km(default_csv, show_plot=False)
|
|
||||||
# print("保存路径:", out_path)
|
|
||||||
|
|
||||||
# 测试 clean_pressure_data_dict_km 函数
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import random
|
|
||||||
|
|
||||||
# 读取 szh_pressure_scada.csv 文件
|
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
csv_path = os.path.join(script_dir, "szh_pressure_scada.csv")
|
|
||||||
data = pd.read_csv(csv_path, header=0, index_col=None, encoding="utf-8")
|
|
||||||
|
|
||||||
# 排除 Time 列,随机选择 5 列
|
|
||||||
columns_to_exclude = ["Time"]
|
|
||||||
available_columns = [col for col in data.columns if col not in columns_to_exclude]
|
|
||||||
selected_columns = random.sample(available_columns, 5)
|
|
||||||
|
|
||||||
# 将选中的列转换为字典
|
|
||||||
data_dict = {col: data[col].tolist() for col in selected_columns}
|
|
||||||
|
|
||||||
print("选中的列:", selected_columns)
|
|
||||||
print("原始数据长度:", len(data_dict[selected_columns[0]]))
|
|
||||||
|
|
||||||
# 调用函数进行清洗
|
|
||||||
cleaned_dict = clean_pressure_data_df_km(data_dict, show_plot=True)
|
|
||||||
|
|
||||||
print("清洗后的字典键:", list(cleaned_dict.keys()))
|
|
||||||
print("清洗后的数据长度:", len(cleaned_dict[selected_columns[0]]))
|
|
||||||
print("测试完成:函数运行正常")
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
from contextlib import contextmanager
|
|
||||||
import fcntl
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from app.algorithms.sensor import kmeans as kmeans_sensor
|
|
||||||
from app.algorithms.sensor import sensitivity
|
|
||||||
from app.native.wndb.s42_sensor_placement import create_sensor_placement
|
|
||||||
from app.services.sensor_placement import (
|
|
||||||
SensorPlacementConflictError,
|
|
||||||
SensorPlacementValidationError,
|
|
||||||
validate_sensor_placement_nodes,
|
|
||||||
)
|
|
||||||
from app.services.tjnetwork import dump_inp
|
|
||||||
|
|
||||||
|
|
||||||
def _sensor_inp_path(name: str) -> Path:
|
|
||||||
if (
|
|
||||||
not name
|
|
||||||
or name in {".", ".."}
|
|
||||||
or "/" in name
|
|
||||||
or "\\" in name
|
|
||||||
or "\x00" in name
|
|
||||||
):
|
|
||||||
raise SensorPlacementValidationError("管网名称不是有效的项目标识")
|
|
||||||
return Path("db_inp") / f"{name}.db.inp"
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def _sensor_inp_lock(name: str):
|
|
||||||
inp_path = _sensor_inp_path(name)
|
|
||||||
inp_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
lock_path = inp_path.with_suffix(".sensor.lock")
|
|
||||||
with lock_path.open("w", encoding="utf-8") as lock_file:
|
|
||||||
try:
|
|
||||||
fcntl.flock(
|
|
||||||
lock_file.fileno(),
|
|
||||||
fcntl.LOCK_EX | fcntl.LOCK_NB,
|
|
||||||
)
|
|
||||||
except BlockingIOError as exc:
|
|
||||||
raise SensorPlacementConflictError(
|
|
||||||
"当前项目已有监测点优化任务正在运行,请稍后重试"
|
|
||||||
) from exc
|
|
||||||
try:
|
|
||||||
yield inp_path
|
|
||||||
finally:
|
|
||||||
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
|
|
||||||
|
|
||||||
|
|
||||||
def _create_validated_placement(
|
|
||||||
name: str,
|
|
||||||
*,
|
|
||||||
scheme_name: str,
|
|
||||||
min_diameter: int,
|
|
||||||
username: str,
|
|
||||||
sensor_location: list[str],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
validate_sensor_placement_nodes(name, sensor_location)
|
|
||||||
return create_sensor_placement(
|
|
||||||
name,
|
|
||||||
scheme_name=scheme_name,
|
|
||||||
min_diameter=min_diameter,
|
|
||||||
username=username,
|
|
||||||
sensor_location=sensor_location,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def pressure_sensor_placement_sensitivity(
|
|
||||||
name: str,
|
|
||||||
scheme_name: str,
|
|
||||||
sensor_number: int,
|
|
||||||
min_diameter: int,
|
|
||||||
username: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
基于改进灵敏度法进行压力监测点优化布置
|
|
||||||
:param name: 数据库名称
|
|
||||||
:param scheme_name: 监测优化布置方案名称
|
|
||||||
:param sensor_number: 传感器数目
|
|
||||||
:param min_diameter: 最小管径
|
|
||||||
:param username: 用户名
|
|
||||||
:return: 新建的监测点方案
|
|
||||||
"""
|
|
||||||
with _sensor_inp_lock(name):
|
|
||||||
sensor_location = sensitivity.get_ID(
|
|
||||||
name=name,
|
|
||||||
sensor_num=sensor_number,
|
|
||||||
min_diameter=min_diameter,
|
|
||||||
)
|
|
||||||
return _create_validated_placement(
|
|
||||||
name,
|
|
||||||
scheme_name=scheme_name,
|
|
||||||
min_diameter=min_diameter,
|
|
||||||
username=username,
|
|
||||||
sensor_location=sensor_location,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# 2025/08/21
|
|
||||||
# 基于kmeans聚类法进行压力监测点优化布置
|
|
||||||
def pressure_sensor_placement_kmeans(
|
|
||||||
name: str,
|
|
||||||
scheme_name: str,
|
|
||||||
sensor_number: int,
|
|
||||||
min_diameter: int,
|
|
||||||
username: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
基于聚类法进行压力监测点优化布置
|
|
||||||
:param name: 数据库名称(注意,此处数据库名称也是inp文件名称,inp文件与pg库名要一样)
|
|
||||||
:param scheme_name: 监测优化布置方案名称
|
|
||||||
:param sensor_number: 传感器数目
|
|
||||||
:param min_diameter: 最小管径
|
|
||||||
:param username: 用户名
|
|
||||||
:return: 新建的监测点方案
|
|
||||||
"""
|
|
||||||
# dump_inp
|
|
||||||
with _sensor_inp_lock(name) as inp_path:
|
|
||||||
dump_inp(name, str(inp_path), "2")
|
|
||||||
sensor_location = kmeans_sensor.kmeans_sensor_placement(
|
|
||||||
name=name,
|
|
||||||
sensor_num=sensor_number,
|
|
||||||
min_diameter=min_diameter,
|
|
||||||
)
|
|
||||||
return _create_validated_placement(
|
|
||||||
name,
|
|
||||||
scheme_name=scheme_name,
|
|
||||||
min_diameter=min_diameter,
|
|
||||||
username=username,
|
|
||||||
sensor_location=sensor_location,
|
|
||||||
)
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import wntr
|
|
||||||
import pandas as pd
|
|
||||||
import numpy as np
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import sklearn.cluster
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
class QD_KMeans(object):
|
|
||||||
def __init__(self, wn, num_monitors):
|
|
||||||
# self.inp = inp
|
|
||||||
self.cluster_num = num_monitors # 聚类中心个数,也即测压点个数
|
|
||||||
self.wn = wn
|
|
||||||
self.monitor_nodes = []
|
|
||||||
self.coords = []
|
|
||||||
self.junction_nodes = {} # Added missing initialization
|
|
||||||
|
|
||||||
def get_junctions_coordinates(self):
|
|
||||||
|
|
||||||
for junction_name in self.wn.junction_name_list:
|
|
||||||
junction = self.wn.get_node(junction_name)
|
|
||||||
self.junction_nodes[junction_name] = junction.coordinates
|
|
||||||
self.coords.append(junction.coordinates)
|
|
||||||
|
|
||||||
# print(f"Total junctions: {self.junction_coordinates}")
|
|
||||||
|
|
||||||
def select_monitoring_points(self):
|
|
||||||
if not self.coords: # Add check if coordinates are collected
|
|
||||||
self.get_junctions_coordinates()
|
|
||||||
coords = np.array(self.coords)
|
|
||||||
coords_normalized = (coords - coords.min(axis=0)) / (
|
|
||||||
coords.max(axis=0) - coords.min(axis=0)
|
|
||||||
)
|
|
||||||
kmeans = sklearn.cluster.KMeans(n_clusters=self.cluster_num, random_state=42)
|
|
||||||
kmeans.fit(coords_normalized)
|
|
||||||
|
|
||||||
for center in kmeans.cluster_centers_:
|
|
||||||
distances = np.sum((coords_normalized - center) ** 2, axis=1)
|
|
||||||
nearest_node = self.wn.junction_name_list[np.argmin(distances)]
|
|
||||||
self.monitor_nodes.append(nearest_node)
|
|
||||||
|
|
||||||
return self.monitor_nodes
|
|
||||||
|
|
||||||
def visualize_network(self):
|
|
||||||
"""Visualize network with monitoring points"""
|
|
||||||
ax = wntr.graphics.plot_network(
|
|
||||||
self.wn,
|
|
||||||
node_attribute=self.monitor_nodes,
|
|
||||||
node_size=30,
|
|
||||||
title="Optimal sensor",
|
|
||||||
)
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
|
|
||||||
def kmeans_sensor_placement(name: str, sensor_num: int, min_diameter: int) -> list:
|
|
||||||
inp_name = f"./db_inp/{name}.db.inp"
|
|
||||||
wn = wntr.network.WaterNetworkModel(inp_name)
|
|
||||||
wn_cluster = QD_KMeans(wn, sensor_num)
|
|
||||||
|
|
||||||
# Select monitoring pointse
|
|
||||||
sensor_ids = wn_cluster.select_monitoring_points()
|
|
||||||
|
|
||||||
# wn_cluster.visualize_network()
|
|
||||||
|
|
||||||
return sensor_ids
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# sensorindex = get_ID(name='suzhouhe_2024_cloud_0817', sensor_num=30, min_diameter=500)
|
|
||||||
sensorindex = kmeans_sensor_placement(name="szh", sensor_num=50, min_diameter=300)
|
|
||||||
print(sensorindex)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from app.algorithms.simulation.scenarios import (
|
|
||||||
convert_to_local_unit,
|
|
||||||
burst_analysis,
|
|
||||||
valve_close_analysis,
|
|
||||||
flushing_analysis,
|
|
||||||
contaminant_simulation,
|
|
||||||
age_analysis,
|
|
||||||
pressure_regulation,
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"convert_to_local_unit",
|
|
||||||
"burst_analysis",
|
|
||||||
"valve_close_analysis",
|
|
||||||
"flushing_analysis",
|
|
||||||
"contaminant_simulation",
|
|
||||||
"age_analysis",
|
|
||||||
"pressure_regulation",
|
|
||||||
]
|
|
||||||
@@ -1,867 +0,0 @@
|
|||||||
import numpy as np
|
|
||||||
from app.services.tjnetwork import (
|
|
||||||
ChangeSet,
|
|
||||||
close_project,
|
|
||||||
copy_project,
|
|
||||||
delete_project,
|
|
||||||
get_pattern,
|
|
||||||
get_patterns,
|
|
||||||
get_pump,
|
|
||||||
get_reservoir,
|
|
||||||
get_status,
|
|
||||||
get_tank,
|
|
||||||
get_time,
|
|
||||||
have_project,
|
|
||||||
is_project_open,
|
|
||||||
open_project,
|
|
||||||
read_all,
|
|
||||||
run_project,
|
|
||||||
set_pattern,
|
|
||||||
set_status,
|
|
||||||
set_tank,
|
|
||||||
set_time,
|
|
||||||
)
|
|
||||||
# from get_real_status import *
|
|
||||||
from datetime import datetime,timedelta
|
|
||||||
from math import modf
|
|
||||||
import json
|
|
||||||
import pytz
|
|
||||||
import requests
|
|
||||||
import time
|
|
||||||
import app.services.project_info as project_info
|
|
||||||
from app.services.time_api import parse_clock_duration_seconds
|
|
||||||
|
|
||||||
url_path = 'http://10.101.15.16:9000/loong' # 内网
|
|
||||||
# url_path = 'http://183.64.62.100:9057/loong' # 外网
|
|
||||||
url_real = url_path + '/api/mpoints/realValue'
|
|
||||||
url_hist = url_path + '/api/curves/data'
|
|
||||||
|
|
||||||
PATTERN_TIME_STEP=15.0
|
|
||||||
DN_900_ID='2498'
|
|
||||||
DN_500_ID='3854'
|
|
||||||
DN_1000_ID='3853'
|
|
||||||
H_RESSURE='2510'
|
|
||||||
L_PRESURE='2514'
|
|
||||||
H_TANK='4780'
|
|
||||||
L_TANK='4854'
|
|
||||||
|
|
||||||
H_REGION_1='SA_ZBBDJSCP000002'
|
|
||||||
H_REGION_2='' #to do
|
|
||||||
L_REGION_1='SA_ZBBDTJSC000001'
|
|
||||||
L_REGION_2='SA_R00003'
|
|
||||||
|
|
||||||
# reservoir basic height
|
|
||||||
RESERVOIR_BASIC_HEIGHT = float(250.35)
|
|
||||||
|
|
||||||
# regions
|
|
||||||
regions = ['hp', 'lp']
|
|
||||||
regions_demand_patterns = {'hp': ['DN900', 'DN500'], 'lp': ['DN1000']} # 出厂水量近似表示用水量
|
|
||||||
regions_patterns = {'hp': ['ChuanYiJiXiao', 'BeiQuanHuaYuan', 'ZhuangYuanFuDi', 'JingNingJiaYuan',
|
|
||||||
'308', 'JiaYinYuan', 'XinChengGuoJi', 'YiJingBeiChen', 'ZhongYangXinDu',
|
|
||||||
'XinHaiJiaYuan', 'DongFengJie', 'DingYaXinYu', 'ZiYunTai', 'XieMaGuangChang',
|
|
||||||
'YongJinFu', 'BianDianZhan', 'BeiNanDaDao', 'TianShengLiJie', 'XueYuanXiaoQu',
|
|
||||||
'YunHuaLu', 'GaoJiaQiao', 'LuZuoFuLuXiaDuan', 'TianRunCheng', 'CaoJiaBa',
|
|
||||||
'PuLingChang', 'QiLongXiaoQu', 'TuanXiao',
|
|
||||||
'TuanShanBaoZhongShiHua', 'XieMa', 'BeiWenQuanJiuHaoErQi', 'LaiYinHuSiQi',
|
|
||||||
'DN500', 'DN900'],
|
|
||||||
'lp': ['PanXiMingDu', 'WanKeJinYuHuaFuGaoCeng', 'KeJiXiao',
|
|
||||||
'LuGouQiao', 'LongJiangHuaYuan', 'LaoQiZhongDui', 'ShiYanCun', 'TianQiDaSha',
|
|
||||||
'TianShengPaiChuSuo', 'TianShengShangPin', 'JiaoTang', 'RenMinHuaYuan',
|
|
||||||
'TaiJiBinJiangYiQi', 'TianQiHuaYuan', 'TaiJiBinJiangErQi', '122Zhong',
|
|
||||||
'WanKeJinYuHuaFuYangFang', 'ChengBeiCaiShiKou', 'WenXingShe', 'YueLiangTianBBGJCZ',
|
|
||||||
'YueLiangTian', 'YueLiangTian200', 'ChengTaoChang', 'HuoCheZhan', 'LiangKu', 'QunXingLu',
|
|
||||||
'JiuYuanErTongYiYuan', 'TangDouHua', 'TaiJiBinJiangErQi(SanJi)',
|
|
||||||
'ZhangDouHua', 'JinYunXiaoQuDN400',
|
|
||||||
'DN1000']}
|
|
||||||
|
|
||||||
# nodes
|
|
||||||
monitor_single_patterns = ['ChuanYiJiXiao', 'BeiQuanHuaYuan', 'ZhuangYuanFuDi', 'JingNingJiaYuan',
|
|
||||||
'308', 'JiaYinYuan', 'XinChengGuoJi', 'YiJingBeiChen', 'ZhongYangXinDu',
|
|
||||||
'XinHaiJiaYuan', 'DongFengJie', 'DingYaXinYu', 'ZiYunTai', 'XieMaGuangChang',
|
|
||||||
'YongJinFu', 'PanXiMingDu', 'WanKeJinYuHuaFuGaoCeng', 'KeJiXiao',
|
|
||||||
'LuGouQiao', 'LongJiangHuaYuan', 'LaoQiZhongDui', 'ShiYanCun', 'TianQiDaSha',
|
|
||||||
'TianShengPaiChuSuo', 'TianShengShangPin', 'JiaoTang', 'RenMinHuaYuan',
|
|
||||||
'TaiJiBinJiangYiQi', 'TianQiHuaYuan', 'TaiJiBinJiangErQi', '122Zhong',
|
|
||||||
'WanKeJinYuHuaFuYangFang']
|
|
||||||
monitor_single_patterns_id = {'ChuanYiJiXiao': '7338', 'BeiQuanHuaYuan': '7315', 'ZhuangYuanFuDi': '7316',
|
|
||||||
'JingNingJiaYuan': '7528', '308': '8272', 'JiaYinYuan': '7304',
|
|
||||||
'XinChengGuoJi': '7325', 'YiJingBeiChen': '7328', 'ZhongYangXinDu': '7329',
|
|
||||||
'XinHaiJiaYuan': '9138', 'DongFengJie': '7302', 'DingYaXinYu': '7331',
|
|
||||||
'ZiYunTai': '7420,9059', 'XieMaGuangChang': '7326', 'YongJinFu': '9059',
|
|
||||||
'PanXiMingDu': '7320', 'WanKeJinYuHuaFuGaoCeng': '7419',
|
|
||||||
'KeJiXiao': '7305', 'LuGouQiao': '7306', 'LongJiangHuaYuan': '7318',
|
|
||||||
'LaoQiZhongDui': '9075', 'ShiYanCun': '7309', 'TianQiDaSha': '7323',
|
|
||||||
'TianShengPaiChuSuo': '7335', 'TianShengShangPin': '7324', 'JiaoTang': '7332',
|
|
||||||
'RenMinHuaYuan': '7322', 'TaiJiBinJiangYiQi': '7333', 'TianQiHuaYuan': '8235',
|
|
||||||
'TaiJiBinJiangErQi': '7334', '122Zhong': '7314', 'WanKeJinYuHuaFuYangFang': '7418'}
|
|
||||||
|
|
||||||
monitor_unity_patterns = ['BianDianZhan', 'BeiNanDaDao', 'TianShengLiJie', 'XueYuanXiaoQu',
|
|
||||||
'YunHuaLu', 'GaoJiaQiao', 'LuZuoFuLuXiaDuan', 'TianRunCheng',
|
|
||||||
'CaoJiaBa', 'PuLingChang', 'QiLongXiaoQu', 'TuanXiao',
|
|
||||||
'ChengBeiCaiShiKou', 'WenXingShe', 'YueLiangTianBBGJCZ',
|
|
||||||
'YueLiangTian', 'YueLiangTian200',
|
|
||||||
'ChengTaoChang', 'HuoCheZhan', 'LiangKu', 'QunXingLu',
|
|
||||||
'TuanShanBaoZhongShiHua', 'XieMa', 'BeiWenQuanJiuHaoErQi', 'LaiYinHuSiQi',
|
|
||||||
'JiuYuanErTongYiYuan', 'TangDouHua', 'TaiJiBinJiangErQi(SanJi)',
|
|
||||||
'ZhangDouHua', 'JinYunXiaoQuDN400',
|
|
||||||
'DN500', 'DN900', 'DN1000']
|
|
||||||
monitor_unity_patterns_id = {'BianDianZhan': '7339', 'BeiNanDaDao': '7319', 'TianShengLiJie': '8242',
|
|
||||||
'XueYuanXiaoQu': '7327', 'YunHuaLu': '7312', 'GaoJiaQiao': '7340',
|
|
||||||
'LuZuoFuLuXiaDuan': '7343', 'TianRunCheng': '7310', 'CaoJiaBa': '7300',
|
|
||||||
'PuLingChang': '7307', 'QiLongXiaoQu': '7321', 'TuanXiao': '8963',
|
|
||||||
'ChengBeiCaiShiKou': '7330', 'WenXingShe': '7311',
|
|
||||||
'YueLiangTianBBGJCZ': '7313', 'YueLiangTian': '7313', 'YueLiangTian200': '7313',
|
|
||||||
'ChengTaoChang': '7301', 'HuoCheZhan': '7303',
|
|
||||||
'LiangKu': '7296', 'QunXingLu': '7308',
|
|
||||||
'DN500': '3854', 'DN900': '2498', 'DN1000': '3853'}
|
|
||||||
monitor_patterns = monitor_single_patterns + monitor_unity_patterns
|
|
||||||
monitor_patterns_id = {**monitor_single_patterns_id, **monitor_unity_patterns_id}
|
|
||||||
# pumps
|
|
||||||
pumps_name = ['1#', '2#', '3#', '4#', '5#', '6#', '7#']
|
|
||||||
pumps = ['PU00000', 'PU00001', 'PU00002', 'PU00003', 'PU00004', 'PU00005', 'PU00006']
|
|
||||||
variable_frequency_pumps = ['PU00004', 'PU00005', 'PU00006']
|
|
||||||
pumps_id = {'PU00000': '2747', 'PU00001': '2776', 'PU00002': '2730', 'PU00003': '2787',
|
|
||||||
'PU00004': '2500', 'PU00005': '2502', 'PU00006': '2504'}
|
|
||||||
# reservoirs
|
|
||||||
reservoirs = ['ZBBDJSCP000002', 'R00003']
|
|
||||||
reservoirs_id = {'ZBBDJSCP000002': '2497', 'R00003': '2571'}
|
|
||||||
# tanks
|
|
||||||
tanks = ['ZBBDTJSC000002', 'ZBBDTJSC000001']
|
|
||||||
tanks_id = {'ZBBDTJSC000002': '4780', 'ZBBDTJSC000001': '9774'}
|
|
||||||
|
|
||||||
|
|
||||||
class DataLoader:
|
|
||||||
"""数据加载器"""
|
|
||||||
def __init__(self, project_name, start_time: datetime, end_time: datetime,
|
|
||||||
pumps_control: dict = None, tank_initial_level_control: dict = None,
|
|
||||||
region_demand_control: dict = None, downloading_prohibition: bool = False):
|
|
||||||
self.project_name = project_name # 数据库名
|
|
||||||
self.current_time = self.round_time(datetime.now(pytz.timezone('Asia/Shanghai')), 1) # 圆整至整分钟
|
|
||||||
self.current_round_time = self.round_time(self.current_time, int(PATTERN_TIME_STEP))
|
|
||||||
self.updating_data_flag = True \
|
|
||||||
if self.current_round_time == self.round_time(start_time, int(PATTERN_TIME_STEP)) \
|
|
||||||
else False # 判断是否从当前时刻开始模拟(是否更新最新监测数据)
|
|
||||||
self.downloading_prohibition = downloading_prohibition # 是否禁止下载数据(默认False: 允许下载)
|
|
||||||
self.updating_data_flag = False if self.downloading_prohibition else self.updating_data_flag
|
|
||||||
self.pattern_start_index = get_pattern_index(
|
|
||||||
self.round_time(start_time, int(PATTERN_TIME_STEP)).strftime("%Y-%m-%d %H:%M:%S")) # pattern起始索引
|
|
||||||
self.pattern_end_index = get_pattern_index(
|
|
||||||
self.round_time(end_time, int(PATTERN_TIME_STEP)).strftime("%Y-%m-%d %H:%M:%S")) # pattern结束索引
|
|
||||||
self.pattern_index_list = list(range(self.pattern_start_index, self.pattern_end_index + 1)) # pattern索引列表
|
|
||||||
self.download_id = self.get_download_id() # 数据下载接口id '7338,7315,7316,...'
|
|
||||||
self.current_time_download_data = dict(
|
|
||||||
zip(self.download_id.split(','),
|
|
||||||
[np.nan]*len(list(self.download_id.split(','))))
|
|
||||||
) # {id(str): value(float)}
|
|
||||||
self.current_time_download_data_flag = dict(
|
|
||||||
zip(self.download_id.split(','),
|
|
||||||
[False]*len(list(self.download_id.split(','))))
|
|
||||||
) # 下载数据是否具备实时性, {id(str): flag(bool)}
|
|
||||||
self.old_flow_data = self.init_dict_of_list(dict(
|
|
||||||
zip(monitor_patterns,
|
|
||||||
[[np.nan]] * (len(monitor_patterns)))
|
|
||||||
)) # {pattern_name(str): flow(float)}
|
|
||||||
self.old_pattern_factor = self.init_dict_of_list(dict(
|
|
||||||
zip(monitor_patterns,
|
|
||||||
[[np.nan]] * (len(monitor_patterns)))
|
|
||||||
)) # {pattern_name(str): [pattern_factor(float)]}
|
|
||||||
self.new_flow_data = self.init_dict_of_list(dict(
|
|
||||||
zip(monitor_patterns,
|
|
||||||
[[np.nan]] * (len(monitor_patterns)))
|
|
||||||
)) # {pattern_name(str): flow(float)}
|
|
||||||
self.new_pattern_factor = self.init_dict_of_list(dict(
|
|
||||||
zip(monitor_patterns,
|
|
||||||
[[np.nan]] * (len(monitor_patterns)))
|
|
||||||
)) # {pattern_name(str): [pattern_factor(float)]}
|
|
||||||
self.reservoir_data = dict(zip(reservoirs, [np.nan]*len(reservoirs))) # {reservoir_name(str): level(float)}
|
|
||||||
self.tank_data = dict(zip(tanks, [np.nan] * len(tanks))) # {tank_name(str): level(float)}
|
|
||||||
self.pump_data = self.init_dict_of_list(
|
|
||||||
dict(zip(pumps, [[np.nan]]*len(pumps)))) # {pump_name(str): [frequency(float)]}
|
|
||||||
self.pump_control = pumps_control # {pump_name(str): [frequency(float)]}
|
|
||||||
self.tank_initial_level_control = tank_initial_level_control # {tank_name(str): level(float)}
|
|
||||||
self.region_demand_current = dict(zip(regions, [0]*len(regions))) # {region_name(str): total_demand(float)}
|
|
||||||
self.region_demand_control = region_demand_control # {region_name(str): total_demand(float)}
|
|
||||||
self.region_demand_control_factor = dict(
|
|
||||||
zip(regions, [1]*len(regions))) # 区域流量控制系数(用于调整用水量), {region_name(str): factor(float)}
|
|
||||||
|
|
||||||
def load_data(self):
|
|
||||||
"""生成数据集"""
|
|
||||||
self.download_data() # 下载实时数据
|
|
||||||
self.get_old_pattern_and_flow() # 读取历史记录pattern信息
|
|
||||||
self.cal_demand_convert_factor() # 计算用水量转换系数(设定用水量时)
|
|
||||||
self.set_new_flow() # 设置'更新'流量
|
|
||||||
self.set_new_pattern_factor() # 设置'更新'pattern factors
|
|
||||||
self.set_reservoirs() # 设置清水池
|
|
||||||
self.set_tanks() # 设置调节池
|
|
||||||
self.set_pumps() # 设置水泵
|
|
||||||
return self.pattern_start_index
|
|
||||||
|
|
||||||
def download_data(self):
|
|
||||||
"""下载数据"""
|
|
||||||
if self.updating_data_flag is True:
|
|
||||||
print('{} -- Start downloading data.'.format(
|
|
||||||
datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
|
|
||||||
data_wait_flag = True
|
|
||||||
while data_wait_flag:
|
|
||||||
try:
|
|
||||||
newest_data_time = self.download_real_data(self.download_id) # 获取实时数据
|
|
||||||
except Exception as e:
|
|
||||||
print('{}\nWaiting for real data.'.format(e))
|
|
||||||
time.sleep(1)
|
|
||||||
else:
|
|
||||||
print('{} -- Downloading data ok. Newest timestamp: {}.'.format(
|
|
||||||
datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
|
||||||
newest_data_time.strftime('%Y-%m-%d %H:%M:%S')))
|
|
||||||
data_wait_flag = False
|
|
||||||
|
|
||||||
def cal_current_region_demand(self):
|
|
||||||
"""计算区域当前用水量"""
|
|
||||||
if self.updating_data_flag is True:
|
|
||||||
for region in self.region_demand_current.keys():
|
|
||||||
total_demand = 0
|
|
||||||
for pipe in regions_demand_patterns[region]:
|
|
||||||
total_demand += self.current_time_download_data[monitor_patterns_id[pipe]] # 出厂流量
|
|
||||||
self.region_demand_current[region] = total_demand
|
|
||||||
|
|
||||||
def cal_history_region_demand(self, pattern_index_list):
|
|
||||||
"""计算区域历史用水量(对应记录的pattern)"""
|
|
||||||
old_demand = {}
|
|
||||||
for region in regions:
|
|
||||||
total_demand = 0
|
|
||||||
for pipe_pattern_name in regions_demand_patterns[region]:
|
|
||||||
old_flows, old_patterns = self.get_history_pattern_info(self.project_name, pipe_pattern_name)
|
|
||||||
for idx in pattern_index_list:
|
|
||||||
total_demand += old_flows[idx] / 4 # 15分钟水量
|
|
||||||
old_demand[region] = total_demand
|
|
||||||
return old_demand
|
|
||||||
|
|
||||||
def cal_demand_convert_factor(self):
|
|
||||||
"""计算用水量转换系数(设定用水量时)"""
|
|
||||||
self.cal_current_region_demand() # 计算区域当前时刻用水量
|
|
||||||
old_demand_moment = self.cal_history_region_demand([self.pattern_start_index]) # 计算区域目标时刻总用水量
|
|
||||||
old_demand_period = self.cal_history_region_demand(self.pattern_index_list) # 计算区域目标时段总用水量
|
|
||||||
for region in regions:
|
|
||||||
self.region_demand_control_factor[region] \
|
|
||||||
= (self.region_demand_current[region] / 4) / old_demand_moment[region] \
|
|
||||||
if self.updating_data_flag is True else 1
|
|
||||||
self.region_demand_control_factor[region] = self.region_demand_control[region] / old_demand_period[region] \
|
|
||||||
if (self.region_demand_control is not None) and (region in self.region_demand_control.keys()) \
|
|
||||||
else self.region_demand_control_factor[region]
|
|
||||||
|
|
||||||
def get_old_pattern_and_flow(self):
|
|
||||||
"""获取所有pattern的选定时段的历史记录的pattern和flow"""
|
|
||||||
for idx in monitor_patterns: # 遍历patterns
|
|
||||||
old_flows, old_patterns = self.get_history_pattern_info(self.project_name, idx)
|
|
||||||
for pattern_idx in self.pattern_index_list:
|
|
||||||
old_flow_data = old_flows[pattern_idx]
|
|
||||||
old_pattern_factor = old_patterns[pattern_idx]
|
|
||||||
if pattern_idx == self.pattern_start_index: # 起始时刻
|
|
||||||
self.old_flow_data[idx][0] = old_flow_data
|
|
||||||
self.old_pattern_factor[idx][0] = old_pattern_factor
|
|
||||||
else:
|
|
||||||
self.old_flow_data[idx].append(old_flow_data)
|
|
||||||
self.old_pattern_factor[idx].append(old_pattern_factor)
|
|
||||||
|
|
||||||
def set_new_flow(self):
|
|
||||||
"""计算模拟时段新流量(相较于历史记录)"""
|
|
||||||
for idx in self.new_flow_data.keys(): # 遍历patterns
|
|
||||||
region_name = None
|
|
||||||
for region in regions_patterns.keys():
|
|
||||||
if idx in regions_patterns[region]:
|
|
||||||
region_name = region # pattern所属分区
|
|
||||||
break
|
|
||||||
# 实时流量
|
|
||||||
if self.updating_data_flag is True:
|
|
||||||
if idx in monitor_unity_patterns[-3:]: # 出水管流量
|
|
||||||
self.new_flow_data[idx][0] = self.current_time_download_data[monitor_patterns_id[idx]]
|
|
||||||
else: # 其余流量
|
|
||||||
self.new_flow_data[idx][0] \
|
|
||||||
= self.region_demand_control_factor[region_name] * self.old_flow_data[idx][0]
|
|
||||||
# if idx == 'ZiYunTai':
|
|
||||||
# idx_a, idx_b = monitor_patterns_id[idx].split(',')
|
|
||||||
# self.new_flow_data[idx][0] \
|
|
||||||
# = self.current_time_download_data[idx_a] - self.current_time_download_data[idx_b]
|
|
||||||
# else:
|
|
||||||
# self.new_flow_data[idx][0] = self.current_time_download_data[monitor_patterns_id[idx]]
|
|
||||||
# for data_id in monitor_patterns_id[idx].split(','):
|
|
||||||
# if (self.current_time_download_data_flag[data_id] is False) \
|
|
||||||
# and (idx not in [pipe for pipe_list in regions_demand_patterns.values()
|
|
||||||
# for pipe in pipe_list]): # 无法获取实时数据
|
|
||||||
# self.new_flow_data[idx][0] \
|
|
||||||
# = self.region_demand_control_factor[region_name] * self.old_flow_data[idx][0]
|
|
||||||
# break
|
|
||||||
# 根据设定用水量修改新流量
|
|
||||||
if (self.region_demand_control is not None) \
|
|
||||||
and (region_name in self.region_demand_control.keys()):
|
|
||||||
for pattern_idx in self.pattern_index_list:
|
|
||||||
if pattern_idx == self.pattern_start_index: # 起始时刻
|
|
||||||
self.new_flow_data[idx][0] \
|
|
||||||
= self.region_demand_control_factor[region_name] * self.old_flow_data[idx][0]
|
|
||||||
else:
|
|
||||||
self.new_flow_data[idx].append(
|
|
||||||
self.region_demand_control_factor[region_name]
|
|
||||||
* self.old_flow_data[idx][self.pattern_index_list.index(pattern_idx)]
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_new_pattern_factor(self):
|
|
||||||
"""更新计算选定时段(设定用水量)/时刻的pattern factor"""
|
|
||||||
pattern_index_list = self.pattern_index_list \
|
|
||||||
if self.region_demand_control is not None \
|
|
||||||
else [self.pattern_start_index]
|
|
||||||
for idx in monitor_patterns: # 遍历patterns
|
|
||||||
for pattern_idx in pattern_index_list: # 遍历需要修改的pattern(index)
|
|
||||||
pattern_idx_cls = pattern_index_list.index(pattern_idx) # 转换index(类表存储结构)
|
|
||||||
old_flow_data = self.old_flow_data[idx][pattern_idx_cls]
|
|
||||||
old_pattern_factor = self.old_pattern_factor[idx][pattern_idx_cls]
|
|
||||||
if pattern_idx_cls == 0: # 起始时刻
|
|
||||||
if idx in monitor_single_patterns:
|
|
||||||
if not np.isnan(self.new_flow_data[idx][0]):
|
|
||||||
self.new_pattern_factor[idx][0] = (self.new_flow_data[idx][0] * 1000 / 3600) # m3/h to L/s
|
|
||||||
if idx in monitor_unity_patterns:
|
|
||||||
if not np.isnan(self.new_flow_data[idx][0]):
|
|
||||||
self.new_pattern_factor[idx][0] \
|
|
||||||
= old_pattern_factor * self.new_flow_data[idx][0] / old_flow_data
|
|
||||||
else:
|
|
||||||
if idx in monitor_single_patterns:
|
|
||||||
if len(self.new_flow_data[idx]) > pattern_idx_cls:
|
|
||||||
self.new_pattern_factor[idx].append(
|
|
||||||
(self.new_flow_data[idx][pattern_idx_cls] * 1000 / 3600)) # m3/h to L/s
|
|
||||||
if idx in monitor_unity_patterns:
|
|
||||||
if len(self.new_flow_data[idx]) > pattern_idx_cls:
|
|
||||||
self.new_pattern_factor[idx].append(
|
|
||||||
old_pattern_factor
|
|
||||||
* self.new_flow_data[idx][pattern_idx_cls]
|
|
||||||
/ old_flow_data)
|
|
||||||
|
|
||||||
def set_reservoirs(self):
|
|
||||||
"""设置清水池"""
|
|
||||||
if self.updating_data_flag is True:
|
|
||||||
for idx in self.reservoir_data.keys():
|
|
||||||
if self.current_time_download_data_flag[reservoirs_id[idx]] is False: # 无法获取实时数据
|
|
||||||
print('There is no current data of reservoir: {}.'.format(idx))
|
|
||||||
else:
|
|
||||||
self.reservoir_data[idx] \
|
|
||||||
= self.current_time_download_data[reservoirs_id[idx]] + RESERVOIR_BASIC_HEIGHT
|
|
||||||
|
|
||||||
def set_tanks(self):
|
|
||||||
"""设置调节池"""
|
|
||||||
for idx in self.tank_data.keys():
|
|
||||||
if self.updating_data_flag is True:
|
|
||||||
if self.current_time_download_data_flag[tanks_id[idx]] is False: # 无法获取实时数据
|
|
||||||
print('There is no current data of tank: {}.'.format(idx))
|
|
||||||
else:
|
|
||||||
self.tank_data[idx] = self.current_time_download_data[tanks_id[idx]]
|
|
||||||
self.tank_data[idx] = self.tank_initial_level_control[idx] \
|
|
||||||
if (self.tank_initial_level_control is not None) and (idx in self.tank_initial_level_control) \
|
|
||||||
else self.tank_data[idx]
|
|
||||||
|
|
||||||
def set_pumps(self):
|
|
||||||
"""设置水泵"""
|
|
||||||
for idx in self.pump_data.keys():
|
|
||||||
if self.updating_data_flag is True:
|
|
||||||
if self.current_time_download_data_flag[pumps_id[idx]] is False: # 无法获取实时数据
|
|
||||||
print('There is no current data of pump: {}.'.format(idx))
|
|
||||||
if (self.pump_control is not None) and (idx in self.pump_control.keys()):
|
|
||||||
self.pump_data[idx] = self.pump_control[idx]
|
|
||||||
else:
|
|
||||||
self.pump_data[idx] = [self.current_time_download_data[pumps_id[idx]]]
|
|
||||||
if (self.pump_control is not None) and (idx in self.pump_control.keys()):
|
|
||||||
self.pump_data[idx] = self.pump_data[idx] + self.pump_control[idx] \
|
|
||||||
if len(self.pump_control[idx]) < len(self.pattern_index_list) \
|
|
||||||
else self.pump_control[idx] # 水泵设定
|
|
||||||
else:
|
|
||||||
if (self.pump_control is not None) and (idx in self.pump_control.keys()):
|
|
||||||
self.pump_data[idx] = self.pump_control[idx]
|
|
||||||
self.pump_data[idx] \
|
|
||||||
= list(np.array(self.pump_data[idx]) / 50) \
|
|
||||||
if idx in variable_frequency_pumps else self.pump_data[idx]
|
|
||||||
|
|
||||||
def set_valves(self):
|
|
||||||
"""设置阀门"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def download_real_data(self, ids: str):
|
|
||||||
"""加载实时数据"""
|
|
||||||
# 数据接口的地址
|
|
||||||
global url_real
|
|
||||||
# 设置GET请求的参数
|
|
||||||
params = {'ids': ids}
|
|
||||||
# 发送GET请求获取数据
|
|
||||||
response = requests.get(url_real, params=params)
|
|
||||||
# 检查响应状态码,200表示请求成功
|
|
||||||
if response.status_code == 200:
|
|
||||||
newest_data_time = None # 下载记录数据的最新时间
|
|
||||||
# 解析响应的JSON数据
|
|
||||||
data = response.json()
|
|
||||||
for realValue in data: # 取出逐个id的数据
|
|
||||||
data_time = convert_utc_to_bj(realValue['datadt']) # datetime
|
|
||||||
self.current_time_download_data[str(realValue['id'])] \
|
|
||||||
= float(realValue['realValue']) # {id(str): value(float)}
|
|
||||||
if data_time > self.current_round_time.replace(tzinfo=None) - timedelta(minutes=5): # 下载数据为实时数据
|
|
||||||
self.current_time_download_data_flag[str(realValue['id'])] = True
|
|
||||||
if newest_data_time is None:
|
|
||||||
newest_data_time = data_time
|
|
||||||
else:
|
|
||||||
newest_data_time = data_time if data_time > newest_data_time else newest_data_time # 更新最新时间
|
|
||||||
if newest_data_time <= self.current_round_time.replace(tzinfo=None) - timedelta(minutes=5): # 最新记录时间早于当前时间
|
|
||||||
warning_text = 'There is no current data with newest timestamp: {}.'.format(
|
|
||||||
newest_data_time.strftime('%Y-%m-%d %H:%M:%S'))
|
|
||||||
delta_time = self.current_round_time.replace(tzinfo=None) - newest_data_time
|
|
||||||
if delta_time < timedelta(minutes=PATTERN_TIME_STEP): # 时间接近(可等待再次下载)
|
|
||||||
raise Exception(warning_text)
|
|
||||||
else:
|
|
||||||
print(warning_text)
|
|
||||||
self.updating_data_flag = False
|
|
||||||
else:
|
|
||||||
for idx in monitor_unity_patterns[-3:]: # 出水管流量
|
|
||||||
if self.current_time_download_data_flag[monitor_patterns_id[idx]] is False: # 无法获取出水管流量的实时数据
|
|
||||||
print('There is no current data of outflow: {}.'.format(idx))
|
|
||||||
self.updating_data_flag = False
|
|
||||||
if self.updating_data_flag is False:
|
|
||||||
print('Abandon updating data with downloaded data.')
|
|
||||||
return newest_data_time
|
|
||||||
else:
|
|
||||||
# 如果请求不成功,打印错误信息
|
|
||||||
print("请求失败,状态码:", response.status_code)
|
|
||||||
raise ConnectionError('Cannot download data.')
|
|
||||||
|
|
||||||
@ staticmethod
|
|
||||||
def init_dict_of_list(dict_of_list):
|
|
||||||
"""初始化值为列表的字典(重新生成列表地址, 防止指向同一列表)"""
|
|
||||||
for idx in dict_of_list.keys():
|
|
||||||
dict_of_list[idx] = dict_of_list[idx].copy()
|
|
||||||
return dict_of_list
|
|
||||||
|
|
||||||
@ staticmethod
|
|
||||||
def get_download_id():
|
|
||||||
"""生成下载数据项的id"""
|
|
||||||
# id_list = (list(monitor_single_patterns_id.values())
|
|
||||||
# + list(monitor_unity_patterns_id.values())
|
|
||||||
# + list(tanks_id.values())
|
|
||||||
# + list(reservoirs_id.values())
|
|
||||||
# + list(pumps_id.values()))
|
|
||||||
id_list = (list(monitor_unity_patterns_id.values())[-3:]
|
|
||||||
+ list(tanks_id.values())
|
|
||||||
+ list(reservoirs_id.values())
|
|
||||||
+ list(pumps_id.values()))
|
|
||||||
id_list = sorted(set(id_list), key=id_list.index)
|
|
||||||
if None in id_list:
|
|
||||||
id_list.remove(None)
|
|
||||||
return ','.join(id_list)
|
|
||||||
|
|
||||||
@ staticmethod
|
|
||||||
def get_history_pattern_info(project_name, pattern_name):
|
|
||||||
"""读取选定pattern的保存的历史pattern信息(flow, factor)"""
|
|
||||||
factors_list = []
|
|
||||||
flow_list = []
|
|
||||||
patterns_info = read_all(project_name,
|
|
||||||
f"select * from history_patterns_flows where id = '{pattern_name}' order by _order")
|
|
||||||
for item in patterns_info:
|
|
||||||
flow_list.append(float(item['flow']))
|
|
||||||
factors_list.append(float(item['factor']))
|
|
||||||
return flow_list, factors_list
|
|
||||||
|
|
||||||
@ staticmethod
|
|
||||||
def judge_time(current_time, time_index_list):
|
|
||||||
"""时间判断"""
|
|
||||||
current_index \
|
|
||||||
= time_index_list.index(current_time) if (current_time in time_index_list) else None
|
|
||||||
return current_index
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_time_index_list(start_time: datetime, end_time: datetime, step: int):
|
|
||||||
"""生成时间索引"""
|
|
||||||
time_index_list = [] # 时间索引[str]
|
|
||||||
time_index = start_time
|
|
||||||
while time_index <= end_time:
|
|
||||||
time_index_list.append(time_index)
|
|
||||||
time_index += timedelta(minutes=step)
|
|
||||||
return time_index_list
|
|
||||||
|
|
||||||
@ staticmethod
|
|
||||||
def round_time(time_: datetime, interval=5):
|
|
||||||
"""时间向下取整到整n分钟(北京时间): 四舍六入五留双/向下取整"""
|
|
||||||
# return datetime.fromtimestamp(round(time_.timestamp() / (60 * interval)) * (60 * interval))
|
|
||||||
return datetime.fromtimestamp(int((time_.timestamp()) // (60 * interval)) * (60 * interval))
|
|
||||||
|
|
||||||
|
|
||||||
def convert_utc_to_bj(utc_time_str):
|
|
||||||
"""将utc时间(str)转换成北京时间(datetime)"""
|
|
||||||
# 解析UTC时间字符串为datetime对象
|
|
||||||
utc_time = datetime.strptime(utc_time_str, '%Y-%m-%dT%H:%M:%SZ')
|
|
||||||
# 设定UTC时区
|
|
||||||
utc_timezone = pytz.timezone('UTC')
|
|
||||||
# 转换为北京时间
|
|
||||||
beijing_timezone = pytz.timezone('Asia/Shanghai')
|
|
||||||
beijing_time = utc_time.replace(tzinfo=utc_timezone).astimezone(beijing_timezone).replace(tzinfo=None)
|
|
||||||
return beijing_time
|
|
||||||
|
|
||||||
|
|
||||||
def get_datetime(cur_datetime:str):
|
|
||||||
str_format = "%Y-%m-%d %H:%M:%S"
|
|
||||||
return datetime.strptime(cur_datetime, str_format)
|
|
||||||
|
|
||||||
|
|
||||||
def get_strftime(cur_datetime: datetime):
|
|
||||||
str_format = "%Y-%m-%d %H:%M:%S"
|
|
||||||
return cur_datetime.strftime(str_format)
|
|
||||||
|
|
||||||
|
|
||||||
def step_time(cur_datetime:str, step=5):
|
|
||||||
str_format="%Y-%m-%d %H:%M:%S"
|
|
||||||
dt=datetime.strptime(cur_datetime,str_format)
|
|
||||||
dt=dt+timedelta(minutes=step)
|
|
||||||
return datetime.strftime(dt,str_format)
|
|
||||||
|
|
||||||
|
|
||||||
def get_pattern_index(cur_datetime:str)->int:
|
|
||||||
str_format="%Y-%m-%d %H:%M:%S"
|
|
||||||
dt=datetime.strptime(cur_datetime,str_format)
|
|
||||||
hr=dt.hour
|
|
||||||
mnt=dt.minute
|
|
||||||
i=int((hr*60+mnt)/PATTERN_TIME_STEP)
|
|
||||||
return i
|
|
||||||
|
|
||||||
def get_pattern_index_str(cur_datetime:str)->str:
|
|
||||||
i=get_pattern_index(cur_datetime)
|
|
||||||
[minN,hrN]=modf(i*PATTERN_TIME_STEP/60)
|
|
||||||
minN_str=str(int(minN*60))
|
|
||||||
minN_str=minN_str.zfill(2)
|
|
||||||
hrN_str=str(int(hrN))
|
|
||||||
hrN_str=hrN_str.zfill(2)
|
|
||||||
str_i='{}:{}:00'.format(hrN_str,minN_str)
|
|
||||||
return str_i
|
|
||||||
|
|
||||||
|
|
||||||
def from_seconds_to_clock (secs: int)->str:
|
|
||||||
hrs=int(secs/3600)
|
|
||||||
minutes=int((secs-hrs*3600)/60)
|
|
||||||
seconds=(secs-hrs*3600-minutes*60)
|
|
||||||
hrs_str=str(hrs).zfill(2)
|
|
||||||
minutes_str=str(minutes).zfill(2)
|
|
||||||
seconds_str=str(seconds).zfill(2)
|
|
||||||
str_clock='{}:{}:{}'.format(hrs_str,minutes_str,seconds_str)
|
|
||||||
return str_clock
|
|
||||||
|
|
||||||
def from_clock_to_seconds (clock: str)->int:
|
|
||||||
str_format="%Y-%m-%d %H:%M:%S"
|
|
||||||
dt=datetime.strptime(clock,str_format)
|
|
||||||
hr=dt.hour
|
|
||||||
mnt=dt.minute
|
|
||||||
seconds=dt.second
|
|
||||||
return hr*3600+mnt*60+seconds
|
|
||||||
|
|
||||||
def from_clock_to_seconds_2 (clock: str)->int:
|
|
||||||
return parse_clock_duration_seconds(clock)
|
|
||||||
|
|
||||||
|
|
||||||
def from_clock_to_seconds_3 (clock: str)->int:
|
|
||||||
return parse_clock_duration_seconds(clock)
|
|
||||||
|
|
||||||
|
|
||||||
###convert datetimestring
|
|
||||||
##"XXXX-XX-XXT00:00:00Z" ->"XXXX-XX-XX 00:00:00"
|
|
||||||
def trim_time_flag(url_date_time:str)->str:
|
|
||||||
str_datetime=str.replace(url_date_time,'T',' ')
|
|
||||||
str_datetime=str.replace(str_datetime,'Z','')
|
|
||||||
return str_datetime
|
|
||||||
|
|
||||||
# 单时间步长模拟
|
|
||||||
def run_simulation(name:str,start_datetime:str,end_datetime:str=None, duration:int=900)->str:
|
|
||||||
if(is_project_open(name)):
|
|
||||||
close_project(name)
|
|
||||||
open_project(name)
|
|
||||||
#get_current_data(cur_datetime)
|
|
||||||
#extract the patternindex from datetime
|
|
||||||
#e.g. 0: the first time step for 00:00-00:14; 1: the second step for 00:15-00:30
|
|
||||||
start_datetime=trim_time_flag(start_datetime)
|
|
||||||
if(end_datetime!=None):
|
|
||||||
end_datetime=trim_time_flag(end_datetime)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## redistribute the basedemand according to the currentTotalQ and the base_totalQ
|
|
||||||
# step 1. get_real _data
|
|
||||||
if end_datetime==None or start_datetime==end_datetime:
|
|
||||||
end_datetime=step_time(start_datetime)
|
|
||||||
|
|
||||||
# # ids=['2498','3854','3853','2510','2514','4780','4854']
|
|
||||||
# # real_data=get_real_data(ids,start_datetime,end_datetime)
|
|
||||||
# print(datetime.now(pytz.timezone('Asia/Shanghai')).strftime("%Y-%m-%d %H:%M:%S")+"--获取实时数据完毕\n")
|
|
||||||
# #step 2. re-distribute the real q to base demand of the node region_sa by region_sa
|
|
||||||
# regions=get_all_service_area_ids(name)
|
|
||||||
# total_demands={}
|
|
||||||
# for region in regions:
|
|
||||||
# total_demands[region]=get_total_base_demand(name,region)
|
|
||||||
# region_demand_factor={}
|
|
||||||
# #Region_ID:SA_ZBBDJSCP000002 高区;SA_R00003+SA_ZBBDTJSC000001 低区
|
|
||||||
# H_region_real_demands=real_data[DN_900_ID][start_datetime]+real_data[DN_500_ID][start_datetime]
|
|
||||||
# L_region_real_demands=real_data[DN_1000_ID][start_datetime]
|
|
||||||
# factor_H_zone=H_region_real_demands/total_demands[H_REGION_1]/3.6 #3.6: m3/h->L/s
|
|
||||||
# factor_L_zone=L_region_real_demands/(total_demands[L_REGION_1]+total_demands[L_REGION_2])/3.6
|
|
||||||
# print(datetime.now(pytz.timezone('Asia/Shanghai')).strftime("%Y-%m-%d %H:%M:%S")+"--流量因子计算完毕完毕\n")
|
|
||||||
# for region in regions:
|
|
||||||
# region_nodes=get_nodes_in_region(name,region)
|
|
||||||
# factor=1
|
|
||||||
# if region==H_REGION_1 or H_REGION_2:
|
|
||||||
# factor=factor_H_zone
|
|
||||||
# else:
|
|
||||||
# factor=factor_L_zone
|
|
||||||
#
|
|
||||||
# for node in region_nodes:
|
|
||||||
# d=get_demand(name,node)
|
|
||||||
# for r in d['demands']:
|
|
||||||
# r['demand']=factor*r['demand']
|
|
||||||
# cs=ChangeSet()
|
|
||||||
# cs.append(d)
|
|
||||||
# set_demand(name,cs)
|
|
||||||
#
|
|
||||||
# #
|
|
||||||
# #
|
|
||||||
# print(datetime.now(pytz.timezone('Asia/Shanghai')).strftime("%Y-%m-%d %H:%M:%S")+"--节点流量重分配完毕\n")
|
|
||||||
|
|
||||||
#step 3. set pattern index to the current time,and set duration to 300 secs
|
|
||||||
#
|
|
||||||
str_pattern_start=get_pattern_index_str(start_datetime)
|
|
||||||
dic_time=get_time(name)
|
|
||||||
dic_time['PATTERN START']=str_pattern_start
|
|
||||||
|
|
||||||
if duration !=None:
|
|
||||||
dic_time['DURATION']=from_seconds_to_clock(duration)
|
|
||||||
else:
|
|
||||||
dic_time['DURATION']=dic_time['HYDRAULIC TIMESTEP']
|
|
||||||
cs=ChangeSet()
|
|
||||||
cs.operations.append(dic_time)
|
|
||||||
set_time(name,cs)
|
|
||||||
|
|
||||||
# step4. run simulation and save the result to name-time.out for download
|
|
||||||
#inp_file = 'inp\\'+name+'.inp'
|
|
||||||
#db_name=name
|
|
||||||
#dump_inp(db_name,inp_file,'2')
|
|
||||||
# result=run_inp(db_name)
|
|
||||||
result=run_project(name)
|
|
||||||
#json string format
|
|
||||||
# simulation_result, output, report
|
|
||||||
result_data=json.loads(result)
|
|
||||||
#print(result_data['simulation_result'])
|
|
||||||
print(datetime.now(pytz.timezone('Asia/Shanghai')).strftime("%Y-%m-%d %H:%M:%S")+'run finished successfully\n')
|
|
||||||
#print(result_data['report'])
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# 在线模拟
|
|
||||||
def run_simulation_ex(name: str, simulation_type: str, start_datetime: str,
|
|
||||||
end_datetime: str = None, duration: int = 0,
|
|
||||||
pump_control: dict[str, list] = None, tank_initial_level_control: dict[str, float] = None,
|
|
||||||
region_demand_control: dict[str, float] = None, valve_control: dict[str, dict] = None,
|
|
||||||
downloading_prohibition: bool = False) -> str:
|
|
||||||
time_cost_start = time.perf_counter()
|
|
||||||
print('{} -- Hydraulic simulation started.'.format(
|
|
||||||
datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d %H:%M:%S')))
|
|
||||||
|
|
||||||
if is_project_open(name):
|
|
||||||
close_project(name)
|
|
||||||
|
|
||||||
if simulation_type.upper() == 'REALTIME': # 实时模拟(修改原数据库)
|
|
||||||
name_c = name
|
|
||||||
elif simulation_type.upper() == 'EXTENDED': # 扩展模拟(复制数据库)
|
|
||||||
name_c = '_'.join([name, 'c'])
|
|
||||||
if have_project(name_c):
|
|
||||||
if is_project_open(name_c):
|
|
||||||
close_project(name_c)
|
|
||||||
delete_project(name_c)
|
|
||||||
copy_project(name, name_c) # 备份项目
|
|
||||||
else:
|
|
||||||
raise Exception('Incorrect simulation type, choose in (realtime, extended)')
|
|
||||||
|
|
||||||
open_project(name_c)
|
|
||||||
|
|
||||||
# 时间处理
|
|
||||||
# extract the pattern index from datetime
|
|
||||||
# e.g. 0: the first time step for 00:00-00:14; 1: the second step for 00:15-00:30
|
|
||||||
# start_datetime = get_strftime(convert_utc_to_bj(start_datetime))
|
|
||||||
start_datetime = trim_time_flag(start_datetime)
|
|
||||||
if end_datetime is not None:
|
|
||||||
# end_datetime = get_strftime(convert_utc_to_bj(end_datetime))
|
|
||||||
end_datetime = trim_time_flag(end_datetime)
|
|
||||||
|
|
||||||
# pump name转化/输入值规范化
|
|
||||||
if pump_control is not None:
|
|
||||||
for key in list(pump_control.keys()):
|
|
||||||
pump_control[key] = [pump_control[key]] if type(pump_control[key]) is not list else pump_control[key]
|
|
||||||
pump_control[pumps[pumps_name.index(key)]] = pump_control.pop(key)
|
|
||||||
|
|
||||||
# 重新分配节点(nodes)水量
|
|
||||||
# 1) (single)base_demand_new=1, pattern_new=real_data
|
|
||||||
# 2) (unity)base_demand_new=base_demand_old, pattern_new=factor*pattern_old(factor=flow_new/flow_old)
|
|
||||||
# 获取需水量数据
|
|
||||||
# a) 历史pattern对应水量(读取保存数据库)
|
|
||||||
# b) 实时水量(数据接口下载)
|
|
||||||
|
|
||||||
# 修改node demand = 1, pattern factor *= demand(monitor single patterns对应node)
|
|
||||||
# nodes = get_nodes(name_c) # nodes
|
|
||||||
# for node_name in nodes: # 遍历nodes
|
|
||||||
# demands_dict = get_demand(name_c, node_name) # {'demands':[{'demand':, 'pattern':}]}
|
|
||||||
# for demands in demands_dict['demands']:
|
|
||||||
# if (demands['pattern'] in monitor_single_patterns) and (demands['demand'] != 1): # 1)
|
|
||||||
# pattern = get_pattern(name_c, demands['pattern'])
|
|
||||||
# pattern['factors'] = list(demands['demand'] * np.array(pattern['factors'])) # 修改pattern
|
|
||||||
# cs = ChangeSet()
|
|
||||||
# cs.append(pattern)
|
|
||||||
# set_pattern(name_c, cs)
|
|
||||||
# demands_dict['demands'][
|
|
||||||
# demands_dict['demands'].index(demands)
|
|
||||||
# ]['demand'] = 1 # 修改demand
|
|
||||||
# cs = ChangeSet()
|
|
||||||
# cs.append(demands_dict)
|
|
||||||
# set_demand(name_c, cs)
|
|
||||||
|
|
||||||
start_time = get_datetime(start_datetime) # datetime
|
|
||||||
end_time = get_datetime(end_datetime) \
|
|
||||||
if end_datetime is not None \
|
|
||||||
else get_datetime(start_datetime) + timedelta(seconds=duration) # datetime
|
|
||||||
# modify_pattern_start_index = get_pattern_index(start_datetime) # 待修改pattern的起始索引(int)
|
|
||||||
|
|
||||||
dataset_loader = DataLoader(project_name=name_c,
|
|
||||||
start_time=start_time, end_time=end_time,
|
|
||||||
pumps_control=pump_control, tank_initial_level_control=tank_initial_level_control,
|
|
||||||
region_demand_control=region_demand_control,
|
|
||||||
downloading_prohibition=downloading_prohibition) # 实例化数据加载器
|
|
||||||
modify_index \
|
|
||||||
= dataset_loader.load_data() # 加载数据(index: 需要修改pattern的factor index, None: 无需修改除水泵和调节池外pattern)
|
|
||||||
new_patterns \
|
|
||||||
= dataset_loader.new_pattern_factor # {name: float,} pattern factor(实时: 更新, 其他: 保持/更新(设定用水量时))
|
|
||||||
tank_init_level = dataset_loader.tank_data # {name: float,} 调节池初始液位(实时: 更新, 其他: 保持/更新(设定液位时))
|
|
||||||
reservoir_level = dataset_loader.reservoir_data # {name: float,} 水库液位(实时: 更新, 其他: 保持)
|
|
||||||
pump_freq = dataset_loader.pump_data # {name: [float,]} 水泵频率(实时: 更新, 其他: 保持/更新(设定状态时))
|
|
||||||
print(datetime.now(pytz.timezone('Asia/Shanghai')).strftime("%Y-%m-%d %H:%M:%S") + " -- Loading data ok.\n")
|
|
||||||
|
|
||||||
pattern_name_list = get_patterns(name_c) # 所有pattern
|
|
||||||
# 修改node pattern/demand
|
|
||||||
# nodes = get_nodes(name_c) # nodes
|
|
||||||
# for node_name in nodes: # 遍历nodes
|
|
||||||
# demands_dict = get_demand(name_c, node_name) # {'demands':[{'demand':, 'pattern':}]}
|
|
||||||
# for demands in demands_dict['demands']:
|
|
||||||
# if demands['pattern'] in monitor_single_patterns: # 1)
|
|
||||||
# demands_dict['demands'][
|
|
||||||
# demands_dict['demands'].index(demands)
|
|
||||||
# ]['demand'] = 1 # 修改demand
|
|
||||||
# pattern = get_pattern(name_c, demands['pattern'])
|
|
||||||
# pattern['factors'][modify_index] = flow_new[demands['pattern']] # 修改pattern
|
|
||||||
# cs = ChangeSet()
|
|
||||||
# cs.append(pattern)
|
|
||||||
# set_pattern(name_c, cs)
|
|
||||||
# if demands['pattern'] in pattern_name_list:
|
|
||||||
# pattern_name_list.remove(demands['pattern']) # 移出待修改pattern列表
|
|
||||||
# else: # 2)
|
|
||||||
# continue
|
|
||||||
# cs = ChangeSet()
|
|
||||||
# cs.append(demands_dict)
|
|
||||||
# set_demand(name_c, cs)
|
|
||||||
for pattern_name in monitor_patterns: # 遍历patterns
|
|
||||||
if not np.isnan(new_patterns[pattern_name][0]):
|
|
||||||
pattern = get_pattern(name_c, pattern_name)
|
|
||||||
pattern['factors'][modify_index:
|
|
||||||
modify_index + len(new_patterns[pattern_name])] \
|
|
||||||
= new_patterns[pattern_name]
|
|
||||||
cs = ChangeSet()
|
|
||||||
cs.append(pattern)
|
|
||||||
set_pattern(name_c, cs)
|
|
||||||
if pattern_name in pattern_name_list:
|
|
||||||
pattern_name_list.remove(pattern_name) # 移出待修改pattern列表
|
|
||||||
|
|
||||||
# 修改清水池(reservoir)液位pattern
|
|
||||||
for reservoir_name in reservoirs: # 遍历reservoirs
|
|
||||||
if (not np.isnan(reservoir_level[reservoir_name])) and (reservoir_level[reservoir_name] != 0):
|
|
||||||
reservoir_pattern = get_pattern(name_c, get_reservoir(name_c, reservoir_name)['pattern'])
|
|
||||||
reservoir_pattern['factors'][modify_index] = reservoir_level[reservoir_name]
|
|
||||||
cs = ChangeSet()
|
|
||||||
cs.append(reservoir_pattern)
|
|
||||||
set_pattern(name_c, cs)
|
|
||||||
if reservoir_pattern['id'] in pattern_name_list:
|
|
||||||
pattern_name_list.remove(reservoir_pattern['id']) # 移出待修改pattern列表
|
|
||||||
|
|
||||||
# 修改调节池(tank)初始液位
|
|
||||||
for tank_name in tanks: # 遍历tanks
|
|
||||||
if (not np.isnan(tank_init_level[tank_name])) and (tank_init_level[tank_name] != 0):
|
|
||||||
tank = get_tank(name_c, tank_name)
|
|
||||||
tank['init_level'] = tank_init_level[tank_name]
|
|
||||||
cs = ChangeSet()
|
|
||||||
cs.append(tank)
|
|
||||||
set_tank(name_c, cs)
|
|
||||||
|
|
||||||
# 修改水泵(pump)pattern
|
|
||||||
for pump_name in pumps: # 遍历pumps
|
|
||||||
if not np.isnan(pump_freq[pump_name][0]):
|
|
||||||
pump_pattern = get_pattern(name_c, get_pump(name_c, pump_name)['pattern'])
|
|
||||||
pump_pattern['factors'][modify_index
|
|
||||||
:modify_index + len(pump_freq[pump_name])] \
|
|
||||||
= pump_freq[pump_name]
|
|
||||||
cs = ChangeSet()
|
|
||||||
cs.append(pump_pattern)
|
|
||||||
set_pattern(name_c, cs)
|
|
||||||
if pump_pattern['id'] in pattern_name_list:
|
|
||||||
pattern_name_list.remove(pump_pattern['id']) # 移出待修改pattern列表
|
|
||||||
# 修改阀门(valve)status和setting
|
|
||||||
if valve_control is not None:
|
|
||||||
for valve in valve_control.keys():
|
|
||||||
status = get_status(name_c, valve)
|
|
||||||
if 'status' in valve_control[valve].keys():
|
|
||||||
status['status'] = valve_control[valve]['status']
|
|
||||||
if 'setting' in valve_control[valve].keys():
|
|
||||||
status['setting'] = valve_control[valve]['setting']
|
|
||||||
if 'k' in valve_control[valve].keys():
|
|
||||||
valve_k = valve_control[valve]['k']
|
|
||||||
if valve_k == 0:
|
|
||||||
status['status'] = 'CLOSED'
|
|
||||||
else:
|
|
||||||
status['setting'] = 0.1036 * pow(valve_k, -3.105)
|
|
||||||
cs = ChangeSet()
|
|
||||||
cs.append(status)
|
|
||||||
set_status(name_c, cs)
|
|
||||||
|
|
||||||
print('Finish demands amending, unmodified patterns: {}.'.format(pattern_name_list))
|
|
||||||
|
|
||||||
# 修改时间信息
|
|
||||||
str_pattern_start = get_pattern_index_str(
|
|
||||||
DataLoader.round_time(start_time, int(PATTERN_TIME_STEP)).strftime("%Y-%m-%d %H:%M:%S"))
|
|
||||||
dic_time = get_time(name_c)
|
|
||||||
dic_time['PATTERN START'] = str_pattern_start
|
|
||||||
|
|
||||||
if duration is not None:
|
|
||||||
dic_time['DURATION'] = from_seconds_to_clock(duration)
|
|
||||||
else:
|
|
||||||
dic_time['DURATION'] = dic_time['HYDRAULIC TIMESTEP']
|
|
||||||
cs = ChangeSet()
|
|
||||||
cs.operations.append(dic_time)
|
|
||||||
set_time(name_c, cs)
|
|
||||||
|
|
||||||
# 运行并返回结果
|
|
||||||
result = run_project(name_c)
|
|
||||||
|
|
||||||
time_cost_end = time.perf_counter()
|
|
||||||
print('{} -- Hydraulic simulation finished, cost time: {:.2f} s.'.format(
|
|
||||||
datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d %H:%M:%S'),
|
|
||||||
time_cost_end - time_cost_start))
|
|
||||||
|
|
||||||
close_project(name_c)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# if get_current_data()==True:
|
|
||||||
# tQ=get_current_total_Q()
|
|
||||||
# print(f"the current tQ is {tQ}\n")
|
|
||||||
# data=get_hist_data(ids,conver_beingtime_to_ucttime('2024-04-10 15:05:00'),conver_beingtime_to_ucttime('2024-04-10 15:10:00'))
|
|
||||||
# open_project("beibeizone")
|
|
||||||
# read_inp("beibeizone","beibeizone-export_nochinese.inp")
|
|
||||||
# run_simulation("beibeizone","2024-04-01T08:00:00Z")
|
|
||||||
# read_inp('bb_server', 'model20_en.inp')
|
|
||||||
run_simulation_ex(
|
|
||||||
name=project_info.name, simulation_type='extended', start_datetime='2024-11-09T02:30:00Z',
|
|
||||||
# end_datetime='2024-05-30T16:00:00Z',
|
|
||||||
# duration=0,
|
|
||||||
# pump_control={'PU00006': [45, 40]}
|
|
||||||
# region_demand_control={'hp': 6000, 'lp': 2000}
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from app.algorithms.valve_isolation.topology_search import valve_isolation_analysis
|
||||||
|
|
||||||
|
__all__ = ["valve_isolation_analysis"]
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
"""Topology-only valve isolation search."""
|
||||||
|
|
||||||
|
from collections import defaultdict, deque
|
||||||
|
from typing import Any, Iterable
|
||||||
|
|
||||||
|
|
||||||
|
VALVE_LINK_TYPE = "valve"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_link_entry(link_entry: str) -> tuple[str, str, str, str]:
|
||||||
|
parts = link_entry.split(":", 3)
|
||||||
|
if len(parts) != 4:
|
||||||
|
raise ValueError(f"Invalid link entry format: {link_entry}")
|
||||||
|
return parts[0], parts[1], parts[2], parts[3]
|
||||||
|
|
||||||
|
|
||||||
|
def valve_isolation_analysis(
|
||||||
|
link_entries: Iterable[str],
|
||||||
|
accident_elements: str | list[str],
|
||||||
|
disabled_valves: list[str] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Determine boundary valves and affected nodes from a topology snapshot."""
|
||||||
|
|
||||||
|
disabled_valves_set = set(disabled_valves or [])
|
||||||
|
target_elements = (
|
||||||
|
[accident_elements]
|
||||||
|
if isinstance(accident_elements, str)
|
||||||
|
else accident_elements
|
||||||
|
)
|
||||||
|
|
||||||
|
pipe_adj: dict[str, set[str]] = defaultdict(set)
|
||||||
|
all_valves: dict[str, tuple[str, str]] = {}
|
||||||
|
link_lookup: dict[str, tuple[str, str, str]] = {}
|
||||||
|
node_set: set[str] = set()
|
||||||
|
for link_entry in link_entries:
|
||||||
|
link_id, link_type, node1, node2 = _parse_link_entry(link_entry)
|
||||||
|
link_type_name = str(link_type).lower()
|
||||||
|
link_lookup[link_id] = (node1, node2, link_type_name)
|
||||||
|
node_set.update((node1, node2))
|
||||||
|
if link_type_name == VALVE_LINK_TYPE:
|
||||||
|
all_valves[link_id] = (node1, node2)
|
||||||
|
else:
|
||||||
|
pipe_adj[node1].add(node2)
|
||||||
|
pipe_adj[node2].add(node1)
|
||||||
|
|
||||||
|
start_nodes: set[str] = set()
|
||||||
|
for element in target_elements:
|
||||||
|
if element in node_set:
|
||||||
|
start_nodes.add(element)
|
||||||
|
elif element in link_lookup:
|
||||||
|
node1, node2, _ = link_lookup[element]
|
||||||
|
start_nodes.update((node1, node2))
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Accident element {element} was not found in topology")
|
||||||
|
|
||||||
|
extra_adj: dict[str, list[str]] = defaultdict(list)
|
||||||
|
boundary_valves: dict[str, tuple[str, str]] = {}
|
||||||
|
for valve_id, (node1, node2) in all_valves.items():
|
||||||
|
if valve_id in disabled_valves_set:
|
||||||
|
extra_adj[node1].append(node2)
|
||||||
|
extra_adj[node2].append(node1)
|
||||||
|
else:
|
||||||
|
boundary_valves[valve_id] = (node1, node2)
|
||||||
|
|
||||||
|
affected_nodes: set[str] = set()
|
||||||
|
queue = deque(start_nodes)
|
||||||
|
while queue:
|
||||||
|
node = queue.popleft()
|
||||||
|
if node in affected_nodes:
|
||||||
|
continue
|
||||||
|
affected_nodes.add(node)
|
||||||
|
queue.extend(pipe_adj.get(node, set()) - affected_nodes)
|
||||||
|
queue.extend(
|
||||||
|
neighbor
|
||||||
|
for neighbor in extra_adj.get(node, ())
|
||||||
|
if neighbor not in affected_nodes
|
||||||
|
)
|
||||||
|
|
||||||
|
must_close_valves: list[str] = []
|
||||||
|
optional_valves: list[str] = []
|
||||||
|
for valve_id, (node1, node2) in boundary_valves.items():
|
||||||
|
node1_affected = node1 in affected_nodes
|
||||||
|
node2_affected = node2 in affected_nodes
|
||||||
|
if node1_affected and node2_affected:
|
||||||
|
optional_valves.append(valve_id)
|
||||||
|
elif node1_affected or node2_affected:
|
||||||
|
must_close_valves.append(valve_id)
|
||||||
|
|
||||||
|
must_close_valves.sort()
|
||||||
|
optional_valves.sort()
|
||||||
|
isolatable = bool(must_close_valves)
|
||||||
|
result: dict[str, Any] = {
|
||||||
|
"accident_elements": target_elements,
|
||||||
|
"disabled_valves": disabled_valves,
|
||||||
|
"affected_nodes": sorted(affected_nodes) if isolatable else [],
|
||||||
|
"affected_node_count": len(affected_nodes),
|
||||||
|
"must_close_valves": must_close_valves,
|
||||||
|
"optional_valves": optional_valves,
|
||||||
|
"isolatable": isolatable,
|
||||||
|
}
|
||||||
|
if len(target_elements) == 1:
|
||||||
|
result["accident_element"] = target_elements[0]
|
||||||
|
return result
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from typing import Generic, TypeVar
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
class PaginatedList(list[T], Generic[T]):
|
||||||
|
"""A page of items carrying the total count from its data source."""
|
||||||
|
|
||||||
|
def __init__(self, items: Iterable[T], *, total: int) -> None:
|
||||||
|
super().__init__(items)
|
||||||
|
self.total = total
|
||||||
@@ -8,6 +8,8 @@ from fastapi.exceptions import RequestValidationError
|
|||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from app.native.wndb.core.database import MaterializedViewRefreshAfterCommitError
|
||||||
|
|
||||||
|
|
||||||
class ProblemDetails(BaseModel):
|
class ProblemDetails(BaseModel):
|
||||||
"""RFC 9457 compatible error response used by the REST contract."""
|
"""RFC 9457 compatible error response used by the REST contract."""
|
||||||
@@ -53,6 +55,24 @@ def _problem_response(
|
|||||||
|
|
||||||
|
|
||||||
def install_problem_details_handlers(app: FastAPI) -> None:
|
def install_problem_details_handlers(app: FastAPI) -> None:
|
||||||
|
@app.exception_handler(MaterializedViewRefreshAfterCommitError)
|
||||||
|
async def materialized_view_refresh_error_handler(
|
||||||
|
request: Request,
|
||||||
|
exc: MaterializedViewRefreshAfterCommitError,
|
||||||
|
) -> JSONResponse:
|
||||||
|
response = _problem_response(
|
||||||
|
request,
|
||||||
|
status_code=503,
|
||||||
|
title="Materialized view refresh failed",
|
||||||
|
detail=(
|
||||||
|
f"Project {exc.project!r} changes were committed, but GIS query "
|
||||||
|
"views could not be refreshed. Do not repeat the write blindly."
|
||||||
|
),
|
||||||
|
code="materialized_view_refresh_failed_after_commit",
|
||||||
|
)
|
||||||
|
response.headers["X-TJWater-Changes-Committed"] = "true"
|
||||||
|
return response
|
||||||
|
|
||||||
@app.exception_handler(RequestValidationError)
|
@app.exception_handler(RequestValidationError)
|
||||||
async def validation_error_handler(
|
async def validation_error_handler(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -252,6 +252,12 @@ async def list_admin_projects(
|
|||||||
"/admin/projects",
|
"/admin/projects",
|
||||||
response_model=AdminProjectResponse,
|
response_model=AdminProjectResponse,
|
||||||
status_code=status.HTTP_201_CREATED,
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
deprecated=True,
|
||||||
|
summary="仅登记已有项目元数据",
|
||||||
|
description=(
|
||||||
|
"仅用于登记已经由外部流程完整创建的资源。新项目应调用 "
|
||||||
|
"POST /admin/project-provisions。"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
async def create_admin_project(
|
async def create_admin_project(
|
||||||
payload: AdminProjectCreateRequest,
|
payload: AdminProjectCreateRequest,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from app.auth.metadata_dependencies import (
|
|||||||
get_current_metadata_admin,
|
get_current_metadata_admin,
|
||||||
get_current_metadata_user,
|
get_current_metadata_user,
|
||||||
)
|
)
|
||||||
|
from app.api.pagination import PaginatedList
|
||||||
from app.core.audit import AuditAction, log_audit_event
|
from app.core.audit import AuditAction, log_audit_event
|
||||||
from app.domain.schemas.audit import AuditLogResponse
|
from app.domain.schemas.audit import AuditLogResponse
|
||||||
from app.infra.db.metadb.database import get_metadata_session
|
from app.infra.db.metadb.database import get_metadata_session
|
||||||
@@ -46,7 +47,7 @@ async def get_audit_logs(
|
|||||||
_current_user=Depends(get_current_metadata_admin),
|
_current_user=Depends(get_current_metadata_admin),
|
||||||
audit_repo: AuditRepository = Depends(get_audit_repository),
|
audit_repo: AuditRepository = Depends(get_audit_repository),
|
||||||
) -> list[AuditLogResponse]:
|
) -> list[AuditLogResponse]:
|
||||||
return await audit_repo.get_logs(
|
items = await audit_repo.get_logs(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
action=action,
|
action=action,
|
||||||
@@ -56,6 +57,15 @@ async def get_audit_logs(
|
|||||||
skip=skip,
|
skip=skip,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
|
total = await audit_repo.get_log_count(
|
||||||
|
user_id=user_id,
|
||||||
|
project_id=project_id,
|
||||||
|
action=action,
|
||||||
|
resource_type=resource_type,
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
|
)
|
||||||
|
return PaginatedList(items, total=total)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
@@ -119,7 +129,7 @@ async def get_my_audit_logs(
|
|||||||
current_user=Depends(get_current_metadata_user),
|
current_user=Depends(get_current_metadata_user),
|
||||||
audit_repo: AuditRepository = Depends(get_audit_repository),
|
audit_repo: AuditRepository = Depends(get_audit_repository),
|
||||||
) -> list[AuditLogResponse]:
|
) -> list[AuditLogResponse]:
|
||||||
return await audit_repo.get_logs(
|
items = await audit_repo.get_logs(
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
action=action,
|
action=action,
|
||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
@@ -127,3 +137,10 @@ async def get_my_audit_logs(
|
|||||||
skip=skip,
|
skip=skip,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
|
total = await audit_repo.get_log_count(
|
||||||
|
user_id=current_user.id,
|
||||||
|
action=action,
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
|
)
|
||||||
|
return PaginatedList(items, total=total)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Body
|
from fastapi import APIRouter, Depends, HTTPException, Body
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
|
||||||
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
||||||
from app.services.burst_detection import (
|
from app.services.burst_detection import (
|
||||||
@@ -43,8 +45,7 @@ class BurstDetectionRequest(BaseModel):
|
|||||||
sensor_nodes: list[str] | None = Field(None, description="传感器节点列表")
|
sensor_nodes: list[str] | None = Field(None, description="传感器节点列表")
|
||||||
scheme_name: str | None = Field(None, description="方案名称")
|
scheme_name: str | None = Field(None, description="方案名称")
|
||||||
data_source: str = Field("monitoring", description="数据来源:monitoring(监测)或simulation(模拟)")
|
data_source: str = Field("monitoring", description="数据来源:monitoring(监测)或simulation(模拟)")
|
||||||
simulation_scheme_name: str | None = Field(None, description="模拟方案名称")
|
simulation_run_id: UUID | None = Field(None, description="分析模拟运行 ID")
|
||||||
simulation_scheme_type: str | None = Field(None, description="模拟方案类型")
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
@@ -73,6 +74,10 @@ async def detect_burst(
|
|||||||
HTTPException: 当处理过程中发生错误时
|
HTTPException: 当处理过程中发生错误时
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return run_burst_detection(**data.model_dump(), username=username)
|
return await run_in_threadpool(
|
||||||
|
run_burst_detection,
|
||||||
|
**data.model_dump(),
|
||||||
|
username=username,
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc))
|
raise HTTPException(status_code=400, detail=str(exc))
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ from typing import Any
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Body
|
from fastapi import APIRouter, Depends, HTTPException, Body
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
|
||||||
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
||||||
from app.services.burst_location import (
|
from app.services.burst_location import (
|
||||||
@@ -32,9 +34,8 @@ class BurstLocationRequest(BaseModel):
|
|||||||
scada_normal_start: datetime | None = Field(None, description="监测数据正常工况开始时间")
|
scada_normal_start: datetime | None = Field(None, description="监测数据正常工况开始时间")
|
||||||
scada_normal_end: datetime | None = Field(None, description="监测数据正常工况结束时间")
|
scada_normal_end: datetime | None = Field(None, description="监测数据正常工况结束时间")
|
||||||
use_scada_flow: bool = Field(False, description="是否使用SCADA流量数据")
|
use_scada_flow: bool = Field(False, description="是否使用SCADA流量数据")
|
||||||
scheme_name: str | None = Field(None, description="方案名称")
|
scheme_name: str | None = Field(None, description="爆管定位运行名称")
|
||||||
simulation_scheme_name: str | None = Field(None, description="模拟方案名称")
|
simulation_run_id: UUID | None = Field(None, description="分析模拟运行 ID")
|
||||||
simulation_scheme_type: str | None = Field(None, description="模拟方案类型")
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
@@ -63,6 +64,10 @@ async def locate_burst(
|
|||||||
HTTPException: 当数据类型或值不正确时
|
HTTPException: 当数据类型或值不正确时
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return run_burst_location_by_network(**data.model_dump(), username=username)
|
return await run_in_threadpool(
|
||||||
|
run_burst_location_by_network,
|
||||||
|
**data.model_dump(),
|
||||||
|
username=username,
|
||||||
|
)
|
||||||
except (TypeError, ValueError) as exc:
|
except (TypeError, ValueError) as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc))
|
raise HTTPException(status_code=400, detail=str(exc))
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
from fastapi import APIRouter, Query
|
|
||||||
from app.infra.cache.redis_client import redis_client
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
@router.delete("/redis-keys/detail", summary="清除单个缓存键", description="根据键名清除单个Redis缓存")
|
|
||||||
async def fastapi_clear_redis_key(key: str = Query(..., description="缓存键名")):
|
|
||||||
"""
|
|
||||||
清除单个缓存键
|
|
||||||
|
|
||||||
根据指定的键名删除Redis中对应的缓存
|
|
||||||
"""
|
|
||||||
redis_client.delete(key)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/redis-keys", summary="清除匹配的缓存键", description="根据模式清除匹配的Redis缓存键")
|
|
||||||
async def fastapi_clear_redis_keys(keys: str = Query(..., description="缓存键模式(支持通配符)")):
|
|
||||||
"""
|
|
||||||
清除匹配的缓存键
|
|
||||||
|
|
||||||
根据指定的模式删除Redis中所有匹配的缓存键
|
|
||||||
"""
|
|
||||||
# delete keys contains the key
|
|
||||||
matched_keys = redis_client.keys(f"*{keys}*")
|
|
||||||
if matched_keys:
|
|
||||||
redis_client.delete(*matched_keys)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/all-redis", summary="清除所有缓存", description="清空整个Redis数据库的所有缓存")
|
|
||||||
async def fastapi_clear_all_redis():
|
|
||||||
"""
|
|
||||||
清除所有缓存
|
|
||||||
|
|
||||||
清空Redis数据库中的所有缓存键值对
|
|
||||||
"""
|
|
||||||
redis_client.flushdb()
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/redis", summary="查询缓存键列表", description="获取Redis中所有的缓存键")
|
|
||||||
async def fastapi_query_redis():
|
|
||||||
"""
|
|
||||||
查询缓存键列表
|
|
||||||
|
|
||||||
获取Redis数据库中所有的缓存键列表
|
|
||||||
"""
|
|
||||||
# Helper to decode bytes to str for JSON response if needed,
|
|
||||||
# but original just returned keys (which might be bytes in redis-py unless decode_responses=True)
|
|
||||||
# create_redis_client usually sets decode_responses=False by default.
|
|
||||||
# We will assume user handles bytes or we should decode.
|
|
||||||
# Original just returned redis_client.keys("*")
|
|
||||||
keys = redis_client.keys("*")
|
|
||||||
# Clean output for API
|
|
||||||
return [k.decode('utf-8') if isinstance(k, bytes) else k for k in keys]
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, Request, Query, Path, Body
|
from typing import Any
|
||||||
from typing import Any, List, Dict, Union
|
|
||||||
|
from fastapi import APIRouter, Body, Query
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
Any,
|
|
||||||
ChangeSet,
|
ChangeSet,
|
||||||
get_control,
|
get_control,
|
||||||
get_control_schema,
|
get_control_schema,
|
||||||
@@ -14,7 +14,7 @@ from app.services.tjnetwork import (
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/network-schemas/control", summary="获取控制架构", description="获取网络中控制对象的架构定义")
|
@router.get("/network-schemas/control", summary="获取控制架构", description="获取网络中控制对象的架构定义")
|
||||||
async def fastapi_get_control_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fastapi_get_control_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取控制架构。
|
"""获取控制架构。
|
||||||
|
|
||||||
返回指定网络中控制对象的属性架构定义。
|
返回指定网络中控制对象的属性架构定义。
|
||||||
@@ -22,7 +22,7 @@ async def fastapi_get_control_schema(network: str = Query(..., description="管
|
|||||||
return get_control_schema(network)
|
return get_control_schema(network)
|
||||||
|
|
||||||
@router.get("/controls/properties", summary="获取控制属性", description="获取指定网络中的控制属性信息")
|
@router.get("/controls/properties", summary="获取控制属性", description="获取指定网络中的控制属性信息")
|
||||||
async def fastapi_get_control_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
def fastapi_get_control_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||||
"""获取控制属性。
|
"""获取控制属性。
|
||||||
|
|
||||||
返回指定网络中的控制对象属性信息。
|
返回指定网络中的控制对象属性信息。
|
||||||
@@ -30,19 +30,19 @@ async def fastapi_get_control_properties(network: str = Query(..., description="
|
|||||||
return get_control(network)
|
return get_control(network)
|
||||||
|
|
||||||
@router.patch("/controls/properties", response_model=None, summary="设置控制属性", description="更新指定网络中的控制属性")
|
@router.patch("/controls/properties", response_model=None, summary="设置控制属性", description="更新指定网络中的控制属性")
|
||||||
async def fastapi_set_control_properties(
|
def fastapi_set_control_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置控制属性。
|
"""设置控制属性。
|
||||||
|
|
||||||
更新指定网络中的控制属性值。
|
更新指定网络中的控制属性值。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return set_control(network, ChangeSet(props))
|
return set_control(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/rule-schemas", summary="获取规则架构", description="获取网络中规则对象的架构定义")
|
@router.get("/rule-schemas", summary="获取规则架构", description="获取网络中规则对象的架构定义")
|
||||||
async def fastapi_get_rule_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fastapi_get_rule_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取规则架构。
|
"""获取规则架构。
|
||||||
|
|
||||||
返回指定网络中规则对象的属性架构定义。
|
返回指定网络中规则对象的属性架构定义。
|
||||||
@@ -50,7 +50,7 @@ async def fastapi_get_rule_schema(network: str = Query(..., description="管网
|
|||||||
return get_rule_schema(network)
|
return get_rule_schema(network)
|
||||||
|
|
||||||
@router.get("/rule-properties", summary="获取规则属性", description="获取指定网络中的规则属性信息")
|
@router.get("/rule-properties", summary="获取规则属性", description="获取指定网络中的规则属性信息")
|
||||||
async def fastapi_get_rule_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
def fastapi_get_rule_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||||
"""获取规则属性。
|
"""获取规则属性。
|
||||||
|
|
||||||
返回指定网络中的规则对象属性信息。
|
返回指定网络中的规则对象属性信息。
|
||||||
@@ -58,13 +58,13 @@ async def fastapi_get_rule_properties(network: str = Query(..., description="管
|
|||||||
return get_rule(network)
|
return get_rule(network)
|
||||||
|
|
||||||
@router.patch("/rule-properties", response_model=None, summary="设置规则属性", description="更新指定网络中的规则属性")
|
@router.patch("/rule-properties", response_model=None, summary="设置规则属性", description="更新指定网络中的规则属性")
|
||||||
async def fastapi_set_rule_properties(
|
def fastapi_set_rule_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置规则属性。
|
"""设置规则属性。
|
||||||
|
|
||||||
更新指定网络中的规则属性值。
|
更新指定网络中的规则属性值。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return set_rule(network, ChangeSet(props))
|
return set_rule(network, ChangeSet(props))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, Request, Query, Path, Body
|
from typing import Any
|
||||||
from typing import Any, List, Dict, Union
|
|
||||||
|
from fastapi import APIRouter, Body, Query
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
Any,
|
|
||||||
ChangeSet,
|
ChangeSet,
|
||||||
add_curve,
|
add_curve,
|
||||||
delete_curve,
|
delete_curve,
|
||||||
@@ -15,7 +15,7 @@ from app.services.tjnetwork import (
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/network-schemas/curve", summary="获取曲线架构", description="获取网络中曲线对象的架构定义")
|
@router.get("/network-schemas/curve", summary="获取曲线架构", description="获取网络中曲线对象的架构定义")
|
||||||
async def fastapi_get_curve_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fastapi_get_curve_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取曲线架构。
|
"""获取曲线架构。
|
||||||
|
|
||||||
返回指定网络中曲线对象的属性架构定义。
|
返回指定网络中曲线对象的属性架构定义。
|
||||||
@@ -23,23 +23,23 @@ async def fastapi_get_curve_schema(network: str = Query(..., description="管网
|
|||||||
return get_curve_schema(network)
|
return get_curve_schema(network)
|
||||||
|
|
||||||
@router.post("/curves", response_model=None, summary="添加曲线", description="在网络中添加一条新的曲线")
|
@router.post("/curves", response_model=None, summary="添加曲线", description="在网络中添加一条新的曲线")
|
||||||
async def fastapi_add_curve(
|
def fastapi_add_curve(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
curve: str = Query(..., description="曲线ID"),
|
curve: str = Query(..., description="曲线ID"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""添加曲线。
|
"""添加曲线。
|
||||||
|
|
||||||
在指定网络中创建一条新的曲线,并设置其初始属性。
|
在指定网络中创建一条新的曲线,并设置其初始属性。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
ps = {
|
ps = {
|
||||||
"id": curve,
|
"id": curve,
|
||||||
} | props
|
} | props
|
||||||
return add_curve(network, ChangeSet(ps))
|
return add_curve(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.delete("/curves", response_model=None, summary="删除曲线", description="从网络中删除指定的曲线")
|
@router.delete("/curves", response_model=None, summary="删除曲线", description="从网络中删除指定的曲线")
|
||||||
async def fastapi_delete_curve(
|
def fastapi_delete_curve(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
curve: str = Query(..., description="曲线ID")
|
curve: str = Query(..., description="曲线ID")
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
@@ -51,7 +51,7 @@ async def fastapi_delete_curve(
|
|||||||
return delete_curve(network, ChangeSet(ps))
|
return delete_curve(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/curves/properties", summary="获取曲线属性", description="获取指定曲线的属性信息")
|
@router.get("/curves/properties", summary="获取曲线属性", description="获取指定曲线的属性信息")
|
||||||
async def fastapi_get_curve_properties(
|
def fastapi_get_curve_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
curve: str = Query(..., description="曲线ID")
|
curve: str = Query(..., description="曲线ID")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -62,21 +62,21 @@ async def fastapi_get_curve_properties(
|
|||||||
return get_curve(network, curve)
|
return get_curve(network, curve)
|
||||||
|
|
||||||
@router.patch("/curves/properties", response_model=None, summary="设置曲线属性", description="更新指定曲线的属性")
|
@router.patch("/curves/properties", response_model=None, summary="设置曲线属性", description="更新指定曲线的属性")
|
||||||
async def fastapi_set_curve_properties(
|
def fastapi_set_curve_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
curve: str = Query(..., description="曲线ID"),
|
curve: str = Query(..., description="曲线ID"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置曲线属性。
|
"""设置曲线属性。
|
||||||
|
|
||||||
更新指定曲线的属性值。
|
更新指定曲线的属性值。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
ps = {"id": curve} | props
|
ps = {"id": curve} | props
|
||||||
return set_curve(network, ChangeSet(ps))
|
return set_curve(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/curves", summary="获取所有曲线", description="获取网络中的所有曲线列表")
|
@router.get("/curves", summary="获取所有曲线", description="获取网络中的所有曲线列表")
|
||||||
async def fastapi_get_curves(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
|
def fastapi_get_curves(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
|
||||||
"""获取所有曲线。
|
"""获取所有曲线。
|
||||||
|
|
||||||
返回指定网络中的所有曲线ID列表。
|
返回指定网络中的所有曲线ID列表。
|
||||||
@@ -84,7 +84,7 @@ async def fastapi_get_curves(network: str = Query(..., description="管网名称
|
|||||||
return get_curves(network)
|
return get_curves(network)
|
||||||
|
|
||||||
@router.get("/curves/existence", summary="检查曲线存在性", description="检查指定的曲线是否存在")
|
@router.get("/curves/existence", summary="检查曲线存在性", description="检查指定的曲线是否存在")
|
||||||
async def fastapi_is_curve(
|
def fastapi_is_curve(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
curve: str = Query(..., description="曲线ID")
|
curve: str = Query(..., description="曲线ID")
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, Request, Query, Path, Body
|
from typing import Any
|
||||||
from typing import Any, List, Dict, Union
|
|
||||||
|
from fastapi import APIRouter, Body, Query
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
Any,
|
|
||||||
ChangeSet,
|
ChangeSet,
|
||||||
get_energy,
|
get_energy,
|
||||||
get_energy_schema,
|
get_energy_schema,
|
||||||
@@ -20,7 +20,7 @@ from app.services.tjnetwork import (
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/network-schemas/time", summary="获取时间选项架构", description="获取网络中时间选项的架构定义")
|
@router.get("/network-schemas/time", summary="获取时间选项架构", description="获取网络中时间选项的架构定义")
|
||||||
async def fastapi_get_time_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fastapi_get_time_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取时间选项架构。
|
"""获取时间选项架构。
|
||||||
|
|
||||||
返回指定网络中时间相关选项的属性架构定义。
|
返回指定网络中时间相关选项的属性架构定义。
|
||||||
@@ -28,7 +28,7 @@ async def fastapi_get_time_schema(network: str = Query(..., description="管网
|
|||||||
return get_time_schema(network)
|
return get_time_schema(network)
|
||||||
|
|
||||||
@router.get("/network-options/time", summary="获取时间选项属性", description="获取指定网络中的时间选项属性信息")
|
@router.get("/network-options/time", summary="获取时间选项属性", description="获取指定网络中的时间选项属性信息")
|
||||||
async def fastapi_get_time_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
def fastapi_get_time_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||||
"""获取时间选项属性。
|
"""获取时间选项属性。
|
||||||
|
|
||||||
返回指定网络中的时间相关选项属性。
|
返回指定网络中的时间相关选项属性。
|
||||||
@@ -36,19 +36,19 @@ async def fastapi_get_time_properties(network: str = Query(..., description="管
|
|||||||
return get_time(network)
|
return get_time(network)
|
||||||
|
|
||||||
@router.patch("/time-properties", response_model=None, summary="设置时间选项属性", description="更新指定网络中的时间选项属性")
|
@router.patch("/time-properties", response_model=None, summary="设置时间选项属性", description="更新指定网络中的时间选项属性")
|
||||||
async def fastapi_set_time_properties(
|
def fastapi_set_time_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置时间选项属性。
|
"""设置时间选项属性。
|
||||||
|
|
||||||
更新指定网络中的时间相关选项属性值。
|
更新指定网络中的时间相关选项属性值。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return set_time(network, ChangeSet(props))
|
return set_time(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/network-schemas/energy", summary="获取能耗选项架构", description="获取网络中能耗选项的架构定义")
|
@router.get("/network-schemas/energy", summary="获取能耗选项架构", description="获取网络中能耗选项的架构定义")
|
||||||
async def fastapi_get_energy_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fastapi_get_energy_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取能耗选项架构。
|
"""获取能耗选项架构。
|
||||||
|
|
||||||
返回指定网络中能耗相关选项的属性架构定义。
|
返回指定网络中能耗相关选项的属性架构定义。
|
||||||
@@ -56,7 +56,7 @@ async def fastapi_get_energy_schema(network: str = Query(..., description="管
|
|||||||
return get_energy_schema(network)
|
return get_energy_schema(network)
|
||||||
|
|
||||||
@router.get("/network-options/energy", summary="获取能耗选项属性", description="获取指定网络中的能耗选项属性信息")
|
@router.get("/network-options/energy", summary="获取能耗选项属性", description="获取指定网络中的能耗选项属性信息")
|
||||||
async def fastapi_get_energy_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
def fastapi_get_energy_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||||
"""获取能耗选项属性。
|
"""获取能耗选项属性。
|
||||||
|
|
||||||
返回指定网络中的能耗相关选项属性。
|
返回指定网络中的能耗相关选项属性。
|
||||||
@@ -64,19 +64,19 @@ async def fastapi_get_energy_properties(network: str = Query(..., description="
|
|||||||
return get_energy(network)
|
return get_energy(network)
|
||||||
|
|
||||||
@router.patch("/energy-properties", response_model=None, summary="设置能耗选项属性", description="更新指定网络中的能耗选项属性")
|
@router.patch("/energy-properties", response_model=None, summary="设置能耗选项属性", description="更新指定网络中的能耗选项属性")
|
||||||
async def fastapi_set_energy_properties(
|
def fastapi_set_energy_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置能耗选项属性。
|
"""设置能耗选项属性。
|
||||||
|
|
||||||
更新指定网络中的能耗相关选项属性值。
|
更新指定网络中的能耗相关选项属性值。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return set_energy(network, ChangeSet(props))
|
return set_energy(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/network-schemas/pump-energy", summary="获取泵能耗选项架构", description="获取网络中泵能耗选项的架构定义")
|
@router.get("/network-schemas/pump-energy", summary="获取泵能耗选项架构", description="获取网络中泵能耗选项的架构定义")
|
||||||
async def fastapi_get_pump_energy_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fastapi_get_pump_energy_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取泵能耗选项架构。
|
"""获取泵能耗选项架构。
|
||||||
|
|
||||||
返回指定网络中泵能耗相关选项的属性架构定义。
|
返回指定网络中泵能耗相关选项的属性架构定义。
|
||||||
@@ -84,7 +84,7 @@ async def fastapi_get_pump_energy_schema(network: str = Query(..., description="
|
|||||||
return get_pump_energy_schema(network)
|
return get_pump_energy_schema(network)
|
||||||
|
|
||||||
@router.get("/network-options/pump-energy", summary="获取泵能耗属性", description="获取指定泵的能耗属性信息")
|
@router.get("/network-options/pump-energy", summary="获取泵能耗属性", description="获取指定泵的能耗属性信息")
|
||||||
async def fastapi_get_pump_energy_proeprties(
|
def fastapi_get_pump_energy_proeprties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="泵ID")
|
pump: str = Query(..., description="泵ID")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -95,21 +95,21 @@ async def fastapi_get_pump_energy_proeprties(
|
|||||||
return get_pump_energy(network, pump)
|
return get_pump_energy(network, pump)
|
||||||
|
|
||||||
@router.patch("/network-options/pump-energy", response_model=None, summary="设置泵能耗属性", description="更新指定泵的能耗属性")
|
@router.patch("/network-options/pump-energy", response_model=None, summary="设置泵能耗属性", description="更新指定泵的能耗属性")
|
||||||
async def fastapi_set_pump_energy_properties(
|
def fastapi_set_pump_energy_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="泵ID"),
|
pump: str = Query(..., description="泵ID"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置泵能耗属性。
|
"""设置泵能耗属性。
|
||||||
|
|
||||||
更新指定泵的能耗相关属性值。
|
更新指定泵的能耗相关属性值。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
ps = {"id": pump} | props
|
ps = {"id": pump} | props
|
||||||
return set_pump_energy(network, ChangeSet(ps))
|
return set_pump_energy(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/network-schemas/option", summary="获取选项架构", description="获取网络中选项对象的架构定义")
|
@router.get("/network-schemas/option", summary="获取选项架构", description="获取网络中选项对象的架构定义")
|
||||||
async def fastapi_get_option_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fastapi_get_option_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取选项架构。
|
"""获取选项架构。
|
||||||
|
|
||||||
返回指定网络中选项对象的属性架构定义。
|
返回指定网络中选项对象的属性架构定义。
|
||||||
@@ -117,7 +117,7 @@ async def fastapi_get_option_schema(network: str = Query(..., description="管
|
|||||||
return get_option_v3_schema(network)
|
return get_option_v3_schema(network)
|
||||||
|
|
||||||
@router.get("/network-options", summary="获取选项属性", description="获取指定网络中的选项属性信息")
|
@router.get("/network-options", summary="获取选项属性", description="获取指定网络中的选项属性信息")
|
||||||
async def fastapi_get_option_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
def fastapi_get_option_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||||
"""获取选项属性。
|
"""获取选项属性。
|
||||||
|
|
||||||
返回指定网络中的选项对象属性信息。
|
返回指定网络中的选项对象属性信息。
|
||||||
@@ -125,13 +125,13 @@ async def fastapi_get_option_properties(network: str = Query(..., description="
|
|||||||
return get_option_v3(network)
|
return get_option_v3(network)
|
||||||
|
|
||||||
@router.patch("/network-options", response_model=None, summary="设置选项属性", description="更新指定网络中的选项属性")
|
@router.patch("/network-options", response_model=None, summary="设置选项属性", description="更新指定网络中的选项属性")
|
||||||
async def fastapi_set_option_properties(
|
def fastapi_set_option_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置选项属性。
|
"""设置选项属性。
|
||||||
|
|
||||||
更新指定网络中的选项属性值。
|
更新指定网络中的选项属性值。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return set_option_v3(network, ChangeSet(props))
|
return set_option_v3(network, ChangeSet(props))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, Request, Query, Path, Body
|
from typing import Any
|
||||||
from typing import Any, List, Dict, Union
|
|
||||||
|
from fastapi import APIRouter, Body, Query
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
Any,
|
|
||||||
ChangeSet,
|
ChangeSet,
|
||||||
add_pattern,
|
add_pattern,
|
||||||
delete_pattern,
|
delete_pattern,
|
||||||
@@ -15,7 +15,7 @@ from app.services.tjnetwork import (
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/network-schemas/pattern", summary="获取模式架构", description="获取网络中模式对象的架构定义")
|
@router.get("/network-schemas/pattern", summary="获取模式架构", description="获取网络中模式对象的架构定义")
|
||||||
async def fastapi_get_pattern_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fastapi_get_pattern_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取模式架构。
|
"""获取模式架构。
|
||||||
|
|
||||||
返回指定网络中模式对象的属性架构定义。
|
返回指定网络中模式对象的属性架构定义。
|
||||||
@@ -23,23 +23,23 @@ async def fastapi_get_pattern_schema(network: str = Query(..., description="管
|
|||||||
return get_pattern_schema(network)
|
return get_pattern_schema(network)
|
||||||
|
|
||||||
@router.post("/patterns", response_model=None, summary="添加模式", description="在网络中添加一个新的模式")
|
@router.post("/patterns", response_model=None, summary="添加模式", description="在网络中添加一个新的模式")
|
||||||
async def fastapi_add_pattern(
|
def fastapi_add_pattern(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pattern: str = Query(..., description="模式ID"),
|
pattern: str = Query(..., description="模式ID"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""添加模式。
|
"""添加模式。
|
||||||
|
|
||||||
在指定网络中创建一个新的模式,并设置其初始属性。
|
在指定网络中创建一个新的模式,并设置其初始属性。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
ps = {
|
ps = {
|
||||||
"id": pattern,
|
"id": pattern,
|
||||||
} | props
|
} | props
|
||||||
return add_pattern(network, ChangeSet(ps))
|
return add_pattern(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.delete("/patterns", response_model=None, summary="删除模式", description="从网络中删除指定的模式")
|
@router.delete("/patterns", response_model=None, summary="删除模式", description="从网络中删除指定的模式")
|
||||||
async def fastapi_delete_pattern(
|
def fastapi_delete_pattern(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pattern: str = Query(..., description="模式ID")
|
pattern: str = Query(..., description="模式ID")
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
@@ -51,7 +51,7 @@ async def fastapi_delete_pattern(
|
|||||||
return delete_pattern(network, ChangeSet(ps))
|
return delete_pattern(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/patterns/properties", summary="获取模式属性", description="获取指定模式的属性信息")
|
@router.get("/patterns/properties", summary="获取模式属性", description="获取指定模式的属性信息")
|
||||||
async def fastapi_get_pattern_properties(
|
def fastapi_get_pattern_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pattern: str = Query(..., description="模式ID")
|
pattern: str = Query(..., description="模式ID")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -62,21 +62,21 @@ async def fastapi_get_pattern_properties(
|
|||||||
return get_pattern(network, pattern)
|
return get_pattern(network, pattern)
|
||||||
|
|
||||||
@router.patch("/patterns/properties", response_model=None, summary="设置模式属性", description="更新指定模式的属性")
|
@router.patch("/patterns/properties", response_model=None, summary="设置模式属性", description="更新指定模式的属性")
|
||||||
async def fastapi_set_pattern_properties(
|
def fastapi_set_pattern_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pattern: str = Query(..., description="模式ID"),
|
pattern: str = Query(..., description="模式ID"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置模式属性。
|
"""设置模式属性。
|
||||||
|
|
||||||
更新指定模式的属性值。
|
更新指定模式的属性值。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
ps = {"id": pattern} | props
|
ps = {"id": pattern} | props
|
||||||
return set_pattern(network, ChangeSet(ps))
|
return set_pattern(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/patterns/existence", summary="检查模式存在性", description="检查指定的模式是否存在")
|
@router.get("/patterns/existence", summary="检查模式存在性", description="检查指定的模式是否存在")
|
||||||
async def fastapi_is_pattern(
|
def fastapi_is_pattern(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pattern: str = Query(..., description="模式ID")
|
pattern: str = Query(..., description="模式ID")
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -87,7 +87,7 @@ async def fastapi_is_pattern(
|
|||||||
return is_pattern(network, pattern)
|
return is_pattern(network, pattern)
|
||||||
|
|
||||||
@router.get("/patterns", summary="获取所有模式", description="获取网络中的所有模式列表")
|
@router.get("/patterns", summary="获取所有模式", description="获取网络中的所有模式列表")
|
||||||
async def fastapi_get_patterns(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
|
def fastapi_get_patterns(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
|
||||||
"""获取所有模式。
|
"""获取所有模式。
|
||||||
|
|
||||||
返回指定网络中的所有模式ID列表。
|
返回指定网络中的所有模式ID列表。
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
from fastapi import APIRouter, Request, Query, Path, Body
|
from typing import Any
|
||||||
from typing import Any, List, Dict, Union
|
|
||||||
|
from fastapi import APIRouter, Body, Query
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
Any,
|
|
||||||
ChangeSet,
|
ChangeSet,
|
||||||
add_mixing,
|
add_mixing,
|
||||||
add_source,
|
add_source,
|
||||||
api,
|
|
||||||
delete_mixing,
|
delete_mixing,
|
||||||
delete_source,
|
delete_source,
|
||||||
get_emitter,
|
get_emitter,
|
||||||
@@ -23,6 +22,7 @@ from app.services.tjnetwork import (
|
|||||||
get_tank_reaction,
|
get_tank_reaction,
|
||||||
get_tank_reaction_schema,
|
get_tank_reaction_schema,
|
||||||
set_emitter,
|
set_emitter,
|
||||||
|
set_mixing,
|
||||||
set_pipe_reaction,
|
set_pipe_reaction,
|
||||||
set_quality,
|
set_quality,
|
||||||
set_reaction,
|
set_reaction,
|
||||||
@@ -33,7 +33,7 @@ from app.services.tjnetwork import (
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/network-schemas/quality", summary="获取水质架构", description="获取网络中水质对象的架构定义")
|
@router.get("/network-schemas/quality", summary="获取水质架构", description="获取网络中水质对象的架构定义")
|
||||||
async def fastapi_get_quality_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fastapi_get_quality_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取水质架构。
|
"""获取水质架构。
|
||||||
|
|
||||||
返回指定网络中水质对象的属性架构定义。
|
返回指定网络中水质对象的属性架构定义。
|
||||||
@@ -41,7 +41,7 @@ async def fastapi_get_quality_schema(network: str = Query(..., description="管
|
|||||||
return get_quality_schema(network)
|
return get_quality_schema(network)
|
||||||
|
|
||||||
@router.get("/quality-configurations/properties", summary="获取水质属性", description="获取指定节点的水质属性信息")
|
@router.get("/quality-configurations/properties", summary="获取水质属性", description="获取指定节点的水质属性信息")
|
||||||
async def fastapi_get_quality_properties(
|
def fastapi_get_quality_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
node: str = Query(..., description="节点ID")
|
node: str = Query(..., description="节点ID")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -52,19 +52,19 @@ async def fastapi_get_quality_properties(
|
|||||||
return get_quality(network, node)
|
return get_quality(network, node)
|
||||||
|
|
||||||
@router.patch("/quality-configurations/properties", response_model=None, summary="设置水质属性", description="更新指定节点的水质属性")
|
@router.patch("/quality-configurations/properties", response_model=None, summary="设置水质属性", description="更新指定节点的水质属性")
|
||||||
async def fastapi_set_quality_properties(
|
def fastapi_set_quality_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置水质属性。
|
"""设置水质属性。
|
||||||
|
|
||||||
更新指定节点的水质属性值。
|
更新指定节点的水质属性值。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return set_quality(network, ChangeSet(props))
|
return set_quality(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/network-schemas/emitter", summary="获取发射器架构", description="获取网络中发射器对象的架构定义")
|
@router.get("/network-schemas/emitter", summary="获取发射器架构", description="获取网络中发射器对象的架构定义")
|
||||||
async def fastapi_get_emitter_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fastapi_get_emitter_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取发射器架构。
|
"""获取发射器架构。
|
||||||
|
|
||||||
返回指定网络中发射器对象的属性架构定义。
|
返回指定网络中发射器对象的属性架构定义。
|
||||||
@@ -72,7 +72,7 @@ async def fastapi_get_emitter_schema(network: str = Query(..., description="管
|
|||||||
return get_emitter_schema(network)
|
return get_emitter_schema(network)
|
||||||
|
|
||||||
@router.get("/emitters/properties", summary="获取发射器属性", description="获取指定连接点的发射器属性信息")
|
@router.get("/emitters/properties", summary="获取发射器属性", description="获取指定连接点的发射器属性信息")
|
||||||
async def fastapi_get_emitter_properties(
|
def fastapi_get_emitter_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="连接点ID")
|
junction: str = Query(..., description="连接点ID")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -83,21 +83,21 @@ async def fastapi_get_emitter_properties(
|
|||||||
return get_emitter(network, junction)
|
return get_emitter(network, junction)
|
||||||
|
|
||||||
@router.patch("/emitters/properties", response_model=None, summary="设置发射器属性", description="更新指定连接点的发射器属性")
|
@router.patch("/emitters/properties", response_model=None, summary="设置发射器属性", description="更新指定连接点的发射器属性")
|
||||||
async def fastapi_set_emitter_properties(
|
def fastapi_set_emitter_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="连接点ID"),
|
junction: str = Query(..., description="连接点ID"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置发射器属性。
|
"""设置发射器属性。
|
||||||
|
|
||||||
更新指定连接点的发射器属性值。
|
更新指定连接点的发射器属性值。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
ps = {"junction": junction} | props
|
ps = {"junction": junction} | props
|
||||||
return set_emitter(network, ChangeSet(ps))
|
return set_emitter(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/network-schemas/source", summary="获取水源架构", description="获取网络中水源对象的架构定义")
|
@router.get("/network-schemas/source", summary="获取水源架构", description="获取网络中水源对象的架构定义")
|
||||||
async def fastapi_get_source_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fastapi_get_source_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取水源架构。
|
"""获取水源架构。
|
||||||
|
|
||||||
返回指定网络中水源对象的属性架构定义。
|
返回指定网络中水源对象的属性架构定义。
|
||||||
@@ -105,7 +105,7 @@ async def fastapi_get_source_schema(network: str = Query(..., description="管
|
|||||||
return get_source_schema(network)
|
return get_source_schema(network)
|
||||||
|
|
||||||
@router.get("/sources/detail", summary="获取水源属性", description="获取指定节点的水源属性信息")
|
@router.get("/sources/detail", summary="获取水源属性", description="获取指定节点的水源属性信息")
|
||||||
async def fastapi_get_source(
|
def fastapi_get_source(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
node: str = Query(..., description="节点ID")
|
node: str = Query(..., description="节点ID")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -116,31 +116,31 @@ async def fastapi_get_source(
|
|||||||
return get_source(network, node)
|
return get_source(network, node)
|
||||||
|
|
||||||
@router.patch("/sources", response_model=None, summary="设置水源属性", description="更新指定节点的水源属性")
|
@router.patch("/sources", response_model=None, summary="设置水源属性", description="更新指定节点的水源属性")
|
||||||
async def fastapi_set_source(
|
def fastapi_set_source(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置水源属性。
|
"""设置水源属性。
|
||||||
|
|
||||||
更新指定节点的水源属性值。
|
更新指定节点的水源属性值。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return set_source(network, ChangeSet(props))
|
return set_source(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/sources", response_model=None, summary="添加水源", description="在网络中添加一个新的水源")
|
@router.post("/sources", response_model=None, summary="添加水源", description="在网络中添加一个新的水源")
|
||||||
async def fastapi_add_source(
|
def fastapi_add_source(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""添加水源。
|
"""添加水源。
|
||||||
|
|
||||||
在指定网络中创建一个新的水源,并设置其初始属性。
|
在指定网络中创建一个新的水源,并设置其初始属性。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return add_source(network, ChangeSet(props))
|
return add_source(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.delete("/sources", response_model=None, summary="删除水源", description="从网络中删除指定节点的水源")
|
@router.delete("/sources", response_model=None, summary="删除水源", description="从网络中删除指定节点的水源")
|
||||||
async def fastapi_delete_source(
|
def fastapi_delete_source(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
node: str = Query(..., description="节点ID")
|
node: str = Query(..., description="节点ID")
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
@@ -152,7 +152,7 @@ async def fastapi_delete_source(
|
|||||||
return delete_source(network, ChangeSet(props))
|
return delete_source(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/network-schemas/reaction", summary="获取反应架构", description="获取网络中反应对象的架构定义")
|
@router.get("/network-schemas/reaction", summary="获取反应架构", description="获取网络中反应对象的架构定义")
|
||||||
async def fastapi_get_reaction_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fastapi_get_reaction_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取反应架构。
|
"""获取反应架构。
|
||||||
|
|
||||||
返回指定网络中反应对象的属性架构定义。
|
返回指定网络中反应对象的属性架构定义。
|
||||||
@@ -160,7 +160,7 @@ async def fastapi_get_reaction_schema(network: str = Query(..., description="管
|
|||||||
return get_reaction_schema(network)
|
return get_reaction_schema(network)
|
||||||
|
|
||||||
@router.get("/reactions/detail", summary="获取反应属性", description="获取指定网络中的反应属性信息")
|
@router.get("/reactions/detail", summary="获取反应属性", description="获取指定网络中的反应属性信息")
|
||||||
async def fastapi_get_reaction(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
def fastapi_get_reaction(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||||
"""获取反应属性。
|
"""获取反应属性。
|
||||||
|
|
||||||
返回指定网络中的反应属性信息。
|
返回指定网络中的反应属性信息。
|
||||||
@@ -168,19 +168,19 @@ async def fastapi_get_reaction(network: str = Query(..., description="管网名
|
|||||||
return get_reaction(network)
|
return get_reaction(network)
|
||||||
|
|
||||||
@router.patch("/reactions", response_model=None, summary="设置反应属性", description="更新指定网络中的反应属性")
|
@router.patch("/reactions", response_model=None, summary="设置反应属性", description="更新指定网络中的反应属性")
|
||||||
async def fastapi_set_reaction(
|
def fastapi_set_reaction(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置反应属性。
|
"""设置反应属性。
|
||||||
|
|
||||||
更新指定网络中的反应属性值。
|
更新指定网络中的反应属性值。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return set_reaction(network, ChangeSet(props))
|
return set_reaction(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/network-schemas/pipe-reaction", summary="获取管道反应架构", description="获取网络中管道反应对象的架构定义")
|
@router.get("/network-schemas/pipe-reaction", summary="获取管道反应架构", description="获取网络中管道反应对象的架构定义")
|
||||||
async def fastapi_get_pipe_reaction_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fastapi_get_pipe_reaction_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取管道反应架构。
|
"""获取管道反应架构。
|
||||||
|
|
||||||
返回指定网络中管道反应对象的属性架构定义。
|
返回指定网络中管道反应对象的属性架构定义。
|
||||||
@@ -188,7 +188,7 @@ async def fastapi_get_pipe_reaction_schema(network: str = Query(..., description
|
|||||||
return get_pipe_reaction_schema(network)
|
return get_pipe_reaction_schema(network)
|
||||||
|
|
||||||
@router.get("/pipe-reactions/detail", summary="获取管道反应属性", description="获取指定管道的反应属性信息")
|
@router.get("/pipe-reactions/detail", summary="获取管道反应属性", description="获取指定管道的反应属性信息")
|
||||||
async def fastapi_get_pipe_reaction(
|
def fastapi_get_pipe_reaction(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID")
|
pipe: str = Query(..., description="管道ID")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -199,19 +199,19 @@ async def fastapi_get_pipe_reaction(
|
|||||||
return get_pipe_reaction(network, pipe)
|
return get_pipe_reaction(network, pipe)
|
||||||
|
|
||||||
@router.patch("/pipe-reactions", response_model=None, summary="设置管道反应属性", description="更新指定管道的反应属性")
|
@router.patch("/pipe-reactions", response_model=None, summary="设置管道反应属性", description="更新指定管道的反应属性")
|
||||||
async def fastapi_set_pipe_reaction(
|
def fastapi_set_pipe_reaction(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置管道反应属性。
|
"""设置管道反应属性。
|
||||||
|
|
||||||
更新指定管道的反应属性值。
|
更新指定管道的反应属性值。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return set_pipe_reaction(network, ChangeSet(props))
|
return set_pipe_reaction(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/network-schemas/tank-reaction", summary="获取水池反应架构", description="获取网络中水池反应对象的架构定义")
|
@router.get("/network-schemas/tank-reaction", summary="获取水池反应架构", description="获取网络中水池反应对象的架构定义")
|
||||||
async def fastapi_get_tank_reaction_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fastapi_get_tank_reaction_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取水池反应架构。
|
"""获取水池反应架构。
|
||||||
|
|
||||||
返回指定网络中水池反应对象的属性架构定义。
|
返回指定网络中水池反应对象的属性架构定义。
|
||||||
@@ -219,7 +219,7 @@ async def fastapi_get_tank_reaction_schema(network: str = Query(..., description
|
|||||||
return get_tank_reaction_schema(network)
|
return get_tank_reaction_schema(network)
|
||||||
|
|
||||||
@router.get("/tank-reactions/detail", summary="获取水池反应属性", description="获取指定水池的反应属性信息")
|
@router.get("/tank-reactions/detail", summary="获取水池反应属性", description="获取指定水池的反应属性信息")
|
||||||
async def fastapi_get_tank_reaction(
|
def fastapi_get_tank_reaction(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水池ID")
|
tank: str = Query(..., description="水池ID")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -230,19 +230,19 @@ async def fastapi_get_tank_reaction(
|
|||||||
return get_tank_reaction(network, tank)
|
return get_tank_reaction(network, tank)
|
||||||
|
|
||||||
@router.patch("/tank-reactions", response_model=None, summary="设置水池反应属性", description="更新指定水池的反应属性")
|
@router.patch("/tank-reactions", response_model=None, summary="设置水池反应属性", description="更新指定水池的反应属性")
|
||||||
async def fastapi_set_tank_reaction(
|
def fastapi_set_tank_reaction(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置水池反应属性。
|
"""设置水池反应属性。
|
||||||
|
|
||||||
更新指定水池的反应属性值。
|
更新指定水池的反应属性值。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return set_tank_reaction(network, ChangeSet(props))
|
return set_tank_reaction(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/network-schemas/mixing", summary="获取混合架构", description="获取网络中混合对象的架构定义")
|
@router.get("/network-schemas/mixing", summary="获取混合架构", description="获取网络中混合对象的架构定义")
|
||||||
async def fastapi_get_mixing_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fastapi_get_mixing_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取混合架构。
|
"""获取混合架构。
|
||||||
|
|
||||||
返回指定网络中混合对象的属性架构定义。
|
返回指定网络中混合对象的属性架构定义。
|
||||||
@@ -250,7 +250,7 @@ async def fastapi_get_mixing_schema(network: str = Query(..., description="管
|
|||||||
return get_mixing_schema(network)
|
return get_mixing_schema(network)
|
||||||
|
|
||||||
@router.get("/mixing-configurations/detail", summary="获取混合属性", description="获取指定水池的混合属性信息")
|
@router.get("/mixing-configurations/detail", summary="获取混合属性", description="获取指定水池的混合属性信息")
|
||||||
async def fastapi_get_mixing(
|
def fastapi_get_mixing(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水池ID")
|
tank: str = Query(..., description="水池ID")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -261,37 +261,37 @@ async def fastapi_get_mixing(
|
|||||||
return get_mixing(network, tank)
|
return get_mixing(network, tank)
|
||||||
|
|
||||||
@router.patch("/mixing-configurations", response_model=None, summary="设置混合属性", description="更新指定水池的混合属性")
|
@router.patch("/mixing-configurations", response_model=None, summary="设置混合属性", description="更新指定水池的混合属性")
|
||||||
async def fastapi_set_mixing(
|
def fastapi_set_mixing(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置混合属性。
|
"""设置混合属性。
|
||||||
|
|
||||||
更新指定水池的混合属性值。
|
更新指定水池的混合属性值。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return api.set_mixing(network, ChangeSet(props))
|
return set_mixing(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/mixing-configurations", response_model=None, summary="添加混合", description="在网络中添加一个新的混合")
|
@router.post("/mixing-configurations", response_model=None, summary="添加混合", description="在网络中添加一个新的混合")
|
||||||
async def fastapi_add_mixing(
|
def fastapi_add_mixing(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""添加混合。
|
"""添加混合。
|
||||||
|
|
||||||
在指定网络中创建一个新的混合,并设置其初始属性。
|
在指定网络中创建一个新的混合,并设置其初始属性。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return add_mixing(network, ChangeSet(props))
|
return add_mixing(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.delete("/mixing-configurations", response_model=None, summary="删除混合", description="从网络中删除指定的混合")
|
@router.delete("/mixing-configurations", response_model=None, summary="删除混合", description="从网络中删除指定的混合")
|
||||||
async def fastapi_delete_mixing(
|
def fastapi_delete_mixing(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""删除混合。
|
"""删除混合。
|
||||||
|
|
||||||
从指定网络中删除指定的混合及其相关数据。
|
从指定网络中删除指定的混合及其相关数据。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return delete_mixing(network, ChangeSet(props))
|
return delete_mixing(network, ChangeSet(props))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, Request, Query, Path, Body, Response
|
from typing import Any
|
||||||
from typing import Any, List, Dict, Union
|
|
||||||
|
from fastapi import APIRouter, Body, Query
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
Any,
|
|
||||||
ChangeSet,
|
ChangeSet,
|
||||||
add_label,
|
add_label,
|
||||||
add_vertex,
|
add_vertex,
|
||||||
@@ -25,7 +25,7 @@ import json
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/network-schemas/vertex", summary="获取图形元素架构", description="获取网络中图形元素对象的架构定义")
|
@router.get("/network-schemas/vertex", summary="获取图形元素架构", description="获取网络中图形元素对象的架构定义")
|
||||||
async def fastapi_get_vertex_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fastapi_get_vertex_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取图形元素架构。
|
"""获取图形元素架构。
|
||||||
|
|
||||||
返回指定网络中图形元素对象的属性架构定义。
|
返回指定网络中图形元素对象的属性架构定义。
|
||||||
@@ -33,7 +33,7 @@ async def fastapi_get_vertex_schema(network: str = Query(..., description="管
|
|||||||
return get_vertex_schema(network)
|
return get_vertex_schema(network)
|
||||||
|
|
||||||
@router.get("/visual-elements/properties", summary="获取图形元素属性", description="获取指定图形元素的属性信息")
|
@router.get("/visual-elements/properties", summary="获取图形元素属性", description="获取指定图形元素的属性信息")
|
||||||
async def fastapi_get_vertex_properties(
|
def fastapi_get_vertex_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
link: str = Query(..., description="图形元素链接")
|
link: str = Query(..., description="图形元素链接")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -44,43 +44,43 @@ async def fastapi_get_vertex_properties(
|
|||||||
return get_vertex(network, link)
|
return get_vertex(network, link)
|
||||||
|
|
||||||
@router.patch("/visual-elements/properties", response_model=None, summary="设置图形元素属性", description="更新指定图形元素的属性")
|
@router.patch("/visual-elements/properties", response_model=None, summary="设置图形元素属性", description="更新指定图形元素的属性")
|
||||||
async def fastapi_set_vertex_properties(
|
def fastapi_set_vertex_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置图形元素属性。
|
"""设置图形元素属性。
|
||||||
|
|
||||||
更新指定图形元素的属性值。
|
更新指定图形元素的属性值。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return set_vertex(network, ChangeSet(props))
|
return set_vertex(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/visual-elements", response_model=None, summary="添加图形元素", description="在网络中添加一个新的图形元素")
|
@router.post("/visual-elements", response_model=None, summary="添加图形元素", description="在网络中添加一个新的图形元素")
|
||||||
async def fastapi_add_vertex(
|
def fastapi_add_vertex(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""添加图形元素。
|
"""添加图形元素。
|
||||||
|
|
||||||
在指定网络中创建一个新的图形元素,并设置其初始属性。
|
在指定网络中创建一个新的图形元素,并设置其初始属性。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return add_vertex(network, ChangeSet(props))
|
return add_vertex(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.delete("/visual-elements", response_model=None, summary="删除图形元素", description="从网络中删除指定的图形元素")
|
@router.delete("/visual-elements", response_model=None, summary="删除图形元素", description="从网络中删除指定的图形元素")
|
||||||
async def fastapi_delete_vertex(
|
def fastapi_delete_vertex(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""删除图形元素。
|
"""删除图形元素。
|
||||||
|
|
||||||
从指定网络中删除指定的图形元素及其相关数据。
|
从指定网络中删除指定的图形元素及其相关数据。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return delete_vertex(network, ChangeSet(props))
|
return delete_vertex(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/visual-elements/links", response_class=PlainTextResponse, summary="获取所有图形元素链接", description="获取网络中的所有图形元素链接列表")
|
@router.get("/visual-elements/links", response_class=PlainTextResponse, summary="获取所有图形元素链接", description="获取网络中的所有图形元素链接列表")
|
||||||
async def fastapi_get_all_vertex_links(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
|
def fastapi_get_all_vertex_links(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
|
||||||
"""获取所有图形元素链接。
|
"""获取所有图形元素链接。
|
||||||
|
|
||||||
返回指定网络中的所有图形元素链接列表。
|
返回指定网络中的所有图形元素链接列表。
|
||||||
@@ -88,7 +88,7 @@ async def fastapi_get_all_vertex_links(network: str = Query(..., description="
|
|||||||
return json.dumps(get_all_vertex_links(network))
|
return json.dumps(get_all_vertex_links(network))
|
||||||
|
|
||||||
@router.get("/all-vertices", response_class=PlainTextResponse, summary="获取所有图形元素", description="获取网络中的所有图形元素详细信息")
|
@router.get("/all-vertices", response_class=PlainTextResponse, summary="获取所有图形元素", description="获取网络中的所有图形元素详细信息")
|
||||||
async def fastapi_get_all_vertices(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[str, Any]]:
|
def fastapi_get_all_vertices(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[str, Any]]:
|
||||||
"""获取所有图形元素。
|
"""获取所有图形元素。
|
||||||
|
|
||||||
返回指定网络中的所有图形元素详细信息。
|
返回指定网络中的所有图形元素详细信息。
|
||||||
@@ -96,7 +96,7 @@ async def fastapi_get_all_vertices(network: str = Query(..., description="管网
|
|||||||
return json.dumps(get_all_vertices(network))
|
return json.dumps(get_all_vertices(network))
|
||||||
|
|
||||||
@router.get("/network-schemas/label", summary="获取标签架构", description="获取网络中标签对象的架构定义")
|
@router.get("/network-schemas/label", summary="获取标签架构", description="获取网络中标签对象的架构定义")
|
||||||
async def fastapi_get_label_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fastapi_get_label_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取标签架构。
|
"""获取标签架构。
|
||||||
|
|
||||||
返回指定网络中标签对象的属性架构定义。
|
返回指定网络中标签对象的属性架构定义。
|
||||||
@@ -104,7 +104,7 @@ async def fastapi_get_label_schema(network: str = Query(..., description="管网
|
|||||||
return get_label_schema(network)
|
return get_label_schema(network)
|
||||||
|
|
||||||
@router.get("/labels/properties", summary="获取标签属性", description="获取指定坐标处的标签属性信息")
|
@router.get("/labels/properties", summary="获取标签属性", description="获取指定坐标处的标签属性信息")
|
||||||
async def fastapi_get_label_properties(
|
def fastapi_get_label_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
x: float = Query(..., description="X坐标"),
|
x: float = Query(..., description="X坐标"),
|
||||||
y: float = Query(..., description="Y坐标")
|
y: float = Query(..., description="Y坐标")
|
||||||
@@ -116,43 +116,43 @@ async def fastapi_get_label_properties(
|
|||||||
return get_label(network, x, y)
|
return get_label(network, x, y)
|
||||||
|
|
||||||
@router.patch("/labels/properties", response_model=None, summary="设置标签属性", description="更新指定标签的属性")
|
@router.patch("/labels/properties", response_model=None, summary="设置标签属性", description="更新指定标签的属性")
|
||||||
async def fastapi_set_label_properties(
|
def fastapi_set_label_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置标签属性。
|
"""设置标签属性。
|
||||||
|
|
||||||
更新指定标签的属性值。
|
更新指定标签的属性值。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return set_label(network, ChangeSet(props))
|
return set_label(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.post("/labels", response_model=None, summary="添加标签", description="在网络中添加一个新的标签")
|
@router.post("/labels", response_model=None, summary="添加标签", description="在网络中添加一个新的标签")
|
||||||
async def fastapi_add_label(
|
def fastapi_add_label(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""添加标签。
|
"""添加标签。
|
||||||
|
|
||||||
在指定网络中创建一个新的标签,并设置其初始属性。
|
在指定网络中创建一个新的标签,并设置其初始属性。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return add_label(network, ChangeSet(props))
|
return add_label(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.delete("/labels", response_model=None, summary="删除标签", description="从网络中删除指定的标签")
|
@router.delete("/labels", response_model=None, summary="删除标签", description="从网络中删除指定的标签")
|
||||||
async def fastapi_delete_label(
|
def fastapi_delete_label(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""删除标签。
|
"""删除标签。
|
||||||
|
|
||||||
从指定网络中删除指定的标签及其相关数据。
|
从指定网络中删除指定的标签及其相关数据。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return delete_label(network, ChangeSet(props))
|
return delete_label(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get("/network-schemas/backdrop", summary="获取背景架构", description="获取网络中背景对象的架构定义")
|
@router.get("/network-schemas/backdrop", summary="获取背景架构", description="获取网络中背景对象的架构定义")
|
||||||
async def fastapi_get_backdrop_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fastapi_get_backdrop_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""获取背景架构。
|
"""获取背景架构。
|
||||||
|
|
||||||
返回指定网络中背景对象的属性架构定义。
|
返回指定网络中背景对象的属性架构定义。
|
||||||
@@ -160,7 +160,7 @@ async def fastapi_get_backdrop_schema(network: str = Query(..., description="管
|
|||||||
return get_backdrop_schema(network)
|
return get_backdrop_schema(network)
|
||||||
|
|
||||||
@router.get("/backdrops/properties", summary="获取背景属性", description="获取指定网络的背景属性信息")
|
@router.get("/backdrops/properties", summary="获取背景属性", description="获取指定网络的背景属性信息")
|
||||||
async def fastapi_get_backdrop_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
def fastapi_get_backdrop_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||||
"""获取背景属性。
|
"""获取背景属性。
|
||||||
|
|
||||||
返回指定网络的背景属性信息。
|
返回指定网络的背景属性信息。
|
||||||
@@ -168,13 +168,13 @@ async def fastapi_get_backdrop_properties(network: str = Query(..., description=
|
|||||||
return get_backdrop(network)
|
return get_backdrop(network)
|
||||||
|
|
||||||
@router.patch("/backdrops/properties", response_model=None, summary="设置背景属性", description="更新指定网络的背景属性")
|
@router.patch("/backdrops/properties", response_model=None, summary="设置背景属性", description="更新指定网络的背景属性")
|
||||||
async def fastapi_set_backdrop_properties(
|
def fastapi_set_backdrop_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置背景属性。
|
"""设置背景属性。
|
||||||
|
|
||||||
更新指定网络的背景属性值。
|
更新指定网络的背景属性值。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return set_backdrop(network, ChangeSet(props))
|
return set_backdrop(network, ChangeSet(props))
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -3,34 +3,49 @@ from typing import Any
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Body
|
from fastapi import APIRouter, Depends, HTTPException, Body
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
|
||||||
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
||||||
from app.services.leakage_identifier import (
|
from app.services.dma_leakage_estimation import (
|
||||||
run_leakage_identification,
|
run_leakage_identification,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
DEFAULT_N_WORKERS = max(1, min((os.cpu_count() or 1) - 1, 4))
|
DEFAULT_N_WORKERS = max(1, min((os.cpu_count() or 1) - 1, 4))
|
||||||
|
MAX_POPULATION_SIZE = 1_000
|
||||||
|
MAX_GENERATIONS = 1_000
|
||||||
|
MAX_DURATION_HOURS = 168
|
||||||
|
|
||||||
|
|
||||||
class LeakageIdentifyRequest(BaseModel):
|
class LeakageIdentifyRequest(BaseModel):
|
||||||
"""漏损识别请求模型"""
|
"""漏损识别请求模型"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
network: str = Field(..., description="管网名称(或数据库名称)")
|
network: str = Field(..., description="管网名称(或数据库名称)")
|
||||||
observed_pressure_data: str | dict[str, list[Any]] | list[dict[str, Any]] | None = Field(
|
observed_pressure_data: dict[str, list[Any]] | list[dict[str, Any]] | None = (
|
||||||
None, description="观测的压力数据"
|
Field(None, description="观测的压力数据;文件路径不属于公共 API 输入")
|
||||||
)
|
)
|
||||||
start_time: float = Field(0, description="起始时间(小时)")
|
start_time: float = Field(0, ge=0, description="起始时间(小时)")
|
||||||
duration: float = Field(24, description="持续时间(小时)")
|
duration: float = Field(
|
||||||
timestep: float = Field(5, description="时间步长(分钟)")
|
24, gt=0, le=MAX_DURATION_HOURS, description="持续时间(小时)"
|
||||||
q_sum: float = Field(0.2, description="总流量(m3/s)")
|
)
|
||||||
|
timestep: float = Field(5, gt=0, le=1440, description="时间步长(分钟)")
|
||||||
|
q_sum: float = Field(0.2, ge=0, description="总流量(m3/s)")
|
||||||
q_sum_unit: str = Field("m3/s", description="流量单位")
|
q_sum_unit: str = Field("m3/s", description="流量单位")
|
||||||
output_dir: str = Field("db_inp", description="输出目录")
|
pop_size: int = Field(
|
||||||
pop_size: int = Field(50, description="种群大小")
|
50, ge=2, le=MAX_POPULATION_SIZE, description="种群大小"
|
||||||
max_gen: int = Field(100, description="最大代数")
|
)
|
||||||
n_workers: int = Field(DEFAULT_N_WORKERS, description="工作线程数")
|
max_gen: int = Field(100, ge=1, le=MAX_GENERATIONS, description="最大代数")
|
||||||
|
n_workers: int = Field(
|
||||||
|
DEFAULT_N_WORKERS,
|
||||||
|
ge=1,
|
||||||
|
le=DEFAULT_N_WORKERS,
|
||||||
|
description="工作进程数",
|
||||||
|
)
|
||||||
output_flow_unit: str = Field("m3/s", description="输出流量单位")
|
output_flow_unit: str = Field("m3/s", description="输出流量单位")
|
||||||
dma_count: int | None = Field(None, description="DMA区域数量")
|
dma_count: int | None = Field(None, ge=1, description="DMA区域数量")
|
||||||
scada_start: datetime | None = Field(None, description="SCADA数据起始时间")
|
scada_start: datetime | None = Field(None, description="SCADA数据起始时间")
|
||||||
scada_end: datetime | None = Field(None, description="SCADA数据结束时间")
|
scada_end: datetime | None = Field(None, description="SCADA数据结束时间")
|
||||||
sensor_nodes: list[str] | None = Field(None, description="传感器节点列表")
|
sensor_nodes: list[str] | None = Field(None, description="传感器节点列表")
|
||||||
@@ -63,6 +78,10 @@ async def identify_leakage(
|
|||||||
HTTPException: 当处理过程中发生错误时
|
HTTPException: 当处理过程中发生错误时
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return run_leakage_identification(**data.model_dump(), username=username)
|
return await run_in_threadpool(
|
||||||
|
run_leakage_identification,
|
||||||
|
**data.model_dump(),
|
||||||
|
username=username,
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc))
|
raise HTTPException(status_code=400, detail=str(exc))
|
||||||
|
|||||||
@@ -2,14 +2,11 @@ import logging
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, Path
|
from fastapi import APIRouter, Depends, HTTPException, status, Query, Path
|
||||||
import psycopg
|
import psycopg
|
||||||
from psycopg import AsyncConnection
|
from psycopg import AsyncConnection
|
||||||
from sqlalchemy import text
|
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
from app.auth.project_dependencies import (
|
from app.auth.project_dependencies import (
|
||||||
ProjectContext,
|
ProjectContext,
|
||||||
get_project_context,
|
get_project_context,
|
||||||
get_project_pg_session,
|
get_project_pg_connection,
|
||||||
get_project_timescale_connection,
|
get_project_timescale_connection,
|
||||||
get_metadata_repository,
|
get_metadata_repository,
|
||||||
)
|
)
|
||||||
@@ -90,7 +87,7 @@ async def list_user_projects(
|
|||||||
|
|
||||||
@router.get("/projects/current/database-health", summary="检查数据库健康状态", description="检查项目数据库连接的健康状况")
|
@router.get("/projects/current/database-health", summary="检查数据库健康状态", description="检查项目数据库连接的健康状况")
|
||||||
async def project_db_health(
|
async def project_db_health(
|
||||||
pg_session: AsyncSession = Depends(get_project_pg_session),
|
pg_conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||||
ts_conn: AsyncConnection = Depends(get_project_timescale_connection),
|
ts_conn: AsyncConnection = Depends(get_project_timescale_connection),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -99,8 +96,10 @@ async def project_db_health(
|
|||||||
检查PostgreSQL和TimescaleDB数据库的连接状态
|
检查PostgreSQL和TimescaleDB数据库的连接状态
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
await pg_session.execute(text("SELECT 1"))
|
async with pg_conn.cursor() as cur:
|
||||||
except SQLAlchemyError as exc:
|
await cur.execute("SELECT 1")
|
||||||
|
await cur.fetchone()
|
||||||
|
except psycopg.Error as exc:
|
||||||
logger.error("Project PostgreSQL health check failed", exc_info=True)
|
logger.error("Project PostgreSQL health check failed", exc_info=True)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
|||||||
@@ -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,3 +1,4 @@
|
|||||||
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
@@ -6,20 +7,40 @@ from fastapi import (
|
|||||||
APIRouter,
|
APIRouter,
|
||||||
Depends,
|
Depends,
|
||||||
File,
|
File,
|
||||||
|
Form,
|
||||||
HTTPException,
|
HTTPException,
|
||||||
Path as ApiPath,
|
Path as ApiPath,
|
||||||
Request,
|
Request,
|
||||||
UploadFile,
|
UploadFile,
|
||||||
status,
|
status,
|
||||||
)
|
)
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
|
||||||
from app.auth.metadata_dependencies import (
|
from app.auth.metadata_dependencies import (
|
||||||
get_current_metadata_admin,
|
get_current_metadata_admin,
|
||||||
get_metadata_repository,
|
get_metadata_repository,
|
||||||
)
|
)
|
||||||
|
from app.auth.project_dependencies import (
|
||||||
|
ProjectContext,
|
||||||
|
resolve_project_business_routing,
|
||||||
|
)
|
||||||
from app.core.audit import AuditAction, log_audit_event
|
from app.core.audit import AuditAction, log_audit_event
|
||||||
|
from app.core.encryption import is_database_encryption_configured
|
||||||
|
from app.domain.schemas.admin_metadata import (
|
||||||
|
AdminProjectResponse,
|
||||||
|
ProjectProvisionResponse,
|
||||||
|
)
|
||||||
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
||||||
|
from app.infra.db.project_routing import activate_project_routing
|
||||||
|
from app.native.wndb.core.database import MaterializedViewRefreshAfterCommitError
|
||||||
from app.services.network_import import network_update
|
from app.services.network_import import network_update
|
||||||
|
from app.services.project_provisioning import (
|
||||||
|
ProjectProvisioningError,
|
||||||
|
ProvisionedProjectInfrastructure,
|
||||||
|
provision_project_infrastructure,
|
||||||
|
validate_project_code,
|
||||||
|
)
|
||||||
from app.services.tjnetwork import run_inp
|
from app.services.tjnetwork import run_inp
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -82,8 +103,8 @@ def _validate_inp_bytes(content: bytes, filename: str) -> str:
|
|||||||
async def _read_upload(file: UploadFile) -> tuple[bytes, str]:
|
async def _read_upload(file: UploadFile) -> tuple[bytes, str]:
|
||||||
filename = Path(file.filename or "").name
|
filename = Path(file.filename or "").name
|
||||||
content = await file.read(MAX_INP_FILE_BYTES + 1)
|
content = await file.read(MAX_INP_FILE_BYTES + 1)
|
||||||
_validate_inp_bytes(content, filename)
|
normalized = _validate_inp_bytes(content, filename).encode("utf-8")
|
||||||
return content, filename
|
return normalized, filename
|
||||||
|
|
||||||
|
|
||||||
async def _audit_model_change(
|
async def _audit_model_change(
|
||||||
@@ -109,7 +130,7 @@ async def _audit_model_change(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _run_uploaded_inp(content: bytes) -> str:
|
def _run_uploaded_inp_sync(content: bytes) -> str:
|
||||||
target_dir = Path("inp")
|
target_dir = Path("inp")
|
||||||
target_dir.mkdir(parents=True, exist_ok=True)
|
target_dir.mkdir(parents=True, exist_ok=True)
|
||||||
model_name = f"admin_model_{uuid4().hex}"
|
model_name = f"admin_model_{uuid4().hex}"
|
||||||
@@ -118,21 +139,31 @@ async def _run_uploaded_inp(content: bytes) -> str:
|
|||||||
return run_inp(model_name)
|
return run_inp(model_name)
|
||||||
|
|
||||||
|
|
||||||
async def _update_from_inp(content: bytes) -> None:
|
async def _run_uploaded_inp(content: bytes) -> str:
|
||||||
|
return await run_in_threadpool(_run_uploaded_inp_sync, content)
|
||||||
|
|
||||||
|
|
||||||
|
def _update_from_inp_sync(content: bytes, project_code: str) -> None:
|
||||||
temp_path: Path | None = None
|
temp_path: Path | None = None
|
||||||
try:
|
try:
|
||||||
with NamedTemporaryFile(suffix=".inp", delete=False) as temp_file:
|
with NamedTemporaryFile(suffix=".inp", delete=False) as temp_file:
|
||||||
temp_file.write(content)
|
temp_file.write(content)
|
||||||
temp_path = Path(temp_file.name)
|
temp_path = Path(temp_file.name)
|
||||||
network_update(str(temp_path))
|
network_update(str(temp_path), project_code)
|
||||||
finally:
|
finally:
|
||||||
if temp_path is not None:
|
if temp_path is not None:
|
||||||
temp_path.unlink(missing_ok=True)
|
temp_path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
async def _apply_model_update(content: bytes) -> None:
|
async def _update_from_inp(content: bytes, project_code: str) -> None:
|
||||||
|
await run_in_threadpool(_update_from_inp_sync, content, project_code)
|
||||||
|
|
||||||
|
|
||||||
|
async def _apply_model_update(content: bytes, project_code: str) -> None:
|
||||||
try:
|
try:
|
||||||
await _update_from_inp(content)
|
await _update_from_inp(content, project_code)
|
||||||
|
except MaterializedViewRefreshAfterCommitError:
|
||||||
|
raise
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
@@ -140,6 +171,166 @@ async def _apply_model_update(content: bytes) -> None:
|
|||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _provision_from_inp_sync(
|
||||||
|
content: bytes,
|
||||||
|
*,
|
||||||
|
code: str,
|
||||||
|
workspace: str,
|
||||||
|
) -> ProvisionedProjectInfrastructure:
|
||||||
|
temp_path: Path | None = None
|
||||||
|
try:
|
||||||
|
with NamedTemporaryFile(suffix=".inp", delete=False) as temp_file:
|
||||||
|
temp_file.write(content)
|
||||||
|
temp_path = Path(temp_file.name)
|
||||||
|
return provision_project_infrastructure(
|
||||||
|
code=code,
|
||||||
|
workspace=workspace,
|
||||||
|
inp_path=temp_path,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
if temp_path is not None:
|
||||||
|
temp_path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/admin/project-provisions",
|
||||||
|
response_model=ProjectProvisionResponse,
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
summary="创建完整供水项目",
|
||||||
|
)
|
||||||
|
async def provision_project(
|
||||||
|
request: Request,
|
||||||
|
name: str = Form(..., min_length=1, max_length=100),
|
||||||
|
code: str = Form(..., min_length=1, max_length=50),
|
||||||
|
description: str | None = Form(default=None),
|
||||||
|
gs_workspace: str | None = Form(default=None, max_length=100),
|
||||||
|
map_zoom: int = Form(default=14, ge=1, le=22),
|
||||||
|
file: UploadFile = File(..., description="EPANET INP 模型文件"),
|
||||||
|
current_user=Depends(get_current_metadata_admin),
|
||||||
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||||
|
) -> ProjectProvisionResponse:
|
||||||
|
try:
|
||||||
|
normalized_code = validate_project_code(code)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=str(exc),
|
||||||
|
) from exc
|
||||||
|
workspace = gs_workspace or normalized_code
|
||||||
|
if await metadata_repo.get_project_by_code(normalized_code) is not None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail="Project code already exists",
|
||||||
|
)
|
||||||
|
if not is_database_encryption_configured():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="DATABASE_ENCRYPTION_KEY is not configured",
|
||||||
|
)
|
||||||
|
|
||||||
|
content, filename = await _read_upload(file)
|
||||||
|
validation_result = await _run_uploaded_inp(content)
|
||||||
|
try:
|
||||||
|
validation_payload = json.loads(validation_result)
|
||||||
|
except (TypeError, json.JSONDecodeError) as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="EPANET validation returned an invalid response",
|
||||||
|
) from exc
|
||||||
|
if validation_payload.get("simulation_result") != "successful":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="EPANET model validation failed",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
infrastructure = await run_in_threadpool(
|
||||||
|
_provision_from_inp_sync,
|
||||||
|
content,
|
||||||
|
code=normalized_code,
|
||||||
|
workspace=workspace,
|
||||||
|
)
|
||||||
|
except ProjectProvisioningError as exc:
|
||||||
|
if isinstance(exc.cause, ValueError):
|
||||||
|
response_status = status.HTTP_409_CONFLICT
|
||||||
|
elif exc.stage == "preflight":
|
||||||
|
response_status = status.HTTP_503_SERVICE_UNAVAILABLE
|
||||||
|
else:
|
||||||
|
response_status = status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=response_status,
|
||||||
|
detail={
|
||||||
|
"stage": exc.stage,
|
||||||
|
"message": str(exc.cause),
|
||||||
|
"cleanup_errors": exc.cleanup_errors,
|
||||||
|
},
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
map_extent = {"bbox": list(infrastructure.map_bbox), "zoom": map_zoom}
|
||||||
|
try:
|
||||||
|
project = await metadata_repo.create_provisioned_project(
|
||||||
|
name=name,
|
||||||
|
code=normalized_code,
|
||||||
|
description=description,
|
||||||
|
gs_workspace=workspace,
|
||||||
|
map_extent=map_extent,
|
||||||
|
creator_user_id=current_user.id,
|
||||||
|
business_dsn=infrastructure.business_dsn,
|
||||||
|
timescale_dsn=infrastructure.timescale_dsn,
|
||||||
|
pool_min_size=1,
|
||||||
|
pool_max_size=4,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
await metadata_repo.session.rollback()
|
||||||
|
cleanup_errors = await run_in_threadpool(infrastructure.cleanup)
|
||||||
|
if isinstance(exc, IntegrityError):
|
||||||
|
response_status = status.HTTP_409_CONFLICT
|
||||||
|
detail = "Project code or workspace conflicts with an existing project"
|
||||||
|
else:
|
||||||
|
response_status = status.HTTP_503_SERVICE_UNAVAILABLE
|
||||||
|
detail = f"Metadata database error: {exc}"
|
||||||
|
if cleanup_errors:
|
||||||
|
detail = f"{detail}; cleanup failures: {', '.join(cleanup_errors)}"
|
||||||
|
raise HTTPException(status_code=response_status, detail=detail) from exc
|
||||||
|
|
||||||
|
await log_audit_event(
|
||||||
|
action=AuditAction.CREATE,
|
||||||
|
user_id=current_user.id,
|
||||||
|
project_id=project.id,
|
||||||
|
resource_type="project_provision",
|
||||||
|
resource_id=str(project.id),
|
||||||
|
request_data={
|
||||||
|
"name": name,
|
||||||
|
"code": normalized_code,
|
||||||
|
"filename": filename,
|
||||||
|
"gs_workspace": workspace,
|
||||||
|
"layers": list(infrastructure.layers),
|
||||||
|
},
|
||||||
|
ip_address=request.client.host if request.client else None,
|
||||||
|
request_method=request.method,
|
||||||
|
request_path=request.url.path,
|
||||||
|
response_status=status.HTTP_201_CREATED,
|
||||||
|
session=metadata_repo.session,
|
||||||
|
)
|
||||||
|
return ProjectProvisionResponse(
|
||||||
|
project=AdminProjectResponse(
|
||||||
|
project_id=project.id,
|
||||||
|
name=project.name,
|
||||||
|
code=project.code,
|
||||||
|
description=project.description,
|
||||||
|
gs_workspace=project.gs_workspace,
|
||||||
|
map_extent=project.map_extent,
|
||||||
|
status=project.status,
|
||||||
|
created_at=project.created_at,
|
||||||
|
updated_at=project.updated_at,
|
||||||
|
),
|
||||||
|
business_database=normalized_code,
|
||||||
|
model_template_database=infrastructure.model_template,
|
||||||
|
timescale_database=normalized_code,
|
||||||
|
geoserver_workspace=workspace,
|
||||||
|
geoserver_layers=list(infrastructure.layers),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/admin/projects/{project_id}/model-imports",
|
"/admin/projects/{project_id}/model-imports",
|
||||||
summary="导入桌面端水力模型",
|
summary="导入桌面端水力模型",
|
||||||
@@ -177,7 +368,19 @@ async def update_project_model(
|
|||||||
) -> dict:
|
) -> dict:
|
||||||
project = await _get_active_project(project_id, metadata_repo)
|
project = await _get_active_project(project_id, metadata_repo)
|
||||||
content, filename = await _read_upload(file)
|
content, filename = await _read_upload(file)
|
||||||
await _apply_model_update(content)
|
routing = await resolve_project_business_routing(
|
||||||
|
ProjectContext(
|
||||||
|
project_id=project.id,
|
||||||
|
project_code=project.code,
|
||||||
|
user_id=current_user.id,
|
||||||
|
project_role="owner",
|
||||||
|
system_role=current_user.role,
|
||||||
|
is_superuser=current_user.is_superuser,
|
||||||
|
),
|
||||||
|
metadata_repo,
|
||||||
|
)
|
||||||
|
with activate_project_routing(routing):
|
||||||
|
await _apply_model_update(content, project.code)
|
||||||
await _audit_model_change(
|
await _audit_model_change(
|
||||||
request=request,
|
request=request,
|
||||||
current_user=current_user,
|
current_user=current_user,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, Request, Query, Path, Body
|
from typing import Any
|
||||||
from typing import Any, List, Dict, Union
|
|
||||||
|
from fastapi import APIRouter, Body, Query
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
Any,
|
|
||||||
ChangeSet,
|
ChangeSet,
|
||||||
calculate_demand_to_network,
|
calculate_demand_to_network,
|
||||||
calculate_demand_to_nodes,
|
calculate_demand_to_nodes,
|
||||||
@@ -22,7 +22,7 @@ router = APIRouter()
|
|||||||
summary="获取需水量属性架构",
|
summary="获取需水量属性架构",
|
||||||
description="获取指定水网中需水量(Demand)的属性架构定义"
|
description="获取指定水网中需水量(Demand)的属性架构定义"
|
||||||
)
|
)
|
||||||
async def fastapi_get_demand_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fastapi_get_demand_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
获取需水量属性架构。
|
获取需水量属性架构。
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ async def fastapi_get_demand_schema(network: str = Query(..., description="管
|
|||||||
summary="获取需水量属性",
|
summary="获取需水量属性",
|
||||||
description="获取指定水网中节点的需水量属性信息"
|
description="获取指定水网中节点的需水量属性信息"
|
||||||
)
|
)
|
||||||
async def fastapi_get_demand_properties(
|
def fastapi_get_demand_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点ID")
|
junction: str = Query(..., description="节点ID")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -55,17 +55,17 @@ async def fastapi_get_demand_properties(
|
|||||||
summary="设置需水量属性",
|
summary="设置需水量属性",
|
||||||
description="设置指定水网中节点的需水量属性信息"
|
description="设置指定水网中节点的需水量属性信息"
|
||||||
)
|
)
|
||||||
async def fastapi_set_demand_properties(
|
def fastapi_set_demand_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点ID"),
|
junction: str = Query(..., description="节点ID"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""
|
"""
|
||||||
设置节点的需水量属性。
|
设置节点的需水量属性。
|
||||||
|
|
||||||
修改指定节点的需水量信息。请求体应包含需水量值、水压等级等属性。
|
修改指定节点的需水量信息。请求体应包含需水量值、水压等级等属性。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
ps = {"junction": junction} | props
|
ps = {"junction": junction} | props
|
||||||
return set_demand(network, ChangeSet(ps))
|
return set_demand(network, ChangeSet(ps))
|
||||||
|
|
||||||
@@ -77,9 +77,9 @@ async def fastapi_set_demand_properties(
|
|||||||
summary="计算需水量到节点分配",
|
summary="计算需水量到节点分配",
|
||||||
description="将总需水量按指定方式分配到多个节点"
|
description="将总需水量按指定方式分配到多个节点"
|
||||||
)
|
)
|
||||||
async def fastapi_calculate_demand_to_nodes(
|
def fastapi_calculate_demand_to_nodes(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> dict[str, float]:
|
) -> dict[str, float]:
|
||||||
"""
|
"""
|
||||||
计算需水量到节点分配。
|
计算需水量到节点分配。
|
||||||
@@ -92,7 +92,7 @@ async def fastapi_calculate_demand_to_nodes(
|
|||||||
"nodes": 节点ID列表(list[str])
|
"nodes": 节点ID列表(list[str])
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
demand = props["demand"]
|
demand = props["demand"]
|
||||||
nodes = props["nodes"]
|
nodes = props["nodes"]
|
||||||
return calculate_demand_to_nodes(network, demand, nodes)
|
return calculate_demand_to_nodes(network, demand, nodes)
|
||||||
@@ -102,9 +102,9 @@ async def fastapi_calculate_demand_to_nodes(
|
|||||||
summary="计算需水量到区域分配",
|
summary="计算需水量到区域分配",
|
||||||
description="将总需水量按区域特征分配到该区域内的节点"
|
description="将总需水量按区域特征分配到该区域内的节点"
|
||||||
)
|
)
|
||||||
async def fastapi_calculate_demand_to_region(
|
def fastapi_calculate_demand_to_region(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> dict[str, float]:
|
) -> dict[str, float]:
|
||||||
"""
|
"""
|
||||||
计算需水量到区域分配。
|
计算需水量到区域分配。
|
||||||
@@ -117,7 +117,7 @@ async def fastapi_calculate_demand_to_region(
|
|||||||
"region": 区域ID(str)
|
"region": 区域ID(str)
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
demand = props["demand"]
|
demand = props["demand"]
|
||||||
region = props["region"]
|
region = props["region"]
|
||||||
return calculate_demand_to_region(network, demand, region)
|
return calculate_demand_to_region(network, demand, region)
|
||||||
@@ -127,7 +127,7 @@ async def fastapi_calculate_demand_to_region(
|
|||||||
summary="计算需水量到整网分配",
|
summary="计算需水量到整网分配",
|
||||||
description="将需水量均匀分配到整个水网的所有需水节点"
|
description="将需水量均匀分配到整个水网的所有需水节点"
|
||||||
)
|
)
|
||||||
async def fastapi_calculate_demand_to_network(
|
def fastapi_calculate_demand_to_network(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
demand: float = Query(..., description="总需水量(m³/h)", gt=0)
|
demand: float = Query(..., description="总需水量(m³/h)", gt=0)
|
||||||
) -> dict[str, float]:
|
) -> dict[str, float]:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, Request, Query, Path, Body
|
from typing import Any
|
||||||
from typing import Any, List, Dict, Union
|
|
||||||
|
from fastapi import APIRouter, Body, Query
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
Any,
|
|
||||||
ChangeSet,
|
ChangeSet,
|
||||||
delete_junction,
|
delete_junction,
|
||||||
delete_pipe,
|
delete_pipe,
|
||||||
@@ -49,7 +49,7 @@ router = APIRouter()
|
|||||||
summary="检查节点有效性",
|
summary="检查节点有效性",
|
||||||
description="检查指定ID是否为水网中的有效节点"
|
description="检查指定ID是否为水网中的有效节点"
|
||||||
)
|
)
|
||||||
async def fastapi_is_node(
|
def fastapi_is_node(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
node: str = Query(..., description="节点ID")
|
node: str = Query(..., description="节点ID")
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -61,7 +61,7 @@ async def fastapi_is_node(
|
|||||||
summary="检查是否为接点",
|
summary="检查是否为接点",
|
||||||
description="检查指定ID是否为水网中的接点(需求点)"
|
description="检查指定ID是否为水网中的接点(需求点)"
|
||||||
)
|
)
|
||||||
async def fastapi_is_junction(
|
def fastapi_is_junction(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
node: str = Query(..., description="节点ID")
|
node: str = Query(..., description="节点ID")
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -73,7 +73,7 @@ async def fastapi_is_junction(
|
|||||||
summary="检查是否为水源",
|
summary="检查是否为水源",
|
||||||
description="检查指定ID是否为水网中的水源(水库/河流)"
|
description="检查指定ID是否为水网中的水源(水库/河流)"
|
||||||
)
|
)
|
||||||
async def fastapi_is_reservoir(
|
def fastapi_is_reservoir(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
node: str = Query(..., description="节点ID")
|
node: str = Query(..., description="节点ID")
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -85,7 +85,7 @@ async def fastapi_is_reservoir(
|
|||||||
summary="检查是否为蓄水池",
|
summary="检查是否为蓄水池",
|
||||||
description="检查指定ID是否为水网中的蓄水池"
|
description="检查指定ID是否为水网中的蓄水池"
|
||||||
)
|
)
|
||||||
async def fastapi_is_tank(
|
def fastapi_is_tank(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
node: str = Query(..., description="节点ID")
|
node: str = Query(..., description="节点ID")
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -97,7 +97,7 @@ async def fastapi_is_tank(
|
|||||||
summary="检查管线有效性",
|
summary="检查管线有效性",
|
||||||
description="检查指定ID是否为水网中的有效管线"
|
description="检查指定ID是否为水网中的有效管线"
|
||||||
)
|
)
|
||||||
async def fastapi_is_link(
|
def fastapi_is_link(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
link: str = Query(..., description="管线ID")
|
link: str = Query(..., description="管线ID")
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -109,7 +109,7 @@ async def fastapi_is_link(
|
|||||||
summary="检查是否为管道",
|
summary="检查是否为管道",
|
||||||
description="检查指定ID是否为水网中的管道"
|
description="检查指定ID是否为水网中的管道"
|
||||||
)
|
)
|
||||||
async def fastapi_is_pipe(
|
def fastapi_is_pipe(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
link: str = Query(..., description="管线ID")
|
link: str = Query(..., description="管线ID")
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -121,7 +121,7 @@ async def fastapi_is_pipe(
|
|||||||
summary="检查是否为泵",
|
summary="检查是否为泵",
|
||||||
description="检查指定ID是否为水网中的泵"
|
description="检查指定ID是否为水网中的泵"
|
||||||
)
|
)
|
||||||
async def fastapi_is_pump(
|
def fastapi_is_pump(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
link: str = Query(..., description="管线ID")
|
link: str = Query(..., description="管线ID")
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -133,7 +133,7 @@ async def fastapi_is_pump(
|
|||||||
summary="检查是否为阀门",
|
summary="检查是否为阀门",
|
||||||
description="检查指定ID是否为水网中的阀门"
|
description="检查指定ID是否为水网中的阀门"
|
||||||
)
|
)
|
||||||
async def fastapi_is_valve(
|
def fastapi_is_valve(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
link: str = Query(..., description="管线ID")
|
link: str = Query(..., description="管线ID")
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -145,7 +145,7 @@ async def fastapi_is_valve(
|
|||||||
summary="获取节点类型",
|
summary="获取节点类型",
|
||||||
description="获取指定节点的类型(接点/水源/蓄水池)"
|
description="获取指定节点的类型(接点/水源/蓄水池)"
|
||||||
)
|
)
|
||||||
async def fastapi_get_node_type(
|
def fastapi_get_node_type(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
node: str = Query(..., description="节点ID")
|
node: str = Query(..., description="节点ID")
|
||||||
) -> str:
|
) -> str:
|
||||||
@@ -157,7 +157,7 @@ async def fastapi_get_node_type(
|
|||||||
summary="获取管线类型",
|
summary="获取管线类型",
|
||||||
description="获取指定管线的类型(管道/泵/阀门)"
|
description="获取指定管线的类型(管道/泵/阀门)"
|
||||||
)
|
)
|
||||||
async def fastapi_get_link_type(
|
def fastapi_get_link_type(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
link: str = Query(..., description="管线ID")
|
link: str = Query(..., description="管线ID")
|
||||||
) -> str:
|
) -> str:
|
||||||
@@ -169,7 +169,7 @@ async def fastapi_get_link_type(
|
|||||||
summary="获取元素类型",
|
summary="获取元素类型",
|
||||||
description="获取指定元素的类型(节点或管线)"
|
description="获取指定元素的类型(节点或管线)"
|
||||||
)
|
)
|
||||||
async def fastapi_get_element_type(
|
def fastapi_get_element_type(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
element: str = Query(..., description="元素ID")
|
element: str = Query(..., description="元素ID")
|
||||||
) -> str:
|
) -> str:
|
||||||
@@ -181,7 +181,7 @@ async def fastapi_get_element_type(
|
|||||||
summary="获取元素类型值",
|
summary="获取元素类型值",
|
||||||
description="获取指定元素的类型数值标识"
|
description="获取指定元素的类型数值标识"
|
||||||
)
|
)
|
||||||
async def fastapi_get_element_type_value(
|
def fastapi_get_element_type_value(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
element: str = Query(..., description="元素ID")
|
element: str = Query(..., description="元素ID")
|
||||||
) -> int:
|
) -> int:
|
||||||
@@ -193,7 +193,7 @@ async def fastapi_get_element_type_value(
|
|||||||
summary="获取所有节点",
|
summary="获取所有节点",
|
||||||
description="获取指定水网中的所有节点ID列表"
|
description="获取指定水网中的所有节点ID列表"
|
||||||
)
|
)
|
||||||
async def fastapi_get_nodes(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
|
def fastapi_get_nodes(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
|
||||||
"""获取水网中所有节点的ID列表。"""
|
"""获取水网中所有节点的ID列表。"""
|
||||||
return get_nodes(network)
|
return get_nodes(network)
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ async def fastapi_get_nodes(network: str = Query(..., description="管网名称
|
|||||||
summary="获取所有管线",
|
summary="获取所有管线",
|
||||||
description="获取指定水网中的所有管线ID列表"
|
description="获取指定水网中的所有管线ID列表"
|
||||||
)
|
)
|
||||||
async def fastapi_get_links(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
|
def fastapi_get_links(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
|
||||||
"""获取水网中所有管线的ID列表。"""
|
"""获取水网中所有管线的ID列表。"""
|
||||||
return get_links(network)
|
return get_links(network)
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ def get_node_links_endpoint(
|
|||||||
summary="获取节点属性",
|
summary="获取节点属性",
|
||||||
description="获取指定节点的所有属性信息"
|
description="获取指定节点的所有属性信息"
|
||||||
)
|
)
|
||||||
async def fast_get_node_properties(
|
def fast_get_node_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
node: str = Query(..., description="节点ID")
|
node: str = Query(..., description="节点ID")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -239,7 +239,7 @@ async def fast_get_node_properties(
|
|||||||
summary="获取管线属性",
|
summary="获取管线属性",
|
||||||
description="获取指定管线的所有属性信息"
|
description="获取指定管线的所有属性信息"
|
||||||
)
|
)
|
||||||
async def fast_get_link_properties(
|
def fast_get_link_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
link: str = Query(..., description="管线ID")
|
link: str = Query(..., description="管线ID")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -251,7 +251,7 @@ async def fast_get_link_properties(
|
|||||||
summary="获取SCADA点属性",
|
summary="获取SCADA点属性",
|
||||||
description="获取指定SCADA点的属性信息"
|
description="获取指定SCADA点的属性信息"
|
||||||
)
|
)
|
||||||
async def fast_get_scada_properties(
|
def fast_get_scada_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
scada: str = Query(..., description="SCADA点ID")
|
scada: str = Query(..., description="SCADA点ID")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -263,7 +263,7 @@ async def fast_get_scada_properties(
|
|||||||
summary="获取所有SCADA点属性",
|
summary="获取所有SCADA点属性",
|
||||||
description="获取指定水网中所有SCADA点的属性信息"
|
description="获取指定水网中所有SCADA点的属性信息"
|
||||||
)
|
)
|
||||||
async def fast_get_all_scada_properties(
|
def fast_get_all_scada_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""获取水网中所有SCADA点的属性列表。"""
|
"""获取水网中所有SCADA点的属性列表。"""
|
||||||
@@ -274,7 +274,7 @@ async def fast_get_all_scada_properties(
|
|||||||
summary="获取指定类型元素属性",
|
summary="获取指定类型元素属性",
|
||||||
description="获取指定类型的元素属性信息"
|
description="获取指定类型的元素属性信息"
|
||||||
)
|
)
|
||||||
async def fast_get_element_properties_with_type(
|
def fast_get_element_properties_with_type(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
elementtype: str = Query(..., description="元素类型"),
|
elementtype: str = Query(..., description="元素类型"),
|
||||||
element: str = Query(..., description="元素ID")
|
element: str = Query(..., description="元素ID")
|
||||||
@@ -287,7 +287,7 @@ async def fast_get_element_properties_with_type(
|
|||||||
summary="获取元素属性",
|
summary="获取元素属性",
|
||||||
description="获取指定元素的属性信息"
|
description="获取指定元素的属性信息"
|
||||||
)
|
)
|
||||||
async def fast_get_element_properties(
|
def fast_get_element_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
element: str = Query(..., description="元素ID")
|
element: str = Query(..., description="元素ID")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -303,7 +303,7 @@ async def fast_get_element_properties(
|
|||||||
summary="获取标题属性架构",
|
summary="获取标题属性架构",
|
||||||
description="获取指定水网的标题(标题)属性架构定义"
|
description="获取指定水网的标题(标题)属性架构定义"
|
||||||
)
|
)
|
||||||
async def fast_get_title_schema(
|
def fast_get_title_schema(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
"""获取水网标题的属性架构。"""
|
"""获取水网标题的属性架构。"""
|
||||||
@@ -314,7 +314,7 @@ async def fast_get_title_schema(
|
|||||||
summary="获取水网标题属性",
|
summary="获取水网标题属性",
|
||||||
description="获取指定水网的标题(Title)信息"
|
description="获取指定水网的标题(Title)信息"
|
||||||
)
|
)
|
||||||
async def fast_get_title(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
def fast_get_title(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||||
"""获取水网的标题属性。"""
|
"""获取水网的标题属性。"""
|
||||||
return get_title(network)
|
return get_title(network)
|
||||||
|
|
||||||
@@ -324,12 +324,12 @@ async def fast_get_title(network: str = Query(..., description="管网名称(
|
|||||||
summary="设置水网标题属性",
|
summary="设置水网标题属性",
|
||||||
description="设置指定水网的标题(Title)信息"
|
description="设置指定水网的标题(Title)信息"
|
||||||
)
|
)
|
||||||
async def fastapi_set_title(
|
def fastapi_set_title(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置水网的标题属性。"""
|
"""设置水网的标题属性。"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return set_title(network, ChangeSet(props))
|
return set_title(network, ChangeSet(props))
|
||||||
|
|
||||||
############################################################
|
############################################################
|
||||||
@@ -341,7 +341,7 @@ async def fastapi_set_title(
|
|||||||
summary="获取状态属性架构",
|
summary="获取状态属性架构",
|
||||||
description="获取指定水网的状态(Status)属性架构定义"
|
description="获取指定水网的状态(Status)属性架构定义"
|
||||||
)
|
)
|
||||||
async def fastapi_get_status_schema(
|
def fastapi_get_status_schema(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
"""获取水网状态的属性架构。"""
|
"""获取水网状态的属性架构。"""
|
||||||
@@ -352,7 +352,7 @@ async def fastapi_get_status_schema(
|
|||||||
summary="获取管线状态",
|
summary="获取管线状态",
|
||||||
description="获取指定管线的状态信息"
|
description="获取指定管线的状态信息"
|
||||||
)
|
)
|
||||||
async def fastapi_get_status(
|
def fastapi_get_status(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
link: str = Query(..., description="管线ID")
|
link: str = Query(..., description="管线ID")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -365,13 +365,13 @@ async def fastapi_get_status(
|
|||||||
summary="设置管线状态",
|
summary="设置管线状态",
|
||||||
description="设置指定管线的状态信息"
|
description="设置指定管线的状态信息"
|
||||||
)
|
)
|
||||||
async def fastapi_set_status_properties(
|
def fastapi_set_status_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
link: str = Query(..., description="管线ID"),
|
link: str = Query(..., description="管线ID"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置管线的状态属性。"""
|
"""设置管线的状态属性。"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
ps = {"link": link} | props
|
ps = {"link": link} | props
|
||||||
return set_status(network, ChangeSet(ps))
|
return set_status(network, ChangeSet(ps))
|
||||||
|
|
||||||
@@ -385,7 +385,7 @@ async def fastapi_set_status_properties(
|
|||||||
summary="删除节点",
|
summary="删除节点",
|
||||||
description="删除指定的节点(接点/水源/蓄水池)"
|
description="删除指定的节点(接点/水源/蓄水池)"
|
||||||
)
|
)
|
||||||
async def fastapi_delete_node(
|
def fastapi_delete_node(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
node: str = Query(..., description="节点ID")
|
node: str = Query(..., description="节点ID")
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
@@ -405,7 +405,7 @@ async def fastapi_delete_node(
|
|||||||
summary="删除管线",
|
summary="删除管线",
|
||||||
description="删除指定的管线(管道/泵/阀门)"
|
description="删除指定的管线(管道/泵/阀门)"
|
||||||
)
|
)
|
||||||
async def fastapi_delete_link(
|
def fastapi_delete_link(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
link: str = Query(..., description="管线ID")
|
link: str = Query(..., description="管线ID")
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ router = APIRouter()
|
|||||||
# # example: set_coord(p, ChangeSet({'node': 'j1', 'x': 1.0, 'y': 2.0}))
|
# # example: set_coord(p, ChangeSet({'node': 'j1', 'x': 1.0, 'y': 2.0}))
|
||||||
# @router.post("/setcoord/", response_model=None)
|
# @router.post("/setcoord/", response_model=None)
|
||||||
# async def fastapi_set_coord(network: str, req: Request) -> ChangeSet:
|
# async def fastapi_set_coord(network: str, req: Request) -> ChangeSet:
|
||||||
# props = await req.json()
|
# props = payload
|
||||||
# return set_coord(network, ChangeSet(props))
|
# return set_coord(network, ChangeSet(props))
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
@@ -35,7 +35,7 @@ router = APIRouter()
|
|||||||
summary="获取节点坐标",
|
summary="获取节点坐标",
|
||||||
description="获取指定节点的地理坐标(X, Y)"
|
description="获取指定节点的地理坐标(X, Y)"
|
||||||
)
|
)
|
||||||
async def fastapi_get_node_coord(
|
def fastapi_get_node_coord(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
node: str = Query(..., description="节点ID")
|
node: str = Query(..., description="节点ID")
|
||||||
) -> dict[str, float] | None:
|
) -> dict[str, float] | None:
|
||||||
@@ -48,7 +48,7 @@ async def fastapi_get_node_coord(
|
|||||||
summary="获取范围内的网络元素",
|
summary="获取范围内的网络元素",
|
||||||
description="获取指定地理范围内的网络节点和管线"
|
description="获取指定地理范围内的网络节点和管线"
|
||||||
)
|
)
|
||||||
async def fastapi_get_network_in_extent(
|
def fastapi_get_network_in_extent(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
x1: float = Query(..., description="范围左下角X坐标", alias="x1"),
|
x1: float = Query(..., description="范围左下角X坐标", alias="x1"),
|
||||||
y1: float = Query(..., description="范围左下角Y坐标", alias="y1"),
|
y1: float = Query(..., description="范围左下角Y坐标", alias="y1"),
|
||||||
@@ -63,7 +63,7 @@ async def fastapi_get_network_in_extent(
|
|||||||
summary="获取主要节点坐标",
|
summary="获取主要节点坐标",
|
||||||
description="获取直径大于等于指定值的节点坐标"
|
description="获取直径大于等于指定值的节点坐标"
|
||||||
)
|
)
|
||||||
async def fastapi_get_majornode_coords(
|
def fastapi_get_majornode_coords(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
diameter: int = Query(..., description="最小直径(mm)", gt=0)
|
diameter: int = Query(..., description="最小直径(mm)", gt=0)
|
||||||
) -> dict[str, dict[str, float]]:
|
) -> dict[str, dict[str, float]]:
|
||||||
@@ -75,7 +75,7 @@ async def fastapi_get_majornode_coords(
|
|||||||
summary="获取主要管道节点",
|
summary="获取主要管道节点",
|
||||||
description="获取直径大于等于指定值的管道的节点ID"
|
description="获取直径大于等于指定值的管道的节点ID"
|
||||||
)
|
)
|
||||||
async def fastapi_get_major_pipe_nodes(
|
def fastapi_get_major_pipe_nodes(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
diameter: int = Query(..., description="最小直径(mm)", gt=0)
|
diameter: int = Query(..., description="最小直径(mm)", gt=0)
|
||||||
) -> list[str] | None:
|
) -> list[str] | None:
|
||||||
@@ -87,7 +87,7 @@ async def fastapi_get_major_pipe_nodes(
|
|||||||
summary="获取网络管线节点",
|
summary="获取网络管线节点",
|
||||||
description="获取指定水网所有管线的起点和终点节点"
|
description="获取指定水网所有管线的起点和终点节点"
|
||||||
)
|
)
|
||||||
async def fastapi_get_network_link_nodes(
|
def fastapi_get_network_link_nodes(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> list[str] | None:
|
) -> list[str] | None:
|
||||||
"""获取网络中所有管线的连接节点。"""
|
"""获取网络中所有管线的连接节点。"""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, Request, Query, Path, Body
|
from typing import Any
|
||||||
from typing import Any, List, Dict, Union
|
|
||||||
|
from fastapi import APIRouter, Body, Query
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
Any,
|
|
||||||
ChangeSet,
|
ChangeSet,
|
||||||
add_junction,
|
add_junction,
|
||||||
delete_junction,
|
delete_junction,
|
||||||
@@ -14,7 +14,7 @@ from app.services.tjnetwork import (
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/network-schemas/junction", summary="获取节点架构", description="获取指定项目的节点属性架构和数据类型定义。")
|
@router.get("/network-schemas/junction", summary="获取节点架构", description="获取指定项目的节点属性架构和数据类型定义。")
|
||||||
async def fast_get_junction_schema(
|
def fast_get_junction_schema(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -28,7 +28,7 @@ async def fast_get_junction_schema(
|
|||||||
return get_junction_schema(network)
|
return get_junction_schema(network)
|
||||||
|
|
||||||
@router.post("/junctions", response_model=None, summary="添加节点", description="在供水网络中添加新的节点,指定节点ID和空间坐标。")
|
@router.post("/junctions", response_model=None, summary="添加节点", description="在供水网络中添加新的节点,指定节点ID和空间坐标。")
|
||||||
async def fastapi_add_junction(
|
def fastapi_add_junction(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID"),
|
junction: str = Query(..., description="节点 ID"),
|
||||||
x: float = Query(..., description="X 坐标"),
|
x: float = Query(..., description="X 坐标"),
|
||||||
@@ -52,7 +52,7 @@ async def fastapi_add_junction(
|
|||||||
return add_junction(network, ChangeSet(ps))
|
return add_junction(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.delete("/junctions", response_model=None, summary="删除节点", description="从供水网络中删除指定的节点。")
|
@router.delete("/junctions", response_model=None, summary="删除节点", description="从供水网络中删除指定的节点。")
|
||||||
async def fastapi_delete_junction(
|
def fastapi_delete_junction(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID")
|
junction: str = Query(..., description="节点 ID")
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
@@ -70,7 +70,7 @@ async def fastapi_delete_junction(
|
|||||||
return delete_junction(network, ChangeSet(ps))
|
return delete_junction(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/junctions/elevation", summary="获取节点标高", description="获取指定节点的标高(海拔高度)。")
|
@router.get("/junctions/elevation", summary="获取节点标高", description="获取指定节点的标高(海拔高度)。")
|
||||||
async def fastapi_get_junction_elevation(
|
def fastapi_get_junction_elevation(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID")
|
junction: str = Query(..., description="节点 ID")
|
||||||
) -> float:
|
) -> float:
|
||||||
@@ -88,7 +88,7 @@ async def fastapi_get_junction_elevation(
|
|||||||
return ps["elevation"]
|
return ps["elevation"]
|
||||||
|
|
||||||
@router.get("/junctions/x", summary="获取节点 X 坐标", description="获取指定节点的 X 坐标值。")
|
@router.get("/junctions/x", summary="获取节点 X 坐标", description="获取指定节点的 X 坐标值。")
|
||||||
async def fastapi_get_junction_x(
|
def fastapi_get_junction_x(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID")
|
junction: str = Query(..., description="节点 ID")
|
||||||
) -> float:
|
) -> float:
|
||||||
@@ -106,7 +106,7 @@ async def fastapi_get_junction_x(
|
|||||||
return ps["x"]
|
return ps["x"]
|
||||||
|
|
||||||
@router.get("/junctions/y", summary="获取节点 Y 坐标", description="获取指定节点的 Y 坐标值。")
|
@router.get("/junctions/y", summary="获取节点 Y 坐标", description="获取指定节点的 Y 坐标值。")
|
||||||
async def fastapi_get_junction_y(
|
def fastapi_get_junction_y(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID")
|
junction: str = Query(..., description="节点 ID")
|
||||||
) -> float:
|
) -> float:
|
||||||
@@ -124,7 +124,7 @@ async def fastapi_get_junction_y(
|
|||||||
return ps["y"]
|
return ps["y"]
|
||||||
|
|
||||||
@router.get("/junctions/coord", summary="获取节点坐标", description="获取指定节点的 X 和 Y 坐标。")
|
@router.get("/junctions/coord", summary="获取节点坐标", description="获取指定节点的 X 和 Y 坐标。")
|
||||||
async def fastapi_get_junction_coord(
|
def fastapi_get_junction_coord(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID")
|
junction: str = Query(..., description="节点 ID")
|
||||||
) -> dict[str, float]:
|
) -> dict[str, float]:
|
||||||
@@ -143,7 +143,7 @@ async def fastapi_get_junction_coord(
|
|||||||
return coord
|
return coord
|
||||||
|
|
||||||
@router.get("/junctions/demand", summary="获取节点需水量", description="获取指定节点的需水量。")
|
@router.get("/junctions/demand", summary="获取节点需水量", description="获取指定节点的需水量。")
|
||||||
async def fastapi_get_junction_demand(
|
def fastapi_get_junction_demand(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID")
|
junction: str = Query(..., description="节点 ID")
|
||||||
) -> float:
|
) -> float:
|
||||||
@@ -161,7 +161,7 @@ async def fastapi_get_junction_demand(
|
|||||||
return ps["demand"]
|
return ps["demand"]
|
||||||
|
|
||||||
@router.get("/junctions/pattern", summary="获取节点需水模式", description="获取指定节点的需水模式标识。")
|
@router.get("/junctions/pattern", summary="获取节点需水模式", description="获取指定节点的需水模式标识。")
|
||||||
async def fastapi_get_junction_pattern(
|
def fastapi_get_junction_pattern(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID")
|
junction: str = Query(..., description="节点 ID")
|
||||||
) -> str:
|
) -> str:
|
||||||
@@ -179,7 +179,7 @@ async def fastapi_get_junction_pattern(
|
|||||||
return ps["pattern"]
|
return ps["pattern"]
|
||||||
|
|
||||||
@router.patch("/junctions/elevation", response_model=None, summary="设置节点标高", description="设置指定节点的标高值。")
|
@router.patch("/junctions/elevation", response_model=None, summary="设置节点标高", description="设置指定节点的标高值。")
|
||||||
async def fastapi_set_junction_elevation(
|
def fastapi_set_junction_elevation(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID"),
|
junction: str = Query(..., description="节点 ID"),
|
||||||
elevation: float = Query(..., description="标高(海拔高度)")
|
elevation: float = Query(..., description="标高(海拔高度)")
|
||||||
@@ -199,7 +199,7 @@ async def fastapi_set_junction_elevation(
|
|||||||
return set_junction(network, ChangeSet(ps))
|
return set_junction(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/junctions/x", response_model=None, summary="设置节点 X 坐标", description="设置指定节点的 X 坐标值。")
|
@router.patch("/junctions/x", response_model=None, summary="设置节点 X 坐标", description="设置指定节点的 X 坐标值。")
|
||||||
async def fastapi_set_junction_x(
|
def fastapi_set_junction_x(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID"),
|
junction: str = Query(..., description="节点 ID"),
|
||||||
x: float = Query(..., description="X 坐标值")
|
x: float = Query(..., description="X 坐标值")
|
||||||
@@ -219,7 +219,7 @@ async def fastapi_set_junction_x(
|
|||||||
return set_junction(network, ChangeSet(ps))
|
return set_junction(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/junctions/y", response_model=None, summary="设置节点 Y 坐标", description="设置指定节点的 Y 坐标值。")
|
@router.patch("/junctions/y", response_model=None, summary="设置节点 Y 坐标", description="设置指定节点的 Y 坐标值。")
|
||||||
async def fastapi_set_junction_y(
|
def fastapi_set_junction_y(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID"),
|
junction: str = Query(..., description="节点 ID"),
|
||||||
y: float = Query(..., description="Y 坐标值")
|
y: float = Query(..., description="Y 坐标值")
|
||||||
@@ -239,7 +239,7 @@ async def fastapi_set_junction_y(
|
|||||||
return set_junction(network, ChangeSet(ps))
|
return set_junction(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/junctions/coord", response_model=None, summary="设置节点坐标", description="设置指定节点的 X 和 Y 坐标。")
|
@router.patch("/junctions/coord", response_model=None, summary="设置节点坐标", description="设置指定节点的 X 和 Y 坐标。")
|
||||||
async def fastapi_set_junction_coord(
|
def fastapi_set_junction_coord(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID"),
|
junction: str = Query(..., description="节点 ID"),
|
||||||
x: float = Query(..., description="X 坐标值"),
|
x: float = Query(..., description="X 坐标值"),
|
||||||
@@ -261,7 +261,7 @@ async def fastapi_set_junction_coord(
|
|||||||
return set_junction(network, ChangeSet(ps))
|
return set_junction(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/junctions/demand", response_model=None, summary="设置节点需水量", description="设置指定节点的需水量。")
|
@router.patch("/junctions/demand", response_model=None, summary="设置节点需水量", description="设置指定节点的需水量。")
|
||||||
async def fastapi_set_junction_demand(
|
def fastapi_set_junction_demand(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID"),
|
junction: str = Query(..., description="节点 ID"),
|
||||||
demand: float = Query(..., description="需水量值")
|
demand: float = Query(..., description="需水量值")
|
||||||
@@ -281,7 +281,7 @@ async def fastapi_set_junction_demand(
|
|||||||
return set_junction(network, ChangeSet(ps))
|
return set_junction(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/junctions/pattern", response_model=None, summary="设置节点需水模式", description="设置指定节点的需水模式标识。")
|
@router.patch("/junctions/pattern", response_model=None, summary="设置节点需水模式", description="设置指定节点的需水模式标识。")
|
||||||
async def fastapi_set_junction_pattern(
|
def fastapi_set_junction_pattern(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID"),
|
junction: str = Query(..., description="节点 ID"),
|
||||||
pattern: str = Query(..., description="需水模式标识")
|
pattern: str = Query(..., description="需水模式标识")
|
||||||
@@ -301,7 +301,7 @@ async def fastapi_set_junction_pattern(
|
|||||||
return set_junction(network, ChangeSet(ps))
|
return set_junction(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/junctions/properties", summary="获取节点属性", description="获取指定节点的所有属性信息。")
|
@router.get("/junctions/properties", summary="获取节点属性", description="获取指定节点的所有属性信息。")
|
||||||
async def fastapi_get_junction_properties(
|
def fastapi_get_junction_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID")
|
junction: str = Query(..., description="节点 ID")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -318,7 +318,7 @@ async def fastapi_get_junction_properties(
|
|||||||
return get_junction(network, junction)
|
return get_junction(network, junction)
|
||||||
|
|
||||||
@router.get("/junctions", summary="获取所有节点属性", description="获取指定项目中所有节点的属性信息。")
|
@router.get("/junctions", summary="获取所有节点属性", description="获取指定项目中所有节点的属性信息。")
|
||||||
async def fastapi_get_all_junction_properties(
|
def fastapi_get_all_junction_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -333,15 +333,14 @@ async def fastapi_get_all_junction_properties(
|
|||||||
list: 包含所有节点属性的列表
|
list: 包含所有节点属性的列表
|
||||||
"""
|
"""
|
||||||
# 缓存查询结果提高性能
|
# 缓存查询结果提高性能
|
||||||
# global redis_client # Redis logic removed for clean split, can be re-added if needed or imported
|
|
||||||
results = get_all_junctions(network)
|
results = get_all_junctions(network)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@router.patch("/junctions/properties", response_model=None, summary="批量设置节点属性", description="批量设置指定节点的多个属性。")
|
@router.patch("/junctions/properties", response_model=None, summary="批量设置节点属性", description="批量设置指定节点的多个属性。")
|
||||||
async def fastapi_set_junction_properties(
|
def fastapi_set_junction_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
junction: str = Query(..., description="节点 ID"),
|
junction: str = Query(..., description="节点 ID"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""
|
"""
|
||||||
批量设置节点属性。
|
批量设置节点属性。
|
||||||
@@ -356,6 +355,6 @@ async def fastapi_set_junction_properties(
|
|||||||
Returns:
|
Returns:
|
||||||
ChangeSet: 包含变更信息的结果
|
ChangeSet: 包含变更信息的结果
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
ps = {"id": junction} | props
|
ps = {"id": junction} | props
|
||||||
return set_junction(network, ChangeSet(ps))
|
return set_junction(network, ChangeSet(ps))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, Request, Query, Path, Body
|
from typing import Any
|
||||||
from typing import Any, List, Dict, Union
|
|
||||||
|
from fastapi import APIRouter, Body, Query
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
Any,
|
|
||||||
ChangeSet,
|
ChangeSet,
|
||||||
PIPE_STATUS_OPEN,
|
PIPE_STATUS_OPEN,
|
||||||
add_pipe,
|
add_pipe,
|
||||||
@@ -15,7 +15,7 @@ from app.services.tjnetwork import (
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/network-schemas/pipe", summary="获取管道模式", description="获取管道对象的模式定义,包含所有可用字段及其类型")
|
@router.get("/network-schemas/pipe", summary="获取管道模式", description="获取管道对象的模式定义,包含所有可用字段及其类型")
|
||||||
async def fastapi_get_pipe_schema(
|
def fastapi_get_pipe_schema(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -30,7 +30,7 @@ async def fastapi_get_pipe_schema(
|
|||||||
return get_pipe_schema(network)
|
return get_pipe_schema(network)
|
||||||
|
|
||||||
@router.post("/pipes", response_model=None, summary="添加管道", description="向网络中添加新的管道,需要提供管道的基本参数如长度、管径、粗糙度等")
|
@router.post("/pipes", response_model=None, summary="添加管道", description="向网络中添加新的管道,需要提供管道的基本参数如长度、管径、粗糙度等")
|
||||||
async def fastapi_add_pipe(
|
def fastapi_add_pipe(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道标识符"),
|
pipe: str = Query(..., description="管道标识符"),
|
||||||
node1: str = Query(..., description="管道起始节点ID"),
|
node1: str = Query(..., description="管道起始节点ID"),
|
||||||
@@ -71,7 +71,7 @@ async def fastapi_add_pipe(
|
|||||||
return add_pipe(network, ChangeSet(ps))
|
return add_pipe(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.delete("/pipes", response_model=None, summary="删除管道", description="从网络中删除指定的管道")
|
@router.delete("/pipes", response_model=None, summary="删除管道", description="从网络中删除指定的管道")
|
||||||
async def fastapi_delete_pipe(
|
def fastapi_delete_pipe(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="要删除的管道ID")
|
pipe: str = Query(..., description="要删除的管道ID")
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
@@ -89,7 +89,7 @@ async def fastapi_delete_pipe(
|
|||||||
return delete_pipe(network, ChangeSet(ps))
|
return delete_pipe(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/pipes/node1", summary="获取管道起始节点", description="获取指定管道的起始节点ID")
|
@router.get("/pipes/node1", summary="获取管道起始节点", description="获取指定管道的起始节点ID")
|
||||||
async def fastapi_get_pipe_node1(
|
def fastapi_get_pipe_node1(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID")
|
pipe: str = Query(..., description="管道ID")
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
@@ -107,7 +107,7 @@ async def fastapi_get_pipe_node1(
|
|||||||
return ps["node1"]
|
return ps["node1"]
|
||||||
|
|
||||||
@router.get("/pipes/node2", summary="获取管道终止节点", description="获取指定管道的终止节点ID")
|
@router.get("/pipes/node2", summary="获取管道终止节点", description="获取指定管道的终止节点ID")
|
||||||
async def fastapi_get_pipe_node2(
|
def fastapi_get_pipe_node2(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID")
|
pipe: str = Query(..., description="管道ID")
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
@@ -125,7 +125,7 @@ async def fastapi_get_pipe_node2(
|
|||||||
return ps["node2"]
|
return ps["node2"]
|
||||||
|
|
||||||
@router.get("/pipes/length", summary="获取管道长度", description="获取指定管道的长度")
|
@router.get("/pipes/length", summary="获取管道长度", description="获取指定管道的长度")
|
||||||
async def fastapi_get_pipe_length(
|
def fastapi_get_pipe_length(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID")
|
pipe: str = Query(..., description="管道ID")
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
@@ -143,7 +143,7 @@ async def fastapi_get_pipe_length(
|
|||||||
return ps["length"]
|
return ps["length"]
|
||||||
|
|
||||||
@router.get("/pipes/diameter", summary="获取管道管径", description="获取指定管道的管径")
|
@router.get("/pipes/diameter", summary="获取管道管径", description="获取指定管道的管径")
|
||||||
async def fastapi_get_pipe_diameter(
|
def fastapi_get_pipe_diameter(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID")
|
pipe: str = Query(..., description="管道ID")
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
@@ -161,7 +161,7 @@ async def fastapi_get_pipe_diameter(
|
|||||||
return ps["diameter"]
|
return ps["diameter"]
|
||||||
|
|
||||||
@router.get("/pipes/roughness", summary="获取管道粗糙度", description="获取指定管道的粗糙度")
|
@router.get("/pipes/roughness", summary="获取管道粗糙度", description="获取指定管道的粗糙度")
|
||||||
async def fastapi_get_pipe_roughness(
|
def fastapi_get_pipe_roughness(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID")
|
pipe: str = Query(..., description="管道ID")
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
@@ -179,7 +179,7 @@ async def fastapi_get_pipe_roughness(
|
|||||||
return ps["roughness"]
|
return ps["roughness"]
|
||||||
|
|
||||||
@router.get("/pipes/minor-loss", summary="获取管道局部阻力系数", description="获取指定管道的局部阻力系数")
|
@router.get("/pipes/minor-loss", summary="获取管道局部阻力系数", description="获取指定管道的局部阻力系数")
|
||||||
async def fastapi_get_pipe_minor_loss(
|
def fastapi_get_pipe_minor_loss(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID")
|
pipe: str = Query(..., description="管道ID")
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
@@ -197,7 +197,7 @@ async def fastapi_get_pipe_minor_loss(
|
|||||||
return ps["minor_loss"]
|
return ps["minor_loss"]
|
||||||
|
|
||||||
@router.get("/pipes/status", summary="获取管道状态", description="获取指定管道的状态(开启或关闭)")
|
@router.get("/pipes/status", summary="获取管道状态", description="获取指定管道的状态(开启或关闭)")
|
||||||
async def fastapi_get_pipe_status(
|
def fastapi_get_pipe_status(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID")
|
pipe: str = Query(..., description="管道ID")
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
@@ -215,7 +215,7 @@ async def fastapi_get_pipe_status(
|
|||||||
return ps["status"]
|
return ps["status"]
|
||||||
|
|
||||||
@router.patch("/pipes/node1", response_model=None, summary="设置管道起始节点", description="设置指定管道的起始节点")
|
@router.patch("/pipes/node1", response_model=None, summary="设置管道起始节点", description="设置指定管道的起始节点")
|
||||||
async def fastapi_set_pipe_node1(
|
def fastapi_set_pipe_node1(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID"),
|
pipe: str = Query(..., description="管道ID"),
|
||||||
node1: str = Query(..., description="新的起始节点ID")
|
node1: str = Query(..., description="新的起始节点ID")
|
||||||
@@ -235,7 +235,7 @@ async def fastapi_set_pipe_node1(
|
|||||||
return set_pipe(network, ChangeSet(ps))
|
return set_pipe(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/pipes/node2", response_model=None, summary="设置管道终止节点", description="设置指定管道的终止节点")
|
@router.patch("/pipes/node2", response_model=None, summary="设置管道终止节点", description="设置指定管道的终止节点")
|
||||||
async def fastapi_set_pipe_node2(
|
def fastapi_set_pipe_node2(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID"),
|
pipe: str = Query(..., description="管道ID"),
|
||||||
node2: str = Query(..., description="新的终止节点ID")
|
node2: str = Query(..., description="新的终止节点ID")
|
||||||
@@ -255,7 +255,7 @@ async def fastapi_set_pipe_node2(
|
|||||||
return set_pipe(network, ChangeSet(ps))
|
return set_pipe(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/pipes/length", response_model=None, summary="设置管道长度", description="设置指定管道的长度")
|
@router.patch("/pipes/length", response_model=None, summary="设置管道长度", description="设置指定管道的长度")
|
||||||
async def fastapi_set_pipe_length(
|
def fastapi_set_pipe_length(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID"),
|
pipe: str = Query(..., description="管道ID"),
|
||||||
length: float = Query(..., description="新的管道长度(单位:米)")
|
length: float = Query(..., description="新的管道长度(单位:米)")
|
||||||
@@ -275,7 +275,7 @@ async def fastapi_set_pipe_length(
|
|||||||
return set_pipe(network, ChangeSet(ps))
|
return set_pipe(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/pipes/diameter", response_model=None, summary="设置管道管径", description="设置指定管道的管径")
|
@router.patch("/pipes/diameter", response_model=None, summary="设置管道管径", description="设置指定管道的管径")
|
||||||
async def fastapi_set_pipe_diameter(
|
def fastapi_set_pipe_diameter(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID"),
|
pipe: str = Query(..., description="管道ID"),
|
||||||
diameter: float = Query(..., description="新的管道管径(单位:毫米)")
|
diameter: float = Query(..., description="新的管道管径(单位:毫米)")
|
||||||
@@ -295,7 +295,7 @@ async def fastapi_set_pipe_diameter(
|
|||||||
return set_pipe(network, ChangeSet(ps))
|
return set_pipe(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/pipes/roughness", response_model=None, summary="设置管道粗糙度", description="设置指定管道的粗糙度")
|
@router.patch("/pipes/roughness", response_model=None, summary="设置管道粗糙度", description="设置指定管道的粗糙度")
|
||||||
async def fastapi_set_pipe_roughness(
|
def fastapi_set_pipe_roughness(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID"),
|
pipe: str = Query(..., description="管道ID"),
|
||||||
roughness: float = Query(..., description="新的管道粗糙度值")
|
roughness: float = Query(..., description="新的管道粗糙度值")
|
||||||
@@ -315,7 +315,7 @@ async def fastapi_set_pipe_roughness(
|
|||||||
return set_pipe(network, ChangeSet(ps))
|
return set_pipe(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/pipes/minor-loss", response_model=None, summary="设置管道局部阻力系数", description="设置指定管道的局部阻力系数")
|
@router.patch("/pipes/minor-loss", response_model=None, summary="设置管道局部阻力系数", description="设置指定管道的局部阻力系数")
|
||||||
async def fastapi_set_pipe_minor_loss(
|
def fastapi_set_pipe_minor_loss(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID"),
|
pipe: str = Query(..., description="管道ID"),
|
||||||
minor_loss: float = Query(..., description="新的局部阻力系数值")
|
minor_loss: float = Query(..., description="新的局部阻力系数值")
|
||||||
@@ -335,7 +335,7 @@ async def fastapi_set_pipe_minor_loss(
|
|||||||
return set_pipe(network, ChangeSet(ps))
|
return set_pipe(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/pipes/status", response_model=None, summary="设置管道状态", description="设置指定管道的状态(开启或关闭)")
|
@router.patch("/pipes/status", response_model=None, summary="设置管道状态", description="设置指定管道的状态(开启或关闭)")
|
||||||
async def fastapi_set_pipe_status(
|
def fastapi_set_pipe_status(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID"),
|
pipe: str = Query(..., description="管道ID"),
|
||||||
status: str = Query(..., description="新的管道状态(开启/关闭)")
|
status: str = Query(..., description="新的管道状态(开启/关闭)")
|
||||||
@@ -355,7 +355,7 @@ async def fastapi_set_pipe_status(
|
|||||||
return set_pipe(network, ChangeSet(ps))
|
return set_pipe(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/pipes/properties", summary="获取管道属性", description="获取指定管道的所有属性信息")
|
@router.get("/pipes/properties", summary="获取管道属性", description="获取指定管道的所有属性信息")
|
||||||
async def fastapi_get_pipe_properties(
|
def fastapi_get_pipe_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID")
|
pipe: str = Query(..., description="管道ID")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -372,7 +372,7 @@ async def fastapi_get_pipe_properties(
|
|||||||
return get_pipe(network, pipe)
|
return get_pipe(network, pipe)
|
||||||
|
|
||||||
@router.get("/pipes", summary="获取所有管道属性", description="获取网络中所有管道的属性信息列表")
|
@router.get("/pipes", summary="获取所有管道属性", description="获取网络中所有管道的属性信息列表")
|
||||||
async def fastapi_get_all_pipe_properties(
|
def fastapi_get_all_pipe_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -385,15 +385,14 @@ async def fastapi_get_all_pipe_properties(
|
|||||||
包含所有管道属性的字典列表
|
包含所有管道属性的字典列表
|
||||||
"""
|
"""
|
||||||
# 缓存查询结果提高性能
|
# 缓存查询结果提高性能
|
||||||
# global redis_client
|
|
||||||
results = get_all_pipes(network)
|
results = get_all_pipes(network)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@router.patch("/pipes/properties", response_model=None, summary="设置管道属性", description="批量设置指定管道的多个属性")
|
@router.patch("/pipes/properties", response_model=None, summary="设置管道属性", description="批量设置指定管道的多个属性")
|
||||||
async def fastapi_set_pipe_properties(
|
def fastapi_set_pipe_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pipe: str = Query(..., description="管道ID"),
|
pipe: str = Query(..., description="管道ID"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""
|
"""
|
||||||
批量设置管道属性。
|
批量设置管道属性。
|
||||||
@@ -406,6 +405,6 @@ async def fastapi_set_pipe_properties(
|
|||||||
Returns:
|
Returns:
|
||||||
ChangeSet对象,包含本次修改的变更信息
|
ChangeSet对象,包含本次修改的变更信息
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
ps = {"id": pipe} | props
|
ps = {"id": pipe} | props
|
||||||
return set_pipe(network, ChangeSet(ps))
|
return set_pipe(network, ChangeSet(ps))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, Request, Query, Path, Body
|
from typing import Any
|
||||||
from typing import Any, List, Dict, Union
|
|
||||||
|
from fastapi import APIRouter, Body, Query
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
Any,
|
|
||||||
ChangeSet,
|
ChangeSet,
|
||||||
add_pump,
|
add_pump,
|
||||||
delete_pump,
|
delete_pump,
|
||||||
@@ -14,7 +14,7 @@ from app.services.tjnetwork import (
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/network-schemas/pump", summary="获取水泵模式", description="获取水泵对象的模式定义,包含所有可用字段及其类型")
|
@router.get("/network-schemas/pump", summary="获取水泵模式", description="获取水泵对象的模式定义,包含所有可用字段及其类型")
|
||||||
async def fastapi_get_pump_schema(
|
def fastapi_get_pump_schema(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -29,7 +29,7 @@ async def fastapi_get_pump_schema(
|
|||||||
return get_pump_schema(network)
|
return get_pump_schema(network)
|
||||||
|
|
||||||
@router.post("/pumps", response_model=None, summary="添加水泵", description="向网络中添加新的水泵,需要提供水泵的基本参数如功率等")
|
@router.post("/pumps", response_model=None, summary="添加水泵", description="向网络中添加新的水泵,需要提供水泵的基本参数如功率等")
|
||||||
async def fastapi_add_pump(
|
def fastapi_add_pump(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="水泵标识符"),
|
pump: str = Query(..., description="水泵标识符"),
|
||||||
node1: str = Query(..., description="水泵起始节点ID"),
|
node1: str = Query(..., description="水泵起始节点ID"),
|
||||||
@@ -53,7 +53,7 @@ async def fastapi_add_pump(
|
|||||||
return add_pump(network, ChangeSet(ps))
|
return add_pump(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.delete("/pumps", response_model=None, summary="删除水泵", description="从网络中删除指定的水泵")
|
@router.delete("/pumps", response_model=None, summary="删除水泵", description="从网络中删除指定的水泵")
|
||||||
async def fastapi_delete_pump(
|
def fastapi_delete_pump(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="要删除的水泵ID")
|
pump: str = Query(..., description="要删除的水泵ID")
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
@@ -71,7 +71,7 @@ async def fastapi_delete_pump(
|
|||||||
return delete_pump(network, ChangeSet(ps))
|
return delete_pump(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/pumps/node1", summary="获取水泵起始节点", description="获取指定水泵的起始节点ID")
|
@router.get("/pumps/node1", summary="获取水泵起始节点", description="获取指定水泵的起始节点ID")
|
||||||
async def fastapi_get_pump_node1(
|
def fastapi_get_pump_node1(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="水泵ID")
|
pump: str = Query(..., description="水泵ID")
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
@@ -89,7 +89,7 @@ async def fastapi_get_pump_node1(
|
|||||||
return ps["node1"]
|
return ps["node1"]
|
||||||
|
|
||||||
@router.get("/pumps/node2", summary="获取水泵终止节点", description="获取指定水泵的终止节点ID")
|
@router.get("/pumps/node2", summary="获取水泵终止节点", description="获取指定水泵的终止节点ID")
|
||||||
async def fastapi_get_pump_node2(
|
def fastapi_get_pump_node2(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="水泵ID")
|
pump: str = Query(..., description="水泵ID")
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
@@ -107,7 +107,7 @@ async def fastapi_get_pump_node2(
|
|||||||
return ps["node2"]
|
return ps["node2"]
|
||||||
|
|
||||||
@router.patch("/pumps/node1", response_model=None, summary="设置水泵起始节点", description="设置指定水泵的起始节点")
|
@router.patch("/pumps/node1", response_model=None, summary="设置水泵起始节点", description="设置指定水泵的起始节点")
|
||||||
async def fastapi_set_pump_node1(
|
def fastapi_set_pump_node1(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="水泵ID"),
|
pump: str = Query(..., description="水泵ID"),
|
||||||
node1: str = Query(..., description="新的起始节点ID")
|
node1: str = Query(..., description="新的起始节点ID")
|
||||||
@@ -127,7 +127,7 @@ async def fastapi_set_pump_node1(
|
|||||||
return set_pump(network, ChangeSet(ps))
|
return set_pump(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/pumps/node2", response_model=None, summary="设置水泵终止节点", description="设置指定水泵的终止节点")
|
@router.patch("/pumps/node2", response_model=None, summary="设置水泵终止节点", description="设置指定水泵的终止节点")
|
||||||
async def fastapi_set_pump_node2(
|
def fastapi_set_pump_node2(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="水泵ID"),
|
pump: str = Query(..., description="水泵ID"),
|
||||||
node2: str = Query(..., description="新的终止节点ID")
|
node2: str = Query(..., description="新的终止节点ID")
|
||||||
@@ -147,7 +147,7 @@ async def fastapi_set_pump_node2(
|
|||||||
return set_pump(network, ChangeSet(ps))
|
return set_pump(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/pumps/properties", summary="获取水泵属性", description="获取指定水泵的所有属性信息")
|
@router.get("/pumps/properties", summary="获取水泵属性", description="获取指定水泵的所有属性信息")
|
||||||
async def fastapi_get_pump_properties(
|
def fastapi_get_pump_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="水泵ID")
|
pump: str = Query(..., description="水泵ID")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -164,7 +164,7 @@ async def fastapi_get_pump_properties(
|
|||||||
return get_pump(network, pump)
|
return get_pump(network, pump)
|
||||||
|
|
||||||
@router.get("/pumps", summary="获取所有水泵属性", description="获取网络中所有水泵的属性信息列表")
|
@router.get("/pumps", summary="获取所有水泵属性", description="获取网络中所有水泵的属性信息列表")
|
||||||
async def fastapi_get_all_pump_properties(
|
def fastapi_get_all_pump_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -177,15 +177,14 @@ async def fastapi_get_all_pump_properties(
|
|||||||
包含所有水泵属性的字典列表
|
包含所有水泵属性的字典列表
|
||||||
"""
|
"""
|
||||||
# 缓存查询结果提高性能
|
# 缓存查询结果提高性能
|
||||||
# global redis_client
|
|
||||||
results = get_all_pumps(network)
|
results = get_all_pumps(network)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@router.patch("/pumps/properties", response_model=None, summary="设置水泵属性", description="批量设置指定水泵的多个属性")
|
@router.patch("/pumps/properties", response_model=None, summary="设置水泵属性", description="批量设置指定水泵的多个属性")
|
||||||
async def fastapi_set_pump_properties(
|
def fastapi_set_pump_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
pump: str = Query(..., description="水泵ID"),
|
pump: str = Query(..., description="水泵ID"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""
|
"""
|
||||||
批量设置水泵属性。
|
批量设置水泵属性。
|
||||||
@@ -198,6 +197,6 @@ async def fastapi_set_pump_properties(
|
|||||||
Returns:
|
Returns:
|
||||||
ChangeSet对象,包含本次修改的变更信息
|
ChangeSet对象,包含本次修改的变更信息
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
ps = {"id": pump} | props
|
ps = {"id": pump} | props
|
||||||
return set_pump(network, ChangeSet(ps))
|
return set_pump(network, ChangeSet(ps))
|
||||||
|
|||||||
@@ -1,534 +1,72 @@
|
|||||||
from fastapi import APIRouter, Request, Query, Path, Body
|
from typing import Any
|
||||||
from typing import Any, List, Dict, Union
|
|
||||||
|
from fastapi import APIRouter, Body, Query
|
||||||
|
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
Any,
|
|
||||||
ChangeSet,
|
ChangeSet,
|
||||||
add_district_metering_area,
|
|
||||||
add_region,
|
add_region,
|
||||||
add_service_area,
|
|
||||||
add_virtual_district,
|
|
||||||
calculate_district_metering_area_for_network,
|
|
||||||
calculate_district_metering_area_for_nodes,
|
|
||||||
calculate_district_metering_area_for_region,
|
|
||||||
calculate_service_area,
|
|
||||||
calculate_virtual_district,
|
|
||||||
delete_district_metering_area,
|
|
||||||
delete_region,
|
delete_region,
|
||||||
delete_service_area,
|
get_nodes_in_region,
|
||||||
delete_virtual_district,
|
|
||||||
generate_district_metering_area,
|
|
||||||
generate_service_area,
|
|
||||||
generate_sub_district_metering_area,
|
|
||||||
generate_virtual_district,
|
|
||||||
get_all_district_metering_area_ids,
|
|
||||||
get_all_district_metering_areas,
|
|
||||||
get_all_service_areas,
|
|
||||||
get_all_virtual_districts,
|
|
||||||
get_district_metering_area,
|
|
||||||
get_district_metering_area_schema,
|
|
||||||
get_region,
|
get_region,
|
||||||
get_region_schema,
|
get_region_schema,
|
||||||
get_service_area,
|
get_regions,
|
||||||
get_service_area_schema,
|
|
||||||
get_virtual_district,
|
|
||||||
get_virtual_district_schema,
|
|
||||||
set_district_metering_area,
|
|
||||||
set_region,
|
set_region,
|
||||||
set_service_area,
|
|
||||||
set_virtual_district,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
############################################################
|
|
||||||
# region 32
|
|
||||||
############################################################
|
|
||||||
|
|
||||||
@router.get(
|
@router.get("/network-schemas/region", summary="获取区域属性架构")
|
||||||
"/network-schemas/region",
|
def get_region_schema_endpoint(
|
||||||
summary="获取区域属性架构",
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
description="获取指定水网的区域属性架构定义"
|
|
||||||
)
|
|
||||||
async def fastapi_get_region_schema(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
"""获取区域的属性架构。"""
|
|
||||||
return get_region_schema(network)
|
return get_region_schema(network)
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/regions/detail",
|
@router.get("/regions", summary="获取区域列表")
|
||||||
summary="获取区域信息",
|
def get_regions_endpoint(
|
||||||
description="获取指定ID的区域详细信息"
|
|
||||||
)
|
|
||||||
async def fastapi_get_region(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
id: str = Query(..., description="区域ID")
|
) -> list[dict[str, Any]]:
|
||||||
|
return [get_region(network, region_id) for region_id in get_regions(network)]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/regions/detail", summary="获取区域信息")
|
||||||
|
def get_region_endpoint(
|
||||||
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
|
id: str = Query(..., description="区域 ID"),
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""获取区域的详细信息。"""
|
|
||||||
return get_region(network, id)
|
return get_region(network, id)
|
||||||
|
|
||||||
@router.patch(
|
|
||||||
"/regions",
|
@router.get("/regions/nodes", summary="获取区域节点")
|
||||||
response_model=None,
|
def get_region_nodes_endpoint(
|
||||||
summary="设置区域属性",
|
|
||||||
description="修改指定区域的属性信息"
|
|
||||||
)
|
|
||||||
async def fastapi_set_region(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
id: str = Query(..., description="区域 ID"),
|
||||||
) -> ChangeSet:
|
|
||||||
"""设置区域属性。"""
|
|
||||||
props = await req.json()
|
|
||||||
return set_region(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/regions",
|
|
||||||
response_model=None,
|
|
||||||
summary="添加新区域",
|
|
||||||
description="向水网添加一个新的区域"
|
|
||||||
)
|
|
||||||
async def fastapi_add_region(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""添加新的区域。"""
|
|
||||||
props = await req.json()
|
|
||||||
return add_region(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.delete(
|
|
||||||
"/regions",
|
|
||||||
response_model=None,
|
|
||||||
summary="删除区域",
|
|
||||||
description="删除指定的区域"
|
|
||||||
)
|
|
||||||
async def fastapi_delete_region(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""删除区域。"""
|
|
||||||
props = await req.json()
|
|
||||||
return delete_region(network, ChangeSet(props))
|
|
||||||
|
|
||||||
|
|
||||||
############################################################
|
|
||||||
# district_metering_area 33
|
|
||||||
############################################################
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/district-metering-areas/for-region",
|
|
||||||
summary="计算区域内DMA分区",
|
|
||||||
description="为指定区域计算区域计量(DMA)分区方案"
|
|
||||||
)
|
|
||||||
async def fastapi_calculate_district_metering_area_for_region(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> list[list[str]]:
|
|
||||||
"""
|
|
||||||
计算区域内DMA分区。
|
|
||||||
|
|
||||||
请求体格式:
|
|
||||||
{
|
|
||||||
"region": 区域ID(str),
|
|
||||||
"part_count": 分区数量(int),
|
|
||||||
"part_type": 分区类型(int)
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
props = await req.json()
|
|
||||||
region = props["region"]
|
|
||||||
part_count = props["part_count"]
|
|
||||||
part_type = props["part_type"]
|
|
||||||
return calculate_district_metering_area_for_region(
|
|
||||||
network, region, part_count, part_type
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/district-metering-areas/for-network",
|
|
||||||
summary="计算整网DMA分区",
|
|
||||||
description="为整个水网计算区域计量(DMA)分区方案"
|
|
||||||
)
|
|
||||||
async def fastapi_calculate_district_metering_area_for_network(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> list[list[str]]:
|
|
||||||
"""
|
|
||||||
计算整网DMA分区。
|
|
||||||
|
|
||||||
请求体格式:
|
|
||||||
{
|
|
||||||
"part_count": 分区数量(int),
|
|
||||||
"part_type": 分区类型(int)
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
props = await req.json()
|
|
||||||
part_count = props["part_count"]
|
|
||||||
part_type = props["part_type"]
|
|
||||||
return calculate_district_metering_area_for_network(network, part_count, part_type)
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/network-schemas/district-metering-area",
|
|
||||||
summary="获取DMA属性架构",
|
|
||||||
description="获取指定水网的区域计量(DMA)属性架构定义"
|
|
||||||
)
|
|
||||||
async def fastapi_get_district_metering_area_schema(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
) -> dict[str, dict[str, Any]]:
|
|
||||||
"""获取DMA的属性架构。"""
|
|
||||||
return get_district_metering_area_schema(network)
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/district-metering-areas/detail",
|
|
||||||
summary="获取DMA信息",
|
|
||||||
description="获取指定ID的区域计量(DMA)详细信息"
|
|
||||||
)
|
|
||||||
async def fastapi_get_district_metering_area(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
id: str = Query(..., description="DMA ID")
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""获取DMA的详细信息。"""
|
|
||||||
return get_district_metering_area(network, id)
|
|
||||||
|
|
||||||
@router.patch(
|
|
||||||
"/district-metering-areas",
|
|
||||||
response_model=None,
|
|
||||||
summary="设置DMA属性",
|
|
||||||
description="修改指定DMA的属性信息"
|
|
||||||
)
|
|
||||||
async def fastapi_set_district_metering_area(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""设置DMA属性。"""
|
|
||||||
props = await req.json()
|
|
||||||
return set_district_metering_area(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/district-metering-areas",
|
|
||||||
response_model=None,
|
|
||||||
summary="添加新DMA",
|
|
||||||
description="向水网添加一个新的区域计量(DMA)"
|
|
||||||
)
|
|
||||||
async def fastapi_add_district_metering_area(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""添加新的DMA。"""
|
|
||||||
props = await req.json()
|
|
||||||
# boundary should be [(x,y), (x,y)]
|
|
||||||
boundary = props.get("boundary", [])
|
|
||||||
newBoundary = []
|
|
||||||
for pt in boundary:
|
|
||||||
if len(pt) >= 2:
|
|
||||||
newBoundary.append((pt[0], pt[1]))
|
|
||||||
props["boundary"] = newBoundary
|
|
||||||
return add_district_metering_area(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.delete(
|
|
||||||
"/district-metering-areas",
|
|
||||||
response_model=None,
|
|
||||||
summary="删除DMA",
|
|
||||||
description="删除指定的区域计量(DMA)"
|
|
||||||
)
|
|
||||||
async def fastapi_delete_district_metering_area(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""删除DMA。"""
|
|
||||||
props = await req.json()
|
|
||||||
return delete_district_metering_area(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/district-metering-areas/ids",
|
|
||||||
summary="获取所有DMA ID",
|
|
||||||
description="获取指定水网中所有DMA的ID列表"
|
|
||||||
)
|
|
||||||
async def fastapi_get_all_district_metering_area_ids(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""获取所有DMA的ID列表。"""
|
return get_nodes_in_region(network, id)
|
||||||
return get_all_district_metering_area_ids(network)
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/district-metering-areas",
|
|
||||||
summary="获取所有DMA",
|
|
||||||
description="获取指定水网中所有DMA的详细信息"
|
|
||||||
)
|
|
||||||
async def getalldistrictmeteringareas(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""获取所有DMA的详细信息列表。"""
|
|
||||||
return get_all_district_metering_areas(network)
|
|
||||||
|
|
||||||
@router.post(
|
@router.patch("/regions", summary="修改区域", response_model=None)
|
||||||
"/district-metering-area-generation-runs",
|
def set_region_endpoint(
|
||||||
response_model=None,
|
|
||||||
summary="生成DMA分区",
|
|
||||||
description="根据参数自动生成水网的DMA分区方案"
|
|
||||||
)
|
|
||||||
async def fastapi_generate_district_metering_area(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
part_count: int = Query(..., description="分区数量", gt=0),
|
payload: dict[str, Any] = Body(...),
|
||||||
part_type: int = Query(..., description="分区类型"),
|
|
||||||
inflate_delta: float = Query(..., description="膨胀参数")
|
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""生成DMA分区。"""
|
return set_region(network, ChangeSet(payload))
|
||||||
return generate_district_metering_area(
|
|
||||||
network, part_count, part_type, inflate_delta
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/sub-district-metering-areas",
|
@router.post("/regions", summary="添加区域", response_model=None)
|
||||||
response_model=None,
|
def add_region_endpoint(
|
||||||
summary="生成DMA子分区",
|
|
||||||
description="为指定DMA生成子DMA分区"
|
|
||||||
)
|
|
||||||
async def fastapi_generate_sub_district_metering_area(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
dma: str = Query(..., description="DMA ID"),
|
payload: dict[str, Any] = Body(...),
|
||||||
part_count: int = Query(..., description="分区数量", gt=0),
|
|
||||||
part_type: int = Query(..., description="分区类型"),
|
|
||||||
inflate_delta: float = Query(..., description="膨胀参数")
|
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""生成DMA子分区。"""
|
payload["boundary"] = [tuple(point[:2]) for point in payload.get("boundary", [])]
|
||||||
return generate_sub_district_metering_area(
|
return add_region(network, ChangeSet(payload))
|
||||||
network, dma, part_count, part_type, inflate_delta
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
############################################################
|
@router.delete("/regions", summary="删除区域", response_model=None)
|
||||||
# service_area 34
|
def delete_region_endpoint(
|
||||||
############################################################
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/service-area-calculations",
|
|
||||||
summary="计算服务区",
|
|
||||||
description="计算指定水网的服务区分区,返回全部时间步结果"
|
|
||||||
)
|
|
||||||
async def fastapi_calculate_service_area(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
) -> list[dict[str, list[str]]]:
|
payload: dict[str, Any] = Body(...),
|
||||||
"""计算服务区分区,返回全部时间步结果。"""
|
|
||||||
return calculate_service_area(network)
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/network-schemas/service-area",
|
|
||||||
summary="获取服务区属性架构",
|
|
||||||
description="获取指定水网的服务区属性架构定义"
|
|
||||||
)
|
|
||||||
async def fastapi_get_service_area_schema(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
) -> dict[str, dict[str, Any]]:
|
|
||||||
"""获取服务区的属性架构。"""
|
|
||||||
return get_service_area_schema(network)
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/service-areas/detail",
|
|
||||||
summary="获取服务区信息",
|
|
||||||
description="获取指定ID的服务区详细信息"
|
|
||||||
)
|
|
||||||
async def fastapi_get_service_area(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
id: str = Query(..., description="服务区ID")
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""获取服务区的详细信息。"""
|
|
||||||
return get_service_area(network, id)
|
|
||||||
|
|
||||||
@router.patch(
|
|
||||||
"/service-areas",
|
|
||||||
response_model=None,
|
|
||||||
summary="设置服务区属性",
|
|
||||||
description="修改指定服务区的属性信息"
|
|
||||||
)
|
|
||||||
async def fastapi_set_service_area(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置服务区属性。"""
|
return delete_region(network, ChangeSet(payload))
|
||||||
props = await req.json()
|
|
||||||
return set_service_area(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/service-areas",
|
|
||||||
response_model=None,
|
|
||||||
summary="添加新服务区",
|
|
||||||
description="向水网添加一个新的服务区"
|
|
||||||
)
|
|
||||||
async def fastapi_add_service_area(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""添加新的服务区。"""
|
|
||||||
props = await req.json()
|
|
||||||
return add_service_area(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.delete(
|
|
||||||
"/service-areas",
|
|
||||||
response_model=None,
|
|
||||||
summary="删除服务区",
|
|
||||||
description="删除指定的服务区"
|
|
||||||
)
|
|
||||||
async def fastapi_delete_service_area(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""删除服务区。"""
|
|
||||||
props = await req.json()
|
|
||||||
return delete_service_area(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/service-areas",
|
|
||||||
summary="获取所有服务区",
|
|
||||||
description="获取指定水网中的所有服务区信息"
|
|
||||||
)
|
|
||||||
async def fastapi_get_all_service_areas(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""获取所有服务区的信息列表。"""
|
|
||||||
return get_all_service_areas(network)
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/service-area-generation-runs",
|
|
||||||
response_model=None,
|
|
||||||
summary="生成服务区分区",
|
|
||||||
description="根据参数自动生成水网的服务区分区"
|
|
||||||
)
|
|
||||||
async def fastapi_generate_service_area(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
inflate_delta: float = Query(..., description="膨胀参数")
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""生成服务区分区。"""
|
|
||||||
return generate_service_area(network, inflate_delta)
|
|
||||||
|
|
||||||
|
|
||||||
############################################################
|
|
||||||
# virtual_district 35
|
|
||||||
############################################################
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/virtual-district-calculations",
|
|
||||||
summary="计算虚拟分区",
|
|
||||||
description="根据指定的压力监测节点作为中心节点计算虚拟分区方案"
|
|
||||||
)
|
|
||||||
async def fastapi_calculate_virtual_district(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
centers: list[str] = Query(..., description="压力监测节点ID列表")
|
|
||||||
) -> dict[str, list[Any]]:
|
|
||||||
"""计算虚拟分区。"""
|
|
||||||
return calculate_virtual_district(network, centers)
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/network-schemas/virtual-district",
|
|
||||||
summary="获取虚拟分区属性架构",
|
|
||||||
description="获取指定水网的虚拟分区属性架构定义"
|
|
||||||
)
|
|
||||||
async def fastapi_get_virtual_district_schema(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
) -> dict[str, dict[str, Any]]:
|
|
||||||
"""获取虚拟分区的属性架构。"""
|
|
||||||
return get_virtual_district_schema(network)
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/virtual-districts/detail",
|
|
||||||
summary="获取虚拟分区信息",
|
|
||||||
description="获取指定ID的虚拟分区详细信息"
|
|
||||||
)
|
|
||||||
async def fastapi_get_virtual_district(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
id: str = Query(..., description="虚拟分区ID")
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""获取虚拟分区的详细信息。"""
|
|
||||||
return get_virtual_district(network, id)
|
|
||||||
|
|
||||||
@router.patch(
|
|
||||||
"/virtual-districts",
|
|
||||||
response_model=None,
|
|
||||||
summary="设置虚拟分区属性",
|
|
||||||
description="修改指定虚拟分区的属性信息"
|
|
||||||
)
|
|
||||||
async def fastapi_set_virtual_district(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""设置虚拟分区属性。"""
|
|
||||||
props = await req.json()
|
|
||||||
return set_virtual_district(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/virtual-districts",
|
|
||||||
response_model=None,
|
|
||||||
summary="添加新虚拟分区",
|
|
||||||
description="向水网添加一个新的虚拟分区"
|
|
||||||
)
|
|
||||||
async def fastapi_add_virtual_district(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""添加新的虚拟分区。"""
|
|
||||||
props = await req.json()
|
|
||||||
return add_virtual_district(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.delete(
|
|
||||||
"/virtual-districts",
|
|
||||||
response_model=None,
|
|
||||||
summary="删除虚拟分区",
|
|
||||||
description="删除指定的虚拟分区"
|
|
||||||
)
|
|
||||||
async def fastapi_delete_virtual_district(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""删除虚拟分区。"""
|
|
||||||
props = await req.json()
|
|
||||||
return delete_virtual_district(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/virtual-districts",
|
|
||||||
summary="获取所有虚拟分区",
|
|
||||||
description="获取指定水网中的所有虚拟分区信息"
|
|
||||||
)
|
|
||||||
async def fastapi_get_all_virtual_district(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""获取所有虚拟分区的信息列表。"""
|
|
||||||
return get_all_virtual_districts(network)
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/virtual-district-generation-runs",
|
|
||||||
response_model=None,
|
|
||||||
summary="生成虚拟分区",
|
|
||||||
description="根据参数自动生成虚拟分区方案"
|
|
||||||
)
|
|
||||||
async def fastapi_generate_virtual_district(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
inflate_delta: float = Query(..., description="膨胀参数"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""生成虚拟分区。"""
|
|
||||||
props = await req.json()
|
|
||||||
return generate_virtual_district(network, props["centers"], inflate_delta)
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/district-metering-areas/for-nodes",
|
|
||||||
summary="计算节点DMA分区",
|
|
||||||
description="为指定节点集计算区域计量(DMA)分区方案"
|
|
||||||
)
|
|
||||||
async def fastapi_calculate_district_metering_area_for_nodes(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> list[list[str]]:
|
|
||||||
"""
|
|
||||||
计算节点DMA分区。
|
|
||||||
|
|
||||||
请求体格式:
|
|
||||||
{
|
|
||||||
"nodes": 节点ID列表(list[str]),
|
|
||||||
"part_count": 分区数量(int),
|
|
||||||
"part_type": 分区类型(int)
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
props = await req.json()
|
|
||||||
nodes = props["nodes"]
|
|
||||||
part_count = props["part_count"]
|
|
||||||
part_type = props["part_type"]
|
|
||||||
return calculate_district_metering_area_for_nodes(
|
|
||||||
network, nodes, part_count, part_type
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, Request, Query, Path, Body
|
from typing import Any
|
||||||
from typing import Any, List, Dict, Union
|
|
||||||
|
from fastapi import APIRouter, Body, Query
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
Any,
|
|
||||||
ChangeSet,
|
ChangeSet,
|
||||||
add_reservoir,
|
add_reservoir,
|
||||||
delete_reservoir,
|
delete_reservoir,
|
||||||
@@ -18,7 +18,7 @@ router = APIRouter()
|
|||||||
summary="获取水库模式",
|
summary="获取水库模式",
|
||||||
description="获取指定供水网络中所有水库的模式/属性字段定义"
|
description="获取指定供水网络中所有水库的模式/属性字段定义"
|
||||||
)
|
)
|
||||||
async def fast_get_reservoir_schema(
|
def fast_get_reservoir_schema(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -40,7 +40,7 @@ async def fast_get_reservoir_schema(
|
|||||||
summary="添加水库",
|
summary="添加水库",
|
||||||
description="在指定供水网络中添加新的水库/水源节点"
|
description="在指定供水网络中添加新的水库/水源节点"
|
||||||
)
|
)
|
||||||
async def fastapi_add_reservoir(
|
def fastapi_add_reservoir(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
reservoir: str = Query(..., description="水库的唯一标识符"),
|
reservoir: str = Query(..., description="水库的唯一标识符"),
|
||||||
x: float = Query(..., description="水库的X坐标"),
|
x: float = Query(..., description="水库的X坐标"),
|
||||||
@@ -71,7 +71,7 @@ async def fastapi_add_reservoir(
|
|||||||
summary="删除水库",
|
summary="删除水库",
|
||||||
description="从指定供水网络中删除指定的水库/水源节点"
|
description="从指定供水网络中删除指定的水库/水源节点"
|
||||||
)
|
)
|
||||||
async def fastapi_delete_reservoir(
|
def fastapi_delete_reservoir(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
reservoir: str = Query(..., description="要删除的水库的唯一标识符")
|
reservoir: str = Query(..., description="要删除的水库的唯一标识符")
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
@@ -95,7 +95,7 @@ async def fastapi_delete_reservoir(
|
|||||||
summary="获取水库水头",
|
summary="获取水库水头",
|
||||||
description="获取指定水库的供水水头/总水头值"
|
description="获取指定水库的供水水头/总水头值"
|
||||||
)
|
)
|
||||||
async def fastapi_get_reservoir_head(
|
def fastapi_get_reservoir_head(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
reservoir: str = Query(..., description="水库的唯一标识符")
|
reservoir: str = Query(..., description="水库的唯一标识符")
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
@@ -119,7 +119,7 @@ async def fastapi_get_reservoir_head(
|
|||||||
summary="获取水库模式",
|
summary="获取水库模式",
|
||||||
description="获取指定水库的运行模式/供水模式"
|
description="获取指定水库的运行模式/供水模式"
|
||||||
)
|
)
|
||||||
async def fastapi_get_reservoir_pattern(
|
def fastapi_get_reservoir_pattern(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
reservoir: str = Query(..., description="水库的唯一标识符")
|
reservoir: str = Query(..., description="水库的唯一标识符")
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
@@ -143,7 +143,7 @@ async def fastapi_get_reservoir_pattern(
|
|||||||
summary="获取水库X坐标",
|
summary="获取水库X坐标",
|
||||||
description="获取指定水库的X坐标位置"
|
description="获取指定水库的X坐标位置"
|
||||||
)
|
)
|
||||||
async def fastapi_get_reservoir_x(
|
def fastapi_get_reservoir_x(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
reservoir: str = Query(..., description="水库的唯一标识符")
|
reservoir: str = Query(..., description="水库的唯一标识符")
|
||||||
) -> dict[str, float] | None:
|
) -> dict[str, float] | None:
|
||||||
@@ -167,7 +167,7 @@ async def fastapi_get_reservoir_x(
|
|||||||
summary="获取水库Y坐标",
|
summary="获取水库Y坐标",
|
||||||
description="获取指定水库的Y坐标位置"
|
description="获取指定水库的Y坐标位置"
|
||||||
)
|
)
|
||||||
async def fastapi_get_reservoir_y(
|
def fastapi_get_reservoir_y(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
reservoir: str = Query(..., description="水库的唯一标识符")
|
reservoir: str = Query(..., description="水库的唯一标识符")
|
||||||
) -> dict[str, float] | None:
|
) -> dict[str, float] | None:
|
||||||
@@ -191,7 +191,7 @@ async def fastapi_get_reservoir_y(
|
|||||||
summary="获取水库坐标",
|
summary="获取水库坐标",
|
||||||
description="获取指定水库的平面坐标(X和Y坐标)"
|
description="获取指定水库的平面坐标(X和Y坐标)"
|
||||||
)
|
)
|
||||||
async def fastapi_get_reservoir_coord(
|
def fastapi_get_reservoir_coord(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
reservoir: str = Query(..., description="水库的唯一标识符")
|
reservoir: str = Query(..., description="水库的唯一标识符")
|
||||||
) -> dict[str, float] | None:
|
) -> dict[str, float] | None:
|
||||||
@@ -217,7 +217,7 @@ async def fastapi_get_reservoir_coord(
|
|||||||
summary="设置水库水头",
|
summary="设置水库水头",
|
||||||
description="更新指定水库的供水水头/总水头值"
|
description="更新指定水库的供水水头/总水头值"
|
||||||
)
|
)
|
||||||
async def fastapi_set_reservoir_head(
|
def fastapi_set_reservoir_head(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
reservoir: str = Query(..., description="水库的唯一标识符"),
|
reservoir: str = Query(..., description="水库的唯一标识符"),
|
||||||
head: float = Query(..., description="新的水头值(米)")
|
head: float = Query(..., description="新的水头值(米)")
|
||||||
@@ -244,7 +244,7 @@ async def fastapi_set_reservoir_head(
|
|||||||
summary="设置水库模式",
|
summary="设置水库模式",
|
||||||
description="更新指定水库的运行模式/供水模式"
|
description="更新指定水库的运行模式/供水模式"
|
||||||
)
|
)
|
||||||
async def fastapi_set_reservoir_pattern(
|
def fastapi_set_reservoir_pattern(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
reservoir: str = Query(..., description="水库的唯一标识符"),
|
reservoir: str = Query(..., description="水库的唯一标识符"),
|
||||||
pattern: str = Query(..., description="新的运行模式")
|
pattern: str = Query(..., description="新的运行模式")
|
||||||
@@ -271,7 +271,7 @@ async def fastapi_set_reservoir_pattern(
|
|||||||
summary="设置水库X坐标",
|
summary="设置水库X坐标",
|
||||||
description="更新指定水库的X坐标位置"
|
description="更新指定水库的X坐标位置"
|
||||||
)
|
)
|
||||||
async def fastapi_set_reservoir_x(
|
def fastapi_set_reservoir_x(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
reservoir: str = Query(..., description="水库的唯一标识符"),
|
reservoir: str = Query(..., description="水库的唯一标识符"),
|
||||||
x: float = Query(..., description="新的X坐标值")
|
x: float = Query(..., description="新的X坐标值")
|
||||||
@@ -298,7 +298,7 @@ async def fastapi_set_reservoir_x(
|
|||||||
summary="设置水库Y坐标",
|
summary="设置水库Y坐标",
|
||||||
description="更新指定水库的Y坐标位置"
|
description="更新指定水库的Y坐标位置"
|
||||||
)
|
)
|
||||||
async def fastapi_set_reservoir_y(
|
def fastapi_set_reservoir_y(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
reservoir: str = Query(..., description="水库的唯一标识符"),
|
reservoir: str = Query(..., description="水库的唯一标识符"),
|
||||||
y: float = Query(..., description="新的Y坐标值")
|
y: float = Query(..., description="新的Y坐标值")
|
||||||
@@ -325,7 +325,7 @@ async def fastapi_set_reservoir_y(
|
|||||||
summary="设置水库坐标",
|
summary="设置水库坐标",
|
||||||
description="更新指定水库的平面坐标(X和Y坐标)"
|
description="更新指定水库的平面坐标(X和Y坐标)"
|
||||||
)
|
)
|
||||||
async def fastapi_set_reservoir_coord(
|
def fastapi_set_reservoir_coord(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
reservoir: str = Query(..., description="水库的唯一标识符"),
|
reservoir: str = Query(..., description="水库的唯一标识符"),
|
||||||
x: float = Query(..., description="新的X坐标值"),
|
x: float = Query(..., description="新的X坐标值"),
|
||||||
@@ -353,7 +353,7 @@ async def fastapi_set_reservoir_coord(
|
|||||||
summary="获取水库属性",
|
summary="获取水库属性",
|
||||||
description="获取指定水库的所有属性"
|
description="获取指定水库的所有属性"
|
||||||
)
|
)
|
||||||
async def fastapi_get_reservoir_properties(
|
def fastapi_get_reservoir_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
reservoir: str = Query(..., description="水库的唯一标识符")
|
reservoir: str = Query(..., description="水库的唯一标识符")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -376,7 +376,7 @@ async def fastapi_get_reservoir_properties(
|
|||||||
summary="获取所有水库属性",
|
summary="获取所有水库属性",
|
||||||
description="获取指定供水网络中所有水库的属性"
|
description="获取指定供水网络中所有水库的属性"
|
||||||
)
|
)
|
||||||
async def fastapi_get_all_reservoir_properties(
|
def fastapi_get_all_reservoir_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -399,10 +399,10 @@ async def fastapi_get_all_reservoir_properties(
|
|||||||
summary="设置水库属性",
|
summary="设置水库属性",
|
||||||
description="批量更新指定水库的多个属性"
|
description="批量更新指定水库的多个属性"
|
||||||
)
|
)
|
||||||
async def fastapi_set_reservoir_properties(
|
def fastapi_set_reservoir_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
reservoir: str = Query(..., description="水库的唯一标识符"),
|
reservoir: str = Query(..., description="水库的唯一标识符"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""
|
"""
|
||||||
设置水库的多个属性。
|
设置水库的多个属性。
|
||||||
@@ -417,6 +417,6 @@ async def fastapi_set_reservoir_properties(
|
|||||||
Returns:
|
Returns:
|
||||||
包含操作变更集的ChangeSet对象
|
包含操作变更集的ChangeSet对象
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
ps = {"id": reservoir} | props
|
ps = {"id": reservoir} | props
|
||||||
return set_reservoir(network, ChangeSet(ps))
|
return set_reservoir(network, ChangeSet(ps))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, Request, Query, Path, Body
|
from typing import Any
|
||||||
from typing import Any, List, Dict, Union
|
|
||||||
|
from fastapi import APIRouter, Body, Query
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
Any,
|
|
||||||
ChangeSet,
|
ChangeSet,
|
||||||
get_tag,
|
get_tag,
|
||||||
get_tag_schema,
|
get_tag_schema,
|
||||||
@@ -20,7 +20,7 @@ router = APIRouter()
|
|||||||
summary="获取标签属性架构",
|
summary="获取标签属性架构",
|
||||||
description="获取指定水网的标签(Tag)属性架构定义"
|
description="获取指定水网的标签(Tag)属性架构定义"
|
||||||
)
|
)
|
||||||
async def fastapi_get_tag_schema(
|
def fastapi_get_tag_schema(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
"""获取标签的属性架构。"""
|
"""获取标签的属性架构。"""
|
||||||
@@ -31,7 +31,7 @@ async def fastapi_get_tag_schema(
|
|||||||
summary="获取标签信息",
|
summary="获取标签信息",
|
||||||
description="获取指定类型和ID的标签信息"
|
description="获取指定类型和ID的标签信息"
|
||||||
)
|
)
|
||||||
async def fastapi_get_tag(
|
def fastapi_get_tag(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
t_type: str = Query(..., description="标签类型"),
|
t_type: str = Query(..., description="标签类型"),
|
||||||
id: str = Query(..., description="元素ID")
|
id: str = Query(..., description="元素ID")
|
||||||
@@ -44,7 +44,7 @@ async def fastapi_get_tag(
|
|||||||
summary="获取所有标签",
|
summary="获取所有标签",
|
||||||
description="获取指定水网中的所有标签信息"
|
description="获取指定水网中的所有标签信息"
|
||||||
)
|
)
|
||||||
async def fastapi_get_tags(
|
def fastapi_get_tags(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""获取水网中所有标签的列表。"""
|
"""获取水网中所有标签的列表。"""
|
||||||
@@ -57,10 +57,10 @@ async def fastapi_get_tags(
|
|||||||
summary="设置标签",
|
summary="设置标签",
|
||||||
description="为指定元素设置或修改标签信息"
|
description="为指定元素设置或修改标签信息"
|
||||||
)
|
)
|
||||||
async def fastapi_set_tag(
|
def fastapi_set_tag(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""设置标签信息。"""
|
"""设置标签信息。"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
return set_tag(network, ChangeSet(props))
|
return set_tag(network, ChangeSet(props))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, Request, Query, Path, Body
|
from typing import Any
|
||||||
from typing import Any, List, Dict, Union
|
|
||||||
|
from fastapi import APIRouter, Body, Query
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
Any,
|
|
||||||
ChangeSet,
|
ChangeSet,
|
||||||
add_tank,
|
add_tank,
|
||||||
delete_tank,
|
delete_tank,
|
||||||
@@ -14,7 +14,7 @@ from app.services.tjnetwork import (
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/network-schemas/tank", summary="获取水箱模式", description="获取指定网络的水箱数据结构模式定义")
|
@router.get("/network-schemas/tank", summary="获取水箱模式", description="获取指定网络的水箱数据结构模式定义")
|
||||||
async def fast_get_tank_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
def fast_get_tank_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
获取水箱的数据结构模式。
|
获取水箱的数据结构模式。
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ async def fast_get_tank_schema(network: str = Query(..., description="管网名
|
|||||||
return get_tank_schema(network)
|
return get_tank_schema(network)
|
||||||
|
|
||||||
@router.post("/tanks", summary="新增水箱", description="向指定网络中新增一个水箱", response_model=None)
|
@router.post("/tanks", summary="新增水箱", description="向指定网络中新增一个水箱", response_model=None)
|
||||||
async def fastapi_add_tank(
|
def fastapi_add_tank(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
x: float = Query(..., description="X坐标"),
|
x: float = Query(..., description="X坐标"),
|
||||||
@@ -71,7 +71,7 @@ async def fastapi_add_tank(
|
|||||||
return add_tank(network, ChangeSet(ps))
|
return add_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.delete("/tanks", summary="删除水箱", description="删除指定网络中的水箱", response_model=None)
|
@router.delete("/tanks", summary="删除水箱", description="删除指定网络中的水箱", response_model=None)
|
||||||
async def fastapi_delete_tank(
|
def fastapi_delete_tank(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
@@ -89,7 +89,7 @@ async def fastapi_delete_tank(
|
|||||||
return delete_tank(network, ChangeSet(ps))
|
return delete_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/tanks/elevation", summary="获取水箱标高", description="获取指定水箱的标高值")
|
@router.get("/tanks/elevation", summary="获取水箱标高", description="获取指定水箱的标高值")
|
||||||
async def fastapi_get_tank_elevation(
|
def fastapi_get_tank_elevation(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
@@ -107,7 +107,7 @@ async def fastapi_get_tank_elevation(
|
|||||||
return ps["elevation"]
|
return ps["elevation"]
|
||||||
|
|
||||||
@router.get("/tanks/init-level", summary="获取水箱初始水位", description="获取指定水箱的初始水位值")
|
@router.get("/tanks/init-level", summary="获取水箱初始水位", description="获取指定水箱的初始水位值")
|
||||||
async def fastapi_get_tank_init_level(
|
def fastapi_get_tank_init_level(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
@@ -125,7 +125,7 @@ async def fastapi_get_tank_init_level(
|
|||||||
return ps["init_level"]
|
return ps["init_level"]
|
||||||
|
|
||||||
@router.get("/tanks/min-level", summary="获取水箱最小水位", description="获取指定水箱的最小水位值")
|
@router.get("/tanks/min-level", summary="获取水箱最小水位", description="获取指定水箱的最小水位值")
|
||||||
async def fastapi_get_tank_min_level(
|
def fastapi_get_tank_min_level(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
@@ -143,7 +143,7 @@ async def fastapi_get_tank_min_level(
|
|||||||
return ps["min_level"]
|
return ps["min_level"]
|
||||||
|
|
||||||
@router.get("/tanks/max-level", summary="获取水箱最大水位", description="获取指定水箱的最大水位值")
|
@router.get("/tanks/max-level", summary="获取水箱最大水位", description="获取指定水箱的最大水位值")
|
||||||
async def fastapi_get_tank_max_level(
|
def fastapi_get_tank_max_level(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
@@ -161,7 +161,7 @@ async def fastapi_get_tank_max_level(
|
|||||||
return ps["max_level"]
|
return ps["max_level"]
|
||||||
|
|
||||||
@router.get("/tanks/diameter", summary="获取水箱直径", description="获取指定水箱的直径值")
|
@router.get("/tanks/diameter", summary="获取水箱直径", description="获取指定水箱的直径值")
|
||||||
async def fastapi_get_tank_diameter(
|
def fastapi_get_tank_diameter(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
@@ -179,7 +179,7 @@ async def fastapi_get_tank_diameter(
|
|||||||
return ps["diameter"]
|
return ps["diameter"]
|
||||||
|
|
||||||
@router.get("/tanks/min-vol", summary="获取水箱最小体积", description="获取指定水箱的最小体积值")
|
@router.get("/tanks/min-vol", summary="获取水箱最小体积", description="获取指定水箱的最小体积值")
|
||||||
async def fastapi_get_tank_min_vol(
|
def fastapi_get_tank_min_vol(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
@@ -197,7 +197,7 @@ async def fastapi_get_tank_min_vol(
|
|||||||
return ps["min_vol"]
|
return ps["min_vol"]
|
||||||
|
|
||||||
@router.get("/tanks/vol-curve", summary="获取水箱容积曲线", description="获取指定水箱的容积曲线标识")
|
@router.get("/tanks/vol-curve", summary="获取水箱容积曲线", description="获取指定水箱的容积曲线标识")
|
||||||
async def fastapi_get_tank_vol_curve(
|
def fastapi_get_tank_vol_curve(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
@@ -215,7 +215,7 @@ async def fastapi_get_tank_vol_curve(
|
|||||||
return ps["vol_curve"]
|
return ps["vol_curve"]
|
||||||
|
|
||||||
@router.get("/tanks/overflow", summary="获取水箱溢流口", description="获取指定水箱的溢流口配置")
|
@router.get("/tanks/overflow", summary="获取水箱溢流口", description="获取指定水箱的溢流口配置")
|
||||||
async def fastapi_get_tank_overflow(
|
def fastapi_get_tank_overflow(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
@@ -233,7 +233,7 @@ async def fastapi_get_tank_overflow(
|
|||||||
return ps["overflow"]
|
return ps["overflow"]
|
||||||
|
|
||||||
@router.get("/tanks/x", summary="获取水箱X坐标", description="获取指定水箱的X坐标值")
|
@router.get("/tanks/x", summary="获取水箱X坐标", description="获取指定水箱的X坐标值")
|
||||||
async def fastapi_get_tank_x(
|
def fastapi_get_tank_x(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
) -> float:
|
) -> float:
|
||||||
@@ -251,7 +251,7 @@ async def fastapi_get_tank_x(
|
|||||||
return ps["x"]
|
return ps["x"]
|
||||||
|
|
||||||
@router.get("/tanks/y", summary="获取水箱Y坐标", description="获取指定水箱的Y坐标值")
|
@router.get("/tanks/y", summary="获取水箱Y坐标", description="获取指定水箱的Y坐标值")
|
||||||
async def fastapi_get_tank_y(
|
def fastapi_get_tank_y(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
) -> float:
|
) -> float:
|
||||||
@@ -269,7 +269,7 @@ async def fastapi_get_tank_y(
|
|||||||
return ps["y"]
|
return ps["y"]
|
||||||
|
|
||||||
@router.get("/tanks/coord", summary="获取水箱坐标", description="获取指定水箱的X和Y坐标")
|
@router.get("/tanks/coord", summary="获取水箱坐标", description="获取指定水箱的X和Y坐标")
|
||||||
async def fastapi_get_tank_coord(
|
def fastapi_get_tank_coord(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
) -> dict[str, float]:
|
) -> dict[str, float]:
|
||||||
@@ -288,7 +288,7 @@ async def fastapi_get_tank_coord(
|
|||||||
return coord
|
return coord
|
||||||
|
|
||||||
@router.patch("/tanks/elevation", summary="设置水箱标高", description="设置指定水箱的标高值", response_model=None)
|
@router.patch("/tanks/elevation", summary="设置水箱标高", description="设置指定水箱的标高值", response_model=None)
|
||||||
async def fastapi_set_tank_elevation(
|
def fastapi_set_tank_elevation(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
elevation: float = Query(..., description="新的标高值")
|
elevation: float = Query(..., description="新的标高值")
|
||||||
@@ -308,7 +308,7 @@ async def fastapi_set_tank_elevation(
|
|||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/tanks/init-level", summary="设置水箱初始水位", description="设置指定水箱的初始水位值", response_model=None)
|
@router.patch("/tanks/init-level", summary="设置水箱初始水位", description="设置指定水箱的初始水位值", response_model=None)
|
||||||
async def fastapi_set_tank_init_level(
|
def fastapi_set_tank_init_level(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
init_level: float = Query(..., description="新的初始水位值")
|
init_level: float = Query(..., description="新的初始水位值")
|
||||||
@@ -328,7 +328,7 @@ async def fastapi_set_tank_init_level(
|
|||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/tanks/min-level", summary="设置水箱最小水位", description="设置指定水箱的最小水位值", response_model=None)
|
@router.patch("/tanks/min-level", summary="设置水箱最小水位", description="设置指定水箱的最小水位值", response_model=None)
|
||||||
async def fastapi_set_tank_min_level(
|
def fastapi_set_tank_min_level(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
min_level: float = Query(..., description="新的最小水位值")
|
min_level: float = Query(..., description="新的最小水位值")
|
||||||
@@ -348,7 +348,7 @@ async def fastapi_set_tank_min_level(
|
|||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/tanks/max-level", summary="设置水箱最大水位", description="设置指定水箱的最大水位值", response_model=None)
|
@router.patch("/tanks/max-level", summary="设置水箱最大水位", description="设置指定水箱的最大水位值", response_model=None)
|
||||||
async def fastapi_set_tank_max_level(
|
def fastapi_set_tank_max_level(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
max_level: float = Query(..., description="新的最大水位值")
|
max_level: float = Query(..., description="新的最大水位值")
|
||||||
@@ -368,7 +368,7 @@ async def fastapi_set_tank_max_level(
|
|||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/tanks/diameter", summary="设置水箱直径", description="设置指定水箱的直径值", response_model=None)
|
@router.patch("/tanks/diameter", summary="设置水箱直径", description="设置指定水箱的直径值", response_model=None)
|
||||||
async def fastapi_set_tank_diameter(
|
def fastapi_set_tank_diameter(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
diameter: float = Query(..., description="新的直径值")
|
diameter: float = Query(..., description="新的直径值")
|
||||||
@@ -388,7 +388,7 @@ async def fastapi_set_tank_diameter(
|
|||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/tanks/min-vol", summary="设置水箱最小体积", description="设置指定水箱的最小体积值", response_model=None)
|
@router.patch("/tanks/min-vol", summary="设置水箱最小体积", description="设置指定水箱的最小体积值", response_model=None)
|
||||||
async def fastapi_set_tank_min_vol(
|
def fastapi_set_tank_min_vol(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
min_vol: float = Query(..., description="新的最小体积值")
|
min_vol: float = Query(..., description="新的最小体积值")
|
||||||
@@ -408,7 +408,7 @@ async def fastapi_set_tank_min_vol(
|
|||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/tanks/vol-curve", summary="设置水箱容积曲线", description="设置指定水箱的容积曲线标识", response_model=None)
|
@router.patch("/tanks/vol-curve", summary="设置水箱容积曲线", description="设置指定水箱的容积曲线标识", response_model=None)
|
||||||
async def fastapi_set_tank_vol_curve(
|
def fastapi_set_tank_vol_curve(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
vol_curve: str = Query(..., description="新的容积曲线标识")
|
vol_curve: str = Query(..., description="新的容积曲线标识")
|
||||||
@@ -428,7 +428,7 @@ async def fastapi_set_tank_vol_curve(
|
|||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/tanks/overflow", summary="设置水箱溢流口", description="设置指定水箱的溢流口配置", response_model=None)
|
@router.patch("/tanks/overflow", summary="设置水箱溢流口", description="设置指定水箱的溢流口配置", response_model=None)
|
||||||
async def fastapi_set_tank_overflow(
|
def fastapi_set_tank_overflow(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
overflow: str = Query(..., description="新的溢流口配置")
|
overflow: str = Query(..., description="新的溢流口配置")
|
||||||
@@ -448,7 +448,7 @@ async def fastapi_set_tank_overflow(
|
|||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/tanks/x", summary="设置水箱X坐标", description="设置指定水箱的X坐标值", response_model=None)
|
@router.patch("/tanks/x", summary="设置水箱X坐标", description="设置指定水箱的X坐标值", response_model=None)
|
||||||
async def fastapi_set_tank_x(
|
def fastapi_set_tank_x(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
x: float = Query(..., description="新的X坐标值")
|
x: float = Query(..., description="新的X坐标值")
|
||||||
@@ -468,7 +468,7 @@ async def fastapi_set_tank_x(
|
|||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/tanks/y", summary="设置水箱Y坐标", description="设置指定水箱的Y坐标值", response_model=None)
|
@router.patch("/tanks/y", summary="设置水箱Y坐标", description="设置指定水箱的Y坐标值", response_model=None)
|
||||||
async def fastapi_set_tank_y(
|
def fastapi_set_tank_y(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
y: float = Query(..., description="新的Y坐标值")
|
y: float = Query(..., description="新的Y坐标值")
|
||||||
@@ -488,7 +488,7 @@ async def fastapi_set_tank_y(
|
|||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.patch("/tanks/coord", summary="设置水箱坐标", description="设置指定水箱的X和Y坐标", response_model=None)
|
@router.patch("/tanks/coord", summary="设置水箱坐标", description="设置指定水箱的X和Y坐标", response_model=None)
|
||||||
async def fastapi_set_tank_coord(
|
def fastapi_set_tank_coord(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
x: float = Query(..., description="新的X坐标值"),
|
x: float = Query(..., description="新的X坐标值"),
|
||||||
@@ -510,7 +510,7 @@ async def fastapi_set_tank_coord(
|
|||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|
||||||
@router.get("/tanks/properties", summary="获取水箱属性", description="获取指定水箱的所有属性")
|
@router.get("/tanks/properties", summary="获取水箱属性", description="获取指定水箱的所有属性")
|
||||||
async def fastapi_get_tank_properties(
|
def fastapi_get_tank_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID")
|
tank: str = Query(..., description="水箱ID")
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -527,7 +527,7 @@ async def fastapi_get_tank_properties(
|
|||||||
return get_tank(network, tank)
|
return get_tank(network, tank)
|
||||||
|
|
||||||
@router.get("/tanks", summary="获取所有水箱属性", description="获取指定网络中所有水箱的属性")
|
@router.get("/tanks", summary="获取所有水箱属性", description="获取指定网络中所有水箱的属性")
|
||||||
async def fastapi_get_all_tank_properties(
|
def fastapi_get_all_tank_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -540,15 +540,14 @@ async def fastapi_get_all_tank_properties(
|
|||||||
包含所有水箱属性的字典列表
|
包含所有水箱属性的字典列表
|
||||||
"""
|
"""
|
||||||
# 缓存查询结果提高性能
|
# 缓存查询结果提高性能
|
||||||
# global redis_client
|
|
||||||
results = get_all_tanks(network)
|
results = get_all_tanks(network)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@router.patch("/tanks/properties", summary="设置水箱属性", description="批量设置指定水箱的多个属性", response_model=None)
|
@router.patch("/tanks/properties", summary="设置水箱属性", description="批量设置指定水箱的多个属性", response_model=None)
|
||||||
async def fastapi_set_tank_properties(
|
def fastapi_set_tank_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
tank: str = Query(..., description="水箱ID"),
|
tank: str = Query(..., description="水箱ID"),
|
||||||
req: Request = None
|
payload: dict[str, Any] = Body(...)
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""
|
"""
|
||||||
批量设置水箱的属性。
|
批量设置水箱的属性。
|
||||||
@@ -561,6 +560,6 @@ async def fastapi_set_tank_properties(
|
|||||||
Returns:
|
Returns:
|
||||||
包含变更信息的ChangeSet对象
|
包含变更信息的ChangeSet对象
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
ps = {"id": tank} | props
|
ps = {"id": tank} | props
|
||||||
return set_tank(network, ChangeSet(ps))
|
return set_tank(network, ChangeSet(ps))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, Request, Query, Path, Body
|
from typing import Any
|
||||||
from typing import Any, List, Dict, Union
|
|
||||||
|
from fastapi import APIRouter, Body, Query
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
Any,
|
|
||||||
ChangeSet,
|
ChangeSet,
|
||||||
VALVES_TYPE_PRV,
|
VALVES_TYPE_PRV,
|
||||||
add_valve,
|
add_valve,
|
||||||
@@ -19,7 +19,7 @@ router = APIRouter()
|
|||||||
summary="获取阀门架构",
|
summary="获取阀门架构",
|
||||||
description="获取指定水网中所有阀门的架构和字段定义",
|
description="获取指定水网中所有阀门的架构和字段定义",
|
||||||
)
|
)
|
||||||
async def fastapi_get_valve_schema(
|
def fastapi_get_valve_schema(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> dict[str, dict[str, Any]]:
|
) -> dict[str, dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -35,7 +35,7 @@ async def fastapi_get_valve_schema(
|
|||||||
summary="添加阀门",
|
summary="添加阀门",
|
||||||
description="在指定的水网中添加新的阀门",
|
description="在指定的水网中添加新的阀门",
|
||||||
)
|
)
|
||||||
async def fastapi_add_valve(
|
def fastapi_add_valve(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
valve: str = Query(..., description="阀门ID"),
|
valve: str = Query(..., description="阀门ID"),
|
||||||
node1: str = Query(..., description="起点节点ID"),
|
node1: str = Query(..., description="起点节点ID"),
|
||||||
@@ -68,7 +68,7 @@ async def fastapi_add_valve(
|
|||||||
summary="删除阀门",
|
summary="删除阀门",
|
||||||
description="从指定的水网中删除指定的阀门",
|
description="从指定的水网中删除指定的阀门",
|
||||||
)
|
)
|
||||||
async def fastapi_delete_valve(
|
def fastapi_delete_valve(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
valve: str = Query(..., description="阀门ID"),
|
valve: str = Query(..., description="阀门ID"),
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
@@ -85,7 +85,7 @@ async def fastapi_delete_valve(
|
|||||||
summary="获取阀门起点节点",
|
summary="获取阀门起点节点",
|
||||||
description="获取指定阀门连接的起点节点ID",
|
description="获取指定阀门连接的起点节点ID",
|
||||||
)
|
)
|
||||||
async def fastapi_get_valve_node1(
|
def fastapi_get_valve_node1(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
valve: str = Query(..., description="阀门ID"),
|
valve: str = Query(..., description="阀门ID"),
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
@@ -102,7 +102,7 @@ async def fastapi_get_valve_node1(
|
|||||||
summary="获取阀门终点节点",
|
summary="获取阀门终点节点",
|
||||||
description="获取指定阀门连接的终点节点ID",
|
description="获取指定阀门连接的终点节点ID",
|
||||||
)
|
)
|
||||||
async def fastapi_get_valve_node2(
|
def fastapi_get_valve_node2(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
valve: str = Query(..., description="阀门ID"),
|
valve: str = Query(..., description="阀门ID"),
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
@@ -119,7 +119,7 @@ async def fastapi_get_valve_node2(
|
|||||||
summary="获取阀门直径",
|
summary="获取阀门直径",
|
||||||
description="获取指定阀门的直径",
|
description="获取指定阀门的直径",
|
||||||
)
|
)
|
||||||
async def fastapi_get_valve_diameter(
|
def fastapi_get_valve_diameter(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
valve: str = Query(..., description="阀门ID"),
|
valve: str = Query(..., description="阀门ID"),
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
@@ -136,7 +136,7 @@ async def fastapi_get_valve_diameter(
|
|||||||
summary="获取阀门类型",
|
summary="获取阀门类型",
|
||||||
description="获取指定阀门的类型",
|
description="获取指定阀门的类型",
|
||||||
)
|
)
|
||||||
async def fastapi_get_valve_type(
|
def fastapi_get_valve_type(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
valve: str = Query(..., description="阀门ID"),
|
valve: str = Query(..., description="阀门ID"),
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
@@ -153,7 +153,7 @@ async def fastapi_get_valve_type(
|
|||||||
summary="获取阀门开度",
|
summary="获取阀门开度",
|
||||||
description="获取指定阀门的开度/设置值",
|
description="获取指定阀门的开度/设置值",
|
||||||
)
|
)
|
||||||
async def fastapi_get_valve_setting(
|
def fastapi_get_valve_setting(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
valve: str = Query(..., description="阀门ID"),
|
valve: str = Query(..., description="阀门ID"),
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
@@ -170,7 +170,7 @@ async def fastapi_get_valve_setting(
|
|||||||
summary="获取阀门损失系数",
|
summary="获取阀门损失系数",
|
||||||
description="获取指定阀门的损失系数",
|
description="获取指定阀门的损失系数",
|
||||||
)
|
)
|
||||||
async def fastapi_get_valve_minor_loss(
|
def fastapi_get_valve_minor_loss(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
valve: str = Query(..., description="阀门ID"),
|
valve: str = Query(..., description="阀门ID"),
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
@@ -188,7 +188,7 @@ async def fastapi_get_valve_minor_loss(
|
|||||||
summary="设置阀门起点节点",
|
summary="设置阀门起点节点",
|
||||||
description="设置指定阀门的起点节点",
|
description="设置指定阀门的起点节点",
|
||||||
)
|
)
|
||||||
async def fastapi_set_valve_node1(
|
def fastapi_set_valve_node1(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
valve: str = Query(..., description="阀门ID"),
|
valve: str = Query(..., description="阀门ID"),
|
||||||
node1: str = Query(..., description="新的起点节点ID"),
|
node1: str = Query(..., description="新的起点节点ID"),
|
||||||
@@ -207,7 +207,7 @@ async def fastapi_set_valve_node1(
|
|||||||
summary="设置阀门终点节点",
|
summary="设置阀门终点节点",
|
||||||
description="设置指定阀门的终点节点",
|
description="设置指定阀门的终点节点",
|
||||||
)
|
)
|
||||||
async def fastapi_set_valve_node2(
|
def fastapi_set_valve_node2(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
valve: str = Query(..., description="阀门ID"),
|
valve: str = Query(..., description="阀门ID"),
|
||||||
node2: str = Query(..., description="新的终点节点ID"),
|
node2: str = Query(..., description="新的终点节点ID"),
|
||||||
@@ -226,7 +226,7 @@ async def fastapi_set_valve_node2(
|
|||||||
summary="设置阀门直径",
|
summary="设置阀门直径",
|
||||||
description="设置指定阀门的直径",
|
description="设置指定阀门的直径",
|
||||||
)
|
)
|
||||||
async def fastapi_set_valve_diameter(
|
def fastapi_set_valve_diameter(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
valve: str = Query(..., description="阀门ID"),
|
valve: str = Query(..., description="阀门ID"),
|
||||||
diameter: float = Query(..., description="新的直径值(mm)"),
|
diameter: float = Query(..., description="新的直径值(mm)"),
|
||||||
@@ -245,7 +245,7 @@ async def fastapi_set_valve_diameter(
|
|||||||
summary="设置阀门类型",
|
summary="设置阀门类型",
|
||||||
description="设置指定阀门的类型",
|
description="设置指定阀门的类型",
|
||||||
)
|
)
|
||||||
async def fastapi_set_valve_type(
|
def fastapi_set_valve_type(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
valve: str = Query(..., description="阀门ID"),
|
valve: str = Query(..., description="阀门ID"),
|
||||||
type: str = Query(..., description="新的阀门类型"),
|
type: str = Query(..., description="新的阀门类型"),
|
||||||
@@ -264,7 +264,7 @@ async def fastapi_set_valve_type(
|
|||||||
summary="设置阀门开度",
|
summary="设置阀门开度",
|
||||||
description="设置指定阀门的开度/设置值",
|
description="设置指定阀门的开度/设置值",
|
||||||
)
|
)
|
||||||
async def fastapi_set_valve_setting(
|
def fastapi_set_valve_setting(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
valve: str = Query(..., description="阀门ID"),
|
valve: str = Query(..., description="阀门ID"),
|
||||||
setting: float = Query(..., description="新的开度值"),
|
setting: float = Query(..., description="新的开度值"),
|
||||||
@@ -282,7 +282,7 @@ async def fastapi_set_valve_setting(
|
|||||||
summary="获取阀门所有属性",
|
summary="获取阀门所有属性",
|
||||||
description="获取指定阀门的所有属性",
|
description="获取指定阀门的所有属性",
|
||||||
)
|
)
|
||||||
async def fastapi_get_valve_properties(
|
def fastapi_get_valve_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
valve: str = Query(..., description="阀门ID"),
|
valve: str = Query(..., description="阀门ID"),
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -298,7 +298,7 @@ async def fastapi_get_valve_properties(
|
|||||||
summary="获取所有阀门属性",
|
summary="获取所有阀门属性",
|
||||||
description="获取指定水网中所有阀门的属性",
|
description="获取指定水网中所有阀门的属性",
|
||||||
)
|
)
|
||||||
async def fastapi_get_all_valve_properties(
|
def fastapi_get_all_valve_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -307,7 +307,6 @@ async def fastapi_get_all_valve_properties(
|
|||||||
返回指定水网中所有阀门的完整属性列表。
|
返回指定水网中所有阀门的完整属性列表。
|
||||||
"""
|
"""
|
||||||
# 缓存查询结果提高性能
|
# 缓存查询结果提高性能
|
||||||
# global redis_client
|
|
||||||
results = get_all_valves(network)
|
results = get_all_valves(network)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@@ -317,16 +316,16 @@ async def fastapi_get_all_valve_properties(
|
|||||||
summary="批量设置阀门属性",
|
summary="批量设置阀门属性",
|
||||||
description="批量设置指定阀门的多个属性",
|
description="批量设置指定阀门的多个属性",
|
||||||
)
|
)
|
||||||
async def fastapi_set_valve_properties(
|
def fastapi_set_valve_properties(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
valve: str = Query(..., description="阀门ID"),
|
valve: str = Query(..., description="阀门ID"),
|
||||||
req: Request = None,
|
payload: dict[str, Any] = Body(...),
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
"""
|
"""
|
||||||
批量设置阀门的属性。
|
批量设置阀门的属性。
|
||||||
|
|
||||||
更新指定阀门的一个或多个属性,通过JSON请求体传递要更新的属性。
|
更新指定阀门的一个或多个属性,通过JSON请求体传递要更新的属性。
|
||||||
"""
|
"""
|
||||||
props = await req.json()
|
props = payload
|
||||||
ps = {"id": valve} | props
|
ps = {"id": valve} | props
|
||||||
return set_valve(network, ChangeSet(ps))
|
return set_valve(network, ChangeSet(ps))
|
||||||
|
|||||||
@@ -1,49 +1,18 @@
|
|||||||
import json
|
import json
|
||||||
from fastapi import APIRouter, Request, HTTPException, Query, Path, Depends
|
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||||
from fastapi.responses import PlainTextResponse
|
|
||||||
from typing import Any, Dict, List
|
|
||||||
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
||||||
from app.auth.project_dependencies import get_metadata_repository
|
from app.auth.project_dependencies import (
|
||||||
from app.auth.permissions import (
|
get_metadata_repository,
|
||||||
ENVIRONMENT_MANAGE,
|
|
||||||
require_permission,
|
|
||||||
)
|
)
|
||||||
from app.domain.schemas.metadata import ProjectMetaResponse
|
from app.domain.schemas.metadata import ProjectMetaResponse
|
||||||
import app.services.project_info as project_info
|
|
||||||
from app.infra.db.postgresql.database import get_database_instance as get_pg_db
|
|
||||||
from app.infra.db.timescaledb.database import get_database_instance as get_ts_db
|
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
ChangeSet,
|
ChangeSet,
|
||||||
list_project,
|
|
||||||
have_project,
|
|
||||||
create_project,
|
|
||||||
delete_project,
|
|
||||||
is_project_open,
|
|
||||||
open_project,
|
|
||||||
close_project,
|
|
||||||
copy_project,
|
|
||||||
export_inp,
|
export_inp,
|
||||||
read_inp,
|
|
||||||
dump_inp,
|
|
||||||
get_all_vertices,
|
get_all_vertices,
|
||||||
get_all_scada_elements,
|
get_all_scada_info,
|
||||||
get_all_district_metering_areas,
|
|
||||||
get_all_service_areas,
|
|
||||||
get_all_virtual_districts,
|
|
||||||
get_extension_data,
|
|
||||||
convert_inp_v3_to_v2,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# For inp file upload/download
|
|
||||||
import os
|
|
||||||
from fastapi import Response, status
|
|
||||||
from fastapi.responses import FileResponse
|
|
||||||
inpDir = "data/" # Assuming data directory exists or is defined somewhere.
|
|
||||||
# In main.py it was likely global. For safety, let's use a relative path or get from config.
|
|
||||||
# But let's stick to what main.py probably used or a default.
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
lockedPrjs: Dict[str, str] = {}
|
|
||||||
|
|
||||||
@router.get("/projects/current", summary="获取项目信息", description="从数据库获取项目的详细信息,包括地图范围等。", response_model=ProjectMetaResponse)
|
@router.get("/projects/current", summary="获取项目信息", description="从数据库获取项目的详细信息,包括地图范围等。", response_model=ProjectMetaResponse)
|
||||||
async def get_project_info_endpoint(
|
async def get_project_info_endpoint(
|
||||||
@@ -69,126 +38,8 @@ async def get_project_info_endpoint(
|
|||||||
project_role="viewer", # Default role for public access
|
project_role="viewer", # Default role for public access
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.get("/project-codes", summary="获取项目列表", description="获取服务器上所有可用的供水管网项目名称列表。")
|
|
||||||
async def list_projects_endpoint() -> list[str]:
|
|
||||||
"""
|
|
||||||
获取项目列表
|
|
||||||
|
|
||||||
返回所有已创建项目的名称列表。
|
|
||||||
"""
|
|
||||||
return list_project()
|
|
||||||
|
|
||||||
@router.get("/projects/existence", summary="检查项目是否存在", description="检查指定名称的项目是否存在。")
|
|
||||||
async def have_project_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
检查项目是否存在
|
|
||||||
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
"""
|
|
||||||
return have_project(network)
|
|
||||||
|
|
||||||
@router.post("/projects", summary="创建新项目", description="创建一个新的供水管网项目。如果项目已存在,可能会覆盖或报错(取决于底层实现)。")
|
|
||||||
async def create_project_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
_=Depends(require_permission(ENVIRONMENT_MANAGE)),
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
创建新项目
|
|
||||||
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
"""
|
|
||||||
create_project(network)
|
|
||||||
return network
|
|
||||||
|
|
||||||
@router.delete("/projects", summary="删除项目", description="永久删除指定的供水管网项目。此操作不可恢复。")
|
|
||||||
async def delete_project_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
_=Depends(require_permission(ENVIRONMENT_MANAGE)),
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
删除项目
|
|
||||||
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
"""
|
|
||||||
delete_project(network)
|
|
||||||
return True
|
|
||||||
|
|
||||||
@router.get("/projects/current/status", summary="检查项目是否已打开", description="检查指定项目是否已被加载到内存中。")
|
|
||||||
async def is_project_open_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
检查项目是否已打开
|
|
||||||
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
"""
|
|
||||||
return is_project_open(network)
|
|
||||||
|
|
||||||
@router.post("/projects/current", summary="打开项目", description="将指定项目加载到内存中,并初始化数据库连接池。")
|
|
||||||
async def open_project_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
打开项目
|
|
||||||
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
"""
|
|
||||||
open_project(network)
|
|
||||||
|
|
||||||
# 尝试连接指定数据库
|
|
||||||
try:
|
|
||||||
# 初始化 PostgreSQL 连接池
|
|
||||||
pg_instance = await get_pg_db(network)
|
|
||||||
async with pg_instance.get_connection() as conn:
|
|
||||||
async with conn.cursor() as cur:
|
|
||||||
await cur.execute("SELECT 1")
|
|
||||||
|
|
||||||
# 初始化 TimescaleDB 连接池
|
|
||||||
ts_instance = await get_ts_db(network)
|
|
||||||
async with ts_instance.get_connection() as conn:
|
|
||||||
async with conn.cursor() as cur:
|
|
||||||
await cur.execute("SELECT 1")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# 记录错误但不阻断项目打开,或者根据需求决定是否阻断
|
|
||||||
# 这里选择打印错误,因为 open_project 原本只负责原生部分
|
|
||||||
print(f"Failed to connect to databases for {network}: {str(e)}")
|
|
||||||
# 如果数据库连接是必须的,可以抛出异常:
|
|
||||||
# raise HTTPException(status_code=500, detail=f"Database connection failed: {str(e)}")
|
|
||||||
|
|
||||||
return network
|
|
||||||
|
|
||||||
@router.delete("/projects/current", summary="关闭项目", description="将指定项目从内存中卸载,释放资源。")
|
|
||||||
async def close_project_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
关闭项目
|
|
||||||
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
"""
|
|
||||||
close_project(network)
|
|
||||||
return True
|
|
||||||
|
|
||||||
@router.post("/project-copies", summary="复制项目", description="将现有项目复制为新项目。")
|
|
||||||
async def copy_project_endpoint(
|
|
||||||
source: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
target: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
_=Depends(require_permission(ENVIRONMENT_MANAGE)),
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
复制项目
|
|
||||||
|
|
||||||
- **source**: 管网名称(或数据库名称)
|
|
||||||
- **target**: 管网名称(或数据库名称)
|
|
||||||
"""
|
|
||||||
copy_project(source, target)
|
|
||||||
return True
|
|
||||||
|
|
||||||
@router.get("/projects/current/exports/change-set", response_model=None, summary="导出项目为 ChangeSet", description="导出项目的变更集 (ChangeSet),包含顶点、SCADA 元素、DMA、SA、VD 等信息。")
|
@router.get("/projects/current/exports/change-set", response_model=None, summary="导出项目为 ChangeSet", description="导出项目的变更集 (ChangeSet),包含顶点、SCADA 元素、DMA、SA、VD 等信息。")
|
||||||
async def export_inp_endpoint(
|
def export_inp_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
version: str = Query(..., description="版本号 (通常用于增量更新)")
|
version: str = Query(..., description="版本号 (通常用于增量更新)")
|
||||||
) -> ChangeSet:
|
) -> ChangeSet:
|
||||||
@@ -200,316 +51,7 @@ async def export_inp_endpoint(
|
|||||||
"""
|
"""
|
||||||
cs = export_inp(network, version)
|
cs = export_inp(network, version)
|
||||||
op = cs.operations[0]
|
op = cs.operations[0]
|
||||||
open_project(network)
|
|
||||||
op["vertex"] = json.dumps(get_all_vertices(network))
|
op["vertex"] = json.dumps(get_all_vertices(network))
|
||||||
op["scada"] = json.dumps(get_all_scada_elements(network))
|
op["scada"] = json.dumps(get_all_scada_info(network))
|
||||||
op["dma"] = json.dumps(get_all_district_metering_areas(network))
|
|
||||||
op["sa"] = json.dumps(get_all_service_areas(network))
|
|
||||||
op["vd"] = json.dumps(get_all_virtual_districts(network))
|
|
||||||
op["legend"] = get_extension_data(network, "legend")
|
|
||||||
|
|
||||||
db = get_extension_data(network, "scada_db")
|
|
||||||
print(db)
|
|
||||||
scada_db = ""
|
|
||||||
if db:
|
|
||||||
scada_db = db
|
|
||||||
print(scada_db)
|
|
||||||
op["scada_db"] = scada_db
|
|
||||||
|
|
||||||
close_project(network)
|
|
||||||
|
|
||||||
return cs
|
|
||||||
|
|
||||||
@router.post("/projects/current/imports", summary="读取 INP 文件到项目", description="从服务器文件系统中读取指定的 INP 文件并加载到项目中。")
|
|
||||||
async def read_inp_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
inp: str = Query(..., description="INP 文件名 (不包含路径)")
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
读取 INP 文件到项目
|
|
||||||
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
- **inp**: INP 文件名
|
|
||||||
"""
|
|
||||||
read_inp(network, inp)
|
|
||||||
return True
|
|
||||||
|
|
||||||
@router.post("/projects/current/exports/inp", summary="导出项目到 INP 文件", description="将项目当前状态保存为 INP 文件到服务器文件系统。")
|
|
||||||
async def dump_inp_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
inp: str = Query(..., description="目标文件名")
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
导出项目到 INP 文件
|
|
||||||
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
- **inp**: 目标文件名
|
|
||||||
"""
|
|
||||||
dump_inp(network, inp)
|
|
||||||
return True
|
|
||||||
|
|
||||||
@router.get("/projects/current/lock", summary="检查项目是否被锁定", description="检查指定项目是否处于锁定状态。")
|
|
||||||
async def is_project_locked_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
检查项目是否被锁定
|
|
||||||
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
"""
|
|
||||||
return network in lockedPrjs.keys()
|
|
||||||
|
|
||||||
@router.get("/projects/current/lock/ownership", summary="检查项目是否被当前用户锁定", description="检查指定项目是否被当前访问地址 (IP) 锁定。")
|
|
||||||
async def is_project_locked_by_me_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
检查项目是否被当前用户锁定
|
|
||||||
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
"""
|
|
||||||
client_host = req.client.host
|
|
||||||
return lockedPrjs.get(network) == client_host
|
|
||||||
|
|
||||||
# 0 successfully locked
|
|
||||||
# 1 already locked by you
|
|
||||||
# 2 locked by others
|
|
||||||
@router.post("/projects/current/lock", summary="锁定项目", description="锁定指定项目以防止并发修改。")
|
|
||||||
async def lock_project_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
锁定项目
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
- **0**: 锁定成功
|
|
||||||
- **1**: 已被当前用户锁定
|
|
||||||
- **2**: 已被其他用户锁定
|
|
||||||
"""
|
|
||||||
client_host = req.client.host
|
|
||||||
if not network in lockedPrjs.keys():
|
|
||||||
lockedPrjs[network] = client_host
|
|
||||||
return 0
|
|
||||||
else:
|
|
||||||
if lockedPrjs.get(network) == client_host:
|
|
||||||
return 1
|
|
||||||
else:
|
|
||||||
return 2
|
|
||||||
|
|
||||||
@router.delete("/projects/current/lock", summary="解锁项目", description="释放对项目的锁定。")
|
|
||||||
def unlock_project_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
解锁项目
|
|
||||||
|
|
||||||
只有锁定者才能解锁。
|
|
||||||
"""
|
|
||||||
client_host = req.client.host
|
|
||||||
if lockedPrjs.get(network) == client_host:
|
|
||||||
print("delete key")
|
|
||||||
del lockedPrjs[network]
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
@router.get("/projects/current/files/inp", status_code=status.HTTP_200_OK, summary="下载 INP 文件", description="从服务器数据目录下载指定的 INP 文件。")
|
|
||||||
async def fastapi_download_inp(
|
|
||||||
name: str = Query(..., description="文件名"),
|
|
||||||
response: Response = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
下载 INP 文件
|
|
||||||
|
|
||||||
- **name**: 文件名
|
|
||||||
"""
|
|
||||||
filePath = inpDir + name
|
|
||||||
if os.path.exists(filePath):
|
|
||||||
return FileResponse(
|
|
||||||
filePath, media_type="application/octet-stream", filename="inp.inp"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
response.status_code = status.HTTP_400_BAD_REQUEST
|
|
||||||
return True
|
|
||||||
|
|
||||||
# DingZQ, 2024-12-28, convert v3 to v2
|
|
||||||
@router.post("/project-conversions", response_model=None, summary="转换 INP V3 为 V2", description="将 EPANET 3.0 格式的 INP 内容转换为 2.x 格式。")
|
|
||||||
async def fastapi_convert_v3_to_v2(
|
|
||||||
req: Request
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""
|
|
||||||
转换 INP V3 为 V2
|
|
||||||
|
|
||||||
- **req**: 请求体,需包含 `{"inp": "..."}` 结构
|
|
||||||
"""
|
|
||||||
network = "v3Tov2"
|
|
||||||
jo_root = await req.json()
|
|
||||||
inp = jo_root["inp"]
|
|
||||||
cs = convert_inp_v3_to_v2(inp)
|
|
||||||
op = cs.operations[0]
|
|
||||||
open_project(network)
|
|
||||||
op["vertex"] = json.dumps(get_all_vertices(network))
|
|
||||||
op["scada"] = json.dumps(get_all_scada_elements(network))
|
|
||||||
op["dma"] = json.dumps(get_all_district_metering_areas(network))
|
|
||||||
op["sa"] = json.dumps(get_all_service_areas(network))
|
|
||||||
op["vd"] = json.dumps(get_all_virtual_districts(network))
|
|
||||||
op["legend"] = get_extension_data(network, "legend")
|
|
||||||
|
|
||||||
db = get_extension_data(network, "scada_db")
|
|
||||||
print(db)
|
|
||||||
scada_db = ""
|
|
||||||
if db:
|
|
||||||
scada_db = db
|
|
||||||
print(scada_db)
|
|
||||||
op["scada_db"] = scada_db
|
|
||||||
|
|
||||||
close_project(network)
|
|
||||||
|
|
||||||
return cs
|
|
||||||
|
|
||||||
async def read_inp_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
inp: str = Query(..., description="INP 文件名 (不包含路径)")
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
读取 INP 文件到项目
|
|
||||||
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
- **inp**: INP 文件名
|
|
||||||
"""
|
|
||||||
read_inp(network, inp)
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def dump_inp_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
inp: str = Query(..., description="目标文件名")
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
导出项目到 INP 文件
|
|
||||||
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
- **inp**: 目标文件名
|
|
||||||
"""
|
|
||||||
dump_inp(network, inp)
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def is_project_locked_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
检查项目是否被锁定
|
|
||||||
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
"""
|
|
||||||
return network in lockedPrjs.keys()
|
|
||||||
|
|
||||||
async def is_project_locked_by_me_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
检查项目是否被当前用户锁定
|
|
||||||
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
"""
|
|
||||||
client_host = req.client.host
|
|
||||||
return lockedPrjs.get(network) == client_host
|
|
||||||
|
|
||||||
# 0 successfully locked
|
|
||||||
# 1 already locked by you
|
|
||||||
# 2 locked by others
|
|
||||||
async def lock_project_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
锁定项目
|
|
||||||
|
|
||||||
返回值:
|
|
||||||
- **0**: 锁定成功
|
|
||||||
- **1**: 已被当前用户锁定
|
|
||||||
- **2**: 已被其他用户锁定
|
|
||||||
"""
|
|
||||||
client_host = req.client.host
|
|
||||||
if not network in lockedPrjs.keys():
|
|
||||||
lockedPrjs[network] = client_host
|
|
||||||
return 0
|
|
||||||
else:
|
|
||||||
if lockedPrjs.get(network) == client_host:
|
|
||||||
return 1
|
|
||||||
else:
|
|
||||||
return 2
|
|
||||||
|
|
||||||
def unlock_project_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
解锁项目
|
|
||||||
|
|
||||||
只有锁定者才能解锁。
|
|
||||||
"""
|
|
||||||
client_host = req.client.host
|
|
||||||
if lockedPrjs.get(network) == client_host:
|
|
||||||
print("delete key")
|
|
||||||
del lockedPrjs[network]
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def fastapi_download_inp(
|
|
||||||
name: str = Query(..., description="文件名"),
|
|
||||||
response: Response = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
下载 INP 文件
|
|
||||||
|
|
||||||
- **name**: 文件名
|
|
||||||
"""
|
|
||||||
filePath = inpDir + name
|
|
||||||
if os.path.exists(filePath):
|
|
||||||
return FileResponse(
|
|
||||||
filePath, media_type="application/octet-stream", filename="inp.inp"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
response.status_code = status.HTTP_400_BAD_REQUEST
|
|
||||||
return True
|
|
||||||
|
|
||||||
# DingZQ, 2024-12-28, convert v3 to v2
|
|
||||||
async def fastapi_convert_v3_to_v2(
|
|
||||||
req: Request
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""
|
|
||||||
转换 INP V3 为 V2
|
|
||||||
|
|
||||||
- **req**: 请求体,需包含 `{"inp": "..."}` 结构
|
|
||||||
"""
|
|
||||||
network = "v3Tov2"
|
|
||||||
jo_root = await req.json()
|
|
||||||
inp = jo_root["inp"]
|
|
||||||
cs = convert_inp_v3_to_v2(inp)
|
|
||||||
op = cs.operations[0]
|
|
||||||
open_project(network)
|
|
||||||
op["vertex"] = json.dumps(get_all_vertices(network))
|
|
||||||
op["scada"] = json.dumps(get_all_scada_elements(network))
|
|
||||||
op["dma"] = json.dumps(get_all_district_metering_areas(network))
|
|
||||||
op["sa"] = json.dumps(get_all_service_areas(network))
|
|
||||||
op["vd"] = json.dumps(get_all_virtual_districts(network))
|
|
||||||
op["legend"] = get_extension_data(network, "legend")
|
|
||||||
|
|
||||||
db = get_extension_data(network, "scada_db")
|
|
||||||
print(db)
|
|
||||||
scada_db = ""
|
|
||||||
if db:
|
|
||||||
scada_db = db
|
|
||||||
print(scada_db)
|
|
||||||
op["scada_db"] = scada_db
|
|
||||||
|
|
||||||
close_project(network)
|
|
||||||
|
|
||||||
return cs
|
return cs
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
from uuid import UUID
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from psycopg import AsyncConnection
|
from psycopg import AsyncConnection
|
||||||
|
|
||||||
from app.infra.db.postgresql.scada import ScadaInfoRepository
|
from app.infra.db.postgresql.analysis import AnalysisRepository
|
||||||
from app.infra.db.postgresql.scheme import SchemeRepository
|
|
||||||
from app.auth.project_dependencies import get_project_pg_connection
|
from app.auth.project_dependencies import get_project_pg_connection
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -15,26 +16,8 @@ async def get_database_connection(
|
|||||||
yield conn
|
yield conn
|
||||||
|
|
||||||
|
|
||||||
@router.get("/scada-info/database-view", summary="获取SCADA信息", description="使用连接池查询所有SCADA信息")
|
@router.get("/analysis/runs", summary="获取分析运行列表")
|
||||||
async def get_scada_info_with_connection(
|
async def get_analysis_runs(
|
||||||
conn: AsyncConnection = Depends(get_database_connection),
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取所有SCADA信息
|
|
||||||
|
|
||||||
返回项目中所有的SCADA设备信息
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
scada_data = await ScadaInfoRepository.get_scadas(conn)
|
|
||||||
return {"success": True, "data": scada_data, "count": len(scada_data)}
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=500, detail=f"查询SCADA信息时发生错误: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/schemes/list-with-connection", summary="获取方案列表", description="使用连接池查询所有方案信息")
|
|
||||||
async def get_scheme_list_with_connection(
|
|
||||||
conn: AsyncConnection = Depends(get_database_connection),
|
conn: AsyncConnection = Depends(get_database_connection),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -43,14 +26,15 @@ async def get_scheme_list_with_connection(
|
|||||||
返回项目中所有方案的详细信息
|
返回项目中所有方案的详细信息
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
scheme_data = await SchemeRepository.get_schemes(conn)
|
runs = await AnalysisRepository.list_runs(conn)
|
||||||
return {"success": True, "data": scheme_data, "count": len(scheme_data)}
|
return {"success": True, "data": runs, "count": len(runs)}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"查询方案信息时发生错误: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"查询分析运行时发生错误: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/burst-locations/database-view", summary="获取爆管定位结果", description="使用连接池查询所有爆管定位结果")
|
@router.get("/analysis/runs/{run_id}", summary="获取分析运行")
|
||||||
async def get_burst_locate_result_with_connection(
|
async def get_analysis_run(
|
||||||
|
run_id: UUID,
|
||||||
conn: AsyncConnection = Depends(get_database_connection),
|
conn: AsyncConnection = Depends(get_database_connection),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -59,17 +43,22 @@ async def get_burst_locate_result_with_connection(
|
|||||||
返回项目中所有的爆管定位分析结果
|
返回项目中所有的爆管定位分析结果
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
burst_data = await SchemeRepository.get_burst_locate_results(conn)
|
run = await AnalysisRepository.get_run(conn, run_id)
|
||||||
return {"success": True, "data": burst_data, "count": len(burst_data)}
|
if run is None:
|
||||||
|
raise HTTPException(status_code=404, detail="分析运行不存在")
|
||||||
|
return run
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500, detail=f"查询爆管定位结果时发生错误: {str(e)}"
|
status_code=500, detail=f"查询分析运行时发生错误: {str(e)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/burst-locations/{burst_incident}", summary="按事件查询爆管定位结果", description="根据爆管事件ID查询对应的爆管定位结果")
|
@router.get("/analysis/runs/{run_id}/results", summary="获取分析结果")
|
||||||
async def get_burst_locate_result_by_incident(
|
async def get_analysis_results(
|
||||||
burst_incident: str = Path(..., description="爆管事件ID"),
|
run_id: UUID,
|
||||||
|
result_type: str | None = Query(default=None, description="结果类型"),
|
||||||
conn: AsyncConnection = Depends(get_database_connection),
|
conn: AsyncConnection = Depends(get_database_connection),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -79,11 +68,9 @@ async def get_burst_locate_result_by_incident(
|
|||||||
burst_incident: 爆管事件的唯一标识符
|
burst_incident: 爆管事件的唯一标识符
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return await SchemeRepository.get_burst_locate_result_by_incident(
|
return await AnalysisRepository.list_results(conn, run_id, result_type)
|
||||||
conn, burst_incident
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
detail=f"根据 burst_incident 查询爆管定位结果时发生错误: {str(e)}",
|
detail=f"查询分析结果时发生错误: {str(e)}",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
from typing import Any, List, Dict
|
|
||||||
from fastapi import APIRouter, Query, Path
|
|
||||||
from app.services.tjnetwork import (
|
|
||||||
get_pipe_risk_probability_now,
|
|
||||||
get_pipe_risk_probability,
|
|
||||||
get_pipes_risk_probability,
|
|
||||||
get_network_pipe_risk_probability_now,
|
|
||||||
get_pipe_risk_probability_geometries,
|
|
||||||
)
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/pipes/risk-probability-now",
|
|
||||||
summary="获取管道当前风险概率",
|
|
||||||
description="获取指定管道当前时刻的风险概率值"
|
|
||||||
)
|
|
||||||
async def fastapi_get_pipe_risk_probability_now(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
pipe_id: str = Query(..., description="管道ID")
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取管道当前风险概率。
|
|
||||||
|
|
||||||
查询指定管道在当前时刻的风险概率值。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
pipe_id: 管道ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
包含风险概率信息的字典
|
|
||||||
"""
|
|
||||||
return get_pipe_risk_probability_now(network, pipe_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/pipes/risk-probability",
|
|
||||||
summary="获取管道风险概率历史",
|
|
||||||
description="获取指定管道的风险概率历史数据"
|
|
||||||
)
|
|
||||||
async def fastapi_get_pipe_risk_probability(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
pipe_id: str = Query(..., description="管道ID")
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取管道风险概率历史。
|
|
||||||
|
|
||||||
查询指定管道的历史风险概率数据。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
pipe_id: 管道ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
包含风险概率历史的字典
|
|
||||||
"""
|
|
||||||
return get_pipe_risk_probability(network, pipe_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/pipes-risk-probabilities",
|
|
||||||
summary="批量获取多条管道风险概率",
|
|
||||||
description="批量获取多条管道的风险概率值"
|
|
||||||
)
|
|
||||||
async def fastapi_get_pipes_risk_probability(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
pipe_ids: str = Query(..., description="逗号分隔的管道ID列表")
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
批量获取多条管道风险概率。
|
|
||||||
|
|
||||||
查询多条指定管道的风险概率值。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
pipe_ids: 逗号分隔的管道ID列表(例如:pipe1,pipe2,pipe3)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
包含多条管道风险概率的列表
|
|
||||||
"""
|
|
||||||
pipeids = pipe_ids.split(",")
|
|
||||||
return get_pipes_risk_probability(network, pipeids)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/network-pipe-risk-probability-nows",
|
|
||||||
summary="获取整个网络的管道风险概率",
|
|
||||||
description="获取指定网络中所有管道的当前风险概率值"
|
|
||||||
)
|
|
||||||
async def fastapi_get_network_pipe_risk_probability_now(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
获取整个网络的管道风险概率。
|
|
||||||
|
|
||||||
查询指定网络中所有管道在当前时刻的风险概率值。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
包含网络内所有管道风险概率的列表
|
|
||||||
"""
|
|
||||||
return get_network_pipe_risk_probability_now(network)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/pipes/risk-probability-geometries",
|
|
||||||
summary="获取管道风险几何信息",
|
|
||||||
description="获取指定网络中管道的风险相关几何数据"
|
|
||||||
)
|
|
||||||
async def fastapi_get_pipe_risk_probability_geometries(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取管道风险几何信息。
|
|
||||||
|
|
||||||
查询指定网络中管道的地理和风险相关的几何数据。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
包含几何信息和风险数据的字典
|
|
||||||
"""
|
|
||||||
return get_pipe_risk_probability_geometries(network)
|
|
||||||
+31
-514
@@ -1,525 +1,42 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from fastapi import APIRouter, Request, Query
|
|
||||||
from app.services.tjnetwork import (
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
ChangeSet,
|
from psycopg import AsyncConnection
|
||||||
get_scada_info,
|
|
||||||
get_all_scada_info,
|
from app.auth.project_dependencies import get_project_pg_connection
|
||||||
get_scada_device_schema,
|
from app.domain.schemas.scada import ScadaDeviceResponse
|
||||||
get_scada_device,
|
from app.infra.db.postgresql.scada import ScadaInfoRepository, get_scada_info_schema
|
||||||
set_scada_device,
|
|
||||||
add_scada_device,
|
|
||||||
delete_scada_device,
|
|
||||||
clean_scada_device,
|
|
||||||
get_all_scada_device_ids,
|
|
||||||
get_all_scada_devices,
|
|
||||||
get_scada_device_data_schema,
|
|
||||||
get_scada_device_data,
|
|
||||||
set_scada_device_data,
|
|
||||||
add_scada_device_data,
|
|
||||||
delete_scada_device_data,
|
|
||||||
clean_scada_device_data,
|
|
||||||
get_scada_element_schema,
|
|
||||||
get_scada_element,
|
|
||||||
set_scada_element,
|
|
||||||
add_scada_element,
|
|
||||||
delete_scada_element,
|
|
||||||
clean_scada_element,
|
|
||||||
get_all_scada_elements,
|
|
||||||
get_scada_element_schema,
|
|
||||||
get_scada_info_schema,
|
|
||||||
)
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
async def fast_get_scada_properties(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
scada: str = Query(..., description="SCADA设备ID")
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取单个SCADA设备的属性信息
|
|
||||||
|
|
||||||
根据管网名称和SCADA设备ID获取该设备的完整属性。
|
@router.get("/network-schemas/scada-device", summary="获取 SCADA 设备结构")
|
||||||
|
def get_scada_device_schema() -> dict[str, dict[str, Any]]:
|
||||||
|
return get_scada_info_schema("")
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
scada: SCADA设备ID
|
|
||||||
|
|
||||||
Returns:
|
@router.get(
|
||||||
SCADA设备的属性字典
|
"/scada-devices",
|
||||||
"""
|
summary="获取 SCADA 设备列表",
|
||||||
return get_scada_info(network, scada)
|
response_model=list[ScadaDeviceResponse],
|
||||||
|
)
|
||||||
async def fast_get_all_scada_properties(
|
async def get_scada_devices(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""
|
return await ScadaInfoRepository.get_scadas(conn)
|
||||||
获取指定管网所有SCADA设备的属性信息
|
|
||||||
|
|
||||||
查询该管网下所有已配置的SCADA设备的属性列表。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SCADA设备属性列表
|
|
||||||
"""
|
|
||||||
return get_all_scada_info(network)
|
|
||||||
|
|
||||||
|
|
||||||
############################################################
|
@router.get(
|
||||||
# scada_device 设备管理
|
"/scada-devices/{device_id}",
|
||||||
############################################################
|
summary="获取 SCADA 设备",
|
||||||
|
response_model=ScadaDeviceResponse,
|
||||||
@router.get("/network-schemas/scada-device", summary="获取SCADA设备架构", tags=["SCADA设备"])
|
)
|
||||||
async def fastapi_get_scada_device_schema(
|
async def get_scada_device(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
device_id: str,
|
||||||
) -> dict[str, dict[str, Any]]:
|
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||||
"""
|
|
||||||
获取SCADA设备的数据架构
|
|
||||||
|
|
||||||
返回SCADA设备表的字段定义和类型信息。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SCADA设备的字段架构信息
|
|
||||||
"""
|
|
||||||
return get_scada_device_schema(network)
|
|
||||||
|
|
||||||
@router.get("/scada-devices/detail", summary="获取SCADA设备", tags=["SCADA设备"])
|
|
||||||
async def fastapi_get_scada_device(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
id: str = Query(..., description="SCADA设备ID")
|
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
device = await ScadaInfoRepository.get_scada(conn, device_id)
|
||||||
获取单个SCADA设备的信息
|
if device is None:
|
||||||
|
raise HTTPException(status_code=404, detail="SCADA 设备不存在")
|
||||||
根据设备ID查询该设备的详细信息。
|
return device
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
id: SCADA设备ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SCADA设备信息
|
|
||||||
"""
|
|
||||||
return get_scada_device(network, id)
|
|
||||||
|
|
||||||
@router.patch("/scada-devices", response_model=None, summary="更新SCADA设备", tags=["SCADA设备"])
|
|
||||||
async def fastapi_set_scada_device(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""
|
|
||||||
更新SCADA设备信息
|
|
||||||
|
|
||||||
修改指定SCADA设备的属性。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
req: 请求体,包含要更新的设备属性
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
变更集合信息
|
|
||||||
"""
|
|
||||||
props = await req.json()
|
|
||||||
return set_scada_device(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.post("/scada-devices", response_model=None, summary="添加SCADA设备", tags=["SCADA设备"])
|
|
||||||
async def fastapi_add_scada_device(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""
|
|
||||||
添加新的SCADA设备
|
|
||||||
|
|
||||||
在指定管网中添加一个新的SCADA设备。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
req: 请求体,包含新设备的属性
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
变更集合信息
|
|
||||||
"""
|
|
||||||
props = await req.json()
|
|
||||||
return add_scada_device(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.delete("/scada-devices", response_model=None, summary="删除SCADA设备", tags=["SCADA设备"])
|
|
||||||
async def fastapi_delete_scada_device(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""
|
|
||||||
删除SCADA设备
|
|
||||||
|
|
||||||
从指定管网中删除一个SCADA设备。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
req: 请求体,包含要删除的设备ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
变更集合信息
|
|
||||||
"""
|
|
||||||
props = await req.json()
|
|
||||||
return delete_scada_device(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.post("/scada-device-cleaning-runs", response_model=None, summary="清空SCADA设备表", tags=["SCADA设备"])
|
|
||||||
async def fastapi_clean_scada_device(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""
|
|
||||||
清空SCADA设备表
|
|
||||||
|
|
||||||
删除指定管网中所有的SCADA设备。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
变更集合信息
|
|
||||||
"""
|
|
||||||
return clean_scada_device(network)
|
|
||||||
|
|
||||||
@router.get("/scada-devices/ids", summary="获取所有SCADA设备ID", tags=["SCADA设备"])
|
|
||||||
async def fastapi_get_all_scada_device_ids(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
) -> list[str]:
|
|
||||||
"""
|
|
||||||
获取指定管网所有SCADA设备的ID列表
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SCADA设备ID列表
|
|
||||||
"""
|
|
||||||
return get_all_scada_device_ids(network)
|
|
||||||
|
|
||||||
@router.get("/scada-devices", summary="获取所有SCADA设备", tags=["SCADA设备"])
|
|
||||||
async def fastapi_get_all_scada_devices(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
获取指定管网所有SCADA设备的完整信息
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SCADA设备信息列表
|
|
||||||
"""
|
|
||||||
return get_all_scada_devices(network)
|
|
||||||
|
|
||||||
|
|
||||||
############################################################
|
|
||||||
# scada_device_data 设备数据管理
|
|
||||||
############################################################
|
|
||||||
|
|
||||||
@router.get("/network-schemas/scada-device-data", summary="获取SCADA设备数据架构", tags=["SCADA设备数据"])
|
|
||||||
async def fastapi_get_scada_device_data_schema(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
) -> dict[str, dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
获取SCADA设备数据的表结构
|
|
||||||
|
|
||||||
返回SCADA设备数据表的字段定义和类型信息。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SCADA设备数据的字段架构信息
|
|
||||||
"""
|
|
||||||
return get_scada_device_data_schema(network)
|
|
||||||
|
|
||||||
@router.get("/scada-device-datas/detail", summary="获取SCADA设备数据", tags=["SCADA设备数据"])
|
|
||||||
async def fastapi_get_scada_device_data(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
device_id: str = Query(..., description="SCADA设备ID")
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取单个SCADA设备的数据
|
|
||||||
|
|
||||||
查询指定设备的监测数据或配置数据。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
device_id: SCADA设备ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SCADA设备数据
|
|
||||||
"""
|
|
||||||
return get_scada_device_data(network, device_id)
|
|
||||||
|
|
||||||
@router.patch("/scada-device-datas", response_model=None, summary="更新SCADA设备数据", tags=["SCADA设备数据"])
|
|
||||||
async def fastapi_set_scada_device_data(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""
|
|
||||||
更新SCADA设备数据
|
|
||||||
|
|
||||||
修改指定SCADA设备的数据。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
req: 请求体,包含要更新的数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
变更集合信息
|
|
||||||
"""
|
|
||||||
props = await req.json()
|
|
||||||
return set_scada_device_data(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.post("/scada-device-datas", response_model=None, summary="添加SCADA设备数据", tags=["SCADA设备数据"])
|
|
||||||
async def fastapi_add_scada_device_data(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""
|
|
||||||
添加新的SCADA设备数据
|
|
||||||
|
|
||||||
为指定SCADA设备添加新的数据记录。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
req: 请求体,包含新数据的内容
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
变更集合信息
|
|
||||||
"""
|
|
||||||
props = await req.json()
|
|
||||||
return add_scada_device_data(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.delete("/scada-device-datas", response_model=None, summary="删除SCADA设备数据", tags=["SCADA设备数据"])
|
|
||||||
async def fastapi_delete_scada_device_data(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""
|
|
||||||
删除SCADA设备数据
|
|
||||||
|
|
||||||
删除指定SCADA设备的数据记录。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
req: 请求体,包含要删除的数据ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
变更集合信息
|
|
||||||
"""
|
|
||||||
props = await req.json()
|
|
||||||
return delete_scada_device_data(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.post("/scada-device-data-cleaning-runs", response_model=None, summary="清空SCADA设备数据表", tags=["SCADA设备数据"])
|
|
||||||
async def fastapi_clean_scada_device_data(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""
|
|
||||||
清空SCADA设备数据表
|
|
||||||
|
|
||||||
删除指定管网中所有SCADA设备的数据。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
变更集合信息
|
|
||||||
"""
|
|
||||||
return clean_scada_device_data(network)
|
|
||||||
|
|
||||||
|
|
||||||
############################################################
|
|
||||||
# scada_element SCADA元素映射
|
|
||||||
############################################################
|
|
||||||
|
|
||||||
@router.get("/network-schemas/scada-element", summary="获取SCADA元素架构", tags=["SCADA元素映射"])
|
|
||||||
async def fastapi_get_scada_element_schema(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
) -> dict[str, dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
获取SCADA元素映射的表结构
|
|
||||||
|
|
||||||
返回SCADA元素映射表的字段定义和类型信息。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SCADA元素映射的字段架构信息
|
|
||||||
"""
|
|
||||||
return get_scada_element_schema(network)
|
|
||||||
|
|
||||||
@router.get("/scada-elements", summary="获取所有SCADA元素映射", tags=["SCADA元素映射"])
|
|
||||||
async def fastapi_get_scada_elements(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
获取指定管网所有SCADA元素映射
|
|
||||||
|
|
||||||
查询所有SCADA设备与管网元素(节点/管道)的映射关系。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SCADA元素映射列表
|
|
||||||
"""
|
|
||||||
return get_all_scada_elements(network)
|
|
||||||
|
|
||||||
@router.get("/scada-elements/detail", summary="获取单个SCADA元素映射", tags=["SCADA元素映射"])
|
|
||||||
async def fastapi_get_scada_element(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
id: str = Query(..., description="SCADA元素映射ID")
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取单个SCADA元素映射的信息
|
|
||||||
|
|
||||||
根据ID查询特定的SCADA设备与管网元素的映射关系。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
id: SCADA元素映射ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SCADA元素映射信息
|
|
||||||
"""
|
|
||||||
return get_scada_element(network, id)
|
|
||||||
|
|
||||||
@router.patch("/scada-elements", response_model=None, summary="更新SCADA元素映射", tags=["SCADA元素映射"])
|
|
||||||
async def fastapi_set_scada_element(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""
|
|
||||||
更新SCADA元素映射
|
|
||||||
|
|
||||||
修改SCADA设备与管网元素的映射关系。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
req: 请求体,包含要更新的映射信息
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
变更集合信息
|
|
||||||
"""
|
|
||||||
props = await req.json()
|
|
||||||
return set_scada_element(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.post("/scada-elements", response_model=None, summary="添加SCADA元素映射", tags=["SCADA元素映射"])
|
|
||||||
async def fastapi_add_scada_element(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""
|
|
||||||
添加新的SCADA元素映射
|
|
||||||
|
|
||||||
创建SCADA设备与管网元素的新映射关系。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
req: 请求体,包含新映射的信息
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
变更集合信息
|
|
||||||
"""
|
|
||||||
props = await req.json()
|
|
||||||
return add_scada_element(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.delete("/scada-elements", response_model=None, summary="删除SCADA元素映射", tags=["SCADA元素映射"])
|
|
||||||
async def fastapi_delete_scada_element(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""
|
|
||||||
删除SCADA元素映射
|
|
||||||
|
|
||||||
移除SCADA设备与管网元素的映射关系。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
req: 请求体,包含要删除的映射ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
变更集合信息
|
|
||||||
"""
|
|
||||||
props = await req.json()
|
|
||||||
return delete_scada_element(network, ChangeSet(props))
|
|
||||||
|
|
||||||
@router.post("/scada-element-cleaning-runs", response_model=None, summary="清空SCADA元素映射表", tags=["SCADA元素映射"])
|
|
||||||
async def fastapi_clean_scada_element(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""
|
|
||||||
清空SCADA元素映射表
|
|
||||||
|
|
||||||
删除指定管网中所有的SCADA元素映射。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
变更集合信息
|
|
||||||
"""
|
|
||||||
return clean_scada_element(network)
|
|
||||||
|
|
||||||
|
|
||||||
############################################################
|
|
||||||
# scada_info SCADA信息
|
|
||||||
############################################################
|
|
||||||
|
|
||||||
@router.get("/scada-info-schemas", summary="获取SCADA信息架构", tags=["SCADA信息"])
|
|
||||||
async def fastapi_get_scada_info_schema(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
) -> dict[str, dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
获取SCADA信息表的结构
|
|
||||||
|
|
||||||
返回SCADA信息表的字段定义和类型信息。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SCADA信息的字段架构信息
|
|
||||||
"""
|
|
||||||
return get_scada_info_schema(network)
|
|
||||||
|
|
||||||
@router.get("/scada-info/detail", summary="获取SCADA信息", tags=["SCADA信息"])
|
|
||||||
async def fastapi_get_scada_info(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
id: str = Query(..., description="SCADA信息ID")
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取单个SCADA信息
|
|
||||||
|
|
||||||
根据ID查询SCADA的详细配置信息。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
id: SCADA信息ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SCADA信息详情
|
|
||||||
"""
|
|
||||||
return get_scada_info(network, id)
|
|
||||||
|
|
||||||
@router.get("/scada-info", summary="获取所有SCADA信息", tags=["SCADA信息"])
|
|
||||||
async def fastapi_get_all_scada_info(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
获取指定管网所有SCADA的信息
|
|
||||||
|
|
||||||
查询该管网下所有已配置的SCADA的完整信息。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
network: 管网名称(或数据库名称)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
SCADA信息列表
|
|
||||||
"""
|
|
||||||
return get_all_scada_info(network)
|
|
||||||
|
|||||||
@@ -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,17 +1,19 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
|
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from starlette.concurrency import run_in_threadpool
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
|
||||||
from app.algorithms.sensor import (
|
|
||||||
pressure_sensor_placement_kmeans,
|
|
||||||
pressure_sensor_placement_sensitivity,
|
|
||||||
)
|
|
||||||
from app.auth.metadata_dependencies import get_current_metadata_user
|
from app.auth.metadata_dependencies import get_current_metadata_user
|
||||||
from app.auth.project_dependencies import ProjectContext, get_project_context
|
from app.auth.project_dependencies import (
|
||||||
|
ProjectContext,
|
||||||
|
get_project_context,
|
||||||
|
use_project_business_routing,
|
||||||
|
)
|
||||||
|
from app.infra.db.project_routing import ActiveProjectRouting
|
||||||
from app.domain.schemas.sensor_placement import (
|
from app.domain.schemas.sensor_placement import (
|
||||||
SensorPointResponse,
|
SensorPointResponse,
|
||||||
SensorPlacementExportRequest,
|
SensorPlacementExportRequest,
|
||||||
@@ -26,8 +28,11 @@ from app.services.sensor_placement import (
|
|||||||
build_sensor_placement_workbook,
|
build_sensor_placement_workbook,
|
||||||
can_edit_sensor_placement,
|
can_edit_sensor_placement,
|
||||||
get_sensor_placement_candidate,
|
get_sensor_placement_candidate,
|
||||||
get_sensor_placement_scheme,
|
get_sensor_placement_run,
|
||||||
update_sensor_placement_scheme,
|
list_sensor_placement_runs,
|
||||||
|
optimize_sensor_placement_by_kmeans,
|
||||||
|
optimize_sensor_placement_by_sensitivity,
|
||||||
|
update_sensor_placement_run,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -74,19 +79,19 @@ def _service_http_error(exc: Exception) -> HTTPException:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_scheme_response(
|
def _get_run_response(
|
||||||
network: str,
|
network: str,
|
||||||
scheme_id: int,
|
run_id: UUID,
|
||||||
current_user: Any,
|
current_user: Any,
|
||||||
project_context: ProjectContext,
|
project_context: ProjectContext,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
scheme = get_sensor_placement_scheme(network, scheme_id)
|
run = get_sensor_placement_run(network, run_id)
|
||||||
return {
|
return {
|
||||||
**scheme,
|
**run,
|
||||||
"can_edit": (
|
"can_edit": (
|
||||||
_can_modify_project(project_context)
|
_can_modify_project(project_context)
|
||||||
and can_edit_sensor_placement(current_user, scheme)
|
and can_edit_sensor_placement(current_user, run)
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
except (
|
except (
|
||||||
@@ -104,6 +109,7 @@ def _get_scheme_response(
|
|||||||
async def get_sensor_placement_candidate_detail(
|
async def get_sensor_placement_candidate_detail(
|
||||||
node_id: str = Path(..., min_length=1, max_length=32),
|
node_id: str = Path(..., min_length=1, max_length=32),
|
||||||
project_context: ProjectContext = Depends(get_project_context),
|
project_context: ProjectContext = Depends(get_project_context),
|
||||||
|
_routing: ActiveProjectRouting = Depends(use_project_business_routing),
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
return await run_in_threadpool(
|
return await run_in_threadpool(
|
||||||
@@ -116,7 +122,7 @@ async def get_sensor_placement_candidate_detail(
|
|||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/sensor-placement-optimization-runs",
|
"/sensor-placement-runs",
|
||||||
response_model=SensorPlacementSchemeResponse,
|
response_model=SensorPlacementSchemeResponse,
|
||||||
summary="创建并返回监测点优化方案",
|
summary="创建并返回监测点优化方案",
|
||||||
)
|
)
|
||||||
@@ -128,21 +134,21 @@ async def optimize_sensor_placement_scheme(
|
|||||||
network = _project_network(payload.network, project_context)
|
network = _project_network(payload.network, project_context)
|
||||||
_require_project_write(project_context)
|
_require_project_write(project_context)
|
||||||
optimizer = (
|
optimizer = (
|
||||||
pressure_sensor_placement_sensitivity
|
optimize_sensor_placement_by_sensitivity
|
||||||
if payload.method == "sensitivity"
|
if payload.method == "sensitivity"
|
||||||
else pressure_sensor_placement_kmeans
|
else optimize_sensor_placement_by_kmeans
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
created = await run_in_threadpool(
|
created = await run_in_threadpool(
|
||||||
optimizer,
|
optimizer,
|
||||||
name=network,
|
project_code=network,
|
||||||
scheme_name=payload.scheme_name,
|
run_name=payload.run_name,
|
||||||
sensor_number=payload.sensor_count,
|
sensor_count=payload.sensor_count,
|
||||||
min_diameter=payload.min_diameter,
|
min_diameter=payload.min_diameter,
|
||||||
username=current_user.username,
|
created_by=current_user.username,
|
||||||
)
|
)
|
||||||
scheme = get_sensor_placement_scheme(network, int(created["id"]))
|
run = get_sensor_placement_run(network, created["run_id"])
|
||||||
return {**scheme, "can_edit": True}
|
return {**run, "can_edit": True}
|
||||||
except (
|
except (
|
||||||
SensorPlacementConflictError,
|
SensorPlacementConflictError,
|
||||||
SensorPlacementValidationError,
|
SensorPlacementValidationError,
|
||||||
@@ -158,31 +164,46 @@ async def optimize_sensor_placement_scheme(
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/sensor-placement-schemes/{scheme_id}",
|
"/sensor-placement-runs",
|
||||||
|
response_model=list[SensorPlacementSchemeResponse],
|
||||||
|
summary="获取监测点优化运行",
|
||||||
|
)
|
||||||
|
async def get_sensor_placement_runs(
|
||||||
|
project_context: ProjectContext = Depends(get_project_context),
|
||||||
|
_routing: ActiveProjectRouting = Depends(use_project_business_routing),
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
return await run_in_threadpool(
|
||||||
|
list_sensor_placement_runs,
|
||||||
|
project_context.project_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/sensor-placement-runs/{run_id}",
|
||||||
response_model=SensorPlacementSchemeResponse,
|
response_model=SensorPlacementSchemeResponse,
|
||||||
summary="获取监测点方案详情",
|
summary="获取监测点方案详情",
|
||||||
)
|
)
|
||||||
async def get_sensor_placement_scheme_detail(
|
def get_sensor_placement_run_detail(
|
||||||
scheme_id: int,
|
run_id: UUID,
|
||||||
network: str = Query(..., min_length=1),
|
network: str = Query(..., min_length=1),
|
||||||
project_context: ProjectContext = Depends(get_project_context),
|
project_context: ProjectContext = Depends(get_project_context),
|
||||||
current_user=Depends(get_current_metadata_user),
|
current_user=Depends(get_current_metadata_user),
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return _get_scheme_response(
|
return _get_run_response(
|
||||||
_project_network(network, project_context),
|
_project_network(network, project_context),
|
||||||
scheme_id,
|
run_id,
|
||||||
current_user,
|
current_user,
|
||||||
project_context,
|
project_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
@router.put(
|
||||||
"/sensor-placement-schemes/{scheme_id}",
|
"/sensor-placement-runs/{run_id}",
|
||||||
response_model=SensorPlacementSchemeResponse,
|
response_model=SensorPlacementSchemeResponse,
|
||||||
summary="覆盖保存监测点方案",
|
summary="覆盖保存监测点方案",
|
||||||
)
|
)
|
||||||
async def overwrite_sensor_placement_scheme(
|
def overwrite_sensor_placement_run(
|
||||||
scheme_id: int,
|
run_id: UUID,
|
||||||
payload: SensorPlacementUpdateRequest,
|
payload: SensorPlacementUpdateRequest,
|
||||||
network: str = Query(..., min_length=1),
|
network: str = Query(..., min_length=1),
|
||||||
project_context: ProjectContext = Depends(get_project_context),
|
project_context: ProjectContext = Depends(get_project_context),
|
||||||
@@ -190,21 +211,21 @@ async def overwrite_sensor_placement_scheme(
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
network = _project_network(network, project_context)
|
network = _project_network(network, project_context)
|
||||||
_require_project_write(project_context)
|
_require_project_write(project_context)
|
||||||
scheme = _get_scheme_response(
|
run = _get_run_response(
|
||||||
network,
|
network,
|
||||||
scheme_id,
|
run_id,
|
||||||
current_user,
|
current_user,
|
||||||
project_context,
|
project_context,
|
||||||
)
|
)
|
||||||
if not scheme["can_edit"]:
|
if not run["can_edit"]:
|
||||||
raise HTTPException(status_code=403, detail="无权修改该监测点方案")
|
raise HTTPException(status_code=403, detail="无权修改该监测点优化运行")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
updated = update_sensor_placement_scheme(
|
updated = update_sensor_placement_run(
|
||||||
network,
|
network,
|
||||||
scheme_id,
|
run_id,
|
||||||
expected_sensor_location=payload.expected_sensor_location,
|
expected_sensor_locations=payload.expected_sensor_locations,
|
||||||
sensor_location=payload.sensor_location,
|
sensor_locations=payload.sensor_locations,
|
||||||
)
|
)
|
||||||
return {**updated, "can_edit": True}
|
return {**updated, "can_edit": True}
|
||||||
except (
|
except (
|
||||||
@@ -216,26 +237,26 @@ async def overwrite_sensor_placement_scheme(
|
|||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/sensor-placement-schemes/{scheme_id}/exports/excel",
|
"/sensor-placement-runs/{run_id}/exports/excel",
|
||||||
summary="导出监测点工程清单",
|
summary="导出监测点工程清单",
|
||||||
)
|
)
|
||||||
async def export_sensor_placement_excel(
|
async def export_sensor_placement_excel(
|
||||||
scheme_id: int,
|
run_id: UUID,
|
||||||
payload: SensorPlacementExportRequest,
|
payload: SensorPlacementExportRequest,
|
||||||
network: str = Query(..., min_length=1),
|
network: str = Query(..., min_length=1),
|
||||||
project_context: ProjectContext = Depends(get_project_context),
|
project_context: ProjectContext = Depends(get_project_context),
|
||||||
current_user=Depends(get_current_metadata_user),
|
current_user=Depends(get_current_metadata_user),
|
||||||
) -> StreamingResponse:
|
) -> StreamingResponse:
|
||||||
network = _project_network(network, project_context)
|
network = _project_network(network, project_context)
|
||||||
scheme = _get_scheme_response(
|
run = _get_run_response(
|
||||||
network,
|
network,
|
||||||
scheme_id,
|
run_id,
|
||||||
current_user,
|
current_user,
|
||||||
project_context,
|
project_context,
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
payload.sensor_location != scheme["sensor_location"]
|
payload.sensor_locations != run["sensor_locations"]
|
||||||
and not scheme["can_edit"]
|
and not run["can_edit"]
|
||||||
):
|
):
|
||||||
raise HTTPException(status_code=403, detail="无权导出该方案的未保存草稿")
|
raise HTTPException(status_code=403, detail="无权导出该方案的未保存草稿")
|
||||||
|
|
||||||
@@ -243,14 +264,14 @@ async def export_sensor_placement_excel(
|
|||||||
workbook = await run_in_threadpool(
|
workbook = await run_in_threadpool(
|
||||||
build_sensor_placement_workbook,
|
build_sensor_placement_workbook,
|
||||||
network=network,
|
network=network,
|
||||||
scheme=scheme,
|
scheme=run,
|
||||||
sensor_location=payload.sensor_location,
|
sensor_location=payload.sensor_locations,
|
||||||
adjustment_status=payload.adjustment_status,
|
adjustment_status=payload.adjustment_status,
|
||||||
)
|
)
|
||||||
except SensorPlacementValidationError as exc:
|
except SensorPlacementValidationError as exc:
|
||||||
raise _service_http_error(exc) from exc
|
raise _service_http_error(exc) from exc
|
||||||
|
|
||||||
filename = f"{scheme['scheme_name']}_监测点清单.xlsx"
|
filename = f"{run['name']}_监测点清单.xlsx"
|
||||||
encoded_filename = quote(filename)
|
encoded_filename = quote(filename)
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
workbook,
|
workbook,
|
||||||
|
|||||||
+112
-470
@@ -1,39 +1,22 @@
|
|||||||
from typing import Any, List, Optional
|
from typing import Any, List, Literal, Optional
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import json
|
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
||||||
import threading
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
|
|
||||||
from fastapi.responses import PlainTextResponse
|
from fastapi.responses import PlainTextResponse
|
||||||
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
||||||
import app.services.simulation as simulation
|
import app.services.simulation as simulation
|
||||||
import app.services.globals as globals
|
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
run_project,
|
run_project,
|
||||||
run_project_return_dict,
|
run_project_return_dict,
|
||||||
run_inp,
|
|
||||||
dump_output,
|
|
||||||
)
|
)
|
||||||
from app.algorithms.simulation.scenarios import (
|
from app.services.simulation_scenarios import (
|
||||||
burst_analysis,
|
burst_analysis,
|
||||||
valve_close_analysis,
|
valve_close_analysis,
|
||||||
flushing_analysis,
|
flushing_analysis,
|
||||||
contaminant_simulation,
|
contaminant_simulation,
|
||||||
age_analysis,
|
|
||||||
# scheduling_analysis,
|
|
||||||
pressure_regulation,
|
pressure_regulation,
|
||||||
)
|
)
|
||||||
from app.algorithms.sensor import (
|
|
||||||
pressure_sensor_placement_sensitivity,
|
|
||||||
pressure_sensor_placement_kmeans,
|
|
||||||
)
|
|
||||||
|
|
||||||
from app.services.simulation_ops import (
|
|
||||||
project_management,
|
|
||||||
scheduling_simulation,
|
|
||||||
daily_scheduling_simulation,
|
|
||||||
)
|
|
||||||
from app.services.valve_isolation import analyze_valve_isolation
|
from app.services.valve_isolation import analyze_valve_isolation
|
||||||
from app.services.time_api import (
|
from app.domain.time import (
|
||||||
parse_aware_time,
|
parse_aware_time,
|
||||||
parse_clock_duration_seconds,
|
parse_clock_duration_seconds,
|
||||||
parse_utc_time,
|
parse_utc_time,
|
||||||
@@ -55,65 +38,13 @@ class RunSimulationManuallyByDate(BaseModel):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class BurstAnalysis(BaseModel):
|
|
||||||
name: str = Field(..., description="管网名称(或数据库名称)")
|
|
||||||
modify_pattern_start_time: str = Field(..., description="模式修改开始时间 (ISO 8601)")
|
|
||||||
burst_ID: List[str] | str | None = Field(None, description="爆管节点/管段ID列表")
|
|
||||||
burst_size: List[float] | float | int | None = Field(None, description="爆管流量大小")
|
|
||||||
modify_total_duration: int = Field(900, description="模拟总时长 (秒)")
|
|
||||||
modify_fixed_pump_pattern: Optional[dict[str, list]] = Field(None, description="定速泵模式修改")
|
|
||||||
modify_variable_pump_pattern: Optional[dict[str, list]] = Field(None, description="变速泵模式修改")
|
|
||||||
modify_valve_opening: Optional[dict[str, float]] = Field(None, description="阀门开度修改")
|
|
||||||
scheme_name: Optional[str] = Field(None, description="方案名称")
|
|
||||||
|
|
||||||
|
|
||||||
class SchedulingAnalysis(BaseModel):
|
|
||||||
network: str = Field(..., description="管网名称(或数据库名称)")
|
|
||||||
start_time: str = Field(..., description="开始时间")
|
|
||||||
pump_control: dict = Field(..., description="泵控制策略")
|
|
||||||
tank_id: str = Field(..., description="水箱ID")
|
|
||||||
water_plant_output_id: str = Field(..., description="水厂出水ID")
|
|
||||||
time_delta: Optional[int] = Field(300, description="时间步长 (秒)")
|
|
||||||
|
|
||||||
|
|
||||||
class PressureRegulation(BaseModel):
|
class PressureRegulation(BaseModel):
|
||||||
network: str = Field(..., description="管网名称(或数据库名称)")
|
network: str = Field(..., description="管网名称(或数据库名称)")
|
||||||
start_time: str = Field(..., description="开始时间")
|
start_time: str = Field(..., description="开始时间")
|
||||||
pump_control: dict = Field(..., description="泵控制策略")
|
pump_control: dict = Field(..., description="泵控制策略")
|
||||||
tank_init_level: Optional[dict] = Field(None, description="水箱初始水位")
|
tank_init_level: Optional[dict] = Field(None, description="水箱初始水位")
|
||||||
duration: Optional[int] = Field(900, description="持续时间 (秒)")
|
duration: Optional[int] = Field(900, description="持续时间 (秒)")
|
||||||
scheme_name: Optional[str] = Field(None, description="方案名称")
|
scheme_name: str = Field(..., min_length=1, description="方案名称")
|
||||||
|
|
||||||
|
|
||||||
class ProjectManagement(BaseModel):
|
|
||||||
network: str = Field(..., description="管网名称(或数据库名称)")
|
|
||||||
start_time: str = Field(..., description="开始时间")
|
|
||||||
pump_control: dict = Field(..., description="泵控制策略")
|
|
||||||
tank_init_level: Optional[dict] = Field(None, description="水箱初始水位")
|
|
||||||
region_demand: Optional[dict] = Field(None, description="区域需水量控制")
|
|
||||||
|
|
||||||
|
|
||||||
class DailySchedulingAnalysis(BaseModel):
|
|
||||||
network: str = Field(..., description="管网名称(或数据库名称)")
|
|
||||||
start_time: str = Field(..., description="开始时间")
|
|
||||||
pump_control: dict = Field(..., description="泵控制策略")
|
|
||||||
reservoir_id: str = Field(..., description="水库ID")
|
|
||||||
tank_id: str = Field(..., description="水箱ID")
|
|
||||||
water_plant_output_id: str = Field(..., description="水厂出水ID")
|
|
||||||
time_delta: Optional[int] = Field(300, description="时间步长 (秒)")
|
|
||||||
|
|
||||||
|
|
||||||
class PumpFailureState(BaseModel):
|
|
||||||
time: str = Field(..., description="故障发生时间")
|
|
||||||
pump_status: dict = Field(..., description="泵状态字典")
|
|
||||||
|
|
||||||
|
|
||||||
class PressureSensorPlacement(BaseModel):
|
|
||||||
name: str = Field(..., description="管网名称(或数据库名称)")
|
|
||||||
scheme_name: str = Field(..., description="方案名称")
|
|
||||||
sensor_number: int = Field(..., description="传感器数量")
|
|
||||||
min_diameter: int = Field(0, description="最小管径限制")
|
|
||||||
username: str = Field(..., description="用户名")
|
|
||||||
|
|
||||||
|
|
||||||
def run_simulation_manually_by_date(
|
def run_simulation_manually_by_date(
|
||||||
@@ -128,19 +59,23 @@ def run_simulation_manually_by_date(
|
|||||||
if hydraulic_step_seconds <= 0:
|
if hydraulic_step_seconds <= 0:
|
||||||
raise ValueError("HYDRAULIC TIMESTEP must be greater than 0.")
|
raise ValueError("HYDRAULIC TIMESTEP must be greater than 0.")
|
||||||
hydraulic_step = timedelta(seconds=hydraulic_step_seconds)
|
hydraulic_step = timedelta(seconds=hydraulic_step_seconds)
|
||||||
|
scada_mappings = simulation.query_corresponding_element_id_and_query_id(
|
||||||
|
network_name
|
||||||
|
)
|
||||||
current_time = start_time
|
current_time = start_time
|
||||||
while current_time < end_datetime:
|
while current_time < end_datetime:
|
||||||
simulation.run_simulation(
|
simulation.run_simulation(
|
||||||
name=network_name,
|
name=network_name,
|
||||||
simulation_type="realtime",
|
simulation_type="realtime",
|
||||||
modify_pattern_start_time=current_time.isoformat(timespec="seconds"),
|
modify_pattern_start_time=current_time.isoformat(timespec="seconds"),
|
||||||
|
scada_mappings=scada_mappings,
|
||||||
)
|
)
|
||||||
current_time += hydraulic_step
|
current_time += hydraulic_step
|
||||||
|
|
||||||
|
|
||||||
# 必须用这个PlainTextResponse,不然每个key都有引号
|
# 必须用这个PlainTextResponse,不然每个key都有引号
|
||||||
@router.post("/project-runs", response_class=PlainTextResponse, summary="运行项目模拟", description="基于指定的管网项目运行标准水力模拟,返回纯文本格式的模拟报告。")
|
@router.post("/project-runs", response_class=PlainTextResponse, summary="运行项目模拟", description="基于指定的管网项目运行标准水力模拟,返回纯文本格式的模拟报告。")
|
||||||
async def run_project_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> str:
|
def run_project_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> str:
|
||||||
"""
|
"""
|
||||||
运行项目模拟
|
运行项目模拟
|
||||||
|
|
||||||
@@ -156,7 +91,7 @@ async def run_project_endpoint(network: str = Query(..., description="管网名
|
|||||||
# output 是 json
|
# output 是 json
|
||||||
# report 是 text
|
# report 是 text
|
||||||
@router.post("/project-return-dict-runs", summary="运行项目模拟(返回字典)", description="基于指定的管网项目运行标准水力模拟,返回JSON格式的字典,包含输出数据和报告文本。")
|
@router.post("/project-return-dict-runs", summary="运行项目模拟(返回字典)", description="基于指定的管网项目运行标准水力模拟,返回JSON格式的字典,包含输出数据和报告文本。")
|
||||||
async def run_project_return_dict_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
def run_project_return_dict_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
运行项目模拟(返回字典)
|
运行项目模拟(返回字典)
|
||||||
|
|
||||||
@@ -171,41 +106,15 @@ async def run_project_return_dict_endpoint(network: str = Query(..., description
|
|||||||
return run_project_return_dict(network)
|
return run_project_return_dict(network)
|
||||||
|
|
||||||
|
|
||||||
# put in inp folder, name without extension
|
|
||||||
@router.post("/inp-runs", summary="运行INP文件", description="运行指定INP文件格式的管网模型进行水力模拟。INP文件应该放在inp文件夹中,参数为文件名不含扩展名。")
|
|
||||||
async def run_inp_endpoint(network: str = Query(..., description="inp文件名(不含扩展名)")) -> str:
|
|
||||||
"""
|
|
||||||
运行INP文件
|
|
||||||
|
|
||||||
- **network**: inp文件名(不含扩展名)
|
|
||||||
|
|
||||||
从inp文件夹中读取指定的INP文件并运行模拟。
|
|
||||||
"""
|
|
||||||
return run_inp(network)
|
|
||||||
|
|
||||||
|
|
||||||
# path is absolute path
|
|
||||||
@router.get("/outputs", summary="导出模拟输出", description="导出指定路径的模拟输出文件内容。参数应为绝对路径。")
|
|
||||||
async def dump_output_endpoint(output: str = Query(..., description="模拟输出文件的绝对路径")) -> str:
|
|
||||||
"""
|
|
||||||
导出模拟输出
|
|
||||||
|
|
||||||
- **output**: 模拟输出文件的绝对路径
|
|
||||||
|
|
||||||
读取并返回指定路径的模拟输出内容。
|
|
||||||
"""
|
|
||||||
return dump_output(output)
|
|
||||||
|
|
||||||
|
|
||||||
# Analysis Endpoints
|
# Analysis Endpoints
|
||||||
@router.post("/burst-analyses", summary="爆管分析(高级)", description="高级版本的爆管分析,支持在指定时间点修改泵控制模式和阀门开度,以分析这些改变对爆管影响的作用。支持固定泵和变速泵的独立控制。")
|
@router.post("/burst-analyses", summary="爆管分析(高级)", description="高级版本的爆管分析,支持在指定时间点修改泵控制模式和阀门开度,以分析这些改变对爆管影响的作用。支持固定泵和变速泵的独立控制。")
|
||||||
async def fastapi_burst_analysis(
|
def fastapi_burst_analysis(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
modify_pattern_start_time: str = Query(..., description="模式修改开始时间(ISO 8601格式)"),
|
modify_pattern_start_time: str = Query(..., description="模式修改开始时间(ISO 8601格式)"),
|
||||||
burst_ID: list[str] = Query(..., description="爆管节点/管段ID列表"),
|
burst_ID: list[str] = Query(..., min_length=1, description="爆管节点/管段ID列表"),
|
||||||
burst_size: list[float] = Query(..., description="对应各爆管点的爆管流量大小列表(L/s)"),
|
burst_size: list[float] = Query(..., min_length=1, description="对应各爆管点的爆管流量大小列表(L/s)"),
|
||||||
modify_total_duration: int = Query(..., description="模拟总时长(秒)"),
|
modify_total_duration: int = Query(..., gt=0, description="模拟总时长(秒)"),
|
||||||
scheme_name: str = Query(..., description="分析方案名称"),
|
scheme_name: str = Query(..., min_length=1, description="分析方案名称"),
|
||||||
username: str = Depends(get_current_keycloak_username),
|
username: str = Depends(get_current_keycloak_username),
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -220,6 +129,11 @@ async def fastapi_burst_analysis(
|
|||||||
|
|
||||||
支持在指定时间修改泵控制模式和阀门开度。
|
支持在指定时间修改泵控制模式和阀门开度。
|
||||||
"""
|
"""
|
||||||
|
if len(burst_ID) != len(burst_size):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=422,
|
||||||
|
detail="burst_id 与 burst_size 的数量必须一致",
|
||||||
|
)
|
||||||
burst_analysis(
|
burst_analysis(
|
||||||
name=network,
|
name=network,
|
||||||
modify_pattern_start_time=modify_pattern_start_time,
|
modify_pattern_start_time=modify_pattern_start_time,
|
||||||
@@ -233,7 +147,7 @@ async def fastapi_burst_analysis(
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/valve-closure-analyses", response_class=PlainTextResponse, summary="阀门关闭分析(高级)", description="高级版本的阀门关闭分析,支持同时关闭多个阀门,并在指定持续时间内进行模拟。返回纯文本格式的分析结果。")
|
@router.post("/valve-closure-analyses", response_class=PlainTextResponse, summary="阀门关闭分析(高级)", description="高级版本的阀门关闭分析,支持同时关闭多个阀门,并在指定持续时间内进行模拟。返回纯文本格式的分析结果。")
|
||||||
async def fastapi_valve_close_analysis(
|
def fastapi_valve_close_analysis(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
start_time: str = Query(..., description="阀门关闭开始时间(ISO 8601格式)"),
|
start_time: str = Query(..., description="阀门关闭开始时间(ISO 8601格式)"),
|
||||||
valves: List[str] = Query(..., description="要关闭的阀门ID列表"),
|
valves: List[str] = Query(..., description="要关闭的阀门ID列表"),
|
||||||
@@ -262,7 +176,7 @@ async def fastapi_valve_close_analysis(
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/valve-isolation-analyses", summary="阀门隔离分析", description="分析当发生突发事件时,通过关闭指定阀门进行隔离,确定哪些阀门必须关闭、哪些可选关闭,以及隔离的可行性。")
|
@router.post("/valve-isolation-analyses", summary="阀门隔离分析", description="分析当发生突发事件时,通过关闭指定阀门进行隔离,确定哪些阀门必须关闭、哪些可选关闭,以及隔离的可行性。")
|
||||||
async def valve_isolation_endpoint(
|
def valve_isolation_endpoint(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
accident_element: List[str] = Query(..., description="发生事故的管段/节点ID列表"),
|
accident_element: List[str] = Query(..., description="发生事故的管段/节点ID列表"),
|
||||||
disabled_valves: List[str] = Query(None, description="已故障的阀门ID列表(可选)"),
|
disabled_valves: List[str] = Query(None, description="已故障的阀门ID列表(可选)"),
|
||||||
@@ -302,12 +216,20 @@ async def valve_isolation_endpoint(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.post("/flushing-analyses", response_class=PlainTextResponse, summary="冲洗分析(高级)", description="高级版本的冲洗分析,支持同时开启多个阀门进行冲洗,指定排污节点,并设置固定的冲洗流量。返回纯文本格式的分析结果。")
|
@router.post("/flushing-analyses", response_class=PlainTextResponse, summary="冲洗分析(高级)", description="高级版本的冲洗分析,支持按状态和设置值控制多个可选阀门,指定排污节点,并设置固定的冲洗流量。返回纯文本格式的分析结果。")
|
||||||
async def fastapi_flushing_analysis(
|
def fastapi_flushing_analysis(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
start_time: str = Query(..., description="冲洗开始时间(ISO 8601格式)"),
|
start_time: str = Query(..., description="冲洗开始时间(ISO 8601格式)"),
|
||||||
valves: List[str] = Query(..., description="要开启的阀门ID列表"),
|
valves: List[str] | None = Query(None, description="参与控制的阀门ID列表(可选)"),
|
||||||
valves_k: List[float] = Query(..., description="对应各阀门的开度列表(0-1)"),
|
valves_k: List[float] | None = Query(
|
||||||
|
None, description="对应各阀门的开度列表(0-1,可选,与valves同时提供)"
|
||||||
|
),
|
||||||
|
valve_statuses: List[Literal["OPEN", "CLOSED", "ACTIVE"]] | None = Query(
|
||||||
|
None, description="对应各阀门的开关状态列表(OPEN、CLOSED或ACTIVE,可选)"
|
||||||
|
),
|
||||||
|
valve_settings: List[str] | None = Query(
|
||||||
|
None, description="对应各阀门的设置值列表(ACTIVE状态下必填)"
|
||||||
|
),
|
||||||
drainage_node_ID: str = Query(..., description="排污节点ID"),
|
drainage_node_ID: str = Query(..., description="排污节点ID"),
|
||||||
flush_flow: float = Query(0, description="冲洗流量(L/s),0表示自动计算"),
|
flush_flow: float = Query(0, description="冲洗流量(L/s),0表示自动计算"),
|
||||||
duration: int | None = Query(None, description="模拟持续时间(秒),默认900秒"),
|
duration: int | None = Query(None, description="模拟持续时间(秒),默认900秒"),
|
||||||
@@ -319,8 +241,10 @@ async def fastapi_flushing_analysis(
|
|||||||
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
- **network**: 管网名称(或数据库名称)
|
||||||
- **start_time**: 冲洗开始时间
|
- **start_time**: 冲洗开始时间
|
||||||
- **valves**: 要开启的阀门ID列表
|
- **valves**: 参与控制的阀门ID列表(可选)
|
||||||
- **valves_k**: 各阀门的开度列表(0-1,与valves对应)
|
- **valves_k**: 各阀门的开度列表(0-1,可选,与valves同时提供)
|
||||||
|
- **valve_statuses**: 各阀门的开关状态列表(OPEN、CLOSED或ACTIVE,可选)
|
||||||
|
- **valve_settings**: 各阀门的设置值列表(ACTIVE状态下必填)
|
||||||
- **drainage_node_ID**: 排污节点ID
|
- **drainage_node_ID**: 排污节点ID
|
||||||
- **flush_flow**: 冲洗流量(L/s)
|
- **flush_flow**: 冲洗流量(L/s)
|
||||||
- **duration**: 模拟持续时间(秒,可选,默认900)
|
- **duration**: 模拟持续时间(秒,可选,默认900)
|
||||||
@@ -328,14 +252,75 @@ async def fastapi_flushing_analysis(
|
|||||||
|
|
||||||
支持多阀联合冲洗操作。
|
支持多阀联合冲洗操作。
|
||||||
"""
|
"""
|
||||||
|
valve_opening = None
|
||||||
|
valve_control = None
|
||||||
|
if valve_statuses is not None and valves_k is not None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=422,
|
||||||
|
detail="valve_statuses 和 valves_k 不能同时提供",
|
||||||
|
)
|
||||||
|
if valve_settings is not None and valve_statuses is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=422,
|
||||||
|
detail="valve_settings 必须与 valve_statuses 同时提供",
|
||||||
|
)
|
||||||
|
if valves is None:
|
||||||
|
if (
|
||||||
|
valves_k is not None
|
||||||
|
or valve_statuses is not None
|
||||||
|
or valve_settings is not None
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=422,
|
||||||
|
detail="阀门控制参数必须与 valves 同时提供",
|
||||||
|
)
|
||||||
|
elif valve_statuses is not None:
|
||||||
|
if len(valves) != len(valve_statuses):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=422, detail="valves 和 valve_statuses 的数量必须一致"
|
||||||
|
)
|
||||||
|
if valve_settings is not None and len(valves) != len(valve_settings):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=422, detail="valves 和 valve_settings 的数量必须一致"
|
||||||
|
)
|
||||||
|
|
||||||
|
settings = valve_settings or [""] * len(valves)
|
||||||
|
valve_control = {}
|
||||||
|
for valve_id, raw_status, raw_setting in zip(
|
||||||
|
valves, valve_statuses, settings
|
||||||
|
):
|
||||||
|
status = raw_status
|
||||||
|
setting = raw_setting.strip()
|
||||||
|
if status == "ACTIVE" and not setting:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=422,
|
||||||
|
detail=f"ACTIVE 状态的阀门 {valve_id} 必须提供设置值",
|
||||||
|
)
|
||||||
|
|
||||||
|
control: dict[str, str] = {"status": status}
|
||||||
|
if status == "ACTIVE":
|
||||||
|
control["setting"] = setting
|
||||||
|
valve_control[valve_id] = control
|
||||||
|
elif valves_k is not None:
|
||||||
|
if len(valves) != len(valves_k):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=422, detail="valves 和 valves_k 的数量必须一致"
|
||||||
|
)
|
||||||
valve_opening = {
|
valve_opening = {
|
||||||
valve_id: float(valves_k[idx]) for idx, valve_id in enumerate(valves)
|
valve_id: float(valve_k)
|
||||||
|
for valve_id, valve_k in zip(valves, valves_k)
|
||||||
}
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=422,
|
||||||
|
detail="提供 valves 时必须同时提供 valve_statuses 或 valves_k",
|
||||||
|
)
|
||||||
result = flushing_analysis(
|
result = flushing_analysis(
|
||||||
name=network,
|
name=network,
|
||||||
modify_pattern_start_time=start_time,
|
modify_pattern_start_time=start_time,
|
||||||
modify_total_duration=duration or 900,
|
modify_total_duration=duration or 900,
|
||||||
modify_valve_opening=valve_opening,
|
modify_valve_opening=valve_opening,
|
||||||
|
valve_control=valve_control,
|
||||||
drainage_node_ID=drainage_node_ID,
|
drainage_node_ID=drainage_node_ID,
|
||||||
flushing_flow=flush_flow,
|
flushing_flow=flush_flow,
|
||||||
scheme_name=scheme_name,
|
scheme_name=scheme_name,
|
||||||
@@ -345,7 +330,7 @@ async def fastapi_flushing_analysis(
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/contaminant-simulations", response_class=PlainTextResponse, summary="污染物模拟", description="对管网中的污染物扩散进行模拟,评估污染源对管网的影响范围和浓度分布。支持指定污染源位置、污染浓度和扩散模式。")
|
@router.post("/contaminant-simulations", response_class=PlainTextResponse, summary="污染物模拟", description="对管网中的污染物扩散进行模拟,评估污染源对管网的影响范围和浓度分布。支持指定污染源位置、污染浓度和扩散模式。")
|
||||||
async def fastapi_contaminant_simulation(
|
def fastapi_contaminant_simulation(
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||||
start_time: str = Query(..., description="污染开始时间(ISO 8601格式)"),
|
start_time: str = Query(..., description="污染开始时间(ISO 8601格式)"),
|
||||||
source: str = Query(..., description="污染源节点ID"),
|
source: str = Query(..., description="污染源节点ID"),
|
||||||
@@ -381,50 +366,8 @@ async def fastapi_contaminant_simulation(
|
|||||||
return result or "success"
|
return result or "success"
|
||||||
|
|
||||||
|
|
||||||
@router.post("/water-age-analyses", response_class=PlainTextResponse, summary="水龄分析(高级)", description="高级版本的水龄分析,在指定时间点进行分析,支持自定义模拟持续时间。返回纯文本格式的分析结果。")
|
|
||||||
async def fastapi_age_analysis(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
start_time: str = Query(..., description="分析开始时间(ISO 8601格式)"),
|
|
||||||
duration: int = Query(..., description="模拟持续时间(秒)"),
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
水龄分析(高级版本)
|
|
||||||
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
- **start_time**: 分析开始时间
|
|
||||||
- **duration**: 模拟持续时间(秒)
|
|
||||||
|
|
||||||
分析指定时间段内管网中各节点的水体停留时间。
|
|
||||||
"""
|
|
||||||
result = age_analysis(network, start_time, duration)
|
|
||||||
return result or "success"
|
|
||||||
|
|
||||||
|
|
||||||
# @router.get("/schedulinganalysis/")
|
|
||||||
# async def scheduling_analysis_endpoint(network: str):
|
|
||||||
# return scheduling_analysis(network)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pressure-regulation-calculations", summary="压力调节(基础)", description="对管网的压力进行调节分析,通过控制泵的运行来维持目标节点的目标压力。此为基础版本。")
|
|
||||||
async def pressure_regulation_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
target_node: str = Query(..., description="目标节点ID"),
|
|
||||||
target_pressure: float = Query(..., description="目标压力值(kPa)"),
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
压力调节(基础版本)
|
|
||||||
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
- **target_node**: 目标节点ID
|
|
||||||
- **target_pressure**: 目标压力值(kPa)
|
|
||||||
|
|
||||||
通过泵控制维持目标节点的压力。
|
|
||||||
"""
|
|
||||||
return pressure_regulation(network, target_node, target_pressure)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pressure-regulation-analyses", summary="压力调节(高级)", description="高级版本的压力调节分析,通过JSON请求体提供详细的控制参数,包括固定泵和变速泵的独立控制、水箱初始水位等。")
|
@router.post("/pressure-regulation-analyses", summary="压力调节(高级)", description="高级版本的压力调节分析,通过JSON请求体提供详细的控制参数,包括固定泵和变速泵的独立控制、水箱初始水位等。")
|
||||||
async def fastapi_pressure_regulation(data: PressureRegulation = Body(..., description="压力调节控制参数")) -> str:
|
def fastapi_pressure_regulation(data: PressureRegulation = Body(..., description="压力调节控制参数")) -> str:
|
||||||
"""
|
"""
|
||||||
压力调节(高级版本)
|
压力调节(高级版本)
|
||||||
|
|
||||||
@@ -434,14 +377,16 @@ async def fastapi_pressure_regulation(data: PressureRegulation = Body(..., descr
|
|||||||
- **pump_control**: 泵控制策略字典
|
- **pump_control**: 泵控制策略字典
|
||||||
- **tank_init_level**: 水箱初始水位字典(可选)
|
- **tank_init_level**: 水箱初始水位字典(可选)
|
||||||
- **duration**: 模拟持续时间(秒,可选,默认900)
|
- **duration**: 模拟持续时间(秒,可选,默认900)
|
||||||
- **scheme_name**: 控制方案名称(可选)
|
- **scheme_name**: 控制方案名称
|
||||||
|
|
||||||
支持固定泵和变速泵的独立控制。
|
支持固定泵和变速泵的独立控制。
|
||||||
"""
|
"""
|
||||||
item = data.dict()
|
item = data.model_dump()
|
||||||
simulation.query_corresponding_element_id_and_query_id(item["network"])
|
scada_mappings = simulation.query_corresponding_element_id_and_query_id(
|
||||||
fixed_pumps = set(globals.fixed_pumps_id.keys())
|
item["network"]
|
||||||
variable_pumps = set(globals.variable_pumps_id.keys())
|
)
|
||||||
|
fixed_pumps = set(scada_mappings.fixed_pumps)
|
||||||
|
variable_pumps = set(scada_mappings.variable_pumps)
|
||||||
fixed_pump_pattern: dict[str, list] = {}
|
fixed_pump_pattern: dict[str, list] = {}
|
||||||
variable_pump_pattern: dict[str, list] = {}
|
variable_pump_pattern: dict[str, list] = {}
|
||||||
for pump_id, values in item["pump_control"].items():
|
for pump_id, values in item["pump_control"].items():
|
||||||
@@ -457,291 +402,13 @@ async def fastapi_pressure_regulation(data: PressureRegulation = Body(..., descr
|
|||||||
modify_fixed_pump_pattern=fixed_pump_pattern or None,
|
modify_fixed_pump_pattern=fixed_pump_pattern or None,
|
||||||
modify_variable_pump_pattern=variable_pump_pattern or None,
|
modify_variable_pump_pattern=variable_pump_pattern or None,
|
||||||
scheme_name=item["scheme_name"],
|
scheme_name=item["scheme_name"],
|
||||||
)
|
scada_mappings=scada_mappings,
|
||||||
return "success"
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/project-managements", summary="项目管理(高级)", description="高级版本的项目管理,通过JSON请求体提供详细的控制参数,包括泵控制策略、水箱初始水位和区域需水量控制。")
|
|
||||||
async def fastapi_project_management(data: ProjectManagement = Body(..., description="项目管理控制参数")) -> str:
|
|
||||||
"""
|
|
||||||
项目管理(高级版本)
|
|
||||||
|
|
||||||
请求体参数:
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
- **start_time**: 管理开始时间
|
|
||||||
- **pump_control**: 泵控制策略字典
|
|
||||||
- **tank_init_level**: 水箱初始水位字典(可选)
|
|
||||||
- **region_demand**: 区域需水量控制字典(可选)
|
|
||||||
|
|
||||||
支持多维度的项目管理。
|
|
||||||
"""
|
|
||||||
item = data.dict()
|
|
||||||
return project_management(
|
|
||||||
prj_name=item["network"],
|
|
||||||
start_datetime=item["start_time"],
|
|
||||||
pump_control=item["pump_control"],
|
|
||||||
tank_initial_level_control=item["tank_init_level"],
|
|
||||||
region_demand_control=item["region_demand"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# @router.get("/dailyschedulinganalysis/")
|
|
||||||
# async def daily_scheduling_analysis_endpoint(network: str):
|
|
||||||
# return daily_scheduling_analysis(network)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/scheduling-analyses", summary="排程分析", description="对管网的供水排程进行分析,优化泵的运行时间和出水流量,平衡水厂出水、水箱进出水,满足用户需求。")
|
|
||||||
async def fastapi_scheduling_analysis(data: SchedulingAnalysis = Body(..., description="排程分析参数")) -> str:
|
|
||||||
"""
|
|
||||||
排程分析
|
|
||||||
|
|
||||||
请求体参数:
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
- **start_time**: 分析开始时间
|
|
||||||
- **pump_control**: 泵控制策略字典
|
|
||||||
- **tank_id**: 水箱ID
|
|
||||||
- **water_plant_output_id**: 水厂出水ID
|
|
||||||
- **time_delta**: 时间步长(秒,可选,默认300)
|
|
||||||
|
|
||||||
用于优化供水排程。
|
|
||||||
"""
|
|
||||||
item = data.dict()
|
|
||||||
return scheduling_simulation(
|
|
||||||
item["network"],
|
|
||||||
item["start_time"],
|
|
||||||
item["pump_control"],
|
|
||||||
item["tank_id"],
|
|
||||||
item["water_plant_output_id"],
|
|
||||||
item["time_delta"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/daily-scheduling-analyses", summary="日排程分析", description="对管网的每日供水排程进行分析,优化水库、水厂、水箱和用户需求的协调,制定合理的每日排程方案。")
|
|
||||||
async def fastapi_daily_scheduling_analysis(data: DailySchedulingAnalysis = Body(..., description="日排程分析参数")) -> str:
|
|
||||||
"""
|
|
||||||
日排程分析
|
|
||||||
|
|
||||||
请求体参数:
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
- **start_time**: 分析开始时间
|
|
||||||
- **pump_control**: 泵控制策略字典
|
|
||||||
- **reservoir_id**: 水库ID
|
|
||||||
- **tank_id**: 水箱ID
|
|
||||||
- **water_plant_output_id**: 水厂出水ID
|
|
||||||
- **time_delta**: 时间步长(秒,可选,默认300)
|
|
||||||
|
|
||||||
用于制定每日供水排程方案。
|
|
||||||
"""
|
|
||||||
item = data.dict()
|
|
||||||
return daily_scheduling_simulation(
|
|
||||||
item["network"],
|
|
||||||
item["start_time"],
|
|
||||||
item["pump_control"],
|
|
||||||
item["reservoir_id"],
|
|
||||||
item["tank_id"],
|
|
||||||
item["water_plant_output_id"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# @router.get("/pumpfailure/")
|
|
||||||
# async def pump_failure_endpoint(network: str, pump_id: str, time: str):
|
|
||||||
# return pump_failure(network, pump_id, time)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pump-failure-events", summary="泵故障管理", description="记录和管理泵的故障状态,包括故障发生时间和受影响的泵列表。系统将记录故障日志并更新泵状态。")
|
|
||||||
async def fastapi_pump_failure(data: PumpFailureState = Body(..., description="泵故障状态信息")) -> str:
|
|
||||||
"""
|
|
||||||
泵故障管理
|
|
||||||
|
|
||||||
请求体参数:
|
|
||||||
- **time**: 故障发生时间
|
|
||||||
- **pump_status**: 泵状态字典,包含第一阶段和第二阶段泵的故障状态
|
|
||||||
|
|
||||||
系统将验证泵信息的有效性并更新故障状态文件。
|
|
||||||
"""
|
|
||||||
item = data.dict()
|
|
||||||
with open("./pump_failure_message.txt", "a", encoding="utf-8-sig") as f1:
|
|
||||||
f1.write("[{}] {}\n".format(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), item))
|
|
||||||
with open("./pump_failure_status.txt", "r", encoding="utf-8-sig") as f2:
|
|
||||||
lines = f2.readlines()
|
|
||||||
first_stage_pump_status_dict = json.loads(json.dumps(eval(lines[0])))
|
|
||||||
second_stage_pump_status_dict = json.loads(json.dumps(eval(lines[-1])))
|
|
||||||
pump_status_dict = {
|
|
||||||
"first": first_stage_pump_status_dict,
|
|
||||||
"second": second_stage_pump_status_dict,
|
|
||||||
}
|
|
||||||
status_info = item.copy()
|
|
||||||
for pump_type in status_info["pump_status"].keys():
|
|
||||||
if pump_type in pump_status_dict.keys():
|
|
||||||
if all(
|
|
||||||
pump_id in pump_status_dict[pump_type].keys()
|
|
||||||
for pump_id in status_info["pump_status"][pump_type].keys()
|
|
||||||
):
|
|
||||||
for pump_id in status_info["pump_status"][pump_type].keys():
|
|
||||||
pump_status_dict[pump_type][pump_id] = int(
|
|
||||||
status_info["pump_status"][pump_type][pump_id]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return json.dumps("ERROR: Wrong Pump ID")
|
|
||||||
else:
|
|
||||||
return json.dumps("ERROR: Wrong Pump Type")
|
|
||||||
with open("./pump_failure_status.txt", "w", encoding="utf-8-sig") as f2_:
|
|
||||||
f2_.write(
|
|
||||||
"{}\n{}".format(pump_status_dict["first"], pump_status_dict["second"])
|
|
||||||
)
|
|
||||||
return json.dumps("SUCCESS")
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pressure-sensor-placement-sensitivity-calculations", summary="压力传感器放置-灵敏度分析(基础)", description="基于灵敏度分析方法,为指定管网项目确定最优的压力传感器放置位置。此为基础版本。")
|
|
||||||
async def pressure_sensor_placement_sensitivity_endpoint(
|
|
||||||
name: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
scheme_name: str = Query(..., description="放置方案名称"),
|
|
||||||
sensor_number: int = Query(..., description="传感器数量"),
|
|
||||||
min_diameter: int = Query(..., description="最小管径限制(毫米)"),
|
|
||||||
username: str = Query(..., description="用户名"),
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
压力传感器放置-灵敏度分析(基础版本)
|
|
||||||
|
|
||||||
- **name**: 管网名称(或数据库名称)
|
|
||||||
- **scheme_name**: 放置方案名称
|
|
||||||
- **sensor_number**: 传感器数量
|
|
||||||
- **min_diameter**: 最小管径限制(毫米)
|
|
||||||
- **username**: 用户名
|
|
||||||
|
|
||||||
基于灵敏度分析方法确定传感器放置位置。
|
|
||||||
"""
|
|
||||||
return pressure_sensor_placement_sensitivity(
|
|
||||||
name, scheme_name, sensor_number, min_diameter, username
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pressure-sensor-placement-sensitivities", summary="压力传感器放置-灵敏度分析(高级)", description="高级版本的压力传感器放置分析,通过JSON请求体提供详细参数。基于灵敏度分析方法确定最优放置位置。")
|
|
||||||
async def fastapi_pressure_sensor_placement_sensitivity(
|
|
||||||
data: PressureSensorPlacement = Body(..., description="传感器放置分析参数"),
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
压力传感器放置-灵敏度分析(高级版本)
|
|
||||||
|
|
||||||
请求体参数:
|
|
||||||
- **name**: 管网名称(或数据库名称)
|
|
||||||
- **scheme_name**: 放置方案名称
|
|
||||||
- **sensor_number**: 传感器数量
|
|
||||||
- **min_diameter**: 最小管径限制(毫米)
|
|
||||||
- **username**: 用户名
|
|
||||||
|
|
||||||
基于灵敏度分析方法确定压力传感器的最优放置位置。
|
|
||||||
"""
|
|
||||||
item = data.dict()
|
|
||||||
pressure_sensor_placement_sensitivity(
|
|
||||||
name=item["name"],
|
|
||||||
scheme_name=item["scheme_name"],
|
|
||||||
sensor_number=item["sensor_number"],
|
|
||||||
min_diameter=item["min_diameter"],
|
|
||||||
username=item["username"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pressure-sensor-placement-kmeans-calculations", summary="压力传感器放置-KMeans聚类分析(基础)", description="基于KMeans聚类算法,为指定管网项目确定压力传感器的最优放置位置。此为基础版本。")
|
|
||||||
async def pressure_sensor_placement_kmeans_endpoint(
|
|
||||||
name: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
scheme_name: str = Query(..., description="放置方案名称"),
|
|
||||||
sensor_number: int = Query(..., description="传感器数量"),
|
|
||||||
min_diameter: int = Query(..., description="最小管径限制(毫米)"),
|
|
||||||
username: str = Query(..., description="用户名"),
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
压力传感器放置-KMeans聚类分析(基础版本)
|
|
||||||
|
|
||||||
- **name**: 管网名称(或数据库名称)
|
|
||||||
- **scheme_name**: 放置方案名称
|
|
||||||
- **sensor_number**: 传感器数量
|
|
||||||
- **min_diameter**: 最小管径限制(毫米)
|
|
||||||
- **username**: 用户名
|
|
||||||
|
|
||||||
基于KMeans聚类算法确定传感器放置位置。
|
|
||||||
"""
|
|
||||||
return pressure_sensor_placement_kmeans(
|
|
||||||
name, scheme_name, sensor_number, min_diameter, username
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pressure-sensor-placement-kmeans", summary="压力传感器放置-KMeans聚类分析(高级)", description="高级版本的压力传感器放置分析,通过JSON请求体提供详细参数。基于KMeans聚类算法确定最优放置位置。")
|
|
||||||
async def fastapi_pressure_sensor_placement_kmeans(
|
|
||||||
data: PressureSensorPlacement = Body(..., description="传感器放置分析参数"),
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
压力传感器放置-KMeans聚类分析(高级版本)
|
|
||||||
|
|
||||||
请求体参数:
|
|
||||||
- **name**: 管网名称(或数据库名称)
|
|
||||||
- **scheme_name**: 放置方案名称
|
|
||||||
- **sensor_number**: 传感器数量
|
|
||||||
- **min_diameter**: 最小管径限制(毫米)
|
|
||||||
- **username**: 用户名
|
|
||||||
|
|
||||||
基于KMeans聚类算法确定压力传感器的最优放置位置。
|
|
||||||
"""
|
|
||||||
item = data.dict()
|
|
||||||
pressure_sensor_placement_kmeans(
|
|
||||||
name=item["name"],
|
|
||||||
scheme_name=item["scheme_name"],
|
|
||||||
sensor_number=item["sensor_number"],
|
|
||||||
min_diameter=item["min_diameter"],
|
|
||||||
username=item["username"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/sensor-placement-schemes", summary="传感器放置方案创建", description="创建新的传感器放置方案,支持灵敏度分析和KMeans聚类两种方法。根据指定的方法自动计算最优的传感器放置位置。")
|
|
||||||
async def fastapi_pressure_sensor_placement(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
scheme_name: str = Query(..., description="放置方案名称"),
|
|
||||||
sensor_type: str = Query(..., description="传感器类型"),
|
|
||||||
method: str = Query(..., description="放置方法('sensitivity'或'kmeans')"),
|
|
||||||
sensor_count: int = Query(..., description="传感器数量"),
|
|
||||||
min_diameter: int = Query(0, description="最小管径限制(毫米),默认0"),
|
|
||||||
user_name: str = Query(..., description="用户名"),
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
传感器放置方案创建
|
|
||||||
|
|
||||||
- **network**: 管网名称(或数据库名称)
|
|
||||||
- **scheme_name**: 放置方案名称
|
|
||||||
- **sensor_type**: 传感器类型
|
|
||||||
- **method**: 放置方法('sensitivity'或'kmeans')
|
|
||||||
- **sensor_count**: 传感器数量
|
|
||||||
- **min_diameter**: 最小管径限制(毫米,默认0)
|
|
||||||
- **user_name**: 用户名
|
|
||||||
|
|
||||||
支持两种放置方法:
|
|
||||||
- sensitivity: 基于灵敏度分析
|
|
||||||
- kmeans: 基于KMeans聚类
|
|
||||||
"""
|
|
||||||
if method not in ["sensitivity", "kmeans"]:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400, detail="Invalid method. Must be 'sensitivity' or 'kmeans'"
|
|
||||||
)
|
|
||||||
if method == "sensitivity":
|
|
||||||
pressure_sensor_placement_sensitivity(
|
|
||||||
name=network,
|
|
||||||
scheme_name=scheme_name,
|
|
||||||
sensor_number=sensor_count,
|
|
||||||
min_diameter=min_diameter,
|
|
||||||
username=user_name,
|
|
||||||
)
|
|
||||||
elif method == "kmeans":
|
|
||||||
pressure_sensor_placement_kmeans(
|
|
||||||
name=network,
|
|
||||||
scheme_name=scheme_name,
|
|
||||||
sensor_number=sensor_count,
|
|
||||||
min_diameter=min_diameter,
|
|
||||||
username=user_name,
|
|
||||||
)
|
)
|
||||||
return "success"
|
return "success"
|
||||||
|
|
||||||
|
|
||||||
@router.post("/simulation-runs", summary="手动运行日期指定模拟", description="根据指定的开始时间和持续时间,手动运行水力模拟。开始时间必须是显式带时区的 ISO 8601 / RFC3339 时间。")
|
@router.post("/simulation-runs", summary="手动运行日期指定模拟", description="根据指定的开始时间和持续时间,手动运行水力模拟。开始时间必须是显式带时区的 ISO 8601 / RFC3339 时间。")
|
||||||
async def fastapi_run_simulation_manually_by_date(
|
def fastapi_run_simulation_manually_by_date(
|
||||||
data: RunSimulationManuallyByDate = Body(..., description="模拟运行参数"),
|
data: RunSimulationManuallyByDate = Body(..., description="模拟运行参数"),
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
@@ -757,31 +424,6 @@ async def fastapi_run_simulation_manually_by_date(
|
|||||||
"""
|
"""
|
||||||
item = data.model_dump()
|
item = data.model_dump()
|
||||||
try:
|
try:
|
||||||
simulation.query_corresponding_element_id_and_query_id(item["name"])
|
|
||||||
simulation.query_corresponding_pattern_id_and_query_id(item["name"])
|
|
||||||
region_result = simulation.query_non_realtime_region(item["name"])
|
|
||||||
globals.source_outflow_region_id = simulation.get_source_outflow_region_id(
|
|
||||||
item["name"], region_result
|
|
||||||
)
|
|
||||||
globals.realtime_region_pipe_flow_and_demand_id = (
|
|
||||||
simulation.query_realtime_region_pipe_flow_and_demand_id(
|
|
||||||
item["name"], region_result
|
|
||||||
)
|
|
||||||
)
|
|
||||||
globals.pipe_flow_region_patterns = simulation.query_pipe_flow_region_patterns(
|
|
||||||
item["name"]
|
|
||||||
)
|
|
||||||
globals.non_realtime_region_patterns = (
|
|
||||||
simulation.query_non_realtime_region_patterns(item["name"], region_result)
|
|
||||||
)
|
|
||||||
(
|
|
||||||
globals.source_outflow_region_patterns,
|
|
||||||
globals.realtime_region_pipe_flow_and_demand_patterns,
|
|
||||||
) = simulation.get_realtime_region_patterns(
|
|
||||||
item["name"],
|
|
||||||
globals.source_outflow_region_id,
|
|
||||||
globals.realtime_region_pipe_flow_and_demand_id,
|
|
||||||
)
|
|
||||||
start_time = parse_utc_time(item["start_time"], field_name="start_time")
|
start_time = parse_utc_time(item["start_time"], field_name="start_time")
|
||||||
run_simulation_manually_by_date(
|
run_simulation_manually_by_date(
|
||||||
item["name"], start_time, item["duration"]
|
item["name"], start_time, item["duration"]
|
||||||
|
|||||||
@@ -1,199 +0,0 @@
|
|||||||
from fastapi import APIRouter, Depends, Request, Query
|
|
||||||
from app.auth.permissions import SIMULATION_RUN, require_permission
|
|
||||||
from app.services.tjnetwork import (
|
|
||||||
ChangeSet,
|
|
||||||
get_current_operation,
|
|
||||||
execute_undo,
|
|
||||||
execute_redo,
|
|
||||||
list_snapshot,
|
|
||||||
have_snapshot,
|
|
||||||
have_snapshot_for_operation,
|
|
||||||
have_snapshot_for_current_operation,
|
|
||||||
take_snapshot_for_operation,
|
|
||||||
take_snapshot_for_current_operation,
|
|
||||||
take_snapshot,
|
|
||||||
pick_snapshot,
|
|
||||||
pick_operation,
|
|
||||||
sync_with_server,
|
|
||||||
execute_batch_commands,
|
|
||||||
execute_batch_command,
|
|
||||||
get_restore_operation,
|
|
||||||
set_restore_operation,
|
|
||||||
)
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
@router.get("/current-operation-ids", summary="获取当前操作ID", description="获取网络当前的操作ID")
|
|
||||||
async def get_current_operation_id_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> int:
|
|
||||||
"""
|
|
||||||
获取当前操作ID
|
|
||||||
|
|
||||||
返回网络当前正在执行的操作ID
|
|
||||||
"""
|
|
||||||
return get_current_operation(network)
|
|
||||||
|
|
||||||
@router.post("/undos", summary="撤销操作", description="撤销网络上最后的一个操作")
|
|
||||||
async def undo_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")):
|
|
||||||
"""
|
|
||||||
撤销操作
|
|
||||||
|
|
||||||
撤销网络上最近执行的一个操作
|
|
||||||
"""
|
|
||||||
return execute_undo(network)
|
|
||||||
|
|
||||||
@router.post("/redos", summary="重做操作", description="重做网络上被撤销的操作")
|
|
||||||
async def redo_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")):
|
|
||||||
"""
|
|
||||||
重做操作
|
|
||||||
|
|
||||||
重做网络上被撤销的操作
|
|
||||||
"""
|
|
||||||
return execute_redo(network)
|
|
||||||
|
|
||||||
@router.get("/snapshots", summary="获取快照列表", description="获取网络中的所有快照")
|
|
||||||
async def list_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[tuple[int, str]]:
|
|
||||||
"""
|
|
||||||
获取快照列表
|
|
||||||
|
|
||||||
返回网络中所有可用的快照及其信息
|
|
||||||
"""
|
|
||||||
return list_snapshot(network)
|
|
||||||
|
|
||||||
@router.get("/snapshots/existence", summary="检查快照是否存在", description="检查指定标签的快照是否存在")
|
|
||||||
async def have_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签")) -> bool:
|
|
||||||
"""
|
|
||||||
检查快照是否存在
|
|
||||||
|
|
||||||
返回指定标签的快照是否存在
|
|
||||||
"""
|
|
||||||
return have_snapshot(network, tag)
|
|
||||||
|
|
||||||
@router.get("/snapshot-for-operations", summary="检查操作快照是否存在", description="检查指定操作ID的快照是否存在")
|
|
||||||
async def have_snapshot_for_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), operation: int = Query(..., description="操作ID")) -> bool:
|
|
||||||
"""
|
|
||||||
检查操作快照是否存在
|
|
||||||
|
|
||||||
返回指定操作ID的快照是否存在
|
|
||||||
"""
|
|
||||||
return have_snapshot_for_operation(network, operation)
|
|
||||||
|
|
||||||
@router.get("/snapshot-for-current-operations", summary="检查当前操作快照是否存在", description="检查当前操作的快照是否存在")
|
|
||||||
async def have_snapshot_for_current_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> bool:
|
|
||||||
"""
|
|
||||||
检查当前操作快照是否存在
|
|
||||||
|
|
||||||
返回当前操作的快照是否存在
|
|
||||||
"""
|
|
||||||
return have_snapshot_for_current_operation(network)
|
|
||||||
|
|
||||||
@router.post("/snapshot-for-operations", summary="为操作创建快照", description="为指定的操作创建快照")
|
|
||||||
async def take_snapshot_for_operation_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
operation: int = Query(..., description="操作ID"),
|
|
||||||
tag: str = Query(..., description="快照标签")
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
为操作创建快照
|
|
||||||
|
|
||||||
为指定操作创建一个带标签的快照
|
|
||||||
"""
|
|
||||||
return take_snapshot_for_operation(network, operation, tag)
|
|
||||||
|
|
||||||
@router.post("/snapshot-for-current-operations", summary="为当前操作创建快照", description="为当前操作创建快照")
|
|
||||||
async def take_snapshot_for_current_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签")) -> None:
|
|
||||||
"""
|
|
||||||
为当前操作创建快照
|
|
||||||
|
|
||||||
为网络当前操作创建一个快照
|
|
||||||
"""
|
|
||||||
return take_snapshot_for_current_operation(network, tag)
|
|
||||||
|
|
||||||
@router.post("/snapshots", summary="创建快照", description="为网络创建一个快照")
|
|
||||||
async def take_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签")) -> None:
|
|
||||||
"""
|
|
||||||
创建快照
|
|
||||||
|
|
||||||
为网络创建一个带标签的快照
|
|
||||||
"""
|
|
||||||
return take_snapshot(network, tag)
|
|
||||||
|
|
||||||
@router.patch("/snapshots", summary="选择快照", description="选择并恢复到指定的快照", response_model=None)
|
|
||||||
async def pick_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签"), discard: bool = Query(False, description="是否丢弃当前更改")) -> ChangeSet:
|
|
||||||
"""
|
|
||||||
选择快照
|
|
||||||
|
|
||||||
选择并恢复到指定的快照
|
|
||||||
"""
|
|
||||||
return pick_snapshot(network, tag, discard)
|
|
||||||
|
|
||||||
@router.patch("/operations", summary="选择操作", description="选择并恢复到指定的操作", response_model=None)
|
|
||||||
async def pick_operation_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
operation: int = Query(..., description="操作ID"),
|
|
||||||
discard: bool = Query(False, description="是否丢弃当前更改")
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""
|
|
||||||
选择操作
|
|
||||||
|
|
||||||
选择并恢复到指定的操作
|
|
||||||
"""
|
|
||||||
return pick_operation(network, operation, discard)
|
|
||||||
|
|
||||||
@router.post("/with-servers", summary="与服务器同步", description="将网络与服务器同步到指定操作", response_model=None)
|
|
||||||
async def sync_with_server_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
operation: int = Query(..., description="目标操作ID"),
|
|
||||||
_=Depends(require_permission(SIMULATION_RUN)),
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""
|
|
||||||
与服务器同步
|
|
||||||
|
|
||||||
将网络与服务器同步到指定的操作
|
|
||||||
"""
|
|
||||||
return sync_with_server(network, operation)
|
|
||||||
|
|
||||||
@router.post("/network-command-batches", summary="执行批量命令", description="执行多个网络操作命令", response_model=None)
|
|
||||||
async def execute_batch_commands_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), req: Request = None) -> ChangeSet:
|
|
||||||
"""
|
|
||||||
执行批量命令
|
|
||||||
|
|
||||||
在网络上执行多个操作命令
|
|
||||||
"""
|
|
||||||
jo_root = await req.json()
|
|
||||||
cs: ChangeSet = ChangeSet()
|
|
||||||
cs.operations = jo_root["operations"]
|
|
||||||
rcs = execute_batch_commands(network, cs)
|
|
||||||
return rcs
|
|
||||||
|
|
||||||
@router.post("/network-command-batches/compressed", summary="执行压缩批量命令", description="执行压缩的批量命令", response_model=None)
|
|
||||||
async def execute_compressed_batch_commands_endpoint(
|
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
req: Request = None
|
|
||||||
) -> ChangeSet:
|
|
||||||
"""
|
|
||||||
执行压缩批量命令
|
|
||||||
|
|
||||||
执行压缩格式的批量命令
|
|
||||||
"""
|
|
||||||
jo_root = await req.json()
|
|
||||||
cs: ChangeSet = ChangeSet()
|
|
||||||
cs.operations = jo_root["operations"]
|
|
||||||
return execute_batch_command(network, cs)
|
|
||||||
|
|
||||||
@router.get("/restore-operations", summary="获取恢复操作ID", description="获取网络的恢复操作ID")
|
|
||||||
async def get_restore_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> int:
|
|
||||||
"""
|
|
||||||
获取恢复操作ID
|
|
||||||
|
|
||||||
返回网络的恢复操作ID
|
|
||||||
"""
|
|
||||||
return get_restore_operation(network)
|
|
||||||
|
|
||||||
@router.patch("/restore-operations", summary="设置恢复操作ID", description="设置网络的恢复操作ID")
|
|
||||||
async def set_restore_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), operation: int = Query(..., description="操作ID")) -> None:
|
|
||||||
"""
|
|
||||||
设置恢复操作ID
|
|
||||||
|
|
||||||
设置网络的恢复操作ID
|
|
||||||
"""
|
|
||||||
return set_restore_operation(network, operation)
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from psycopg import AsyncConnection
|
||||||
|
|
||||||
|
from app.infra.db.timescaledb.repositories.analysis import AnalysisResultsRepository
|
||||||
|
|
||||||
|
from .dependencies import get_timescale_connection
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/timeseries/analysis/runs/{run_id}/nodes/{node_id}")
|
||||||
|
async def get_analysis_node_series(
|
||||||
|
run_id: UUID,
|
||||||
|
node_id: str,
|
||||||
|
start_time: datetime = Query(...),
|
||||||
|
end_time: datetime = Query(...),
|
||||||
|
field: str = Query(...),
|
||||||
|
conn: AsyncConnection = Depends(get_timescale_connection),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
return await AnalysisResultsRepository.get_node_series(
|
||||||
|
conn, run_id, node_id, start_time, end_time, field
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/timeseries/analysis/runs/{run_id}/links/{link_id}")
|
||||||
|
async def get_analysis_link_series(
|
||||||
|
run_id: UUID,
|
||||||
|
link_id: str,
|
||||||
|
start_time: datetime = Query(...),
|
||||||
|
end_time: datetime = Query(...),
|
||||||
|
field: str = Query(...),
|
||||||
|
conn: AsyncConnection = Depends(get_timescale_connection),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
return await AnalysisResultsRepository.get_link_series(
|
||||||
|
conn, run_id, link_id, start_time, end_time, field
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/timeseries/analysis/runs/{run_id}/values")
|
||||||
|
async def get_analysis_values_at_time(
|
||||||
|
run_id: UUID,
|
||||||
|
result_time: datetime = Query(...),
|
||||||
|
element_type: str = Query(..., pattern="^(node|link)$"),
|
||||||
|
field: str = Query(...),
|
||||||
|
conn: AsyncConnection = Depends(get_timescale_connection),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
return await AnalysisResultsRepository.get_values_at_time(
|
||||||
|
conn, run_id, element_type, result_time, field
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from psycopg import AsyncConnection
|
from psycopg import AsyncConnection
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from app.infra.db.timescaledb.composite_queries import CompositeQueries
|
from app.services.timeseries_analysis import TimeseriesAnalysisService
|
||||||
from .dependencies import get_timescale_connection, get_postgres_connection
|
from .dependencies import get_timescale_connection, get_postgres_connection
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -13,8 +14,7 @@ async def get_scada_associated_simulation_data(
|
|||||||
start_time: datetime = Query(..., description="查询开始时间"),
|
start_time: datetime = Query(..., description="查询开始时间"),
|
||||||
end_time: datetime = Query(..., description="查询结束时间"),
|
end_time: datetime = Query(..., description="查询结束时间"),
|
||||||
device_ids: str = Query(..., description="SCADA设备ID列表,逗号分隔"),
|
device_ids: str = Query(..., description="SCADA设备ID列表,逗号分隔"),
|
||||||
scheme_type: str = Query(None, description="方案类型,若为空则查询实时数据"),
|
run_id: UUID | None = Query(None, description="分析运行 ID;为空时查询实时数据"),
|
||||||
scheme_name: str = Query(None, description="方案名称,若为空则查询实时数据"),
|
|
||||||
timescale_conn: AsyncConnection = Depends(get_timescale_connection),
|
timescale_conn: AsyncConnection = Depends(get_timescale_connection),
|
||||||
postgres_conn: AsyncConnection = Depends(get_postgres_connection),
|
postgres_conn: AsyncConnection = Depends(get_postgres_connection),
|
||||||
):
|
):
|
||||||
@@ -22,14 +22,13 @@ async def get_scada_associated_simulation_data(
|
|||||||
获取SCADA关联的link/node模拟值
|
获取SCADA关联的link/node模拟值
|
||||||
|
|
||||||
根据传入的SCADA device_ids,找到关联的link/node,
|
根据传入的SCADA device_ids,找到关联的link/node,
|
||||||
并根据对应的type,查询对应的模拟数据。支持查询实时或方案数据。
|
并根据对应的type,查询对应的模拟数据。支持查询实时或分析运行数据。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
start_time: 查询开始时间
|
start_time: 查询开始时间
|
||||||
end_time: 查询结束时间
|
end_time: 查询结束时间
|
||||||
device_ids: SCADA设备ID列表,用逗号分隔
|
device_ids: SCADA设备ID列表,用逗号分隔
|
||||||
scheme_type: 方案类型,若为空则查询实时数据
|
run_id: 分析运行 ID,若为空则查询实时数据
|
||||||
scheme_name: 方案名称,若为空则查询实时数据
|
|
||||||
timescale_conn: TimescaleDB连接
|
timescale_conn: TimescaleDB连接
|
||||||
postgres_conn: PostgreSQL连接
|
postgres_conn: PostgreSQL连接
|
||||||
|
|
||||||
@@ -46,19 +45,18 @@ async def get_scada_associated_simulation_data(
|
|||||||
else []
|
else []
|
||||||
)
|
)
|
||||||
|
|
||||||
if scheme_type and scheme_name:
|
if run_id is not None:
|
||||||
result = await CompositeQueries.get_scada_associated_scheme_simulation_data(
|
result = await TimeseriesAnalysisService.get_scada_associated_analysis_simulation_data(
|
||||||
timescale_conn,
|
timescale_conn,
|
||||||
postgres_conn,
|
postgres_conn,
|
||||||
device_ids_list,
|
device_ids_list,
|
||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
scheme_type,
|
run_id,
|
||||||
scheme_name,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result = (
|
result = (
|
||||||
await CompositeQueries.get_scada_associated_realtime_simulation_data(
|
await TimeseriesAnalysisService.get_scada_associated_realtime_simulation_data(
|
||||||
timescale_conn,
|
timescale_conn,
|
||||||
postgres_conn,
|
postgres_conn,
|
||||||
device_ids_list,
|
device_ids_list,
|
||||||
@@ -80,23 +78,21 @@ async def get_feature_simulation_data(
|
|||||||
feature_infos: str = Query(
|
feature_infos: str = Query(
|
||||||
..., description="特征信息,格式: id1:type1,id2:type2,type为pipe(管道)或junction(节点)"
|
..., description="特征信息,格式: id1:type1,id2:type2,type为pipe(管道)或junction(节点)"
|
||||||
),
|
),
|
||||||
scheme_type: str = Query(None, description="方案类型,若为空则查询实时数据"),
|
run_id: UUID | None = Query(None, description="分析运行 ID;为空时查询实时数据"),
|
||||||
scheme_name: str = Query(None, description="方案名称,若为空则查询实时数据"),
|
|
||||||
timescale_conn: AsyncConnection = Depends(get_timescale_connection),
|
timescale_conn: AsyncConnection = Depends(get_timescale_connection),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
获取link/node模拟值
|
获取link/node模拟值
|
||||||
|
|
||||||
根据传入的featureInfos,找到关联的link/node,
|
根据传入的featureInfos,找到关联的link/node,
|
||||||
并根据对应的type,查询对应的模拟数据。支持查询实时或方案数据。
|
并根据对应的type,查询对应的模拟数据。支持查询实时或分析运行数据。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
start_time: 查询开始时间
|
start_time: 查询开始时间
|
||||||
end_time: 查询结束时间
|
end_time: 查询结束时间
|
||||||
feature_infos: 格式为 "element_id1:type1,element_id2:type2"
|
feature_infos: 格式为 "element_id1:type1,element_id2:type2"
|
||||||
例如: "P1:pipe,J1:junction"
|
例如: "P1:pipe,J1:junction"
|
||||||
scheme_type: 方案类型,若为空则查询实时数据
|
run_id: 分析运行 ID,若为空则查询实时数据
|
||||||
scheme_name: 方案名称,若为空则查询实时数据
|
|
||||||
timescale_conn: TimescaleDB连接
|
timescale_conn: TimescaleDB连接
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -119,17 +115,16 @@ async def get_feature_simulation_data(
|
|||||||
if not feature_infos_list:
|
if not feature_infos_list:
|
||||||
raise HTTPException(status_code=400, detail="feature_infos cannot be empty")
|
raise HTTPException(status_code=400, detail="feature_infos cannot be empty")
|
||||||
|
|
||||||
if scheme_type and scheme_name:
|
if run_id is not None:
|
||||||
result = await CompositeQueries.get_scheme_simulation_data(
|
result = await TimeseriesAnalysisService.get_analysis_simulation_data(
|
||||||
timescale_conn,
|
timescale_conn,
|
||||||
feature_infos_list,
|
feature_infos_list,
|
||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
scheme_type,
|
run_id,
|
||||||
scheme_name,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result = await CompositeQueries.get_realtime_simulation_data(
|
result = await TimeseriesAnalysisService.get_realtime_simulation_data(
|
||||||
timescale_conn,
|
timescale_conn,
|
||||||
feature_infos_list,
|
feature_infos_list,
|
||||||
start_time,
|
start_time,
|
||||||
@@ -173,7 +168,7 @@ async def get_element_associated_scada_data(
|
|||||||
HTTPException: 当查询参数无效时返回400错误,未找到关联数据返回404错误
|
HTTPException: 当查询参数无效时返回400错误,未找到关联数据返回404错误
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
result = await CompositeQueries.get_element_associated_scada_data(
|
result = await TimeseriesAnalysisService.get_element_associated_scada_data(
|
||||||
timescale_conn, postgres_conn, element_id, start_time, end_time, use_cleaned
|
timescale_conn, postgres_conn, element_id, start_time, end_time, use_cleaned
|
||||||
)
|
)
|
||||||
if result is None:
|
if result is None:
|
||||||
@@ -221,7 +216,7 @@ async def clean_scada_data(
|
|||||||
if device_ids
|
if device_ids
|
||||||
else []
|
else []
|
||||||
)
|
)
|
||||||
return await CompositeQueries.clean_scada_data(
|
return await TimeseriesAnalysisService.clean_scada_data(
|
||||||
timescale_conn, postgres_conn, device_ids_list, start_time, end_time
|
timescale_conn, postgres_conn, device_ids_list, start_time, end_time
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -231,8 +226,8 @@ async def clean_scada_data(
|
|||||||
@router.get("/pipeline-health-predictions", summary="预测管道健康状况")
|
@router.get("/pipeline-health-predictions", summary="预测管道健康状况")
|
||||||
async def predict_pipeline_health(
|
async def predict_pipeline_health(
|
||||||
query_time: datetime = Query(..., description="查询时间"),
|
query_time: datetime = Query(..., description="查询时间"),
|
||||||
network_name: str = Query(..., description="管网名称(或数据库名称)"),
|
|
||||||
timescale_conn: AsyncConnection = Depends(get_timescale_connection),
|
timescale_conn: AsyncConnection = Depends(get_timescale_connection),
|
||||||
|
postgres_conn: AsyncConnection = Depends(get_postgres_connection),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
预测管道健康状况
|
预测管道健康状况
|
||||||
@@ -242,7 +237,6 @@ async def predict_pipeline_health(
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
query_time: 查询时间
|
query_time: 查询时间
|
||||||
network_name: 管网名称(或数据库名称)
|
|
||||||
timescale_conn: TimescaleDB连接
|
timescale_conn: TimescaleDB连接
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -252,8 +246,8 @@ async def predict_pipeline_health(
|
|||||||
HTTPException: 当模型文件不存在返回404错误,其他错误返回400或500错误
|
HTTPException: 当模型文件不存在返回404错误,其他错误返回400或500错误
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return await CompositeQueries.predict_pipeline_health(
|
return await TimeseriesAnalysisService.predict_pipeline_health(
|
||||||
timescale_conn, network_name, query_time
|
timescale_conn, postgres_conn, query_time
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
|
|||||||
from typing import List
|
from typing import List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from psycopg import AsyncConnection
|
from psycopg import AsyncConnection
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from app.infra.db.timescaledb.repositories.realtime import RealtimeRepository
|
from app.infra.db.timescaledb.repositories.realtime import RealtimeRepository
|
||||||
from .dependencies import get_timescale_connection
|
from .dependencies import get_timescale_connection
|
||||||
@@ -13,9 +14,31 @@ TIME_RANGE_START_DESC = f"时间范围开始时间。{TIME_WITH_TZ_DESC}"
|
|||||||
TIME_RANGE_END_DESC = f"时间范围结束时间。{TIME_WITH_TZ_DESC}"
|
TIME_RANGE_END_DESC = f"时间范围结束时间。{TIME_WITH_TZ_DESC}"
|
||||||
|
|
||||||
|
|
||||||
|
class RealtimeLinkBatchItem(BaseModel):
|
||||||
|
time: datetime
|
||||||
|
id: str
|
||||||
|
flow: float | None = None
|
||||||
|
friction: float | None = None
|
||||||
|
headloss: float | None = None
|
||||||
|
quality: float | None = None
|
||||||
|
reaction: float | None = None
|
||||||
|
setting: float | None = None
|
||||||
|
status: float | None = None
|
||||||
|
velocity: float | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class RealtimeNodeBatchItem(BaseModel):
|
||||||
|
time: datetime
|
||||||
|
id: str
|
||||||
|
actual_demand: float | None = None
|
||||||
|
total_head: float | None = None
|
||||||
|
pressure: float | None = None
|
||||||
|
quality: float | None = None
|
||||||
|
|
||||||
|
|
||||||
@router.post("/timeseries/realtime/links/batches", status_code=201, summary="批量插入实时管道数据")
|
@router.post("/timeseries/realtime/links/batches", status_code=201, summary="批量插入实时管道数据")
|
||||||
async def insert_realtime_links(
|
async def insert_realtime_links(
|
||||||
data: List[dict] = Body(..., description="管道数据列表,每项包含管道ID、时间戳等信息"),
|
data: List[RealtimeLinkBatchItem] = Body(..., description="同一时间点的管道快照数据"),
|
||||||
conn: AsyncConnection = Depends(get_timescale_connection)
|
conn: AsyncConnection = Depends(get_timescale_connection)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -29,7 +52,9 @@ async def insert_realtime_links(
|
|||||||
Returns:
|
Returns:
|
||||||
插入成功的记录数
|
插入成功的记录数
|
||||||
"""
|
"""
|
||||||
await RealtimeRepository.insert_links_batch(conn, data)
|
await RealtimeRepository.insert_links_batch(
|
||||||
|
conn, [item.model_dump() for item in data]
|
||||||
|
)
|
||||||
return {"message": f"Inserted {len(data)} records"}
|
return {"message": f"Inserted {len(data)} records"}
|
||||||
|
|
||||||
|
|
||||||
@@ -119,7 +144,7 @@ async def update_realtime_link_field(
|
|||||||
|
|
||||||
@router.post("/timeseries/realtime/nodes/batches", status_code=201, summary="批量插入实时节点数据")
|
@router.post("/timeseries/realtime/nodes/batches", status_code=201, summary="批量插入实时节点数据")
|
||||||
async def insert_realtime_nodes(
|
async def insert_realtime_nodes(
|
||||||
data: List[dict] = Body(..., description="节点数据列表,每项包含节点ID、时间戳等信息"),
|
data: List[RealtimeNodeBatchItem] = Body(..., description="同一时间点的节点快照数据"),
|
||||||
conn: AsyncConnection = Depends(get_timescale_connection)
|
conn: AsyncConnection = Depends(get_timescale_connection)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -133,7 +158,9 @@ async def insert_realtime_nodes(
|
|||||||
Returns:
|
Returns:
|
||||||
插入成功的记录数
|
插入成功的记录数
|
||||||
"""
|
"""
|
||||||
await RealtimeRepository.insert_nodes_batch(conn, data)
|
await RealtimeRepository.insert_nodes_batch(
|
||||||
|
conn, [item.model_dump() for item in data]
|
||||||
|
)
|
||||||
return {"message": f"Inserted {len(data)} records"}
|
return {"message": f"Inserted {len(data)} records"}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,41 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
|
|||||||
from typing import List
|
from typing import List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from psycopg import AsyncConnection
|
from psycopg import AsyncConnection
|
||||||
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
|
from app.infra.db.postgresql.scada import ScadaInfoRepository
|
||||||
from app.infra.db.timescaledb.repositories.scada import ScadaRepository
|
from app.infra.db.timescaledb.repositories.scada import ScadaRepository
|
||||||
from .dependencies import get_timescale_connection
|
from .dependencies import get_postgres_connection, get_timescale_connection
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
SCADA_BATCH_MAX_ITEMS = 10_000
|
||||||
|
|
||||||
|
|
||||||
|
class ScadaReadingBatchItem(BaseModel):
|
||||||
|
time: datetime
|
||||||
|
device_id: str = Field(min_length=1)
|
||||||
|
monitored_value: float | None = None
|
||||||
|
cleaned_value: float | None = None
|
||||||
|
|
||||||
|
@field_validator("device_id")
|
||||||
|
@classmethod
|
||||||
|
def normalize_device_id(cls, value: str) -> str:
|
||||||
|
normalized = value.strip()
|
||||||
|
if not normalized:
|
||||||
|
raise ValueError("device_id must not be blank")
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
@router.post("/timeseries/scada-readings/batches", status_code=201, summary="批量插入SCADA监测数据")
|
@router.post("/timeseries/scada-readings/batches", status_code=201, summary="批量插入SCADA监测数据")
|
||||||
async def insert_scada_data(
|
async def insert_scada_data(
|
||||||
data: List[dict] = Body(..., description="SCADA设备监测数据列表"),
|
data: List[ScadaReadingBatchItem] = Body(
|
||||||
|
...,
|
||||||
|
min_length=1,
|
||||||
|
max_length=SCADA_BATCH_MAX_ITEMS,
|
||||||
|
description="SCADA设备监测数据列表",
|
||||||
|
),
|
||||||
conn: AsyncConnection = Depends(get_timescale_connection),
|
conn: AsyncConnection = Depends(get_timescale_connection),
|
||||||
|
postgres_conn: AsyncConnection = Depends(get_postgres_connection),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
批量插入SCADA监测数据
|
批量插入SCADA监测数据
|
||||||
@@ -25,7 +49,20 @@ async def insert_scada_data(
|
|||||||
Returns:
|
Returns:
|
||||||
插入成功的记录数
|
插入成功的记录数
|
||||||
"""
|
"""
|
||||||
await ScadaRepository.insert_scada_batch(conn, data)
|
rows = [item.model_dump() for item in data]
|
||||||
|
requested_ids = list(dict.fromkeys(item["device_id"] for item in rows))
|
||||||
|
existing_ids = await ScadaInfoRepository.get_existing_device_ids(
|
||||||
|
postgres_conn, requested_ids
|
||||||
|
)
|
||||||
|
missing_ids = [
|
||||||
|
device_id for device_id in requested_ids if device_id not in existing_ids
|
||||||
|
]
|
||||||
|
if missing_ids:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=422,
|
||||||
|
detail=f"SCADA devices do not exist in BizDB: {', '.join(missing_ids)}",
|
||||||
|
)
|
||||||
|
await ScadaRepository.insert_scada_batch(conn, rows)
|
||||||
return {"message": f"Inserted {len(data)} records"}
|
return {"message": f"Inserted {len(data)} records"}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
|
||||||
+57
-14
@@ -11,12 +11,23 @@ from fastapi import APIRouter, Depends, Query
|
|||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
from fastapi.routing import APIRoute
|
from fastapi.routing import APIRoute
|
||||||
from pydantic import BaseModel, JsonValue, create_model
|
from pydantic import BaseModel, JsonValue, create_model
|
||||||
|
from starlette.concurrency import run_in_threadpool
|
||||||
from starlette.responses import Response
|
from starlette.responses import Response
|
||||||
|
|
||||||
from app.api.problem_details import ProblemDetails
|
from app.api.problem_details import ProblemDetails
|
||||||
|
from app.api.pagination import PaginatedList
|
||||||
from app.api.v1.router import api_router as handler_api_router
|
from app.api.v1.router import api_router as handler_api_router
|
||||||
from app.auth.metadata_dependencies import get_current_metadata_user
|
from app.auth.metadata_dependencies import get_current_metadata_user
|
||||||
from app.auth.project_dependencies import ProjectContext, get_project_context
|
from app.auth.project_dependencies import (
|
||||||
|
ProjectContext,
|
||||||
|
get_project_business_routing,
|
||||||
|
get_project_context,
|
||||||
|
get_project_simulation_routing,
|
||||||
|
)
|
||||||
|
from app.infra.db.project_routing import (
|
||||||
|
ActiveProjectRouting,
|
||||||
|
activate_project_routing,
|
||||||
|
)
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
@@ -28,6 +39,14 @@ class Page(BaseModel, Generic[T]):
|
|||||||
offset: int
|
offset: int
|
||||||
|
|
||||||
|
|
||||||
|
async def _call_endpoint(endpoint, *args, **kwargs):
|
||||||
|
"""Call async handlers directly and offload synchronous handlers."""
|
||||||
|
if inspect.iscoroutinefunction(endpoint):
|
||||||
|
return await endpoint(*args, **kwargs)
|
||||||
|
result = await run_in_threadpool(endpoint, *args, **kwargs)
|
||||||
|
return await result if inspect.isawaitable(result) else result
|
||||||
|
|
||||||
|
|
||||||
_NAME_IS_NETWORK = {
|
_NAME_IS_NETWORK = {
|
||||||
"pressure_sensor_placement_sensitivity_endpoint",
|
"pressure_sensor_placement_sensitivity_endpoint",
|
||||||
"pressure_sensor_placement_kmeans_endpoint",
|
"pressure_sensor_placement_kmeans_endpoint",
|
||||||
@@ -41,8 +60,14 @@ _PUBLIC_PARAMETER_RENAMES = {
|
|||||||
"burst_ID": "burst_id",
|
"burst_ID": "burst_id",
|
||||||
"drainage_node_ID": "drainage_node_id",
|
"drainage_node_ID": "drainage_node_id",
|
||||||
}
|
}
|
||||||
_MODEL_NAME_IS_NETWORK = {"RunSimulationManuallyByDate"}
|
_MODEL_NAME_IS_NETWORK = {"RunSimulationManuallyByDate", "PressureSensorPlacement"}
|
||||||
_MODEL_USERNAME_FROM_AUTH: set[str] = set()
|
_MODEL_USERNAME_FROM_AUTH = {"PressureSensorPlacement"}
|
||||||
|
_TIMESCALE_ROUTED_ENDPOINT_MODULES = {
|
||||||
|
"app.api.v1.endpoints.burst_detection",
|
||||||
|
"app.api.v1.endpoints.burst_location",
|
||||||
|
"app.api.v1.endpoints.leakage",
|
||||||
|
"app.api.v1.endpoints.simulation",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _clean_name(name: str) -> str:
|
def _clean_name(name: str) -> str:
|
||||||
@@ -127,10 +152,14 @@ def _with_header_project_context(endpoint, route_name: str):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
injected_context_name = existing_context_parameter or "_rest_project_context"
|
injected_context_name = existing_context_parameter or "_rest_project_context"
|
||||||
|
injected_routing_name = "_rest_project_routing"
|
||||||
injected_user_name = "_rest_current_user"
|
injected_user_name = "_rest_current_user"
|
||||||
|
|
||||||
@wraps(endpoint)
|
@wraps(endpoint)
|
||||||
async def wrapper(*args, **kwargs):
|
async def wrapper(*args, **kwargs):
|
||||||
|
project_routing = kwargs.pop(injected_routing_name, None)
|
||||||
|
if not isinstance(project_routing, ActiveProjectRouting):
|
||||||
|
raise RuntimeError("REST project database routing was not resolved")
|
||||||
project_context = kwargs.get(injected_context_name)
|
project_context = kwargs.get(injected_context_name)
|
||||||
if not isinstance(project_context, ProjectContext):
|
if not isinstance(project_context, ProjectContext):
|
||||||
raise RuntimeError("REST project context was not resolved")
|
raise RuntimeError("REST project context was not resolved")
|
||||||
@@ -161,10 +190,8 @@ def _with_header_project_context(endpoint, route_name: str):
|
|||||||
kwargs[parameter_name] = original_model.model_validate(data)
|
kwargs[parameter_name] = original_model.model_validate(data)
|
||||||
if model_has_username:
|
if model_has_username:
|
||||||
kwargs.pop(injected_user_name, None)
|
kwargs.pop(injected_user_name, None)
|
||||||
result = endpoint(*args, **kwargs)
|
with activate_project_routing(project_routing):
|
||||||
if inspect.isawaitable(result):
|
return await _call_endpoint(endpoint, *args, **kwargs)
|
||||||
return await result
|
|
||||||
return result
|
|
||||||
|
|
||||||
parameters = []
|
parameters = []
|
||||||
for name, parameter in signature.parameters.items():
|
for name, parameter in signature.parameters.items():
|
||||||
@@ -180,6 +207,13 @@ def _with_header_project_context(endpoint, route_name: str):
|
|||||||
if name in body_models:
|
if name in body_models:
|
||||||
parameter = parameter.replace(annotation=body_models[name][1])
|
parameter = parameter.replace(annotation=body_models[name][1])
|
||||||
parameters.append(parameter)
|
parameters.append(parameter)
|
||||||
|
routing_dependency = (
|
||||||
|
get_project_simulation_routing
|
||||||
|
if (
|
||||||
|
endpoint.__module__ in _TIMESCALE_ROUTED_ENDPOINT_MODULES
|
||||||
|
)
|
||||||
|
else get_project_business_routing
|
||||||
|
)
|
||||||
if not existing_context_parameter:
|
if not existing_context_parameter:
|
||||||
parameters.append(
|
parameters.append(
|
||||||
inspect.Parameter(
|
inspect.Parameter(
|
||||||
@@ -189,6 +223,14 @@ def _with_header_project_context(endpoint, route_name: str):
|
|||||||
default=Depends(get_project_context),
|
default=Depends(get_project_context),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
parameters.append(
|
||||||
|
inspect.Parameter(
|
||||||
|
injected_routing_name,
|
||||||
|
kind=inspect.Parameter.KEYWORD_ONLY,
|
||||||
|
annotation=ActiveProjectRouting,
|
||||||
|
default=Depends(routing_dependency),
|
||||||
|
)
|
||||||
|
)
|
||||||
if username_parameter or model_has_username:
|
if username_parameter or model_has_username:
|
||||||
parameters.append(
|
parameters.append(
|
||||||
inspect.Parameter(
|
inspect.Parameter(
|
||||||
@@ -224,15 +266,18 @@ def _with_pagination(endpoint):
|
|||||||
else:
|
else:
|
||||||
limit = kwargs.pop("_rest_limit")
|
limit = kwargs.pop("_rest_limit")
|
||||||
offset = kwargs.pop("_rest_offset")
|
offset = kwargs.pop("_rest_offset")
|
||||||
result = endpoint(*args, **kwargs)
|
result = await _call_endpoint(endpoint, *args, **kwargs)
|
||||||
if inspect.isawaitable(result):
|
|
||||||
result = await result
|
|
||||||
if not isinstance(result, list):
|
if not isinstance(result, list):
|
||||||
return result
|
return result
|
||||||
if handler_handles_pagination:
|
if handler_handles_pagination:
|
||||||
|
if not isinstance(result, PaginatedList):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Paginated handler {endpoint.__name__!r} must return "
|
||||||
|
"PaginatedList with the real total"
|
||||||
|
)
|
||||||
return Page(
|
return Page(
|
||||||
items=result,
|
items=result,
|
||||||
total=offset + len(result),
|
total=result.total,
|
||||||
limit=limit or len(result),
|
limit=limit or len(result),
|
||||||
offset=offset,
|
offset=offset,
|
||||||
)
|
)
|
||||||
@@ -270,9 +315,7 @@ def _with_jsonable_response(endpoint):
|
|||||||
|
|
||||||
@wraps(endpoint)
|
@wraps(endpoint)
|
||||||
async def wrapper(*args, **kwargs):
|
async def wrapper(*args, **kwargs):
|
||||||
result = endpoint(*args, **kwargs)
|
result = await _call_endpoint(endpoint, *args, **kwargs)
|
||||||
if inspect.isawaitable(result):
|
|
||||||
result = await result
|
|
||||||
if isinstance(result, Response):
|
if isinstance(result, Response):
|
||||||
return result
|
return result
|
||||||
return jsonable_encoder(result)
|
return jsonable_encoder(result)
|
||||||
|
|||||||
+7
-41
@@ -7,21 +7,15 @@ from app.api.v1.endpoints import (
|
|||||||
audit,
|
audit,
|
||||||
burst_detection,
|
burst_detection,
|
||||||
burst_location,
|
burst_location,
|
||||||
cache,
|
|
||||||
extension,
|
|
||||||
geocoding,
|
geocoding,
|
||||||
leakage,
|
leakage,
|
||||||
meta,
|
meta,
|
||||||
misc,
|
|
||||||
model_import,
|
model_import,
|
||||||
project,
|
project,
|
||||||
project_data,
|
project_data,
|
||||||
risk,
|
|
||||||
scada,
|
scada,
|
||||||
schemes,
|
|
||||||
sensor_placement,
|
sensor_placement,
|
||||||
simulation,
|
simulation,
|
||||||
snapshots,
|
|
||||||
web_search,
|
web_search,
|
||||||
)
|
)
|
||||||
from app.api.v1.endpoints.components import (
|
from app.api.v1.endpoints.components import (
|
||||||
@@ -46,15 +40,14 @@ from app.api.v1.endpoints.network import (
|
|||||||
valves,
|
valves,
|
||||||
)
|
)
|
||||||
from app.api.v1.endpoints.timeseries import (
|
from app.api.v1.endpoints.timeseries import (
|
||||||
|
analysis as ts_analysis,
|
||||||
composite as ts_composite,
|
composite as ts_composite,
|
||||||
realtime as ts_realtime,
|
realtime as ts_realtime,
|
||||||
scada as ts_scada,
|
scada as ts_scada,
|
||||||
scheme as ts_scheme,
|
|
||||||
)
|
)
|
||||||
from app.auth.permissions import (
|
from app.auth.permissions import (
|
||||||
BURST_RUN,
|
BURST_RUN,
|
||||||
OPTIMIZATION_RUN,
|
OPTIMIZATION_RUN,
|
||||||
RISK_RUN,
|
|
||||||
SCADA_CLEAN,
|
SCADA_CLEAN,
|
||||||
SCADA_VIEW,
|
SCADA_VIEW,
|
||||||
SIMULATION_RUN,
|
SIMULATION_RUN,
|
||||||
@@ -89,7 +82,6 @@ simulation_access = Depends(
|
|||||||
webgis_view_access = Depends(require_permission(WEBGIS_VIEW))
|
webgis_view_access = Depends(require_permission(WEBGIS_VIEW))
|
||||||
simulation_run_access = Depends(require_permission(SIMULATION_RUN))
|
simulation_run_access = Depends(require_permission(SIMULATION_RUN))
|
||||||
burst_run_access = Depends(require_permission(BURST_RUN))
|
burst_run_access = Depends(require_permission(BURST_RUN))
|
||||||
risk_run_access = Depends(require_permission(RISK_RUN))
|
|
||||||
optimization_run_access = Depends(require_permission(OPTIMIZATION_RUN))
|
optimization_run_access = Depends(require_permission(OPTIMIZATION_RUN))
|
||||||
|
|
||||||
# Core services
|
# Core services
|
||||||
@@ -140,37 +132,16 @@ api_router.include_router(
|
|||||||
tags=["Simulation Control"],
|
tags=["Simulation Control"],
|
||||||
dependencies=[simulation_run_access],
|
dependencies=[simulation_run_access],
|
||||||
)
|
)
|
||||||
api_router.include_router(scada.router, dependencies=[scada_access])
|
api_router.include_router(
|
||||||
|
scada.router,
|
||||||
|
tags=["SCADA Metadata"],
|
||||||
|
dependencies=[scada_access],
|
||||||
|
)
|
||||||
api_router.include_router(
|
api_router.include_router(
|
||||||
sensor_placement.router,
|
sensor_placement.router,
|
||||||
tags=["Sensor Placement"],
|
tags=["Sensor Placement"],
|
||||||
dependencies=[optimization_run_access],
|
dependencies=[optimization_run_access],
|
||||||
)
|
)
|
||||||
api_router.include_router(
|
|
||||||
snapshots.router,
|
|
||||||
tags=["Snapshots"],
|
|
||||||
dependencies=[simulation_access],
|
|
||||||
)
|
|
||||||
api_router.include_router(
|
|
||||||
schemes.router,
|
|
||||||
tags=["Schemes"],
|
|
||||||
dependencies=[simulation_access],
|
|
||||||
)
|
|
||||||
api_router.include_router(
|
|
||||||
misc.router,
|
|
||||||
tags=["Misc"],
|
|
||||||
dependencies=[webgis_view_access],
|
|
||||||
)
|
|
||||||
api_router.include_router(
|
|
||||||
risk.router,
|
|
||||||
tags=["Risk"],
|
|
||||||
dependencies=[risk_run_access],
|
|
||||||
)
|
|
||||||
api_router.include_router(
|
|
||||||
cache.router,
|
|
||||||
tags=["Cache"],
|
|
||||||
dependencies=[simulation_run_access],
|
|
||||||
)
|
|
||||||
api_router.include_router(
|
api_router.include_router(
|
||||||
web_search.router,
|
web_search.router,
|
||||||
tags=["Web Search"],
|
tags=["Web Search"],
|
||||||
@@ -200,7 +171,7 @@ api_router.include_router(
|
|||||||
# TimescaleDB data
|
# TimescaleDB data
|
||||||
for endpoint_router, tag in (
|
for endpoint_router, tag in (
|
||||||
(ts_realtime.router, "TimescaleDB - Realtime"),
|
(ts_realtime.router, "TimescaleDB - Realtime"),
|
||||||
(ts_scheme.router, "TimescaleDB - Scheme"),
|
(ts_analysis.router, "TimescaleDB - Analysis"),
|
||||||
):
|
):
|
||||||
api_router.include_router(
|
api_router.include_router(
|
||||||
endpoint_router,
|
endpoint_router,
|
||||||
@@ -223,8 +194,3 @@ api_router.include_router(
|
|||||||
tags=["Project Data"],
|
tags=["Project Data"],
|
||||||
dependencies=[webgis_view_access],
|
dependencies=[webgis_view_access],
|
||||||
)
|
)
|
||||||
api_router.include_router(
|
|
||||||
extension.router,
|
|
||||||
tags=["Extension"],
|
|
||||||
dependencies=[webgis_access],
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from app.infra.db.metadb.repositories.metadata_repository import (
|
|||||||
MetadataRepository,
|
MetadataRepository,
|
||||||
ProjectDbRouting,
|
ProjectDbRouting,
|
||||||
)
|
)
|
||||||
|
from app.infra.db.project_routing import ActiveProjectRouting, activate_project_routing
|
||||||
|
|
||||||
DB_ROLE_BIZ_DATA = "biz_data"
|
DB_ROLE_BIZ_DATA = "biz_data"
|
||||||
DB_ROLE_IOT_DATA = "iot_data"
|
DB_ROLE_IOT_DATA = "iot_data"
|
||||||
@@ -99,6 +100,70 @@ async def get_project_context(
|
|||||||
return await resolve_project_context(x_project_id, current_user, metadata_repo)
|
return await resolve_project_context(x_project_id, current_user, metadata_repo)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_project_business_routing(
|
||||||
|
ctx: ProjectContext = Depends(get_project_context),
|
||||||
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||||
|
) -> ActiveProjectRouting:
|
||||||
|
return await resolve_project_business_routing(ctx, metadata_repo)
|
||||||
|
|
||||||
|
|
||||||
|
async def use_project_business_routing(
|
||||||
|
routing: ActiveProjectRouting = Depends(get_project_business_routing),
|
||||||
|
) -> AsyncGenerator[ActiveProjectRouting, None]:
|
||||||
|
"""Keep the routed BizDB active for an entire endpoint invocation."""
|
||||||
|
with activate_project_routing(routing):
|
||||||
|
yield routing
|
||||||
|
|
||||||
|
|
||||||
|
async def resolve_project_business_routing(
|
||||||
|
ctx: ProjectContext,
|
||||||
|
metadata_repo: MetadataRepository,
|
||||||
|
) -> ActiveProjectRouting:
|
||||||
|
business = await _get_project_routing(
|
||||||
|
metadata_repo,
|
||||||
|
ctx.project_id,
|
||||||
|
DB_ROLE_BIZ_DATA,
|
||||||
|
DB_TYPE_POSTGRES,
|
||||||
|
"PostgreSQL",
|
||||||
|
)
|
||||||
|
return ActiveProjectRouting(
|
||||||
|
project_code=ctx.project_code,
|
||||||
|
business_dsn=business.dsn,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_project_simulation_routing(
|
||||||
|
ctx: ProjectContext = Depends(get_project_context),
|
||||||
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||||
|
) -> ActiveProjectRouting:
|
||||||
|
return await resolve_project_simulation_routing(ctx, metadata_repo)
|
||||||
|
|
||||||
|
|
||||||
|
async def resolve_project_simulation_routing(
|
||||||
|
ctx: ProjectContext,
|
||||||
|
metadata_repo: MetadataRepository,
|
||||||
|
) -> ActiveProjectRouting:
|
||||||
|
business = await _get_project_routing(
|
||||||
|
metadata_repo,
|
||||||
|
ctx.project_id,
|
||||||
|
DB_ROLE_BIZ_DATA,
|
||||||
|
DB_TYPE_POSTGRES,
|
||||||
|
"PostgreSQL",
|
||||||
|
)
|
||||||
|
timescale = await _get_project_routing(
|
||||||
|
metadata_repo,
|
||||||
|
ctx.project_id,
|
||||||
|
DB_ROLE_IOT_DATA,
|
||||||
|
DB_TYPE_TIMESCALE,
|
||||||
|
"TimescaleDB",
|
||||||
|
)
|
||||||
|
return ActiveProjectRouting(
|
||||||
|
project_code=ctx.project_code,
|
||||||
|
business_dsn=business.dsn,
|
||||||
|
timescale_dsn=timescale.dsn,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _get_project_routing(
|
async def _get_project_routing(
|
||||||
metadata_repo: MetadataRepository,
|
metadata_repo: MetadataRepository,
|
||||||
project_id: UUID,
|
project_id: UUID,
|
||||||
@@ -132,31 +197,6 @@ async def _get_project_routing(
|
|||||||
return routing
|
return routing
|
||||||
|
|
||||||
|
|
||||||
async def get_project_pg_session(
|
|
||||||
ctx: ProjectContext = Depends(get_project_context),
|
|
||||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
|
||||||
) -> AsyncGenerator[AsyncSession, None]:
|
|
||||||
routing = await _get_project_routing(
|
|
||||||
metadata_repo,
|
|
||||||
ctx.project_id,
|
|
||||||
DB_ROLE_BIZ_DATA,
|
|
||||||
DB_TYPE_POSTGRES,
|
|
||||||
"PostgreSQL",
|
|
||||||
)
|
|
||||||
|
|
||||||
pool_min_size = routing.pool_min_size or settings.PROJECT_PG_POOL_SIZE
|
|
||||||
pool_max_size = routing.pool_max_size or settings.PROJECT_PG_POOL_SIZE
|
|
||||||
sessionmaker = await project_connection_manager.get_pg_sessionmaker(
|
|
||||||
ctx.project_id,
|
|
||||||
DB_ROLE_BIZ_DATA,
|
|
||||||
routing.dsn,
|
|
||||||
pool_min_size,
|
|
||||||
pool_max_size,
|
|
||||||
)
|
|
||||||
async with sessionmaker() as session:
|
|
||||||
yield session
|
|
||||||
|
|
||||||
|
|
||||||
async def get_project_pg_connection(
|
async def get_project_pg_connection(
|
||||||
ctx: ProjectContext = Depends(get_project_context),
|
ctx: ProjectContext = Depends(get_project_context),
|
||||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||||
@@ -169,16 +209,23 @@ async def get_project_pg_connection(
|
|||||||
"PostgreSQL",
|
"PostgreSQL",
|
||||||
)
|
)
|
||||||
|
|
||||||
pool_min_size = routing.pool_min_size or settings.PROJECT_PG_POOL_SIZE
|
pool_min_size = (
|
||||||
pool_max_size = routing.pool_max_size or settings.PROJECT_PG_POOL_SIZE
|
routing.pool_min_size
|
||||||
pool = await project_connection_manager.get_pg_pool(
|
if routing.pool_min_size is not None
|
||||||
|
else settings.PROJECT_PG_POOL_MIN_SIZE
|
||||||
|
)
|
||||||
|
pool_max_size = (
|
||||||
|
routing.pool_max_size
|
||||||
|
if routing.pool_max_size is not None
|
||||||
|
else settings.PROJECT_PG_POOL_SIZE
|
||||||
|
)
|
||||||
|
async with project_connection_manager.pg_connection(
|
||||||
ctx.project_id,
|
ctx.project_id,
|
||||||
DB_ROLE_BIZ_DATA,
|
DB_ROLE_BIZ_DATA,
|
||||||
routing.dsn,
|
routing.dsn,
|
||||||
pool_min_size,
|
pool_min_size,
|
||||||
pool_max_size,
|
pool_max_size,
|
||||||
)
|
) as conn:
|
||||||
async with pool.connection() as conn:
|
|
||||||
yield conn
|
yield conn
|
||||||
|
|
||||||
|
|
||||||
@@ -194,14 +241,21 @@ async def get_project_timescale_connection(
|
|||||||
"TimescaleDB",
|
"TimescaleDB",
|
||||||
)
|
)
|
||||||
|
|
||||||
pool_min_size = routing.pool_min_size or settings.PROJECT_TS_POOL_MIN_SIZE
|
pool_min_size = (
|
||||||
pool_max_size = routing.pool_max_size or settings.PROJECT_TS_POOL_MAX_SIZE
|
routing.pool_min_size
|
||||||
pool = await project_connection_manager.get_timescale_pool(
|
if routing.pool_min_size is not None
|
||||||
|
else settings.PROJECT_TS_POOL_MIN_SIZE
|
||||||
|
)
|
||||||
|
pool_max_size = (
|
||||||
|
routing.pool_max_size
|
||||||
|
if routing.pool_max_size is not None
|
||||||
|
else settings.PROJECT_TS_POOL_MAX_SIZE
|
||||||
|
)
|
||||||
|
async with project_connection_manager.timescale_connection(
|
||||||
ctx.project_id,
|
ctx.project_id,
|
||||||
DB_ROLE_IOT_DATA,
|
DB_ROLE_IOT_DATA,
|
||||||
routing.dsn,
|
routing.dsn,
|
||||||
pool_min_size,
|
pool_min_size,
|
||||||
pool_max_size,
|
pool_max_size,
|
||||||
)
|
) as conn:
|
||||||
async with pool.connection() as conn:
|
|
||||||
yield conn
|
yield conn
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ def sanitize_sensitive_data(data: dict) -> dict:
|
|||||||
"token",
|
"token",
|
||||||
"api_key",
|
"api_key",
|
||||||
"apikey",
|
"apikey",
|
||||||
|
"dsn",
|
||||||
"credit_card",
|
"credit_card",
|
||||||
"ssn",
|
"ssn",
|
||||||
"social_security",
|
"social_security",
|
||||||
|
|||||||
+20
-12
@@ -26,12 +26,6 @@ class Settings(BaseSettings):
|
|||||||
TIMESCALEDB_DB_PORT: str = "5433"
|
TIMESCALEDB_DB_PORT: str = "5433"
|
||||||
TIMESCALEDB_DB_USER: str = "postgres"
|
TIMESCALEDB_DB_USER: str = "postgres"
|
||||||
TIMESCALEDB_DB_PASSWORD: str = "password"
|
TIMESCALEDB_DB_PASSWORD: str = "password"
|
||||||
# InfluxDB
|
|
||||||
INFLUXDB_URL: str = "http://localhost:8086"
|
|
||||||
INFLUXDB_TOKEN: str = "token"
|
|
||||||
INFLUXDB_ORG: str = "org"
|
|
||||||
INFLUXDB_BUCKET: str = "bucket"
|
|
||||||
|
|
||||||
# Metadata Database Config (PostgreSQL)
|
# Metadata Database Config (PostgreSQL)
|
||||||
METADATA_DB_NAME: str = "system_hub"
|
METADATA_DB_NAME: str = "system_hub"
|
||||||
METADATA_DB_HOST: str = "localhost"
|
METADATA_DB_HOST: str = "localhost"
|
||||||
@@ -42,12 +36,26 @@ class Settings(BaseSettings):
|
|||||||
METADATA_DB_POOL_SIZE: int = 5
|
METADATA_DB_POOL_SIZE: int = 5
|
||||||
METADATA_DB_MAX_OVERFLOW: int = 10
|
METADATA_DB_MAX_OVERFLOW: int = 10
|
||||||
|
|
||||||
PROJECT_PG_CACHE_SIZE: int = 50
|
PROJECT_PG_CACHE_SIZE: int = 16
|
||||||
PROJECT_TS_CACHE_SIZE: int = 50
|
PROJECT_TS_CACHE_SIZE: int = 16
|
||||||
PROJECT_PG_POOL_SIZE: int = 5
|
PROJECT_PG_POOL_MIN_SIZE: int = 0
|
||||||
PROJECT_PG_MAX_OVERFLOW: int = 10
|
PROJECT_PG_POOL_SIZE: int = 4
|
||||||
PROJECT_TS_POOL_MIN_SIZE: int = 1
|
PROJECT_PG_MAX_OVERFLOW: int = 2
|
||||||
PROJECT_TS_POOL_MAX_SIZE: int = 10
|
PROJECT_TS_POOL_MIN_SIZE: int = 0
|
||||||
|
PROJECT_TS_POOL_MAX_SIZE: int = 4
|
||||||
|
WNDB_SCHEMA_TEMPLATE_DB_NAME: str = "tjwater_v2_schema_template"
|
||||||
|
TIMESCALEDB_SCHEMA_TEMPLATE_DB_NAME: str = "tjwater_v2_timescale_template"
|
||||||
|
WNDB_TEMP_DB_MAX_COUNT: int = 8
|
||||||
|
|
||||||
|
# GeoServer project provisioning
|
||||||
|
GEOSERVER_URL: str = "http://localhost:8080/geoserver"
|
||||||
|
GEOSERVER_USERNAME: str = ""
|
||||||
|
GEOSERVER_PASSWORD: str = ""
|
||||||
|
GEOSERVER_DB_HOST: str = ""
|
||||||
|
GEOSERVER_DB_PORT: str = ""
|
||||||
|
GEOSERVER_DB_USER: str = ""
|
||||||
|
GEOSERVER_DB_PASSWORD: str = ""
|
||||||
|
GEOSERVER_CLIENT_CACHE_SECONDS: int = 30 * 24 * 60 * 60
|
||||||
|
|
||||||
# Keycloak access token verification
|
# Keycloak access token verification
|
||||||
KEYCLOAK_PUBLIC_KEY: str = ""
|
KEYCLOAK_PUBLIC_KEY: str = ""
|
||||||
|
|||||||
@@ -99,6 +99,15 @@ class AdminProjectResponse(BaseModel):
|
|||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectProvisionResponse(BaseModel):
|
||||||
|
project: AdminProjectResponse
|
||||||
|
business_database: str
|
||||||
|
model_template_database: str
|
||||||
|
timescale_database: str
|
||||||
|
geoserver_workspace: str
|
||||||
|
geoserver_layers: list[str]
|
||||||
|
|
||||||
|
|
||||||
class ProjectDatabaseUpsertRequest(BaseModel):
|
class ProjectDatabaseUpsertRequest(BaseModel):
|
||||||
db_role: ProjectDbRole
|
db_role: ProjectDbRole
|
||||||
dsn: str | None = Field(default=None, min_length=1)
|
dsn: str | None = Field(default=None, min_length=1)
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class ScadaDeviceResponse(BaseModel):
|
||||||
|
"""Project SCADA metadata keyed by the canonical device identifier."""
|
||||||
|
|
||||||
|
device_id: str
|
||||||
|
device_type: str
|
||||||
|
node_id: str | None = None
|
||||||
|
link_id: str | None = None
|
||||||
|
api_query_id: str | None = None
|
||||||
|
transmission_mode: str
|
||||||
|
transmission_frequency: str
|
||||||
|
reliability: int | None = None
|
||||||
|
x: float | None = None
|
||||||
|
y: float | None = None
|
||||||
|
longitude: float | None = None
|
||||||
|
latitude: float | None = None
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, field_validator
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ class SensorPlacementOptimizeRequest(BaseModel):
|
|||||||
max_length=63,
|
max_length=63,
|
||||||
pattern=r"^[^/\\\x00]+$",
|
pattern=r"^[^/\\\x00]+$",
|
||||||
)
|
)
|
||||||
scheme_name: str = Field(..., min_length=1, max_length=32)
|
run_name: str = Field(..., min_length=1, max_length=64)
|
||||||
sensor_type: Literal["pressure"]
|
sensor_type: Literal["pressure"]
|
||||||
method: Literal["sensitivity", "kmeans"]
|
method: Literal["sensitivity", "kmeans"]
|
||||||
sensor_count: int = Field(..., gt=0, le=200)
|
sensor_count: int = Field(..., gt=0, le=200)
|
||||||
@@ -39,27 +40,27 @@ class SensorPlacementOptimizeRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class SensorPlacementUpdateRequest(BaseModel):
|
class SensorPlacementUpdateRequest(BaseModel):
|
||||||
expected_sensor_location: list[str] = Field(
|
expected_sensor_locations: list[str] = Field(
|
||||||
...,
|
...,
|
||||||
min_length=1,
|
min_length=1,
|
||||||
max_length=200,
|
max_length=200,
|
||||||
)
|
)
|
||||||
sensor_location: list[str] = Field(..., min_length=1, max_length=200)
|
sensor_locations: list[str] = Field(..., min_length=1, max_length=200)
|
||||||
|
|
||||||
@field_validator("expected_sensor_location", "sensor_location")
|
@field_validator("expected_sensor_locations", "sensor_locations")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_locations(cls, value: list[str]) -> list[str]:
|
def validate_locations(cls, value: list[str]) -> list[str]:
|
||||||
return _normalize_location_ids(value)
|
return _normalize_location_ids(value)
|
||||||
|
|
||||||
|
|
||||||
class SensorPlacementExportRequest(BaseModel):
|
class SensorPlacementExportRequest(BaseModel):
|
||||||
sensor_location: list[str] = Field(..., min_length=1, max_length=200)
|
sensor_locations: list[str] = Field(..., min_length=1, max_length=200)
|
||||||
adjustment_status: dict[str, AdjustmentStatus] = Field(
|
adjustment_status: dict[str, AdjustmentStatus] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
max_length=200,
|
max_length=200,
|
||||||
)
|
)
|
||||||
|
|
||||||
@field_validator("sensor_location")
|
@field_validator("sensor_locations")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_locations(cls, value: list[str]) -> list[str]:
|
def validate_locations(cls, value: list[str]) -> list[str]:
|
||||||
return _normalize_location_ids(value)
|
return _normalize_location_ids(value)
|
||||||
@@ -81,12 +82,13 @@ class SensorPointResponse(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class SensorPlacementSchemeResponse(BaseModel):
|
class SensorPlacementSchemeResponse(BaseModel):
|
||||||
id: int
|
run_id: UUID
|
||||||
scheme_name: str
|
name: str
|
||||||
sensor_number: int
|
sensor_count: int
|
||||||
min_diameter: int
|
min_diameter: int
|
||||||
username: str
|
created_by: str
|
||||||
create_time: datetime
|
created_at: datetime
|
||||||
sensor_location: list[str]
|
status: str
|
||||||
|
sensor_locations: list[str]
|
||||||
sensor_points: list[SensorPointResponse]
|
sensor_points: list[SensorPointResponse]
|
||||||
can_edit: bool = False
|
can_edit: bool = False
|
||||||
|
|||||||
Vendored
Vendored
-19
@@ -1,19 +0,0 @@
|
|||||||
import redis
|
|
||||||
import msgpack
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
# Initialize Redis connection
|
|
||||||
redis_client = redis.Redis(host="127.0.0.1", port=6379, db=0)
|
|
||||||
|
|
||||||
def encode_datetime(obj: Any) -> Any:
|
|
||||||
"""Serialize datetime objects to dictionary format."""
|
|
||||||
if isinstance(obj, datetime):
|
|
||||||
return {"__datetime__": True, "as_str": obj.strftime("%Y%m%dT%H:%M:%S.%f")}
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def decode_datetime(obj: Any) -> Any:
|
|
||||||
"""Deserialize dictionary format to datetime objects."""
|
|
||||||
if "__datetime__" in obj:
|
|
||||||
return datetime.strptime(obj["as_str"], "%Y%m%dT%H:%M:%S.%f")
|
|
||||||
return obj
|
|
||||||
+194
-125
@@ -1,40 +1,29 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from psycopg import AsyncConnection
|
||||||
from psycopg_pool import AsyncConnectionPool
|
from psycopg_pool import AsyncConnectionPool
|
||||||
from psycopg.rows import dict_row
|
from psycopg.rows import dict_row
|
||||||
from sqlalchemy.engine.url import make_url
|
|
||||||
from sqlalchemy.ext.asyncio import (
|
|
||||||
AsyncEngine,
|
|
||||||
AsyncSession,
|
|
||||||
async_sessionmaker,
|
|
||||||
create_async_engine,
|
|
||||||
)
|
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
_check_async_connection = AsyncConnectionPool.check_connection
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass
|
||||||
class PgEngineEntry:
|
|
||||||
engine: AsyncEngine
|
|
||||||
sessionmaker: async_sessionmaker[AsyncSession]
|
|
||||||
connection_url: str
|
|
||||||
pool_min_size: int
|
|
||||||
pool_max_size: int
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class PoolEntry:
|
class PoolEntry:
|
||||||
pool: AsyncConnectionPool
|
pool: AsyncConnectionPool
|
||||||
connection_url: str
|
connection_url: str
|
||||||
pool_min_size: int
|
pool_min_size: int
|
||||||
pool_max_size: int
|
pool_max_size: int
|
||||||
|
borrow_count: int = 0
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -45,83 +34,23 @@ class CacheKey:
|
|||||||
|
|
||||||
class ProjectConnectionManager:
|
class ProjectConnectionManager:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._pg_cache: Dict[CacheKey, PgEngineEntry] = OrderedDict()
|
|
||||||
self._ts_cache: Dict[CacheKey, PoolEntry] = OrderedDict()
|
self._ts_cache: Dict[CacheKey, PoolEntry] = OrderedDict()
|
||||||
self._pg_raw_cache: Dict[CacheKey, PoolEntry] = OrderedDict()
|
self._pg_raw_cache: Dict[CacheKey, PoolEntry] = OrderedDict()
|
||||||
self._pg_lock = asyncio.Lock()
|
self._retired_ts: list[tuple[CacheKey, PoolEntry]] = []
|
||||||
|
self._retired_pg: list[tuple[CacheKey, PoolEntry]] = []
|
||||||
self._ts_lock = asyncio.Lock()
|
self._ts_lock = asyncio.Lock()
|
||||||
self._pg_raw_lock = asyncio.Lock()
|
self._pg_raw_lock = asyncio.Lock()
|
||||||
|
|
||||||
def _normalize_pg_url(self, url: str) -> str:
|
async def _get_timescale_pool_locked(
|
||||||
parsed = make_url(url)
|
|
||||||
if parsed.drivername in {"postgresql", "postgres"}:
|
|
||||||
parsed = parsed.set(drivername="postgresql+psycopg")
|
|
||||||
return parsed.render_as_string(hide_password=False)
|
|
||||||
|
|
||||||
async def get_pg_sessionmaker(
|
|
||||||
self,
|
self,
|
||||||
project_id: UUID,
|
project_id: UUID,
|
||||||
db_role: str,
|
db_role: str,
|
||||||
connection_url: str,
|
connection_url: str,
|
||||||
pool_min_size: int,
|
pool_min_size: int,
|
||||||
pool_max_size: int,
|
pool_max_size: int,
|
||||||
) -> async_sessionmaker[AsyncSession]:
|
) -> tuple[CacheKey, AsyncConnectionPool]:
|
||||||
async with self._pg_lock:
|
pool_min_size = max(0, pool_min_size)
|
||||||
normalized_url = self._normalize_pg_url(connection_url)
|
pool_max_size = max(1, pool_min_size, pool_max_size)
|
||||||
pool_min_size = max(1, pool_min_size)
|
|
||||||
pool_max_size = max(pool_min_size, pool_max_size)
|
|
||||||
|
|
||||||
key = CacheKey(project_id=project_id, db_role=db_role)
|
|
||||||
entry = self._pg_cache.get(key)
|
|
||||||
if entry:
|
|
||||||
if (
|
|
||||||
entry.connection_url == normalized_url
|
|
||||||
and entry.pool_min_size == pool_min_size
|
|
||||||
and entry.pool_max_size == pool_max_size
|
|
||||||
):
|
|
||||||
self._pg_cache.move_to_end(key)
|
|
||||||
return entry.sessionmaker
|
|
||||||
|
|
||||||
await entry.engine.dispose()
|
|
||||||
logger.info(
|
|
||||||
"Rebuilding PostgreSQL engine for project %s (%s) due to config change",
|
|
||||||
project_id,
|
|
||||||
db_role,
|
|
||||||
)
|
|
||||||
self._pg_cache.pop(key, None)
|
|
||||||
|
|
||||||
engine = create_async_engine(
|
|
||||||
normalized_url,
|
|
||||||
pool_size=pool_min_size,
|
|
||||||
max_overflow=max(0, pool_max_size - pool_min_size),
|
|
||||||
pool_pre_ping=True,
|
|
||||||
)
|
|
||||||
sessionmaker = async_sessionmaker(engine, expire_on_commit=False)
|
|
||||||
self._pg_cache[key] = PgEngineEntry(
|
|
||||||
engine=engine,
|
|
||||||
sessionmaker=sessionmaker,
|
|
||||||
connection_url=normalized_url,
|
|
||||||
pool_min_size=pool_min_size,
|
|
||||||
pool_max_size=pool_max_size,
|
|
||||||
)
|
|
||||||
await self._evict_pg_if_needed()
|
|
||||||
logger.info(
|
|
||||||
"Created PostgreSQL engine for project %s (%s)", project_id, db_role
|
|
||||||
)
|
|
||||||
return sessionmaker
|
|
||||||
|
|
||||||
async def get_timescale_pool(
|
|
||||||
self,
|
|
||||||
project_id: UUID,
|
|
||||||
db_role: str,
|
|
||||||
connection_url: str,
|
|
||||||
pool_min_size: int,
|
|
||||||
pool_max_size: int,
|
|
||||||
) -> AsyncConnectionPool:
|
|
||||||
async with self._ts_lock:
|
|
||||||
pool_min_size = max(1, pool_min_size)
|
|
||||||
pool_max_size = max(pool_min_size, pool_max_size)
|
|
||||||
|
|
||||||
key = CacheKey(project_id=project_id, db_role=db_role)
|
key = CacheKey(project_id=project_id, db_role=db_role)
|
||||||
entry = self._ts_cache.get(key)
|
entry = self._ts_cache.get(key)
|
||||||
if entry:
|
if entry:
|
||||||
@@ -131,15 +60,12 @@ class ProjectConnectionManager:
|
|||||||
and entry.pool_max_size == pool_max_size
|
and entry.pool_max_size == pool_max_size
|
||||||
):
|
):
|
||||||
self._ts_cache.move_to_end(key)
|
self._ts_cache.move_to_end(key)
|
||||||
return entry.pool
|
return key, entry.pool
|
||||||
|
|
||||||
await entry.pool.close()
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Rebuilding TimescaleDB pool for project %s (%s) due to config change",
|
"Rebuilding TimescaleDB pool for project %s (%s) due to config change",
|
||||||
project_id,
|
project_id,
|
||||||
db_role,
|
db_role,
|
||||||
)
|
)
|
||||||
self._ts_cache.pop(key, None)
|
|
||||||
|
|
||||||
pool = AsyncConnectionPool(
|
pool = AsyncConnectionPool(
|
||||||
conninfo=connection_url,
|
conninfo=connection_url,
|
||||||
@@ -147,32 +73,33 @@ class ProjectConnectionManager:
|
|||||||
max_size=pool_max_size,
|
max_size=pool_max_size,
|
||||||
open=False,
|
open=False,
|
||||||
kwargs={"row_factory": dict_row},
|
kwargs={"row_factory": dict_row},
|
||||||
|
check=_check_async_connection,
|
||||||
)
|
)
|
||||||
await pool.open()
|
await pool.open()
|
||||||
|
if entry is not None:
|
||||||
|
if entry.borrow_count:
|
||||||
|
self._retired_ts.append((key, entry))
|
||||||
|
else:
|
||||||
|
await entry.pool.close()
|
||||||
self._ts_cache[key] = PoolEntry(
|
self._ts_cache[key] = PoolEntry(
|
||||||
pool=pool,
|
pool=pool,
|
||||||
connection_url=connection_url,
|
connection_url=connection_url,
|
||||||
pool_min_size=pool_min_size,
|
pool_min_size=pool_min_size,
|
||||||
pool_max_size=pool_max_size,
|
pool_max_size=pool_max_size,
|
||||||
)
|
)
|
||||||
await self._evict_ts_if_needed()
|
logger.info("Created TimescaleDB pool for project %s (%s)", project_id, db_role)
|
||||||
logger.info(
|
return key, pool
|
||||||
"Created TimescaleDB pool for project %s (%s)", project_id, db_role
|
|
||||||
)
|
|
||||||
return pool
|
|
||||||
|
|
||||||
async def get_pg_pool(
|
async def _get_pg_pool_locked(
|
||||||
self,
|
self,
|
||||||
project_id: UUID,
|
project_id: UUID,
|
||||||
db_role: str,
|
db_role: str,
|
||||||
connection_url: str,
|
connection_url: str,
|
||||||
pool_min_size: int,
|
pool_min_size: int,
|
||||||
pool_max_size: int,
|
pool_max_size: int,
|
||||||
) -> AsyncConnectionPool:
|
) -> tuple[CacheKey, AsyncConnectionPool]:
|
||||||
async with self._pg_raw_lock:
|
pool_min_size = max(0, pool_min_size)
|
||||||
pool_min_size = max(1, pool_min_size)
|
pool_max_size = max(1, pool_min_size, pool_max_size)
|
||||||
pool_max_size = max(pool_min_size, pool_max_size)
|
|
||||||
|
|
||||||
key = CacheKey(project_id=project_id, db_role=db_role)
|
key = CacheKey(project_id=project_id, db_role=db_role)
|
||||||
entry = self._pg_raw_cache.get(key)
|
entry = self._pg_raw_cache.get(key)
|
||||||
if entry:
|
if entry:
|
||||||
@@ -182,15 +109,12 @@ class ProjectConnectionManager:
|
|||||||
and entry.pool_max_size == pool_max_size
|
and entry.pool_max_size == pool_max_size
|
||||||
):
|
):
|
||||||
self._pg_raw_cache.move_to_end(key)
|
self._pg_raw_cache.move_to_end(key)
|
||||||
return entry.pool
|
return key, entry.pool
|
||||||
|
|
||||||
await entry.pool.close()
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Rebuilding PostgreSQL pool for project %s (%s) due to config change",
|
"Rebuilding PostgreSQL pool for project %s (%s) due to config change",
|
||||||
project_id,
|
project_id,
|
||||||
db_role,
|
db_role,
|
||||||
)
|
)
|
||||||
self._pg_raw_cache.pop(key, None)
|
|
||||||
|
|
||||||
pool = AsyncConnectionPool(
|
pool = AsyncConnectionPool(
|
||||||
conninfo=connection_url,
|
conninfo=connection_url,
|
||||||
@@ -198,33 +122,115 @@ class ProjectConnectionManager:
|
|||||||
max_size=pool_max_size,
|
max_size=pool_max_size,
|
||||||
open=False,
|
open=False,
|
||||||
kwargs={"row_factory": dict_row},
|
kwargs={"row_factory": dict_row},
|
||||||
|
check=_check_async_connection,
|
||||||
)
|
)
|
||||||
await pool.open()
|
await pool.open()
|
||||||
|
if entry is not None:
|
||||||
|
if entry.borrow_count:
|
||||||
|
self._retired_pg.append((key, entry))
|
||||||
|
else:
|
||||||
|
await entry.pool.close()
|
||||||
self._pg_raw_cache[key] = PoolEntry(
|
self._pg_raw_cache[key] = PoolEntry(
|
||||||
pool=pool,
|
pool=pool,
|
||||||
connection_url=connection_url,
|
connection_url=connection_url,
|
||||||
pool_min_size=pool_min_size,
|
pool_min_size=pool_min_size,
|
||||||
pool_max_size=pool_max_size,
|
pool_max_size=pool_max_size,
|
||||||
)
|
)
|
||||||
|
logger.info("Created PostgreSQL pool for project %s (%s)", project_id, db_role)
|
||||||
|
return key, pool
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def pg_connection(
|
||||||
|
self,
|
||||||
|
project_id: UUID,
|
||||||
|
db_role: str,
|
||||||
|
connection_url: str,
|
||||||
|
pool_min_size: int,
|
||||||
|
pool_max_size: int,
|
||||||
|
) -> AsyncIterator[AsyncConnection]:
|
||||||
|
async with self._pg_raw_lock:
|
||||||
|
key, pool = await self._get_pg_pool_locked(
|
||||||
|
project_id,
|
||||||
|
db_role,
|
||||||
|
connection_url,
|
||||||
|
pool_min_size,
|
||||||
|
pool_max_size,
|
||||||
|
)
|
||||||
|
borrowed_entry = self._pg_raw_cache[key]
|
||||||
|
borrowed_entry.borrow_count += 1
|
||||||
await self._evict_pg_raw_if_needed()
|
await self._evict_pg_raw_if_needed()
|
||||||
logger.info(
|
|
||||||
"Created PostgreSQL pool for project %s (%s)", project_id, db_role
|
|
||||||
)
|
|
||||||
return pool
|
|
||||||
|
|
||||||
async def _evict_pg_if_needed(self) -> None:
|
try:
|
||||||
while len(self._pg_cache) > settings.PROJECT_PG_CACHE_SIZE:
|
async with pool.connection() as conn:
|
||||||
key, entry = self._pg_cache.popitem(last=False)
|
yield conn
|
||||||
await entry.engine.dispose()
|
finally:
|
||||||
logger.info(
|
async with self._pg_raw_lock:
|
||||||
"Evicted PostgreSQL engine for project %s (%s)",
|
borrowed_entry.borrow_count -= 1
|
||||||
key.project_id,
|
if borrowed_entry.borrow_count == 0 and borrowed_entry.pool is not (
|
||||||
key.db_role,
|
self._pg_raw_cache.get(key).pool
|
||||||
)
|
if key in self._pg_raw_cache
|
||||||
|
else None
|
||||||
|
):
|
||||||
|
self._retired_pg = [
|
||||||
|
item for item in self._retired_pg if item[1] is not borrowed_entry
|
||||||
|
]
|
||||||
|
await borrowed_entry.pool.close()
|
||||||
|
await self._evict_pg_raw_if_needed()
|
||||||
|
|
||||||
async def _evict_ts_if_needed(self) -> None:
|
@asynccontextmanager
|
||||||
|
async def timescale_connection(
|
||||||
|
self,
|
||||||
|
project_id: UUID,
|
||||||
|
db_role: str,
|
||||||
|
connection_url: str,
|
||||||
|
pool_min_size: int,
|
||||||
|
pool_max_size: int,
|
||||||
|
) -> AsyncIterator[AsyncConnection]:
|
||||||
|
async with self._ts_lock:
|
||||||
|
key, pool = await self._get_timescale_pool_locked(
|
||||||
|
project_id,
|
||||||
|
db_role,
|
||||||
|
connection_url,
|
||||||
|
pool_min_size,
|
||||||
|
pool_max_size,
|
||||||
|
)
|
||||||
|
borrowed_entry = self._ts_cache[key]
|
||||||
|
borrowed_entry.borrow_count += 1
|
||||||
|
await self._evict_ts_if_needed()
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with pool.connection() as conn:
|
||||||
|
yield conn
|
||||||
|
finally:
|
||||||
|
async with self._ts_lock:
|
||||||
|
borrowed_entry.borrow_count -= 1
|
||||||
|
if borrowed_entry.borrow_count == 0 and borrowed_entry.pool is not (
|
||||||
|
self._ts_cache.get(key).pool
|
||||||
|
if key in self._ts_cache
|
||||||
|
else None
|
||||||
|
):
|
||||||
|
self._retired_ts = [
|
||||||
|
item for item in self._retired_ts if item[1] is not borrowed_entry
|
||||||
|
]
|
||||||
|
await borrowed_entry.pool.close()
|
||||||
|
await self._evict_ts_if_needed()
|
||||||
|
|
||||||
|
async def _evict_ts_if_needed(
|
||||||
|
self, protected_key: CacheKey | None = None
|
||||||
|
) -> None:
|
||||||
while len(self._ts_cache) > settings.PROJECT_TS_CACHE_SIZE:
|
while len(self._ts_cache) > settings.PROJECT_TS_CACHE_SIZE:
|
||||||
key, entry = self._ts_cache.popitem(last=False)
|
idle = next(
|
||||||
|
(
|
||||||
|
(key, entry)
|
||||||
|
for key, entry in self._ts_cache.items()
|
||||||
|
if entry.borrow_count == 0 and key != protected_key
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if idle is None:
|
||||||
|
return
|
||||||
|
key, entry = idle
|
||||||
|
self._ts_cache.pop(key)
|
||||||
await entry.pool.close()
|
await entry.pool.close()
|
||||||
logger.info(
|
logger.info(
|
||||||
"Evicted TimescaleDB pool for project %s (%s)",
|
"Evicted TimescaleDB pool for project %s (%s)",
|
||||||
@@ -232,9 +238,22 @@ class ProjectConnectionManager:
|
|||||||
key.db_role,
|
key.db_role,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _evict_pg_raw_if_needed(self) -> None:
|
async def _evict_pg_raw_if_needed(
|
||||||
|
self, protected_key: CacheKey | None = None
|
||||||
|
) -> None:
|
||||||
while len(self._pg_raw_cache) > settings.PROJECT_PG_CACHE_SIZE:
|
while len(self._pg_raw_cache) > settings.PROJECT_PG_CACHE_SIZE:
|
||||||
key, entry = self._pg_raw_cache.popitem(last=False)
|
idle = next(
|
||||||
|
(
|
||||||
|
(key, entry)
|
||||||
|
for key, entry in self._pg_raw_cache.items()
|
||||||
|
if entry.borrow_count == 0 and key != protected_key
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if idle is None:
|
||||||
|
return
|
||||||
|
key, entry = idle
|
||||||
|
self._pg_raw_cache.pop(key)
|
||||||
await entry.pool.close()
|
await entry.pool.close()
|
||||||
logger.info(
|
logger.info(
|
||||||
"Evicted PostgreSQL pool for project %s (%s)",
|
"Evicted PostgreSQL pool for project %s (%s)",
|
||||||
@@ -242,17 +261,61 @@ class ProjectConnectionManager:
|
|||||||
key.db_role,
|
key.db_role,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def close_all(self) -> None:
|
async def close_project(
|
||||||
async with self._pg_lock:
|
self, project_id: UUID, db_role: str | None = None
|
||||||
for key, entry in list(self._pg_cache.items()):
|
) -> bool:
|
||||||
await entry.engine.dispose()
|
"""Close this worker's idle pools for a project.
|
||||||
|
|
||||||
|
Returns ``False`` without interrupting requests when any matching pool is
|
||||||
|
currently borrowed. Callers may retry after those requests complete.
|
||||||
|
"""
|
||||||
|
closed = True
|
||||||
|
for cache, lock, label in (
|
||||||
|
(self._ts_cache, self._ts_lock, "TimescaleDB"),
|
||||||
|
(self._pg_raw_cache, self._pg_raw_lock, "PostgreSQL"),
|
||||||
|
):
|
||||||
|
async with lock:
|
||||||
|
keys = [
|
||||||
|
key
|
||||||
|
for key in cache
|
||||||
|
if key.project_id == project_id
|
||||||
|
and (db_role is None or key.db_role == db_role)
|
||||||
|
]
|
||||||
|
for key in keys:
|
||||||
|
entry = cache[key]
|
||||||
|
if entry.borrow_count:
|
||||||
|
closed = False
|
||||||
|
continue
|
||||||
|
cache.pop(key)
|
||||||
|
await entry.pool.close()
|
||||||
logger.info(
|
logger.info(
|
||||||
"Closed PostgreSQL engine for project %s (%s)",
|
"Closed %s pool for project %s (%s)",
|
||||||
|
label,
|
||||||
key.project_id,
|
key.project_id,
|
||||||
key.db_role,
|
key.db_role,
|
||||||
)
|
)
|
||||||
self._pg_cache.clear()
|
for retired, lock in (
|
||||||
|
(self._retired_ts, self._ts_lock),
|
||||||
|
(self._retired_pg, self._pg_raw_lock),
|
||||||
|
):
|
||||||
|
async with lock:
|
||||||
|
matches = [
|
||||||
|
item
|
||||||
|
for item in retired
|
||||||
|
if item[0].project_id == project_id
|
||||||
|
and (db_role is None or item[0].db_role == db_role)
|
||||||
|
]
|
||||||
|
if any(entry.borrow_count for _key, entry in matches):
|
||||||
|
closed = False
|
||||||
|
for item in matches:
|
||||||
|
key, entry = item
|
||||||
|
if entry.borrow_count:
|
||||||
|
continue
|
||||||
|
retired.remove(item)
|
||||||
|
await entry.pool.close()
|
||||||
|
return closed
|
||||||
|
|
||||||
|
async def close_all(self) -> None:
|
||||||
async with self._ts_lock:
|
async with self._ts_lock:
|
||||||
for key, entry in list(self._ts_cache.items()):
|
for key, entry in list(self._ts_cache.items()):
|
||||||
await entry.pool.close()
|
await entry.pool.close()
|
||||||
@@ -262,6 +325,9 @@ class ProjectConnectionManager:
|
|||||||
key.db_role,
|
key.db_role,
|
||||||
)
|
)
|
||||||
self._ts_cache.clear()
|
self._ts_cache.clear()
|
||||||
|
for _key, entry in self._retired_ts:
|
||||||
|
await entry.pool.close()
|
||||||
|
self._retired_ts.clear()
|
||||||
|
|
||||||
async with self._pg_raw_lock:
|
async with self._pg_raw_lock:
|
||||||
for key, entry in list(self._pg_raw_cache.items()):
|
for key, entry in list(self._pg_raw_cache.items()):
|
||||||
@@ -272,6 +338,9 @@ class ProjectConnectionManager:
|
|||||||
key.db_role,
|
key.db_role,
|
||||||
)
|
)
|
||||||
self._pg_raw_cache.clear()
|
self._pg_raw_cache.clear()
|
||||||
|
for _key, entry in self._retired_pg:
|
||||||
|
await entry.pool.close()
|
||||||
|
self._retired_pg.clear()
|
||||||
|
|
||||||
|
|
||||||
project_connection_manager = ProjectConnectionManager()
|
project_connection_manager = ProjectConnectionManager()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user