Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2581631b51 | ||
|
|
c250e97b87 | ||
|
|
69a7d53aff | ||
|
|
e4975b7be3 | ||
|
|
a7e1ce6ef4 | ||
|
|
4350612807 | ||
|
|
b0a23a8012 | ||
|
|
70c5b0e445 | ||
|
|
29f691731c | ||
|
|
1432934f12 | ||
|
|
b0e9d480ef | ||
|
|
87d922ea61 | ||
|
|
c126e99b60 | ||
|
|
b6a6527bab | ||
|
|
f010f071eb | ||
|
|
0be31869b5 | ||
|
|
eac6b78598 | ||
|
|
1d88f8efbe | ||
|
|
ba947b616b | ||
|
|
ae1a657554 | ||
|
|
3fbb17bb30 | ||
|
|
ddbb50173c | ||
|
|
437eb5a19a | ||
|
|
31e2728db1 | ||
|
|
03bb2d75c2 | ||
|
|
b977bf6725 | ||
|
|
045d6c5b49 |
+5
-2
@@ -11,9 +11,12 @@ dist/
|
||||
package/
|
||||
temp/
|
||||
data/
|
||||
# db_inp/
|
||||
db_inp/
|
||||
inp/
|
||||
# .env
|
||||
.env
|
||||
.env.*
|
||||
logs/
|
||||
coverage/
|
||||
*.pyc
|
||||
*.dump
|
||||
app/algorithms/health/model/my_survival_forest_model_quxi.joblib
|
||||
|
||||
@@ -45,6 +45,7 @@ METADATA_DB_PASSWORD="password"
|
||||
KEYCLOAK_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
|
||||
KEYCLOAK_ALGORITHM=RS256
|
||||
KEYCLOAK_AUDIENCE="account"
|
||||
KEYCLOAK_ACCESS_TOKEN_MAX_AGE_SECONDS=900
|
||||
|
||||
|
||||
# ============================================
|
||||
|
||||
+16
-251
@@ -1,262 +1,27 @@
|
||||
name: Server CI/CD
|
||||
name: Server CI/CD v2
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
- "latest"
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
docker-image:
|
||||
runs-on: ubuntu-22.04
|
||||
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
|
||||
build-test-publish-and-deploy:
|
||||
uses: OrgTJWater/ci-templates/.gitea/workflows/container-cd.yml@main
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Normalize image metadata
|
||||
env:
|
||||
RAW_REGISTRY_HOST: ${{ vars.REGISTRY_HOST }}
|
||||
RAW_REPOSITORY: ${{ github.repository }}
|
||||
RAW_REF_NAME: ${{ github.ref_name }}
|
||||
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_REPOSITORY_PATH="$(printf '%s' "$REPOSITORY_PATH" | 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:
|
||||
image_name: gitea.waternetwork.cn/orgtjwater/tjwater-backend
|
||||
dockerfile: Dockerfile
|
||||
build_context: .
|
||||
test_command: |
|
||||
test -f app/api/v1/endpoints/access.py
|
||||
grep -Fq 'api_router.include_router(access.router' app/api/v1/router.py
|
||||
grep -Fq '@router.get("/projects"' app/api/v1/endpoints/meta.py
|
||||
grep -Fq '@router.get("/projects/current"' app/api/v1/endpoints/project.py
|
||||
grep -Fq '@router.post("/audit-events"' app/api/v1/endpoints/audit.py
|
||||
deploy_service: backend
|
||||
deploy_host: 192.168.1.114
|
||||
secrets:
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
GITEA_SERVER_URL: ${{ github.server_url }}
|
||||
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."
|
||||
DEV_DEPLOY_SSH_KEY: ${{ secrets.DEV_DEPLOY_SSH_KEY }}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
This repository contains the TJWater Python backend. Main application code lives in `app/`: API routes under `app/api`, authentication in `app/auth`, configuration in `app/core`, database and repository code in `app/infra`, domain models/schemas in `app/domain`, and business logic in `app/services` and `app/algorithms`.
|
||||
|
||||
Tests are under `tests/`, split into `tests/unit`, `tests/api`, and `tests/auth`. CLI code lives in `cli/tjwater_cli`, with CLI tests in `cli/tests`. SQL and sample assets are stored in `resources/`; deployment files are in `Dockerfile`, `.gitea/workflows/package.yml`, and `infra/docker/docker-compose.yml`. Local data directories such as `db_inp/`, `temp/`, `data/`, and `.env` are ignored and should not be committed.
|
||||
Tests are under `tests/`, split into `tests/unit`, `tests/api`, and `tests/auth`. SQL and sample assets are stored in `resources/`; deployment files are in `Dockerfile`, `.gitea/workflows/package.yml`, and `infra/docker/docker-compose.yml`. Local data directories such as `db_inp/`, `temp/`, `data/`, and `.env` are ignored and should not be committed.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
@@ -29,7 +29,7 @@ The project uses `pytest`. Name test files `test_*.py` and test functions `test_
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
History uses a mix of Conventional Commit prefixes and concise Chinese messages, for example `feat(api): add Tianditu geocoding`, `fix(cli): constrain timeseries option values`, or `更新 cli 命令...`. Prefer `feat(scope): ...`, `fix(scope): ...`, or a clear Chinese summary.
|
||||
History uses a mix of Conventional Commit prefixes and concise Chinese messages, for example `feat(api): add Tianditu geocoding` or `fix(auth): validate project context`. Prefer `feat(scope): ...`, `fix(scope): ...`, or a clear Chinese summary.
|
||||
|
||||
Pull requests should describe the behavior change, list verification commands, mention configuration or migration impacts, and link related issues. Include API examples or screenshots only when they clarify user-facing behavior.
|
||||
|
||||
|
||||
@@ -13,10 +13,29 @@ TJWater metadata stores only business snapshots and authorization data:
|
||||
The backend does not accept passwords, does not issue local JWTs, and does not
|
||||
trust frontend-supplied user IDs.
|
||||
|
||||
## Fixed Project RBAC
|
||||
|
||||
Project roles are stored directly in
|
||||
`user_project_membership.project_role`; there is no separate role table or
|
||||
user-defined permission editor in this delivery.
|
||||
|
||||
| Role | Main access |
|
||||
| --- | --- |
|
||||
| `modeler` | Model upload/import, simulation, burst, risk, and optimization analysis |
|
||||
| `dispatcher` | SCADA cleaning, simulation and burst analysis |
|
||||
| `auditor` | Project read access and project-scoped audit logs |
|
||||
| `viewer` | WebGIS and read-only risk results |
|
||||
|
||||
Legacy `owner`, `admin`, and `member` values remain supported for existing
|
||||
records. The backend is the authorization boundary; the frontend uses
|
||||
`GET /api/v1/access/context` only to hide unavailable menus and guard routes.
|
||||
System admins receive environment, membership, and global-audit permissions,
|
||||
but still need a project membership for project business APIs.
|
||||
|
||||
## Login Snapshot Refresh
|
||||
|
||||
Every authenticated metadata-user resolution validates the Keycloak access token
|
||||
and reads `sub`, `preferred_username` or `username`, and `email` claims. The
|
||||
and reads `sub`, `preferred_username`, and `email` claims. The
|
||||
backend finds `users` by `keycloak_id = sub`, rejects inactive or missing users,
|
||||
then refreshes `username`, `email`, and `last_login_at`.
|
||||
|
||||
@@ -77,14 +96,22 @@ Apply metadata patches in order:
|
||||
|
||||
1. `resources/sql/004_metadata_auth_management.sql`
|
||||
2. `resources/sql/005_metadata_project_configuration.sql`
|
||||
3. `resources/sql/006_metadata_rbac_roles.sql`
|
||||
|
||||
`004` creates Keycloak-backed metadata users and project memberships. `005`
|
||||
creates project and project database routing tables with uniqueness, role/type,
|
||||
and pool-size constraints.
|
||||
and pool-size constraints. `006` extends existing membership constraints with
|
||||
the fixed delivery roles.
|
||||
|
||||
## Frontend System Management
|
||||
|
||||
`/system-admin` is shown only after `GET /api/v1/admin/me` confirms metadata
|
||||
admin access. The page lets admins maintain metadata users, project members,
|
||||
projects, project database routing for `biz_data` and `iot_data`, connection
|
||||
health checks. This replaces direct SQL editing for normal project onboarding.
|
||||
`/system-admin` is shown only when `GET /api/v1/access/context` returns
|
||||
`environment.manage`. The page lets admins maintain metadata users, project
|
||||
members, projects, project database routing for `biz_data` and `iot_data`, and
|
||||
connection health checks. This replaces direct SQL editing for normal project
|
||||
onboarding.
|
||||
|
||||
Hydraulic model authoring is outside the Web application. Models are prepared
|
||||
in the desktop modeling client and uploaded/imported by an authorized modeler;
|
||||
the system administrator configures the project environment and database
|
||||
routing.
|
||||
|
||||
+2
-4
@@ -14,13 +14,11 @@ COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir uv
|
||||
RUN uv pip install --system --no-cache-dir -r requirements.txt
|
||||
|
||||
# 将代码放入子目录 'app',将数据放入子目录 'db_inp'
|
||||
# 这样临时文件默认会生成在 /app 下,而代码在 /app/app 下,实现了分离
|
||||
# 本地数据目录和环境变量在运行时通过 Compose 挂载或注入,
|
||||
# 不应进入镜像构建上下文。
|
||||
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}'" && \
|
||||
rm -f app/algorithms/health/model/my_survival_forest_model_quxi.zip
|
||||
# COPY db_inp ./db_inp
|
||||
COPY .env .
|
||||
RUN mkdir -p db_inp temp data inp
|
||||
|
||||
# 设置 PYTHONPATH 以便 uvicorn 找到 app 模块
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# TJWaterServerBinary 内部后端
|
||||
|
||||
`TJWaterServerBinary` 是 TJWater 内部版 Python 后端,基于 FastAPI 提供认证、项目、管网、模拟、爆管、漏损、SCADA 和地图服务集成能力。该仓库用于内部开发和完整功能维护。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Python 3.12
|
||||
- FastAPI / Uvicorn
|
||||
- Pydantic / SQLAlchemy / psycopg
|
||||
- Redis、PostgreSQL、PostGIS、TimescaleDB
|
||||
- WNTR、EPANET、Cython、科学计算与空间分析依赖
|
||||
- pytest
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
app/main.py FastAPI 入口
|
||||
app/api/ HTTP API 路由
|
||||
app/auth/ 认证和权限上下文
|
||||
app/core/ 配置、日志和基础设施初始化
|
||||
app/domain/ 领域模型和 Pydantic schema
|
||||
app/infra/ 数据库、缓存、EPANET 和外部集成
|
||||
app/services/ 业务服务编排
|
||||
app/algorithms/ 管网算法、模拟、爆管、漏损、清洗和健康分析
|
||||
app/native/ 本地管网数据读写与转换
|
||||
tests/ 后端测试
|
||||
resources/ SQL、模板和示例资源
|
||||
infra/docker/ Docker Compose 编排
|
||||
```
|
||||
|
||||
## 本地开发
|
||||
|
||||
推荐使用已有 conda 环境:
|
||||
|
||||
```bash
|
||||
conda run -n server python -m pytest tests/unit tests/auth -q
|
||||
conda run -n server uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
```
|
||||
|
||||
如需要进入环境:
|
||||
|
||||
```bash
|
||||
conda activate server
|
||||
```
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
conda run -n server python -m pytest tests -q
|
||||
conda run -n server python scripts/run_server.py
|
||||
docker build -t tjwater-server:local .
|
||||
docker compose -f infra/docker/docker-compose.yml config
|
||||
```
|
||||
|
||||
- `pytest`:运行自动化测试。
|
||||
- `scripts/run_server.py`:使用项目脚本启动服务。
|
||||
- `docker build`:构建后端镜像。
|
||||
- `docker compose config`:检查 compose 配置和变量展开。
|
||||
|
||||
## 开发规范
|
||||
|
||||
- Python 文件、函数、变量、Pydantic 字段、JSON body 字段和 query 参数使用 `snake_case`。
|
||||
- Python 类和 Pydantic 模型使用 `PascalCase`。
|
||||
- 新 HTTP 路径使用 `kebab-case`,例如 `/api/v1/pressure-status/analyze`。
|
||||
- 优先复用现有 FastAPI/service/repository 边界。
|
||||
- 不要把临时数据、数据库 dump、日志或本地运行产物纳入提交。
|
||||
|
||||
## 测试与发布
|
||||
|
||||
提交前根据改动范围运行最小有效测试:
|
||||
|
||||
```bash
|
||||
conda run -n server python -m pytest tests/unit tests/auth -q
|
||||
```
|
||||
|
||||
发布镜像前建议运行:
|
||||
|
||||
```bash
|
||||
docker build -t tjwater-server:local .
|
||||
```
|
||||
|
||||
Gitea 包工作流位于 `.gitea/workflows/package.yml`,通常由 tag 触发构建、推送镜像并通知部署 webhook。
|
||||
|
||||
## 安全规则
|
||||
|
||||
不要提交 `.env`、客户数据、数据库 dump、日志、生成缓存、`db_inp/`、`temp/`、`data/` 或本地密钥。CI/CD 凭据应放在 Gitea secrets 和仓库变量中。
|
||||
@@ -149,14 +149,16 @@ def valve_isolation_analysis(
|
||||
|
||||
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),
|
||||
"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": len(must_close_valves) > 0,
|
||||
"isolatable": isolatable,
|
||||
}
|
||||
|
||||
if len(target_elements) == 1:
|
||||
|
||||
@@ -1,14 +1,77 @@
|
||||
import psycopg
|
||||
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.core.config import get_pgconn_string
|
||||
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
|
||||
) -> None:
|
||||
name: str,
|
||||
scheme_name: str,
|
||||
sensor_number: int,
|
||||
min_diameter: int,
|
||||
username: str,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
基于改进灵敏度法进行压力监测点优化布置
|
||||
:param name: 数据库名称
|
||||
@@ -16,41 +79,32 @@ def pressure_sensor_placement_sensitivity(
|
||||
:param sensor_number: 传感器数目
|
||||
:param min_diameter: 最小管径
|
||||
:param username: 用户名
|
||||
:return:
|
||||
:return: 新建的监测点方案
|
||||
"""
|
||||
with _sensor_inp_lock(name):
|
||||
sensor_location = sensitivity.get_ID(
|
||||
name=name, sensor_num=sensor_number, min_diameter=min_diameter
|
||||
name=name,
|
||||
sensor_num=sensor_number,
|
||||
min_diameter=min_diameter,
|
||||
)
|
||||
try:
|
||||
conn_string = get_pgconn_string(db_name=name)
|
||||
with psycopg.connect(conn_string) as conn:
|
||||
with conn.cursor() as cur:
|
||||
sql = """
|
||||
INSERT INTO sensor_placement (scheme_name, sensor_number, min_diameter, username, sensor_location)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
"""
|
||||
|
||||
cur.execute(
|
||||
sql,
|
||||
(
|
||||
scheme_name,
|
||||
sensor_number,
|
||||
min_diameter,
|
||||
username,
|
||||
sensor_location,
|
||||
),
|
||||
return _create_validated_placement(
|
||||
name,
|
||||
scheme_name=scheme_name,
|
||||
min_diameter=min_diameter,
|
||||
username=username,
|
||||
sensor_location=sensor_location,
|
||||
)
|
||||
conn.commit()
|
||||
print("方案信息存储成功!")
|
||||
except Exception as e:
|
||||
print(f"存储方案信息时出错:{e}")
|
||||
|
||||
|
||||
# 2025/08/21
|
||||
# 基于kmeans聚类法进行压力监测点优化布置
|
||||
def pressure_sensor_placement_kmeans(
|
||||
name: str, scheme_name: str, sensor_number: int, min_diameter: int, username: str
|
||||
) -> None:
|
||||
name: str,
|
||||
scheme_name: str,
|
||||
sensor_number: int,
|
||||
min_diameter: int,
|
||||
username: str,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
基于聚类法进行压力监测点优化布置
|
||||
:param name: 数据库名称(注意,此处数据库名称也是inp文件名称,inp文件与pg库名要一样)
|
||||
@@ -58,34 +112,20 @@ def pressure_sensor_placement_kmeans(
|
||||
:param sensor_number: 传感器数目
|
||||
:param min_diameter: 最小管径
|
||||
:param username: 用户名
|
||||
:return:
|
||||
:return: 新建的监测点方案
|
||||
"""
|
||||
# dump_inp
|
||||
inp_name = f"./db_inp/{name}.db.inp"
|
||||
dump_inp(name, inp_name, "2")
|
||||
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
|
||||
name=name,
|
||||
sensor_num=sensor_number,
|
||||
min_diameter=min_diameter,
|
||||
)
|
||||
try:
|
||||
conn_string = get_pgconn_string(db_name=name)
|
||||
with psycopg.connect(conn_string) as conn:
|
||||
with conn.cursor() as cur:
|
||||
sql = """
|
||||
INSERT INTO sensor_placement (scheme_name, sensor_number, min_diameter, username, sensor_location)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
"""
|
||||
|
||||
cur.execute(
|
||||
sql,
|
||||
(
|
||||
scheme_name,
|
||||
sensor_number,
|
||||
min_diameter,
|
||||
username,
|
||||
sensor_location,
|
||||
),
|
||||
return _create_validated_placement(
|
||||
name,
|
||||
scheme_name=scheme_name,
|
||||
min_diameter=min_diameter,
|
||||
username=username,
|
||||
sensor_location=sensor_location,
|
||||
)
|
||||
conn.commit()
|
||||
print("方案信息存储成功!")
|
||||
except Exception as e:
|
||||
print(f"存储方案信息时出错:{e}")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -72,6 +72,7 @@ def burst_analysis(
|
||||
modify_variable_pump_pattern: dict[str, list] = None,
|
||||
modify_valve_opening: dict[str, float] = None,
|
||||
scheme_name: str = None,
|
||||
username: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
爆管模拟
|
||||
@@ -86,6 +87,9 @@ def burst_analysis(
|
||||
:param scheme_name: 方案名称
|
||||
:return:
|
||||
"""
|
||||
if not username:
|
||||
raise ValueError("username is required when storing burst analysis scheme")
|
||||
|
||||
scheme_detail: dict = {
|
||||
"burst_ID": burst_ID,
|
||||
"burst_size": burst_size,
|
||||
@@ -211,7 +215,7 @@ def burst_analysis(
|
||||
name=name,
|
||||
scheme_name=scheme_name,
|
||||
scheme_type="burst_analysis",
|
||||
username="admin",
|
||||
username=username,
|
||||
scheme_start_time=modify_pattern_start_time,
|
||||
scheme_detail=scheme_detail,
|
||||
)
|
||||
@@ -311,6 +315,8 @@ def flushing_analysis(
|
||||
drainage_node_ID: str = None,
|
||||
flushing_flow: float = 0,
|
||||
scheme_name: str = None,
|
||||
username: str | None = None,
|
||||
valve_control: dict[str, dict] = None,
|
||||
) -> None:
|
||||
"""
|
||||
管道冲洗模拟
|
||||
@@ -318,14 +324,19 @@ def flushing_analysis(
|
||||
:param modify_pattern_start_time: 模拟开始时间,格式为'2024-11-25T09:00:00+08:00'
|
||||
:param modify_total_duration: 模拟总历时,秒
|
||||
:param modify_valve_opening: dict中包含多个阀门开启度,str为阀门的id,float为修改后的阀门开启度
|
||||
:param valve_control: dict中可分别指定阀门的status、setting和k
|
||||
:param drainage_node_ID: 冲洗排放口所在节点ID
|
||||
:param flushing_flow: 冲洗水量,传入参数单位为m3/h
|
||||
:param scheme_name: 方案名称
|
||||
:return:
|
||||
"""
|
||||
if not username:
|
||||
raise ValueError("username is required when storing flushing analysis scheme")
|
||||
|
||||
scheme_detail: dict = {
|
||||
"duration": modify_total_duration,
|
||||
"valve_opening": modify_valve_opening,
|
||||
"valve_control": valve_control,
|
||||
"drainage_node_ID": drainage_node_ID,
|
||||
"flushing_flow": flushing_flow,
|
||||
}
|
||||
@@ -442,6 +453,7 @@ def flushing_analysis(
|
||||
modify_pattern_start_time=modify_pattern_start_time,
|
||||
modify_total_duration=modify_total_duration,
|
||||
modify_valve_opening=modify_valve_opening,
|
||||
valve_control=valve_control,
|
||||
scheme_type="flushing_analysis",
|
||||
scheme_name=scheme_name,
|
||||
)
|
||||
@@ -455,7 +467,7 @@ def flushing_analysis(
|
||||
name=name,
|
||||
scheme_name=scheme_name,
|
||||
scheme_type="flushing_analysis",
|
||||
username="admin",
|
||||
username=username,
|
||||
scheme_start_time=modify_pattern_start_time,
|
||||
scheme_detail=scheme_detail,
|
||||
)
|
||||
@@ -473,6 +485,7 @@ def contaminant_simulation(
|
||||
concentration: float, # 污染源浓度,单位mg/L
|
||||
scheme_name: str = None,
|
||||
source_pattern: str = None, # 污染源时间变化模式名称
|
||||
username: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
污染模拟
|
||||
@@ -486,6 +499,9 @@ def contaminant_simulation(
|
||||
:param scheme_name: 方案名称
|
||||
:return:
|
||||
"""
|
||||
if not username:
|
||||
raise ValueError("username is required when storing contaminant analysis scheme")
|
||||
|
||||
scheme_detail: dict = {
|
||||
"source": source,
|
||||
"concentration": concentration,
|
||||
@@ -608,7 +624,7 @@ def contaminant_simulation(
|
||||
name=name,
|
||||
scheme_name=scheme_name,
|
||||
scheme_type="contaminant_analysis",
|
||||
username="admin",
|
||||
username=username,
|
||||
scheme_start_time=modify_pattern_start_time,
|
||||
scheme_detail=scheme_detail,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ProblemDetails(BaseModel):
|
||||
"""RFC 9457 compatible error response used by the REST contract."""
|
||||
|
||||
type: str
|
||||
title: str
|
||||
status: int
|
||||
detail: str
|
||||
instance: str
|
||||
code: str
|
||||
trace_id: str
|
||||
errors: list[dict[str, Any]] = Field(default_factory=list)
|
||||
|
||||
|
||||
def _trace_id(request: Request) -> str:
|
||||
return request.headers.get("X-Request-Id") or str(uuid4())
|
||||
|
||||
|
||||
def _problem_response(
|
||||
request: Request,
|
||||
*,
|
||||
status_code: int,
|
||||
title: str,
|
||||
detail: str,
|
||||
code: str,
|
||||
errors: list[dict[str, Any]] | None = None,
|
||||
) -> JSONResponse:
|
||||
problem = ProblemDetails(
|
||||
type=f"https://tjwater.example/problems/{code.replace('_', '-')}",
|
||||
title=title,
|
||||
status=status_code,
|
||||
detail=detail,
|
||||
instance=request.url.path,
|
||||
code=code,
|
||||
trace_id=_trace_id(request),
|
||||
errors=errors or [],
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=status_code,
|
||||
content=problem.model_dump(mode="json"),
|
||||
media_type="application/problem+json",
|
||||
)
|
||||
|
||||
|
||||
def install_problem_details_handlers(app: FastAPI) -> None:
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_error_handler(
|
||||
request: Request,
|
||||
exc: RequestValidationError,
|
||||
) -> JSONResponse:
|
||||
return _problem_response(
|
||||
request,
|
||||
status_code=422,
|
||||
title="Validation error",
|
||||
detail="Request validation failed",
|
||||
code="validation_error",
|
||||
errors=exc.errors(),
|
||||
)
|
||||
|
||||
@app.exception_handler(HTTPException)
|
||||
async def http_error_handler(request: Request, exc: HTTPException) -> JSONResponse:
|
||||
detail = exc.detail if isinstance(exc.detail, str) else str(exc.detail)
|
||||
code_by_status = {
|
||||
401: "unauthenticated",
|
||||
403: "forbidden",
|
||||
404: "not_found",
|
||||
409: "conflict",
|
||||
422: "validation_error",
|
||||
503: "dependency_unavailable",
|
||||
}
|
||||
return _problem_response(
|
||||
request,
|
||||
status_code=exc.status_code,
|
||||
title=code_by_status.get(exc.status_code, "request_error")
|
||||
.replace("_", " ")
|
||||
.title(),
|
||||
detail=detail,
|
||||
code=code_by_status.get(exc.status_code, "request_error"),
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
from fastapi import APIRouter, Depends, Header
|
||||
|
||||
from app.auth.metadata_dependencies import (
|
||||
get_current_metadata_user,
|
||||
get_metadata_repository,
|
||||
)
|
||||
from app.auth.permissions import resolve_permissions
|
||||
from app.auth.project_dependencies import resolve_project_context
|
||||
from app.domain.schemas.access import AccessContextResponse
|
||||
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/access-context", response_model=AccessContextResponse)
|
||||
async def get_access_context(
|
||||
x_project_id: str | None = Header(default=None, alias="X-Project-Id"),
|
||||
current_user=Depends(get_current_metadata_user),
|
||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||
) -> AccessContextResponse:
|
||||
project_context = (
|
||||
await resolve_project_context(x_project_id, current_user, metadata_repo)
|
||||
if x_project_id
|
||||
else None
|
||||
)
|
||||
permissions = resolve_permissions(
|
||||
project_role=project_context.project_role if project_context else None,
|
||||
system_role=current_user.role,
|
||||
is_superuser=current_user.is_superuser,
|
||||
)
|
||||
return AccessContextResponse(
|
||||
user_id=current_user.id,
|
||||
username=current_user.username,
|
||||
system_role=current_user.role,
|
||||
is_system_admin=current_user.is_superuser or current_user.role == "admin",
|
||||
project_id=project_context.project_id if project_context else None,
|
||||
project_role=project_context.project_role if project_context else None,
|
||||
permissions=sorted(permissions),
|
||||
)
|
||||
@@ -151,14 +151,14 @@ async def _upsert_and_audit_metadata_user(
|
||||
return MetadataUserResponse.model_validate(user)
|
||||
|
||||
|
||||
@router.get("/me", response_model=MetadataUserResponse)
|
||||
@router.get("/admin/users/me", response_model=MetadataUserResponse)
|
||||
async def get_metadata_admin_me(
|
||||
current_user=Depends(get_current_metadata_admin),
|
||||
) -> MetadataUserResponse:
|
||||
return MetadataUserResponse.model_validate(current_user)
|
||||
|
||||
|
||||
@router.post("/users/sync", response_model=MetadataUserResponse)
|
||||
@router.post("/admin/user-syncs", response_model=MetadataUserResponse)
|
||||
async def sync_metadata_user(
|
||||
payload: MetadataUserSyncRequest,
|
||||
current_user=Depends(get_current_metadata_admin),
|
||||
@@ -184,7 +184,7 @@ async def sync_metadata_user(
|
||||
|
||||
|
||||
|
||||
@router.post("/users/sync/batch", response_model=List[MetadataUserSyncResult])
|
||||
@router.post("/admin/user-syncs/batches", response_model=List[MetadataUserSyncResult])
|
||||
async def sync_metadata_users_batch(
|
||||
payload: MetadataUsersBatchSyncRequest,
|
||||
current_user=Depends(get_current_metadata_admin),
|
||||
@@ -228,7 +228,7 @@ async def sync_metadata_users_batch(
|
||||
return results
|
||||
|
||||
|
||||
@router.get("/users", response_model=List[MetadataUserResponse])
|
||||
@router.get("/admin/users", response_model=List[MetadataUserResponse])
|
||||
async def list_metadata_users(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
@@ -239,7 +239,7 @@ async def list_metadata_users(
|
||||
return [MetadataUserResponse.model_validate(user) for user in users]
|
||||
|
||||
|
||||
@router.get("/projects", response_model=List[AdminProjectResponse])
|
||||
@router.get("/admin/projects", response_model=List[AdminProjectResponse])
|
||||
async def list_admin_projects(
|
||||
current_user=Depends(get_current_metadata_admin),
|
||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||
@@ -249,7 +249,7 @@ async def list_admin_projects(
|
||||
|
||||
|
||||
@router.post(
|
||||
"/projects",
|
||||
"/admin/projects",
|
||||
response_model=AdminProjectResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
@@ -266,6 +266,7 @@ async def create_admin_project(
|
||||
gs_workspace=payload.gs_workspace,
|
||||
map_extent=payload.map_extent,
|
||||
status=payload.status,
|
||||
creator_user_id=current_user.id,
|
||||
)
|
||||
except IntegrityError as exc:
|
||||
raise HTTPException(
|
||||
@@ -292,7 +293,7 @@ async def create_admin_project(
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/projects/{project_id}",
|
||||
"/admin/projects/{project_id}",
|
||||
response_model=AdminProjectResponse,
|
||||
)
|
||||
async def update_admin_project(
|
||||
@@ -331,7 +332,7 @@ async def update_admin_project(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/projects/{project_id}/databases",
|
||||
"/admin/projects/{project_id}/databases",
|
||||
response_model=List[ProjectDatabaseResponse],
|
||||
)
|
||||
async def list_project_databases(
|
||||
@@ -347,7 +348,7 @@ async def list_project_databases(
|
||||
|
||||
|
||||
@router.put(
|
||||
"/projects/{project_id}/databases",
|
||||
"/admin/projects/{project_id}/databases",
|
||||
response_model=ProjectDatabaseResponse,
|
||||
)
|
||||
async def upsert_project_database(
|
||||
@@ -420,7 +421,7 @@ async def upsert_project_database(
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/projects/{project_id}/databases/{db_role}",
|
||||
"/admin/projects/{project_id}/databases/{db_role}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
async def delete_project_database(
|
||||
@@ -448,7 +449,7 @@ async def delete_project_database(
|
||||
|
||||
|
||||
@router.post(
|
||||
"/projects/{project_id}/databases/{db_role}/health",
|
||||
"/admin/projects/{project_id}/databases/{db_role}/health-checks",
|
||||
response_model=ProjectDatabaseHealthResponse,
|
||||
)
|
||||
async def check_project_database_health(
|
||||
@@ -503,7 +504,7 @@ async def check_project_database_health(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/users/{user_id}", response_model=MetadataUserResponse)
|
||||
@router.get("/admin/users/{user_id}", response_model=MetadataUserResponse)
|
||||
async def get_metadata_user(
|
||||
user_id: UUID = Path(...),
|
||||
current_user=Depends(get_current_metadata_admin),
|
||||
@@ -515,7 +516,7 @@ async def get_metadata_user(
|
||||
return MetadataUserResponse.model_validate(user)
|
||||
|
||||
|
||||
@router.patch("/users/{user_id}", response_model=MetadataUserResponse)
|
||||
@router.patch("/admin/users/{user_id}", response_model=MetadataUserResponse)
|
||||
async def update_metadata_user(
|
||||
payload: MetadataUserUpdateRequest,
|
||||
user_id: UUID = Path(...),
|
||||
@@ -548,7 +549,7 @@ async def update_metadata_user(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/projects/{project_id}/members",
|
||||
"/admin/projects/{project_id}/members",
|
||||
response_model=List[ProjectMemberResponse],
|
||||
)
|
||||
async def list_project_members(
|
||||
@@ -566,7 +567,7 @@ async def list_project_members(
|
||||
|
||||
|
||||
@router.post(
|
||||
"/projects/{project_id}/members",
|
||||
"/admin/projects/{project_id}/members",
|
||||
response_model=ProjectMemberResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
@@ -621,7 +622,7 @@ async def add_project_member(
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/projects/{project_id}/members/{user_id}",
|
||||
"/admin/projects/{project_id}/members/{user_id}",
|
||||
response_model=ProjectMemberResponse,
|
||||
)
|
||||
async def update_project_member(
|
||||
@@ -667,7 +668,7 @@ async def update_project_member(
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/projects/{project_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete("/admin/projects/{project_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def remove_project_member(
|
||||
project_id: UUID = Path(...),
|
||||
user_id: UUID = Path(...),
|
||||
|
||||
@@ -9,6 +9,7 @@ from app.auth.project_dependencies import (
|
||||
ProjectContext,
|
||||
get_project_context,
|
||||
)
|
||||
from app.auth.permissions import permissions_for_context
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -22,10 +23,11 @@ class AgentAuthContextResponse(BaseModel):
|
||||
project_id: str
|
||||
network: str
|
||||
project_role: str
|
||||
permissions: list[str]
|
||||
token_expires_at: str | None = None
|
||||
|
||||
|
||||
@router.get("/agent/auth/context", response_model=AgentAuthContextResponse)
|
||||
@router.get("/agent-auth-context", response_model=AgentAuthContextResponse)
|
||||
async def get_agent_auth_context(
|
||||
ctx: ProjectContext = Depends(get_project_context),
|
||||
current_user=Depends(get_current_metadata_user),
|
||||
@@ -46,5 +48,6 @@ async def get_agent_auth_context(
|
||||
project_id=str(ctx.project_id),
|
||||
network=ctx.project_code,
|
||||
project_role=ctx.project_role,
|
||||
permissions=sorted(permissions_for_context(ctx)),
|
||||
token_expires_at=token_expires_at,
|
||||
)
|
||||
|
||||
@@ -1,56 +1,52 @@
|
||||
"""
|
||||
审计日志 API 接口
|
||||
|
||||
仅管理员可访问
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, Query, Path
|
||||
from app.domain.schemas.audit import AuditLogResponse
|
||||
from app.infra.db.metadb.repositories.audit_repository import AuditRepository
|
||||
from typing import Literal
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Request, status
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth.metadata_dependencies import (
|
||||
get_current_metadata_admin,
|
||||
get_current_metadata_user,
|
||||
)
|
||||
from app.core.audit import AuditAction, log_audit_event
|
||||
from app.domain.schemas.audit import AuditLogResponse
|
||||
from app.infra.db.metadb.database import get_metadata_session
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.infra.db.metadb.repositories.audit_repository import AuditRepository
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class SessionAuditEventRequest(BaseModel):
|
||||
event: Literal["login", "logout"]
|
||||
|
||||
|
||||
async def get_audit_repository(
|
||||
session: AsyncSession = Depends(get_metadata_session),
|
||||
) -> AuditRepository:
|
||||
"""获取审计日志仓储"""
|
||||
return AuditRepository(session)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/logs",
|
||||
"/audit-logs",
|
||||
summary="查询审计日志",
|
||||
description="查询审计日志(仅管理员)",
|
||||
response_model=List[AuditLogResponse],
|
||||
response_model=list[AuditLogResponse],
|
||||
)
|
||||
async def get_audit_logs(
|
||||
user_id: Optional[UUID] = Query(None, description="按用户ID过滤"),
|
||||
project_id: Optional[UUID] = Query(None, description="按项目ID过滤"),
|
||||
action: Optional[str] = Query(None, description="按操作类型过滤"),
|
||||
resource_type: Optional[str] = Query(None, description="按资源类型过滤"),
|
||||
start_time: Optional[datetime] = Query(None, description="开始时间"),
|
||||
end_time: Optional[datetime] = Query(None, description="结束时间"),
|
||||
user_id: UUID | None = Query(None, description="按用户ID过滤"),
|
||||
project_id: UUID | None = Query(None, description="按项目ID过滤"),
|
||||
action: str | None = Query(None, description="按操作类型过滤"),
|
||||
resource_type: str | None = Query(None, description="按资源类型过滤"),
|
||||
start_time: datetime | None = Query(None, description="开始时间"),
|
||||
end_time: datetime | None = Query(None, description="结束时间"),
|
||||
skip: int = Query(0, ge=0, description="跳过记录数"),
|
||||
limit: int = Query(100, ge=1, le=1000, description="限制记录数"),
|
||||
current_user=Depends(get_current_metadata_admin),
|
||||
_current_user=Depends(get_current_metadata_admin),
|
||||
audit_repo: AuditRepository = Depends(get_audit_repository),
|
||||
) -> List[AuditLogResponse]:
|
||||
"""
|
||||
查询审计日志
|
||||
|
||||
支持按用户、时间、操作类型等条件过滤,仅管理员可访问
|
||||
"""
|
||||
logs = await audit_repo.get_logs(
|
||||
) -> list[AuditLogResponse]:
|
||||
return await audit_repo.get_logs(
|
||||
user_id=user_id,
|
||||
project_id=project_id,
|
||||
action=action,
|
||||
@@ -60,29 +56,23 @@ async def get_audit_logs(
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
return logs
|
||||
|
||||
|
||||
@router.get(
|
||||
"/logs/count",
|
||||
"/audit-logs/count",
|
||||
summary="获取审计日志总数",
|
||||
description="获取审计日志总数(仅管理员)",
|
||||
)
|
||||
async def get_audit_logs_count(
|
||||
user_id: Optional[UUID] = Query(None, description="按用户ID过滤"),
|
||||
project_id: Optional[UUID] = Query(None, description="按项目ID过滤"),
|
||||
action: Optional[str] = Query(None, description="按操作类型过滤"),
|
||||
resource_type: Optional[str] = Query(None, description="按资源类型过滤"),
|
||||
start_time: Optional[datetime] = Query(None, description="开始时间"),
|
||||
end_time: Optional[datetime] = Query(None, description="结束时间"),
|
||||
current_user=Depends(get_current_metadata_admin),
|
||||
user_id: UUID | None = Query(None, description="按用户ID过滤"),
|
||||
project_id: UUID | None = Query(None, description="按项目ID过滤"),
|
||||
action: str | None = Query(None, description="按操作类型过滤"),
|
||||
resource_type: str | None = Query(None, description="按资源类型过滤"),
|
||||
start_time: datetime | None = Query(None, description="开始时间"),
|
||||
end_time: datetime | None = Query(None, description="结束时间"),
|
||||
_current_user=Depends(get_current_metadata_admin),
|
||||
audit_repo: AuditRepository = Depends(get_audit_repository),
|
||||
) -> dict:
|
||||
"""
|
||||
获取审计日志总数
|
||||
|
||||
获取符合条件的审计日志的总数,仅管理员可访问
|
||||
"""
|
||||
count = await audit_repo.get_log_count(
|
||||
user_id=user_id,
|
||||
project_id=project_id,
|
||||
@@ -94,27 +84,42 @@ async def get_audit_logs_count(
|
||||
return {"count": count}
|
||||
|
||||
|
||||
@router.post("/audit-events", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def record_session_event(
|
||||
payload: SessionAuditEventRequest,
|
||||
request: Request,
|
||||
current_user=Depends(get_current_metadata_user),
|
||||
session: AsyncSession = Depends(get_metadata_session),
|
||||
) -> None:
|
||||
await log_audit_event(
|
||||
action=AuditAction.LOGIN if payload.event == "login" else AuditAction.LOGOUT,
|
||||
user_id=current_user.id,
|
||||
resource_type="session",
|
||||
resource_id=str(current_user.keycloak_id),
|
||||
ip_address=request.client.host if request.client else None,
|
||||
request_method=request.method,
|
||||
request_path=request.url.path,
|
||||
response_status=status.HTTP_204_NO_CONTENT,
|
||||
session=session,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/logs/my",
|
||||
"/audit-logs/mine",
|
||||
summary="查询我的审计日志",
|
||||
description="查询当前用户的审计日志",
|
||||
response_model=List[AuditLogResponse],
|
||||
response_model=list[AuditLogResponse],
|
||||
)
|
||||
async def get_my_audit_logs(
|
||||
action: Optional[str] = Query(None, description="按操作类型过滤"),
|
||||
start_time: Optional[datetime] = Query(None, description="开始时间"),
|
||||
end_time: Optional[datetime] = Query(None, description="结束时间"),
|
||||
action: str | None = Query(None, description="按操作类型过滤"),
|
||||
start_time: datetime | None = Query(None, description="开始时间"),
|
||||
end_time: datetime | None = Query(None, description="结束时间"),
|
||||
skip: int = Query(0, ge=0, description="跳过记录数"),
|
||||
limit: int = Query(100, ge=1, le=1000, description="限制记录数"),
|
||||
current_user=Depends(get_current_metadata_user),
|
||||
audit_repo: AuditRepository = Depends(get_audit_repository),
|
||||
) -> List[AuditLogResponse]:
|
||||
"""
|
||||
查询当前用户的审计日志
|
||||
|
||||
普通用户只能查看自己的操作记录
|
||||
"""
|
||||
logs = await audit_repo.get_logs(
|
||||
) -> list[AuditLogResponse]:
|
||||
return await audit_repo.get_logs(
|
||||
user_id=current_user.id,
|
||||
action=action,
|
||||
start_time=start_time,
|
||||
@@ -122,4 +127,3 @@ async def get_my_audit_logs(
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
return logs
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
|
||||
from fastapi import APIRouter, Depends, HTTPException, Body
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
||||
from app.services.burst_detection import (
|
||||
get_burst_detection_scheme_detail,
|
||||
list_burst_detection_schemes,
|
||||
run_burst_detection,
|
||||
)
|
||||
|
||||
@@ -50,7 +48,7 @@ class BurstDetectionRequest(BaseModel):
|
||||
|
||||
|
||||
@router.post(
|
||||
"/detect/",
|
||||
"/burst-detections",
|
||||
summary="执行爆管检测",
|
||||
description="基于压力观测数据和其他参数执行爆管检测分析"
|
||||
)
|
||||
@@ -78,64 +76,3 @@ async def detect_burst(
|
||||
return run_burst_detection(**data.model_dump(), username=username)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/schemes/",
|
||||
summary="查询爆管检测方案列表",
|
||||
description="获取指定网络的所有爆管检测方案"
|
||||
)
|
||||
async def query_burst_detection_schemes(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
query_date: datetime | None = Query(None, description="查询日期(可选)"),
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
获取爆管检测方案列表。
|
||||
|
||||
查询指定网络的所有已配置的爆管检测方案,
|
||||
可按日期进行筛选。
|
||||
|
||||
Args:
|
||||
network: 管网名称(或数据库名称)
|
||||
query_date: 查询日期(可选)
|
||||
|
||||
Returns:
|
||||
爆管检测方案列表
|
||||
|
||||
Raises:
|
||||
HTTPException: 当查询失败时
|
||||
"""
|
||||
try:
|
||||
return list_burst_detection_schemes(network=network, query_date=query_date)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/schemes/{scheme_name}",
|
||||
summary="获取爆管检测方案详情",
|
||||
description="获取指定爆管检测方案的详细信息"
|
||||
)
|
||||
async def query_burst_detection_scheme_detail(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
scheme_name: str = Path(..., description="爆管检测方案名称"),
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
获取爆管检测方案详情。
|
||||
|
||||
查询指定爆管检测方案的完整配置和参数信息。
|
||||
|
||||
Args:
|
||||
network: 管网名称(或数据库名称)
|
||||
scheme_name: 爆管检测方案名称
|
||||
|
||||
Returns:
|
||||
包含方案详情的字典
|
||||
|
||||
Raises:
|
||||
HTTPException: 当查询失败时
|
||||
"""
|
||||
try:
|
||||
return get_burst_detection_scheme_detail(network=network, scheme_name=scheme_name)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
@@ -3,13 +3,11 @@ from datetime import datetime
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
|
||||
from fastapi import APIRouter, Depends, HTTPException, Body
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
||||
from app.services.burst_location import (
|
||||
get_burst_location_scheme_detail,
|
||||
list_burst_location_schemes,
|
||||
run_burst_location_by_network,
|
||||
)
|
||||
|
||||
@@ -40,7 +38,7 @@ class BurstLocationRequest(BaseModel):
|
||||
|
||||
|
||||
@router.post(
|
||||
"/locate/",
|
||||
"/burst-locations",
|
||||
summary="执行爆管定位",
|
||||
description="基于压力和流量数据定位管网中的爆管位置"
|
||||
)
|
||||
@@ -68,64 +66,3 @@ async def locate_burst(
|
||||
return run_burst_location_by_network(**data.model_dump(), username=username)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/schemes/",
|
||||
summary="查询爆管定位方案列表",
|
||||
description="获取指定网络的所有爆管定位方案"
|
||||
)
|
||||
async def query_burst_schemes(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
query_date: datetime | None = Query(None, description="查询日期(可选)")
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
获取爆管定位方案列表。
|
||||
|
||||
查询指定网络的所有已配置的爆管定位方案,
|
||||
可按日期进行筛选。
|
||||
|
||||
Args:
|
||||
network: 管网名称(或数据库名称)
|
||||
query_date: 查询日期(可选)
|
||||
|
||||
Returns:
|
||||
爆管定位方案列表
|
||||
|
||||
Raises:
|
||||
HTTPException: 当查询失败时
|
||||
"""
|
||||
try:
|
||||
return list_burst_location_schemes(network=network, query_date=query_date)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/schemes/{scheme_name}",
|
||||
summary="获取爆管定位方案详情",
|
||||
description="获取指定爆管定位方案的详细信息"
|
||||
)
|
||||
async def query_burst_scheme_detail(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
scheme_name: str = Path(..., description="爆管定位方案名称")
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
获取爆管定位方案详情。
|
||||
|
||||
查询指定爆管定位方案的完整配置和参数信息。
|
||||
|
||||
Args:
|
||||
network: 管网名称(或数据库名称)
|
||||
scheme_name: 爆管定位方案名称
|
||||
|
||||
Returns:
|
||||
包含方案详情的字典
|
||||
|
||||
Raises:
|
||||
HTTPException: 当查询失败时
|
||||
"""
|
||||
try:
|
||||
return get_burst_location_scheme_detail(network=network, scheme_name=scheme_name)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
@@ -3,7 +3,7 @@ from app.infra.cache.redis_client import redis_client
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/clearrediskey/", summary="清除单个缓存键", description="根据键名清除单个Redis缓存")
|
||||
@router.delete("/redis-keys/detail", summary="清除单个缓存键", description="根据键名清除单个Redis缓存")
|
||||
async def fastapi_clear_redis_key(key: str = Query(..., description="缓存键名")):
|
||||
"""
|
||||
清除单个缓存键
|
||||
@@ -14,7 +14,7 @@ async def fastapi_clear_redis_key(key: str = Query(..., description="缓存键
|
||||
return True
|
||||
|
||||
|
||||
@router.post("/clearrediskeys/", summary="清除匹配的缓存键", description="根据模式清除匹配的Redis缓存键")
|
||||
@router.delete("/redis-keys", summary="清除匹配的缓存键", description="根据模式清除匹配的Redis缓存键")
|
||||
async def fastapi_clear_redis_keys(keys: str = Query(..., description="缓存键模式(支持通配符)")):
|
||||
"""
|
||||
清除匹配的缓存键
|
||||
@@ -29,7 +29,7 @@ async def fastapi_clear_redis_keys(keys: str = Query(..., description="缓存键
|
||||
return True
|
||||
|
||||
|
||||
@router.post("/clearallredis/", summary="清除所有缓存", description="清空整个Redis数据库的所有缓存")
|
||||
@router.delete("/all-redis", summary="清除所有缓存", description="清空整个Redis数据库的所有缓存")
|
||||
async def fastapi_clear_all_redis():
|
||||
"""
|
||||
清除所有缓存
|
||||
@@ -40,7 +40,7 @@ async def fastapi_clear_all_redis():
|
||||
return True
|
||||
|
||||
|
||||
@router.get("/queryredis/", summary="查询缓存键列表", description="获取Redis中所有的缓存键")
|
||||
@router.get("/redis", summary="查询缓存键列表", description="获取Redis中所有的缓存键")
|
||||
async def fastapi_query_redis():
|
||||
"""
|
||||
查询缓存键列表
|
||||
|
||||
@@ -13,7 +13,7 @@ from app.services.tjnetwork import (
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/getcontrolschema/", summary="获取控制架构", description="获取网络中控制对象的架构定义")
|
||||
@router.get("/network-schemas/control", summary="获取控制架构", description="获取网络中控制对象的架构定义")
|
||||
async def fastapi_get_control_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||
"""获取控制架构。
|
||||
|
||||
@@ -21,7 +21,7 @@ async def fastapi_get_control_schema(network: str = Query(..., description="管
|
||||
"""
|
||||
return get_control_schema(network)
|
||||
|
||||
@router.get("/getcontrolproperties/", summary="获取控制属性", description="获取指定网络中的控制属性信息")
|
||||
@router.get("/controls/properties", summary="获取控制属性", description="获取指定网络中的控制属性信息")
|
||||
async def fastapi_get_control_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||
"""获取控制属性。
|
||||
|
||||
@@ -29,7 +29,7 @@ async def fastapi_get_control_properties(network: str = Query(..., description="
|
||||
"""
|
||||
return get_control(network)
|
||||
|
||||
@router.post("/setcontrolproperties/", response_model=None, summary="设置控制属性", description="更新指定网络中的控制属性")
|
||||
@router.patch("/controls/properties", response_model=None, summary="设置控制属性", description="更新指定网络中的控制属性")
|
||||
async def fastapi_set_control_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -41,7 +41,7 @@ async def fastapi_set_control_properties(
|
||||
props = await req.json()
|
||||
return set_control(network, ChangeSet(props))
|
||||
|
||||
@router.get("/getruleschema/", summary="获取规则架构", description="获取网络中规则对象的架构定义")
|
||||
@router.get("/rule-schemas", summary="获取规则架构", description="获取网络中规则对象的架构定义")
|
||||
async def fastapi_get_rule_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||
"""获取规则架构。
|
||||
|
||||
@@ -49,7 +49,7 @@ async def fastapi_get_rule_schema(network: str = Query(..., description="管网
|
||||
"""
|
||||
return get_rule_schema(network)
|
||||
|
||||
@router.get("/getruleproperties/", summary="获取规则属性", description="获取指定网络中的规则属性信息")
|
||||
@router.get("/rule-properties", summary="获取规则属性", description="获取指定网络中的规则属性信息")
|
||||
async def fastapi_get_rule_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||
"""获取规则属性。
|
||||
|
||||
@@ -57,7 +57,7 @@ async def fastapi_get_rule_properties(network: str = Query(..., description="管
|
||||
"""
|
||||
return get_rule(network)
|
||||
|
||||
@router.post("/setruleproperties/", response_model=None, summary="设置规则属性", description="更新指定网络中的规则属性")
|
||||
@router.patch("/rule-properties", response_model=None, summary="设置规则属性", description="更新指定网络中的规则属性")
|
||||
async def fastapi_set_rule_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
|
||||
@@ -14,7 +14,7 @@ from app.services.tjnetwork import (
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/getcurveschema", summary="获取曲线架构", description="获取网络中曲线对象的架构定义")
|
||||
@router.get("/network-schemas/curve", summary="获取曲线架构", description="获取网络中曲线对象的架构定义")
|
||||
async def fastapi_get_curve_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||
"""获取曲线架构。
|
||||
|
||||
@@ -22,7 +22,7 @@ async def fastapi_get_curve_schema(network: str = Query(..., description="管网
|
||||
"""
|
||||
return get_curve_schema(network)
|
||||
|
||||
@router.post("/addcurve/", response_model=None, summary="添加曲线", description="在网络中添加一条新的曲线")
|
||||
@router.post("/curves", response_model=None, summary="添加曲线", description="在网络中添加一条新的曲线")
|
||||
async def fastapi_add_curve(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
curve: str = Query(..., description="曲线ID"),
|
||||
@@ -38,7 +38,7 @@ async def fastapi_add_curve(
|
||||
} | props
|
||||
return add_curve(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/deletecurve/", response_model=None, summary="删除曲线", description="从网络中删除指定的曲线")
|
||||
@router.delete("/curves", response_model=None, summary="删除曲线", description="从网络中删除指定的曲线")
|
||||
async def fastapi_delete_curve(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
curve: str = Query(..., description="曲线ID")
|
||||
@@ -50,7 +50,7 @@ async def fastapi_delete_curve(
|
||||
ps = {"id": curve}
|
||||
return delete_curve(network, ChangeSet(ps))
|
||||
|
||||
@router.get("/getcurveproperties/", summary="获取曲线属性", description="获取指定曲线的属性信息")
|
||||
@router.get("/curves/properties", summary="获取曲线属性", description="获取指定曲线的属性信息")
|
||||
async def fastapi_get_curve_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
curve: str = Query(..., description="曲线ID")
|
||||
@@ -61,7 +61,7 @@ async def fastapi_get_curve_properties(
|
||||
"""
|
||||
return get_curve(network, curve)
|
||||
|
||||
@router.post("/setcurveproperties/", response_model=None, summary="设置曲线属性", description="更新指定曲线的属性")
|
||||
@router.patch("/curves/properties", response_model=None, summary="设置曲线属性", description="更新指定曲线的属性")
|
||||
async def fastapi_set_curve_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
curve: str = Query(..., description="曲线ID"),
|
||||
@@ -75,7 +75,7 @@ async def fastapi_set_curve_properties(
|
||||
ps = {"id": curve} | props
|
||||
return set_curve(network, ChangeSet(ps))
|
||||
|
||||
@router.get("/getcurves/", summary="获取所有曲线", description="获取网络中的所有曲线列表")
|
||||
@router.get("/curves", summary="获取所有曲线", description="获取网络中的所有曲线列表")
|
||||
async def fastapi_get_curves(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
|
||||
"""获取所有曲线。
|
||||
|
||||
@@ -83,7 +83,7 @@ async def fastapi_get_curves(network: str = Query(..., description="管网名称
|
||||
"""
|
||||
return get_curves(network)
|
||||
|
||||
@router.get("/iscurve/", summary="检查曲线存在性", description="检查指定的曲线是否存在")
|
||||
@router.get("/curves/existence", summary="检查曲线存在性", description="检查指定的曲线是否存在")
|
||||
async def fastapi_is_curve(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
curve: str = Query(..., description="曲线ID")
|
||||
|
||||
@@ -19,7 +19,7 @@ from app.services.tjnetwork import (
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/gettimeschema", summary="获取时间选项架构", description="获取网络中时间选项的架构定义")
|
||||
@router.get("/network-schemas/time", summary="获取时间选项架构", description="获取网络中时间选项的架构定义")
|
||||
async def fastapi_get_time_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||
"""获取时间选项架构。
|
||||
|
||||
@@ -27,7 +27,7 @@ async def fastapi_get_time_schema(network: str = Query(..., description="管网
|
||||
"""
|
||||
return get_time_schema(network)
|
||||
|
||||
@router.get("/gettimeproperties/", summary="获取时间选项属性", description="获取指定网络中的时间选项属性信息")
|
||||
@router.get("/network-options/time", summary="获取时间选项属性", description="获取指定网络中的时间选项属性信息")
|
||||
async def fastapi_get_time_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||
"""获取时间选项属性。
|
||||
|
||||
@@ -35,7 +35,7 @@ async def fastapi_get_time_properties(network: str = Query(..., description="管
|
||||
"""
|
||||
return get_time(network)
|
||||
|
||||
@router.post("/settimeproperties/", response_model=None, summary="设置时间选项属性", description="更新指定网络中的时间选项属性")
|
||||
@router.patch("/time-properties", response_model=None, summary="设置时间选项属性", description="更新指定网络中的时间选项属性")
|
||||
async def fastapi_set_time_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -47,7 +47,7 @@ async def fastapi_set_time_properties(
|
||||
props = await req.json()
|
||||
return set_time(network, ChangeSet(props))
|
||||
|
||||
@router.get("/getenergyschema/", summary="获取能耗选项架构", description="获取网络中能耗选项的架构定义")
|
||||
@router.get("/network-schemas/energy", summary="获取能耗选项架构", description="获取网络中能耗选项的架构定义")
|
||||
async def fastapi_get_energy_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||
"""获取能耗选项架构。
|
||||
|
||||
@@ -55,7 +55,7 @@ async def fastapi_get_energy_schema(network: str = Query(..., description="管
|
||||
"""
|
||||
return get_energy_schema(network)
|
||||
|
||||
@router.get("/getenergyproperties/", summary="获取能耗选项属性", description="获取指定网络中的能耗选项属性信息")
|
||||
@router.get("/network-options/energy", summary="获取能耗选项属性", description="获取指定网络中的能耗选项属性信息")
|
||||
async def fastapi_get_energy_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||
"""获取能耗选项属性。
|
||||
|
||||
@@ -63,7 +63,7 @@ async def fastapi_get_energy_properties(network: str = Query(..., description="
|
||||
"""
|
||||
return get_energy(network)
|
||||
|
||||
@router.post("/setenergyproperties/", response_model=None, summary="设置能耗选项属性", description="更新指定网络中的能耗选项属性")
|
||||
@router.patch("/energy-properties", response_model=None, summary="设置能耗选项属性", description="更新指定网络中的能耗选项属性")
|
||||
async def fastapi_set_energy_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -75,7 +75,7 @@ async def fastapi_set_energy_properties(
|
||||
props = await req.json()
|
||||
return set_energy(network, ChangeSet(props))
|
||||
|
||||
@router.get("/getpumpenergyschema/", 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]]:
|
||||
"""获取泵能耗选项架构。
|
||||
|
||||
@@ -83,7 +83,7 @@ async def fastapi_get_pump_energy_schema(network: str = Query(..., description="
|
||||
"""
|
||||
return get_pump_energy_schema(network)
|
||||
|
||||
@router.get("/getpumpenergyproperties//", summary="获取泵能耗属性", description="获取指定泵的能耗属性信息")
|
||||
@router.get("/network-options/pump-energy", summary="获取泵能耗属性", description="获取指定泵的能耗属性信息")
|
||||
async def fastapi_get_pump_energy_proeprties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pump: str = Query(..., description="泵ID")
|
||||
@@ -94,7 +94,7 @@ async def fastapi_get_pump_energy_proeprties(
|
||||
"""
|
||||
return get_pump_energy(network, pump)
|
||||
|
||||
@router.get("/setpumpenergyproperties//", response_model=None, summary="设置泵能耗属性", description="更新指定泵的能耗属性")
|
||||
@router.patch("/network-options/pump-energy", response_model=None, summary="设置泵能耗属性", description="更新指定泵的能耗属性")
|
||||
async def fastapi_set_pump_energy_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pump: str = Query(..., description="泵ID"),
|
||||
@@ -108,7 +108,7 @@ async def fastapi_set_pump_energy_properties(
|
||||
ps = {"id": pump} | props
|
||||
return set_pump_energy(network, ChangeSet(ps))
|
||||
|
||||
@router.get("/getoptionschema/", summary="获取选项架构", description="获取网络中选项对象的架构定义")
|
||||
@router.get("/network-schemas/option", summary="获取选项架构", description="获取网络中选项对象的架构定义")
|
||||
async def fastapi_get_option_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||
"""获取选项架构。
|
||||
|
||||
@@ -116,7 +116,7 @@ async def fastapi_get_option_schema(network: str = Query(..., description="管
|
||||
"""
|
||||
return get_option_v3_schema(network)
|
||||
|
||||
@router.get("/getoptionproperties/", summary="获取选项属性", description="获取指定网络中的选项属性信息")
|
||||
@router.get("/network-options", summary="获取选项属性", description="获取指定网络中的选项属性信息")
|
||||
async def fastapi_get_option_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||
"""获取选项属性。
|
||||
|
||||
@@ -124,7 +124,7 @@ async def fastapi_get_option_properties(network: str = Query(..., description="
|
||||
"""
|
||||
return get_option_v3(network)
|
||||
|
||||
@router.post("/setoptionproperties/", response_model=None, summary="设置选项属性", description="更新指定网络中的选项属性")
|
||||
@router.patch("/network-options", response_model=None, summary="设置选项属性", description="更新指定网络中的选项属性")
|
||||
async def fastapi_set_option_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
|
||||
@@ -14,7 +14,7 @@ from app.services.tjnetwork import (
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/getpatternschema", summary="获取模式架构", description="获取网络中模式对象的架构定义")
|
||||
@router.get("/network-schemas/pattern", summary="获取模式架构", description="获取网络中模式对象的架构定义")
|
||||
async def fastapi_get_pattern_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||
"""获取模式架构。
|
||||
|
||||
@@ -22,7 +22,7 @@ async def fastapi_get_pattern_schema(network: str = Query(..., description="管
|
||||
"""
|
||||
return get_pattern_schema(network)
|
||||
|
||||
@router.post("/addpattern/", response_model=None, summary="添加模式", description="在网络中添加一个新的模式")
|
||||
@router.post("/patterns", response_model=None, summary="添加模式", description="在网络中添加一个新的模式")
|
||||
async def fastapi_add_pattern(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pattern: str = Query(..., description="模式ID"),
|
||||
@@ -38,7 +38,7 @@ async def fastapi_add_pattern(
|
||||
} | props
|
||||
return add_pattern(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/deletepattern/", response_model=None, summary="删除模式", description="从网络中删除指定的模式")
|
||||
@router.delete("/patterns", response_model=None, summary="删除模式", description="从网络中删除指定的模式")
|
||||
async def fastapi_delete_pattern(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pattern: str = Query(..., description="模式ID")
|
||||
@@ -50,7 +50,7 @@ async def fastapi_delete_pattern(
|
||||
ps = {"id": pattern}
|
||||
return delete_pattern(network, ChangeSet(ps))
|
||||
|
||||
@router.get("/getpatternproperties/", summary="获取模式属性", description="获取指定模式的属性信息")
|
||||
@router.get("/patterns/properties", summary="获取模式属性", description="获取指定模式的属性信息")
|
||||
async def fastapi_get_pattern_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pattern: str = Query(..., description="模式ID")
|
||||
@@ -61,7 +61,7 @@ async def fastapi_get_pattern_properties(
|
||||
"""
|
||||
return get_pattern(network, pattern)
|
||||
|
||||
@router.post("/setpatternproperties/", response_model=None, summary="设置模式属性", description="更新指定模式的属性")
|
||||
@router.patch("/patterns/properties", response_model=None, summary="设置模式属性", description="更新指定模式的属性")
|
||||
async def fastapi_set_pattern_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pattern: str = Query(..., description="模式ID"),
|
||||
@@ -75,7 +75,7 @@ async def fastapi_set_pattern_properties(
|
||||
ps = {"id": pattern} | props
|
||||
return set_pattern(network, ChangeSet(ps))
|
||||
|
||||
@router.get("/ispattern/", summary="检查模式存在性", description="检查指定的模式是否存在")
|
||||
@router.get("/patterns/existence", summary="检查模式存在性", description="检查指定的模式是否存在")
|
||||
async def fastapi_is_pattern(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pattern: str = Query(..., description="模式ID")
|
||||
@@ -86,7 +86,7 @@ async def fastapi_is_pattern(
|
||||
"""
|
||||
return is_pattern(network, pattern)
|
||||
|
||||
@router.get("/getpatterns/", summary="获取所有模式", description="获取网络中的所有模式列表")
|
||||
@router.get("/patterns", summary="获取所有模式", description="获取网络中的所有模式列表")
|
||||
async def fastapi_get_patterns(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[str]:
|
||||
"""获取所有模式。
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ from app.services.tjnetwork import (
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/getqualityschema/", summary="获取水质架构", description="获取网络中水质对象的架构定义")
|
||||
@router.get("/network-schemas/quality", summary="获取水质架构", description="获取网络中水质对象的架构定义")
|
||||
async def fastapi_get_quality_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||
"""获取水质架构。
|
||||
|
||||
@@ -40,7 +40,7 @@ async def fastapi_get_quality_schema(network: str = Query(..., description="管
|
||||
"""
|
||||
return get_quality_schema(network)
|
||||
|
||||
@router.get("/getqualityproperties/", summary="获取水质属性", description="获取指定节点的水质属性信息")
|
||||
@router.get("/quality-configurations/properties", summary="获取水质属性", description="获取指定节点的水质属性信息")
|
||||
async def fastapi_get_quality_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
node: str = Query(..., description="节点ID")
|
||||
@@ -51,7 +51,7 @@ async def fastapi_get_quality_properties(
|
||||
"""
|
||||
return get_quality(network, node)
|
||||
|
||||
@router.post("/setqualityproperties/", response_model=None, summary="设置水质属性", description="更新指定节点的水质属性")
|
||||
@router.patch("/quality-configurations/properties", response_model=None, summary="设置水质属性", description="更新指定节点的水质属性")
|
||||
async def fastapi_set_quality_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -63,7 +63,7 @@ async def fastapi_set_quality_properties(
|
||||
props = await req.json()
|
||||
return set_quality(network, ChangeSet(props))
|
||||
|
||||
@router.get("/getemitterschema", summary="获取发射器架构", description="获取网络中发射器对象的架构定义")
|
||||
@router.get("/network-schemas/emitter", summary="获取发射器架构", description="获取网络中发射器对象的架构定义")
|
||||
async def fastapi_get_emitter_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||
"""获取发射器架构。
|
||||
|
||||
@@ -71,7 +71,7 @@ async def fastapi_get_emitter_schema(network: str = Query(..., description="管
|
||||
"""
|
||||
return get_emitter_schema(network)
|
||||
|
||||
@router.get("/getemitterproperties/", summary="获取发射器属性", description="获取指定连接点的发射器属性信息")
|
||||
@router.get("/emitters/properties", summary="获取发射器属性", description="获取指定连接点的发射器属性信息")
|
||||
async def fastapi_get_emitter_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
junction: str = Query(..., description="连接点ID")
|
||||
@@ -82,7 +82,7 @@ async def fastapi_get_emitter_properties(
|
||||
"""
|
||||
return get_emitter(network, junction)
|
||||
|
||||
@router.post("/setemitterproperties/", response_model=None, summary="设置发射器属性", description="更新指定连接点的发射器属性")
|
||||
@router.patch("/emitters/properties", response_model=None, summary="设置发射器属性", description="更新指定连接点的发射器属性")
|
||||
async def fastapi_set_emitter_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
junction: str = Query(..., description="连接点ID"),
|
||||
@@ -96,7 +96,7 @@ async def fastapi_set_emitter_properties(
|
||||
ps = {"junction": junction} | props
|
||||
return set_emitter(network, ChangeSet(ps))
|
||||
|
||||
@router.get("/getsourcechema/", summary="获取水源架构", description="获取网络中水源对象的架构定义")
|
||||
@router.get("/network-schemas/source", summary="获取水源架构", description="获取网络中水源对象的架构定义")
|
||||
async def fastapi_get_source_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||
"""获取水源架构。
|
||||
|
||||
@@ -104,7 +104,7 @@ async def fastapi_get_source_schema(network: str = Query(..., description="管
|
||||
"""
|
||||
return get_source_schema(network)
|
||||
|
||||
@router.get("/getsource/", summary="获取水源属性", description="获取指定节点的水源属性信息")
|
||||
@router.get("/sources/detail", summary="获取水源属性", description="获取指定节点的水源属性信息")
|
||||
async def fastapi_get_source(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
node: str = Query(..., description="节点ID")
|
||||
@@ -115,7 +115,7 @@ async def fastapi_get_source(
|
||||
"""
|
||||
return get_source(network, node)
|
||||
|
||||
@router.post("/setsource/", response_model=None, summary="设置水源属性", description="更新指定节点的水源属性")
|
||||
@router.patch("/sources", response_model=None, summary="设置水源属性", description="更新指定节点的水源属性")
|
||||
async def fastapi_set_source(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -127,7 +127,7 @@ async def fastapi_set_source(
|
||||
props = await req.json()
|
||||
return set_source(network, ChangeSet(props))
|
||||
|
||||
@router.post("/addsource/", response_model=None, summary="添加水源", description="在网络中添加一个新的水源")
|
||||
@router.post("/sources", response_model=None, summary="添加水源", description="在网络中添加一个新的水源")
|
||||
async def fastapi_add_source(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -139,7 +139,7 @@ async def fastapi_add_source(
|
||||
props = await req.json()
|
||||
return add_source(network, ChangeSet(props))
|
||||
|
||||
@router.post("/deletesource/", response_model=None, summary="删除水源", description="从网络中删除指定节点的水源")
|
||||
@router.delete("/sources", response_model=None, summary="删除水源", description="从网络中删除指定节点的水源")
|
||||
async def fastapi_delete_source(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
node: str = Query(..., description="节点ID")
|
||||
@@ -151,7 +151,7 @@ async def fastapi_delete_source(
|
||||
props = {"node": node}
|
||||
return delete_source(network, ChangeSet(props))
|
||||
|
||||
@router.get("/getreactionschema/", summary="获取反应架构", description="获取网络中反应对象的架构定义")
|
||||
@router.get("/network-schemas/reaction", summary="获取反应架构", description="获取网络中反应对象的架构定义")
|
||||
async def fastapi_get_reaction_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||
"""获取反应架构。
|
||||
|
||||
@@ -159,7 +159,7 @@ async def fastapi_get_reaction_schema(network: str = Query(..., description="管
|
||||
"""
|
||||
return get_reaction_schema(network)
|
||||
|
||||
@router.get("/getreaction/", summary="获取反应属性", description="获取指定网络中的反应属性信息")
|
||||
@router.get("/reactions/detail", summary="获取反应属性", description="获取指定网络中的反应属性信息")
|
||||
async def fastapi_get_reaction(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||
"""获取反应属性。
|
||||
|
||||
@@ -167,7 +167,7 @@ async def fastapi_get_reaction(network: str = Query(..., description="管网名
|
||||
"""
|
||||
return get_reaction(network)
|
||||
|
||||
@router.post("/setreaction/", response_model=None, summary="设置反应属性", description="更新指定网络中的反应属性")
|
||||
@router.patch("/reactions", response_model=None, summary="设置反应属性", description="更新指定网络中的反应属性")
|
||||
async def fastapi_set_reaction(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -179,7 +179,7 @@ async def fastapi_set_reaction(
|
||||
props = await req.json()
|
||||
return set_reaction(network, ChangeSet(props))
|
||||
|
||||
@router.get("/getpipereactionschema/", 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]]:
|
||||
"""获取管道反应架构。
|
||||
|
||||
@@ -187,7 +187,7 @@ async def fastapi_get_pipe_reaction_schema(network: str = Query(..., description
|
||||
"""
|
||||
return get_pipe_reaction_schema(network)
|
||||
|
||||
@router.get("/getpipereaction/", summary="获取管道反应属性", description="获取指定管道的反应属性信息")
|
||||
@router.get("/pipe-reactions/detail", summary="获取管道反应属性", description="获取指定管道的反应属性信息")
|
||||
async def fastapi_get_pipe_reaction(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pipe: str = Query(..., description="管道ID")
|
||||
@@ -198,7 +198,7 @@ async def fastapi_get_pipe_reaction(
|
||||
"""
|
||||
return get_pipe_reaction(network, pipe)
|
||||
|
||||
@router.post("/setpipereaction/", response_model=None, summary="设置管道反应属性", description="更新指定管道的反应属性")
|
||||
@router.patch("/pipe-reactions", response_model=None, summary="设置管道反应属性", description="更新指定管道的反应属性")
|
||||
async def fastapi_set_pipe_reaction(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -210,7 +210,7 @@ async def fastapi_set_pipe_reaction(
|
||||
props = await req.json()
|
||||
return set_pipe_reaction(network, ChangeSet(props))
|
||||
|
||||
@router.get("/gettankreactionschema/", 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]]:
|
||||
"""获取水池反应架构。
|
||||
|
||||
@@ -218,7 +218,7 @@ async def fastapi_get_tank_reaction_schema(network: str = Query(..., description
|
||||
"""
|
||||
return get_tank_reaction_schema(network)
|
||||
|
||||
@router.get("/gettankreaction/", summary="获取水池反应属性", description="获取指定水池的反应属性信息")
|
||||
@router.get("/tank-reactions/detail", summary="获取水池反应属性", description="获取指定水池的反应属性信息")
|
||||
async def fastapi_get_tank_reaction(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水池ID")
|
||||
@@ -229,7 +229,7 @@ async def fastapi_get_tank_reaction(
|
||||
"""
|
||||
return get_tank_reaction(network, tank)
|
||||
|
||||
@router.post("/settankreaction/", response_model=None, summary="设置水池反应属性", description="更新指定水池的反应属性")
|
||||
@router.patch("/tank-reactions", response_model=None, summary="设置水池反应属性", description="更新指定水池的反应属性")
|
||||
async def fastapi_set_tank_reaction(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -241,7 +241,7 @@ async def fastapi_set_tank_reaction(
|
||||
props = await req.json()
|
||||
return set_tank_reaction(network, ChangeSet(props))
|
||||
|
||||
@router.get("/getmixingschema/", summary="获取混合架构", description="获取网络中混合对象的架构定义")
|
||||
@router.get("/network-schemas/mixing", summary="获取混合架构", description="获取网络中混合对象的架构定义")
|
||||
async def fastapi_get_mixing_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||
"""获取混合架构。
|
||||
|
||||
@@ -249,7 +249,7 @@ async def fastapi_get_mixing_schema(network: str = Query(..., description="管
|
||||
"""
|
||||
return get_mixing_schema(network)
|
||||
|
||||
@router.get("/getmixing/", summary="获取混合属性", description="获取指定水池的混合属性信息")
|
||||
@router.get("/mixing-configurations/detail", summary="获取混合属性", description="获取指定水池的混合属性信息")
|
||||
async def fastapi_get_mixing(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水池ID")
|
||||
@@ -260,7 +260,7 @@ async def fastapi_get_mixing(
|
||||
"""
|
||||
return get_mixing(network, tank)
|
||||
|
||||
@router.post("/setmixing/", response_model=None, summary="设置混合属性", description="更新指定水池的混合属性")
|
||||
@router.patch("/mixing-configurations", response_model=None, summary="设置混合属性", description="更新指定水池的混合属性")
|
||||
async def fastapi_set_mixing(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -272,7 +272,7 @@ async def fastapi_set_mixing(
|
||||
props = await req.json()
|
||||
return api.set_mixing(network, ChangeSet(props))
|
||||
|
||||
@router.post("/addmixing/", response_model=None, summary="添加混合", description="在网络中添加一个新的混合")
|
||||
@router.post("/mixing-configurations", response_model=None, summary="添加混合", description="在网络中添加一个新的混合")
|
||||
async def fastapi_add_mixing(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -284,7 +284,7 @@ async def fastapi_add_mixing(
|
||||
props = await req.json()
|
||||
return add_mixing(network, ChangeSet(props))
|
||||
|
||||
@router.post("/deletemixing/", response_model=None, summary="删除混合", description="从网络中删除指定的混合")
|
||||
@router.delete("/mixing-configurations", response_model=None, summary="删除混合", description="从网络中删除指定的混合")
|
||||
async def fastapi_delete_mixing(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
|
||||
@@ -24,7 +24,7 @@ import json
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/getvertexschema/", summary="获取图形元素架构", description="获取网络中图形元素对象的架构定义")
|
||||
@router.get("/network-schemas/vertex", summary="获取图形元素架构", description="获取网络中图形元素对象的架构定义")
|
||||
async def fastapi_get_vertex_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||
"""获取图形元素架构。
|
||||
|
||||
@@ -32,7 +32,7 @@ async def fastapi_get_vertex_schema(network: str = Query(..., description="管
|
||||
"""
|
||||
return get_vertex_schema(network)
|
||||
|
||||
@router.get("/getvertexproperties/", summary="获取图形元素属性", description="获取指定图形元素的属性信息")
|
||||
@router.get("/visual-elements/properties", summary="获取图形元素属性", description="获取指定图形元素的属性信息")
|
||||
async def fastapi_get_vertex_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
link: str = Query(..., description="图形元素链接")
|
||||
@@ -43,7 +43,7 @@ async def fastapi_get_vertex_properties(
|
||||
"""
|
||||
return get_vertex(network, link)
|
||||
|
||||
@router.post("/setvertexproperties/", response_model=None, summary="设置图形元素属性", description="更新指定图形元素的属性")
|
||||
@router.patch("/visual-elements/properties", response_model=None, summary="设置图形元素属性", description="更新指定图形元素的属性")
|
||||
async def fastapi_set_vertex_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -55,7 +55,7 @@ async def fastapi_set_vertex_properties(
|
||||
props = await req.json()
|
||||
return set_vertex(network, ChangeSet(props))
|
||||
|
||||
@router.post("/addvertex/", response_model=None, summary="添加图形元素", description="在网络中添加一个新的图形元素")
|
||||
@router.post("/visual-elements", response_model=None, summary="添加图形元素", description="在网络中添加一个新的图形元素")
|
||||
async def fastapi_add_vertex(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -67,7 +67,7 @@ async def fastapi_add_vertex(
|
||||
props = await req.json()
|
||||
return add_vertex(network, ChangeSet(props))
|
||||
|
||||
@router.post("/deletevertex/", response_model=None, summary="删除图形元素", description="从网络中删除指定的图形元素")
|
||||
@router.delete("/visual-elements", response_model=None, summary="删除图形元素", description="从网络中删除指定的图形元素")
|
||||
async def fastapi_delete_vertex(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -79,7 +79,7 @@ async def fastapi_delete_vertex(
|
||||
props = await req.json()
|
||||
return delete_vertex(network, ChangeSet(props))
|
||||
|
||||
@router.get("/getallvertexlinks/", 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]:
|
||||
"""获取所有图形元素链接。
|
||||
|
||||
@@ -87,7 +87,7 @@ async def fastapi_get_all_vertex_links(network: str = Query(..., description="
|
||||
"""
|
||||
return json.dumps(get_all_vertex_links(network))
|
||||
|
||||
@router.get("/getallvertices/", 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]]:
|
||||
"""获取所有图形元素。
|
||||
|
||||
@@ -95,7 +95,7 @@ async def fastapi_get_all_vertices(network: str = Query(..., description="管网
|
||||
"""
|
||||
return json.dumps(get_all_vertices(network))
|
||||
|
||||
@router.get("/getlabelschema/", summary="获取标签架构", description="获取网络中标签对象的架构定义")
|
||||
@router.get("/network-schemas/label", summary="获取标签架构", description="获取网络中标签对象的架构定义")
|
||||
async def fastapi_get_label_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||
"""获取标签架构。
|
||||
|
||||
@@ -103,7 +103,7 @@ async def fastapi_get_label_schema(network: str = Query(..., description="管网
|
||||
"""
|
||||
return get_label_schema(network)
|
||||
|
||||
@router.get("/getlabelproperties/", summary="获取标签属性", description="获取指定坐标处的标签属性信息")
|
||||
@router.get("/labels/properties", summary="获取标签属性", description="获取指定坐标处的标签属性信息")
|
||||
async def fastapi_get_label_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
x: float = Query(..., description="X坐标"),
|
||||
@@ -115,7 +115,7 @@ async def fastapi_get_label_properties(
|
||||
"""
|
||||
return get_label(network, x, y)
|
||||
|
||||
@router.post("/setlabelproperties/", response_model=None, summary="设置标签属性", description="更新指定标签的属性")
|
||||
@router.patch("/labels/properties", response_model=None, summary="设置标签属性", description="更新指定标签的属性")
|
||||
async def fastapi_set_label_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -127,7 +127,7 @@ async def fastapi_set_label_properties(
|
||||
props = await req.json()
|
||||
return set_label(network, ChangeSet(props))
|
||||
|
||||
@router.post("/addlabel/", response_model=None, summary="添加标签", description="在网络中添加一个新的标签")
|
||||
@router.post("/labels", response_model=None, summary="添加标签", description="在网络中添加一个新的标签")
|
||||
async def fastapi_add_label(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -139,7 +139,7 @@ async def fastapi_add_label(
|
||||
props = await req.json()
|
||||
return add_label(network, ChangeSet(props))
|
||||
|
||||
@router.post("/deletelabel/", response_model=None, summary="删除标签", description="从网络中删除指定的标签")
|
||||
@router.delete("/labels", response_model=None, summary="删除标签", description="从网络中删除指定的标签")
|
||||
async def fastapi_delete_label(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -151,7 +151,7 @@ async def fastapi_delete_label(
|
||||
props = await req.json()
|
||||
return delete_label(network, ChangeSet(props))
|
||||
|
||||
@router.get("/getbackdropschema/", summary="获取背景架构", description="获取网络中背景对象的架构定义")
|
||||
@router.get("/network-schemas/backdrop", summary="获取背景架构", description="获取网络中背景对象的架构定义")
|
||||
async def fastapi_get_backdrop_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||
"""获取背景架构。
|
||||
|
||||
@@ -159,7 +159,7 @@ async def fastapi_get_backdrop_schema(network: str = Query(..., description="管
|
||||
"""
|
||||
return get_backdrop_schema(network)
|
||||
|
||||
@router.get("/getbackdropproperties/", summary="获取背景属性", description="获取指定网络的背景属性信息")
|
||||
@router.get("/backdrops/properties", summary="获取背景属性", description="获取指定网络的背景属性信息")
|
||||
async def fastapi_get_backdrop_properties(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, Any]:
|
||||
"""获取背景属性。
|
||||
|
||||
@@ -167,7 +167,7 @@ async def fastapi_get_backdrop_properties(network: str = Query(..., description=
|
||||
"""
|
||||
return get_backdrop(network)
|
||||
|
||||
@router.post("/setbackdropproperties/", response_model=None, summary="设置背景属性", description="更新指定网络的背景属性")
|
||||
@router.patch("/backdrops/properties", response_model=None, summary="设置背景属性", description="更新指定网络的背景属性")
|
||||
async def fastapi_set_backdrop_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
|
||||
@@ -11,7 +11,7 @@ from app.services.tjnetwork import (
|
||||
router = APIRouter()
|
||||
|
||||
@router.get(
|
||||
"/getallextensiondatakeys/",
|
||||
"/all-extension-data-keys",
|
||||
summary="获取所有扩展数据键",
|
||||
description="获取指定网络的所有扩展数据的键列表"
|
||||
)
|
||||
@@ -32,7 +32,7 @@ async def get_all_extension_data_keys_endpoint(
|
||||
return get_all_extension_data_keys(network)
|
||||
|
||||
@router.get(
|
||||
"/getallextensiondata/",
|
||||
"/all-extension-datas",
|
||||
summary="获取所有扩展数据",
|
||||
description="获取指定网络的所有扩展数据"
|
||||
)
|
||||
@@ -53,7 +53,7 @@ async def get_all_extension_data_endpoint(
|
||||
return get_all_extension_data(network)
|
||||
|
||||
@router.get(
|
||||
"/getextensiondata/",
|
||||
"/extension-datas",
|
||||
summary="获取指定扩展数据",
|
||||
description="获取指定网络中指定键的扩展数据值"
|
||||
)
|
||||
@@ -75,8 +75,8 @@ async def get_extension_data_endpoint(
|
||||
"""
|
||||
return get_extension_data(network, key)
|
||||
|
||||
@router.post(
|
||||
"/setextensiondata/",
|
||||
@router.patch(
|
||||
"/extension-datas",
|
||||
response_model=None,
|
||||
summary="设置扩展数据",
|
||||
description="设置指定网络中的扩展数据"
|
||||
|
||||
@@ -13,7 +13,7 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/tianditu/geocode",
|
||||
"/geocoding-requests",
|
||||
summary="Tianditu Geocoding",
|
||||
description="调用天地图地理编码服务,将结构化地址转换为经纬度",
|
||||
)
|
||||
|
||||
@@ -2,13 +2,11 @@ import os
|
||||
from typing import Any
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
|
||||
from fastapi import APIRouter, Depends, HTTPException, Body
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
||||
from app.services.leakage_identifier import (
|
||||
get_leakage_identify_scheme_detail,
|
||||
list_leakage_identify_schemes,
|
||||
run_leakage_identification,
|
||||
)
|
||||
|
||||
@@ -40,7 +38,7 @@ class LeakageIdentifyRequest(BaseModel):
|
||||
|
||||
|
||||
@router.post(
|
||||
"/identify/",
|
||||
"/leakage-identifications",
|
||||
summary="执行漏损识别",
|
||||
description="基于压力观测数据和遗传算法识别管网中的漏损位置和大小"
|
||||
)
|
||||
@@ -68,66 +66,3 @@ async def identify_leakage(
|
||||
return run_leakage_identification(**data.model_dump(), username=username)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/schemes/",
|
||||
summary="查询漏损识别方案列表",
|
||||
description="获取指定网络的所有漏损识别方案"
|
||||
)
|
||||
async def query_leakage_schemes(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
query_date: datetime | None = Query(None, description="查询日期(可选)")
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
获取漏损识别方案列表。
|
||||
|
||||
查询指定网络的所有已配置的漏损识别方案,
|
||||
可按日期进行筛选。
|
||||
|
||||
Args:
|
||||
network: 管网名称(或数据库名称)
|
||||
query_date: 查询日期(可选)
|
||||
|
||||
Returns:
|
||||
漏损识别方案列表
|
||||
|
||||
Raises:
|
||||
HTTPException: 当查询失败时
|
||||
"""
|
||||
try:
|
||||
return list_leakage_identify_schemes(network=network, query_date=query_date)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/schemes/{scheme_name}",
|
||||
summary="获取漏损识别方案详情",
|
||||
description="获取指定漏损识别方案的详细信息"
|
||||
)
|
||||
async def query_leakage_scheme_detail(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
scheme_name: str = Path(..., description="漏损识别方案名称")
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
获取漏损识别方案详情。
|
||||
|
||||
查询指定漏损识别方案的完整配置和参数信息。
|
||||
|
||||
Args:
|
||||
network: 管网名称(或数据库名称)
|
||||
scheme_name: 漏损识别方案名称
|
||||
|
||||
Returns:
|
||||
包含方案详情的字典
|
||||
|
||||
Raises:
|
||||
HTTPException: 当查询失败时
|
||||
"""
|
||||
try:
|
||||
return get_leakage_identify_scheme_detail(
|
||||
network=network, scheme_name=scheme_name
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
@@ -25,7 +25,7 @@ router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("/meta/project", summary="获取项目元数据", description="获取当前项目的元数据和配置信息", response_model=ProjectMetaResponse)
|
||||
@router.get("/projects/current/metadata", summary="获取项目元数据", description="获取当前项目的元数据和配置信息", response_model=ProjectMetaResponse)
|
||||
async def get_project_metadata(
|
||||
ctx: ProjectContext = Depends(get_project_context),
|
||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||
@@ -52,7 +52,7 @@ async def get_project_metadata(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/meta/projects", summary="列出用户项目", description="获取当前用户有权限的所有项目列表", response_model=list[ProjectSummaryResponse])
|
||||
@router.get("/projects", summary="列出用户项目", description="获取当前用户有权限的所有项目列表", response_model=list[ProjectSummaryResponse])
|
||||
async def list_user_projects(
|
||||
current_user=Depends(get_current_metadata_user),
|
||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||
@@ -88,7 +88,7 @@ async def list_user_projects(
|
||||
]
|
||||
|
||||
|
||||
@router.get("/meta/db/health", summary="检查数据库健康状态", description="检查项目数据库连接的健康状况")
|
||||
@router.get("/projects/current/database-health", summary="检查数据库健康状态", description="检查项目数据库连接的健康状况")
|
||||
async def project_db_health(
|
||||
pg_session: AsyncSession = Depends(get_project_pg_session),
|
||||
ts_conn: AsyncConnection = Depends(get_project_timescale_connection),
|
||||
|
||||
@@ -11,7 +11,6 @@ from app.services.tjnetwork import (
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/getjson/", summary="获取JSON示例", description="获取JSON格式响应示例")
|
||||
async def fastapi_get_json():
|
||||
"""
|
||||
获取JSON示例
|
||||
@@ -29,7 +28,6 @@ async def fastapi_get_json():
|
||||
|
||||
|
||||
@router.get("/sensor-placement-schemes", summary="获取所有传感器位置", description="获取网络中所有传感器的放置位置信息")
|
||||
@router.get("/getallsensorplacements/", summary="获取所有传感器位置(旧路径)", description="获取网络中所有传感器的放置位置信息", deprecated=True)
|
||||
async def fastapi_get_all_sensor_placements(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[Any, Any]]:
|
||||
"""
|
||||
获取所有传感器位置
|
||||
@@ -39,7 +37,7 @@ async def fastapi_get_all_sensor_placements(network: str = Query(..., descriptio
|
||||
return get_all_sensor_placements(network)
|
||||
|
||||
|
||||
@router.get("/getallburstlocateresults/", summary="获取所有爆管定位结果", description="获取网络中所有爆管定位的分析结果")
|
||||
@router.get("/burst-locations", summary="获取所有爆管定位结果", description="获取网络中所有爆管定位的分析结果")
|
||||
async def fastapi_get_all_burst_locate_results(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[Any, Any]]:
|
||||
"""
|
||||
获取所有爆管定位结果
|
||||
@@ -54,7 +52,6 @@ class Item(BaseModel):
|
||||
str_info: str
|
||||
|
||||
|
||||
@router.post("/test_dict/", summary="测试字典处理", description="测试处理字典类型数据")
|
||||
async def fastapi_test_dict(data: Item) -> dict[str, str]:
|
||||
"""
|
||||
测试字典处理
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Depends,
|
||||
File,
|
||||
HTTPException,
|
||||
Path as ApiPath,
|
||||
Request,
|
||||
UploadFile,
|
||||
status,
|
||||
)
|
||||
|
||||
from app.auth.metadata_dependencies import (
|
||||
get_current_metadata_admin,
|
||||
get_metadata_repository,
|
||||
)
|
||||
from app.core.audit import AuditAction, log_audit_event
|
||||
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
||||
from app.services.network_import import network_update
|
||||
from app.services.tjnetwork import run_inp
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
MAX_INP_FILE_BYTES = 50 * 1024 * 1024
|
||||
INP_SECTIONS = ("[TITLE]", "[JUNCTIONS]", "[RESERVOIRS]", "[TANKS]", "[PIPES]")
|
||||
|
||||
|
||||
async def _get_active_project(project_id: UUID, metadata_repo: MetadataRepository):
|
||||
project = await metadata_repo.get_project_by_id(project_id)
|
||||
if project is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Project not found",
|
||||
)
|
||||
if project.status != "active":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Project is not active",
|
||||
)
|
||||
return project
|
||||
|
||||
|
||||
def _validate_inp_bytes(content: bytes, filename: str) -> str:
|
||||
if Path(filename).suffix.lower() != ".inp":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Only .inp model files are accepted",
|
||||
)
|
||||
if not content:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="INP file is empty",
|
||||
)
|
||||
if len(content) > MAX_INP_FILE_BYTES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail="INP file exceeds the 50 MiB limit",
|
||||
)
|
||||
for encoding in ("utf-8-sig", "gb18030"):
|
||||
try:
|
||||
text = content.decode(encoding)
|
||||
break
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="INP file encoding is not supported",
|
||||
)
|
||||
upper_text = text.upper()
|
||||
if not any(section in upper_text for section in INP_SECTIONS):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid INP file structure",
|
||||
)
|
||||
return text
|
||||
|
||||
|
||||
async def _read_upload(file: UploadFile) -> tuple[bytes, str]:
|
||||
filename = Path(file.filename or "").name
|
||||
content = await file.read(MAX_INP_FILE_BYTES + 1)
|
||||
_validate_inp_bytes(content, filename)
|
||||
return content, filename
|
||||
|
||||
|
||||
async def _audit_model_change(
|
||||
*,
|
||||
request: Request,
|
||||
current_user,
|
||||
metadata_repo: MetadataRepository,
|
||||
project_id: UUID,
|
||||
action: str,
|
||||
) -> None:
|
||||
await log_audit_event(
|
||||
action=AuditAction.UPDATE,
|
||||
user_id=current_user.id,
|
||||
project_id=project_id,
|
||||
resource_type="hydraulic_model",
|
||||
resource_id=action,
|
||||
request_data={"operation": action},
|
||||
ip_address=request.client.host if request.client else None,
|
||||
request_method=request.method,
|
||||
request_path=request.url.path,
|
||||
response_status=status.HTTP_200_OK,
|
||||
session=metadata_repo.session,
|
||||
)
|
||||
|
||||
|
||||
async def _run_uploaded_inp(content: bytes) -> str:
|
||||
target_dir = Path("inp")
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
model_name = f"admin_model_{uuid4().hex}"
|
||||
target_path = target_dir / f"{model_name}.inp"
|
||||
target_path.write_bytes(content)
|
||||
return run_inp(model_name)
|
||||
|
||||
|
||||
async def _update_from_inp(content: bytes) -> None:
|
||||
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)
|
||||
network_update(str(temp_path))
|
||||
finally:
|
||||
if temp_path is not None:
|
||||
temp_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
async def _apply_model_update(content: bytes) -> None:
|
||||
try:
|
||||
await _update_from_inp(content)
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"数据库操作失败: {exc}",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/admin/projects/{project_id}/model-imports",
|
||||
summary="导入桌面端水力模型",
|
||||
)
|
||||
async def import_project_model(
|
||||
request: Request,
|
||||
project_id: UUID = ApiPath(...),
|
||||
file: UploadFile = File(..., description="桌面端导出的 INP 模型文件"),
|
||||
current_user=Depends(get_current_metadata_admin),
|
||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||
) -> dict:
|
||||
project = await _get_active_project(project_id, metadata_repo)
|
||||
content, filename = await _read_upload(file)
|
||||
result = await _run_uploaded_inp(content)
|
||||
await _audit_model_change(
|
||||
request=request,
|
||||
current_user=current_user,
|
||||
metadata_repo=metadata_repo,
|
||||
project_id=project.id,
|
||||
action="import",
|
||||
)
|
||||
return {"project_id": str(project.id), "filename": filename, "result": result}
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/admin/projects/{project_id}/model-imports",
|
||||
summary="更新桌面端水力模型",
|
||||
)
|
||||
async def update_project_model(
|
||||
request: Request,
|
||||
project_id: UUID = ApiPath(...),
|
||||
file: UploadFile = File(..., description="桌面端导出的 INP 模型文件"),
|
||||
current_user=Depends(get_current_metadata_admin),
|
||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||
) -> dict:
|
||||
project = await _get_active_project(project_id, metadata_repo)
|
||||
content, filename = await _read_upload(file)
|
||||
await _apply_model_update(content)
|
||||
await _audit_model_change(
|
||||
request=request,
|
||||
current_user=current_user,
|
||||
metadata_repo=metadata_repo,
|
||||
project_id=project.id,
|
||||
action="update",
|
||||
)
|
||||
return {"project_id": str(project.id), "filename": filename, "updated": True}
|
||||
@@ -18,7 +18,7 @@ router = APIRouter()
|
||||
############################################################
|
||||
|
||||
@router.get(
|
||||
"/getdemandschema",
|
||||
"/network-schemas/demand",
|
||||
summary="获取需水量属性架构",
|
||||
description="获取指定水网中需水量(Demand)的属性架构定义"
|
||||
)
|
||||
@@ -32,7 +32,7 @@ async def fastapi_get_demand_schema(network: str = Query(..., description="管
|
||||
|
||||
|
||||
@router.get(
|
||||
"/getdemandproperties/",
|
||||
"/demands/properties",
|
||||
summary="获取需水量属性",
|
||||
description="获取指定水网中节点的需水量属性信息"
|
||||
)
|
||||
@@ -49,8 +49,8 @@ async def fastapi_get_demand_properties(
|
||||
|
||||
|
||||
# example: set_demand(p, ChangeSet({'junction': 'j1', 'demands': [{'demand': 10.0, 'pattern': None, 'category': 'x'}, {'demand': 20.0, 'pattern': None, 'category': None}]}))
|
||||
@router.post(
|
||||
"/setdemandproperties/",
|
||||
@router.patch(
|
||||
"/demands/properties",
|
||||
response_model=None,
|
||||
summary="设置需水量属性",
|
||||
description="设置指定水网中节点的需水量属性信息"
|
||||
@@ -72,8 +72,8 @@ async def fastapi_set_demand_properties(
|
||||
############################################################
|
||||
# water distribution 36.[Water Distribution]
|
||||
############################################################
|
||||
@router.get(
|
||||
"/calculatedemandtonodes/",
|
||||
@router.post(
|
||||
"/demands/to-nodes",
|
||||
summary="计算需水量到节点分配",
|
||||
description="将总需水量按指定方式分配到多个节点"
|
||||
)
|
||||
@@ -97,8 +97,8 @@ async def fastapi_calculate_demand_to_nodes(
|
||||
nodes = props["nodes"]
|
||||
return calculate_demand_to_nodes(network, demand, nodes)
|
||||
|
||||
@router.get(
|
||||
"/calculatedemandtoregion/",
|
||||
@router.post(
|
||||
"/demands/to-region",
|
||||
summary="计算需水量到区域分配",
|
||||
description="将总需水量按区域特征分配到该区域内的节点"
|
||||
)
|
||||
@@ -122,8 +122,8 @@ async def fastapi_calculate_demand_to_region(
|
||||
region = props["region"]
|
||||
return calculate_demand_to_region(network, demand, region)
|
||||
|
||||
@router.get(
|
||||
"/calculatedemandtonetwork/",
|
||||
@router.post(
|
||||
"/demands/to-network",
|
||||
summary="计算需水量到整网分配",
|
||||
description="将需水量均匀分配到整个水网的所有需水节点"
|
||||
)
|
||||
|
||||
@@ -45,7 +45,7 @@ router = APIRouter()
|
||||
############################################################
|
||||
|
||||
@router.get(
|
||||
"/isnode/",
|
||||
"/nodes/existence",
|
||||
summary="检查节点有效性",
|
||||
description="检查指定ID是否为水网中的有效节点"
|
||||
)
|
||||
@@ -57,7 +57,7 @@ async def fastapi_is_node(
|
||||
return is_node(network, node)
|
||||
|
||||
@router.get(
|
||||
"/isjunction/",
|
||||
"/junctions/existence",
|
||||
summary="检查是否为接点",
|
||||
description="检查指定ID是否为水网中的接点(需求点)"
|
||||
)
|
||||
@@ -69,7 +69,7 @@ async def fastapi_is_junction(
|
||||
return is_junction(network, node)
|
||||
|
||||
@router.get(
|
||||
"/isreservoir/",
|
||||
"/reservoirs/existence",
|
||||
summary="检查是否为水源",
|
||||
description="检查指定ID是否为水网中的水源(水库/河流)"
|
||||
)
|
||||
@@ -81,7 +81,7 @@ async def fastapi_is_reservoir(
|
||||
return is_reservoir(network, node)
|
||||
|
||||
@router.get(
|
||||
"/istank/",
|
||||
"/tanks/existence",
|
||||
summary="检查是否为蓄水池",
|
||||
description="检查指定ID是否为水网中的蓄水池"
|
||||
)
|
||||
@@ -93,7 +93,7 @@ async def fastapi_is_tank(
|
||||
return is_tank(network, node)
|
||||
|
||||
@router.get(
|
||||
"/islink/",
|
||||
"/links/existence",
|
||||
summary="检查管线有效性",
|
||||
description="检查指定ID是否为水网中的有效管线"
|
||||
)
|
||||
@@ -105,7 +105,7 @@ async def fastapi_is_link(
|
||||
return is_link(network, link)
|
||||
|
||||
@router.get(
|
||||
"/ispipe/",
|
||||
"/pipes/existence",
|
||||
summary="检查是否为管道",
|
||||
description="检查指定ID是否为水网中的管道"
|
||||
)
|
||||
@@ -117,7 +117,7 @@ async def fastapi_is_pipe(
|
||||
return is_pipe(network, link)
|
||||
|
||||
@router.get(
|
||||
"/ispump/",
|
||||
"/pumps/existence",
|
||||
summary="检查是否为泵",
|
||||
description="检查指定ID是否为水网中的泵"
|
||||
)
|
||||
@@ -129,7 +129,7 @@ async def fastapi_is_pump(
|
||||
return is_pump(network, link)
|
||||
|
||||
@router.get(
|
||||
"/isvalve/",
|
||||
"/valves/existence",
|
||||
summary="检查是否为阀门",
|
||||
description="检查指定ID是否为水网中的阀门"
|
||||
)
|
||||
@@ -141,7 +141,7 @@ async def fastapi_is_valve(
|
||||
return is_valve(network, link)
|
||||
|
||||
@router.get(
|
||||
"/getnodetype/",
|
||||
"/node-types",
|
||||
summary="获取节点类型",
|
||||
description="获取指定节点的类型(接点/水源/蓄水池)"
|
||||
)
|
||||
@@ -153,7 +153,7 @@ async def fastapi_get_node_type(
|
||||
return get_node_type(network, node)
|
||||
|
||||
@router.get(
|
||||
"/getlinktype/",
|
||||
"/link-types",
|
||||
summary="获取管线类型",
|
||||
description="获取指定管线的类型(管道/泵/阀门)"
|
||||
)
|
||||
@@ -165,7 +165,7 @@ async def fastapi_get_link_type(
|
||||
return get_link_type(network, link)
|
||||
|
||||
@router.get(
|
||||
"/getelementtype/",
|
||||
"/element-types",
|
||||
summary="获取元素类型",
|
||||
description="获取指定元素的类型(节点或管线)"
|
||||
)
|
||||
@@ -177,7 +177,7 @@ async def fastapi_get_element_type(
|
||||
return get_element_type(network, element)
|
||||
|
||||
@router.get(
|
||||
"/getelementtypevalue/",
|
||||
"/element-type-values",
|
||||
summary="获取元素类型值",
|
||||
description="获取指定元素的类型数值标识"
|
||||
)
|
||||
@@ -189,7 +189,7 @@ async def fastapi_get_element_type_value(
|
||||
return get_element_type_value(network, element)
|
||||
|
||||
@router.get(
|
||||
"/getnodes/",
|
||||
"/nodes",
|
||||
summary="获取所有节点",
|
||||
description="获取指定水网中的所有节点ID列表"
|
||||
)
|
||||
@@ -198,7 +198,7 @@ async def fastapi_get_nodes(network: str = Query(..., description="管网名称
|
||||
return get_nodes(network)
|
||||
|
||||
@router.get(
|
||||
"/getlinks/",
|
||||
"/links",
|
||||
summary="获取所有管线",
|
||||
description="获取指定水网中的所有管线ID列表"
|
||||
)
|
||||
@@ -207,7 +207,7 @@ async def fastapi_get_links(network: str = Query(..., description="管网名称
|
||||
return get_links(network)
|
||||
|
||||
@router.get(
|
||||
"/getnodelinks/",
|
||||
"/node-links",
|
||||
summary="获取节点的关联管线",
|
||||
description="获取指定节点连接的所有管线ID列表"
|
||||
)
|
||||
@@ -223,7 +223,7 @@ def get_node_links_endpoint(
|
||||
############################################################
|
||||
|
||||
@router.get(
|
||||
"/getnodeproperties/",
|
||||
"/node-properties",
|
||||
summary="获取节点属性",
|
||||
description="获取指定节点的所有属性信息"
|
||||
)
|
||||
@@ -235,7 +235,7 @@ async def fast_get_node_properties(
|
||||
return get_node_properties(network, node)
|
||||
|
||||
@router.get(
|
||||
"/getlinkproperties/",
|
||||
"/link-properties",
|
||||
summary="获取管线属性",
|
||||
description="获取指定管线的所有属性信息"
|
||||
)
|
||||
@@ -247,7 +247,7 @@ async def fast_get_link_properties(
|
||||
return get_link_properties(network, link)
|
||||
|
||||
@router.get(
|
||||
"/getscadaproperties/",
|
||||
"/scada-properties",
|
||||
summary="获取SCADA点属性",
|
||||
description="获取指定SCADA点的属性信息"
|
||||
)
|
||||
@@ -259,7 +259,7 @@ async def fast_get_scada_properties(
|
||||
return get_scada_info(network, scada)
|
||||
|
||||
@router.get(
|
||||
"/getallscadaproperties/",
|
||||
"/all-scada-properties",
|
||||
summary="获取所有SCADA点属性",
|
||||
description="获取指定水网中所有SCADA点的属性信息"
|
||||
)
|
||||
@@ -270,7 +270,7 @@ async def fast_get_all_scada_properties(
|
||||
return get_all_scada_info(network)
|
||||
|
||||
@router.get(
|
||||
"/getelementpropertieswithtype/",
|
||||
"/element-properties-with-types",
|
||||
summary="获取指定类型元素属性",
|
||||
description="获取指定类型的元素属性信息"
|
||||
)
|
||||
@@ -283,7 +283,7 @@ async def fast_get_element_properties_with_type(
|
||||
return get_element_properties_with_type(network, elementtype, element)
|
||||
|
||||
@router.get(
|
||||
"/getelementproperties/",
|
||||
"/element-properties",
|
||||
summary="获取元素属性",
|
||||
description="获取指定元素的属性信息"
|
||||
)
|
||||
@@ -299,7 +299,7 @@ async def fast_get_element_properties(
|
||||
############################################################
|
||||
|
||||
@router.get(
|
||||
"/gettitleschema/",
|
||||
"/title-schemas",
|
||||
summary="获取标题属性架构",
|
||||
description="获取指定水网的标题(标题)属性架构定义"
|
||||
)
|
||||
@@ -310,7 +310,7 @@ async def fast_get_title_schema(
|
||||
return get_title_schema(network)
|
||||
|
||||
@router.get(
|
||||
"/gettitle/",
|
||||
"/titles",
|
||||
summary="获取水网标题属性",
|
||||
description="获取指定水网的标题(Title)信息"
|
||||
)
|
||||
@@ -318,8 +318,8 @@ async def fast_get_title(network: str = Query(..., description="管网名称(
|
||||
"""获取水网的标题属性。"""
|
||||
return get_title(network)
|
||||
|
||||
@router.get(
|
||||
"/settitle/",
|
||||
@router.patch(
|
||||
"/titles",
|
||||
response_model=None,
|
||||
summary="设置水网标题属性",
|
||||
description="设置指定水网的标题(Title)信息"
|
||||
@@ -337,7 +337,7 @@ async def fastapi_set_title(
|
||||
############################################################
|
||||
|
||||
@router.get(
|
||||
"/getstatusschema",
|
||||
"/status-schemas",
|
||||
summary="获取状态属性架构",
|
||||
description="获取指定水网的状态(Status)属性架构定义"
|
||||
)
|
||||
@@ -348,7 +348,7 @@ async def fastapi_get_status_schema(
|
||||
return get_status_schema(network)
|
||||
|
||||
@router.get(
|
||||
"/getstatus/",
|
||||
"/status",
|
||||
summary="获取管线状态",
|
||||
description="获取指定管线的状态信息"
|
||||
)
|
||||
@@ -359,8 +359,8 @@ async def fastapi_get_status(
|
||||
"""获取管线的状态属性。"""
|
||||
return get_status(network, link)
|
||||
|
||||
@router.post(
|
||||
"/setstatus/",
|
||||
@router.patch(
|
||||
"/status-properties",
|
||||
response_model=None,
|
||||
summary="设置管线状态",
|
||||
description="设置指定管线的状态信息"
|
||||
@@ -379,8 +379,8 @@ async def fastapi_set_status_properties(
|
||||
# General Deletion
|
||||
############################################################
|
||||
|
||||
@router.post(
|
||||
"/deletenode/",
|
||||
@router.delete(
|
||||
"/nodes",
|
||||
response_model=None,
|
||||
summary="删除节点",
|
||||
description="删除指定的节点(接点/水源/蓄水池)"
|
||||
@@ -399,8 +399,8 @@ async def fastapi_delete_node(
|
||||
return delete_tank(network, ChangeSet(ps))
|
||||
return ChangeSet() # Should probably raise error or return empty
|
||||
|
||||
@router.post(
|
||||
"/deletelink/",
|
||||
@router.delete(
|
||||
"/links",
|
||||
response_model=None,
|
||||
summary="删除管线",
|
||||
description="删除指定的管线(管道/泵/阀门)"
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
from fastapi import APIRouter, Request, Depends, Query, Path, Body
|
||||
from typing import Any, List, Dict, Union
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from app.services.tjnetwork import (
|
||||
Any,
|
||||
get_all_scada_info,
|
||||
get_major_node_coords,
|
||||
get_major_pipe_nodes,
|
||||
get_network_in_extent,
|
||||
get_network_link_nodes,
|
||||
get_network_node_coords,
|
||||
get_node_coord,
|
||||
)
|
||||
from app.auth.metadata_dependencies import get_current_metadata_user
|
||||
from app.infra.cache.redis_client import redis_client, encode_datetime, decode_datetime
|
||||
import msgpack
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -35,7 +31,7 @@ router = APIRouter()
|
||||
# return set_coord(network, ChangeSet(props))
|
||||
|
||||
@router.get(
|
||||
"/getnodecoord/",
|
||||
"/node-coords",
|
||||
summary="获取节点坐标",
|
||||
description="获取指定节点的地理坐标(X, Y)"
|
||||
)
|
||||
@@ -48,7 +44,7 @@ async def fastapi_get_node_coord(
|
||||
|
||||
# Additional geometry queries found in main.py logic (implicit or explicit)
|
||||
@router.get(
|
||||
"/getnetworkinextent/",
|
||||
"/network-in-extents",
|
||||
summary="获取范围内的网络元素",
|
||||
description="获取指定地理范围内的网络节点和管线"
|
||||
)
|
||||
@@ -63,34 +59,7 @@ async def fastapi_get_network_in_extent(
|
||||
return get_network_in_extent(network, x1, y1, x2, y2)
|
||||
|
||||
@router.get(
|
||||
"/getnetworkgeometries/",
|
||||
dependencies=[Depends(get_current_metadata_user)],
|
||||
summary="获取完整网络几何信息",
|
||||
description="获取整个水网的所有节点、管线和SCADA点的几何信息(需要身份验证)"
|
||||
)
|
||||
async def fastapi_get_network_geometries(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
) -> dict[str, Any] | None:
|
||||
"""获取完整的网络几何信息,包括所有节点、管线和SCADA点。结果从缓存返回。"""
|
||||
cache_key = f"getnetworkgeometries_{network}"
|
||||
data = redis_client.get(cache_key)
|
||||
if data:
|
||||
loaded_dict = msgpack.unpackb(data, object_hook=decode_datetime)
|
||||
return loaded_dict
|
||||
|
||||
coords = get_network_node_coords(network)
|
||||
nodes = []
|
||||
for node_id, coord in coords.items():
|
||||
nodes.append(f"{node_id}:{coord['type']}:{coord['x']}:{coord['y']}")
|
||||
links = get_network_link_nodes(network)
|
||||
scadas = get_all_scada_info(network)
|
||||
|
||||
results = {"nodes": nodes, "links": links, "scadas": scadas}
|
||||
redis_client.set(cache_key, msgpack.packb(results, default=encode_datetime))
|
||||
return results
|
||||
|
||||
@router.get(
|
||||
"/getmajornodecoords/",
|
||||
"/majornode-coords",
|
||||
summary="获取主要节点坐标",
|
||||
description="获取直径大于等于指定值的节点坐标"
|
||||
)
|
||||
@@ -102,7 +71,7 @@ async def fastapi_get_majornode_coords(
|
||||
return get_major_node_coords(network, diameter)
|
||||
|
||||
@router.get(
|
||||
"/getmajorpipenodes/",
|
||||
"/major-pipe-nodes",
|
||||
summary="获取主要管道节点",
|
||||
description="获取直径大于等于指定值的管道的节点ID"
|
||||
)
|
||||
@@ -114,7 +83,7 @@ async def fastapi_get_major_pipe_nodes(
|
||||
return get_major_pipe_nodes(network, diameter)
|
||||
|
||||
@router.get(
|
||||
"/getnetworklinknodes/",
|
||||
"/network-link-nodes",
|
||||
summary="获取网络管线节点",
|
||||
description="获取指定水网所有管线的起点和终点节点"
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ from app.services.tjnetwork import (
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/getjunctionschema", summary="获取节点架构", description="获取指定项目的节点属性架构和数据类型定义。")
|
||||
@router.get("/network-schemas/junction", summary="获取节点架构", description="获取指定项目的节点属性架构和数据类型定义。")
|
||||
async def fast_get_junction_schema(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
@@ -27,7 +27,7 @@ async def fast_get_junction_schema(
|
||||
"""
|
||||
return get_junction_schema(network)
|
||||
|
||||
@router.post("/addjunction/", response_model=None, summary="添加节点", description="在供水网络中添加新的节点,指定节点ID和空间坐标。")
|
||||
@router.post("/junctions", response_model=None, summary="添加节点", description="在供水网络中添加新的节点,指定节点ID和空间坐标。")
|
||||
async def fastapi_add_junction(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
junction: str = Query(..., description="节点 ID"),
|
||||
@@ -51,7 +51,7 @@ async def fastapi_add_junction(
|
||||
ps = {"id": junction, "x": x, "y": y, "elevation": z}
|
||||
return add_junction(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/deletejunction/", response_model=None, summary="删除节点", description="从供水网络中删除指定的节点。")
|
||||
@router.delete("/junctions", response_model=None, summary="删除节点", description="从供水网络中删除指定的节点。")
|
||||
async def fastapi_delete_junction(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
junction: str = Query(..., description="节点 ID")
|
||||
@@ -69,7 +69,7 @@ async def fastapi_delete_junction(
|
||||
ps = {"id": junction}
|
||||
return delete_junction(network, ChangeSet(ps))
|
||||
|
||||
@router.get("/getjunctionelevation/", summary="获取节点标高", description="获取指定节点的标高(海拔高度)。")
|
||||
@router.get("/junctions/elevation", summary="获取节点标高", description="获取指定节点的标高(海拔高度)。")
|
||||
async def fastapi_get_junction_elevation(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
junction: str = Query(..., description="节点 ID")
|
||||
@@ -87,7 +87,7 @@ async def fastapi_get_junction_elevation(
|
||||
ps = get_junction(network, junction)
|
||||
return ps["elevation"]
|
||||
|
||||
@router.get("/getjunctionx/", summary="获取节点 X 坐标", description="获取指定节点的 X 坐标值。")
|
||||
@router.get("/junctions/x", summary="获取节点 X 坐标", description="获取指定节点的 X 坐标值。")
|
||||
async def fastapi_get_junction_x(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
junction: str = Query(..., description="节点 ID")
|
||||
@@ -105,7 +105,7 @@ async def fastapi_get_junction_x(
|
||||
ps = get_junction(network, junction)
|
||||
return ps["x"]
|
||||
|
||||
@router.get("/getjunctiony/", summary="获取节点 Y 坐标", description="获取指定节点的 Y 坐标值。")
|
||||
@router.get("/junctions/y", summary="获取节点 Y 坐标", description="获取指定节点的 Y 坐标值。")
|
||||
async def fastapi_get_junction_y(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
junction: str = Query(..., description="节点 ID")
|
||||
@@ -123,7 +123,7 @@ async def fastapi_get_junction_y(
|
||||
ps = get_junction(network, junction)
|
||||
return ps["y"]
|
||||
|
||||
@router.get("/getjunctioncoord/", summary="获取节点坐标", description="获取指定节点的 X 和 Y 坐标。")
|
||||
@router.get("/junctions/coord", summary="获取节点坐标", description="获取指定节点的 X 和 Y 坐标。")
|
||||
async def fastapi_get_junction_coord(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
junction: str = Query(..., description="节点 ID")
|
||||
@@ -142,7 +142,7 @@ async def fastapi_get_junction_coord(
|
||||
coord = {"x": ps["x"], "y": ps["y"]}
|
||||
return coord
|
||||
|
||||
@router.get("/getjunctiondemand/", summary="获取节点需水量", description="获取指定节点的需水量。")
|
||||
@router.get("/junctions/demand", summary="获取节点需水量", description="获取指定节点的需水量。")
|
||||
async def fastapi_get_junction_demand(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
junction: str = Query(..., description="节点 ID")
|
||||
@@ -160,7 +160,7 @@ async def fastapi_get_junction_demand(
|
||||
ps = get_junction(network, junction)
|
||||
return ps["demand"]
|
||||
|
||||
@router.get("/getjunctionpattern/", summary="获取节点需水模式", description="获取指定节点的需水模式标识。")
|
||||
@router.get("/junctions/pattern", summary="获取节点需水模式", description="获取指定节点的需水模式标识。")
|
||||
async def fastapi_get_junction_pattern(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
junction: str = Query(..., description="节点 ID")
|
||||
@@ -178,7 +178,7 @@ async def fastapi_get_junction_pattern(
|
||||
ps = get_junction(network, junction)
|
||||
return ps["pattern"]
|
||||
|
||||
@router.post("/setjunctionelevation/", response_model=None, summary="设置节点标高", description="设置指定节点的标高值。")
|
||||
@router.patch("/junctions/elevation", response_model=None, summary="设置节点标高", description="设置指定节点的标高值。")
|
||||
async def fastapi_set_junction_elevation(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
junction: str = Query(..., description="节点 ID"),
|
||||
@@ -198,7 +198,7 @@ async def fastapi_set_junction_elevation(
|
||||
ps = {"id": junction, "elevation": elevation}
|
||||
return set_junction(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/setjunctionx/", response_model=None, summary="设置节点 X 坐标", description="设置指定节点的 X 坐标值。")
|
||||
@router.patch("/junctions/x", response_model=None, summary="设置节点 X 坐标", description="设置指定节点的 X 坐标值。")
|
||||
async def fastapi_set_junction_x(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
junction: str = Query(..., description="节点 ID"),
|
||||
@@ -218,7 +218,7 @@ async def fastapi_set_junction_x(
|
||||
ps = {"id": junction, "x": x}
|
||||
return set_junction(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/setjunctiony/", response_model=None, summary="设置节点 Y 坐标", description="设置指定节点的 Y 坐标值。")
|
||||
@router.patch("/junctions/y", response_model=None, summary="设置节点 Y 坐标", description="设置指定节点的 Y 坐标值。")
|
||||
async def fastapi_set_junction_y(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
junction: str = Query(..., description="节点 ID"),
|
||||
@@ -238,7 +238,7 @@ async def fastapi_set_junction_y(
|
||||
ps = {"id": junction, "y": y}
|
||||
return set_junction(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/setjunctioncoord/", response_model=None, summary="设置节点坐标", description="设置指定节点的 X 和 Y 坐标。")
|
||||
@router.patch("/junctions/coord", response_model=None, summary="设置节点坐标", description="设置指定节点的 X 和 Y 坐标。")
|
||||
async def fastapi_set_junction_coord(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
junction: str = Query(..., description="节点 ID"),
|
||||
@@ -260,7 +260,7 @@ async def fastapi_set_junction_coord(
|
||||
ps = {"id": junction, "x": x, "y": y}
|
||||
return set_junction(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/setjunctiondemand/", response_model=None, summary="设置节点需水量", description="设置指定节点的需水量。")
|
||||
@router.patch("/junctions/demand", response_model=None, summary="设置节点需水量", description="设置指定节点的需水量。")
|
||||
async def fastapi_set_junction_demand(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
junction: str = Query(..., description="节点 ID"),
|
||||
@@ -280,7 +280,7 @@ async def fastapi_set_junction_demand(
|
||||
ps = {"id": junction, "demand": demand}
|
||||
return set_junction(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/setjunctionpattern/", response_model=None, summary="设置节点需水模式", description="设置指定节点的需水模式标识。")
|
||||
@router.patch("/junctions/pattern", response_model=None, summary="设置节点需水模式", description="设置指定节点的需水模式标识。")
|
||||
async def fastapi_set_junction_pattern(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
junction: str = Query(..., description="节点 ID"),
|
||||
@@ -300,7 +300,7 @@ async def fastapi_set_junction_pattern(
|
||||
ps = {"id": junction, "pattern": pattern}
|
||||
return set_junction(network, ChangeSet(ps))
|
||||
|
||||
@router.get("/getjunctionproperties/", summary="获取节点属性", description="获取指定节点的所有属性信息。")
|
||||
@router.get("/junctions/properties", summary="获取节点属性", description="获取指定节点的所有属性信息。")
|
||||
async def fastapi_get_junction_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
junction: str = Query(..., description="节点 ID")
|
||||
@@ -317,7 +317,7 @@ async def fastapi_get_junction_properties(
|
||||
"""
|
||||
return get_junction(network, junction)
|
||||
|
||||
@router.get("/getalljunctionproperties/", summary="获取所有节点属性", description="获取指定项目中所有节点的属性信息。")
|
||||
@router.get("/junctions", summary="获取所有节点属性", description="获取指定项目中所有节点的属性信息。")
|
||||
async def fastapi_get_all_junction_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
) -> list[dict[str, Any]]:
|
||||
@@ -337,7 +337,7 @@ async def fastapi_get_all_junction_properties(
|
||||
results = get_all_junctions(network)
|
||||
return results
|
||||
|
||||
@router.post("/setjunctionproperties/", response_model=None, summary="批量设置节点属性", description="批量设置指定节点的多个属性。")
|
||||
@router.patch("/junctions/properties", response_model=None, summary="批量设置节点属性", description="批量设置指定节点的多个属性。")
|
||||
async def fastapi_set_junction_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
junction: str = Query(..., description="节点 ID"),
|
||||
|
||||
@@ -14,7 +14,7 @@ from app.services.tjnetwork import (
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/getpipeschema", summary="获取管道模式", description="获取管道对象的模式定义,包含所有可用字段及其类型")
|
||||
@router.get("/network-schemas/pipe", summary="获取管道模式", description="获取管道对象的模式定义,包含所有可用字段及其类型")
|
||||
async def fastapi_get_pipe_schema(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
@@ -29,7 +29,7 @@ async def fastapi_get_pipe_schema(
|
||||
"""
|
||||
return get_pipe_schema(network)
|
||||
|
||||
@router.post("/addpipe/", response_model=None, summary="添加管道", description="向网络中添加新的管道,需要提供管道的基本参数如长度、管径、粗糙度等")
|
||||
@router.post("/pipes", response_model=None, summary="添加管道", description="向网络中添加新的管道,需要提供管道的基本参数如长度、管径、粗糙度等")
|
||||
async def fastapi_add_pipe(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pipe: str = Query(..., description="管道标识符"),
|
||||
@@ -70,7 +70,7 @@ async def fastapi_add_pipe(
|
||||
}
|
||||
return add_pipe(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/deletepipe/", response_model=None, summary="删除管道", description="从网络中删除指定的管道")
|
||||
@router.delete("/pipes", response_model=None, summary="删除管道", description="从网络中删除指定的管道")
|
||||
async def fastapi_delete_pipe(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pipe: str = Query(..., description="要删除的管道ID")
|
||||
@@ -88,7 +88,7 @@ async def fastapi_delete_pipe(
|
||||
ps = {"id": pipe}
|
||||
return delete_pipe(network, ChangeSet(ps))
|
||||
|
||||
@router.get("/getpipenode1/", summary="获取管道起始节点", description="获取指定管道的起始节点ID")
|
||||
@router.get("/pipes/node1", summary="获取管道起始节点", description="获取指定管道的起始节点ID")
|
||||
async def fastapi_get_pipe_node1(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pipe: str = Query(..., description="管道ID")
|
||||
@@ -106,7 +106,7 @@ async def fastapi_get_pipe_node1(
|
||||
ps = get_pipe(network, pipe)
|
||||
return ps["node1"]
|
||||
|
||||
@router.get("/getpipenode2/", summary="获取管道终止节点", description="获取指定管道的终止节点ID")
|
||||
@router.get("/pipes/node2", summary="获取管道终止节点", description="获取指定管道的终止节点ID")
|
||||
async def fastapi_get_pipe_node2(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pipe: str = Query(..., description="管道ID")
|
||||
@@ -124,7 +124,7 @@ async def fastapi_get_pipe_node2(
|
||||
ps = get_pipe(network, pipe)
|
||||
return ps["node2"]
|
||||
|
||||
@router.get("/getpipelength/", summary="获取管道长度", description="获取指定管道的长度")
|
||||
@router.get("/pipes/length", summary="获取管道长度", description="获取指定管道的长度")
|
||||
async def fastapi_get_pipe_length(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pipe: str = Query(..., description="管道ID")
|
||||
@@ -142,7 +142,7 @@ async def fastapi_get_pipe_length(
|
||||
ps = get_pipe(network, pipe)
|
||||
return ps["length"]
|
||||
|
||||
@router.get("/getpipediameter/", summary="获取管道管径", description="获取指定管道的管径")
|
||||
@router.get("/pipes/diameter", summary="获取管道管径", description="获取指定管道的管径")
|
||||
async def fastapi_get_pipe_diameter(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pipe: str = Query(..., description="管道ID")
|
||||
@@ -160,7 +160,7 @@ async def fastapi_get_pipe_diameter(
|
||||
ps = get_pipe(network, pipe)
|
||||
return ps["diameter"]
|
||||
|
||||
@router.get("/getpiperoughness/", summary="获取管道粗糙度", description="获取指定管道的粗糙度")
|
||||
@router.get("/pipes/roughness", summary="获取管道粗糙度", description="获取指定管道的粗糙度")
|
||||
async def fastapi_get_pipe_roughness(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pipe: str = Query(..., description="管道ID")
|
||||
@@ -178,7 +178,7 @@ async def fastapi_get_pipe_roughness(
|
||||
ps = get_pipe(network, pipe)
|
||||
return ps["roughness"]
|
||||
|
||||
@router.get("/getpipeminorloss/", summary="获取管道局部阻力系数", description="获取指定管道的局部阻力系数")
|
||||
@router.get("/pipes/minor-loss", summary="获取管道局部阻力系数", description="获取指定管道的局部阻力系数")
|
||||
async def fastapi_get_pipe_minor_loss(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pipe: str = Query(..., description="管道ID")
|
||||
@@ -196,7 +196,7 @@ async def fastapi_get_pipe_minor_loss(
|
||||
ps = get_pipe(network, pipe)
|
||||
return ps["minor_loss"]
|
||||
|
||||
@router.get("/getpipestatus/", summary="获取管道状态", description="获取指定管道的状态(开启或关闭)")
|
||||
@router.get("/pipes/status", summary="获取管道状态", description="获取指定管道的状态(开启或关闭)")
|
||||
async def fastapi_get_pipe_status(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pipe: str = Query(..., description="管道ID")
|
||||
@@ -214,7 +214,7 @@ async def fastapi_get_pipe_status(
|
||||
ps = get_pipe(network, pipe)
|
||||
return ps["status"]
|
||||
|
||||
@router.post("/setpipenode1/", response_model=None, summary="设置管道起始节点", description="设置指定管道的起始节点")
|
||||
@router.patch("/pipes/node1", response_model=None, summary="设置管道起始节点", description="设置指定管道的起始节点")
|
||||
async def fastapi_set_pipe_node1(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pipe: str = Query(..., description="管道ID"),
|
||||
@@ -234,7 +234,7 @@ async def fastapi_set_pipe_node1(
|
||||
ps = {"id": pipe, "node1": node1}
|
||||
return set_pipe(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/setpipenode2/", response_model=None, summary="设置管道终止节点", description="设置指定管道的终止节点")
|
||||
@router.patch("/pipes/node2", response_model=None, summary="设置管道终止节点", description="设置指定管道的终止节点")
|
||||
async def fastapi_set_pipe_node2(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pipe: str = Query(..., description="管道ID"),
|
||||
@@ -254,7 +254,7 @@ async def fastapi_set_pipe_node2(
|
||||
ps = {"id": pipe, "node2": node2}
|
||||
return set_pipe(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/setpipelength/", response_model=None, summary="设置管道长度", description="设置指定管道的长度")
|
||||
@router.patch("/pipes/length", response_model=None, summary="设置管道长度", description="设置指定管道的长度")
|
||||
async def fastapi_set_pipe_length(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pipe: str = Query(..., description="管道ID"),
|
||||
@@ -274,7 +274,7 @@ async def fastapi_set_pipe_length(
|
||||
ps = {"id": pipe, "length": length}
|
||||
return set_pipe(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/setpipediameter/", response_model=None, summary="设置管道管径", description="设置指定管道的管径")
|
||||
@router.patch("/pipes/diameter", response_model=None, summary="设置管道管径", description="设置指定管道的管径")
|
||||
async def fastapi_set_pipe_diameter(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pipe: str = Query(..., description="管道ID"),
|
||||
@@ -294,7 +294,7 @@ async def fastapi_set_pipe_diameter(
|
||||
ps = {"id": pipe, "diameter": diameter}
|
||||
return set_pipe(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/setpiperoughness/", response_model=None, summary="设置管道粗糙度", description="设置指定管道的粗糙度")
|
||||
@router.patch("/pipes/roughness", response_model=None, summary="设置管道粗糙度", description="设置指定管道的粗糙度")
|
||||
async def fastapi_set_pipe_roughness(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pipe: str = Query(..., description="管道ID"),
|
||||
@@ -314,7 +314,7 @@ async def fastapi_set_pipe_roughness(
|
||||
ps = {"id": pipe, "roughness": roughness}
|
||||
return set_pipe(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/setpipeminorloss/", response_model=None, summary="设置管道局部阻力系数", description="设置指定管道的局部阻力系数")
|
||||
@router.patch("/pipes/minor-loss", response_model=None, summary="设置管道局部阻力系数", description="设置指定管道的局部阻力系数")
|
||||
async def fastapi_set_pipe_minor_loss(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pipe: str = Query(..., description="管道ID"),
|
||||
@@ -334,7 +334,7 @@ async def fastapi_set_pipe_minor_loss(
|
||||
ps = {"id": pipe, "minor_loss": minor_loss}
|
||||
return set_pipe(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/setpipestatus/", response_model=None, summary="设置管道状态", description="设置指定管道的状态(开启或关闭)")
|
||||
@router.patch("/pipes/status", response_model=None, summary="设置管道状态", description="设置指定管道的状态(开启或关闭)")
|
||||
async def fastapi_set_pipe_status(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pipe: str = Query(..., description="管道ID"),
|
||||
@@ -354,7 +354,7 @@ async def fastapi_set_pipe_status(
|
||||
ps = {"id": pipe, "status": status}
|
||||
return set_pipe(network, ChangeSet(ps))
|
||||
|
||||
@router.get("/getpipeproperties/", summary="获取管道属性", description="获取指定管道的所有属性信息")
|
||||
@router.get("/pipes/properties", summary="获取管道属性", description="获取指定管道的所有属性信息")
|
||||
async def fastapi_get_pipe_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pipe: str = Query(..., description="管道ID")
|
||||
@@ -371,7 +371,7 @@ async def fastapi_get_pipe_properties(
|
||||
"""
|
||||
return get_pipe(network, pipe)
|
||||
|
||||
@router.get("/getallpipeproperties/", summary="获取所有管道属性", description="获取网络中所有管道的属性信息列表")
|
||||
@router.get("/pipes", summary="获取所有管道属性", description="获取网络中所有管道的属性信息列表")
|
||||
async def fastapi_get_all_pipe_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
) -> list[dict[str, Any]]:
|
||||
@@ -389,7 +389,7 @@ async def fastapi_get_all_pipe_properties(
|
||||
results = get_all_pipes(network)
|
||||
return results
|
||||
|
||||
@router.post("/setpipeproperties/", response_model=None, summary="设置管道属性", description="批量设置指定管道的多个属性")
|
||||
@router.patch("/pipes/properties", response_model=None, summary="设置管道属性", description="批量设置指定管道的多个属性")
|
||||
async def fastapi_set_pipe_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pipe: str = Query(..., description="管道ID"),
|
||||
|
||||
@@ -13,7 +13,7 @@ from app.services.tjnetwork import (
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/getpumpschema", summary="获取水泵模式", description="获取水泵对象的模式定义,包含所有可用字段及其类型")
|
||||
@router.get("/network-schemas/pump", summary="获取水泵模式", description="获取水泵对象的模式定义,包含所有可用字段及其类型")
|
||||
async def fastapi_get_pump_schema(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
@@ -28,7 +28,7 @@ async def fastapi_get_pump_schema(
|
||||
"""
|
||||
return get_pump_schema(network)
|
||||
|
||||
@router.post("/addpump/", response_model=None, summary="添加水泵", description="向网络中添加新的水泵,需要提供水泵的基本参数如功率等")
|
||||
@router.post("/pumps", response_model=None, summary="添加水泵", description="向网络中添加新的水泵,需要提供水泵的基本参数如功率等")
|
||||
async def fastapi_add_pump(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pump: str = Query(..., description="水泵标识符"),
|
||||
@@ -52,7 +52,7 @@ async def fastapi_add_pump(
|
||||
ps = {"id": pump, "node1": node1, "node2": node2, "power": power}
|
||||
return add_pump(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/deletepump/", response_model=None, summary="删除水泵", description="从网络中删除指定的水泵")
|
||||
@router.delete("/pumps", response_model=None, summary="删除水泵", description="从网络中删除指定的水泵")
|
||||
async def fastapi_delete_pump(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pump: str = Query(..., description="要删除的水泵ID")
|
||||
@@ -70,7 +70,7 @@ async def fastapi_delete_pump(
|
||||
ps = {"id": pump}
|
||||
return delete_pump(network, ChangeSet(ps))
|
||||
|
||||
@router.get("/getpumpnode1/", summary="获取水泵起始节点", description="获取指定水泵的起始节点ID")
|
||||
@router.get("/pumps/node1", summary="获取水泵起始节点", description="获取指定水泵的起始节点ID")
|
||||
async def fastapi_get_pump_node1(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pump: str = Query(..., description="水泵ID")
|
||||
@@ -88,7 +88,7 @@ async def fastapi_get_pump_node1(
|
||||
ps = get_pump(network, pump)
|
||||
return ps["node1"]
|
||||
|
||||
@router.get("/getpumpnode2/", summary="获取水泵终止节点", description="获取指定水泵的终止节点ID")
|
||||
@router.get("/pumps/node2", summary="获取水泵终止节点", description="获取指定水泵的终止节点ID")
|
||||
async def fastapi_get_pump_node2(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pump: str = Query(..., description="水泵ID")
|
||||
@@ -106,7 +106,7 @@ async def fastapi_get_pump_node2(
|
||||
ps = get_pump(network, pump)
|
||||
return ps["node2"]
|
||||
|
||||
@router.post("/setpumpnode1/", response_model=None, summary="设置水泵起始节点", description="设置指定水泵的起始节点")
|
||||
@router.patch("/pumps/node1", response_model=None, summary="设置水泵起始节点", description="设置指定水泵的起始节点")
|
||||
async def fastapi_set_pump_node1(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pump: str = Query(..., description="水泵ID"),
|
||||
@@ -126,7 +126,7 @@ async def fastapi_set_pump_node1(
|
||||
ps = {"id": pump, "node1": node1}
|
||||
return set_pump(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/setpumpnode2/", response_model=None, summary="设置水泵终止节点", description="设置指定水泵的终止节点")
|
||||
@router.patch("/pumps/node2", response_model=None, summary="设置水泵终止节点", description="设置指定水泵的终止节点")
|
||||
async def fastapi_set_pump_node2(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pump: str = Query(..., description="水泵ID"),
|
||||
@@ -146,7 +146,7 @@ async def fastapi_set_pump_node2(
|
||||
ps = {"id": pump, "node2": node2}
|
||||
return set_pump(network, ChangeSet(ps))
|
||||
|
||||
@router.get("/getpumpproperties/", summary="获取水泵属性", description="获取指定水泵的所有属性信息")
|
||||
@router.get("/pumps/properties", summary="获取水泵属性", description="获取指定水泵的所有属性信息")
|
||||
async def fastapi_get_pump_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pump: str = Query(..., description="水泵ID")
|
||||
@@ -163,7 +163,7 @@ async def fastapi_get_pump_properties(
|
||||
"""
|
||||
return get_pump(network, pump)
|
||||
|
||||
@router.get("/getallpumpproperties/", summary="获取所有水泵属性", description="获取网络中所有水泵的属性信息列表")
|
||||
@router.get("/pumps", summary="获取所有水泵属性", description="获取网络中所有水泵的属性信息列表")
|
||||
async def fastapi_get_all_pump_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
) -> list[dict[str, Any]]:
|
||||
@@ -181,7 +181,7 @@ async def fastapi_get_all_pump_properties(
|
||||
results = get_all_pumps(network)
|
||||
return results
|
||||
|
||||
@router.post("/setpumpproperties/", response_model=None, summary="设置水泵属性", description="批量设置指定水泵的多个属性")
|
||||
@router.patch("/pumps/properties", response_model=None, summary="设置水泵属性", description="批量设置指定水泵的多个属性")
|
||||
async def fastapi_set_pump_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
pump: str = Query(..., description="水泵ID"),
|
||||
|
||||
@@ -45,7 +45,7 @@ router = APIRouter()
|
||||
############################################################
|
||||
|
||||
@router.get(
|
||||
"/getregionschema/",
|
||||
"/network-schemas/region",
|
||||
summary="获取区域属性架构",
|
||||
description="获取指定水网的区域属性架构定义"
|
||||
)
|
||||
@@ -56,7 +56,7 @@ async def fastapi_get_region_schema(
|
||||
return get_region_schema(network)
|
||||
|
||||
@router.get(
|
||||
"/getregion/",
|
||||
"/regions/detail",
|
||||
summary="获取区域信息",
|
||||
description="获取指定ID的区域详细信息"
|
||||
)
|
||||
@@ -67,8 +67,8 @@ async def fastapi_get_region(
|
||||
"""获取区域的详细信息。"""
|
||||
return get_region(network, id)
|
||||
|
||||
@router.post(
|
||||
"/setregion/",
|
||||
@router.patch(
|
||||
"/regions",
|
||||
response_model=None,
|
||||
summary="设置区域属性",
|
||||
description="修改指定区域的属性信息"
|
||||
@@ -82,7 +82,7 @@ async def fastapi_set_region(
|
||||
return set_region(network, ChangeSet(props))
|
||||
|
||||
@router.post(
|
||||
"/addregion/",
|
||||
"/regions",
|
||||
response_model=None,
|
||||
summary="添加新区域",
|
||||
description="向水网添加一个新的区域"
|
||||
@@ -95,8 +95,8 @@ async def fastapi_add_region(
|
||||
props = await req.json()
|
||||
return add_region(network, ChangeSet(props))
|
||||
|
||||
@router.post(
|
||||
"/deleteregion/",
|
||||
@router.delete(
|
||||
"/regions",
|
||||
response_model=None,
|
||||
summary="删除区域",
|
||||
description="删除指定的区域"
|
||||
@@ -114,8 +114,8 @@ async def fastapi_delete_region(
|
||||
# district_metering_area 33
|
||||
############################################################
|
||||
|
||||
@router.get(
|
||||
"/calculatedistrictmeteringareaforregion/",
|
||||
@router.post(
|
||||
"/district-metering-areas/for-region",
|
||||
summary="计算区域内DMA分区",
|
||||
description="为指定区域计算区域计量(DMA)分区方案"
|
||||
)
|
||||
@@ -141,8 +141,8 @@ async def fastapi_calculate_district_metering_area_for_region(
|
||||
network, region, part_count, part_type
|
||||
)
|
||||
|
||||
@router.get(
|
||||
"/calculatedistrictmeteringareafornetwork/",
|
||||
@router.post(
|
||||
"/district-metering-areas/for-network",
|
||||
summary="计算整网DMA分区",
|
||||
description="为整个水网计算区域计量(DMA)分区方案"
|
||||
)
|
||||
@@ -165,7 +165,7 @@ async def fastapi_calculate_district_metering_area_for_network(
|
||||
return calculate_district_metering_area_for_network(network, part_count, part_type)
|
||||
|
||||
@router.get(
|
||||
"/getdistrictmeteringareaschema/",
|
||||
"/network-schemas/district-metering-area",
|
||||
summary="获取DMA属性架构",
|
||||
description="获取指定水网的区域计量(DMA)属性架构定义"
|
||||
)
|
||||
@@ -176,7 +176,7 @@ async def fastapi_get_district_metering_area_schema(
|
||||
return get_district_metering_area_schema(network)
|
||||
|
||||
@router.get(
|
||||
"/getdistrictmeteringarea/",
|
||||
"/district-metering-areas/detail",
|
||||
summary="获取DMA信息",
|
||||
description="获取指定ID的区域计量(DMA)详细信息"
|
||||
)
|
||||
@@ -187,8 +187,8 @@ async def fastapi_get_district_metering_area(
|
||||
"""获取DMA的详细信息。"""
|
||||
return get_district_metering_area(network, id)
|
||||
|
||||
@router.post(
|
||||
"/setdistrictmeteringarea/",
|
||||
@router.patch(
|
||||
"/district-metering-areas",
|
||||
response_model=None,
|
||||
summary="设置DMA属性",
|
||||
description="修改指定DMA的属性信息"
|
||||
@@ -202,7 +202,7 @@ async def fastapi_set_district_metering_area(
|
||||
return set_district_metering_area(network, ChangeSet(props))
|
||||
|
||||
@router.post(
|
||||
"/adddistrictmeteringarea/",
|
||||
"/district-metering-areas",
|
||||
response_model=None,
|
||||
summary="添加新DMA",
|
||||
description="向水网添加一个新的区域计量(DMA)"
|
||||
@@ -222,8 +222,8 @@ async def fastapi_add_district_metering_area(
|
||||
props["boundary"] = newBoundary
|
||||
return add_district_metering_area(network, ChangeSet(props))
|
||||
|
||||
@router.post(
|
||||
"/deletedistrictmeteringarea/",
|
||||
@router.delete(
|
||||
"/district-metering-areas",
|
||||
response_model=None,
|
||||
summary="删除DMA",
|
||||
description="删除指定的区域计量(DMA)"
|
||||
@@ -237,7 +237,7 @@ async def fastapi_delete_district_metering_area(
|
||||
return delete_district_metering_area(network, ChangeSet(props))
|
||||
|
||||
@router.get(
|
||||
"/getalldistrictmeteringareaids/",
|
||||
"/district-metering-areas/ids",
|
||||
summary="获取所有DMA ID",
|
||||
description="获取指定水网中所有DMA的ID列表"
|
||||
)
|
||||
@@ -248,7 +248,7 @@ async def fastapi_get_all_district_metering_area_ids(
|
||||
return get_all_district_metering_area_ids(network)
|
||||
|
||||
@router.get(
|
||||
"/getalldistrictmeteringareas/",
|
||||
"/district-metering-areas",
|
||||
summary="获取所有DMA",
|
||||
description="获取指定水网中所有DMA的详细信息"
|
||||
)
|
||||
@@ -259,7 +259,7 @@ async def getalldistrictmeteringareas(
|
||||
return get_all_district_metering_areas(network)
|
||||
|
||||
@router.post(
|
||||
"/generatedistrictmeteringarea/",
|
||||
"/district-metering-area-generation-runs",
|
||||
response_model=None,
|
||||
summary="生成DMA分区",
|
||||
description="根据参数自动生成水网的DMA分区方案"
|
||||
@@ -276,7 +276,7 @@ async def fastapi_generate_district_metering_area(
|
||||
)
|
||||
|
||||
@router.post(
|
||||
"/generatesubdistrictmeteringarea/",
|
||||
"/sub-district-metering-areas",
|
||||
response_model=None,
|
||||
summary="生成DMA子分区",
|
||||
description="为指定DMA生成子DMA分区"
|
||||
@@ -298,8 +298,8 @@ async def fastapi_generate_sub_district_metering_area(
|
||||
# service_area 34
|
||||
############################################################
|
||||
|
||||
@router.get(
|
||||
"/calculateservicearea/",
|
||||
@router.post(
|
||||
"/service-area-calculations",
|
||||
summary="计算服务区",
|
||||
description="计算指定水网的服务区分区,返回全部时间步结果"
|
||||
)
|
||||
@@ -310,7 +310,7 @@ async def fastapi_calculate_service_area(
|
||||
return calculate_service_area(network)
|
||||
|
||||
@router.get(
|
||||
"/getserviceareaschema/",
|
||||
"/network-schemas/service-area",
|
||||
summary="获取服务区属性架构",
|
||||
description="获取指定水网的服务区属性架构定义"
|
||||
)
|
||||
@@ -321,7 +321,7 @@ async def fastapi_get_service_area_schema(
|
||||
return get_service_area_schema(network)
|
||||
|
||||
@router.get(
|
||||
"/getservicearea/",
|
||||
"/service-areas/detail",
|
||||
summary="获取服务区信息",
|
||||
description="获取指定ID的服务区详细信息"
|
||||
)
|
||||
@@ -332,8 +332,8 @@ async def fastapi_get_service_area(
|
||||
"""获取服务区的详细信息。"""
|
||||
return get_service_area(network, id)
|
||||
|
||||
@router.post(
|
||||
"/setservicearea/",
|
||||
@router.patch(
|
||||
"/service-areas",
|
||||
response_model=None,
|
||||
summary="设置服务区属性",
|
||||
description="修改指定服务区的属性信息"
|
||||
@@ -347,7 +347,7 @@ async def fastapi_set_service_area(
|
||||
return set_service_area(network, ChangeSet(props))
|
||||
|
||||
@router.post(
|
||||
"/addservicearea/",
|
||||
"/service-areas",
|
||||
response_model=None,
|
||||
summary="添加新服务区",
|
||||
description="向水网添加一个新的服务区"
|
||||
@@ -360,8 +360,8 @@ async def fastapi_add_service_area(
|
||||
props = await req.json()
|
||||
return add_service_area(network, ChangeSet(props))
|
||||
|
||||
@router.post(
|
||||
"/deleteservicearea/",
|
||||
@router.delete(
|
||||
"/service-areas",
|
||||
response_model=None,
|
||||
summary="删除服务区",
|
||||
description="删除指定的服务区"
|
||||
@@ -375,7 +375,7 @@ async def fastapi_delete_service_area(
|
||||
return delete_service_area(network, ChangeSet(props))
|
||||
|
||||
@router.get(
|
||||
"/getallserviceareas/",
|
||||
"/service-areas",
|
||||
summary="获取所有服务区",
|
||||
description="获取指定水网中的所有服务区信息"
|
||||
)
|
||||
@@ -386,7 +386,7 @@ async def fastapi_get_all_service_areas(
|
||||
return get_all_service_areas(network)
|
||||
|
||||
@router.post(
|
||||
"/generateservicearea/",
|
||||
"/service-area-generation-runs",
|
||||
response_model=None,
|
||||
summary="生成服务区分区",
|
||||
description="根据参数自动生成水网的服务区分区"
|
||||
@@ -403,8 +403,8 @@ async def fastapi_generate_service_area(
|
||||
# virtual_district 35
|
||||
############################################################
|
||||
|
||||
@router.get(
|
||||
"/calculatevirtualdistrict/",
|
||||
@router.post(
|
||||
"/virtual-district-calculations",
|
||||
summary="计算虚拟分区",
|
||||
description="根据指定的压力监测节点作为中心节点计算虚拟分区方案"
|
||||
)
|
||||
@@ -416,7 +416,7 @@ async def fastapi_calculate_virtual_district(
|
||||
return calculate_virtual_district(network, centers)
|
||||
|
||||
@router.get(
|
||||
"/getvirtualdistrictschema/",
|
||||
"/network-schemas/virtual-district",
|
||||
summary="获取虚拟分区属性架构",
|
||||
description="获取指定水网的虚拟分区属性架构定义"
|
||||
)
|
||||
@@ -427,7 +427,7 @@ async def fastapi_get_virtual_district_schema(
|
||||
return get_virtual_district_schema(network)
|
||||
|
||||
@router.get(
|
||||
"/getvirtualdistrict/",
|
||||
"/virtual-districts/detail",
|
||||
summary="获取虚拟分区信息",
|
||||
description="获取指定ID的虚拟分区详细信息"
|
||||
)
|
||||
@@ -438,8 +438,8 @@ async def fastapi_get_virtual_district(
|
||||
"""获取虚拟分区的详细信息。"""
|
||||
return get_virtual_district(network, id)
|
||||
|
||||
@router.post(
|
||||
"/setvirtualdistrict/",
|
||||
@router.patch(
|
||||
"/virtual-districts",
|
||||
response_model=None,
|
||||
summary="设置虚拟分区属性",
|
||||
description="修改指定虚拟分区的属性信息"
|
||||
@@ -453,7 +453,7 @@ async def fastapi_set_virtual_district(
|
||||
return set_virtual_district(network, ChangeSet(props))
|
||||
|
||||
@router.post(
|
||||
"/addvirtualdistrict/",
|
||||
"/virtual-districts",
|
||||
response_model=None,
|
||||
summary="添加新虚拟分区",
|
||||
description="向水网添加一个新的虚拟分区"
|
||||
@@ -466,8 +466,8 @@ async def fastapi_add_virtual_district(
|
||||
props = await req.json()
|
||||
return add_virtual_district(network, ChangeSet(props))
|
||||
|
||||
@router.post(
|
||||
"/deletevirtualdistrict/",
|
||||
@router.delete(
|
||||
"/virtual-districts",
|
||||
response_model=None,
|
||||
summary="删除虚拟分区",
|
||||
description="删除指定的虚拟分区"
|
||||
@@ -481,7 +481,7 @@ async def fastapi_delete_virtual_district(
|
||||
return delete_virtual_district(network, ChangeSet(props))
|
||||
|
||||
@router.get(
|
||||
"/getallvirtualdistrict/",
|
||||
"/virtual-districts",
|
||||
summary="获取所有虚拟分区",
|
||||
description="获取指定水网中的所有虚拟分区信息"
|
||||
)
|
||||
@@ -492,7 +492,7 @@ async def fastapi_get_all_virtual_district(
|
||||
return get_all_virtual_districts(network)
|
||||
|
||||
@router.post(
|
||||
"/generatevirtualdistrict/",
|
||||
"/virtual-district-generation-runs",
|
||||
response_model=None,
|
||||
summary="生成虚拟分区",
|
||||
description="根据参数自动生成虚拟分区方案"
|
||||
@@ -506,8 +506,8 @@ async def fastapi_generate_virtual_district(
|
||||
props = await req.json()
|
||||
return generate_virtual_district(network, props["centers"], inflate_delta)
|
||||
|
||||
@router.get(
|
||||
"/calculatedistrictmeteringareafornodes/",
|
||||
@router.post(
|
||||
"/district-metering-areas/for-nodes",
|
||||
summary="计算节点DMA分区",
|
||||
description="为指定节点集计算区域计量(DMA)分区方案"
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ from app.services.tjnetwork import (
|
||||
router = APIRouter()
|
||||
|
||||
@router.get(
|
||||
"/getreservoirschema",
|
||||
"/network-schemas/reservoir",
|
||||
summary="获取水库模式",
|
||||
description="获取指定供水网络中所有水库的模式/属性字段定义"
|
||||
)
|
||||
@@ -35,7 +35,7 @@ async def fast_get_reservoir_schema(
|
||||
return get_reservoir_schema(network)
|
||||
|
||||
@router.post(
|
||||
"/addreservoir/",
|
||||
"/reservoirs",
|
||||
response_model=None,
|
||||
summary="添加水库",
|
||||
description="在指定供水网络中添加新的水库/水源节点"
|
||||
@@ -65,8 +65,8 @@ async def fastapi_add_reservoir(
|
||||
ps = {"id": reservoir, "x": x, "y": y, "head": head}
|
||||
return add_reservoir(network, ChangeSet(ps))
|
||||
|
||||
@router.post(
|
||||
"/deletereservoir/",
|
||||
@router.delete(
|
||||
"/reservoirs",
|
||||
response_model=None,
|
||||
summary="删除水库",
|
||||
description="从指定供水网络中删除指定的水库/水源节点"
|
||||
@@ -91,7 +91,7 @@ async def fastapi_delete_reservoir(
|
||||
return delete_reservoir(network, ChangeSet(ps))
|
||||
|
||||
@router.get(
|
||||
"/getreservoirhead/",
|
||||
"/reservoirs/head",
|
||||
summary="获取水库水头",
|
||||
description="获取指定水库的供水水头/总水头值"
|
||||
)
|
||||
@@ -115,7 +115,7 @@ async def fastapi_get_reservoir_head(
|
||||
return ps["head"]
|
||||
|
||||
@router.get(
|
||||
"/getreservoirpattern/",
|
||||
"/reservoirs/pattern",
|
||||
summary="获取水库模式",
|
||||
description="获取指定水库的运行模式/供水模式"
|
||||
)
|
||||
@@ -139,7 +139,7 @@ async def fastapi_get_reservoir_pattern(
|
||||
return ps["pattern"]
|
||||
|
||||
@router.get(
|
||||
"/getreservoirx/",
|
||||
"/reservoirs/x",
|
||||
summary="获取水库X坐标",
|
||||
description="获取指定水库的X坐标位置"
|
||||
)
|
||||
@@ -163,7 +163,7 @@ async def fastapi_get_reservoir_x(
|
||||
return ps["x"]
|
||||
|
||||
@router.get(
|
||||
"/getreservoiry/",
|
||||
"/reservoirs/y",
|
||||
summary="获取水库Y坐标",
|
||||
description="获取指定水库的Y坐标位置"
|
||||
)
|
||||
@@ -187,7 +187,7 @@ async def fastapi_get_reservoir_y(
|
||||
return ps["y"]
|
||||
|
||||
@router.get(
|
||||
"/getreservoircoord/",
|
||||
"/reservoirs/coord",
|
||||
summary="获取水库坐标",
|
||||
description="获取指定水库的平面坐标(X和Y坐标)"
|
||||
)
|
||||
@@ -211,8 +211,8 @@ async def fastapi_get_reservoir_coord(
|
||||
coord = {"id": reservoir, "x": ps["x"], "y": ps["y"]}
|
||||
return coord
|
||||
|
||||
@router.post(
|
||||
"/setreservoirhead/",
|
||||
@router.patch(
|
||||
"/reservoirs/head",
|
||||
response_model=None,
|
||||
summary="设置水库水头",
|
||||
description="更新指定水库的供水水头/总水头值"
|
||||
@@ -238,8 +238,8 @@ async def fastapi_set_reservoir_head(
|
||||
ps = {"id": reservoir, "head": head}
|
||||
return set_reservoir(network, ChangeSet(ps))
|
||||
|
||||
@router.post(
|
||||
"/setreservoirpattern/",
|
||||
@router.patch(
|
||||
"/reservoirs/pattern",
|
||||
response_model=None,
|
||||
summary="设置水库模式",
|
||||
description="更新指定水库的运行模式/供水模式"
|
||||
@@ -265,8 +265,8 @@ async def fastapi_set_reservoir_pattern(
|
||||
ps = {"id": reservoir, "pattern": pattern}
|
||||
return set_reservoir(network, ChangeSet(ps))
|
||||
|
||||
@router.post(
|
||||
"/setreservoirx/",
|
||||
@router.patch(
|
||||
"/reservoirs/x",
|
||||
response_model=None,
|
||||
summary="设置水库X坐标",
|
||||
description="更新指定水库的X坐标位置"
|
||||
@@ -292,8 +292,8 @@ async def fastapi_set_reservoir_x(
|
||||
ps = {"id": reservoir, "x": x}
|
||||
return set_reservoir(network, ChangeSet(ps))
|
||||
|
||||
@router.post(
|
||||
"/setreservoiry/",
|
||||
@router.patch(
|
||||
"/reservoirs/y",
|
||||
response_model=None,
|
||||
summary="设置水库Y坐标",
|
||||
description="更新指定水库的Y坐标位置"
|
||||
@@ -319,8 +319,8 @@ async def fastapi_set_reservoir_y(
|
||||
ps = {"id": reservoir, "y": y}
|
||||
return set_reservoir(network, ChangeSet(ps))
|
||||
|
||||
@router.post(
|
||||
"/setreservoircoord/",
|
||||
@router.patch(
|
||||
"/reservoirs/coord",
|
||||
response_model=None,
|
||||
summary="设置水库坐标",
|
||||
description="更新指定水库的平面坐标(X和Y坐标)"
|
||||
@@ -349,7 +349,7 @@ async def fastapi_set_reservoir_coord(
|
||||
return set_reservoir(network, ChangeSet(ps))
|
||||
|
||||
@router.get(
|
||||
"/getreservoirproperties/",
|
||||
"/reservoirs/properties",
|
||||
summary="获取水库属性",
|
||||
description="获取指定水库的所有属性"
|
||||
)
|
||||
@@ -372,7 +372,7 @@ async def fastapi_get_reservoir_properties(
|
||||
return get_reservoir(network, reservoir)
|
||||
|
||||
@router.get(
|
||||
"/getallreservoirproperties/",
|
||||
"/reservoirs",
|
||||
summary="获取所有水库属性",
|
||||
description="获取指定供水网络中所有水库的属性"
|
||||
)
|
||||
@@ -393,8 +393,8 @@ async def fastapi_get_all_reservoir_properties(
|
||||
results = get_all_reservoirs(network)
|
||||
return results
|
||||
|
||||
@router.post(
|
||||
"/setreservoirproperties/",
|
||||
@router.patch(
|
||||
"/reservoirs/properties",
|
||||
response_model=None,
|
||||
summary="设置水库属性",
|
||||
description="批量更新指定水库的多个属性"
|
||||
|
||||
@@ -16,7 +16,7 @@ router = APIRouter()
|
||||
############################################################
|
||||
|
||||
@router.get(
|
||||
"/gettagschema/",
|
||||
"/network-schemas/tag",
|
||||
summary="获取标签属性架构",
|
||||
description="获取指定水网的标签(Tag)属性架构定义"
|
||||
)
|
||||
@@ -27,7 +27,7 @@ async def fastapi_get_tag_schema(
|
||||
return get_tag_schema(network)
|
||||
|
||||
@router.get(
|
||||
"/gettag/",
|
||||
"/tags/detail",
|
||||
summary="获取标签信息",
|
||||
description="获取指定类型和ID的标签信息"
|
||||
)
|
||||
@@ -40,7 +40,7 @@ async def fastapi_get_tag(
|
||||
return get_tag(network, t_type, id)
|
||||
|
||||
@router.get(
|
||||
"/gettags/",
|
||||
"/tags",
|
||||
summary="获取所有标签",
|
||||
description="获取指定水网中的所有标签信息"
|
||||
)
|
||||
@@ -51,8 +51,8 @@ async def fastapi_get_tags(
|
||||
tags = get_tags(network)
|
||||
return tags
|
||||
|
||||
@router.post(
|
||||
"/settag/",
|
||||
@router.patch(
|
||||
"/tags",
|
||||
response_model=None,
|
||||
summary="设置标签",
|
||||
description="为指定元素设置或修改标签信息"
|
||||
|
||||
@@ -13,7 +13,7 @@ from app.services.tjnetwork import (
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/gettankschema", summary="获取水箱模式", description="获取指定网络的水箱数据结构模式定义")
|
||||
@router.get("/network-schemas/tank", summary="获取水箱模式", description="获取指定网络的水箱数据结构模式定义")
|
||||
async def fast_get_tank_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[str, Any]]:
|
||||
"""
|
||||
获取水箱的数据结构模式。
|
||||
@@ -26,7 +26,7 @@ async def fast_get_tank_schema(network: str = Query(..., description="管网名
|
||||
"""
|
||||
return get_tank_schema(network)
|
||||
|
||||
@router.post("/addtank/", summary="新增水箱", description="向指定网络中新增一个水箱", response_model=None)
|
||||
@router.post("/tanks", summary="新增水箱", description="向指定网络中新增一个水箱", response_model=None)
|
||||
async def fastapi_add_tank(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID"),
|
||||
@@ -70,7 +70,7 @@ async def fastapi_add_tank(
|
||||
}
|
||||
return add_tank(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/deletetank/", summary="删除水箱", description="删除指定网络中的水箱", response_model=None)
|
||||
@router.delete("/tanks", summary="删除水箱", description="删除指定网络中的水箱", response_model=None)
|
||||
async def fastapi_delete_tank(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID")
|
||||
@@ -88,7 +88,7 @@ async def fastapi_delete_tank(
|
||||
ps = {"id": tank}
|
||||
return delete_tank(network, ChangeSet(ps))
|
||||
|
||||
@router.get("/gettankelevation/", summary="获取水箱标高", description="获取指定水箱的标高值")
|
||||
@router.get("/tanks/elevation", summary="获取水箱标高", description="获取指定水箱的标高值")
|
||||
async def fastapi_get_tank_elevation(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID")
|
||||
@@ -106,7 +106,7 @@ async def fastapi_get_tank_elevation(
|
||||
ps = get_tank(network, tank)
|
||||
return ps["elevation"]
|
||||
|
||||
@router.get("/gettankinitlevel/", summary="获取水箱初始水位", description="获取指定水箱的初始水位值")
|
||||
@router.get("/tanks/init-level", summary="获取水箱初始水位", description="获取指定水箱的初始水位值")
|
||||
async def fastapi_get_tank_init_level(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID")
|
||||
@@ -124,7 +124,7 @@ async def fastapi_get_tank_init_level(
|
||||
ps = get_tank(network, tank)
|
||||
return ps["init_level"]
|
||||
|
||||
@router.get("/gettankminlevel/", summary="获取水箱最小水位", description="获取指定水箱的最小水位值")
|
||||
@router.get("/tanks/min-level", summary="获取水箱最小水位", description="获取指定水箱的最小水位值")
|
||||
async def fastapi_get_tank_min_level(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID")
|
||||
@@ -142,7 +142,7 @@ async def fastapi_get_tank_min_level(
|
||||
ps = get_tank(network, tank)
|
||||
return ps["min_level"]
|
||||
|
||||
@router.get("/gettankmaxlevel/", summary="获取水箱最大水位", description="获取指定水箱的最大水位值")
|
||||
@router.get("/tanks/max-level", summary="获取水箱最大水位", description="获取指定水箱的最大水位值")
|
||||
async def fastapi_get_tank_max_level(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID")
|
||||
@@ -160,7 +160,7 @@ async def fastapi_get_tank_max_level(
|
||||
ps = get_tank(network, tank)
|
||||
return ps["max_level"]
|
||||
|
||||
@router.get("/gettankdiameter/", summary="获取水箱直径", description="获取指定水箱的直径值")
|
||||
@router.get("/tanks/diameter", summary="获取水箱直径", description="获取指定水箱的直径值")
|
||||
async def fastapi_get_tank_diameter(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID")
|
||||
@@ -178,7 +178,7 @@ async def fastapi_get_tank_diameter(
|
||||
ps = get_tank(network, tank)
|
||||
return ps["diameter"]
|
||||
|
||||
@router.get("/gettankminvol/", summary="获取水箱最小体积", description="获取指定水箱的最小体积值")
|
||||
@router.get("/tanks/min-vol", summary="获取水箱最小体积", description="获取指定水箱的最小体积值")
|
||||
async def fastapi_get_tank_min_vol(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID")
|
||||
@@ -196,7 +196,7 @@ async def fastapi_get_tank_min_vol(
|
||||
ps = get_tank(network, tank)
|
||||
return ps["min_vol"]
|
||||
|
||||
@router.get("/gettankvolcurve/", summary="获取水箱容积曲线", description="获取指定水箱的容积曲线标识")
|
||||
@router.get("/tanks/vol-curve", summary="获取水箱容积曲线", description="获取指定水箱的容积曲线标识")
|
||||
async def fastapi_get_tank_vol_curve(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID")
|
||||
@@ -214,7 +214,7 @@ async def fastapi_get_tank_vol_curve(
|
||||
ps = get_tank(network, tank)
|
||||
return ps["vol_curve"]
|
||||
|
||||
@router.get("/gettankoverflow/", summary="获取水箱溢流口", description="获取指定水箱的溢流口配置")
|
||||
@router.get("/tanks/overflow", summary="获取水箱溢流口", description="获取指定水箱的溢流口配置")
|
||||
async def fastapi_get_tank_overflow(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID")
|
||||
@@ -232,7 +232,7 @@ async def fastapi_get_tank_overflow(
|
||||
ps = get_tank(network, tank)
|
||||
return ps["overflow"]
|
||||
|
||||
@router.get("/gettankx/", summary="获取水箱X坐标", description="获取指定水箱的X坐标值")
|
||||
@router.get("/tanks/x", summary="获取水箱X坐标", description="获取指定水箱的X坐标值")
|
||||
async def fastapi_get_tank_x(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID")
|
||||
@@ -250,7 +250,7 @@ async def fastapi_get_tank_x(
|
||||
ps = get_tank(network, tank)
|
||||
return ps["x"]
|
||||
|
||||
@router.get("/gettanky/", summary="获取水箱Y坐标", description="获取指定水箱的Y坐标值")
|
||||
@router.get("/tanks/y", summary="获取水箱Y坐标", description="获取指定水箱的Y坐标值")
|
||||
async def fastapi_get_tank_y(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID")
|
||||
@@ -268,7 +268,7 @@ async def fastapi_get_tank_y(
|
||||
ps = get_tank(network, tank)
|
||||
return ps["y"]
|
||||
|
||||
@router.get("/gettankcoord/", summary="获取水箱坐标", description="获取指定水箱的X和Y坐标")
|
||||
@router.get("/tanks/coord", summary="获取水箱坐标", description="获取指定水箱的X和Y坐标")
|
||||
async def fastapi_get_tank_coord(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID")
|
||||
@@ -287,7 +287,7 @@ async def fastapi_get_tank_coord(
|
||||
coord = {"x": ps["x"], "y": ps["y"]}
|
||||
return coord
|
||||
|
||||
@router.post("/settankelevation/", summary="设置水箱标高", description="设置指定水箱的标高值", response_model=None)
|
||||
@router.patch("/tanks/elevation", summary="设置水箱标高", description="设置指定水箱的标高值", response_model=None)
|
||||
async def fastapi_set_tank_elevation(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID"),
|
||||
@@ -307,7 +307,7 @@ async def fastapi_set_tank_elevation(
|
||||
ps = {"id": tank, "elevation": elevation}
|
||||
return set_tank(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/settankinitlevel/", summary="设置水箱初始水位", description="设置指定水箱的初始水位值", response_model=None)
|
||||
@router.patch("/tanks/init-level", summary="设置水箱初始水位", description="设置指定水箱的初始水位值", response_model=None)
|
||||
async def fastapi_set_tank_init_level(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID"),
|
||||
@@ -327,7 +327,7 @@ async def fastapi_set_tank_init_level(
|
||||
ps = {"id": tank, "init_level": init_level}
|
||||
return set_tank(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/settankminlevel/", summary="设置水箱最小水位", description="设置指定水箱的最小水位值", response_model=None)
|
||||
@router.patch("/tanks/min-level", summary="设置水箱最小水位", description="设置指定水箱的最小水位值", response_model=None)
|
||||
async def fastapi_set_tank_min_level(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID"),
|
||||
@@ -347,7 +347,7 @@ async def fastapi_set_tank_min_level(
|
||||
ps = {"id": tank, "min_level": min_level}
|
||||
return set_tank(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/settankmaxlevel/", summary="设置水箱最大水位", description="设置指定水箱的最大水位值", response_model=None)
|
||||
@router.patch("/tanks/max-level", summary="设置水箱最大水位", description="设置指定水箱的最大水位值", response_model=None)
|
||||
async def fastapi_set_tank_max_level(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID"),
|
||||
@@ -367,7 +367,7 @@ async def fastapi_set_tank_max_level(
|
||||
ps = {"id": tank, "max_level": max_level}
|
||||
return set_tank(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/settankdiameter/", summary="设置水箱直径", description="设置指定水箱的直径值", response_model=None)
|
||||
@router.patch("/tanks/diameter", summary="设置水箱直径", description="设置指定水箱的直径值", response_model=None)
|
||||
async def fastapi_set_tank_diameter(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID"),
|
||||
@@ -387,7 +387,7 @@ async def fastapi_set_tank_diameter(
|
||||
ps = {"id": tank, "diameter": diameter}
|
||||
return set_tank(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/settankminvol/", summary="设置水箱最小体积", description="设置指定水箱的最小体积值", response_model=None)
|
||||
@router.patch("/tanks/min-vol", summary="设置水箱最小体积", description="设置指定水箱的最小体积值", response_model=None)
|
||||
async def fastapi_set_tank_min_vol(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID"),
|
||||
@@ -407,7 +407,7 @@ async def fastapi_set_tank_min_vol(
|
||||
ps = {"id": tank, "min_vol": min_vol}
|
||||
return set_tank(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/settankvolcurve/", summary="设置水箱容积曲线", description="设置指定水箱的容积曲线标识", response_model=None)
|
||||
@router.patch("/tanks/vol-curve", summary="设置水箱容积曲线", description="设置指定水箱的容积曲线标识", response_model=None)
|
||||
async def fastapi_set_tank_vol_curve(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID"),
|
||||
@@ -427,7 +427,7 @@ async def fastapi_set_tank_vol_curve(
|
||||
ps = {"id": tank, "vol_curve": vol_curve}
|
||||
return set_tank(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/settankoverflow/", summary="设置水箱溢流口", description="设置指定水箱的溢流口配置", response_model=None)
|
||||
@router.patch("/tanks/overflow", summary="设置水箱溢流口", description="设置指定水箱的溢流口配置", response_model=None)
|
||||
async def fastapi_set_tank_overflow(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID"),
|
||||
@@ -447,7 +447,7 @@ async def fastapi_set_tank_overflow(
|
||||
ps = {"id": tank, "overflow": overflow}
|
||||
return set_tank(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/settankx/", summary="设置水箱X坐标", description="设置指定水箱的X坐标值", response_model=None)
|
||||
@router.patch("/tanks/x", summary="设置水箱X坐标", description="设置指定水箱的X坐标值", response_model=None)
|
||||
async def fastapi_set_tank_x(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID"),
|
||||
@@ -467,7 +467,7 @@ async def fastapi_set_tank_x(
|
||||
ps = {"id": tank, "x": x}
|
||||
return set_tank(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/settanky/", summary="设置水箱Y坐标", description="设置指定水箱的Y坐标值", response_model=None)
|
||||
@router.patch("/tanks/y", summary="设置水箱Y坐标", description="设置指定水箱的Y坐标值", response_model=None)
|
||||
async def fastapi_set_tank_y(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID"),
|
||||
@@ -487,7 +487,7 @@ async def fastapi_set_tank_y(
|
||||
ps = {"id": tank, "y": y}
|
||||
return set_tank(network, ChangeSet(ps))
|
||||
|
||||
@router.post("/settankcoord/", summary="设置水箱坐标", description="设置指定水箱的X和Y坐标", response_model=None)
|
||||
@router.patch("/tanks/coord", summary="设置水箱坐标", description="设置指定水箱的X和Y坐标", response_model=None)
|
||||
async def fastapi_set_tank_coord(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID"),
|
||||
@@ -509,7 +509,7 @@ async def fastapi_set_tank_coord(
|
||||
ps = {"id": tank, "x": x, "y": y}
|
||||
return set_tank(network, ChangeSet(ps))
|
||||
|
||||
@router.get("/gettankproperties/", summary="获取水箱属性", description="获取指定水箱的所有属性")
|
||||
@router.get("/tanks/properties", summary="获取水箱属性", description="获取指定水箱的所有属性")
|
||||
async def fastapi_get_tank_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID")
|
||||
@@ -526,7 +526,7 @@ async def fastapi_get_tank_properties(
|
||||
"""
|
||||
return get_tank(network, tank)
|
||||
|
||||
@router.get("/getalltankproperties/", summary="获取所有水箱属性", description="获取指定网络中所有水箱的属性")
|
||||
@router.get("/tanks", summary="获取所有水箱属性", description="获取指定网络中所有水箱的属性")
|
||||
async def fastapi_get_all_tank_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
) -> list[dict[str, Any]]:
|
||||
@@ -544,7 +544,7 @@ async def fastapi_get_all_tank_properties(
|
||||
results = get_all_tanks(network)
|
||||
return results
|
||||
|
||||
@router.post("/settankproperties/", summary="设置水箱属性", description="批量设置指定水箱的多个属性", response_model=None)
|
||||
@router.patch("/tanks/properties", summary="设置水箱属性", description="批量设置指定水箱的多个属性", response_model=None)
|
||||
async def fastapi_set_tank_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
tank: str = Query(..., description="水箱ID"),
|
||||
|
||||
@@ -15,7 +15,7 @@ from app.services.tjnetwork import (
|
||||
router = APIRouter()
|
||||
|
||||
@router.get(
|
||||
"/getvalveschema",
|
||||
"/network-schemas/valve",
|
||||
summary="获取阀门架构",
|
||||
description="获取指定水网中所有阀门的架构和字段定义",
|
||||
)
|
||||
@@ -30,7 +30,7 @@ async def fastapi_get_valve_schema(
|
||||
return get_valve_schema(network)
|
||||
|
||||
@router.post(
|
||||
"/addvalve/",
|
||||
"/valves",
|
||||
response_model=None,
|
||||
summary="添加阀门",
|
||||
description="在指定的水网中添加新的阀门",
|
||||
@@ -62,8 +62,8 @@ async def fastapi_add_valve(
|
||||
|
||||
return add_valve(network, ChangeSet(ps))
|
||||
|
||||
@router.post(
|
||||
"/deletevalve/",
|
||||
@router.delete(
|
||||
"/valves",
|
||||
response_model=None,
|
||||
summary="删除阀门",
|
||||
description="从指定的水网中删除指定的阀门",
|
||||
@@ -81,7 +81,7 @@ async def fastapi_delete_valve(
|
||||
return delete_valve(network, ChangeSet(ps))
|
||||
|
||||
@router.get(
|
||||
"/getvalvenode1/",
|
||||
"/valves/node1",
|
||||
summary="获取阀门起点节点",
|
||||
description="获取指定阀门连接的起点节点ID",
|
||||
)
|
||||
@@ -98,7 +98,7 @@ async def fastapi_get_valve_node1(
|
||||
return ps["node1"]
|
||||
|
||||
@router.get(
|
||||
"/getvalvenode2/",
|
||||
"/valves/node2",
|
||||
summary="获取阀门终点节点",
|
||||
description="获取指定阀门连接的终点节点ID",
|
||||
)
|
||||
@@ -115,7 +115,7 @@ async def fastapi_get_valve_node2(
|
||||
return ps["node2"]
|
||||
|
||||
@router.get(
|
||||
"/getvalvediameter/",
|
||||
"/valves/diameter",
|
||||
summary="获取阀门直径",
|
||||
description="获取指定阀门的直径",
|
||||
)
|
||||
@@ -132,7 +132,7 @@ async def fastapi_get_valve_diameter(
|
||||
return ps["diameter"]
|
||||
|
||||
@router.get(
|
||||
"/getvalvetype/",
|
||||
"/valves/type",
|
||||
summary="获取阀门类型",
|
||||
description="获取指定阀门的类型",
|
||||
)
|
||||
@@ -149,7 +149,7 @@ async def fastapi_get_valve_type(
|
||||
return ps["type"]
|
||||
|
||||
@router.get(
|
||||
"/getvalvesetting/",
|
||||
"/valves/setting",
|
||||
summary="获取阀门开度",
|
||||
description="获取指定阀门的开度/设置值",
|
||||
)
|
||||
@@ -166,7 +166,7 @@ async def fastapi_get_valve_setting(
|
||||
return ps["setting"]
|
||||
|
||||
@router.get(
|
||||
"/getvalveminorloss/",
|
||||
"/valves/minor-loss",
|
||||
summary="获取阀门损失系数",
|
||||
description="获取指定阀门的损失系数",
|
||||
)
|
||||
@@ -182,8 +182,8 @@ async def fastapi_get_valve_minor_loss(
|
||||
ps = get_valve(network, valve)
|
||||
return ps["minor_loss"]
|
||||
|
||||
@router.post(
|
||||
"/setvalvenode1/",
|
||||
@router.patch(
|
||||
"/valves/node1",
|
||||
response_model=None,
|
||||
summary="设置阀门起点节点",
|
||||
description="设置指定阀门的起点节点",
|
||||
@@ -201,8 +201,8 @@ async def fastapi_set_valve_node1(
|
||||
ps = {"id": valve, "node1": node1}
|
||||
return set_valve(network, ChangeSet(ps))
|
||||
|
||||
@router.post(
|
||||
"/setvalvenode2/",
|
||||
@router.patch(
|
||||
"/valves/node2",
|
||||
response_model=None,
|
||||
summary="设置阀门终点节点",
|
||||
description="设置指定阀门的终点节点",
|
||||
@@ -220,8 +220,8 @@ async def fastapi_set_valve_node2(
|
||||
ps = {"id": valve, "node2": node2}
|
||||
return set_valve(network, ChangeSet(ps))
|
||||
|
||||
@router.post(
|
||||
"/setvalvenodediameter/",
|
||||
@router.patch(
|
||||
"/valves/diameter",
|
||||
response_model=None,
|
||||
summary="设置阀门直径",
|
||||
description="设置指定阀门的直径",
|
||||
@@ -239,8 +239,8 @@ async def fastapi_set_valve_diameter(
|
||||
ps = {"id": valve, "diameter": diameter}
|
||||
return set_valve(network, ChangeSet(ps))
|
||||
|
||||
@router.post(
|
||||
"/setvalvetype/",
|
||||
@router.patch(
|
||||
"/valves/type",
|
||||
response_model=None,
|
||||
summary="设置阀门类型",
|
||||
description="设置指定阀门的类型",
|
||||
@@ -258,8 +258,8 @@ async def fastapi_set_valve_type(
|
||||
ps = {"id": valve, "type": type}
|
||||
return set_valve(network, ChangeSet(ps))
|
||||
|
||||
@router.post(
|
||||
"/setvalvesetting/",
|
||||
@router.patch(
|
||||
"/valves/setting",
|
||||
response_model=None,
|
||||
summary="设置阀门开度",
|
||||
description="设置指定阀门的开度/设置值",
|
||||
@@ -278,7 +278,7 @@ async def fastapi_set_valve_setting(
|
||||
return set_valve(network, ChangeSet(ps))
|
||||
|
||||
@router.get(
|
||||
"/getvalveproperties/",
|
||||
"/valves/properties",
|
||||
summary="获取阀门所有属性",
|
||||
description="获取指定阀门的所有属性",
|
||||
)
|
||||
@@ -294,7 +294,7 @@ async def fastapi_get_valve_properties(
|
||||
return get_valve(network, valve)
|
||||
|
||||
@router.get(
|
||||
"/getallvalveproperties/",
|
||||
"/valves",
|
||||
summary="获取所有阀门属性",
|
||||
description="获取指定水网中所有阀门的属性",
|
||||
)
|
||||
@@ -311,8 +311,8 @@ async def fastapi_get_all_valve_properties(
|
||||
results = get_all_valves(network)
|
||||
return results
|
||||
|
||||
@router.post(
|
||||
"/setvalveproperties/",
|
||||
@router.patch(
|
||||
"/valves/properties",
|
||||
response_model=None,
|
||||
summary="批量设置阀门属性",
|
||||
description="批量设置指定阀门的多个属性",
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import json
|
||||
from fastapi import APIRouter, Request, HTTPException, Query, Path, Body, Depends
|
||||
from fastapi import APIRouter, Request, HTTPException, Query, Path, Depends
|
||||
from fastapi.responses import PlainTextResponse
|
||||
from typing import Any, Dict, List
|
||||
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
||||
from app.auth.project_dependencies import get_metadata_repository
|
||||
from app.auth.permissions import (
|
||||
ENVIRONMENT_MANAGE,
|
||||
require_permission,
|
||||
)
|
||||
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
|
||||
@@ -18,7 +22,6 @@ from app.services.tjnetwork import (
|
||||
open_project,
|
||||
close_project,
|
||||
copy_project,
|
||||
import_inp,
|
||||
export_inp,
|
||||
read_inp,
|
||||
dump_inp,
|
||||
@@ -42,8 +45,7 @@ inpDir = "data/" # Assuming data directory exists or is defined somewhere.
|
||||
router = APIRouter()
|
||||
lockedPrjs: Dict[str, str] = {}
|
||||
|
||||
@router.get("/project-info", summary="获取项目信息", description="从数据库获取项目的详细信息,包括地图范围等。", response_model=ProjectMetaResponse)
|
||||
@router.get("/project_info/", summary="获取项目信息(旧路径)", description="从数据库获取项目的详细信息,包括地图范围等。", response_model=ProjectMetaResponse, deprecated=True)
|
||||
@router.get("/projects/current", summary="获取项目信息", description="从数据库获取项目的详细信息,包括地图范围等。", response_model=ProjectMetaResponse)
|
||||
async def get_project_info_endpoint(
|
||||
network: str = Query(..., description="管网名称(或项目代码)"),
|
||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||
@@ -67,7 +69,7 @@ async def get_project_info_endpoint(
|
||||
project_role="viewer", # Default role for public access
|
||||
)
|
||||
|
||||
@router.get("/listprojects/", summary="获取项目列表", description="获取服务器上所有可用的供水管网项目名称列表。")
|
||||
@router.get("/project-codes", summary="获取项目列表", description="获取服务器上所有可用的供水管网项目名称列表。")
|
||||
async def list_projects_endpoint() -> list[str]:
|
||||
"""
|
||||
获取项目列表
|
||||
@@ -76,7 +78,7 @@ async def list_projects_endpoint() -> list[str]:
|
||||
"""
|
||||
return list_project()
|
||||
|
||||
@router.get("/haveproject/", summary="检查项目是否存在", description="检查指定名称的项目是否存在。")
|
||||
@router.get("/projects/existence", summary="检查项目是否存在", description="检查指定名称的项目是否存在。")
|
||||
async def have_project_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
):
|
||||
@@ -87,9 +89,10 @@ async def have_project_endpoint(
|
||||
"""
|
||||
return have_project(network)
|
||||
|
||||
@router.post("/createproject/", summary="创建新项目", description="创建一个新的供水管网项目。如果项目已存在,可能会覆盖或报错(取决于底层实现)。")
|
||||
@router.post("/projects", summary="创建新项目", description="创建一个新的供水管网项目。如果项目已存在,可能会覆盖或报错(取决于底层实现)。")
|
||||
async def create_project_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
_=Depends(require_permission(ENVIRONMENT_MANAGE)),
|
||||
):
|
||||
"""
|
||||
创建新项目
|
||||
@@ -99,9 +102,10 @@ async def create_project_endpoint(
|
||||
create_project(network)
|
||||
return network
|
||||
|
||||
@router.post("/deleteproject/", summary="删除项目", description="永久删除指定的供水管网项目。此操作不可恢复。")
|
||||
@router.delete("/projects", summary="删除项目", description="永久删除指定的供水管网项目。此操作不可恢复。")
|
||||
async def delete_project_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
_=Depends(require_permission(ENVIRONMENT_MANAGE)),
|
||||
):
|
||||
"""
|
||||
删除项目
|
||||
@@ -111,7 +115,7 @@ async def delete_project_endpoint(
|
||||
delete_project(network)
|
||||
return True
|
||||
|
||||
@router.get("/isprojectopen/", summary="检查项目是否已打开", description="检查指定项目是否已被加载到内存中。")
|
||||
@router.get("/projects/current/status", summary="检查项目是否已打开", description="检查指定项目是否已被加载到内存中。")
|
||||
async def is_project_open_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
):
|
||||
@@ -122,8 +126,7 @@ async def is_project_open_endpoint(
|
||||
"""
|
||||
return is_project_open(network)
|
||||
|
||||
@router.post("/projects/open", summary="打开项目", description="将指定项目加载到内存中,并初始化数据库连接池。")
|
||||
@router.post("/openproject/", summary="打开项目(旧路径)", description="将指定项目加载到内存中,并初始化数据库连接池。", deprecated=True)
|
||||
@router.post("/projects/current", summary="打开项目", description="将指定项目加载到内存中,并初始化数据库连接池。")
|
||||
async def open_project_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
):
|
||||
@@ -157,7 +160,7 @@ async def open_project_endpoint(
|
||||
|
||||
return network
|
||||
|
||||
@router.post("/closeproject/", summary="关闭项目", description="将指定项目从内存中卸载,释放资源。")
|
||||
@router.delete("/projects/current", summary="关闭项目", description="将指定项目从内存中卸载,释放资源。")
|
||||
async def close_project_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
):
|
||||
@@ -169,10 +172,11 @@ async def close_project_endpoint(
|
||||
close_project(network)
|
||||
return True
|
||||
|
||||
@router.post("/copyproject/", summary="复制项目", description="将现有项目复制为新项目。")
|
||||
@router.post("/project-copies", summary="复制项目", description="将现有项目复制为新项目。")
|
||||
async def copy_project_endpoint(
|
||||
source: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
target: str = Query(..., description="管网名称(或数据库名称)")
|
||||
target: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
_=Depends(require_permission(ENVIRONMENT_MANAGE)),
|
||||
):
|
||||
"""
|
||||
复制项目
|
||||
@@ -183,25 +187,7 @@ async def copy_project_endpoint(
|
||||
copy_project(source, target)
|
||||
return True
|
||||
|
||||
@router.post("/importinp/", summary="导入 INP 文件内容", description="将 INP 格式的文本内容导入到指定项目中。")
|
||||
async def import_inp_endpoint(
|
||||
req: Request,
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
):
|
||||
"""
|
||||
导入 INP 文件内容
|
||||
|
||||
- **network**: 管网名称(或数据库名称)
|
||||
- **req**: 请求体,需包含 `{"inp": "..."}` 结构
|
||||
"""
|
||||
jo_root = await req.json()
|
||||
inp_text = jo_root["inp"]
|
||||
ps = {"inp": inp_text}
|
||||
ret = import_inp(network, ChangeSet(ps))
|
||||
print(ret)
|
||||
return ret
|
||||
|
||||
@router.get("/exportinp/", 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(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
version: str = Query(..., description="版本号 (通常用于增量更新)")
|
||||
@@ -234,7 +220,7 @@ async def export_inp_endpoint(
|
||||
|
||||
return cs
|
||||
|
||||
@router.post("/readinp/", summary="读取 INP 文件到项目", description="从服务器文件系统中读取指定的 INP 文件并加载到项目中。")
|
||||
@router.post("/projects/current/imports", summary="读取 INP 文件到项目", description="从服务器文件系统中读取指定的 INP 文件并加载到项目中。")
|
||||
async def read_inp_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
inp: str = Query(..., description="INP 文件名 (不包含路径)")
|
||||
@@ -248,7 +234,7 @@ async def read_inp_endpoint(
|
||||
read_inp(network, inp)
|
||||
return True
|
||||
|
||||
@router.get("/dumpinp/", summary="导出项目到 INP 文件", description="将项目当前状态保存为 INP 文件到服务器文件系统。")
|
||||
@router.post("/projects/current/exports/inp", summary="导出项目到 INP 文件", description="将项目当前状态保存为 INP 文件到服务器文件系统。")
|
||||
async def dump_inp_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
inp: str = Query(..., description="目标文件名")
|
||||
@@ -262,7 +248,7 @@ async def dump_inp_endpoint(
|
||||
dump_inp(network, inp)
|
||||
return True
|
||||
|
||||
@router.get("/isprojectlocked/", summary="检查项目是否被锁定", description="检查指定项目是否处于锁定状态。")
|
||||
@router.get("/projects/current/lock", summary="检查项目是否被锁定", description="检查指定项目是否处于锁定状态。")
|
||||
async def is_project_locked_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -274,7 +260,7 @@ async def is_project_locked_endpoint(
|
||||
"""
|
||||
return network in lockedPrjs.keys()
|
||||
|
||||
@router.get("/isprojectlockedbyme/", summary="检查项目是否被当前用户锁定", description="检查指定项目是否被当前客户端 (IP) 锁定。")
|
||||
@router.get("/projects/current/lock/ownership", summary="检查项目是否被当前用户锁定", description="检查指定项目是否被当前访问地址 (IP) 锁定。")
|
||||
async def is_project_locked_by_me_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -290,7 +276,7 @@ async def is_project_locked_by_me_endpoint(
|
||||
# 0 successfully locked
|
||||
# 1 already locked by you
|
||||
# 2 locked by others
|
||||
@router.post("/lockproject/", summary="锁定项目", description="锁定指定项目以防止并发修改。")
|
||||
@router.post("/projects/current/lock", summary="锁定项目", description="锁定指定项目以防止并发修改。")
|
||||
async def lock_project_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -313,7 +299,7 @@ async def lock_project_endpoint(
|
||||
else:
|
||||
return 2
|
||||
|
||||
@router.post("/unlockproject/", summary="解锁项目", description="释放对项目的锁定。")
|
||||
@router.delete("/projects/current/lock", summary="解锁项目", description="释放对项目的锁定。")
|
||||
def unlock_project_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -331,27 +317,7 @@ def unlock_project_endpoint(
|
||||
|
||||
return False
|
||||
|
||||
# inp file operations
|
||||
@router.post("/uploadinp/", status_code=status.HTTP_200_OK, summary="上传 INP 文件", description="上传 INP 文件到服务器数据目录。")
|
||||
async def fastapi_upload_inp(
|
||||
afile: bytes = Body(..., description="文件二进制内容"),
|
||||
name: str = Query(..., description="保存的文件名")
|
||||
):
|
||||
"""
|
||||
上传 INP 文件
|
||||
|
||||
- **afile**: 文件内容
|
||||
- **name**: 文件名
|
||||
"""
|
||||
if not os.path.exists(inpDir):
|
||||
os.makedirs(inpDir, exist_ok=True)
|
||||
|
||||
filePath = inpDir + str(name)
|
||||
with open(filePath, "wb") as f:
|
||||
f.write(afile)
|
||||
return True
|
||||
|
||||
@router.get("/downloadinp/", status_code=status.HTTP_200_OK, summary="下载 INP 文件", description="从服务器数据目录下载指定的 INP 文件。")
|
||||
@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
|
||||
@@ -371,7 +337,7 @@ async def fastapi_download_inp(
|
||||
return True
|
||||
|
||||
# DingZQ, 2024-12-28, convert v3 to v2
|
||||
@router.get("/convertv3tov2/", response_model=None, summary="转换 INP V3 为 V2", description="将 EPANET 3.0 格式的 INP 内容转换为 2.x 格式。")
|
||||
@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:
|
||||
@@ -405,7 +371,6 @@ async def fastapi_convert_v3_to_v2(
|
||||
|
||||
return cs
|
||||
|
||||
@router.post("/readinp/", summary="读取 INP 文件到项目", description="从服务器文件系统中读取指定的 INP 文件并加载到项目中。")
|
||||
async def read_inp_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
inp: str = Query(..., description="INP 文件名 (不包含路径)")
|
||||
@@ -419,7 +384,6 @@ async def read_inp_endpoint(
|
||||
read_inp(network, inp)
|
||||
return True
|
||||
|
||||
@router.get("/dumpinp/", summary="导出项目到 INP 文件", description="将项目当前状态保存为 INP 文件到服务器文件系统。")
|
||||
async def dump_inp_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
inp: str = Query(..., description="目标文件名")
|
||||
@@ -433,7 +397,6 @@ async def dump_inp_endpoint(
|
||||
dump_inp(network, inp)
|
||||
return True
|
||||
|
||||
@router.get("/isprojectlocked/", summary="检查项目是否被锁定", description="检查指定项目是否处于锁定状态。")
|
||||
async def is_project_locked_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -445,7 +408,6 @@ async def is_project_locked_endpoint(
|
||||
"""
|
||||
return network in lockedPrjs.keys()
|
||||
|
||||
@router.get("/isprojectlockedbyme/", summary="检查项目是否被当前用户锁定", description="检查指定项目是否被当前客户端 (IP) 锁定。")
|
||||
async def is_project_locked_by_me_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -461,7 +423,6 @@ async def is_project_locked_by_me_endpoint(
|
||||
# 0 successfully locked
|
||||
# 1 already locked by you
|
||||
# 2 locked by others
|
||||
@router.post("/lockproject/", summary="锁定项目", description="锁定指定项目以防止并发修改。")
|
||||
async def lock_project_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -484,7 +445,6 @@ async def lock_project_endpoint(
|
||||
else:
|
||||
return 2
|
||||
|
||||
@router.post("/unlockproject/", summary="解锁项目", description="释放对项目的锁定。")
|
||||
def unlock_project_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -502,27 +462,6 @@ def unlock_project_endpoint(
|
||||
|
||||
return False
|
||||
|
||||
# inp file operations
|
||||
@router.post("/uploadinp/", status_code=status.HTTP_200_OK, summary="上传 INP 文件", description="上传 INP 文件到服务器数据目录。")
|
||||
async def fastapi_upload_inp(
|
||||
afile: bytes = Body(..., description="文件二进制内容"),
|
||||
name: str = Query(..., description="保存的文件名")
|
||||
):
|
||||
"""
|
||||
上传 INP 文件
|
||||
|
||||
- **afile**: 文件内容
|
||||
- **name**: 文件名
|
||||
"""
|
||||
if not os.path.exists(inpDir):
|
||||
os.makedirs(inpDir, exist_ok=True)
|
||||
|
||||
filePath = inpDir + str(name)
|
||||
with open(filePath, "wb") as f:
|
||||
f.write(afile)
|
||||
return True
|
||||
|
||||
@router.get("/downloadinp/", status_code=status.HTTP_200_OK, summary="下载 INP 文件", description="从服务器数据目录下载指定的 INP 文件。")
|
||||
async def fastapi_download_inp(
|
||||
name: str = Query(..., description="文件名"),
|
||||
response: Response = None
|
||||
@@ -542,7 +481,6 @@ async def fastapi_download_inp(
|
||||
return True
|
||||
|
||||
# DingZQ, 2024-12-28, convert v3 to v2
|
||||
@router.get("/convertv3tov2/", response_model=None, summary="转换 INP V3 为 V2", description="将 EPANET 3.0 格式的 INP 内容转换为 2.x 格式。")
|
||||
async def fastapi_convert_v3_to_v2(
|
||||
req: Request
|
||||
) -> ChangeSet:
|
||||
|
||||
@@ -15,7 +15,7 @@ async def get_database_connection(
|
||||
yield conn
|
||||
|
||||
|
||||
@router.get("/scada-info", summary="获取SCADA信息", description="使用连接池查询所有SCADA信息")
|
||||
@router.get("/scada-info/database-view", summary="获取SCADA信息", description="使用连接池查询所有SCADA信息")
|
||||
async def get_scada_info_with_connection(
|
||||
conn: AsyncConnection = Depends(get_database_connection),
|
||||
):
|
||||
@@ -33,7 +33,7 @@ async def get_scada_info_with_connection(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/scheme-list", summary="获取方案列表", description="使用连接池查询所有方案信息")
|
||||
@router.get("/schemes/list-with-connection", summary="获取方案列表", description="使用连接池查询所有方案信息")
|
||||
async def get_scheme_list_with_connection(
|
||||
conn: AsyncConnection = Depends(get_database_connection),
|
||||
):
|
||||
@@ -49,7 +49,7 @@ async def get_scheme_list_with_connection(
|
||||
raise HTTPException(status_code=500, detail=f"查询方案信息时发生错误: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/burst-locate-result", summary="获取爆管定位结果", description="使用连接池查询所有爆管定位结果")
|
||||
@router.get("/burst-locations/database-view", summary="获取爆管定位结果", description="使用连接池查询所有爆管定位结果")
|
||||
async def get_burst_locate_result_with_connection(
|
||||
conn: AsyncConnection = Depends(get_database_connection),
|
||||
):
|
||||
@@ -67,7 +67,7 @@ async def get_burst_locate_result_with_connection(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/burst-locate-result/{burst_incident}", summary="按事件查询爆管定位结果", description="根据爆管事件ID查询对应的爆管定位结果")
|
||||
@router.get("/burst-locations/{burst_incident}", summary="按事件查询爆管定位结果", description="根据爆管事件ID查询对应的爆管定位结果")
|
||||
async def get_burst_locate_result_by_incident(
|
||||
burst_incident: str = Path(..., description="爆管事件ID"),
|
||||
conn: AsyncConnection = Depends(get_database_connection),
|
||||
|
||||
@@ -11,7 +11,7 @@ from app.services.tjnetwork import (
|
||||
router = APIRouter()
|
||||
|
||||
@router.get(
|
||||
"/getpiperiskprobabilitynow/",
|
||||
"/pipes/risk-probability-now",
|
||||
summary="获取管道当前风险概率",
|
||||
description="获取指定管道当前时刻的风险概率值"
|
||||
)
|
||||
@@ -35,7 +35,7 @@ async def fastapi_get_pipe_risk_probability_now(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/getpiperiskprobability/",
|
||||
"/pipes/risk-probability",
|
||||
summary="获取管道风险概率历史",
|
||||
description="获取指定管道的风险概率历史数据"
|
||||
)
|
||||
@@ -59,7 +59,7 @@ async def fastapi_get_pipe_risk_probability(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/getpipesriskprobability/",
|
||||
"/pipes-risk-probabilities",
|
||||
summary="批量获取多条管道风险概率",
|
||||
description="批量获取多条管道的风险概率值"
|
||||
)
|
||||
@@ -84,7 +84,7 @@ async def fastapi_get_pipes_risk_probability(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/getnetworkpiperiskprobabilitynow/",
|
||||
"/network-pipe-risk-probability-nows",
|
||||
summary="获取整个网络的管道风险概率",
|
||||
description="获取指定网络中所有管道的当前风险概率值"
|
||||
)
|
||||
@@ -106,7 +106,7 @@ async def fastapi_get_network_pipe_risk_probability_now(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/getpiperiskprobabilitygeometries/",
|
||||
"/pipes/risk-probability-geometries",
|
||||
summary="获取管道风险几何信息",
|
||||
description="获取指定网络中管道的风险相关几何数据"
|
||||
)
|
||||
|
||||
@@ -31,7 +31,6 @@ from app.services.tjnetwork import (
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/getscadaproperties/", summary="获取SCADA属性", tags=["SCADA基础"])
|
||||
async def fast_get_scada_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
scada: str = Query(..., description="SCADA设备ID")
|
||||
@@ -50,7 +49,6 @@ async def fast_get_scada_properties(
|
||||
"""
|
||||
return get_scada_info(network, scada)
|
||||
|
||||
@router.get("/getallscadaproperties/", summary="获取所有SCADA属性", tags=["SCADA基础"])
|
||||
async def fast_get_all_scada_properties(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
) -> list[dict[str, Any]]:
|
||||
@@ -72,7 +70,7 @@ async def fast_get_all_scada_properties(
|
||||
# scada_device 设备管理
|
||||
############################################################
|
||||
|
||||
@router.get("/getscadadeviceschema/", summary="获取SCADA设备架构", tags=["SCADA设备"])
|
||||
@router.get("/network-schemas/scada-device", summary="获取SCADA设备架构", tags=["SCADA设备"])
|
||||
async def fastapi_get_scada_device_schema(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
@@ -89,7 +87,7 @@ async def fastapi_get_scada_device_schema(
|
||||
"""
|
||||
return get_scada_device_schema(network)
|
||||
|
||||
@router.get("/getscadadevice/", summary="获取SCADA设备", tags=["SCADA设备"])
|
||||
@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")
|
||||
@@ -108,7 +106,7 @@ async def fastapi_get_scada_device(
|
||||
"""
|
||||
return get_scada_device(network, id)
|
||||
|
||||
@router.post("/setscadadevice/", response_model=None, summary="更新SCADA设备", tags=["SCADA设备"])
|
||||
@router.patch("/scada-devices", response_model=None, summary="更新SCADA设备", tags=["SCADA设备"])
|
||||
async def fastapi_set_scada_device(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -128,7 +126,7 @@ async def fastapi_set_scada_device(
|
||||
props = await req.json()
|
||||
return set_scada_device(network, ChangeSet(props))
|
||||
|
||||
@router.post("/addscadadevice/", response_model=None, summary="添加SCADA设备", tags=["SCADA设备"])
|
||||
@router.post("/scada-devices", response_model=None, summary="添加SCADA设备", tags=["SCADA设备"])
|
||||
async def fastapi_add_scada_device(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -148,7 +146,7 @@ async def fastapi_add_scada_device(
|
||||
props = await req.json()
|
||||
return add_scada_device(network, ChangeSet(props))
|
||||
|
||||
@router.post("/deletescadadevice/", response_model=None, summary="删除SCADA设备", tags=["SCADA设备"])
|
||||
@router.delete("/scada-devices", response_model=None, summary="删除SCADA设备", tags=["SCADA设备"])
|
||||
async def fastapi_delete_scada_device(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -168,7 +166,7 @@ async def fastapi_delete_scada_device(
|
||||
props = await req.json()
|
||||
return delete_scada_device(network, ChangeSet(props))
|
||||
|
||||
@router.post("/cleanscadadevice/", response_model=None, summary="清空SCADA设备表", tags=["SCADA设备"])
|
||||
@router.post("/scada-device-cleaning-runs", response_model=None, summary="清空SCADA设备表", tags=["SCADA设备"])
|
||||
async def fastapi_clean_scada_device(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
) -> ChangeSet:
|
||||
@@ -185,7 +183,7 @@ async def fastapi_clean_scada_device(
|
||||
"""
|
||||
return clean_scada_device(network)
|
||||
|
||||
@router.get("/getallscadadeviceids/", summary="获取所有SCADA设备ID", tags=["SCADA设备"])
|
||||
@router.get("/scada-devices/ids", summary="获取所有SCADA设备ID", tags=["SCADA设备"])
|
||||
async def fastapi_get_all_scada_device_ids(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
) -> list[str]:
|
||||
@@ -200,7 +198,7 @@ async def fastapi_get_all_scada_device_ids(
|
||||
"""
|
||||
return get_all_scada_device_ids(network)
|
||||
|
||||
@router.get("/getallscadadevices/", summary="获取所有SCADA设备", tags=["SCADA设备"])
|
||||
@router.get("/scada-devices", summary="获取所有SCADA设备", tags=["SCADA设备"])
|
||||
async def fastapi_get_all_scada_devices(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
) -> list[dict[str, Any]]:
|
||||
@@ -220,7 +218,7 @@ async def fastapi_get_all_scada_devices(
|
||||
# scada_device_data 设备数据管理
|
||||
############################################################
|
||||
|
||||
@router.get("/getscadadevicedataschema/", summary="获取SCADA设备数据架构", tags=["SCADA设备数据"])
|
||||
@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]]:
|
||||
@@ -237,7 +235,7 @@ async def fastapi_get_scada_device_data_schema(
|
||||
"""
|
||||
return get_scada_device_data_schema(network)
|
||||
|
||||
@router.get("/getscadadevicedata/", summary="获取SCADA设备数据", tags=["SCADA设备数据"])
|
||||
@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")
|
||||
@@ -256,7 +254,7 @@ async def fastapi_get_scada_device_data(
|
||||
"""
|
||||
return get_scada_device_data(network, device_id)
|
||||
|
||||
@router.post("/setscadadevicedata/", response_model=None, summary="更新SCADA设备数据", tags=["SCADA设备数据"])
|
||||
@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
|
||||
@@ -276,7 +274,7 @@ async def fastapi_set_scada_device_data(
|
||||
props = await req.json()
|
||||
return set_scada_device_data(network, ChangeSet(props))
|
||||
|
||||
@router.post("/addscadadevicedata/", response_model=None, summary="添加SCADA设备数据", tags=["SCADA设备数据"])
|
||||
@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
|
||||
@@ -296,7 +294,7 @@ async def fastapi_add_scada_device_data(
|
||||
props = await req.json()
|
||||
return add_scada_device_data(network, ChangeSet(props))
|
||||
|
||||
@router.post("/deletescadadevicedata/", response_model=None, summary="删除SCADA设备数据", tags=["SCADA设备数据"])
|
||||
@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
|
||||
@@ -316,7 +314,7 @@ async def fastapi_delete_scada_device_data(
|
||||
props = await req.json()
|
||||
return delete_scada_device_data(network, ChangeSet(props))
|
||||
|
||||
@router.post("/cleanscadadevicedata/", response_model=None, summary="清空SCADA设备数据表", tags=["SCADA设备数据"])
|
||||
@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:
|
||||
@@ -338,7 +336,7 @@ async def fastapi_clean_scada_device_data(
|
||||
# scada_element SCADA元素映射
|
||||
############################################################
|
||||
|
||||
@router.get("/getscadaelementschema/", summary="获取SCADA元素架构", tags=["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]]:
|
||||
@@ -355,7 +353,7 @@ async def fastapi_get_scada_element_schema(
|
||||
"""
|
||||
return get_scada_element_schema(network)
|
||||
|
||||
@router.get("/getscadaelements/", summary="获取所有SCADA元素映射", tags=["SCADA元素映射"])
|
||||
@router.get("/scada-elements", summary="获取所有SCADA元素映射", tags=["SCADA元素映射"])
|
||||
async def fastapi_get_scada_elements(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
) -> list[dict[str, Any]]:
|
||||
@@ -372,7 +370,7 @@ async def fastapi_get_scada_elements(
|
||||
"""
|
||||
return get_all_scada_elements(network)
|
||||
|
||||
@router.get("/getscadaelement/", summary="获取单个SCADA元素映射", tags=["SCADA元素映射"])
|
||||
@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")
|
||||
@@ -391,7 +389,7 @@ async def fastapi_get_scada_element(
|
||||
"""
|
||||
return get_scada_element(network, id)
|
||||
|
||||
@router.post("/setscadaelement/", response_model=None, summary="更新SCADA元素映射", tags=["SCADA元素映射"])
|
||||
@router.patch("/scada-elements", response_model=None, summary="更新SCADA元素映射", tags=["SCADA元素映射"])
|
||||
async def fastapi_set_scada_element(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -411,7 +409,7 @@ async def fastapi_set_scada_element(
|
||||
props = await req.json()
|
||||
return set_scada_element(network, ChangeSet(props))
|
||||
|
||||
@router.post("/addscadaelement/", response_model=None, summary="添加SCADA元素映射", tags=["SCADA元素映射"])
|
||||
@router.post("/scada-elements", response_model=None, summary="添加SCADA元素映射", tags=["SCADA元素映射"])
|
||||
async def fastapi_add_scada_element(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -431,7 +429,7 @@ async def fastapi_add_scada_element(
|
||||
props = await req.json()
|
||||
return add_scada_element(network, ChangeSet(props))
|
||||
|
||||
@router.post("/deletescadaelement/", response_model=None, summary="删除SCADA元素映射", tags=["SCADA元素映射"])
|
||||
@router.delete("/scada-elements", response_model=None, summary="删除SCADA元素映射", tags=["SCADA元素映射"])
|
||||
async def fastapi_delete_scada_element(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
req: Request = None
|
||||
@@ -451,7 +449,7 @@ async def fastapi_delete_scada_element(
|
||||
props = await req.json()
|
||||
return delete_scada_element(network, ChangeSet(props))
|
||||
|
||||
@router.post("/cleanscadaelement/", response_model=None, summary="清空SCADA元素映射表", tags=["SCADA元素映射"])
|
||||
@router.post("/scada-element-cleaning-runs", response_model=None, summary="清空SCADA元素映射表", tags=["SCADA元素映射"])
|
||||
async def fastapi_clean_scada_element(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
) -> ChangeSet:
|
||||
@@ -473,7 +471,7 @@ async def fastapi_clean_scada_element(
|
||||
# scada_info SCADA信息
|
||||
############################################################
|
||||
|
||||
@router.get("/getscadainfoschema/", summary="获取SCADA信息架构", tags=["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]]:
|
||||
@@ -490,7 +488,7 @@ async def fastapi_get_scada_info_schema(
|
||||
"""
|
||||
return get_scada_info_schema(network)
|
||||
|
||||
@router.get("/getscadainfo/", summary="获取SCADA信息", tags=["SCADA信息"])
|
||||
@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")
|
||||
@@ -509,7 +507,7 @@ async def fastapi_get_scada_info(
|
||||
"""
|
||||
return get_scada_info(network, id)
|
||||
|
||||
@router.get("/getallscadainfo/", summary="获取所有SCADA信息", tags=["SCADA信息"])
|
||||
@router.get("/scada-info", summary="获取所有SCADA信息", tags=["SCADA信息"])
|
||||
async def fastapi_get_all_scada_info(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)")
|
||||
) -> list[dict[str, Any]]:
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
from fastapi import APIRouter, Query
|
||||
from typing import Any, List, Dict
|
||||
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("/getschemeschema/", summary="获取方案模式", description="获取指定网络的方案模式定义")
|
||||
@router.get("/network-schemas/scheme", summary="获取方案模式", description="获取指定网络的方案模式定义")
|
||||
async def fastapi_get_scheme_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[Any, Any]]:
|
||||
"""
|
||||
获取方案模式定义
|
||||
@@ -13,7 +16,7 @@ async def fastapi_get_scheme_schema(network: str = Query(..., description="管
|
||||
"""
|
||||
return get_scheme_schema(network)
|
||||
|
||||
@router.get("/getscheme/", summary="获取单个方案", description="根据名称获取指定的方案信息")
|
||||
@router.get("/schemes/detail", summary="获取单个方案", description="根据名称获取指定的方案信息")
|
||||
async def fastapi_get_scheme(network: str = Query(..., description="管网名称(或数据库名称)"), schema_name: str = Query(..., description="方案名称")) -> dict[Any, Any]:
|
||||
"""
|
||||
获取单个方案详情
|
||||
@@ -23,11 +26,35 @@ async def fastapi_get_scheme(network: str = Query(..., description="管网名称
|
||||
return get_scheme(network, schema_name)
|
||||
|
||||
@router.get("/schemes", summary="获取所有方案", description="获取指定网络的所有方案信息")
|
||||
@router.get("/getallschemes/", summary="获取所有方案(旧路径)", description="获取指定网络的所有方案信息", deprecated=True)
|
||||
async def fastapi_get_all_schemes(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[Any, Any]]:
|
||||
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]]:
|
||||
"""
|
||||
获取所有方案列表
|
||||
|
||||
返回指定网络中所有可用的方案
|
||||
"""
|
||||
return get_all_schemes(network)
|
||||
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
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
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.project_dependencies import ProjectContext, get_project_context
|
||||
from app.domain.schemas.sensor_placement import (
|
||||
SensorPointResponse,
|
||||
SensorPlacementExportRequest,
|
||||
SensorPlacementOptimizeRequest,
|
||||
SensorPlacementSchemeResponse,
|
||||
SensorPlacementUpdateRequest,
|
||||
)
|
||||
from app.services.sensor_placement import (
|
||||
SensorPlacementConflictError,
|
||||
SensorPlacementNotFoundError,
|
||||
SensorPlacementValidationError,
|
||||
build_sensor_placement_workbook,
|
||||
can_edit_sensor_placement,
|
||||
get_sensor_placement_candidate,
|
||||
get_sensor_placement_scheme,
|
||||
update_sensor_placement_scheme,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _project_network(network: str, project_context: ProjectContext) -> str:
|
||||
if network != project_context.project_code:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="请求的管网不属于当前项目",
|
||||
)
|
||||
return project_context.project_code
|
||||
|
||||
|
||||
def _can_modify_project(project_context: ProjectContext) -> bool:
|
||||
return project_context.project_role == "member"
|
||||
|
||||
|
||||
def _require_project_write(
|
||||
project_context: ProjectContext,
|
||||
) -> None:
|
||||
if not _can_modify_project(project_context):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="当前项目角色为只读,不能修改监测点方案",
|
||||
)
|
||||
|
||||
|
||||
def _service_http_error(exc: Exception) -> HTTPException:
|
||||
if isinstance(exc, SensorPlacementNotFoundError):
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(exc),
|
||||
)
|
||||
if isinstance(exc, SensorPlacementConflictError):
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=str(exc),
|
||||
)
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=str(exc),
|
||||
)
|
||||
|
||||
|
||||
def _get_scheme_response(
|
||||
network: str,
|
||||
scheme_id: int,
|
||||
current_user: Any,
|
||||
project_context: ProjectContext,
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
scheme = get_sensor_placement_scheme(network, scheme_id)
|
||||
return {
|
||||
**scheme,
|
||||
"can_edit": (
|
||||
_can_modify_project(project_context)
|
||||
and can_edit_sensor_placement(current_user, scheme)
|
||||
),
|
||||
}
|
||||
except (
|
||||
SensorPlacementNotFoundError,
|
||||
SensorPlacementValidationError,
|
||||
) as exc:
|
||||
raise _service_http_error(exc) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/sensor-placement-candidates/{node_id}",
|
||||
response_model=SensorPointResponse,
|
||||
summary="获取监测点候选节点详情",
|
||||
)
|
||||
async def get_sensor_placement_candidate_detail(
|
||||
node_id: str = Path(..., min_length=1, max_length=32),
|
||||
project_context: ProjectContext = Depends(get_project_context),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await run_in_threadpool(
|
||||
get_sensor_placement_candidate,
|
||||
project_context.project_code,
|
||||
node_id,
|
||||
)
|
||||
except SensorPlacementValidationError as exc:
|
||||
raise _service_http_error(exc) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/sensor-placement-optimization-runs",
|
||||
response_model=SensorPlacementSchemeResponse,
|
||||
summary="创建并返回监测点优化方案",
|
||||
)
|
||||
async def optimize_sensor_placement_scheme(
|
||||
payload: SensorPlacementOptimizeRequest,
|
||||
project_context: ProjectContext = Depends(get_project_context),
|
||||
current_user=Depends(get_current_metadata_user),
|
||||
) -> dict[str, Any]:
|
||||
network = _project_network(payload.network, project_context)
|
||||
_require_project_write(project_context)
|
||||
optimizer = (
|
||||
pressure_sensor_placement_sensitivity
|
||||
if payload.method == "sensitivity"
|
||||
else pressure_sensor_placement_kmeans
|
||||
)
|
||||
try:
|
||||
created = await run_in_threadpool(
|
||||
optimizer,
|
||||
name=network,
|
||||
scheme_name=payload.scheme_name,
|
||||
sensor_number=payload.sensor_count,
|
||||
min_diameter=payload.min_diameter,
|
||||
username=current_user.username,
|
||||
)
|
||||
scheme = get_sensor_placement_scheme(network, int(created["id"]))
|
||||
return {**scheme, "can_edit": True}
|
||||
except (
|
||||
SensorPlacementConflictError,
|
||||
SensorPlacementValidationError,
|
||||
ValueError,
|
||||
) as exc:
|
||||
raise _service_http_error(exc) from exc
|
||||
except Exception as exc:
|
||||
logger.exception("Sensor placement optimization failed")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="监测点优化失败,请稍后重试",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/sensor-placement-schemes/{scheme_id}",
|
||||
response_model=SensorPlacementSchemeResponse,
|
||||
summary="获取监测点方案详情",
|
||||
)
|
||||
async def get_sensor_placement_scheme_detail(
|
||||
scheme_id: int,
|
||||
network: str = Query(..., min_length=1),
|
||||
project_context: ProjectContext = Depends(get_project_context),
|
||||
current_user=Depends(get_current_metadata_user),
|
||||
) -> dict[str, Any]:
|
||||
return _get_scheme_response(
|
||||
_project_network(network, project_context),
|
||||
scheme_id,
|
||||
current_user,
|
||||
project_context,
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/sensor-placement-schemes/{scheme_id}",
|
||||
response_model=SensorPlacementSchemeResponse,
|
||||
summary="覆盖保存监测点方案",
|
||||
)
|
||||
async def overwrite_sensor_placement_scheme(
|
||||
scheme_id: int,
|
||||
payload: SensorPlacementUpdateRequest,
|
||||
network: str = Query(..., min_length=1),
|
||||
project_context: ProjectContext = Depends(get_project_context),
|
||||
current_user=Depends(get_current_metadata_user),
|
||||
) -> dict[str, Any]:
|
||||
network = _project_network(network, project_context)
|
||||
_require_project_write(project_context)
|
||||
scheme = _get_scheme_response(
|
||||
network,
|
||||
scheme_id,
|
||||
current_user,
|
||||
project_context,
|
||||
)
|
||||
if not scheme["can_edit"]:
|
||||
raise HTTPException(status_code=403, detail="无权修改该监测点方案")
|
||||
|
||||
try:
|
||||
updated = update_sensor_placement_scheme(
|
||||
network,
|
||||
scheme_id,
|
||||
expected_sensor_location=payload.expected_sensor_location,
|
||||
sensor_location=payload.sensor_location,
|
||||
)
|
||||
return {**updated, "can_edit": True}
|
||||
except (
|
||||
SensorPlacementConflictError,
|
||||
SensorPlacementNotFoundError,
|
||||
SensorPlacementValidationError,
|
||||
) as exc:
|
||||
raise _service_http_error(exc) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/sensor-placement-schemes/{scheme_id}/exports/excel",
|
||||
summary="导出监测点工程清单",
|
||||
)
|
||||
async def export_sensor_placement_excel(
|
||||
scheme_id: int,
|
||||
payload: SensorPlacementExportRequest,
|
||||
network: str = Query(..., min_length=1),
|
||||
project_context: ProjectContext = Depends(get_project_context),
|
||||
current_user=Depends(get_current_metadata_user),
|
||||
) -> StreamingResponse:
|
||||
network = _project_network(network, project_context)
|
||||
scheme = _get_scheme_response(
|
||||
network,
|
||||
scheme_id,
|
||||
current_user,
|
||||
project_context,
|
||||
)
|
||||
if (
|
||||
payload.sensor_location != scheme["sensor_location"]
|
||||
and not scheme["can_edit"]
|
||||
):
|
||||
raise HTTPException(status_code=403, detail="无权导出该方案的未保存草稿")
|
||||
|
||||
try:
|
||||
workbook = await run_in_threadpool(
|
||||
build_sensor_placement_workbook,
|
||||
network=network,
|
||||
scheme=scheme,
|
||||
sensor_location=payload.sensor_location,
|
||||
adjustment_status=payload.adjustment_status,
|
||||
)
|
||||
except SensorPlacementValidationError as exc:
|
||||
raise _service_http_error(exc) from exc
|
||||
|
||||
filename = f"{scheme['scheme_name']}_监测点清单.xlsx"
|
||||
encoded_filename = quote(filename)
|
||||
return StreamingResponse(
|
||||
workbook,
|
||||
media_type=(
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
),
|
||||
headers={
|
||||
"Content-Disposition": (
|
||||
f"attachment; filename*=UTF-8''{encoded_filename}"
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -1,11 +1,10 @@
|
||||
from typing import Any, List, Optional
|
||||
from typing import Any, List, Literal, Optional
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import threading
|
||||
from fastapi import APIRouter, HTTPException, File, UploadFile, Query, Path, Body
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
|
||||
from fastapi.responses import PlainTextResponse
|
||||
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
||||
import app.services.simulation as simulation
|
||||
import app.services.globals as globals
|
||||
from app.services.tjnetwork import (
|
||||
@@ -28,7 +27,6 @@ from app.algorithms.sensor import (
|
||||
pressure_sensor_placement_kmeans,
|
||||
)
|
||||
|
||||
from app.services.network_import import network_update
|
||||
from app.services.simulation_ops import (
|
||||
project_management,
|
||||
scheduling_simulation,
|
||||
@@ -141,7 +139,7 @@ def run_simulation_manually_by_date(
|
||||
|
||||
|
||||
# 必须用这个PlainTextResponse,不然每个key都有引号
|
||||
@router.get("/runproject/", response_class=PlainTextResponse, summary="运行项目模拟", description="基于指定的管网项目运行标准水力模拟,返回纯文本格式的模拟报告。")
|
||||
@router.post("/project-runs", response_class=PlainTextResponse, summary="运行项目模拟", description="基于指定的管网项目运行标准水力模拟,返回纯文本格式的模拟报告。")
|
||||
async def run_project_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> str:
|
||||
"""
|
||||
运行项目模拟
|
||||
@@ -157,7 +155,7 @@ async def run_project_endpoint(network: str = Query(..., description="管网名
|
||||
# output 和 report
|
||||
# output 是 json
|
||||
# report 是 text
|
||||
@router.get("/runprojectreturndict/", 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]:
|
||||
"""
|
||||
运行项目模拟(返回字典)
|
||||
@@ -174,7 +172,7 @@ async def run_project_return_dict_endpoint(network: str = Query(..., description
|
||||
|
||||
|
||||
# put in inp folder, name without extension
|
||||
@router.get("/runinp/", summary="运行INP文件", description="运行指定INP文件格式的管网模型进行水力模拟。INP文件应该放在inp文件夹中,参数为文件名不含扩展名。")
|
||||
@router.post("/inp-runs", summary="运行INP文件", description="运行指定INP文件格式的管网模型进行水力模拟。INP文件应该放在inp文件夹中,参数为文件名不含扩展名。")
|
||||
async def run_inp_endpoint(network: str = Query(..., description="inp文件名(不含扩展名)")) -> str:
|
||||
"""
|
||||
运行INP文件
|
||||
@@ -187,7 +185,7 @@ async def run_inp_endpoint(network: str = Query(..., description="inp文件名
|
||||
|
||||
|
||||
# path is absolute path
|
||||
@router.get("/dumpoutput/", summary="导出模拟输出", description="导出指定路径的模拟输出文件内容。参数应为绝对路径。")
|
||||
@router.get("/outputs", summary="导出模拟输出", description="导出指定路径的模拟输出文件内容。参数应为绝对路径。")
|
||||
async def dump_output_endpoint(output: str = Query(..., description="模拟输出文件的绝对路径")) -> str:
|
||||
"""
|
||||
导出模拟输出
|
||||
@@ -200,8 +198,7 @@ async def dump_output_endpoint(output: str = Query(..., description="模拟输
|
||||
|
||||
|
||||
# Analysis Endpoints
|
||||
@router.get("/burst-analysis", summary="爆管分析(高级)", description="高级版本的爆管分析,支持在指定时间点修改泵控制模式和阀门开度,以分析这些改变对爆管影响的作用。支持固定泵和变速泵的独立控制。")
|
||||
@router.get("/burst_analysis/", summary="爆管分析(高级,旧路径)", description="高级版本的爆管分析,支持在指定时间点修改泵控制模式和阀门开度,以分析这些改变对爆管影响的作用。支持固定泵和变速泵的独立控制。", deprecated=True)
|
||||
@router.post("/burst-analyses", summary="爆管分析(高级)", description="高级版本的爆管分析,支持在指定时间点修改泵控制模式和阀门开度,以分析这些改变对爆管影响的作用。支持固定泵和变速泵的独立控制。")
|
||||
async def fastapi_burst_analysis(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
modify_pattern_start_time: str = Query(..., description="模式修改开始时间(ISO 8601格式)"),
|
||||
@@ -209,6 +206,7 @@ async def fastapi_burst_analysis(
|
||||
burst_size: list[float] = Query(..., description="对应各爆管点的爆管流量大小列表(L/s)"),
|
||||
modify_total_duration: int = Query(..., description="模拟总时长(秒)"),
|
||||
scheme_name: str = Query(..., description="分析方案名称"),
|
||||
username: str = Depends(get_current_keycloak_username),
|
||||
) -> str:
|
||||
"""
|
||||
爆管分析(高级版本)
|
||||
@@ -229,11 +227,12 @@ async def fastapi_burst_analysis(
|
||||
burst_size=burst_size,
|
||||
modify_total_duration=modify_total_duration,
|
||||
scheme_name=scheme_name,
|
||||
username=username,
|
||||
)
|
||||
return "success"
|
||||
|
||||
|
||||
@router.get("/valve_close_analysis/", response_class=PlainTextResponse, summary="阀门关闭分析(高级)", description="高级版本的阀门关闭分析,支持同时关闭多个阀门,并在指定持续时间内进行模拟。返回纯文本格式的分析结果。")
|
||||
@router.post("/valve-closure-analyses", response_class=PlainTextResponse, summary="阀门关闭分析(高级)", description="高级版本的阀门关闭分析,支持同时关闭多个阀门,并在指定持续时间内进行模拟。返回纯文本格式的分析结果。")
|
||||
async def fastapi_valve_close_analysis(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
start_time: str = Query(..., description="阀门关闭开始时间(ISO 8601格式)"),
|
||||
@@ -262,8 +261,7 @@ async def fastapi_valve_close_analysis(
|
||||
return result or "success"
|
||||
|
||||
|
||||
@router.get("/valve-isolation-analysis", summary="阀门隔离分析", description="分析当发生突发事件时,通过关闭指定阀门进行隔离,确定哪些阀门必须关闭、哪些可选关闭,以及隔离的可行性。")
|
||||
@router.get("/valve_isolation_analysis/", summary="阀门隔离分析(旧路径)", description="分析当发生突发事件时,通过关闭指定阀门进行隔离,确定哪些阀门必须关闭、哪些可选关闭,以及隔离的可行性。", deprecated=True)
|
||||
@router.post("/valve-isolation-analyses", summary="阀门隔离分析", description="分析当发生突发事件时,通过关闭指定阀门进行隔离,确定哪些阀门必须关闭、哪些可选关闭,以及隔离的可行性。")
|
||||
async def valve_isolation_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
accident_element: List[str] = Query(..., description="发生事故的管段/节点ID列表"),
|
||||
@@ -279,7 +277,8 @@ async def valve_isolation_endpoint(
|
||||
返回隔离方案,包括:
|
||||
- must_close_valves: 必须关闭的阀门列表
|
||||
- optional_valves: 可选关闭的阀门列表
|
||||
- affected_nodes: 受影响的节点列表
|
||||
- affected_nodes: 受影响的节点列表;不可隔离时为空列表
|
||||
- affected_node_count: 受影响的节点总数
|
||||
- isolatable: 是否可以有效隔离
|
||||
"""
|
||||
# result = {
|
||||
@@ -303,25 +302,35 @@ async def valve_isolation_endpoint(
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/flushing-analysis", response_class=PlainTextResponse, summary="冲洗分析(高级)", description="高级版本的冲洗分析,支持同时开启多个阀门进行冲洗,指定排污节点,并设置固定的冲洗流量。返回纯文本格式的分析结果。")
|
||||
@router.get("/flushing_analysis/", response_class=PlainTextResponse, summary="冲洗分析(高级,旧路径)", description="高级版本的冲洗分析,支持同时开启多个阀门进行冲洗,指定排污节点,并设置固定的冲洗流量。返回纯文本格式的分析结果。", deprecated=True)
|
||||
@router.post("/flushing-analyses", response_class=PlainTextResponse, summary="冲洗分析(高级)", description="高级版本的冲洗分析,支持按状态和设置值控制多个可选阀门,指定排污节点,并设置固定的冲洗流量。返回纯文本格式的分析结果。")
|
||||
async def fastapi_flushing_analysis(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
start_time: str = Query(..., description="冲洗开始时间(ISO 8601格式)"),
|
||||
valves: List[str] = Query(..., description="要开启的阀门ID列表"),
|
||||
valves_k: List[float] = Query(..., description="对应各阀门的开度列表(0-1)"),
|
||||
valves: List[str] | None = Query(None, description="参与控制的阀门ID列表(可选)"),
|
||||
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"),
|
||||
flush_flow: float = Query(0, description="冲洗流量(L/s),0表示自动计算"),
|
||||
duration: int | None = Query(None, description="模拟持续时间(秒),默认900秒"),
|
||||
scheme_name: str = Query(..., description="冲洗方案名称"),
|
||||
username: str = Depends(get_current_keycloak_username),
|
||||
) -> str:
|
||||
"""
|
||||
冲洗分析(高级版本)
|
||||
|
||||
- **network**: 管网名称(或数据库名称)
|
||||
- **start_time**: 冲洗开始时间
|
||||
- **valves**: 要开启的阀门ID列表
|
||||
- **valves_k**: 各阀门的开度列表(0-1,与valves对应)
|
||||
- **valves**: 参与控制的阀门ID列表(可选)
|
||||
- **valves_k**: 各阀门的开度列表(0-1,可选,与valves同时提供)
|
||||
- **valve_statuses**: 各阀门的开关状态列表(OPEN、CLOSED或ACTIVE,可选)
|
||||
- **valve_settings**: 各阀门的设置值列表(ACTIVE状态下必填)
|
||||
- **drainage_node_ID**: 排污节点ID
|
||||
- **flush_flow**: 冲洗流量(L/s)
|
||||
- **duration**: 模拟持续时间(秒,可选,默认900)
|
||||
@@ -329,23 +338,84 @@ 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_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(
|
||||
name=network,
|
||||
modify_pattern_start_time=start_time,
|
||||
modify_total_duration=duration or 900,
|
||||
modify_valve_opening=valve_opening,
|
||||
valve_control=valve_control,
|
||||
drainage_node_ID=drainage_node_ID,
|
||||
flushing_flow=flush_flow,
|
||||
scheme_name=scheme_name,
|
||||
username=username,
|
||||
)
|
||||
return result or "success"
|
||||
|
||||
|
||||
@router.get("/contaminant-simulation", response_class=PlainTextResponse, summary="污染物模拟", description="对管网中的污染物扩散进行模拟,评估污染源对管网的影响范围和浓度分布。支持指定污染源位置、污染浓度和扩散模式。")
|
||||
@router.get("/contaminant_simulation/", response_class=PlainTextResponse, summary="污染物模拟(旧路径)", description="对管网中的污染物扩散进行模拟,评估污染源对管网的影响范围和浓度分布。支持指定污染源位置、污染浓度和扩散模式。", deprecated=True)
|
||||
@router.post("/contaminant-simulations", response_class=PlainTextResponse, summary="污染物模拟", description="对管网中的污染物扩散进行模拟,评估污染源对管网的影响范围和浓度分布。支持指定污染源位置、污染浓度和扩散模式。")
|
||||
async def fastapi_contaminant_simulation(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
start_time: str = Query(..., description="污染开始时间(ISO 8601格式)"),
|
||||
@@ -354,6 +424,7 @@ async def fastapi_contaminant_simulation(
|
||||
duration: int = Query(..., description="模拟持续时间(秒)"),
|
||||
scheme_name: str = Query(..., description="模拟方案名称"),
|
||||
pattern: str | None = Query(None, description="污染源模式ID(可选)"),
|
||||
username: str = Depends(get_current_keycloak_username),
|
||||
) -> str:
|
||||
"""
|
||||
污染物模拟
|
||||
@@ -376,11 +447,12 @@ async def fastapi_contaminant_simulation(
|
||||
source=source,
|
||||
concentration=concentration,
|
||||
source_pattern=pattern,
|
||||
username=username,
|
||||
)
|
||||
return result or "success"
|
||||
|
||||
|
||||
@router.get("/age_analysis/", response_class=PlainTextResponse, summary="水龄分析(高级)", description="高级版本的水龄分析,在指定时间点进行分析,支持自定义模拟持续时间。返回纯文本格式的分析结果。")
|
||||
@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格式)"),
|
||||
@@ -404,7 +476,7 @@ async def fastapi_age_analysis(
|
||||
# return scheduling_analysis(network)
|
||||
|
||||
|
||||
@router.get("/pressureregulation/", summary="压力调节(基础)", description="对管网的压力进行调节分析,通过控制泵的运行来维持目标节点的目标压力。此为基础版本。")
|
||||
@router.post("/pressure-regulation-calculations", summary="压力调节(基础)", description="对管网的压力进行调节分析,通过控制泵的运行来维持目标节点的目标压力。此为基础版本。")
|
||||
async def pressure_regulation_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
target_node: str = Query(..., description="目标节点ID"),
|
||||
@@ -422,7 +494,7 @@ async def pressure_regulation_endpoint(
|
||||
return pressure_regulation(network, target_node, target_pressure)
|
||||
|
||||
|
||||
@router.post("/pressure_regulation/", summary="压力调节(高级)", description="高级版本的压力调节分析,通过JSON请求体提供详细的控制参数,包括固定泵和变速泵的独立控制、水箱初始水位等。")
|
||||
@router.post("/pressure-regulation-analyses", summary="压力调节(高级)", description="高级版本的压力调节分析,通过JSON请求体提供详细的控制参数,包括固定泵和变速泵的独立控制、水箱初始水位等。")
|
||||
async def fastapi_pressure_regulation(data: PressureRegulation = Body(..., description="压力调节控制参数")) -> str:
|
||||
"""
|
||||
压力调节(高级版本)
|
||||
@@ -460,7 +532,7 @@ async def fastapi_pressure_regulation(data: PressureRegulation = Body(..., descr
|
||||
return "success"
|
||||
|
||||
|
||||
@router.post("/project_management/", summary="项目管理(高级)", description="高级版本的项目管理,通过JSON请求体提供详细的控制参数,包括泵控制策略、水箱初始水位和区域需水量控制。")
|
||||
@router.post("/project-managements", summary="项目管理(高级)", description="高级版本的项目管理,通过JSON请求体提供详细的控制参数,包括泵控制策略、水箱初始水位和区域需水量控制。")
|
||||
async def fastapi_project_management(data: ProjectManagement = Body(..., description="项目管理控制参数")) -> str:
|
||||
"""
|
||||
项目管理(高级版本)
|
||||
@@ -489,7 +561,7 @@ async def fastapi_project_management(data: ProjectManagement = Body(..., descrip
|
||||
# return daily_scheduling_analysis(network)
|
||||
|
||||
|
||||
@router.post("/scheduling_analysis/", summary="排程分析", description="对管网的供水排程进行分析,优化泵的运行时间和出水流量,平衡水厂出水、水箱进出水,满足用户需求。")
|
||||
@router.post("/scheduling-analyses", summary="排程分析", description="对管网的供水排程进行分析,优化泵的运行时间和出水流量,平衡水厂出水、水箱进出水,满足用户需求。")
|
||||
async def fastapi_scheduling_analysis(data: SchedulingAnalysis = Body(..., description="排程分析参数")) -> str:
|
||||
"""
|
||||
排程分析
|
||||
@@ -515,7 +587,7 @@ async def fastapi_scheduling_analysis(data: SchedulingAnalysis = Body(..., descr
|
||||
)
|
||||
|
||||
|
||||
@router.post("/daily_scheduling_analysis/", summary="日排程分析", description="对管网的每日供水排程进行分析,优化水库、水厂、水箱和用户需求的协调,制定合理的每日排程方案。")
|
||||
@router.post("/daily-scheduling-analyses", summary="日排程分析", description="对管网的每日供水排程进行分析,优化水库、水厂、水箱和用户需求的协调,制定合理的每日排程方案。")
|
||||
async def fastapi_daily_scheduling_analysis(data: DailySchedulingAnalysis = Body(..., description="日排程分析参数")) -> str:
|
||||
"""
|
||||
日排程分析
|
||||
@@ -542,52 +614,12 @@ async def fastapi_daily_scheduling_analysis(data: DailySchedulingAnalysis = Body
|
||||
)
|
||||
|
||||
|
||||
@router.post("/network_project/", summary="导入网络项目", description="通过上传INP格式的管网文件导入新的网络项目。系统将自动处理文件并执行模拟。")
|
||||
async def fastapi_network_project(file: UploadFile = File(..., description="INP格式的管网文件")) -> str:
|
||||
"""
|
||||
导入网络项目
|
||||
|
||||
- **file**: 上传的INP格式管网文件
|
||||
|
||||
系统将上传的文件保存到inp文件夹并执行模拟。
|
||||
"""
|
||||
temp_file_dir = "./inp/"
|
||||
if not os.path.exists(temp_file_dir):
|
||||
os.mkdir(temp_file_dir)
|
||||
temp_file_name = f'network_project_{datetime.now().strftime("%Y%m%d")}'
|
||||
temp_file_path = f"{temp_file_dir}{temp_file_name}.inp"
|
||||
with open(temp_file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
return run_inp(temp_file_name)
|
||||
|
||||
|
||||
@router.post("/network_update/", summary="管网更新(高级)", description="通过上传更新文件对管网进行高级的更新操作。系统将处理更新文件并应用到数据库。")
|
||||
async def fastapi_network_update(file: UploadFile = File(..., description="包含管网更新信息的文件")) -> str:
|
||||
"""
|
||||
管网更新(高级版本)
|
||||
|
||||
- **file**: 包含管网更新信息的文件
|
||||
|
||||
系统将处理上传的文件并应用管网更新。
|
||||
"""
|
||||
default_folder = "./"
|
||||
temp_file_name = f'network_update_{datetime.now().strftime("%Y%m%d")}'
|
||||
temp_file_path = os.path.join(default_folder, temp_file_name)
|
||||
try:
|
||||
with open(temp_file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
network_update(temp_file_path)
|
||||
return json.dumps({"message": "管网更新成功"})
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"数据库操作失败: {exc}")
|
||||
|
||||
|
||||
# @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/", summary="泵故障管理", description="记录和管理泵的故障状态,包括故障发生时间和受影响的泵列表。系统将记录故障日志并更新泵状态。")
|
||||
@router.post("/pump-failure-events", summary="泵故障管理", description="记录和管理泵的故障状态,包括故障发生时间和受影响的泵列表。系统将记录故障日志并更新泵状态。")
|
||||
async def fastapi_pump_failure(data: PumpFailureState = Body(..., description="泵故障状态信息")) -> str:
|
||||
"""
|
||||
泵故障管理
|
||||
@@ -631,7 +663,7 @@ async def fastapi_pump_failure(data: PumpFailureState = Body(..., description="
|
||||
return json.dumps("SUCCESS")
|
||||
|
||||
|
||||
@router.get("/pressuresensorplacementsensitivity/", summary="压力传感器放置-灵敏度分析(基础)", description="基于灵敏度分析方法,为指定管网项目确定最优的压力传感器放置位置。此为基础版本。")
|
||||
@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="放置方案名称"),
|
||||
@@ -655,7 +687,7 @@ async def pressure_sensor_placement_sensitivity_endpoint(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/pressure_sensor_placement_sensitivity/", summary="压力传感器放置-灵敏度分析(高级)", description="高级版本的压力传感器放置分析,通过JSON请求体提供详细参数。基于灵敏度分析方法确定最优放置位置。")
|
||||
@router.post("/pressure-sensor-placement-sensitivities", summary="压力传感器放置-灵敏度分析(高级)", description="高级版本的压力传感器放置分析,通过JSON请求体提供详细参数。基于灵敏度分析方法确定最优放置位置。")
|
||||
async def fastapi_pressure_sensor_placement_sensitivity(
|
||||
data: PressureSensorPlacement = Body(..., description="传感器放置分析参数"),
|
||||
) -> None:
|
||||
@@ -681,7 +713,7 @@ async def fastapi_pressure_sensor_placement_sensitivity(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/pressuresensorplacementkmeans/", summary="压力传感器放置-KMeans聚类分析(基础)", description="基于KMeans聚类算法,为指定管网项目确定压力传感器的最优放置位置。此为基础版本。")
|
||||
@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="放置方案名称"),
|
||||
@@ -705,7 +737,7 @@ async def pressure_sensor_placement_kmeans_endpoint(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/pressure_sensor_placement_kmeans/", summary="压力传感器放置-KMeans聚类分析(高级)", description="高级版本的压力传感器放置分析,通过JSON请求体提供详细参数。基于KMeans聚类算法确定最优放置位置。")
|
||||
@router.post("/pressure-sensor-placement-kmeans", summary="压力传感器放置-KMeans聚类分析(高级)", description="高级版本的压力传感器放置分析,通过JSON请求体提供详细参数。基于KMeans聚类算法确定最优放置位置。")
|
||||
async def fastapi_pressure_sensor_placement_kmeans(
|
||||
data: PressureSensorPlacement = Body(..., description="传感器放置分析参数"),
|
||||
) -> None:
|
||||
@@ -732,7 +764,6 @@ async def fastapi_pressure_sensor_placement_kmeans(
|
||||
|
||||
|
||||
@router.post("/sensor-placement-schemes", summary="传感器放置方案创建", description="创建新的传感器放置方案,支持灵敏度分析和KMeans聚类两种方法。根据指定的方法自动计算最优的传感器放置位置。")
|
||||
@router.post("/sensorplacementscheme/create", summary="传感器放置方案创建(旧路径)", description="创建新的传感器放置方案,支持灵敏度分析和KMeans聚类两种方法。根据指定的方法自动计算最优的传感器放置位置。", deprecated=True)
|
||||
async def fastapi_pressure_sensor_placement(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
scheme_name: str = Query(..., description="放置方案名称"),
|
||||
@@ -780,8 +811,7 @@ async def fastapi_pressure_sensor_placement(
|
||||
return "success"
|
||||
|
||||
|
||||
@router.post("/simulations/run-by-date", summary="手动运行日期指定模拟", description="根据指定的开始时间和持续时间,手动运行水力模拟。开始时间必须是显式带时区的 ISO 8601 / RFC3339 时间。")
|
||||
@router.post("/runsimulationmanuallybydate/", summary="手动运行日期指定模拟(旧路径)", description="根据指定的开始时间和持续时间,手动运行水力模拟。开始时间必须是显式带时区的 ISO 8601 / RFC3339 时间。", deprecated=True)
|
||||
@router.post("/simulation-runs", summary="手动运行日期指定模拟", description="根据指定的开始时间和持续时间,手动运行水力模拟。开始时间必须是显式带时区的 ISO 8601 / RFC3339 时间。")
|
||||
async def fastapi_run_simulation_manually_by_date(
|
||||
data: RunSimulationManuallyByDate = Body(..., description="模拟运行参数"),
|
||||
) -> dict[str, str]:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from fastapi import APIRouter, Request, Query
|
||||
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,
|
||||
@@ -22,7 +23,7 @@ from app.services.tjnetwork import (
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/getcurrentoperationid/", summary="获取当前操作ID", description="获取网络当前的操作ID")
|
||||
@router.get("/current-operation-ids", summary="获取当前操作ID", description="获取网络当前的操作ID")
|
||||
async def get_current_operation_id_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> int:
|
||||
"""
|
||||
获取当前操作ID
|
||||
@@ -31,7 +32,7 @@ async def get_current_operation_id_endpoint(network: str = Query(..., descriptio
|
||||
"""
|
||||
return get_current_operation(network)
|
||||
|
||||
@router.post("/undo/", summary="撤销操作", description="撤销网络上最后的一个操作")
|
||||
@router.post("/undos", summary="撤销操作", description="撤销网络上最后的一个操作")
|
||||
async def undo_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")):
|
||||
"""
|
||||
撤销操作
|
||||
@@ -40,7 +41,7 @@ async def undo_endpoint(network: str = Query(..., description="管网名称(
|
||||
"""
|
||||
return execute_undo(network)
|
||||
|
||||
@router.post("/redo/", summary="重做操作", description="重做网络上被撤销的操作")
|
||||
@router.post("/redos", summary="重做操作", description="重做网络上被撤销的操作")
|
||||
async def redo_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")):
|
||||
"""
|
||||
重做操作
|
||||
@@ -49,7 +50,7 @@ async def redo_endpoint(network: str = Query(..., description="管网名称(
|
||||
"""
|
||||
return execute_redo(network)
|
||||
|
||||
@router.get("/getsnapshots/", summary="获取快照列表", description="获取网络中的所有快照")
|
||||
@router.get("/snapshots", summary="获取快照列表", description="获取网络中的所有快照")
|
||||
async def list_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[tuple[int, str]]:
|
||||
"""
|
||||
获取快照列表
|
||||
@@ -58,7 +59,7 @@ async def list_snapshot_endpoint(network: str = Query(..., description="管网
|
||||
"""
|
||||
return list_snapshot(network)
|
||||
|
||||
@router.get("/havesnapshot/", summary="检查快照是否存在", description="检查指定标签的快照是否存在")
|
||||
@router.get("/snapshots/existence", summary="检查快照是否存在", description="检查指定标签的快照是否存在")
|
||||
async def have_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签")) -> bool:
|
||||
"""
|
||||
检查快照是否存在
|
||||
@@ -67,7 +68,7 @@ async def have_snapshot_endpoint(network: str = Query(..., description="管网
|
||||
"""
|
||||
return have_snapshot(network, tag)
|
||||
|
||||
@router.get("/havesnapshotforoperation/", summary="检查操作快照是否存在", description="检查指定操作ID的快照是否存在")
|
||||
@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:
|
||||
"""
|
||||
检查操作快照是否存在
|
||||
@@ -76,7 +77,7 @@ async def have_snapshot_for_operation_endpoint(network: str = Query(..., descrip
|
||||
"""
|
||||
return have_snapshot_for_operation(network, operation)
|
||||
|
||||
@router.get("/havesnapshotforcurrentoperation/", summary="检查当前操作快照是否存在", description="检查当前操作的快照是否存在")
|
||||
@router.get("/snapshot-for-current-operations", summary="检查当前操作快照是否存在", description="检查当前操作的快照是否存在")
|
||||
async def have_snapshot_for_current_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> bool:
|
||||
"""
|
||||
检查当前操作快照是否存在
|
||||
@@ -85,7 +86,7 @@ async def have_snapshot_for_current_operation_endpoint(network: str = Query(...,
|
||||
"""
|
||||
return have_snapshot_for_current_operation(network)
|
||||
|
||||
@router.post("/takesnapshotforoperation/", summary="为操作创建快照", description="为指定的操作创建快照")
|
||||
@router.post("/snapshot-for-operations", summary="为操作创建快照", description="为指定的操作创建快照")
|
||||
async def take_snapshot_for_operation_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
operation: int = Query(..., description="操作ID"),
|
||||
@@ -98,7 +99,7 @@ async def take_snapshot_for_operation_endpoint(
|
||||
"""
|
||||
return take_snapshot_for_operation(network, operation, tag)
|
||||
|
||||
@router.post("/takesnapshotforcurrentoperation", summary="为当前操作创建快照", description="为当前操作创建快照")
|
||||
@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:
|
||||
"""
|
||||
为当前操作创建快照
|
||||
@@ -107,17 +108,7 @@ async def take_snapshot_for_current_operation_endpoint(network: str = Query(...,
|
||||
"""
|
||||
return take_snapshot_for_current_operation(network, tag)
|
||||
|
||||
# 兼容旧拼写: takenapshotforcurrentoperation
|
||||
@router.post("/takenapshotforcurrentoperation", summary="为当前操作创建快照(兼容模式)", description="为当前操作创建快照(兼容旧的API路径)")
|
||||
async def take_snapshot_for_current_operation_legacy_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签")) -> None:
|
||||
"""
|
||||
为当前操作创建快照(兼容模式)
|
||||
|
||||
兼容旧的API路径,为网络当前操作创建一个快照
|
||||
"""
|
||||
return take_snapshot_for_current_operation(network, tag)
|
||||
|
||||
@router.post("/takesnapshot/", summary="创建快照", description="为网络创建一个快照")
|
||||
@router.post("/snapshots", summary="创建快照", description="为网络创建一个快照")
|
||||
async def take_snapshot_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), tag: str = Query(..., description="快照标签")) -> None:
|
||||
"""
|
||||
创建快照
|
||||
@@ -126,7 +117,7 @@ async def take_snapshot_endpoint(network: str = Query(..., description="管网
|
||||
"""
|
||||
return take_snapshot(network, tag)
|
||||
|
||||
@router.post("/picksnapshot/", summary="选择快照", description="选择并恢复到指定的快照", response_model=None)
|
||||
@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:
|
||||
"""
|
||||
选择快照
|
||||
@@ -135,7 +126,7 @@ async def pick_snapshot_endpoint(network: str = Query(..., description="管网
|
||||
"""
|
||||
return pick_snapshot(network, tag, discard)
|
||||
|
||||
@router.post("/pickoperation/", summary="选择操作", description="选择并恢复到指定的操作", response_model=None)
|
||||
@router.patch("/operations", summary="选择操作", description="选择并恢复到指定的操作", response_model=None)
|
||||
async def pick_operation_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
operation: int = Query(..., description="操作ID"),
|
||||
@@ -148,8 +139,12 @@ async def pick_operation_endpoint(
|
||||
"""
|
||||
return pick_operation(network, operation, discard)
|
||||
|
||||
@router.get("/syncwithserver/", summary="与服务器同步", description="将网络与服务器同步到指定操作", response_model=None)
|
||||
async def sync_with_server_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), operation: int = Query(..., description="目标操作ID")) -> ChangeSet:
|
||||
@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:
|
||||
"""
|
||||
与服务器同步
|
||||
|
||||
@@ -157,7 +152,7 @@ async def sync_with_server_endpoint(network: str = Query(..., description="管
|
||||
"""
|
||||
return sync_with_server(network, operation)
|
||||
|
||||
@router.post("/batch/", summary="执行批量命令", description="执行多个网络操作命令", response_model=None)
|
||||
@router.post("/network-command-batches", summary="执行批量命令", description="执行多个网络操作命令", response_model=None)
|
||||
async def execute_batch_commands_endpoint(network: str = Query(..., description="管网名称(或数据库名称)"), req: Request = None) -> ChangeSet:
|
||||
"""
|
||||
执行批量命令
|
||||
@@ -170,7 +165,7 @@ async def execute_batch_commands_endpoint(network: str = Query(..., description=
|
||||
rcs = execute_batch_commands(network, cs)
|
||||
return rcs
|
||||
|
||||
@router.post("/compressedbatch/", summary="执行压缩批量命令", description="执行压缩的批量命令", response_model=None)
|
||||
@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
|
||||
@@ -185,7 +180,7 @@ async def execute_compressed_batch_commands_endpoint(
|
||||
cs.operations = jo_root["operations"]
|
||||
return execute_batch_command(network, cs)
|
||||
|
||||
@router.get("/getrestoreoperation/", summary="获取恢复操作ID", description="获取网络的恢复操作ID")
|
||||
@router.get("/restore-operations", summary="获取恢复操作ID", description="获取网络的恢复操作ID")
|
||||
async def get_restore_operation_endpoint(network: str = Query(..., description="管网名称(或数据库名称)")) -> int:
|
||||
"""
|
||||
获取恢复操作ID
|
||||
@@ -194,7 +189,7 @@ async def get_restore_operation_endpoint(network: str = Query(..., description="
|
||||
"""
|
||||
return get_restore_operation(network)
|
||||
|
||||
@router.post("/setrestoreoperation/", summary="设置恢复操作ID", description="设置网络的恢复操作ID")
|
||||
@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
|
||||
|
||||
@@ -8,7 +8,7 @@ from .dependencies import get_timescale_connection, get_postgres_connection
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/composite/scada-simulation", summary="获取SCADA关联的模拟数据")
|
||||
@router.get("/timeseries/views/scada-simulations", summary="获取SCADA关联的模拟数据")
|
||||
async def get_scada_associated_simulation_data(
|
||||
start_time: datetime = Query(..., description="查询开始时间"),
|
||||
end_time: datetime = Query(..., description="查询结束时间"),
|
||||
@@ -73,7 +73,7 @@ async def get_scada_associated_simulation_data(
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/composite/element-simulation", summary="获取管网元素的模拟数据")
|
||||
@router.get("/timeseries/views/element-simulations", summary="获取管网元素的模拟数据")
|
||||
async def get_feature_simulation_data(
|
||||
start_time: datetime = Query(..., description="查询开始时间"),
|
||||
end_time: datetime = Query(..., description="查询结束时间"),
|
||||
@@ -143,7 +143,7 @@ async def get_feature_simulation_data(
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/composite/element-scada", summary="获取管网元素关联的SCADA监测数据")
|
||||
@router.get("/timeseries/views/element-scada-readings", summary="获取管网元素关联的SCADA监测数据")
|
||||
async def get_element_associated_scada_data(
|
||||
element_id: str = Query(..., description="管网元素ID(管道或节点)"),
|
||||
start_time: datetime = Query(..., description="查询开始时间"),
|
||||
@@ -185,7 +185,7 @@ async def get_element_associated_scada_data(
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/composite/clean-scada", summary="清洗SCADA监测数据")
|
||||
@router.post("/timeseries/scada-cleaning-runs", summary="清洗SCADA监测数据")
|
||||
async def clean_scada_data(
|
||||
device_ids: str = Query(..., description="设备ID列表或 'all' 表示清洗所有设备"),
|
||||
start_time: datetime = Query(..., description="清洗数据的开始时间"),
|
||||
@@ -228,7 +228,7 @@ async def clean_scada_data(
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/composite/pipeline-health-prediction", summary="预测管道健康状况")
|
||||
@router.get("/pipeline-health-predictions", summary="预测管道健康状况")
|
||||
async def predict_pipeline_health(
|
||||
query_time: datetime = Query(..., description="查询时间"),
|
||||
network_name: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
|
||||
@@ -13,7 +13,7 @@ TIME_RANGE_START_DESC = f"时间范围开始时间。{TIME_WITH_TZ_DESC}"
|
||||
TIME_RANGE_END_DESC = f"时间范围结束时间。{TIME_WITH_TZ_DESC}"
|
||||
|
||||
|
||||
@router.post("/realtime/links/batch", status_code=201, summary="批量插入实时管道数据")
|
||||
@router.post("/timeseries/realtime/links/batches", status_code=201, summary="批量插入实时管道数据")
|
||||
async def insert_realtime_links(
|
||||
data: List[dict] = Body(..., description="管道数据列表,每项包含管道ID、时间戳等信息"),
|
||||
conn: AsyncConnection = Depends(get_timescale_connection)
|
||||
@@ -34,7 +34,7 @@ async def insert_realtime_links(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/realtime/links",
|
||||
"/timeseries/realtime/links",
|
||||
summary="查询实时管道数据",
|
||||
description="按时间范围查询实时管道数据。start_time 和 end_time 必须显式带时区;允许传 UTC+8,服务端会先归一化为 UTC 再执行查询。",
|
||||
)
|
||||
@@ -60,7 +60,7 @@ async def get_realtime_links(
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/realtime/links",
|
||||
"/timeseries/realtime/links",
|
||||
summary="删除实时管道数据",
|
||||
description="按时间范围删除实时管道数据。start_time 和 end_time 必须显式带时区;允许传 UTC+8,服务端按请求中的绝对时间删除对应 UTC 数据。",
|
||||
)
|
||||
@@ -85,7 +85,7 @@ async def delete_realtime_links(
|
||||
return {"message": "Deleted successfully"}
|
||||
|
||||
|
||||
@router.patch("/realtime/links/{link_id}/field", summary="更新实时管道字段")
|
||||
@router.patch("/timeseries/realtime/links/{link_id}/field", summary="更新实时管道字段")
|
||||
async def update_realtime_link_field(
|
||||
link_id: str = Path(..., description="管道ID"),
|
||||
time: datetime = Query(..., description=f"要更新记录的时间戳。{TIME_WITH_TZ_DESC}"),
|
||||
@@ -117,7 +117,7 @@ async def update_realtime_link_field(
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/realtime/nodes/batch", status_code=201, summary="批量插入实时节点数据")
|
||||
@router.post("/timeseries/realtime/nodes/batches", status_code=201, summary="批量插入实时节点数据")
|
||||
async def insert_realtime_nodes(
|
||||
data: List[dict] = Body(..., description="节点数据列表,每项包含节点ID、时间戳等信息"),
|
||||
conn: AsyncConnection = Depends(get_timescale_connection)
|
||||
@@ -138,7 +138,7 @@ async def insert_realtime_nodes(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/realtime/nodes",
|
||||
"/timeseries/realtime/nodes",
|
||||
summary="查询实时节点数据",
|
||||
description="按时间范围查询实时节点数据。start_time 和 end_time 必须显式带时区;允许传 UTC+8,服务端会先归一化为 UTC 再执行查询。",
|
||||
)
|
||||
@@ -164,7 +164,7 @@ async def get_realtime_nodes(
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/realtime/nodes",
|
||||
"/timeseries/realtime/nodes",
|
||||
summary="删除实时节点数据",
|
||||
description="按时间范围删除实时节点数据。start_time 和 end_time 必须显式带时区;允许传 UTC+8,服务端按请求中的绝对时间删除对应 UTC 数据。",
|
||||
)
|
||||
@@ -191,7 +191,7 @@ async def delete_realtime_nodes(
|
||||
|
||||
|
||||
|
||||
@router.post("/realtime/simulation/store", status_code=201, summary="存储实时模拟结果")
|
||||
@router.post("/timeseries/realtime/simulation-results", status_code=201, summary="存储实时模拟结果")
|
||||
async def store_realtime_simulation_result(
|
||||
node_result_list: List[dict] = Body(..., description="节点模拟结果列表"),
|
||||
link_result_list: List[dict] = Body(..., description="管道模拟结果列表"),
|
||||
@@ -218,7 +218,7 @@ async def store_realtime_simulation_result(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/realtime/query/by-time-property",
|
||||
"/timeseries/realtime/records",
|
||||
summary="按时间和属性查询实时数据",
|
||||
description="查询指定时间点的实时属性值。query_time 必须显式带时区;允许传 UTC+8,服务端会先归一化为 UTC 再执行查询。",
|
||||
)
|
||||
@@ -254,7 +254,7 @@ async def query_realtime_records_by_time_property(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/realtime/query/by-id-time",
|
||||
"/timeseries/realtime/simulation-results",
|
||||
summary="按ID和时间查询实时模拟数据",
|
||||
description="查询指定元素在某一时间点的实时模拟结果。query_time 必须显式带时区;允许传 UTC+8,服务端会先归一化为 UTC 再执行查询。",
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ from .dependencies import get_timescale_connection
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/scada/batch", status_code=201, summary="批量插入SCADA监测数据")
|
||||
@router.post("/timeseries/scada-readings/batches", status_code=201, summary="批量插入SCADA监测数据")
|
||||
async def insert_scada_data(
|
||||
data: List[dict] = Body(..., description="SCADA设备监测数据列表"),
|
||||
conn: AsyncConnection = Depends(get_timescale_connection),
|
||||
@@ -29,7 +29,7 @@ async def insert_scada_data(
|
||||
return {"message": f"Inserted {len(data)} records"}
|
||||
|
||||
|
||||
@router.get("/scada/by-ids-time-range", summary="按设备ID和时间范围查询SCADA数据")
|
||||
@router.get("/timeseries/scada-readings", summary="按设备ID和时间范围查询SCADA数据")
|
||||
async def get_scada_by_ids_time_range(
|
||||
start_time: datetime = Query(..., description="查询开始时间"),
|
||||
end_time: datetime = Query(..., description="查询结束时间"),
|
||||
@@ -60,7 +60,7 @@ async def get_scada_by_ids_time_range(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/scada/by-ids-field-time-range", summary="按设备ID、字段和时间范围查询SCADA数据"
|
||||
"/timeseries/scada-readings/fields", summary="按设备ID、字段和时间范围查询SCADA数据"
|
||||
)
|
||||
async def get_scada_field_by_ids_time_range(
|
||||
start_time: datetime = Query(..., description="查询开始时间"),
|
||||
@@ -101,7 +101,7 @@ async def get_scada_field_by_ids_time_range(
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.patch("/scada/{device_id}/field", summary="更新SCADA设备字段")
|
||||
@router.patch("/timeseries/scada-readings/{device_id}/field", summary="更新SCADA设备字段")
|
||||
async def update_scada_field(
|
||||
device_id: str = Path(..., description="设备ID"),
|
||||
time: datetime = Query(..., description="更新数据的时间戳"),
|
||||
@@ -133,7 +133,7 @@ async def update_scada_field(
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/scada/by-id-time-range", summary="按设备ID和时间范围删除SCADA数据")
|
||||
@router.delete("/timeseries/scada-readings", summary="按设备ID和时间范围删除SCADA数据")
|
||||
async def delete_scada_data(
|
||||
device_id: str = Query(..., description="设备ID"),
|
||||
start_time: datetime = Query(..., description="删除开始时间"),
|
||||
|
||||
@@ -9,7 +9,7 @@ from .dependencies import get_timescale_connection
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/scheme/links/batch", status_code=201, summary="批量插入方案管道数据")
|
||||
@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),
|
||||
@@ -29,7 +29,7 @@ async def insert_scheme_links(
|
||||
return {"message": f"Inserted {len(data)} records"}
|
||||
|
||||
|
||||
@router.get("/scheme/links", summary="查询方案管道数据")
|
||||
@router.get("/timeseries/schemes/links", summary="查询方案管道数据")
|
||||
async def get_scheme_links(
|
||||
scheme_type: str = Query(..., description="方案类型"),
|
||||
scheme_name: str = Query(..., description="方案名称"),
|
||||
@@ -56,7 +56,7 @@ async def get_scheme_links(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/scheme/links/{link_id}/field", summary="查询方案管道字段数据")
|
||||
@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="方案类型"),
|
||||
@@ -93,7 +93,7 @@ async def get_scheme_link_field(
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.patch("/scheme/links/{link_id}/field", summary="更新方案管道字段")
|
||||
@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="方案类型"),
|
||||
@@ -131,7 +131,7 @@ async def update_scheme_link_field(
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/scheme/links", summary="删除方案管道数据")
|
||||
@router.delete("/timeseries/schemes/links", summary="删除方案管道数据")
|
||||
async def delete_scheme_links(
|
||||
scheme_type: str = Query(..., description="方案类型"),
|
||||
scheme_name: str = Query(..., description="方案名称"),
|
||||
@@ -159,7 +159,7 @@ async def delete_scheme_links(
|
||||
return {"message": "Deleted successfully"}
|
||||
|
||||
|
||||
@router.post("/scheme/nodes/batch", status_code=201, summary="批量插入方案节点数据")
|
||||
@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),
|
||||
@@ -179,7 +179,7 @@ async def insert_scheme_nodes(
|
||||
return {"message": f"Inserted {len(data)} records"}
|
||||
|
||||
|
||||
@router.get("/scheme/nodes/{node_id}/field", summary="查询方案节点字段数据")
|
||||
@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="方案类型"),
|
||||
@@ -216,7 +216,7 @@ async def get_scheme_node_field(
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.patch("/scheme/nodes/{node_id}/field", summary="更新方案节点字段")
|
||||
@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="方案类型"),
|
||||
@@ -254,7 +254,7 @@ async def update_scheme_node_field(
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/scheme/nodes", summary="删除方案节点数据")
|
||||
@router.delete("/timeseries/schemes/nodes", summary="删除方案节点数据")
|
||||
async def delete_scheme_nodes(
|
||||
scheme_type: str = Query(..., description="方案类型"),
|
||||
scheme_name: str = Query(..., description="方案名称"),
|
||||
@@ -282,7 +282,7 @@ async def delete_scheme_nodes(
|
||||
return {"message": "Deleted successfully"}
|
||||
|
||||
|
||||
@router.post("/scheme/simulation/store", status_code=201, summary="存储方案模拟结果")
|
||||
@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="方案名称"),
|
||||
@@ -318,7 +318,7 @@ async def store_scheme_simulation_result(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/scheme/query/by-scheme-time-property", summary="按方案、时间和属性查询数据"
|
||||
"/timeseries/schemes/records", summary="按方案、时间和属性查询数据"
|
||||
)
|
||||
async def query_scheme_records_by_scheme_time_property(
|
||||
scheme_type: str = Query(..., description="方案类型"),
|
||||
@@ -355,7 +355,7 @@ async def query_scheme_records_by_scheme_time_property(
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/scheme/query/by-id-time", summary="按ID和时间查询方案模拟数据")
|
||||
@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="方案名称"),
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
from fastapi import APIRouter, Request, Query
|
||||
from typing import Any, List, Dict, Union
|
||||
from app.services.tjnetwork import Any, get_all_users, get_user, get_user_schema
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
###########################################################
|
||||
# user 39
|
||||
###########################################################
|
||||
|
||||
@router.get("/getuserschema/", summary="获取用户模式", description="获取指定网络的用户模式定义")
|
||||
async def fastapi_get_user_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[Any, Any]]:
|
||||
"""
|
||||
获取用户模式定义
|
||||
|
||||
返回指定网络的用户模式结构定义
|
||||
"""
|
||||
return get_user_schema(network)
|
||||
|
||||
@router.get("/getuser/", summary="获取单个用户", description="获取指定网络中的单个用户信息")
|
||||
async def fastapi_get_user(network: str = Query(..., description="管网名称(或数据库名称)"), user_name: str = Query(..., description="用户名")) -> dict[Any, Any]:
|
||||
"""
|
||||
获取用户信息
|
||||
|
||||
返回指定网络中指定用户名的详细信息
|
||||
"""
|
||||
return get_user(network, user_name)
|
||||
|
||||
@router.get("/getallusers/", summary="获取所有用户", description="获取指定网络的所有用户列表")
|
||||
async def fastapi_get_all_users(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[Any, Any]]:
|
||||
"""
|
||||
获取所有用户列表
|
||||
|
||||
返回指定网络中所有用户的信息
|
||||
"""
|
||||
return get_all_users(network)
|
||||
@@ -13,7 +13,7 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/web-search",
|
||||
"/web-searches",
|
||||
summary="Web Search",
|
||||
description="调用 Bocha Web Search API 获取实时网页搜索结果",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import re
|
||||
from collections.abc import Iterable
|
||||
from copy import copy
|
||||
from functools import wraps
|
||||
from typing import Any, Generic, TypeVar, get_args, get_origin
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.routing import APIRoute
|
||||
from pydantic import BaseModel, JsonValue, create_model
|
||||
from starlette.responses import Response
|
||||
|
||||
from app.api.problem_details import ProblemDetails
|
||||
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.project_dependencies import ProjectContext, get_project_context
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Page(BaseModel, Generic[T]):
|
||||
items: list[T]
|
||||
total: int
|
||||
limit: int
|
||||
offset: int
|
||||
|
||||
|
||||
_NAME_IS_NETWORK = {
|
||||
"pressure_sensor_placement_sensitivity_endpoint",
|
||||
"pressure_sensor_placement_kmeans_endpoint",
|
||||
}
|
||||
_DERIVE_USERNAME = {
|
||||
"pressure_sensor_placement_sensitivity_endpoint": "username",
|
||||
"pressure_sensor_placement_kmeans_endpoint": "username",
|
||||
"fastapi_pressure_sensor_placement": "user_name",
|
||||
}
|
||||
_PUBLIC_PARAMETER_RENAMES = {
|
||||
"burst_ID": "burst_id",
|
||||
"drainage_node_ID": "drainage_node_id",
|
||||
}
|
||||
_MODEL_NAME_IS_NETWORK = {"RunSimulationManuallyByDate"}
|
||||
_MODEL_USERNAME_FROM_AUTH: set[str] = set()
|
||||
|
||||
|
||||
def _clean_name(name: str) -> str:
|
||||
for prefix in ("fastapi_", "fast_"):
|
||||
if name.startswith(prefix):
|
||||
name = name[len(prefix) :]
|
||||
break
|
||||
if name.endswith("_endpoint"):
|
||||
name = name[: -len("_endpoint")]
|
||||
return name
|
||||
|
||||
|
||||
def _rest_body_model(annotation):
|
||||
if not inspect.isclass(annotation) or not issubclass(annotation, BaseModel):
|
||||
return None
|
||||
project_fields = {
|
||||
name
|
||||
for name in ("network", "network_name")
|
||||
if name in annotation.model_fields
|
||||
}
|
||||
if annotation.__name__ in _MODEL_NAME_IS_NETWORK and "name" in annotation.model_fields:
|
||||
project_fields.add("name")
|
||||
username_fields = (
|
||||
{
|
||||
name
|
||||
for name in ("username", "user_name")
|
||||
if name in annotation.model_fields
|
||||
}
|
||||
if annotation.__name__ in _MODEL_USERNAME_FROM_AUTH
|
||||
else set()
|
||||
)
|
||||
excluded_fields = project_fields | username_fields
|
||||
if not excluded_fields:
|
||||
return None
|
||||
|
||||
public_fields = {
|
||||
name: (field.annotation, copy(field))
|
||||
for name, field in annotation.model_fields.items()
|
||||
if name not in excluded_fields
|
||||
}
|
||||
public_model = create_model(
|
||||
f"{annotation.__name__}Rest",
|
||||
__module__=annotation.__module__,
|
||||
**public_fields,
|
||||
)
|
||||
return annotation, public_model, project_fields, username_fields
|
||||
|
||||
|
||||
def _with_header_project_context(endpoint, route_name: str):
|
||||
signature = inspect.signature(endpoint)
|
||||
network_parameters = [
|
||||
name for name in ("network", "network_name") if name in signature.parameters
|
||||
]
|
||||
if route_name in _NAME_IS_NETWORK and "name" in signature.parameters:
|
||||
network_parameters.append("name")
|
||||
username_parameter = _DERIVE_USERNAME.get(route_name)
|
||||
parameter_renames = {
|
||||
internal: public
|
||||
for internal, public in _PUBLIC_PARAMETER_RENAMES.items()
|
||||
if internal in signature.parameters
|
||||
}
|
||||
body_models = {
|
||||
name: body_model
|
||||
for name, parameter in signature.parameters.items()
|
||||
if (body_model := _rest_body_model(parameter.annotation)) is not None
|
||||
}
|
||||
model_has_username = any(model[3] for model in body_models.values())
|
||||
if (
|
||||
not network_parameters
|
||||
and not username_parameter
|
||||
and not parameter_renames
|
||||
and not body_models
|
||||
):
|
||||
return endpoint
|
||||
|
||||
existing_context_parameter = next(
|
||||
(
|
||||
name
|
||||
for name, parameter in signature.parameters.items()
|
||||
if parameter.annotation is ProjectContext
|
||||
),
|
||||
None,
|
||||
)
|
||||
injected_context_name = existing_context_parameter or "_rest_project_context"
|
||||
injected_user_name = "_rest_current_user"
|
||||
|
||||
@wraps(endpoint)
|
||||
async def wrapper(*args, **kwargs):
|
||||
project_context = kwargs.get(injected_context_name)
|
||||
if not isinstance(project_context, ProjectContext):
|
||||
raise RuntimeError("REST project context was not resolved")
|
||||
if not existing_context_parameter:
|
||||
kwargs.pop(injected_context_name, None)
|
||||
for parameter_name in network_parameters:
|
||||
kwargs[parameter_name] = project_context.project_code
|
||||
if username_parameter:
|
||||
kwargs[username_parameter] = kwargs[injected_user_name].username
|
||||
kwargs.pop(injected_user_name, None)
|
||||
for internal_name, public_name in parameter_renames.items():
|
||||
kwargs[internal_name] = kwargs.pop(public_name)
|
||||
for parameter_name, (
|
||||
original_model,
|
||||
_public_model,
|
||||
project_fields,
|
||||
username_fields,
|
||||
) in body_models.items():
|
||||
data = kwargs[parameter_name].model_dump()
|
||||
data.update(
|
||||
{field_name: project_context.project_code for field_name in project_fields}
|
||||
)
|
||||
if username_fields:
|
||||
current_user = kwargs[injected_user_name]
|
||||
data.update(
|
||||
{field_name: current_user.username for field_name in username_fields}
|
||||
)
|
||||
kwargs[parameter_name] = original_model.model_validate(data)
|
||||
if model_has_username:
|
||||
kwargs.pop(injected_user_name, None)
|
||||
result = endpoint(*args, **kwargs)
|
||||
if inspect.isawaitable(result):
|
||||
return await result
|
||||
return result
|
||||
|
||||
parameters = []
|
||||
for name, parameter in signature.parameters.items():
|
||||
if name in network_parameters or name == username_parameter:
|
||||
continue
|
||||
public_name = parameter_renames.get(name, name)
|
||||
if public_name != name:
|
||||
default = copy(parameter.default)
|
||||
default.alias = public_name
|
||||
default.validation_alias = public_name
|
||||
default.serialization_alias = public_name
|
||||
parameter = parameter.replace(name=public_name, default=default)
|
||||
if name in body_models:
|
||||
parameter = parameter.replace(annotation=body_models[name][1])
|
||||
parameters.append(parameter)
|
||||
if not existing_context_parameter:
|
||||
parameters.append(
|
||||
inspect.Parameter(
|
||||
injected_context_name,
|
||||
kind=inspect.Parameter.KEYWORD_ONLY,
|
||||
annotation=ProjectContext,
|
||||
default=Depends(get_project_context),
|
||||
)
|
||||
)
|
||||
if username_parameter or model_has_username:
|
||||
parameters.append(
|
||||
inspect.Parameter(
|
||||
injected_user_name,
|
||||
kind=inspect.Parameter.KEYWORD_ONLY,
|
||||
default=Depends(get_current_metadata_user),
|
||||
)
|
||||
)
|
||||
wrapper.__signature__ = signature.replace(parameters=parameters)
|
||||
return wrapper
|
||||
|
||||
|
||||
def _with_pagination(endpoint):
|
||||
signature = inspect.signature(endpoint)
|
||||
handler_limit_parameter = "limit" if "limit" in signature.parameters else None
|
||||
handler_offset_parameter = next(
|
||||
(
|
||||
parameter_name
|
||||
for parameter_name in ("offset", "skip")
|
||||
if parameter_name in signature.parameters
|
||||
),
|
||||
None,
|
||||
)
|
||||
handler_handles_pagination = bool(
|
||||
handler_limit_parameter or handler_offset_parameter
|
||||
)
|
||||
|
||||
@wraps(endpoint)
|
||||
async def wrapper(*args, **kwargs):
|
||||
if handler_handles_pagination:
|
||||
limit = kwargs.get(handler_limit_parameter, 0)
|
||||
offset = kwargs.get(handler_offset_parameter, 0)
|
||||
else:
|
||||
limit = kwargs.pop("_rest_limit")
|
||||
offset = kwargs.pop("_rest_offset")
|
||||
result = endpoint(*args, **kwargs)
|
||||
if inspect.isawaitable(result):
|
||||
result = await result
|
||||
if not isinstance(result, list):
|
||||
return result
|
||||
if handler_handles_pagination:
|
||||
return Page(
|
||||
items=result,
|
||||
total=offset + len(result),
|
||||
limit=limit or len(result),
|
||||
offset=offset,
|
||||
)
|
||||
return Page(
|
||||
items=result[offset : offset + limit],
|
||||
total=len(result),
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
parameters = list(signature.parameters.values())
|
||||
if not handler_handles_pagination:
|
||||
parameters.extend(
|
||||
[
|
||||
inspect.Parameter(
|
||||
"_rest_limit",
|
||||
kind=inspect.Parameter.KEYWORD_ONLY,
|
||||
annotation=int,
|
||||
default=Query(100, ge=1, le=1000, alias="limit"),
|
||||
),
|
||||
inspect.Parameter(
|
||||
"_rest_offset",
|
||||
kind=inspect.Parameter.KEYWORD_ONLY,
|
||||
annotation=int,
|
||||
default=Query(0, ge=0, alias="offset"),
|
||||
),
|
||||
]
|
||||
)
|
||||
wrapper.__signature__ = signature.replace(parameters=parameters)
|
||||
return wrapper
|
||||
|
||||
|
||||
def _with_jsonable_response(endpoint):
|
||||
"""Normalize untyped handler results before JsonValue validation."""
|
||||
|
||||
@wraps(endpoint)
|
||||
async def wrapper(*args, **kwargs):
|
||||
result = endpoint(*args, **kwargs)
|
||||
if inspect.isawaitable(result):
|
||||
result = await result
|
||||
if isinstance(result, Response):
|
||||
return result
|
||||
return jsonable_encoder(result)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def _adapt_route(route: APIRoute) -> APIRoute:
|
||||
methods = route.methods or set()
|
||||
if len(methods) != 1:
|
||||
raise RuntimeError(
|
||||
f"REST route {route.name!r} must declare exactly one HTTP method"
|
||||
)
|
||||
method = next(iter(methods))
|
||||
responses = dict(route.responses or {})
|
||||
for status_code, description in (
|
||||
(401, "Authentication required"),
|
||||
(403, "Insufficient permission"),
|
||||
(404, "Resource not found"),
|
||||
(409, "Resource conflict"),
|
||||
(422, "Validation error"),
|
||||
(503, "Dependency unavailable"),
|
||||
):
|
||||
responses.setdefault(
|
||||
status_code,
|
||||
{"model": ProblemDetails, "description": description},
|
||||
)
|
||||
|
||||
endpoint = _with_header_project_context(route.endpoint, route.name)
|
||||
response_model = route.response_model
|
||||
has_untyped_response = response_model is None
|
||||
if has_untyped_response:
|
||||
endpoint = _with_jsonable_response(endpoint)
|
||||
if get_origin(response_model) is list:
|
||||
item_type = get_args(response_model)[0] if get_args(response_model) else JsonValue
|
||||
response_model = Page[item_type]
|
||||
endpoint = _with_pagination(endpoint)
|
||||
|
||||
clean_name = _clean_name(route.name)
|
||||
creates_resource = clean_name.startswith(
|
||||
("add_", "create_", "copy_", "import_", "insert_", "store_", "take_", "upload_")
|
||||
) or route.name == "fastapi_pressure_sensor_placement"
|
||||
status_code = (
|
||||
204
|
||||
if method == "DELETE"
|
||||
else 201
|
||||
if method == "POST" and creates_resource
|
||||
else route.status_code
|
||||
)
|
||||
if status_code == 204:
|
||||
response_model = None
|
||||
elif response_model is None:
|
||||
response_model = JsonValue
|
||||
|
||||
return APIRoute(
|
||||
path=route.path,
|
||||
endpoint=endpoint,
|
||||
response_model=response_model,
|
||||
status_code=status_code,
|
||||
tags=route.tags,
|
||||
dependencies=route.dependencies,
|
||||
summary=route.summary,
|
||||
description=route.description,
|
||||
response_description=route.response_description,
|
||||
responses=responses,
|
||||
deprecated=False,
|
||||
name=route.name,
|
||||
methods={method},
|
||||
operation_id=f"{method.lower()}_{re.sub(r'[^a-z0-9]+', '_', route.path).strip('_')}",
|
||||
response_model_include=route.response_model_include,
|
||||
response_model_exclude=route.response_model_exclude,
|
||||
response_model_by_alias=route.response_model_by_alias,
|
||||
response_model_exclude_unset=route.response_model_exclude_unset,
|
||||
response_model_exclude_defaults=route.response_model_exclude_defaults,
|
||||
response_model_exclude_none=route.response_model_exclude_none,
|
||||
include_in_schema=route.include_in_schema,
|
||||
response_class=route.response_class,
|
||||
callbacks=route.callbacks,
|
||||
openapi_extra=route.openapi_extra,
|
||||
)
|
||||
|
||||
|
||||
def build_rest_router(routes: Iterable[Any]) -> APIRouter:
|
||||
router = APIRouter()
|
||||
seen: dict[tuple[str, str], APIRoute] = {}
|
||||
operation_ids: set[str] = set()
|
||||
|
||||
for route in routes:
|
||||
if not isinstance(route, APIRoute):
|
||||
continue
|
||||
|
||||
methods = route.methods or set()
|
||||
if len(methods) != 1:
|
||||
raise RuntimeError(
|
||||
f"REST route {route.name!r} must declare exactly one HTTP method"
|
||||
)
|
||||
method = next(iter(methods))
|
||||
key = (method, route.path)
|
||||
if key in seen:
|
||||
previous = seen[key]
|
||||
raise RuntimeError(
|
||||
"REST route collision for "
|
||||
f"{method} {route.path}: {previous.name!r} and {route.name!r}."
|
||||
)
|
||||
|
||||
adapted = _adapt_route(route)
|
||||
if adapted.operation_id in operation_ids:
|
||||
adapted.operation_id = f"{adapted.operation_id}_{route.name}"
|
||||
seen[key] = route
|
||||
operation_ids.add(adapted.operation_id or "")
|
||||
router.routes.append(adapted)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
api_router = build_rest_router(handler_api_router.routes)
|
||||
+202
-90
@@ -1,118 +1,230 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.api.v1.endpoints import (
|
||||
access,
|
||||
admin_metadata,
|
||||
agent_auth,
|
||||
project,
|
||||
simulation,
|
||||
scada,
|
||||
extension,
|
||||
snapshots,
|
||||
# data_query,
|
||||
users,
|
||||
schemes,
|
||||
misc,
|
||||
risk,
|
||||
cache,
|
||||
leakage,
|
||||
audit,
|
||||
burst_detection,
|
||||
burst_location,
|
||||
audit, # 新增:审计日志
|
||||
meta,
|
||||
web_search,
|
||||
cache,
|
||||
extension,
|
||||
geocoding,
|
||||
)
|
||||
from app.api.v1.endpoints.network import (
|
||||
general,
|
||||
junctions,
|
||||
reservoirs,
|
||||
tanks,
|
||||
pipes,
|
||||
pumps,
|
||||
valves,
|
||||
tags,
|
||||
demands,
|
||||
geometry,
|
||||
regions,
|
||||
leakage,
|
||||
meta,
|
||||
misc,
|
||||
model_import,
|
||||
project,
|
||||
project_data,
|
||||
risk,
|
||||
scada,
|
||||
schemes,
|
||||
sensor_placement,
|
||||
simulation,
|
||||
snapshots,
|
||||
web_search,
|
||||
)
|
||||
from app.api.v1.endpoints.components import (
|
||||
curves,
|
||||
patterns,
|
||||
controls,
|
||||
curves,
|
||||
options,
|
||||
patterns,
|
||||
quality,
|
||||
visuals,
|
||||
)
|
||||
|
||||
from app.api.v1.endpoints import project_data
|
||||
from app.api.v1.endpoints.network import (
|
||||
demands,
|
||||
general,
|
||||
geometry,
|
||||
junctions,
|
||||
pipes,
|
||||
pumps,
|
||||
regions,
|
||||
reservoirs,
|
||||
tags,
|
||||
tanks,
|
||||
valves,
|
||||
)
|
||||
from app.api.v1.endpoints.timeseries import (
|
||||
realtime as ts_realtime,
|
||||
scheme as ts_scheme,
|
||||
scada as ts_scada,
|
||||
composite as ts_composite,
|
||||
realtime as ts_realtime,
|
||||
scada as ts_scada,
|
||||
scheme as ts_scheme,
|
||||
)
|
||||
from app.auth.permissions import (
|
||||
BURST_RUN,
|
||||
OPTIMIZATION_RUN,
|
||||
RISK_RUN,
|
||||
SCADA_CLEAN,
|
||||
SCADA_VIEW,
|
||||
SIMULATION_RUN,
|
||||
SIMULATION_VIEW,
|
||||
WEBGIS_EDIT,
|
||||
WEBGIS_VIEW,
|
||||
require_method_permission,
|
||||
require_permission,
|
||||
)
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
# Core Services
|
||||
webgis_access = Depends(
|
||||
require_method_permission(
|
||||
read_permission=WEBGIS_VIEW,
|
||||
write_permission=WEBGIS_EDIT,
|
||||
)
|
||||
)
|
||||
scada_access = Depends(
|
||||
require_method_permission(
|
||||
read_permission=SCADA_VIEW,
|
||||
write_permission=SCADA_CLEAN,
|
||||
)
|
||||
)
|
||||
simulation_access = Depends(
|
||||
require_method_permission(
|
||||
read_permission=SIMULATION_VIEW,
|
||||
write_permission=SIMULATION_RUN,
|
||||
)
|
||||
)
|
||||
|
||||
webgis_view_access = Depends(require_permission(WEBGIS_VIEW))
|
||||
simulation_run_access = Depends(require_permission(SIMULATION_RUN))
|
||||
burst_run_access = Depends(require_permission(BURST_RUN))
|
||||
risk_run_access = Depends(require_permission(RISK_RUN))
|
||||
optimization_run_access = Depends(require_permission(OPTIMIZATION_RUN))
|
||||
|
||||
# Core services
|
||||
api_router.include_router(access.router, tags=["Access Control"])
|
||||
api_router.include_router(agent_auth.router, tags=["Agent Auth"])
|
||||
api_router.include_router(
|
||||
admin_metadata.router, prefix="/admin", tags=["Metadata Admin"]
|
||||
admin_metadata.router,
|
||||
tags=["Metadata Admin"],
|
||||
)
|
||||
api_router.include_router(audit.router, prefix="/audit", tags=["Audit Logs"]) # 新增
|
||||
api_router.include_router(model_import.router, tags=["Model Administration"])
|
||||
api_router.include_router(audit.router, tags=["Audit Logs"])
|
||||
api_router.include_router(meta.router, tags=["Metadata"])
|
||||
api_router.include_router(project.router, tags=["Project"])
|
||||
|
||||
# Network Elements (Node/Link Types)
|
||||
api_router.include_router(general.router, tags=["Network General"])
|
||||
api_router.include_router(junctions.router, tags=["Junctions"])
|
||||
api_router.include_router(reservoirs.router, tags=["Reservoirs"])
|
||||
api_router.include_router(tanks.router, tags=["Tanks"])
|
||||
api_router.include_router(pipes.router, tags=["Pipes"])
|
||||
api_router.include_router(pumps.router, tags=["Pumps"])
|
||||
api_router.include_router(valves.router, tags=["Valves"])
|
||||
|
||||
# Network Features
|
||||
api_router.include_router(tags.router, tags=["Tags"])
|
||||
api_router.include_router(demands.router, tags=["Demands"])
|
||||
api_router.include_router(geometry.router, tags=["Geometry & Coordinates"])
|
||||
api_router.include_router(regions.router, tags=["Regions & DMAs"])
|
||||
|
||||
# Components & Controls
|
||||
api_router.include_router(curves.router, tags=["Curves"])
|
||||
api_router.include_router(patterns.router, tags=["Patterns"])
|
||||
api_router.include_router(controls.router, tags=["Controls & Rules"])
|
||||
api_router.include_router(options.router, tags=["Options"])
|
||||
api_router.include_router(quality.router, tags=["Quality"])
|
||||
api_router.include_router(visuals.router, tags=["Visuals"])
|
||||
|
||||
# Simulation & Data
|
||||
api_router.include_router(simulation.router, tags=["Simulation Control"])
|
||||
# api_router.include_router(data_query.router, tags=["Data Query & InfluxDB"])
|
||||
api_router.include_router(scada.router)
|
||||
api_router.include_router(snapshots.router, tags=["Snapshots"])
|
||||
api_router.include_router(users.router, tags=["Users"])
|
||||
api_router.include_router(schemes.router, tags=["Schemes"])
|
||||
api_router.include_router(misc.router, tags=["Misc"])
|
||||
api_router.include_router(risk.router, tags=["Risk"])
|
||||
api_router.include_router(cache.router, tags=["Cache"])
|
||||
api_router.include_router(web_search.router, tags=["Web Search"])
|
||||
api_router.include_router(geocoding.router, tags=["Geocoding"])
|
||||
api_router.include_router(leakage.router, prefix="/leakage", tags=["Leakage"])
|
||||
api_router.include_router(
|
||||
burst_detection.router, prefix="/burst-detection", tags=["Burst Detection"]
|
||||
)
|
||||
api_router.include_router(
|
||||
burst_location.router, prefix="/burst-location", tags=["Burst Location"]
|
||||
project.router,
|
||||
tags=["Project"],
|
||||
dependencies=[webgis_access],
|
||||
)
|
||||
|
||||
# TimescaleDB Data Access
|
||||
api_router.include_router(ts_realtime.router, tags=["TimescaleDB - Realtime"])
|
||||
api_router.include_router(ts_scheme.router, tags=["TimescaleDB - Scheme"])
|
||||
api_router.include_router(ts_scada.router, tags=["TimescaleDB - SCADA"])
|
||||
api_router.include_router(ts_composite.router, tags=["TimescaleDB - Composite"])
|
||||
# WebGIS data
|
||||
for endpoint_router, tag in (
|
||||
(general.router, "Network General"),
|
||||
(junctions.router, "Junctions"),
|
||||
(reservoirs.router, "Reservoirs"),
|
||||
(tanks.router, "Tanks"),
|
||||
(pipes.router, "Pipes"),
|
||||
(pumps.router, "Pumps"),
|
||||
(valves.router, "Valves"),
|
||||
(tags.router, "Tags"),
|
||||
(demands.router, "Demands"),
|
||||
(geometry.router, "Geometry & Coordinates"),
|
||||
(regions.router, "Regions & DMAs"),
|
||||
(curves.router, "Curves"),
|
||||
(patterns.router, "Patterns"),
|
||||
(controls.router, "Controls & Rules"),
|
||||
(options.router, "Options"),
|
||||
(quality.router, "Quality"),
|
||||
(visuals.router, "Visuals"),
|
||||
):
|
||||
api_router.include_router(
|
||||
endpoint_router,
|
||||
tags=[tag],
|
||||
dependencies=[webgis_access],
|
||||
)
|
||||
|
||||
# Project Data (PostgreSQL)
|
||||
api_router.include_router(project_data.router, tags=["Project Data"])
|
||||
# Simulation and analysis
|
||||
api_router.include_router(
|
||||
simulation.router,
|
||||
tags=["Simulation Control"],
|
||||
dependencies=[simulation_run_access],
|
||||
)
|
||||
api_router.include_router(scada.router, dependencies=[scada_access])
|
||||
api_router.include_router(
|
||||
sensor_placement.router,
|
||||
tags=["Sensor Placement"],
|
||||
dependencies=[optimization_run_access],
|
||||
)
|
||||
api_router.include_router(
|
||||
snapshots.router,
|
||||
tags=["Snapshots"],
|
||||
dependencies=[simulation_access],
|
||||
)
|
||||
api_router.include_router(
|
||||
schemes.router,
|
||||
tags=["Schemes"],
|
||||
dependencies=[simulation_access],
|
||||
)
|
||||
api_router.include_router(
|
||||
misc.router,
|
||||
tags=["Misc"],
|
||||
dependencies=[webgis_view_access],
|
||||
)
|
||||
api_router.include_router(
|
||||
risk.router,
|
||||
tags=["Risk"],
|
||||
dependencies=[risk_run_access],
|
||||
)
|
||||
api_router.include_router(
|
||||
cache.router,
|
||||
tags=["Cache"],
|
||||
dependencies=[simulation_run_access],
|
||||
)
|
||||
api_router.include_router(
|
||||
web_search.router,
|
||||
tags=["Web Search"],
|
||||
dependencies=[webgis_view_access],
|
||||
)
|
||||
api_router.include_router(
|
||||
geocoding.router,
|
||||
tags=["Geocoding"],
|
||||
dependencies=[webgis_view_access],
|
||||
)
|
||||
api_router.include_router(
|
||||
leakage.router,
|
||||
tags=["Leakage"],
|
||||
dependencies=[burst_run_access],
|
||||
)
|
||||
api_router.include_router(
|
||||
burst_detection.router,
|
||||
tags=["Burst Detection"],
|
||||
dependencies=[burst_run_access],
|
||||
)
|
||||
api_router.include_router(
|
||||
burst_location.router,
|
||||
tags=["Burst Location"],
|
||||
dependencies=[burst_run_access],
|
||||
)
|
||||
|
||||
# Extension
|
||||
api_router.include_router(extension.router, tags=["Extension"])
|
||||
# TimescaleDB data
|
||||
for endpoint_router, tag in (
|
||||
(ts_realtime.router, "TimescaleDB - Realtime"),
|
||||
(ts_scheme.router, "TimescaleDB - Scheme"),
|
||||
):
|
||||
api_router.include_router(
|
||||
endpoint_router,
|
||||
tags=[tag],
|
||||
dependencies=[simulation_access],
|
||||
)
|
||||
|
||||
for endpoint_router, tag in (
|
||||
(ts_scada.router, "TimescaleDB - SCADA"),
|
||||
(ts_composite.router, "TimescaleDB - Composite"),
|
||||
):
|
||||
api_router.include_router(
|
||||
endpoint_router,
|
||||
tags=[tag],
|
||||
dependencies=[scada_access],
|
||||
)
|
||||
|
||||
api_router.include_router(
|
||||
project_data.router,
|
||||
tags=["Project Data"],
|
||||
dependencies=[webgis_view_access],
|
||||
)
|
||||
api_router.include_router(
|
||||
extension.router,
|
||||
tags=["Extension"],
|
||||
dependencies=[webgis_access],
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# import logging
|
||||
import time
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
@@ -23,12 +23,22 @@ def _decode_keycloak_token(token: str) -> dict:
|
||||
|
||||
key = settings.KEYCLOAK_PUBLIC_KEY.replace("\\n", "\n")
|
||||
|
||||
return jwt.decode(
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
key,
|
||||
algorithms=[settings.KEYCLOAK_ALGORITHM],
|
||||
audience=settings.KEYCLOAK_AUDIENCE or None,
|
||||
)
|
||||
if settings.KEYCLOAK_ACCESS_TOKEN_MAX_AGE_SECONDS <= 0:
|
||||
return payload
|
||||
|
||||
issued_at = payload.get("iat")
|
||||
if not isinstance(issued_at, (int, float)) or (
|
||||
time.time() >= issued_at + settings.KEYCLOAK_ACCESS_TOKEN_MAX_AGE_SECONDS
|
||||
):
|
||||
raise JWTError("Keycloak access token is older than the allowed maximum age")
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
async def get_current_keycloak_payload(
|
||||
@@ -73,14 +83,18 @@ async def get_current_keycloak_sub(
|
||||
) from exc
|
||||
|
||||
|
||||
async def get_current_keycloak_username(
|
||||
payload: dict = Depends(get_current_keycloak_payload),
|
||||
) -> str:
|
||||
username = payload.get("preferred_username") or payload.get("username")
|
||||
def get_keycloak_preferred_username(payload: dict) -> str:
|
||||
username = payload.get("preferred_username")
|
||||
if not username:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Missing username claim",
|
||||
detail="Missing preferred_username claim",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return str(username)
|
||||
|
||||
|
||||
async def get_current_keycloak_username(
|
||||
payload: dict = Depends(get_current_keycloak_payload),
|
||||
) -> str:
|
||||
return get_keycloak_preferred_username(payload)
|
||||
|
||||
@@ -6,7 +6,10 @@ from fastapi import Depends, HTTPException, status
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth.keycloak_dependencies import get_current_keycloak_payload
|
||||
from app.auth.keycloak_dependencies import (
|
||||
get_current_keycloak_payload,
|
||||
get_keycloak_preferred_username,
|
||||
)
|
||||
from app.infra.db.metadb.database import get_metadata_session
|
||||
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
||||
|
||||
@@ -38,11 +41,6 @@ def _keycloak_sub_from_payload(payload: dict) -> UUID:
|
||||
) from exc
|
||||
|
||||
|
||||
def _username_from_payload(payload: dict) -> str | None:
|
||||
username = payload.get("preferred_username") or payload.get("username")
|
||||
return str(username) if username else None
|
||||
|
||||
|
||||
def _email_from_payload(payload: dict) -> str | None:
|
||||
email = payload.get("email")
|
||||
return str(email) if email else None
|
||||
@@ -53,6 +51,7 @@ async def get_current_metadata_user(
|
||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||
):
|
||||
keycloak_sub = _keycloak_sub_from_payload(keycloak_payload)
|
||||
username = get_keycloak_preferred_username(keycloak_payload)
|
||||
try:
|
||||
user = await metadata_repo.get_user_by_keycloak_id(keycloak_sub)
|
||||
except SQLAlchemyError as exc:
|
||||
@@ -62,7 +61,7 @@ async def get_current_metadata_user(
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Metadata database error: {exc}",
|
||||
detail="Metadata database is unavailable",
|
||||
) from exc
|
||||
if not user or not user.is_active:
|
||||
raise HTTPException(
|
||||
@@ -71,7 +70,7 @@ async def get_current_metadata_user(
|
||||
try:
|
||||
user = await metadata_repo.refresh_user_keycloak_snapshot(
|
||||
user,
|
||||
username=_username_from_payload(keycloak_payload),
|
||||
username=username,
|
||||
email=_email_from_payload(keycloak_payload),
|
||||
)
|
||||
except SQLAlchemyError as exc:
|
||||
@@ -81,7 +80,7 @@ async def get_current_metadata_user(
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Metadata database error: {exc}",
|
||||
detail="Metadata database is unavailable",
|
||||
) from exc
|
||||
return user
|
||||
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
|
||||
from app.auth.project_dependencies import ProjectContext, get_project_context
|
||||
|
||||
WEBGIS_VIEW = "webgis.view"
|
||||
WEBGIS_EDIT = "webgis.edit"
|
||||
SCADA_VIEW = "scada.view"
|
||||
SCADA_CLEAN = "scada.clean"
|
||||
SIMULATION_VIEW = "simulation.view"
|
||||
SIMULATION_RUN = "simulation.run"
|
||||
BURST_VIEW = "burst.view"
|
||||
BURST_RUN = "burst.run"
|
||||
RISK_VIEW = "risk.view"
|
||||
RISK_RUN = "risk.run"
|
||||
OPTIMIZATION_VIEW = "optimization.view"
|
||||
OPTIMIZATION_RUN = "optimization.run"
|
||||
MODEL_IMPORT = "model.import"
|
||||
AUDIT_VIEW = "audit.view"
|
||||
ENVIRONMENT_MANAGE = "environment.manage"
|
||||
MEMBERSHIP_MANAGE = "membership.manage"
|
||||
|
||||
PROJECT_MEMBER_PERMISSIONS = frozenset(
|
||||
{
|
||||
WEBGIS_VIEW,
|
||||
WEBGIS_EDIT,
|
||||
SCADA_VIEW,
|
||||
SCADA_CLEAN,
|
||||
SIMULATION_VIEW,
|
||||
SIMULATION_RUN,
|
||||
BURST_VIEW,
|
||||
BURST_RUN,
|
||||
RISK_VIEW,
|
||||
RISK_RUN,
|
||||
OPTIMIZATION_VIEW,
|
||||
OPTIMIZATION_RUN,
|
||||
}
|
||||
)
|
||||
|
||||
PROJECT_VIEWER_PERMISSIONS = frozenset(
|
||||
{
|
||||
WEBGIS_VIEW,
|
||||
SCADA_VIEW,
|
||||
SIMULATION_VIEW,
|
||||
}
|
||||
)
|
||||
|
||||
SYSTEM_ADMIN_PERMISSIONS = frozenset(
|
||||
{
|
||||
MODEL_IMPORT,
|
||||
AUDIT_VIEW,
|
||||
ENVIRONMENT_MANAGE,
|
||||
MEMBERSHIP_MANAGE,
|
||||
}
|
||||
)
|
||||
|
||||
PROJECT_ROLE_PERMISSIONS: dict[str, frozenset[str]] = {
|
||||
"member": PROJECT_MEMBER_PERMISSIONS,
|
||||
"viewer": PROJECT_VIEWER_PERMISSIONS,
|
||||
}
|
||||
|
||||
|
||||
def resolve_permissions(
|
||||
*,
|
||||
project_role: str | None,
|
||||
system_role: str,
|
||||
is_superuser: bool,
|
||||
) -> frozenset[str]:
|
||||
permissions = set(PROJECT_ROLE_PERMISSIONS.get(project_role or "", frozenset()))
|
||||
if is_superuser or system_role == "admin":
|
||||
permissions.update(SYSTEM_ADMIN_PERMISSIONS)
|
||||
return frozenset(permissions)
|
||||
|
||||
|
||||
def permissions_for_context(ctx: ProjectContext) -> frozenset[str]:
|
||||
return resolve_permissions(
|
||||
project_role=ctx.project_role,
|
||||
system_role=ctx.system_role,
|
||||
is_superuser=ctx.is_superuser,
|
||||
)
|
||||
|
||||
|
||||
def _permission_denied(permission: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail={
|
||||
"code": "permission_denied",
|
||||
"permission": permission,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def _enforce_project_scope(request: Request, ctx: ProjectContext) -> None:
|
||||
requested_network = (
|
||||
request.path_params.get("network")
|
||||
or request.query_params.get("network")
|
||||
)
|
||||
if not requested_network:
|
||||
content_type = request.headers.get("content-type", "")
|
||||
if content_type.startswith("application/json"):
|
||||
try:
|
||||
payload = await request.json()
|
||||
except (ValueError, RuntimeError):
|
||||
payload = None
|
||||
if isinstance(payload, dict):
|
||||
requested_network = payload.get("network")
|
||||
|
||||
if requested_network and str(requested_network) != ctx.project_code:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail={
|
||||
"code": "project_scope_denied",
|
||||
"project_id": str(ctx.project_id),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def require_permission(
|
||||
permission: str,
|
||||
) -> Callable[..., Awaitable[ProjectContext]]:
|
||||
async def dependency(
|
||||
request: Request,
|
||||
ctx: ProjectContext = Depends(get_project_context),
|
||||
) -> ProjectContext:
|
||||
if permission not in permissions_for_context(ctx):
|
||||
raise _permission_denied(permission)
|
||||
await _enforce_project_scope(request, ctx)
|
||||
return ctx
|
||||
|
||||
return dependency
|
||||
|
||||
|
||||
def require_method_permission(
|
||||
*,
|
||||
read_permission: str,
|
||||
write_permission: str,
|
||||
) -> Callable[..., Awaitable[ProjectContext]]:
|
||||
async def dependency(
|
||||
request: Request,
|
||||
ctx: ProjectContext = Depends(get_project_context),
|
||||
) -> ProjectContext:
|
||||
permission = (
|
||||
read_permission
|
||||
if request.method.upper() in {"GET", "HEAD", "OPTIONS"}
|
||||
else write_permission
|
||||
)
|
||||
if permission not in permissions_for_context(ctx):
|
||||
raise _permission_denied(permission)
|
||||
await _enforce_project_scope(request, ctx)
|
||||
return ctx
|
||||
|
||||
return dependency
|
||||
|
||||
|
||||
def has_permission(user: Any, project_role: str | None, permission: str) -> bool:
|
||||
return permission in resolve_permissions(
|
||||
project_role=project_role,
|
||||
system_role=str(getattr(user, "role", "user")),
|
||||
is_superuser=bool(getattr(user, "is_superuser", False)),
|
||||
)
|
||||
@@ -1,18 +1,21 @@
|
||||
import logging
|
||||
from collections.abc import AsyncGenerator
|
||||
from dataclasses import dataclass
|
||||
from typing import AsyncGenerator
|
||||
from uuid import UUID
|
||||
|
||||
import logging
|
||||
from fastapi import Depends, Header, HTTPException, status
|
||||
from psycopg import AsyncConnection
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth.keycloak_dependencies import get_current_keycloak_sub
|
||||
from app.auth.metadata_dependencies import get_current_metadata_user
|
||||
from app.core.config import settings
|
||||
from app.infra.db.dynamic_manager import project_connection_manager
|
||||
from app.infra.db.metadb.database import get_metadata_session
|
||||
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
||||
from app.infra.db.metadb.repositories.metadata_repository import (
|
||||
MetadataRepository,
|
||||
ProjectDbRouting,
|
||||
)
|
||||
|
||||
DB_ROLE_BIZ_DATA = "biz_data"
|
||||
DB_ROLE_IOT_DATA = "iot_data"
|
||||
@@ -28,6 +31,8 @@ class ProjectContext:
|
||||
project_code: str
|
||||
user_id: UUID
|
||||
project_role: str
|
||||
system_role: str = "user"
|
||||
is_superuser: bool = False
|
||||
|
||||
|
||||
async def get_metadata_repository(
|
||||
@@ -36,10 +41,10 @@ async def get_metadata_repository(
|
||||
return MetadataRepository(session)
|
||||
|
||||
|
||||
async def get_project_context(
|
||||
x_project_id: str = Header(..., alias="X-Project-Id"),
|
||||
keycloak_sub: UUID = Depends(get_current_keycloak_sub),
|
||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||
async def resolve_project_context(
|
||||
x_project_id: str,
|
||||
current_user,
|
||||
metadata_repo: MetadataRepository,
|
||||
) -> ProjectContext:
|
||||
try:
|
||||
project_uuid = UUID(x_project_id)
|
||||
@@ -59,17 +64,9 @@ async def get_project_context(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail="Project is not active"
|
||||
)
|
||||
|
||||
user = await metadata_repo.get_user_by_keycloak_id(keycloak_sub)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail="User not registered"
|
||||
membership_role = await metadata_repo.get_membership_role(
|
||||
project_uuid, current_user.id
|
||||
)
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user"
|
||||
)
|
||||
|
||||
membership_role = await metadata_repo.get_membership_role(project_uuid, user.id)
|
||||
if not membership_role:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail="No access to project"
|
||||
@@ -81,43 +78,70 @@ async def get_project_context(
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Metadata database error: {exc}",
|
||||
detail="Metadata database is unavailable",
|
||||
) from exc
|
||||
|
||||
return ProjectContext(
|
||||
project_id=project.id,
|
||||
project_code=project.code,
|
||||
user_id=user.id,
|
||||
user_id=current_user.id,
|
||||
project_role=membership_role,
|
||||
system_role=current_user.role,
|
||||
is_superuser=current_user.is_superuser,
|
||||
)
|
||||
|
||||
|
||||
async def get_project_context(
|
||||
x_project_id: str = Header(..., alias="X-Project-Id"),
|
||||
current_user=Depends(get_current_metadata_user),
|
||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||
) -> ProjectContext:
|
||||
return await resolve_project_context(x_project_id, current_user, metadata_repo)
|
||||
|
||||
|
||||
async def _get_project_routing(
|
||||
metadata_repo: MetadataRepository,
|
||||
project_id: UUID,
|
||||
db_role: str,
|
||||
expected_db_type: str,
|
||||
database_label: str,
|
||||
) -> ProjectDbRouting:
|
||||
try:
|
||||
routing = await metadata_repo.get_project_db_routing(project_id, db_role)
|
||||
except ValueError as exc:
|
||||
logger.error(
|
||||
"Invalid project %s routing DSN configuration",
|
||||
database_label,
|
||||
exc_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Project {database_label} routing DSN is invalid: {exc}",
|
||||
) from exc
|
||||
|
||||
if not routing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Project {database_label} not configured",
|
||||
)
|
||||
if routing.db_type != expected_db_type:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Project {database_label} type mismatch",
|
||||
)
|
||||
return routing
|
||||
|
||||
|
||||
async def get_project_pg_session(
|
||||
ctx: ProjectContext = Depends(get_project_context),
|
||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||
) -> AsyncGenerator[AsyncSession, None]:
|
||||
try:
|
||||
routing = await metadata_repo.get_project_db_routing(
|
||||
ctx.project_id, DB_ROLE_BIZ_DATA
|
||||
)
|
||||
except ValueError as exc:
|
||||
logger.error(
|
||||
"Invalid project PostgreSQL routing DSN configuration",
|
||||
exc_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Project PostgreSQL routing DSN is invalid: {exc}",
|
||||
) from exc
|
||||
if not routing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Project PostgreSQL not configured",
|
||||
)
|
||||
if routing.db_type != DB_TYPE_POSTGRES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Project PostgreSQL type mismatch",
|
||||
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
|
||||
@@ -137,28 +161,12 @@ async def get_project_pg_connection(
|
||||
ctx: ProjectContext = Depends(get_project_context),
|
||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||
) -> AsyncGenerator[AsyncConnection, None]:
|
||||
try:
|
||||
routing = await metadata_repo.get_project_db_routing(
|
||||
ctx.project_id, DB_ROLE_BIZ_DATA
|
||||
)
|
||||
except ValueError as exc:
|
||||
logger.error(
|
||||
"Invalid project PostgreSQL routing DSN configuration",
|
||||
exc_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Project PostgreSQL routing DSN is invalid: {exc}",
|
||||
) from exc
|
||||
if not routing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Project PostgreSQL not configured",
|
||||
)
|
||||
if routing.db_type != DB_TYPE_POSTGRES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Project PostgreSQL type mismatch",
|
||||
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
|
||||
@@ -178,28 +186,12 @@ async def get_project_timescale_connection(
|
||||
ctx: ProjectContext = Depends(get_project_context),
|
||||
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||
) -> AsyncGenerator[AsyncConnection, None]:
|
||||
try:
|
||||
routing = await metadata_repo.get_project_db_routing(
|
||||
ctx.project_id, DB_ROLE_IOT_DATA
|
||||
)
|
||||
except ValueError as exc:
|
||||
logger.error(
|
||||
"Invalid project TimescaleDB routing DSN configuration",
|
||||
exc_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Project TimescaleDB routing DSN is invalid: {exc}",
|
||||
) from exc
|
||||
if not routing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Project TimescaleDB not configured",
|
||||
)
|
||||
if routing.db_type != DB_TYPE_TIMESCALE:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Project TimescaleDB type mismatch",
|
||||
routing = await _get_project_routing(
|
||||
metadata_repo,
|
||||
ctx.project_id,
|
||||
DB_ROLE_IOT_DATA,
|
||||
DB_TYPE_TIMESCALE,
|
||||
"TimescaleDB",
|
||||
)
|
||||
|
||||
pool_min_size = routing.pool_min_size or settings.PROJECT_TS_POOL_MIN_SIZE
|
||||
|
||||
+1
-1
@@ -8,7 +8,6 @@ class Settings(BaseSettings):
|
||||
PROJECT_NAME: str = "TJWater Server"
|
||||
ENVIRONMENT: str = "production"
|
||||
API_V1_STR: str = "/api/v1"
|
||||
|
||||
NETWORK_NAME: str = "default_network"
|
||||
|
||||
# 敏感配置加密密钥 (Fernet)
|
||||
@@ -54,6 +53,7 @@ class Settings(BaseSettings):
|
||||
KEYCLOAK_PUBLIC_KEY: str = ""
|
||||
KEYCLOAK_ALGORITHM: str = "RS256"
|
||||
KEYCLOAK_AUDIENCE: str = ""
|
||||
KEYCLOAK_ACCESS_TOKEN_MAX_AGE_SECONDS: int = 900
|
||||
|
||||
# Bocha Web Search API
|
||||
BOCHA_API_KEY: str = ""
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class AccessContextResponse(BaseModel):
|
||||
user_id: UUID
|
||||
username: str
|
||||
system_role: str
|
||||
is_system_admin: bool
|
||||
project_id: UUID | None = None
|
||||
project_role: str | None = None
|
||||
permissions: list[str]
|
||||
@@ -5,8 +5,8 @@ from uuid import UUID
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
|
||||
BusinessRole = Literal["admin", "user", "operator", "viewer"]
|
||||
ProjectRole = Literal["owner", "admin", "member", "viewer"]
|
||||
BusinessRole = Literal["admin", "user"]
|
||||
ProjectRole = Literal["member", "viewer"]
|
||||
ProjectStatus = Literal["active", "inactive", "archived"]
|
||||
ProjectDbRole = Literal["biz_data", "iot_data"]
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
AdjustmentStatus = Literal["current", "original", "added", "replaced"]
|
||||
|
||||
|
||||
def _normalize_location_ids(value: list[str]) -> list[str]:
|
||||
normalized = [str(item).strip() for item in value]
|
||||
if any(not item for item in normalized):
|
||||
raise ValueError("sensor locations cannot contain blank node IDs")
|
||||
if len(set(normalized)) != len(normalized):
|
||||
raise ValueError("sensor locations cannot contain duplicate node IDs")
|
||||
return normalized
|
||||
|
||||
|
||||
class SensorPlacementOptimizeRequest(BaseModel):
|
||||
network: str = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
max_length=63,
|
||||
pattern=r"^[^/\\\x00]+$",
|
||||
)
|
||||
scheme_name: str = Field(..., min_length=1, max_length=32)
|
||||
sensor_type: Literal["pressure"]
|
||||
method: Literal["sensitivity", "kmeans"]
|
||||
sensor_count: int = Field(..., gt=0, le=200)
|
||||
min_diameter: int = Field(default=0, ge=0)
|
||||
|
||||
@field_validator("network")
|
||||
@classmethod
|
||||
def validate_network(cls, value: str) -> str:
|
||||
normalized = value.strip()
|
||||
if normalized in {".", ".."}:
|
||||
raise ValueError("network must be a project identifier")
|
||||
return normalized
|
||||
|
||||
|
||||
class SensorPlacementUpdateRequest(BaseModel):
|
||||
expected_sensor_location: list[str] = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
max_length=200,
|
||||
)
|
||||
sensor_location: list[str] = Field(..., min_length=1, max_length=200)
|
||||
|
||||
@field_validator("expected_sensor_location", "sensor_location")
|
||||
@classmethod
|
||||
def validate_locations(cls, value: list[str]) -> list[str]:
|
||||
return _normalize_location_ids(value)
|
||||
|
||||
|
||||
class SensorPlacementExportRequest(BaseModel):
|
||||
sensor_location: list[str] = Field(..., min_length=1, max_length=200)
|
||||
adjustment_status: dict[str, AdjustmentStatus] = Field(
|
||||
default_factory=dict,
|
||||
max_length=200,
|
||||
)
|
||||
|
||||
@field_validator("sensor_location")
|
||||
@classmethod
|
||||
def validate_locations(cls, value: list[str]) -> list[str]:
|
||||
return _normalize_location_ids(value)
|
||||
|
||||
|
||||
class SensorPointResponse(BaseModel):
|
||||
node_id: str
|
||||
max_pipe_diameter: float | None = Field(
|
||||
...,
|
||||
description="节点关联管道的最大管径,单位:毫米",
|
||||
)
|
||||
project_x: float
|
||||
project_y: float
|
||||
map_x: float
|
||||
map_y: float
|
||||
longitude: float
|
||||
latitude: float
|
||||
elevation: float
|
||||
|
||||
|
||||
class SensorPlacementSchemeResponse(BaseModel):
|
||||
id: int
|
||||
scheme_name: str
|
||||
sensor_number: int
|
||||
min_diameter: int
|
||||
username: str
|
||||
create_time: datetime
|
||||
sensor_location: list[str]
|
||||
sensor_points: list[SensorPointResponse]
|
||||
can_edit: bool = False
|
||||
@@ -58,6 +58,8 @@ class AuditMiddleware(BaseHTTPMiddleware):
|
||||
"/meta/projects",
|
||||
"/api/v1/openproject/",
|
||||
"/openproject/",
|
||||
"/api/v1/audit/session-events",
|
||||
"/audit/session-events",
|
||||
}
|
||||
EXCLUDED_PATH_PREFIXES = (
|
||||
)
|
||||
@@ -80,27 +82,9 @@ class AuditMiddleware(BaseHTTPMiddleware):
|
||||
request_data = None
|
||||
if should_capture_body:
|
||||
try:
|
||||
# 注意:读取 body 后需要重新设置,避免影响后续处理
|
||||
original_receive = request._receive
|
||||
body = await request.body()
|
||||
if body:
|
||||
request_data = json.loads(body.decode())
|
||||
|
||||
# 重新构造请求以供后续使用:仅回放一次,后续回落原始 receive
|
||||
body_sent = False
|
||||
|
||||
async def receive():
|
||||
nonlocal body_sent
|
||||
if not body_sent:
|
||||
body_sent = True
|
||||
return {
|
||||
"type": "http.request",
|
||||
"body": body,
|
||||
"more_body": False,
|
||||
}
|
||||
return await original_receive()
|
||||
|
||||
request._receive = receive
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to read request body for audit: {e}")
|
||||
|
||||
|
||||
@@ -211,6 +211,7 @@ class MetadataRepository:
|
||||
gs_workspace: str,
|
||||
map_extent: dict | None,
|
||||
status: str,
|
||||
creator_user_id: UUID | None = None,
|
||||
) -> models.Project:
|
||||
project = models.Project(
|
||||
id=uuid4(),
|
||||
@@ -224,6 +225,15 @@ class MetadataRepository:
|
||||
updated_at=_utcnow(),
|
||||
)
|
||||
self.session.add(project)
|
||||
if creator_user_id is not None:
|
||||
self.session.add(
|
||||
models.UserProjectMembership(
|
||||
id=uuid4(),
|
||||
user_id=creator_user_id,
|
||||
project_id=project.id,
|
||||
project_role="member",
|
||||
)
|
||||
)
|
||||
await self.session.commit()
|
||||
await self.session.refresh(project)
|
||||
return project
|
||||
@@ -483,7 +493,7 @@ class MetadataRepository:
|
||||
gs_workspace=project.gs_workspace,
|
||||
map_extent=project.map_extent,
|
||||
status=project.status,
|
||||
project_role="owner",
|
||||
project_role="member",
|
||||
)
|
||||
for project in result.scalars().all()
|
||||
]
|
||||
|
||||
+4
-1
@@ -6,7 +6,8 @@ import logging
|
||||
from datetime import datetime
|
||||
|
||||
import app.services.project_info as project_info
|
||||
from app.api.v1.router import api_router
|
||||
from app.api.problem_details import install_problem_details_handlers
|
||||
from app.api.v1.rest_router import api_router
|
||||
from app.infra.db.timescaledb.database import db as tsdb
|
||||
from app.infra.db.postgresql.database import db as pgdb
|
||||
from app.infra.db.dynamic_manager import project_connection_manager
|
||||
@@ -64,11 +65,13 @@ app = FastAPI(
|
||||
docs_url=None if is_production else "/docs",
|
||||
redoc_url=None if is_production else "/redoc",
|
||||
openapi_url=None if is_production else "/openapi.json",
|
||||
redirect_slashes=False,
|
||||
)
|
||||
|
||||
|
||||
# Include Routers
|
||||
app.include_router(api_router, prefix="/api/v1")
|
||||
install_problem_details_handlers(app)
|
||||
# Legcy Routers without version prefix
|
||||
# app.include_router(api_router)
|
||||
|
||||
|
||||
@@ -320,7 +320,11 @@ from .s23_options_util import (
|
||||
from .s23_options_util import get_option_v3_schema, get_option_v3
|
||||
from .batch_api import set_option_v3_ex
|
||||
|
||||
from .s24_coordinates import get_node_coord, get_nodes_in_extent, get_links_in_extent
|
||||
from .s24_coordinates import (
|
||||
get_links_in_extent,
|
||||
get_node_coord,
|
||||
get_nodes_in_extent,
|
||||
)
|
||||
|
||||
from .s25_vertices import (
|
||||
get_vertex_schema,
|
||||
@@ -456,8 +460,6 @@ from .s36_wda_cal import (
|
||||
# -----------------------------------------------------------------------------
|
||||
from .s38_scada_info import get_scada_info_schema, get_scada_info, get_all_scada_info
|
||||
|
||||
from .s39_user import get_user_schema, get_user, get_all_users
|
||||
|
||||
from .s40_schema import get_scheme_schema, get_scheme, get_all_schemes
|
||||
|
||||
from .s41_pipe_risk_probability import (
|
||||
@@ -468,6 +470,11 @@ from .s41_pipe_risk_probability import (
|
||||
get_pipe_risk_probability_geometries,
|
||||
)
|
||||
|
||||
from .s42_sensor_placement import get_all_sensor_placements
|
||||
from .s42_sensor_placement import (
|
||||
get_all_sensor_placements,
|
||||
get_sensor_placement,
|
||||
get_sensor_placement_nodes,
|
||||
update_sensor_placement,
|
||||
)
|
||||
|
||||
from .s43_burst_locate_result import get_all_burst_locate_results
|
||||
|
||||
@@ -20,6 +20,17 @@ def _close_connection(connection: pg.Connection) -> None:
|
||||
connection.close()
|
||||
|
||||
|
||||
def _is_healthy(connection: pg.Connection) -> bool:
|
||||
if _is_closed(connection):
|
||||
return False
|
||||
try:
|
||||
with connection.cursor() as cur:
|
||||
cur.execute("SELECT 1")
|
||||
except pg.Error:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _get_project_lock(name: str) -> RLock:
|
||||
with _registry_lock:
|
||||
lock = _project_locks.get(name)
|
||||
@@ -32,7 +43,7 @@ def _get_project_lock(name: str) -> RLock:
|
||||
def open_connection(name: str) -> pg.Connection:
|
||||
with _get_project_lock(name):
|
||||
connection = g_conn_dict.get(name)
|
||||
if connection is None or _is_closed(connection):
|
||||
if connection is None or not _is_healthy(connection):
|
||||
if connection is not None:
|
||||
_close_connection(connection)
|
||||
connection = pg.connect(
|
||||
@@ -47,8 +58,9 @@ def is_connection_open(name: str) -> bool:
|
||||
connection = g_conn_dict.get(name)
|
||||
if connection is None:
|
||||
return False
|
||||
if _is_closed(connection):
|
||||
if not _is_healthy(connection):
|
||||
del g_conn_dict[name]
|
||||
_close_connection(connection)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
from .database import *
|
||||
from .s0_base import *
|
||||
|
||||
class User(object):
|
||||
def __init__(self, input: dict[str, Any]) -> None:
|
||||
self.type = 'user'
|
||||
self.id = str(input['user_id'])
|
||||
self.name = str(input['username'])
|
||||
self.password = str(input['password'])
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return { 'type': self.type, 'id': self.id, 'name': self.name, 'password': self.password }
|
||||
|
||||
def as_id_dict(self) -> dict[str, Any]:
|
||||
return { 'type': self.type, 'id': self.id }
|
||||
|
||||
|
||||
def get_user_schema(name: str) -> dict[str, dict[Any, Any]]:
|
||||
return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True },
|
||||
'name' : {'type': 'str' , 'optional': False , 'readonly': False},
|
||||
'password' : {'type': 'str' , 'optional': False , 'readonly': False} }
|
||||
|
||||
def get_user(name: str, user_name: str) -> dict[Any, Any]:
|
||||
t = try_read(name, f"select * from users where username = '{user_name}'")
|
||||
if t == None:
|
||||
return {}
|
||||
|
||||
d = {}
|
||||
d['id'] = str(t['user_id'])
|
||||
d['name'] = str(t['username'])
|
||||
# d['password'] = str(t['password'])
|
||||
|
||||
return d
|
||||
|
||||
def get_all_users(name: str) -> list[dict[Any, Any]]:
|
||||
return read_all(name, "select * from users")
|
||||
|
||||
@@ -1,7 +1,124 @@
|
||||
from .database import *
|
||||
from .s0_base import *
|
||||
from .s42_sensor_placement import *
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
def get_all_sensor_placements(name: str) -> list[dict[Any, Any]]:
|
||||
from psycopg.rows import dict_row
|
||||
|
||||
from .connection import project_connection
|
||||
from .database import read_all
|
||||
|
||||
|
||||
def get_all_sensor_placements(name: str) -> list[dict[str, Any]]:
|
||||
return read_all(name, "select * from sensor_placement")
|
||||
|
||||
|
||||
def create_sensor_placement(
|
||||
name: str,
|
||||
*,
|
||||
scheme_name: str,
|
||||
min_diameter: int,
|
||||
username: str,
|
||||
sensor_location: list[str],
|
||||
) -> dict[str, Any]:
|
||||
with project_connection(name) as conn:
|
||||
with conn.cursor(row_factory=dict_row) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO sensor_placement (
|
||||
scheme_name,
|
||||
sensor_number,
|
||||
min_diameter,
|
||||
username,
|
||||
sensor_location
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
RETURNING *
|
||||
""",
|
||||
(
|
||||
scheme_name,
|
||||
len(sensor_location),
|
||||
min_diameter,
|
||||
username,
|
||||
sensor_location,
|
||||
),
|
||||
)
|
||||
created = cur.fetchone()
|
||||
if created is None:
|
||||
raise RuntimeError("监测点方案写入失败")
|
||||
return dict(created)
|
||||
|
||||
|
||||
def get_sensor_placement(name: str, scheme_id: int) -> dict[str, Any] | None:
|
||||
with project_connection(name) as conn:
|
||||
with conn.cursor(row_factory=dict_row) as cur:
|
||||
cur.execute(
|
||||
"SELECT * FROM sensor_placement WHERE id = %s",
|
||||
(scheme_id,),
|
||||
)
|
||||
return cur.fetchone()
|
||||
|
||||
|
||||
def get_sensor_placement_nodes(
|
||||
name: str,
|
||||
node_ids: list[str],
|
||||
) -> list[dict[str, Any]]:
|
||||
if not node_ids:
|
||||
return []
|
||||
with project_connection(name) as conn:
|
||||
with conn.cursor(row_factory=dict_row) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
WITH incident_pipe_diameters AS (
|
||||
SELECT node_id, MAX(diameter) AS max_pipe_diameter
|
||||
FROM (
|
||||
SELECT node1 AS node_id, diameter
|
||||
FROM pipes
|
||||
WHERE node1 = ANY(%s)
|
||||
UNION ALL
|
||||
SELECT node2 AS node_id, diameter
|
||||
FROM pipes
|
||||
WHERE node2 = ANY(%s)
|
||||
) AS incident_pipes
|
||||
GROUP BY node_id
|
||||
)
|
||||
SELECT DISTINCT ON (gj.id)
|
||||
gj.id AS node_id,
|
||||
ipd.max_pipe_diameter,
|
||||
gj.elevation,
|
||||
ST_X(c.coord) AS project_x,
|
||||
ST_Y(c.coord) AS project_y,
|
||||
ST_X(gj.geom) AS map_x,
|
||||
ST_Y(gj.geom) AS map_y
|
||||
FROM geo_junctions_mat AS gj
|
||||
JOIN coordinates AS c ON c.node = gj.id
|
||||
LEFT JOIN incident_pipe_diameters AS ipd ON ipd.node_id = gj.id
|
||||
WHERE gj.id = ANY(%s)
|
||||
ORDER BY gj.id
|
||||
""",
|
||||
(node_ids, node_ids, node_ids),
|
||||
)
|
||||
return list(cur.fetchall())
|
||||
|
||||
|
||||
def update_sensor_placement(
|
||||
name: str,
|
||||
scheme_id: int,
|
||||
*,
|
||||
expected_sensor_location: list[str],
|
||||
sensor_location: list[str],
|
||||
) -> dict[str, Any] | None:
|
||||
with project_connection(name) as conn:
|
||||
with conn.cursor(row_factory=dict_row) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE sensor_placement
|
||||
SET sensor_location = %s, sensor_number = %s
|
||||
WHERE id = %s AND sensor_location = %s
|
||||
RETURNING *
|
||||
""",
|
||||
(
|
||||
sensor_location,
|
||||
len(sensor_location),
|
||||
scheme_id,
|
||||
expected_sensor_location,
|
||||
),
|
||||
)
|
||||
return cur.fetchone()
|
||||
|
||||
@@ -476,7 +476,7 @@ def _get_simulation_scheme_burst_ids(
|
||||
) -> list[str]:
|
||||
if not scheme_name:
|
||||
return []
|
||||
rows = query_scheme_list(network) or []
|
||||
rows = query_scheme_list(network, scheme_type=scheme_type) or []
|
||||
for row in rows:
|
||||
if len(row) < 7:
|
||||
continue
|
||||
|
||||
@@ -11,53 +11,6 @@ from app.core.config import get_pgconn_string
|
||||
from app.services.time_api import parse_utc_time
|
||||
|
||||
|
||||
# 2025/03/23
|
||||
def create_user(name: str, username: str, password: str):
|
||||
"""
|
||||
创建用户
|
||||
:param name: 数据库名称
|
||||
:param username: 用户名
|
||||
:param password: 密码
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
# 动态替换数据库名称
|
||||
conn_string = get_pgconn_string(db_name=name)
|
||||
# 连接到 PostgreSQL 数据库(这里是数据库 "bb")
|
||||
with psycopg.connect(conn_string) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO users (username, password) VALUES (%s, %s)",
|
||||
(username, password),
|
||||
)
|
||||
# 提交事务
|
||||
conn.commit()
|
||||
print("新用户创建成功!")
|
||||
except Exception as e:
|
||||
print(f"创建用户出错:{e}")
|
||||
|
||||
|
||||
# 2025/03/23
|
||||
def delete_user(name: str, username: str):
|
||||
"""
|
||||
删除用户
|
||||
:param name: 数据库名称
|
||||
:param username: 用户名
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
# 动态替换数据库名称
|
||||
conn_string = get_pgconn_string(db_name=name)
|
||||
# 连接到 PostgreSQL 数据库(这里是数据库 "bb")
|
||||
with psycopg.connect(conn_string) as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("DELETE FROM users WHERE username = %s", (username,))
|
||||
conn.commit()
|
||||
print(f"用户 {username} 删除成功!")
|
||||
except Exception as e:
|
||||
print(f"删除用户出错:{e}")
|
||||
|
||||
|
||||
# 2025/03/23
|
||||
def scheme_name_exists(name: str, scheme_name: str) -> bool:
|
||||
"""
|
||||
@@ -98,8 +51,8 @@ def store_scheme_info(
|
||||
:param name: 数据库名称
|
||||
:param scheme_name: 方案名称
|
||||
:param scheme_type: 方案类型
|
||||
:param username: 用户名(需在 users 表中已存在)
|
||||
:param scheme_start_time: 方案起始时间(字符串)
|
||||
:param username: MetaDB 中的用户名快照
|
||||
:param scheme_start_time: 带时区的方案起始时间;写入前统一转换为 UTC
|
||||
:param scheme_detail: 方案详情(字典,会转换为 JSON)
|
||||
:return:
|
||||
"""
|
||||
@@ -154,10 +107,16 @@ def delete_scheme_info(name: str, scheme_name: str) -> None:
|
||||
|
||||
|
||||
# 2025/03/23
|
||||
def query_scheme_list(name: str) -> list:
|
||||
def query_scheme_list(
|
||||
name: str,
|
||||
scheme_type: str | None = None,
|
||||
query_date: date | None = None,
|
||||
) -> list:
|
||||
"""
|
||||
查询pg数据库中的scheme_list,按照 create_time 降序排列,离现在时间最近的记录排在最前面
|
||||
:param name: 项目名称(数据库名称)
|
||||
:param scheme_type: 方案类型;为空时返回全部类型
|
||||
:param query_date: 查询日期;为空时不按日期过滤
|
||||
:return: 返回查询结果的所有行
|
||||
"""
|
||||
try:
|
||||
@@ -166,7 +125,37 @@ def query_scheme_list(name: str) -> list:
|
||||
# 连接到 PostgreSQL 数据库(这里是数据库 "bb")
|
||||
with psycopg.connect(conn_string) as conn:
|
||||
with conn.cursor() as cur:
|
||||
# 按 create_time 降序排列
|
||||
if scheme_type and query_date is not None:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT *
|
||||
FROM scheme_list
|
||||
WHERE scheme_type = %s AND DATE(create_time) = %s
|
||||
ORDER BY create_time DESC
|
||||
""",
|
||||
(scheme_type, query_date),
|
||||
)
|
||||
elif scheme_type:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT *
|
||||
FROM scheme_list
|
||||
WHERE scheme_type = %s
|
||||
ORDER BY create_time DESC
|
||||
""",
|
||||
(scheme_type,),
|
||||
)
|
||||
elif query_date is not None:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT *
|
||||
FROM scheme_list
|
||||
WHERE DATE(create_time) = %s
|
||||
ORDER BY create_time DESC
|
||||
""",
|
||||
(query_date,),
|
||||
)
|
||||
else:
|
||||
cur.execute("SELECT * FROM scheme_list ORDER BY create_time DESC")
|
||||
rows = cur.fetchall()
|
||||
return rows
|
||||
@@ -175,6 +164,85 @@ def query_scheme_list(name: str) -> list:
|
||||
print(f"查询错误:{e}")
|
||||
|
||||
|
||||
def _filter_scheme_detail_scope(
|
||||
result: dict,
|
||||
name: str,
|
||||
scheme_type: str | None = None,
|
||||
) -> dict:
|
||||
if not result:
|
||||
return {}
|
||||
if scheme_type and result.get("scheme_type") != scheme_type:
|
||||
return {}
|
||||
network = result.get("network")
|
||||
if network not in (None, name):
|
||||
return {}
|
||||
return result
|
||||
|
||||
|
||||
def query_scheme_detail(
|
||||
name: str,
|
||||
scheme_name: str,
|
||||
scheme_type: str | None = None,
|
||||
) -> dict:
|
||||
if scheme_type == "dma_leak_identification":
|
||||
return _filter_scheme_detail_scope(
|
||||
query_leakage_identify_scheme_detail(name, scheme_name),
|
||||
name,
|
||||
scheme_type,
|
||||
)
|
||||
if scheme_type == "burst_detection":
|
||||
return _filter_scheme_detail_scope(
|
||||
query_burst_detection_scheme_detail(name, scheme_name),
|
||||
name,
|
||||
scheme_type,
|
||||
)
|
||||
if scheme_type == "burst_location":
|
||||
return _filter_scheme_detail_scope(
|
||||
query_burst_location_scheme_detail(name, scheme_name),
|
||||
name,
|
||||
scheme_type,
|
||||
)
|
||||
|
||||
conn_string = get_pgconn_string(db_name=name)
|
||||
with psycopg.connect(conn_string) as conn:
|
||||
with conn.cursor() as cur:
|
||||
if scheme_type:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
|
||||
FROM public.scheme_list
|
||||
WHERE scheme_name = %s AND scheme_type = %s
|
||||
LIMIT 1
|
||||
""",
|
||||
(scheme_name, scheme_type),
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
|
||||
FROM public.scheme_list
|
||||
WHERE scheme_name = %s
|
||||
LIMIT 1
|
||||
""",
|
||||
(scheme_name,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
return {}
|
||||
detail = row[6] if isinstance(row[6], dict) else {}
|
||||
return _filter_scheme_detail_scope({
|
||||
"scheme_id": row[0],
|
||||
"scheme_name": row[1],
|
||||
"scheme_type": row[2],
|
||||
"username": row[3],
|
||||
"create_time": row[4],
|
||||
"scheme_start_time": row[5],
|
||||
"scheme_detail": detail,
|
||||
"network": detail.get("network"),
|
||||
"result_payload": detail.get("result_payload", {}),
|
||||
}, name, scheme_type)
|
||||
|
||||
|
||||
def store_leakage_identify_result(
|
||||
name: str,
|
||||
scheme_name: str,
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Alignment, Font, PatternFill
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
from openpyxl.utils import get_column_letter
|
||||
from pyproj import Transformer
|
||||
|
||||
from app.native import wndb
|
||||
|
||||
|
||||
class SensorPlacementNotFoundError(LookupError):
|
||||
pass
|
||||
|
||||
|
||||
class SensorPlacementValidationError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class SensorPlacementConflictError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
_to_wgs84 = Transformer.from_crs("EPSG:3857", "EPSG:4326", always_xy=True)
|
||||
_STATUS_LABELS = {
|
||||
"current": "当前方案",
|
||||
"original": "原方案",
|
||||
"added": "新增",
|
||||
"replaced": "替换",
|
||||
}
|
||||
_COORDINATE_DESCRIPTION = (
|
||||
"工程 X/Y: 项目地方坐标系;地图 X/Y: EPSG:3857;经纬度: WGS84"
|
||||
)
|
||||
_LIST_HEADERS = (
|
||||
"序号",
|
||||
"节点 ID",
|
||||
"经度",
|
||||
"纬度",
|
||||
"工程 X",
|
||||
"工程 Y",
|
||||
"地图 X",
|
||||
"地图 Y",
|
||||
"高程",
|
||||
"调整状态",
|
||||
)
|
||||
_LIST_COLUMN_WIDTHS = (8, 20, 16, 16, 18, 18, 18, 18, 14, 14)
|
||||
|
||||
|
||||
def _normalize_locations(sensor_location: list[str]) -> list[str]:
|
||||
normalized = [str(node_id).strip() for node_id in sensor_location]
|
||||
if not normalized or any(not node_id for node_id in normalized):
|
||||
raise SensorPlacementValidationError("监测点列表不能为空")
|
||||
if len(set(normalized)) != len(normalized):
|
||||
raise SensorPlacementValidationError("监测点列表不能包含重复节点")
|
||||
return normalized
|
||||
|
||||
|
||||
def _sensor_points(
|
||||
network: str,
|
||||
sensor_location: list[str],
|
||||
) -> list[dict[str, Any]]:
|
||||
nodes = wndb.get_sensor_placement_nodes(network, sensor_location)
|
||||
by_id = {str(node["node_id"]): node for node in nodes}
|
||||
missing = [node_id for node_id in sensor_location if node_id not in by_id]
|
||||
if missing:
|
||||
raise SensorPlacementValidationError(
|
||||
f"以下节点不存在或不是 junction: {', '.join(missing)}"
|
||||
)
|
||||
|
||||
points: list[dict[str, Any]] = []
|
||||
for node_id in sensor_location:
|
||||
node = by_id[node_id]
|
||||
project_x = float(node["project_x"])
|
||||
project_y = float(node["project_y"])
|
||||
map_x = float(node["map_x"])
|
||||
map_y = float(node["map_y"])
|
||||
longitude, latitude = _to_wgs84.transform(map_x, map_y)
|
||||
points.append(
|
||||
{
|
||||
"node_id": node_id,
|
||||
"max_pipe_diameter": (
|
||||
float(node["max_pipe_diameter"])
|
||||
if node["max_pipe_diameter"] is not None
|
||||
else None
|
||||
),
|
||||
"project_x": project_x,
|
||||
"project_y": project_y,
|
||||
"map_x": map_x,
|
||||
"map_y": map_y,
|
||||
"longitude": float(longitude),
|
||||
"latitude": float(latitude),
|
||||
"elevation": float(node["elevation"]),
|
||||
}
|
||||
)
|
||||
return points
|
||||
|
||||
|
||||
def get_sensor_placement_candidate(
|
||||
network: str,
|
||||
node_id: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Return the authoritative editable point data for one junction."""
|
||||
|
||||
return _sensor_points(network, _normalize_locations([node_id]))[0]
|
||||
|
||||
|
||||
def validate_sensor_placement_nodes(
|
||||
network: str,
|
||||
sensor_location: list[str],
|
||||
) -> None:
|
||||
_sensor_points(network, _normalize_locations(sensor_location))
|
||||
|
||||
|
||||
def get_sensor_placement_scheme(network: str, scheme_id: int) -> dict[str, Any]:
|
||||
scheme = wndb.get_sensor_placement(network, scheme_id)
|
||||
if scheme is None:
|
||||
raise SensorPlacementNotFoundError("监测点方案不存在")
|
||||
|
||||
locations = [str(item) for item in (scheme.get("sensor_location") or [])]
|
||||
return {
|
||||
**scheme,
|
||||
"sensor_number": len(locations),
|
||||
"sensor_location": locations,
|
||||
"sensor_points": _sensor_points(network, locations),
|
||||
}
|
||||
|
||||
|
||||
def update_sensor_placement_scheme(
|
||||
network: str,
|
||||
scheme_id: int,
|
||||
*,
|
||||
expected_sensor_location: list[str],
|
||||
sensor_location: list[str],
|
||||
) -> dict[str, Any]:
|
||||
expected = _normalize_locations(expected_sensor_location)
|
||||
next_locations = _normalize_locations(sensor_location)
|
||||
_sensor_points(network, next_locations)
|
||||
|
||||
updated = wndb.update_sensor_placement(
|
||||
network,
|
||||
scheme_id,
|
||||
expected_sensor_location=expected,
|
||||
sensor_location=next_locations,
|
||||
)
|
||||
if updated is None:
|
||||
if wndb.get_sensor_placement(network, scheme_id) is None:
|
||||
raise SensorPlacementNotFoundError("监测点方案不存在")
|
||||
raise SensorPlacementConflictError("方案已被其他用户修改,请重新加载")
|
||||
return get_sensor_placement_scheme(network, scheme_id)
|
||||
|
||||
|
||||
def can_edit_sensor_placement(user: Any, scheme: dict[str, Any]) -> bool:
|
||||
return bool(
|
||||
getattr(user, "is_superuser", False)
|
||||
or getattr(user, "role", None) == "admin"
|
||||
or getattr(user, "username", None) == scheme.get("username")
|
||||
)
|
||||
|
||||
|
||||
def _safe_excel_text(value: Any) -> str:
|
||||
text = "" if value is None else str(value)
|
||||
if text.startswith(("=", "+", "-", "@")):
|
||||
return f"'{text}"
|
||||
return text
|
||||
|
||||
|
||||
def _populate_info_sheet(
|
||||
sheet: Worksheet,
|
||||
*,
|
||||
network: str,
|
||||
scheme: dict[str, Any],
|
||||
location_count: int,
|
||||
is_draft: bool,
|
||||
) -> None:
|
||||
created_at = scheme["create_time"]
|
||||
if isinstance(created_at, datetime):
|
||||
created_at = created_at.isoformat(timespec="minutes")
|
||||
|
||||
rows = [
|
||||
("项目", network),
|
||||
("方案名称", scheme["scheme_name"]),
|
||||
("监测点数量", location_count),
|
||||
("最小管径", scheme["min_diameter"]),
|
||||
("创建人", scheme["username"]),
|
||||
("创建时间", created_at),
|
||||
("导出时间", datetime.now().astimezone().isoformat(timespec="minutes")),
|
||||
("文档状态", "未保存草稿" if is_draft else "当前方案"),
|
||||
("坐标说明", _COORDINATE_DESCRIPTION),
|
||||
]
|
||||
for row_index, (label, value) in enumerate(rows, start=1):
|
||||
sheet.cell(row=row_index, column=1, value=label)
|
||||
safe_value = _safe_excel_text(value) if isinstance(value, str) else value
|
||||
sheet.cell(row=row_index, column=2, value=safe_value)
|
||||
sheet.column_dimensions["A"].width = 18
|
||||
sheet.column_dimensions["B"].width = 64
|
||||
|
||||
|
||||
def _populate_list_sheet(
|
||||
sheet: Worksheet,
|
||||
*,
|
||||
points: list[dict[str, Any]],
|
||||
adjustment_status: dict[str, str],
|
||||
) -> None:
|
||||
sheet.append(_LIST_HEADERS)
|
||||
for index, point in enumerate(points, start=1):
|
||||
status = adjustment_status.get(point["node_id"], "current")
|
||||
sheet.append(
|
||||
[
|
||||
index,
|
||||
_safe_excel_text(point["node_id"]),
|
||||
point["longitude"],
|
||||
point["latitude"],
|
||||
point["project_x"],
|
||||
point["project_y"],
|
||||
point["map_x"],
|
||||
point["map_y"],
|
||||
point["elevation"],
|
||||
_STATUS_LABELS.get(status, "当前方案"),
|
||||
]
|
||||
)
|
||||
|
||||
header_fill = PatternFill("solid", fgColor="257DD4")
|
||||
for cell in sheet[1]:
|
||||
cell.fill = header_fill
|
||||
cell.font = Font(color="FFFFFF", bold=True)
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||
sheet.freeze_panes = "A2"
|
||||
sheet.auto_filter.ref = sheet.dimensions
|
||||
for index, width in enumerate(_LIST_COLUMN_WIDTHS, start=1):
|
||||
sheet.column_dimensions[get_column_letter(index)].width = width
|
||||
for row in sheet.iter_rows(min_row=2):
|
||||
row[0].alignment = Alignment(horizontal="center")
|
||||
for cell in row[2:9]:
|
||||
cell.number_format = "0.000000"
|
||||
|
||||
|
||||
def build_sensor_placement_workbook(
|
||||
*,
|
||||
network: str,
|
||||
scheme: dict[str, Any],
|
||||
sensor_location: list[str],
|
||||
adjustment_status: dict[str, str],
|
||||
) -> BytesIO:
|
||||
locations = _normalize_locations(sensor_location)
|
||||
points = _sensor_points(network, locations)
|
||||
is_draft = locations != list(scheme["sensor_location"])
|
||||
|
||||
workbook = Workbook()
|
||||
info_sheet = workbook.active
|
||||
info_sheet.title = "方案信息"
|
||||
_populate_info_sheet(
|
||||
info_sheet,
|
||||
network=network,
|
||||
scheme=scheme,
|
||||
location_count=len(locations),
|
||||
is_draft=is_draft,
|
||||
)
|
||||
|
||||
list_sheet = workbook.create_sheet("监测点清单")
|
||||
_populate_list_sheet(
|
||||
list_sheet,
|
||||
points=points,
|
||||
adjustment_status=adjustment_status,
|
||||
)
|
||||
|
||||
output = BytesIO()
|
||||
workbook.save(output)
|
||||
output.seek(0)
|
||||
return output
|
||||
@@ -686,6 +686,28 @@ def get_history_pattern_info(project_name, pattern_name):
|
||||
return flow_list, factor_list
|
||||
|
||||
|
||||
def _apply_valve_control(
|
||||
project_name: str, valve_control: dict[str, dict]
|
||||
) -> None:
|
||||
"""Apply explicit valve status, setting, and opening controls."""
|
||||
for valve_name, control in valve_control.items():
|
||||
valve_status = get_status(project_name, valve_name)
|
||||
if "status" in control:
|
||||
valve_status["status"] = control["status"]
|
||||
if "setting" in control:
|
||||
valve_status["setting"] = control["setting"]
|
||||
if "k" in control:
|
||||
valve_k = control["k"]
|
||||
if valve_k == 0:
|
||||
valve_status["status"] = "CLOSED"
|
||||
else:
|
||||
valve_status["setting"] = 0.1036 * pow(valve_k, -3.105)
|
||||
|
||||
cs = ChangeSet()
|
||||
cs.append(valve_status)
|
||||
set_status(project_name, cs)
|
||||
|
||||
|
||||
# 2025/01/11
|
||||
def run_simulation(
|
||||
name: str,
|
||||
@@ -701,6 +723,7 @@ def run_simulation(
|
||||
modify_valve_opening: dict[str, float] = None,
|
||||
scheme_type: str = None,
|
||||
scheme_name: str = None,
|
||||
valve_control: dict[str, dict] = None,
|
||||
) -> None:
|
||||
"""
|
||||
传入需要修改的参数,改变数据库中对应位置的值,然后计算,返回结果
|
||||
@@ -715,6 +738,7 @@ def run_simulation(
|
||||
:param modify_fixed_pump_pattern: dict中包含多个水泵模式,str为工频水泵的id,list为修改后的pattern
|
||||
:param modify_variable_pump_pattern: dict中包含多个水泵模式,str为变频水泵的id,list为修改后的pattern
|
||||
:param modify_valve_opening: dict中包含多个阀门开启度,str为阀门的id,float为修改后的阀门开启度
|
||||
:param valve_control: dict中可分别指定阀门的status、setting和k;存在时优先于modify_valve_opening
|
||||
:param scheme_type: 模拟方案类型
|
||||
:param scheme_name:模拟方案名称
|
||||
:return:
|
||||
@@ -1200,8 +1224,11 @@ def run_simulation(
|
||||
cs = ChangeSet()
|
||||
cs.append(pump_pattern)
|
||||
set_pattern(name_c, cs)
|
||||
# 修改阀门(valve)的状态setting和status
|
||||
if modify_valve_opening:
|
||||
# 显式阀门控制沿用 run_simulation_ex 的处理顺序和覆盖规则。
|
||||
if valve_control is not None:
|
||||
_apply_valve_control(name_c, valve_control)
|
||||
# 保留原开度参数逻辑,兼容现有方案调用。
|
||||
elif modify_valve_opening:
|
||||
for valve_name in modify_valve_opening.keys():
|
||||
if not np.isnan(modify_valve_opening[valve_name]):
|
||||
valve_status = get_status(name_c, valve_name)
|
||||
|
||||
+27
-17
@@ -1290,19 +1290,6 @@ def get_scada_info(name: str, id: str) -> dict[str, Any]:
|
||||
def get_all_scada_info(name: str) -> list[dict[str, Any]]:
|
||||
return api.get_all_scada_info(name)
|
||||
|
||||
# DingZQ 2025-03-27
|
||||
############################################################
|
||||
# 39 users
|
||||
############################################################
|
||||
def get_user_schema(name: str) -> dict[str, dict[str, Any]]:
|
||||
return api.get_user_schema(name)
|
||||
|
||||
def get_user(name: str, user_name: str) -> dict[str, Any]:
|
||||
return api.get_user(name, user_name=user_name)
|
||||
|
||||
def get_all_users(name: str) -> list[dict[str, Any]]:
|
||||
return api.get_all_users(name)
|
||||
|
||||
############################################################
|
||||
# scheme 40
|
||||
############################################################
|
||||
@@ -1312,9 +1299,35 @@ def get_scheme_schema(name: str) -> dict[str, dict[str, Any]]:
|
||||
def get_scheme(name: str, schema_name: str) -> dict[str, Any]:
|
||||
return api.get_scheme(name, schema_name)
|
||||
|
||||
def get_all_schemes(name: str) -> list[dict[str, Any]]:
|
||||
def get_all_schemes(
|
||||
name: str,
|
||||
scheme_type: str | None = None,
|
||||
query_date: Any | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
if scheme_type is None and query_date is None:
|
||||
return api.get_all_schemes(name)
|
||||
|
||||
from app.services.scheme_management import query_scheme_list
|
||||
|
||||
rows = query_scheme_list(name, scheme_type=scheme_type, query_date=query_date) or []
|
||||
columns = [
|
||||
"scheme_id",
|
||||
"scheme_name",
|
||||
"scheme_type",
|
||||
"username",
|
||||
"create_time",
|
||||
"scheme_start_time",
|
||||
"scheme_detail",
|
||||
]
|
||||
result = []
|
||||
for row in rows:
|
||||
item = dict(zip(columns, row, strict=False))
|
||||
detail = item.get("scheme_detail")
|
||||
if isinstance(detail, dict) and detail.get("network") not in (None, name):
|
||||
continue
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
############################################################
|
||||
# pipe_risk_probability 41
|
||||
############################################################
|
||||
@@ -1344,6 +1357,3 @@ def get_all_sensor_placements(name: str) -> list[dict[Any, Any]]:
|
||||
############################################################
|
||||
def get_all_burst_locate_results(name: str) -> list[dict[Any, Any]]:
|
||||
return api.get_all_burst_locate_results(name)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
dist/
|
||||
build/
|
||||
__pycache__/
|
||||
@@ -1,79 +0,0 @@
|
||||
# TJWater CLI
|
||||
|
||||
独立于服务端主代码的 Python CLI 文件夹,放在 `TJWaterServerBinary/cli/` 下,供 agent 服务器使用**编译后的可执行文件**直接调用,并通过 stdout/stderr 参与管道。
|
||||
|
||||
## 构建可执行产物
|
||||
|
||||
```bash
|
||||
cd TJWaterServerBinary/cli
|
||||
python -m pip install -r requirements.txt
|
||||
python -m pip install -r requirements-build.txt
|
||||
chmod +x build.sh
|
||||
./build.sh
|
||||
```
|
||||
|
||||
构建完成后,直接使用编译产物:
|
||||
|
||||
```bash
|
||||
./dist/tjwater-cli/tjwater-cli help
|
||||
```
|
||||
|
||||
这个可执行文件可以直接参与管道:
|
||||
|
||||
```bash
|
||||
./dist/tjwater-cli/tjwater-cli help | jq
|
||||
```
|
||||
|
||||
当前采用 `PyInstaller onedir` 方式输出到 `dist/tjwater-cli/`,避免 onefile 在部分 agent/server 环境下依赖临时目录解包执行的问题。
|
||||
|
||||
如果需要在开发时直接走源码入口,也可以显式使用 Python:
|
||||
|
||||
```bash
|
||||
python -m tjwater_cli help
|
||||
```
|
||||
|
||||
## 部署到 agent 服务器
|
||||
|
||||
最简单的方式是把 `dist/tjwater-cli/` 整个目录同步到 agent 服务器,然后直接执行:
|
||||
|
||||
```bash
|
||||
./tjwater-cli/tjwater-cli help
|
||||
```
|
||||
|
||||
如果希望打包传输:
|
||||
|
||||
```bash
|
||||
cd TJWaterServerBinary/cli
|
||||
tar -C dist -czf tjwater-cli-linux-amd64.tar.gz tjwater-cli
|
||||
```
|
||||
|
||||
如果希望放到 PATH 中:
|
||||
|
||||
```bash
|
||||
ln -s /path/to/TJWaterServerBinary/cli/dist/tjwater-cli/tjwater-cli /usr/local/bin/tjwater-cli
|
||||
tjwater-cli help | jq
|
||||
```
|
||||
|
||||
## 运行与构建依赖
|
||||
|
||||
```bash
|
||||
cd TJWaterServerBinary/cli
|
||||
python -m pip install -r requirements.txt
|
||||
python -m pip install -r requirements-build.txt
|
||||
```
|
||||
|
||||
`requirements.txt` 仅包含运行 CLI 的依赖;`requirements-build.txt` 仅包含生成可执行文件所需的构建依赖。
|
||||
|
||||
## 认证上下文
|
||||
|
||||
CLI 通过 `--auth-context` 读取 JSON 文件。常用字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"server": "http://backend-host:8000",
|
||||
"access_token": "...",
|
||||
"project_id": "...",
|
||||
"network": "...",
|
||||
"username": "..."
|
||||
}
|
||||
```
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
if [ -n "${PYTHON:-}" ]; then
|
||||
PYTHON_BIN="$PYTHON"
|
||||
elif command -v python >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python"
|
||||
else
|
||||
PYTHON_BIN="python3"
|
||||
fi
|
||||
|
||||
cd "$ROOT"
|
||||
|
||||
"$PYTHON_BIN" -m PyInstaller --noconfirm --clean tjwater.spec
|
||||
|
||||
BIN_PATH="$ROOT/dist/"
|
||||
if [ ! -x "$BIN_PATH" ]; then
|
||||
echo "build succeeded but executable was not created: $BIN_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"$BIN_PATH" help >/dev/null
|
||||
|
||||
echo "built executable: $BIN_PATH"
|
||||
@@ -1,5 +0,0 @@
|
||||
from tjwater_cli.main import console_entry
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
console_entry()
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"include": [
|
||||
"tjwater_cli",
|
||||
"tests"
|
||||
],
|
||||
"executionEnvironments": [
|
||||
{
|
||||
"root": ".",
|
||||
"extraPaths": [
|
||||
"."
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pyinstaller>=6.11,<7
|
||||
@@ -1,3 +0,0 @@
|
||||
click>=8.1,<9
|
||||
requests>=2.31,<3
|
||||
typer>=0.12,<1
|
||||
@@ -1,6 +0,0 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
@@ -1,960 +0,0 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from tjwater_cli import common, core
|
||||
from tjwater_cli.main import app, main
|
||||
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
class DummyResponse:
|
||||
def __init__(self, *, status_code=200, json_data=None, text="", headers=None, content=None):
|
||||
self.status_code = status_code
|
||||
self._json_data = json_data
|
||||
self.text = text
|
||||
self.headers = headers or {"content-type": "application/json"}
|
||||
self.content = content if content is not None else text.encode("utf-8")
|
||||
|
||||
@property
|
||||
def ok(self):
|
||||
return 200 <= self.status_code < 300
|
||||
|
||||
def json(self):
|
||||
if self._json_data is None:
|
||||
raise ValueError("no json")
|
||||
return self._json_data
|
||||
|
||||
|
||||
def test_load_auth_context_supports_aliases(monkeypatch):
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_PROJECT_ID", "p1")
|
||||
monkeypatch.setenv("TJWATER_USERNAME", "tester")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "net1")
|
||||
|
||||
auth = core.load_auth_context(auth_stdin=False)
|
||||
|
||||
assert auth.server == "http://server"
|
||||
assert auth.access_token == "abc"
|
||||
assert auth.project_id == "p1"
|
||||
assert auth.username == "tester"
|
||||
assert auth.network == "net1"
|
||||
|
||||
|
||||
def test_build_runtime_context_uses_default_server(monkeypatch):
|
||||
monkeypatch.delenv("TJWATER_SERVER", raising=False)
|
||||
monkeypatch.delenv("TJWATER_ACCESS_TOKEN", raising=False)
|
||||
monkeypatch.delenv("TJWATER_PROJECT_ID", raising=False)
|
||||
monkeypatch.delenv("TJWATER_USERNAME", raising=False)
|
||||
monkeypatch.delenv("TJWATER_NETWORK", raising=False)
|
||||
monkeypatch.delenv("TJWATER_EXTRA_HEADERS", raising=False)
|
||||
|
||||
runtime = core.build_runtime_context(
|
||||
server=None,
|
||||
scheme=None,
|
||||
timeout=core.DEFAULT_TIMEOUT,
|
||||
request_id="req-1",
|
||||
)
|
||||
|
||||
assert runtime.server == core.DEFAULT_SERVER
|
||||
|
||||
|
||||
def test_auth_stdin_can_be_reused_with_runtime_context_cache(monkeypatch):
|
||||
observed_runtime_ids: list[int] = []
|
||||
|
||||
def fake_request_json(ctx, **kwargs):
|
||||
observed_runtime_ids.append(id(ctx))
|
||||
assert ctx.auth.access_token == "token-1"
|
||||
assert kwargs["params"] == {"network": "tjwater", "junction": "11"}
|
||||
return {"id": "11"}, 5
|
||||
|
||||
monkeypatch.setattr(common, "request_json", fake_request_json)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["--auth-stdin", "network", "get-junction-properties", "--junction", "11"],
|
||||
input=json.dumps(
|
||||
{
|
||||
"server": "http://server",
|
||||
"access_token": "token-1",
|
||||
"project_id": "project-1",
|
||||
"network": "tjwater",
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert payload["ok"] is True
|
||||
assert payload["data"] == {"id": "11"}
|
||||
assert len(observed_runtime_ids) == 1
|
||||
|
||||
|
||||
def test_network_get_junction_properties_uses_network_context(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_request_json(ctx, **kwargs):
|
||||
captured["access_token"] = ctx.auth.access_token
|
||||
captured["path"] = kwargs["path"]
|
||||
captured["params"] = kwargs["params"]
|
||||
return {"id": "J1"}, 5
|
||||
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
|
||||
monkeypatch.setattr(common, "request_json", fake_request_json)
|
||||
|
||||
result = runner.invoke(app, ["network", "get-junction-properties", "--junction", "J1"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert payload["ok"] is True
|
||||
assert payload["data"] == {"id": "J1"}
|
||||
assert captured == {
|
||||
"access_token": "abc",
|
||||
"path": "/getjunctionproperties/",
|
||||
"params": {"network": "tjwater", "junction": "J1"},
|
||||
}
|
||||
|
||||
|
||||
def test_network_get_pipe_properties_uses_network_context(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_request_json(ctx, **kwargs):
|
||||
captured["access_token"] = ctx.auth.access_token
|
||||
captured["path"] = kwargs["path"]
|
||||
captured["params"] = kwargs["params"]
|
||||
return {"id": "P1"}, 5
|
||||
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
|
||||
monkeypatch.setattr(common, "request_json", fake_request_json)
|
||||
|
||||
result = runner.invoke(app, ["network", "get-pipe-properties", "--pipe", "P1"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert payload["ok"] is True
|
||||
assert payload["data"] == {"id": "P1"}
|
||||
assert captured == {
|
||||
"access_token": "abc",
|
||||
"path": "/getpipeproperties/",
|
||||
"params": {"network": "tjwater", "pipe": "P1"},
|
||||
}
|
||||
|
||||
|
||||
def test_network_get_all_pipes_properties_uses_network_context(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_request_json(ctx, **kwargs):
|
||||
captured["access_token"] = ctx.auth.access_token
|
||||
captured["path"] = kwargs["path"]
|
||||
captured["params"] = kwargs["params"]
|
||||
return [{"id": "P1"}], 5
|
||||
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
|
||||
monkeypatch.setattr(common, "request_json", fake_request_json)
|
||||
|
||||
result = runner.invoke(app, ["network", "get-all-pipes-properties"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert payload["ok"] is True
|
||||
assert payload["data"] == [{"id": "P1"}]
|
||||
assert captured == {
|
||||
"access_token": "abc",
|
||||
"path": "/getallpipeproperties/",
|
||||
"params": {"network": "tjwater"},
|
||||
}
|
||||
|
||||
|
||||
def test_network_get_reservoir_properties_uses_network_context(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_request_json(ctx, **kwargs):
|
||||
captured["access_token"] = ctx.auth.access_token
|
||||
captured["path"] = kwargs["path"]
|
||||
captured["params"] = kwargs["params"]
|
||||
return {"id": "R1"}, 5
|
||||
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
|
||||
monkeypatch.setattr(common, "request_json", fake_request_json)
|
||||
|
||||
result = runner.invoke(app, ["network", "get-reservoir-properties", "--reservoir", "R1"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert payload["ok"] is True
|
||||
assert payload["data"] == {"id": "R1"}
|
||||
assert captured == {
|
||||
"access_token": "abc",
|
||||
"path": "/getreservoirproperties/",
|
||||
"params": {"network": "tjwater", "reservoir": "R1"},
|
||||
}
|
||||
|
||||
|
||||
def test_network_get_all_reservoir_properties_uses_network_context(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_request_json(ctx, **kwargs):
|
||||
captured["access_token"] = ctx.auth.access_token
|
||||
captured["path"] = kwargs["path"]
|
||||
captured["params"] = kwargs["params"]
|
||||
return [{"id": "R1"}], 5
|
||||
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
|
||||
monkeypatch.setattr(common, "request_json", fake_request_json)
|
||||
|
||||
result = runner.invoke(app, ["network", "get-all-reservoirs-properties"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert payload["ok"] is True
|
||||
assert payload["data"] == [{"id": "R1"}]
|
||||
assert captured == {
|
||||
"access_token": "abc",
|
||||
"path": "/getallreservoirproperties/",
|
||||
"params": {"network": "tjwater"},
|
||||
}
|
||||
|
||||
|
||||
def test_network_get_tank_properties_uses_network_context(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_request_json(ctx, **kwargs):
|
||||
captured["access_token"] = ctx.auth.access_token
|
||||
captured["path"] = kwargs["path"]
|
||||
captured["params"] = kwargs["params"]
|
||||
return {"id": "T1"}, 5
|
||||
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
|
||||
monkeypatch.setattr(common, "request_json", fake_request_json)
|
||||
|
||||
result = runner.invoke(app, ["network", "get-tank-properties", "--tank", "T1"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert payload["ok"] is True
|
||||
assert payload["data"] == {"id": "T1"}
|
||||
assert captured == {
|
||||
"access_token": "abc",
|
||||
"path": "/gettankproperties/",
|
||||
"params": {"network": "tjwater", "tank": "T1"},
|
||||
}
|
||||
|
||||
|
||||
def test_network_get_all_tank_properties_uses_network_context(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_request_json(ctx, **kwargs):
|
||||
captured["access_token"] = ctx.auth.access_token
|
||||
captured["path"] = kwargs["path"]
|
||||
captured["params"] = kwargs["params"]
|
||||
return [{"id": "T1"}], 5
|
||||
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
|
||||
monkeypatch.setattr(common, "request_json", fake_request_json)
|
||||
|
||||
result = runner.invoke(app, ["network", "get-all-tanks-properties"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert payload["ok"] is True
|
||||
assert payload["data"] == [{"id": "T1"}]
|
||||
assert captured == {
|
||||
"access_token": "abc",
|
||||
"path": "/getalltankproperties/",
|
||||
"params": {"network": "tjwater"},
|
||||
}
|
||||
|
||||
|
||||
def test_network_get_pump_properties_uses_network_context(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_request_json(ctx, **kwargs):
|
||||
captured["access_token"] = ctx.auth.access_token
|
||||
captured["path"] = kwargs["path"]
|
||||
captured["params"] = kwargs["params"]
|
||||
return {"id": "PU1"}, 5
|
||||
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
|
||||
monkeypatch.setattr(common, "request_json", fake_request_json)
|
||||
|
||||
result = runner.invoke(app, ["network", "get-pump-properties", "--pump", "PU1"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert payload["ok"] is True
|
||||
assert payload["data"] == {"id": "PU1"}
|
||||
assert captured == {
|
||||
"access_token": "abc",
|
||||
"path": "/getpumpproperties/",
|
||||
"params": {"network": "tjwater", "pump": "PU1"},
|
||||
}
|
||||
|
||||
|
||||
def test_network_get_all_pump_properties_uses_network_context(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_request_json(ctx, **kwargs):
|
||||
captured["access_token"] = ctx.auth.access_token
|
||||
captured["path"] = kwargs["path"]
|
||||
captured["params"] = kwargs["params"]
|
||||
return [{"id": "PU1"}], 5
|
||||
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
|
||||
monkeypatch.setattr(common, "request_json", fake_request_json)
|
||||
|
||||
result = runner.invoke(app, ["network", "get-all-pumps-properties"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert payload["ok"] is True
|
||||
assert payload["data"] == [{"id": "PU1"}]
|
||||
assert captured == {
|
||||
"access_token": "abc",
|
||||
"path": "/getallpumpproperties/",
|
||||
"params": {"network": "tjwater"},
|
||||
}
|
||||
|
||||
|
||||
def test_network_get_valve_properties_uses_network_context(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_request_json(ctx, **kwargs):
|
||||
captured["access_token"] = ctx.auth.access_token
|
||||
captured["path"] = kwargs["path"]
|
||||
captured["params"] = kwargs["params"]
|
||||
return {"id": "V1"}, 5
|
||||
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
|
||||
monkeypatch.setattr(common, "request_json", fake_request_json)
|
||||
|
||||
result = runner.invoke(app, ["network", "get-valve-properties", "--valve", "V1"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert payload["ok"] is True
|
||||
assert payload["data"] == {"id": "V1"}
|
||||
assert captured == {
|
||||
"access_token": "abc",
|
||||
"path": "/getvalveproperties/",
|
||||
"params": {"network": "tjwater", "valve": "V1"},
|
||||
}
|
||||
|
||||
|
||||
def test_network_get_all_valve_properties_uses_network_context(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_request_json(ctx, **kwargs):
|
||||
captured["access_token"] = ctx.auth.access_token
|
||||
captured["path"] = kwargs["path"]
|
||||
captured["params"] = kwargs["params"]
|
||||
return [{"id": "V1"}], 5
|
||||
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
|
||||
monkeypatch.setattr(common, "request_json", fake_request_json)
|
||||
|
||||
result = runner.invoke(app, ["network", "get-all-valves-properties"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert payload["ok"] is True
|
||||
assert payload["data"] == [{"id": "V1"}]
|
||||
assert captured == {
|
||||
"access_token": "abc",
|
||||
"path": "/getallvalveproperties/",
|
||||
"params": {"network": "tjwater"},
|
||||
}
|
||||
|
||||
|
||||
def test_help_outputs_json_lists_commands():
|
||||
result = runner.invoke(app, ["help"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert payload["schema_version"] == "tjwater-cli/v1"
|
||||
assert any(command["command"] == "analysis" for command in payload["commands"])
|
||||
assert all(command["command"] != "project" for command in payload["commands"])
|
||||
assert payload["menu_level"] == 1
|
||||
assert all(command["command"] != "project list" for command in payload["commands"])
|
||||
|
||||
|
||||
def test_help_option_json_is_removed():
|
||||
result = runner.invoke(app, ["help", "--json"])
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "No such option: --json" in result.output
|
||||
|
||||
|
||||
def test_simulation_help_lists_subcommands():
|
||||
result = runner.invoke(app, ["simulation", "help"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert payload["summary"] == "模拟运行与调度相关命令。"
|
||||
commands = {command["command"]: command for command in payload["commands"]}
|
||||
assert commands["simulation run"]["summary"] == "触发指定绝对时间的模拟运行"
|
||||
assert commands["simulation run"]["usage"] == "tjwater-cli simulation run --start-time <START_TIME> --duration <DURATION>"
|
||||
assert "tjwater-cli" in commands["simulation run"]["example"]
|
||||
assert "simulation run" in commands["simulation run"]["example"]
|
||||
|
||||
|
||||
def test_nested_group_help_lists_examples():
|
||||
result = runner.invoke(app, ["analysis", "leakage", "help"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert payload["summary"] == "漏损分析相关命令。"
|
||||
commands = {command["command"]: command for command in payload["commands"]}
|
||||
assert commands["analysis leakage identify"]["summary"] == "执行漏损识别"
|
||||
assert commands["analysis leakage identify"]["example"] == "tjwater-cli analysis leakage identify --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --scheme leak_case_01"
|
||||
|
||||
|
||||
def test_analysis_help_uses_group_summaries_for_nested_groups():
|
||||
result = runner.invoke(app, ["analysis", "help"])
|
||||
payload = json.loads(result.stdout)
|
||||
commands = {command["command"]: command for command in payload["commands"]}
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert commands["analysis leakage"]["summary"] == "漏损分析相关命令。"
|
||||
assert commands["analysis burst-detection"]["summary"] == "爆管检测相关命令。"
|
||||
assert "analysis burst-location" not in commands
|
||||
assert "analysis risk" not in commands
|
||||
assert commands["analysis burst"]["example"] == "tjwater-cli analysis burst --start-time 2025-01-02T03:04:05+08:00 --duration 900 --burst-file ./burst.json --scheme burst_case_01"
|
||||
assert commands["analysis valve"]["example"] == "tjwater-cli analysis valve --mode close --start-time 2025-01-02T03:04:05+08:00 --valve V1 --valve V2 --duration 900 --scheme valve_case_01"
|
||||
|
||||
|
||||
def test_bare_analysis_uses_typer_help_with_descriptions():
|
||||
result = runner.invoke(app, ["analysis"])
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "分析计算与诊断相关命令。" in result.stdout
|
||||
assert "burst 执行爆管分析" in result.stdout
|
||||
assert "valve" in result.stdout
|
||||
assert "leakage 漏损分析相关命令。" in result.stdout
|
||||
assert "burst-location" not in result.stdout
|
||||
assert "risk" not in result.stdout
|
||||
|
||||
|
||||
def test_leaf_help_outputs_json():
|
||||
result = runner.invoke(app, ["help", "simulation", "run"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert payload["command"] == "simulation run"
|
||||
assert payload["output"] == "模拟触发结果;实时数据需通过 data timeseries 命令按时间段查询"
|
||||
assert payload["usage"] == "tjwater-cli simulation run --start-time <START_TIME> --duration <DURATION>"
|
||||
assert len(payload["examples"]) == 1
|
||||
assert "simulation run" in payload["examples"][0]
|
||||
|
||||
|
||||
def test_root_help_flag_uses_typer_style_with_examples():
|
||||
result = runner.invoke(app, ["--help"], prog_name="tjwater-cli")
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Usage: tjwater-cli" in result.stdout
|
||||
assert "Examples:" in result.stdout
|
||||
assert "tjwater-cli help simulation run" in result.stdout
|
||||
|
||||
|
||||
def test_leaf_help_flag_includes_usage_and_example():
|
||||
result = runner.invoke(app, ["simulation", "run", "--help"], prog_name="tjwater-cli")
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Usage: tjwater-cli simulation run [OPTIONS]" in result.stdout
|
||||
assert "Usage example:" in result.stdout
|
||||
assert "--start-time <START_TIME>" in result.stdout
|
||||
assert "--duration" in result.stdout
|
||||
assert "Examples:" in result.stdout
|
||||
assert "tjwater-cli simulation run" in result.stdout
|
||||
assert "START_TIME" in result.stdout
|
||||
assert "DURATION" in result.stdout
|
||||
|
||||
|
||||
def test_realtime_simulation_help_clarifies_type_values():
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["data", "timeseries", "realtime", "simulation-by-id-time", "--help"],
|
||||
prog_name="tjwater-cli",
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "links/nodes 是子命令" in result.stdout
|
||||
assert "pipe" in result.stdout
|
||||
assert "junction" in result.stdout
|
||||
|
||||
|
||||
def test_realtime_property_help_lists_supported_fields():
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["data", "timeseries", "realtime", "simulation-by-time-property", "--help"],
|
||||
prog_name="tjwater-cli",
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "flow" in result.stdout
|
||||
assert "pressure" in result.stdout
|
||||
assert "actual_demand" in result.stdout
|
||||
assert "velocity" in result.stdout
|
||||
|
||||
|
||||
def test_analysis_burst_returns_next_step_to_fetch_scheme(monkeypatch, tmp_path: Path):
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "demo")
|
||||
burst_path = tmp_path / "burst.json"
|
||||
burst_path.write_text('[{"id":"P1","size":3.5}]', encoding="utf-8")
|
||||
|
||||
def fake_request(**kwargs):
|
||||
return DummyResponse(text="success", headers={"content-type": "text/plain"})
|
||||
|
||||
monkeypatch.setattr(core.requests, "request", fake_request)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"analysis",
|
||||
"burst",
|
||||
"--start-time",
|
||||
"2025-01-02T03:04:05+08:00",
|
||||
"--duration",
|
||||
"30",
|
||||
"--burst-file",
|
||||
str(burst_path),
|
||||
"--scheme",
|
||||
"burst_case_01",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert '"summary": "爆管分析执行成功"' in result.stdout
|
||||
assert "tjwater-cli data scheme get --name burst_case_01" in result.stdout
|
||||
assert "tjwater-cli data scheme list" in result.stdout
|
||||
|
||||
|
||||
def test_analysis_contaminant_sends_required_scheme_name(monkeypatch):
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "demo")
|
||||
captured = {}
|
||||
|
||||
def fake_request(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return DummyResponse(text="success", headers={"content-type": "text/plain"})
|
||||
|
||||
monkeypatch.setattr(core.requests, "request", fake_request)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"analysis",
|
||||
"contaminant",
|
||||
"--start-time",
|
||||
"2025-01-02T03:04:05+08:00",
|
||||
"--duration",
|
||||
"900",
|
||||
"--source-node",
|
||||
"N1",
|
||||
"--concentration",
|
||||
"10.0",
|
||||
"--scheme",
|
||||
"contam_case_01",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert captured["params"] == {
|
||||
"network": "demo",
|
||||
"start_time": "2025-01-02T03:04:05+08:00",
|
||||
"source": "N1",
|
||||
"concentration": 10.0,
|
||||
"duration": 900,
|
||||
"scheme_name": "contam_case_01",
|
||||
}
|
||||
|
||||
|
||||
def test_analysis_flushing_sends_required_scheme_name(monkeypatch, tmp_path: Path):
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "demo")
|
||||
captured = {}
|
||||
valve_path = tmp_path / "valve.json"
|
||||
valve_path.write_text('[{"valve":"V1","opening":0.5}]', encoding="utf-8")
|
||||
|
||||
def fake_request(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return DummyResponse(text="success", headers={"content-type": "text/plain"})
|
||||
|
||||
monkeypatch.setattr(core.requests, "request", fake_request)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"analysis",
|
||||
"flushing",
|
||||
"--start-time",
|
||||
"2025-01-02T03:04:05+08:00",
|
||||
"--valve-setting-file",
|
||||
str(valve_path),
|
||||
"--drainage-node",
|
||||
"N1",
|
||||
"--flow",
|
||||
"100.0",
|
||||
"--duration",
|
||||
"900",
|
||||
"--scheme",
|
||||
"flush_case_01",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert captured["params"] == {
|
||||
"network": "demo",
|
||||
"start_time": "2025-01-02T03:04:05+08:00",
|
||||
"valves": ["V1"],
|
||||
"valves_k": [0.5],
|
||||
"drainage_node_ID": "N1",
|
||||
"flush_flow": 100.0,
|
||||
"duration": 900,
|
||||
"scheme_name": "flush_case_01",
|
||||
}
|
||||
|
||||
|
||||
def test_analysis_valve_close_sends_required_scheme_name(monkeypatch):
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "demo")
|
||||
captured = {}
|
||||
|
||||
def fake_request(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return DummyResponse(text="success", headers={"content-type": "text/plain"})
|
||||
|
||||
monkeypatch.setattr(core.requests, "request", fake_request)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"analysis",
|
||||
"valve",
|
||||
"--mode",
|
||||
"close",
|
||||
"--start-time",
|
||||
"2025-01-02T03:04:05+08:00",
|
||||
"--valve",
|
||||
"V1",
|
||||
"--duration",
|
||||
"900",
|
||||
"--scheme",
|
||||
"valve_case_01",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert captured["params"] == {
|
||||
"network": "demo",
|
||||
"start_time": "2025-01-02T03:04:05+08:00",
|
||||
"valves": ["V1"],
|
||||
"duration": 900,
|
||||
"scheme_name": "valve_case_01",
|
||||
}
|
||||
|
||||
|
||||
def test_analysis_contaminant_requires_scheme(monkeypatch, capsys):
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "demo")
|
||||
|
||||
exit_code = main(
|
||||
[
|
||||
"analysis",
|
||||
"contaminant",
|
||||
"--start-time",
|
||||
"2025-01-02T03:04:05+08:00",
|
||||
"--duration",
|
||||
"900",
|
||||
"--source-node",
|
||||
"N1",
|
||||
"--concentration",
|
||||
"10.0",
|
||||
],
|
||||
)
|
||||
|
||||
stdout = capsys.readouterr().out
|
||||
|
||||
assert exit_code == 2
|
||||
assert '"code": "SCHEME_REQUIRED"' in stdout
|
||||
|
||||
|
||||
def test_analysis_flushing_requires_scheme(monkeypatch, tmp_path: Path, capsys):
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "demo")
|
||||
valve_path = tmp_path / "valve.json"
|
||||
valve_path.write_text('[{"valve":"V1","opening":0.5}]', encoding="utf-8")
|
||||
|
||||
exit_code = main(
|
||||
[
|
||||
"analysis",
|
||||
"flushing",
|
||||
"--start-time",
|
||||
"2025-01-02T03:04:05+08:00",
|
||||
"--valve-setting-file",
|
||||
str(valve_path),
|
||||
"--drainage-node",
|
||||
"N1",
|
||||
"--flow",
|
||||
"100.0",
|
||||
],
|
||||
)
|
||||
|
||||
stdout = capsys.readouterr().out
|
||||
|
||||
assert exit_code == 2
|
||||
assert '"code": "SCHEME_REQUIRED"' in stdout
|
||||
|
||||
|
||||
def test_analysis_valve_close_requires_scheme(monkeypatch, capsys):
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "demo")
|
||||
|
||||
exit_code = main(
|
||||
[
|
||||
"analysis",
|
||||
"valve",
|
||||
"--mode",
|
||||
"close",
|
||||
"--start-time",
|
||||
"2025-01-02T03:04:05+08:00",
|
||||
"--valve",
|
||||
"V1",
|
||||
"--duration",
|
||||
"900",
|
||||
],
|
||||
)
|
||||
|
||||
stdout = capsys.readouterr().out
|
||||
|
||||
assert exit_code == 2
|
||||
assert '"code": "SCHEME_REQUIRED"' in stdout
|
||||
|
||||
|
||||
def test_main_missing_option_error_includes_usage_and_next_step(capsys):
|
||||
exit_code = main(["simulation", "run"])
|
||||
stdout = capsys.readouterr().out
|
||||
|
||||
assert exit_code == 2
|
||||
assert '"summary": "缺少参数"' in stdout
|
||||
assert '"code": "MISSING_PARAMETER"' in stdout
|
||||
assert '"usage": "tjwater-cli simulation run --start-time <START_TIME> --duration <DURATION>"' in stdout
|
||||
assert '"tjwater-cli help simulation run"' in stdout
|
||||
|
||||
|
||||
def test_main_invalid_enum_value_is_rejected_before_request(capsys):
|
||||
exit_code = main(
|
||||
[
|
||||
"data",
|
||||
"timeseries",
|
||||
"realtime",
|
||||
"simulation-by-id-time",
|
||||
"--id",
|
||||
"J1",
|
||||
"--type",
|
||||
"links",
|
||||
"--time",
|
||||
"2025-01-02T03:30:00+08:00",
|
||||
]
|
||||
)
|
||||
stdout = capsys.readouterr().out
|
||||
|
||||
assert exit_code == 2
|
||||
assert '"summary": "参数无效"' in stdout
|
||||
assert '"code": "INVALID_PARAMETER"' in stdout
|
||||
assert "links" in stdout
|
||||
assert "pipe" in stdout
|
||||
assert "junction" in stdout
|
||||
|
||||
|
||||
def test_main_invalid_pipe_property_is_rejected_before_request(capsys):
|
||||
exit_code = main(
|
||||
[
|
||||
"data",
|
||||
"timeseries",
|
||||
"realtime",
|
||||
"simulation-by-time-property",
|
||||
"--type",
|
||||
"pipe",
|
||||
"--time",
|
||||
"2025-01-02T03:30:00+08:00",
|
||||
"--property",
|
||||
"pressure",
|
||||
]
|
||||
)
|
||||
stdout = capsys.readouterr().out
|
||||
|
||||
assert exit_code == 2
|
||||
assert '"code": "INVALID_PROPERTY"' in stdout
|
||||
assert "flow" in stdout
|
||||
assert "velocity" in stdout
|
||||
|
||||
|
||||
def test_main_invalid_scada_field_is_rejected_before_request(capsys):
|
||||
exit_code = main(
|
||||
[
|
||||
"data",
|
||||
"timeseries",
|
||||
"scada",
|
||||
"query",
|
||||
"--device-id",
|
||||
"D1",
|
||||
"--start-time",
|
||||
"2025-01-02T03:00:00+08:00",
|
||||
"--end-time",
|
||||
"2025-01-02T04:00:00+08:00",
|
||||
"--field",
|
||||
"flow",
|
||||
]
|
||||
)
|
||||
stdout = capsys.readouterr().out
|
||||
|
||||
assert exit_code == 2
|
||||
assert '"code": "INVALID_FIELD"' in stdout
|
||||
assert "monitored_value" in stdout
|
||||
assert "cleaned_value" in stdout
|
||||
|
||||
|
||||
def test_data_scada_get_rejects_removed_kind_before_request(capsys):
|
||||
exit_code = main(["data", "scada", "get", "--kind", "device", "--id", "D1"])
|
||||
stdout = capsys.readouterr().out
|
||||
|
||||
assert exit_code == 2
|
||||
assert '"code": "INVALID_PARAMETER"' in stdout
|
||||
assert "device" in stdout
|
||||
assert "info" in stdout
|
||||
|
||||
|
||||
def test_data_scada_list_help_only_shows_info_kind():
|
||||
result = runner.invoke(app, ["data", "scada", "list", "--help"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "info" in result.stdout
|
||||
assert "device" not in result.stdout
|
||||
assert "element" not in result.stdout
|
||||
|
||||
|
||||
def test_data_scada_help_no_longer_lists_schema():
|
||||
result = runner.invoke(app, ["data", "scada", "help"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
commands = {command["command"] for command in payload["commands"]}
|
||||
assert "data scada get" in commands
|
||||
assert "data scada list" in commands
|
||||
assert "data scada schema" not in commands
|
||||
|
||||
|
||||
def test_data_scada_schema_command_is_removed():
|
||||
result = runner.invoke(app, ["data", "scada", "schema", "--kind", "info"])
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "No such command 'schema'" in result.output
|
||||
|
||||
|
||||
def test_data_help_no_longer_lists_extension_or_misc():
|
||||
result = runner.invoke(app, ["data", "help"])
|
||||
payload = json.loads(result.stdout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
commands = {command["command"] for command in payload["commands"]}
|
||||
assert "data timeseries" in commands
|
||||
assert "data scada" in commands
|
||||
assert "data scheme" in commands
|
||||
assert "data extension" not in commands
|
||||
assert "data misc" not in commands
|
||||
|
||||
|
||||
def test_removed_data_extension_and_misc_commands_fail():
|
||||
extension_result = runner.invoke(app, ["data", "extension", "list"])
|
||||
misc_result = runner.invoke(app, ["data", "misc", "sensor-placements"])
|
||||
|
||||
assert extension_result.exit_code == 2
|
||||
assert "No such command 'extension'" in extension_result.output
|
||||
assert misc_result.exit_code == 2
|
||||
assert "No such command 'misc'" in misc_result.output
|
||||
|
||||
|
||||
def test_main_bare_analysis_returns_typer_help_without_json_error(capsys):
|
||||
exit_code = main(["analysis"])
|
||||
stdout = capsys.readouterr().out
|
||||
|
||||
assert exit_code == 0
|
||||
assert "Usage: tjwater-cli analysis" in stdout
|
||||
assert "分析计算与诊断相关命令。" in stdout
|
||||
assert '"ok": false' not in stdout
|
||||
|
||||
|
||||
def test_simulation_run_translates_rfc3339(monkeypatch):
|
||||
monkeypatch.setenv("TJWATER_SERVER", "http://server")
|
||||
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
|
||||
monkeypatch.setenv("TJWATER_NETWORK", "demo")
|
||||
captured = {}
|
||||
|
||||
def fake_request(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return DummyResponse(json_data={"status": "success", "message": "Simulation started"})
|
||||
|
||||
monkeypatch.setattr(core.requests, "request", fake_request)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"simulation",
|
||||
"run",
|
||||
"--start-time",
|
||||
"2025-01-02T03:04:05+08:00",
|
||||
"--duration",
|
||||
"30",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert captured["json"] == {
|
||||
"name": "demo",
|
||||
"start_time": "2025-01-02T03:04:05+08:00",
|
||||
"duration": 30,
|
||||
}
|
||||
assert "tjwater-cli data timeseries realtime links" in result.stdout
|
||||
assert "tjwater-cli data timeseries realtime nodes" in result.stdout
|
||||
|
||||
|
||||
def test_removed_project_command_returns_not_found(capsys):
|
||||
exit_code = main(["project", "list"])
|
||||
stdout = capsys.readouterr().out
|
||||
|
||||
assert exit_code == 2
|
||||
assert '"code": "COMMAND_NOT_FOUND"' in stdout or "No such command: project" in stdout
|
||||
@@ -1,45 +0,0 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
from PyInstaller.utils.hooks import collect_data_files
|
||||
|
||||
|
||||
datas = collect_data_files("certifi")
|
||||
|
||||
|
||||
a = Analysis(
|
||||
["entrypoint.py"],
|
||||
pathex=["."],
|
||||
binaries=[],
|
||||
datas=datas,
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name="tjwater-cli",
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=True,
|
||||
)
|
||||
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name="tjwater-cli",
|
||||
)
|
||||
@@ -1,3 +0,0 @@
|
||||
from .main import app, main
|
||||
|
||||
__all__ = ["app", "main"]
|
||||
@@ -1,5 +0,0 @@
|
||||
from .main import console_entry
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
console_entry()
|
||||
@@ -1,76 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typer
|
||||
|
||||
from .formatters import TJWaterGroup
|
||||
|
||||
app = typer.Typer(help="TJWater agent CLI", add_completion=False, no_args_is_help=True, cls=TJWaterGroup)
|
||||
network_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
component_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
component_option_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
simulation_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
analysis_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
analysis_leakage_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
analysis_leakage_schemes_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
analysis_burst_detection_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
analysis_burst_detection_schemes_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
analysis_burst_location_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
analysis_burst_location_schemes_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
analysis_risk_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
analysis_sensor_placement_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
data_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
data_timeseries_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
data_timeseries_realtime_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
data_timeseries_scheme_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
data_timeseries_scada_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
data_timeseries_composite_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
data_scada_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
data_scheme_app = typer.Typer(no_args_is_help=True, cls=TJWaterGroup)
|
||||
|
||||
app.add_typer(network_app, name="network")
|
||||
app.add_typer(component_app, name="component")
|
||||
component_app.add_typer(component_option_app, name="option")
|
||||
app.add_typer(simulation_app, name="simulation")
|
||||
app.add_typer(analysis_app, name="analysis")
|
||||
analysis_app.add_typer(analysis_sensor_placement_app, name="sensor-placement")
|
||||
analysis_app.add_typer(analysis_leakage_app, name="leakage")
|
||||
analysis_leakage_app.add_typer(analysis_leakage_schemes_app, name="schemes")
|
||||
analysis_app.add_typer(analysis_burst_detection_app, name="burst-detection")
|
||||
analysis_burst_detection_app.add_typer(analysis_burst_detection_schemes_app, name="schemes")
|
||||
analysis_app.add_typer(analysis_burst_location_app, name="burst-location")
|
||||
analysis_burst_location_app.add_typer(analysis_burst_location_schemes_app, name="schemes")
|
||||
analysis_app.add_typer(analysis_risk_app, name="risk")
|
||||
app.add_typer(data_app, name="data")
|
||||
data_app.add_typer(data_timeseries_app, name="timeseries")
|
||||
data_timeseries_app.add_typer(data_timeseries_realtime_app, name="realtime")
|
||||
data_timeseries_app.add_typer(data_timeseries_scheme_app, name="scheme")
|
||||
data_timeseries_app.add_typer(data_timeseries_scada_app, name="scada")
|
||||
data_timeseries_app.add_typer(data_timeseries_composite_app, name="composite")
|
||||
data_app.add_typer(data_scada_app, name="scada")
|
||||
data_app.add_typer(data_scheme_app, name="scheme")
|
||||
|
||||
GROUP_HELP_APPS: list[tuple[typer.Typer, tuple[str, ...]]] = [
|
||||
(network_app, ("network",)),
|
||||
(component_app, ("component",)),
|
||||
(component_option_app, ("component", "option")),
|
||||
(simulation_app, ("simulation",)),
|
||||
(analysis_app, ("analysis",)),
|
||||
(analysis_sensor_placement_app, ("analysis", "sensor-placement")),
|
||||
(analysis_leakage_app, ("analysis", "leakage")),
|
||||
(analysis_leakage_schemes_app, ("analysis", "leakage", "schemes")),
|
||||
(analysis_burst_detection_app, ("analysis", "burst-detection")),
|
||||
(analysis_burst_detection_schemes_app, ("analysis", "burst-detection", "schemes")),
|
||||
(analysis_burst_location_app, ("analysis", "burst-location")),
|
||||
(analysis_burst_location_schemes_app, ("analysis", "burst-location", "schemes")),
|
||||
(analysis_risk_app, ("analysis", "risk")),
|
||||
(data_app, ("data",)),
|
||||
(data_timeseries_app, ("data", "timeseries")),
|
||||
(data_timeseries_realtime_app, ("data", "timeseries", "realtime")),
|
||||
(data_timeseries_scheme_app, ("data", "timeseries", "scheme")),
|
||||
(data_timeseries_scada_app, ("data", "timeseries", "scada")),
|
||||
(data_timeseries_composite_app, ("data", "timeseries", "composite")),
|
||||
(data_scada_app, ("data", "scada")),
|
||||
(data_scheme_app, ("data", "scheme")),
|
||||
]
|
||||
|
||||
TOP_LEVEL_COMMANDS = {"help", "network", "component", "simulation", "analysis", "data"}
|
||||
@@ -1,524 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
import typer
|
||||
|
||||
from .apps import (
|
||||
analysis_app,
|
||||
analysis_burst_detection_app,
|
||||
analysis_burst_detection_schemes_app,
|
||||
analysis_burst_location_app,
|
||||
analysis_burst_location_schemes_app,
|
||||
analysis_leakage_app,
|
||||
analysis_leakage_schemes_app,
|
||||
analysis_risk_app,
|
||||
analysis_sensor_placement_app,
|
||||
simulation_app,
|
||||
)
|
||||
from .common import emit_api, runtime_context
|
||||
from .core import (
|
||||
CLIError,
|
||||
emit_success,
|
||||
parse_burst_file,
|
||||
parse_optional_dataset_file,
|
||||
parse_time_with_timezone,
|
||||
parse_valve_setting_file,
|
||||
request_json,
|
||||
require_network,
|
||||
require_username,
|
||||
resolve_scheme,
|
||||
)
|
||||
from .option_types import DataSource, ValveMode
|
||||
|
||||
|
||||
@simulation_app.command("run")
|
||||
def simulation_run(
|
||||
ctx: typer.Context,
|
||||
start_time: Annotated[str, typer.Option("--start-time", help="RFC3339 开始时间")],
|
||||
duration: Annotated[int, typer.Option("--duration", help="持续分钟数")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
network = require_network(runtime)
|
||||
parsed = parse_time_with_timezone(start_time, option_name="--start-time")
|
||||
end_time = (parsed + timedelta(minutes=duration)).isoformat()
|
||||
body = {
|
||||
"name": network,
|
||||
"start_time": parsed.replace(microsecond=0).isoformat(),
|
||||
"duration": duration,
|
||||
}
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="触发模拟成功",
|
||||
method="POST",
|
||||
path="/simulations/run-by-date",
|
||||
json_body=body,
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
next_commands=[
|
||||
f"tjwater-cli data timeseries realtime links --start-time {parsed.isoformat()} --end-time {end_time}",
|
||||
f"tjwater-cli data timeseries realtime nodes --start-time {parsed.isoformat()} --end-time {end_time}",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@analysis_app.command("burst")
|
||||
def analysis_burst(
|
||||
ctx: typer.Context,
|
||||
start_time: Annotated[str, typer.Option("--start-time", help="RFC3339 开始时间")],
|
||||
duration: Annotated[int, typer.Option("--duration", help="持续秒数")],
|
||||
burst_file: Annotated[Path, typer.Option("--burst-file", help="爆管输入 JSON 文件")],
|
||||
scheme: Annotated[str | None, typer.Option("--scheme", help="方案名称")] = None,
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
ids, sizes = parse_burst_file(burst_file)
|
||||
scheme_name = resolve_scheme(runtime, scheme, required=True)
|
||||
params = {
|
||||
"network": require_network(runtime),
|
||||
"modify_pattern_start_time": parse_time_with_timezone(start_time, option_name="--start-time").isoformat(),
|
||||
"burst_ID": ids,
|
||||
"burst_size": sizes,
|
||||
"modify_total_duration": duration,
|
||||
"scheme_name": scheme_name,
|
||||
}
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="爆管分析执行成功",
|
||||
method="GET",
|
||||
path="/burst-analysis",
|
||||
params=params,
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
next_commands=[
|
||||
f"tjwater-cli data scheme get --name {scheme_name}",
|
||||
"tjwater-cli data scheme list",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@analysis_app.command("valve")
|
||||
def analysis_valve(
|
||||
ctx: typer.Context,
|
||||
mode: Annotated[ValveMode, typer.Option("--mode", help="分析模式,仅支持 close|isolation")],
|
||||
start_time: Annotated[str | None, typer.Option("--start-time", help="close 模式需要")] = None,
|
||||
valve: Annotated[list[str] | None, typer.Option("--valve", help="阀门 ID,可重复")] = None,
|
||||
element: Annotated[list[str] | None, typer.Option("--element", help="isolation 模式的事故元素,可重复")] = None,
|
||||
disabled_valve: Annotated[list[str] | None, typer.Option("--disabled-valve", help="故障阀门,可重复")] = None,
|
||||
duration: Annotated[int | None, typer.Option("--duration", help="close 模式持续秒数")] = None,
|
||||
scheme: Annotated[str | None, typer.Option("--scheme", help="close 模式的方案名称")] = None,
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
network = require_network(runtime)
|
||||
if mode == ValveMode.CLOSE:
|
||||
if not start_time or not valve:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="INVALID_VALVE_CLOSE_ARGS",
|
||||
message="close mode requires --start-time and at least one --valve",
|
||||
exit_code=2,
|
||||
)
|
||||
params = {
|
||||
"network": network,
|
||||
"start_time": parse_time_with_timezone(start_time, option_name="--start-time").isoformat(),
|
||||
"valves": valve,
|
||||
"duration": duration or 900,
|
||||
"scheme_name": resolve_scheme(runtime, scheme, required=True),
|
||||
}
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="阀门关闭分析执行成功",
|
||||
method="GET",
|
||||
path="/valve_close_analysis/",
|
||||
params=params,
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
return
|
||||
if mode == ValveMode.ISOLATION:
|
||||
if not element:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="INVALID_VALVE_ISOLATION_ARGS",
|
||||
message="isolation mode requires at least one --element",
|
||||
exit_code=2,
|
||||
)
|
||||
params = {"network": network, "accident_element": element}
|
||||
if disabled_valve:
|
||||
params["disabled_valves"] = disabled_valve
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="阀门隔离分析执行成功",
|
||||
method="GET",
|
||||
path="/valve-isolation-analysis",
|
||||
params=params,
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
return
|
||||
raise AssertionError(f"unreachable valve mode: {mode}")
|
||||
|
||||
|
||||
@analysis_app.command("flushing")
|
||||
def analysis_flushing(
|
||||
ctx: typer.Context,
|
||||
start_time: Annotated[str, typer.Option("--start-time", help="RFC3339 开始时间")],
|
||||
valve_setting_file: Annotated[Path, typer.Option("--valve-setting-file", help="阀门开度 JSON 文件")],
|
||||
drainage_node: Annotated[str, typer.Option("--drainage-node", help="排污节点")],
|
||||
flow: Annotated[float, typer.Option("--flow", help="冲洗流量")],
|
||||
duration: Annotated[int | None, typer.Option("--duration", help="持续秒数")] = None,
|
||||
scheme: Annotated[str | None, typer.Option("--scheme", help="方案名称")] = None,
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
valves, openings = parse_valve_setting_file(valve_setting_file)
|
||||
params = {
|
||||
"network": require_network(runtime),
|
||||
"start_time": parse_time_with_timezone(start_time, option_name="--start-time").isoformat(),
|
||||
"valves": valves,
|
||||
"valves_k": openings,
|
||||
"drainage_node_ID": drainage_node,
|
||||
"flush_flow": flow,
|
||||
"duration": duration or 900,
|
||||
"scheme_name": resolve_scheme(runtime, scheme, required=True),
|
||||
}
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="冲洗分析执行成功",
|
||||
method="GET",
|
||||
path="/flushing-analysis",
|
||||
params=params,
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@analysis_app.command("age")
|
||||
def analysis_age(
|
||||
ctx: typer.Context,
|
||||
start_time: Annotated[str, typer.Option("--start-time", help="RFC3339 开始时间")],
|
||||
duration: Annotated[int, typer.Option("--duration", help="持续秒数")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="水龄分析执行成功",
|
||||
method="GET",
|
||||
path="/age_analysis/",
|
||||
params={
|
||||
"network": require_network(runtime),
|
||||
"start_time": parse_time_with_timezone(start_time, option_name="--start-time").isoformat(),
|
||||
"duration": duration,
|
||||
},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@analysis_app.command("contaminant")
|
||||
def analysis_contaminant(
|
||||
ctx: typer.Context,
|
||||
start_time: Annotated[str, typer.Option("--start-time", help="RFC3339 开始时间")],
|
||||
duration: Annotated[int, typer.Option("--duration", help="持续秒数")],
|
||||
source_node: Annotated[str, typer.Option("--source-node", help="污染源节点")],
|
||||
concentration: Annotated[float, typer.Option("--concentration", help="浓度")],
|
||||
pattern: Annotated[str | None, typer.Option("--pattern", help="模式 ID")] = None,
|
||||
scheme: Annotated[str | None, typer.Option("--scheme", help="方案名称")] = None,
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
params = {
|
||||
"network": require_network(runtime),
|
||||
"start_time": parse_time_with_timezone(start_time, option_name="--start-time").isoformat(),
|
||||
"source": source_node,
|
||||
"concentration": concentration,
|
||||
"duration": duration,
|
||||
"scheme_name": resolve_scheme(runtime, scheme, required=True),
|
||||
}
|
||||
if pattern:
|
||||
params["pattern"] = pattern
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="污染物模拟执行成功",
|
||||
method="GET",
|
||||
path="/contaminant-simulation",
|
||||
params=params,
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@analysis_sensor_placement_app.command("kmeans")
|
||||
def analysis_sensor_placement_kmeans(
|
||||
ctx: typer.Context,
|
||||
count: Annotated[int, typer.Option("--count", help="传感器数量")],
|
||||
min_diameter: Annotated[int, typer.Option("--min-diameter", help="最小管径")] = 0,
|
||||
scheme: Annotated[str | None, typer.Option("--scheme", help="方案名称")] = None,
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
body = {
|
||||
"name": require_network(runtime),
|
||||
"scheme_name": resolve_scheme(runtime, scheme, required=True),
|
||||
"sensor_number": count,
|
||||
"min_diameter": min_diameter,
|
||||
"username": require_username(runtime),
|
||||
}
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="传感器选址执行成功",
|
||||
method="POST",
|
||||
path="/pressure_sensor_placement_kmeans/",
|
||||
json_body=body,
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
require_username_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@analysis_leakage_app.command("identify")
|
||||
def analysis_leakage_identify(
|
||||
ctx: typer.Context,
|
||||
start_time: Annotated[str, typer.Option("--start-time", help="RFC3339 开始时间")],
|
||||
end_time: Annotated[str, typer.Option("--end-time", help="RFC3339 结束时间")],
|
||||
scheme: Annotated[str | None, typer.Option("--scheme", help="方案名称")] = None,
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
body = {
|
||||
"network": require_network(runtime),
|
||||
"scada_start": parse_time_with_timezone(start_time, option_name="--start-time").isoformat(),
|
||||
"scada_end": parse_time_with_timezone(end_time, option_name="--end-time").isoformat(),
|
||||
"scheme_name": resolve_scheme(runtime, scheme, required=True),
|
||||
}
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="漏损识别执行成功",
|
||||
method="POST",
|
||||
path="/leakage/identify/",
|
||||
json_body=body,
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@analysis_leakage_schemes_app.command("list")
|
||||
def analysis_leakage_schemes_list(ctx: typer.Context) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取漏损方案列表成功",
|
||||
method="GET",
|
||||
path="/leakage/schemes/",
|
||||
params={"network": require_network(runtime)},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@analysis_leakage_schemes_app.command("get")
|
||||
def analysis_leakage_schemes_get(
|
||||
ctx: typer.Context,
|
||||
scheme_name: Annotated[str, typer.Argument(help="方案名称")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取漏损方案详情成功",
|
||||
method="GET",
|
||||
path=f"/leakage/schemes/{scheme_name}",
|
||||
params={"network": require_network(runtime)},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@analysis_burst_detection_app.command("detect")
|
||||
def analysis_burst_detection_detect(
|
||||
ctx: typer.Context,
|
||||
start_time: Annotated[str, typer.Option("--start-time", help="RFC3339 开始时间")],
|
||||
end_time: Annotated[str, typer.Option("--end-time", help="RFC3339 结束时间")],
|
||||
scheme: Annotated[str | None, typer.Option("--scheme", help="方案名称")] = None,
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
body = {
|
||||
"network": require_network(runtime),
|
||||
"scada_start": parse_time_with_timezone(start_time, option_name="--start-time").isoformat(),
|
||||
"scada_end": parse_time_with_timezone(end_time, option_name="--end-time").isoformat(),
|
||||
"scheme_name": resolve_scheme(runtime, scheme, required=True),
|
||||
}
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="爆管检测执行成功",
|
||||
method="POST",
|
||||
path="/burst-detection/detect/",
|
||||
json_body=body,
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@analysis_burst_detection_schemes_app.command("list")
|
||||
def analysis_burst_detection_schemes_list(ctx: typer.Context) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取爆管检测方案列表成功",
|
||||
method="GET",
|
||||
path="/burst-detection/schemes/",
|
||||
params={"network": require_network(runtime)},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@analysis_burst_detection_schemes_app.command("get")
|
||||
def analysis_burst_detection_schemes_get(
|
||||
ctx: typer.Context,
|
||||
scheme_name: Annotated[str, typer.Argument(help="方案名称")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取爆管检测方案详情成功",
|
||||
method="GET",
|
||||
path=f"/burst-detection/schemes/{scheme_name}",
|
||||
params={"network": require_network(runtime)},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@analysis_burst_location_app.command("locate")
|
||||
def analysis_burst_location_locate(
|
||||
ctx: typer.Context,
|
||||
start_time: Annotated[str, typer.Option("--start-time", help="RFC3339 开始时间")],
|
||||
end_time: Annotated[str, typer.Option("--end-time", help="RFC3339 结束时间")],
|
||||
burst_leakage: Annotated[float, typer.Option("--burst-leakage", help="爆管漏水量")],
|
||||
scheme: Annotated[str | None, typer.Option("--scheme", help="方案名称")] = None,
|
||||
data_source: Annotated[DataSource, typer.Option("--data-source", help="数据来源,仅支持 monitoring|simulation")] = DataSource.MONITORING,
|
||||
pressure_scada_id: Annotated[list[str] | None, typer.Option("--pressure-scada-id", help="压力 SCADA ID,可重复")] = None,
|
||||
flow_scada_id: Annotated[list[str] | None, typer.Option("--flow-scada-id", help="流量 SCADA ID,可重复")] = None,
|
||||
pressure_file: Annotated[Path | None, typer.Option("--pressure-file", help="包含 burst_pressure/normal_pressure 的 JSON 文件")] = None,
|
||||
flow_file: Annotated[Path | None, typer.Option("--flow-file", help="包含 burst_flow/normal_flow 的 JSON 文件")] = None,
|
||||
use_scada_flow: Annotated[bool, typer.Option("--use-scada-flow", help="启用 SCADA 流量")] = False,
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
pressure_payload = parse_optional_dataset_file(pressure_file, label="pressure") or {}
|
||||
flow_payload = parse_optional_dataset_file(flow_file, label="flow") or {}
|
||||
body = {
|
||||
"network": require_network(runtime),
|
||||
"scheme_name": resolve_scheme(runtime, scheme, required=True),
|
||||
"data_source": data_source.value,
|
||||
"scada_burst_start": parse_time_with_timezone(start_time, option_name="--start-time").isoformat(),
|
||||
"scada_burst_end": parse_time_with_timezone(end_time, option_name="--end-time").isoformat(),
|
||||
"burst_leakage": burst_leakage,
|
||||
"use_scada_flow": use_scada_flow,
|
||||
}
|
||||
if pressure_scada_id:
|
||||
body["pressure_scada_ids"] = pressure_scada_id
|
||||
if flow_scada_id:
|
||||
body["flow_scada_ids"] = flow_scada_id
|
||||
if isinstance(pressure_payload, dict):
|
||||
body.update({key: value for key, value in pressure_payload.items() if key in {"burst_pressure", "normal_pressure"}})
|
||||
if isinstance(flow_payload, dict):
|
||||
body.update({key: value for key, value in flow_payload.items() if key in {"burst_flow", "normal_flow"}})
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="爆管定位执行成功",
|
||||
method="POST",
|
||||
path="/burst-location/locate/",
|
||||
json_body=body,
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@analysis_burst_location_schemes_app.command("list")
|
||||
def analysis_burst_location_schemes_list(ctx: typer.Context) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取爆管定位方案列表成功",
|
||||
method="GET",
|
||||
path="/burst-location/schemes/",
|
||||
params={"network": require_network(runtime)},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@analysis_burst_location_schemes_app.command("get")
|
||||
def analysis_burst_location_schemes_get(
|
||||
ctx: typer.Context,
|
||||
scheme_name: Annotated[str, typer.Argument(help="方案名称")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取爆管定位方案详情成功",
|
||||
method="GET",
|
||||
path=f"/burst-location/schemes/{scheme_name}",
|
||||
params={"network": require_network(runtime)},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@analysis_risk_app.command("pipe-now")
|
||||
def analysis_risk_pipe_now(
|
||||
ctx: typer.Context,
|
||||
pipe: Annotated[str, typer.Option("--pipe", help="管道 ID")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取当前管道风险成功",
|
||||
method="GET",
|
||||
path="/getpiperiskprobabilitynow/",
|
||||
params={"network": require_network(runtime), "pipe_id": pipe},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@analysis_risk_app.command("pipe-history")
|
||||
def analysis_risk_pipe_history(
|
||||
ctx: typer.Context,
|
||||
pipe: Annotated[str, typer.Option("--pipe", help="管道 ID")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取历史管道风险成功",
|
||||
method="GET",
|
||||
path="/getpiperiskprobability/",
|
||||
params={"network": require_network(runtime), "pipe_id": pipe},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@analysis_risk_app.command("network")
|
||||
def analysis_risk_network(ctx: typer.Context) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
network = require_network(runtime)
|
||||
probabilities, duration_prob = request_json(
|
||||
runtime,
|
||||
method="GET",
|
||||
path="/getnetworkpiperiskprobabilitynow/",
|
||||
params={"network": network},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
geometries, duration_geo = request_json(
|
||||
runtime,
|
||||
method="GET",
|
||||
path="/getpiperiskprobabilitygeometries/",
|
||||
params={"network": network},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
emit_success(
|
||||
summary="读取全网风险成功",
|
||||
data={"probabilities": probabilities, "geometries": geometries},
|
||||
ctx=runtime,
|
||||
duration_ms=duration_prob + duration_geo,
|
||||
)
|
||||
@@ -1,508 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
import typer
|
||||
|
||||
from .apps import (
|
||||
data_scada_app,
|
||||
data_scheme_app,
|
||||
data_timeseries_composite_app,
|
||||
data_timeseries_realtime_app,
|
||||
data_timeseries_scada_app,
|
||||
data_timeseries_scheme_app,
|
||||
)
|
||||
from .common import emit_api, runtime_context
|
||||
from .core import CLIError, parse_time_with_timezone, require_network, resolve_scheme
|
||||
from .option_types import (
|
||||
CompositeKind,
|
||||
ElementType,
|
||||
JUNCTION_TIMESERIES_FIELDS,
|
||||
SCADA_TIMESERIES_FIELDS,
|
||||
ScadaListKind,
|
||||
SimulationQuery,
|
||||
timeseries_fields_for_element_type,
|
||||
)
|
||||
|
||||
|
||||
def _scheme_type_option(scheme_type: str | None) -> str:
|
||||
return scheme_type or "simulation"
|
||||
|
||||
|
||||
def _validate_element_property(element_type: ElementType, property_name: str, *, option_name: str) -> str:
|
||||
valid_fields = timeseries_fields_for_element_type(element_type)
|
||||
if property_name not in valid_fields:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="INVALID_PROPERTY",
|
||||
message=f"{option_name} for --type {element_type.value} must be one of: {', '.join(valid_fields)}",
|
||||
exit_code=2,
|
||||
)
|
||||
return property_name
|
||||
|
||||
|
||||
def _validate_node_field(field_name: str, *, option_name: str) -> str:
|
||||
if field_name not in JUNCTION_TIMESERIES_FIELDS:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="INVALID_FIELD",
|
||||
message=f"{option_name} must be one of: {', '.join(JUNCTION_TIMESERIES_FIELDS)}",
|
||||
exit_code=2,
|
||||
)
|
||||
return field_name
|
||||
|
||||
|
||||
def _validate_scada_field(field_name: str, *, option_name: str) -> str:
|
||||
if field_name not in SCADA_TIMESERIES_FIELDS:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="INVALID_FIELD",
|
||||
message=f"{option_name} must be one of: {', '.join(SCADA_TIMESERIES_FIELDS)}",
|
||||
exit_code=2,
|
||||
)
|
||||
return field_name
|
||||
|
||||
|
||||
@data_timeseries_realtime_app.command("links")
|
||||
def data_realtime_links(
|
||||
ctx: typer.Context,
|
||||
start_time: Annotated[str, typer.Option("--start-time", help="开始时间")],
|
||||
end_time: Annotated[str, typer.Option("--end-time", help="结束时间")],
|
||||
) -> None:
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取实时管道数据成功",
|
||||
method="GET",
|
||||
path="/realtime/links",
|
||||
params={
|
||||
"start_time": parse_time_with_timezone(start_time, option_name="--start-time").isoformat(),
|
||||
"end_time": parse_time_with_timezone(end_time, option_name="--end-time").isoformat(),
|
||||
},
|
||||
require_auth=True,
|
||||
require_project=True,
|
||||
)
|
||||
|
||||
|
||||
@data_timeseries_realtime_app.command("nodes")
|
||||
def data_realtime_nodes(
|
||||
ctx: typer.Context,
|
||||
start_time: Annotated[str, typer.Option("--start-time", help="开始时间")],
|
||||
end_time: Annotated[str, typer.Option("--end-time", help="结束时间")],
|
||||
) -> None:
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取实时节点数据成功",
|
||||
method="GET",
|
||||
path="/realtime/nodes",
|
||||
params={
|
||||
"start_time": parse_time_with_timezone(start_time, option_name="--start-time").isoformat(),
|
||||
"end_time": parse_time_with_timezone(end_time, option_name="--end-time").isoformat(),
|
||||
},
|
||||
require_auth=True,
|
||||
require_project=True,
|
||||
)
|
||||
|
||||
|
||||
@data_timeseries_realtime_app.command("simulation-by-id-time")
|
||||
def data_realtime_simulation_by_id_time(
|
||||
ctx: typer.Context,
|
||||
id: Annotated[str, typer.Option("--id", help="元素 ID")],
|
||||
type: Annotated[ElementType, typer.Option("--type", help="元素类型,仅支持 pipe|junction;links/nodes 是子命令")],
|
||||
time: Annotated[str, typer.Option("--time", help="查询时间")],
|
||||
) -> None:
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取实时模拟数据成功",
|
||||
method="GET",
|
||||
path="/realtime/query/by-id-time",
|
||||
params={
|
||||
"id": id,
|
||||
"type": type.value,
|
||||
"query_time": parse_time_with_timezone(time, option_name="--time").isoformat(),
|
||||
},
|
||||
require_auth=True,
|
||||
require_project=True,
|
||||
)
|
||||
|
||||
|
||||
@data_timeseries_realtime_app.command("simulation-by-time-property")
|
||||
def data_realtime_simulation_by_time_property(
|
||||
ctx: typer.Context,
|
||||
type: Annotated[ElementType, typer.Option("--type", help="元素类型,仅支持 pipe|junction;links/nodes 是子命令")],
|
||||
time: Annotated[str, typer.Option("--time", help="查询时间")],
|
||||
property: Annotated[str, typer.Option("--property", help="属性名;pipe: flow|friction|headloss|quality|reaction|setting|status|velocity;junction: actual_demand|total_head|pressure|quality")],
|
||||
) -> None:
|
||||
property = _validate_element_property(type, property, option_name="--property")
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取实时属性聚合数据成功",
|
||||
method="GET",
|
||||
path="/realtime/query/by-time-property",
|
||||
params={
|
||||
"type": type.value,
|
||||
"query_time": parse_time_with_timezone(time, option_name="--time").isoformat(),
|
||||
"property": property,
|
||||
},
|
||||
require_auth=True,
|
||||
require_project=True,
|
||||
)
|
||||
|
||||
|
||||
@data_timeseries_scheme_app.command("links")
|
||||
def data_scheme_links(
|
||||
ctx: typer.Context,
|
||||
start_time: Annotated[str, typer.Option("--start-time", help="开始时间")],
|
||||
end_time: Annotated[str, typer.Option("--end-time", help="结束时间")],
|
||||
scheme: Annotated[str | None, typer.Option("--scheme", help="方案名称")] = None,
|
||||
scheme_type: Annotated[str | None, typer.Option("--scheme-type", help="方案类型")] = None,
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取方案管道数据成功",
|
||||
method="GET",
|
||||
path="/scheme/links",
|
||||
params={
|
||||
"scheme_name": resolve_scheme(runtime, scheme, required=True),
|
||||
"scheme_type": _scheme_type_option(scheme_type),
|
||||
"start_time": parse_time_with_timezone(start_time, option_name="--start-time").isoformat(),
|
||||
"end_time": parse_time_with_timezone(end_time, option_name="--end-time").isoformat(),
|
||||
},
|
||||
require_auth=True,
|
||||
require_project=True,
|
||||
)
|
||||
|
||||
|
||||
@data_timeseries_scheme_app.command("node-field")
|
||||
def data_scheme_node_field(
|
||||
ctx: typer.Context,
|
||||
node: Annotated[str, typer.Option("--node", help="节点 ID")],
|
||||
field: Annotated[str, typer.Option("--field", help="字段名,仅支持 actual_demand|total_head|pressure|quality")],
|
||||
start_time: Annotated[str, typer.Option("--start-time", help="开始时间")],
|
||||
end_time: Annotated[str, typer.Option("--end-time", help="结束时间")],
|
||||
scheme: Annotated[str | None, typer.Option("--scheme", help="方案名称")] = None,
|
||||
scheme_type: Annotated[str | None, typer.Option("--scheme-type", help="方案类型")] = None,
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
field = _validate_node_field(field, option_name="--field")
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取方案节点字段成功",
|
||||
method="GET",
|
||||
path=f"/scheme/nodes/{node}/field",
|
||||
params={
|
||||
"field": field,
|
||||
"scheme_name": resolve_scheme(runtime, scheme, required=True),
|
||||
"scheme_type": _scheme_type_option(scheme_type),
|
||||
"start_time": parse_time_with_timezone(start_time, option_name="--start-time").isoformat(),
|
||||
"end_time": parse_time_with_timezone(end_time, option_name="--end-time").isoformat(),
|
||||
},
|
||||
require_auth=True,
|
||||
require_project=True,
|
||||
)
|
||||
|
||||
|
||||
@data_timeseries_scheme_app.command("simulation")
|
||||
def data_scheme_simulation(
|
||||
ctx: typer.Context,
|
||||
query: Annotated[SimulationQuery, typer.Option("--query", help="查询模式,仅支持 by-id-time|by-scheme-time-property")],
|
||||
scheme: Annotated[str | None, typer.Option("--scheme", help="方案名称")] = None,
|
||||
scheme_type: Annotated[str | None, typer.Option("--scheme-type", help="方案类型")] = None,
|
||||
id: Annotated[str | None, typer.Option("--id", help="元素 ID")] = None,
|
||||
time: Annotated[str, typer.Option("--time", help="查询时间")] = "",
|
||||
type: Annotated[ElementType, typer.Option("--type", help="元素类型,仅支持 pipe|junction;links/nodes 是子命令")] = ElementType.PIPE,
|
||||
property: Annotated[str | None, typer.Option("--property", help="属性名;pipe: flow|friction|headloss|quality|reaction|setting|status|velocity;junction: actual_demand|total_head|pressure|quality")] = None,
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
params = {
|
||||
"scheme_name": resolve_scheme(runtime, scheme, required=True),
|
||||
"scheme_type": _scheme_type_option(scheme_type),
|
||||
"query_time": parse_time_with_timezone(time, option_name="--time").isoformat(),
|
||||
"type": type.value,
|
||||
}
|
||||
if query == SimulationQuery.BY_ID_TIME:
|
||||
if not id:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="ID_REQUIRED",
|
||||
message="--id is required for --query by-id-time",
|
||||
exit_code=2,
|
||||
)
|
||||
params["id"] = id
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取方案单点模拟数据成功",
|
||||
method="GET",
|
||||
path="/scheme/query/by-id-time",
|
||||
params=params,
|
||||
require_auth=True,
|
||||
require_project=True,
|
||||
)
|
||||
return
|
||||
if query == SimulationQuery.BY_SCHEME_TIME_PROPERTY:
|
||||
if not property:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="PROPERTY_REQUIRED",
|
||||
message="--property is required for --query by-scheme-time-property",
|
||||
exit_code=2,
|
||||
)
|
||||
property = _validate_element_property(type, property, option_name="--property")
|
||||
params["property"] = property
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取方案属性聚合数据成功",
|
||||
method="GET",
|
||||
path="/scheme/query/by-scheme-time-property",
|
||||
params=params,
|
||||
require_auth=True,
|
||||
require_project=True,
|
||||
)
|
||||
return
|
||||
raise AssertionError(f"unreachable query variant: {query}")
|
||||
|
||||
|
||||
@data_timeseries_scada_app.command("query")
|
||||
def data_scada_query(
|
||||
ctx: typer.Context,
|
||||
device_id: Annotated[list[str], typer.Option("--device-id", help="设备 ID,可重复")],
|
||||
start_time: Annotated[str, typer.Option("--start-time", help="开始时间")],
|
||||
end_time: Annotated[str, typer.Option("--end-time", help="结束时间")],
|
||||
field: Annotated[str | None, typer.Option("--field", help="字段名,仅支持 monitored_value|cleaned_value")] = None,
|
||||
) -> None:
|
||||
path = "/scada/by-ids-field-time-range" if field else "/scada/by-ids-time-range"
|
||||
params = {
|
||||
"device_ids": ",".join(device_id),
|
||||
"start_time": parse_time_with_timezone(start_time, option_name="--start-time").isoformat(),
|
||||
"end_time": parse_time_with_timezone(end_time, option_name="--end-time").isoformat(),
|
||||
}
|
||||
if field:
|
||||
field = _validate_scada_field(field, option_name="--field")
|
||||
params["field"] = field
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取 SCADA 时序成功",
|
||||
method="GET",
|
||||
path=path,
|
||||
params=params,
|
||||
require_auth=True,
|
||||
require_project=True,
|
||||
)
|
||||
|
||||
|
||||
@data_timeseries_composite_app.callback(invoke_without_command=True)
|
||||
def data_timeseries_composite(
|
||||
ctx: typer.Context,
|
||||
kind: Annotated[CompositeKind | None, typer.Option("--kind", help="复合查询类型,仅支持 scada-simulation|element-simulation|element-scada")] = None,
|
||||
feature: Annotated[list[str] | None, typer.Option("--feature", help="特征值,可重复")] = None,
|
||||
start_time: Annotated[str | None, typer.Option("--start-time", help="开始时间")] = None,
|
||||
end_time: Annotated[str | None, typer.Option("--end-time", help="结束时间")] = None,
|
||||
pipe: Annotated[str | None, typer.Option("--pipe", help="pipeline-health 用管道 ID")] = None,
|
||||
scheme: Annotated[str | None, typer.Option("--scheme", help="方案名称")] = None,
|
||||
scheme_type: Annotated[str | None, typer.Option("--scheme-type", help="方案类型")] = None,
|
||||
use_cleaned: Annotated[bool, typer.Option("--use-cleaned", help="element-scada 使用清洗值")] = False,
|
||||
) -> None:
|
||||
_ = pipe
|
||||
if ctx.invoked_subcommand is not None:
|
||||
return
|
||||
if not kind or not start_time or not end_time:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="INVALID_COMPOSITE_ARGS",
|
||||
message="composite query requires --kind, --start-time, and --end-time",
|
||||
exit_code=2,
|
||||
)
|
||||
runtime = runtime_context(ctx)
|
||||
params = {
|
||||
"start_time": parse_time_with_timezone(start_time, option_name="--start-time").isoformat(),
|
||||
"end_time": parse_time_with_timezone(end_time, option_name="--end-time").isoformat(),
|
||||
}
|
||||
if kind == CompositeKind.SCADA_SIMULATION:
|
||||
if not feature:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="FEATURE_REQUIRED",
|
||||
message="--feature is required for scada-simulation",
|
||||
exit_code=2,
|
||||
)
|
||||
params["device_ids"] = ",".join(feature)
|
||||
scheme_name = resolve_scheme(runtime, scheme)
|
||||
if scheme_name:
|
||||
params["scheme_name"] = scheme_name
|
||||
params["scheme_type"] = _scheme_type_option(scheme_type)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取复合 SCADA-模拟数据成功",
|
||||
method="GET",
|
||||
path="/composite/scada-simulation",
|
||||
params=params,
|
||||
require_auth=True,
|
||||
require_project=True,
|
||||
)
|
||||
return
|
||||
if kind == CompositeKind.ELEMENT_SIMULATION:
|
||||
if not feature:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="FEATURE_REQUIRED",
|
||||
message="--feature is required for element-simulation",
|
||||
exit_code=2,
|
||||
)
|
||||
params["feature_infos"] = ",".join(feature)
|
||||
scheme_name = resolve_scheme(runtime, scheme)
|
||||
if scheme_name:
|
||||
params["scheme_name"] = scheme_name
|
||||
params["scheme_type"] = _scheme_type_option(scheme_type)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取复合元素模拟数据成功",
|
||||
method="GET",
|
||||
path="/composite/element-simulation",
|
||||
params=params,
|
||||
require_auth=True,
|
||||
require_project=True,
|
||||
)
|
||||
return
|
||||
if kind == CompositeKind.ELEMENT_SCADA:
|
||||
if not feature or len(feature) != 1:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="FEATURE_REQUIRED",
|
||||
message="element-scada requires exactly one --feature as element_id",
|
||||
exit_code=2,
|
||||
)
|
||||
params["element_id"] = feature[0]
|
||||
params["use_cleaned"] = use_cleaned
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取元素关联 SCADA 数据成功",
|
||||
method="GET",
|
||||
path="/composite/element-scada",
|
||||
params=params,
|
||||
require_auth=True,
|
||||
require_project=True,
|
||||
)
|
||||
return
|
||||
raise AssertionError(f"unreachable composite kind: {kind}")
|
||||
|
||||
|
||||
@data_timeseries_composite_app.command("pipeline-health")
|
||||
def data_composite_pipeline_health(
|
||||
ctx: typer.Context,
|
||||
pipe: Annotated[str, typer.Option("--pipe", help="管道 ID")],
|
||||
start_time: Annotated[str, typer.Option("--start-time", help="开始时间")],
|
||||
end_time: Annotated[str, typer.Option("--end-time", help="结束时间")],
|
||||
) -> None:
|
||||
_ = pipe, start_time
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取管道健康预测成功",
|
||||
method="GET",
|
||||
path="/composite/pipeline-health-prediction",
|
||||
params={
|
||||
"network_name": require_network(runtime_context(ctx)),
|
||||
"query_time": parse_time_with_timezone(end_time, option_name="--end-time").isoformat(),
|
||||
},
|
||||
require_auth=True,
|
||||
require_project=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
def _scada_mapping(kind: str, action: str) -> tuple[str, dict[str, str]]:
|
||||
mapping = {
|
||||
("info", "get"): ("/getscadainfo/", {"id_param": "id"}),
|
||||
("info", "list"): ("/getallscadainfo/", {}),
|
||||
}
|
||||
result = mapping.get((kind, action))
|
||||
if result is None:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="INVALID_SCADA_KIND",
|
||||
message=f"unsupported scada {action} kind: {kind}",
|
||||
exit_code=2,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@data_scada_app.command("get")
|
||||
def data_scada_get(
|
||||
ctx: typer.Context,
|
||||
kind: Annotated[ScadaListKind, typer.Option("--kind", help="SCADA 类型,仅支持 info")],
|
||||
id: Annotated[str, typer.Option("--id", help="记录 ID")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
path, meta = _scada_mapping(kind.value, "get")
|
||||
params = {"network": require_network(runtime), meta["id_param"]: id}
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取 SCADA 数据成功",
|
||||
method="GET",
|
||||
path=path,
|
||||
params=params,
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@data_scada_app.command("list")
|
||||
def data_scada_list(
|
||||
ctx: typer.Context,
|
||||
kind: Annotated[ScadaListKind, typer.Option("--kind", help="SCADA 类型,仅支持 info")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
path, _ = _scada_mapping(kind.value, "list")
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取 SCADA 列表成功",
|
||||
method="GET",
|
||||
path=path,
|
||||
params={"network": require_network(runtime)},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@data_scheme_app.command("schema")
|
||||
def data_scheme_schema(ctx: typer.Context) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取方案 schema 成功",
|
||||
method="GET",
|
||||
path="/getschemeschema/",
|
||||
params={"network": require_network(runtime)},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@data_scheme_app.command("get")
|
||||
def data_scheme_get(
|
||||
ctx: typer.Context,
|
||||
name: Annotated[str, typer.Option("--name", help="方案名称")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取方案成功",
|
||||
method="GET",
|
||||
path="/getscheme/",
|
||||
params={"network": require_network(runtime), "schema_name": name},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@data_scheme_app.command("list")
|
||||
def data_scheme_list(ctx: typer.Context) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取方案列表成功",
|
||||
method="GET",
|
||||
path="/schemes",
|
||||
params={"network": require_network(runtime)},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
@@ -1,255 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
import typer
|
||||
|
||||
from .apps import component_option_app, network_app
|
||||
from .common import emit_api, runtime_context
|
||||
from .core import CLIError, require_network
|
||||
from .option_types import ComponentOptionKind
|
||||
|
||||
|
||||
@network_app.command("get-junction-properties")
|
||||
def network_get_junction_properties(
|
||||
ctx: typer.Context,
|
||||
junction: Annotated[str, typer.Option("--junction", help="节点 ID")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取节点属性成功",
|
||||
method="GET",
|
||||
path="/getjunctionproperties/",
|
||||
params={"network": require_network(runtime), "junction": junction},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@network_app.command("get-pipe-properties")
|
||||
def network_get_pipe_properties(
|
||||
ctx: typer.Context,
|
||||
pipe: Annotated[str, typer.Option("--pipe", help="管道 ID")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取管道属性成功",
|
||||
method="GET",
|
||||
path="/getpipeproperties/",
|
||||
params={"network": require_network(runtime), "pipe": pipe},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@network_app.command("get-all-pipes-properties")
|
||||
def network_get_all_pipes_properties(ctx: typer.Context) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取全部管道属性成功",
|
||||
method="GET",
|
||||
path="/getallpipeproperties/",
|
||||
params={"network": require_network(runtime)},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@network_app.command("get-reservoir-properties")
|
||||
def network_get_reservoir_properties(
|
||||
ctx: typer.Context,
|
||||
reservoir: Annotated[str, typer.Option("--reservoir", help="水库 ID")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取水库属性成功",
|
||||
method="GET",
|
||||
path="/getreservoirproperties/",
|
||||
params={"network": require_network(runtime), "reservoir": reservoir},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@network_app.command("get-all-reservoirs-properties")
|
||||
def network_get_all_reservoir_properties(ctx: typer.Context) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取全部水库属性成功",
|
||||
method="GET",
|
||||
path="/getallreservoirproperties/",
|
||||
params={"network": require_network(runtime)},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@network_app.command("get-tank-properties")
|
||||
def network_get_tank_properties(
|
||||
ctx: typer.Context,
|
||||
tank: Annotated[str, typer.Option("--tank", help="水箱 ID")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取水箱属性成功",
|
||||
method="GET",
|
||||
path="/gettankproperties/",
|
||||
params={"network": require_network(runtime), "tank": tank},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@network_app.command("get-all-tanks-properties")
|
||||
def network_get_all_tank_properties(ctx: typer.Context) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取全部水箱属性成功",
|
||||
method="GET",
|
||||
path="/getalltankproperties/",
|
||||
params={"network": require_network(runtime)},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@network_app.command("get-pump-properties")
|
||||
def network_get_pump_properties(
|
||||
ctx: typer.Context,
|
||||
pump: Annotated[str, typer.Option("--pump", help="水泵 ID")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取水泵属性成功",
|
||||
method="GET",
|
||||
path="/getpumpproperties/",
|
||||
params={"network": require_network(runtime), "pump": pump},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@network_app.command("get-all-pumps-properties")
|
||||
def network_get_all_pump_properties(ctx: typer.Context) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取全部水泵属性成功",
|
||||
method="GET",
|
||||
path="/getallpumpproperties/",
|
||||
params={"network": require_network(runtime)},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@network_app.command("get-valve-properties")
|
||||
def network_get_valve_properties(
|
||||
ctx: typer.Context,
|
||||
valve: Annotated[str, typer.Option("--valve", help="阀门 ID")],
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取阀门属性成功",
|
||||
method="GET",
|
||||
path="/getvalveproperties/",
|
||||
params={"network": require_network(runtime), "valve": valve},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@network_app.command("get-all-valves-properties")
|
||||
def network_get_all_valve_properties(ctx: typer.Context) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取全部阀门属性成功",
|
||||
method="GET",
|
||||
path="/getallvalveproperties/",
|
||||
params={"network": require_network(runtime)},
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@component_option_app.command("schema")
|
||||
def component_option_schema(
|
||||
ctx: typer.Context,
|
||||
kind: Annotated[ComponentOptionKind, typer.Option("--kind", help="选项类型,仅支持 time|energy|pump-energy|network")],
|
||||
pump: Annotated[str | None, typer.Option("--pump", help="pump-energy 时需要的泵 ID")] = None,
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
path = _component_option_path(kind.value, schema=True)
|
||||
params = {"network": require_network(runtime)}
|
||||
if kind == ComponentOptionKind.PUMP_ENERGY and pump:
|
||||
params["pump"] = pump
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取选项 schema 成功",
|
||||
method="GET",
|
||||
path=path,
|
||||
params=params,
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
@component_option_app.command("get")
|
||||
def component_option_get(
|
||||
ctx: typer.Context,
|
||||
kind: Annotated[ComponentOptionKind, typer.Option("--kind", help="选项类型,仅支持 time|energy|pump-energy|network")],
|
||||
pump: Annotated[str | None, typer.Option("--pump", help="pump-energy 时需要的泵 ID")] = None,
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
path = _component_option_path(kind.value, schema=False)
|
||||
params = {"network": require_network(runtime)}
|
||||
if kind == ComponentOptionKind.PUMP_ENERGY:
|
||||
if not pump:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="PUMP_REQUIRED",
|
||||
message="--pump is required when --kind pump-energy",
|
||||
exit_code=2,
|
||||
)
|
||||
params["pump"] = pump
|
||||
emit_api(
|
||||
ctx,
|
||||
summary="读取选项属性成功",
|
||||
method="GET",
|
||||
path=path,
|
||||
params=params,
|
||||
require_auth=True,
|
||||
require_network_ctx=True,
|
||||
)
|
||||
|
||||
|
||||
def _component_option_path(kind: str, *, schema: bool) -> str:
|
||||
routes = {
|
||||
("time", True): "/gettimeschema",
|
||||
("time", False): "/gettimeproperties/",
|
||||
("energy", True): "/getenergyschema/",
|
||||
("energy", False): "/getenergyproperties/",
|
||||
("pump-energy", True): "/getpumpenergyschema/",
|
||||
("pump-energy", False): "/getpumpenergyproperties//",
|
||||
("network", True): "/getoptionschema/",
|
||||
("network", False): "/getoptionproperties/",
|
||||
}
|
||||
path = routes.get((kind, schema))
|
||||
if path is None:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="INVALID_KIND",
|
||||
message="--kind must be one of time, energy, pump-energy, network",
|
||||
exit_code=2,
|
||||
)
|
||||
return path
|
||||
@@ -1,63 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import typer
|
||||
|
||||
from .core import DEFAULT_TIMEOUT, build_runtime_context, emit_success, request_json
|
||||
|
||||
|
||||
def runtime_context(ctx: typer.Context):
|
||||
obj = ctx.obj
|
||||
if not isinstance(obj, dict):
|
||||
obj = {}
|
||||
ctx.obj = obj
|
||||
|
||||
cached_runtime = obj.get("_runtime_context")
|
||||
if cached_runtime is not None:
|
||||
return cached_runtime
|
||||
|
||||
runtime = build_runtime_context(
|
||||
server=obj.get("server"),
|
||||
auth_stdin=obj.get("auth_stdin", False),
|
||||
scheme=obj.get("scheme"),
|
||||
timeout=obj.get("timeout", DEFAULT_TIMEOUT),
|
||||
request_id=obj.get("request_id"),
|
||||
)
|
||||
obj["_runtime_context"] = runtime
|
||||
return runtime
|
||||
|
||||
|
||||
def emit_api(
|
||||
ctx: typer.Context,
|
||||
*,
|
||||
summary: str,
|
||||
method: str,
|
||||
path: str,
|
||||
params: dict[str, Any] | None = None,
|
||||
json_body: Any = None,
|
||||
require_auth: bool = True,
|
||||
require_project: bool = False,
|
||||
require_network_ctx: bool = False,
|
||||
require_username_ctx: bool = False,
|
||||
next_commands: list[str] | None = None,
|
||||
) -> None:
|
||||
runtime = runtime_context(ctx)
|
||||
data, duration_ms = request_json(
|
||||
runtime,
|
||||
method=method,
|
||||
path=path,
|
||||
params=params,
|
||||
json_body=json_body,
|
||||
require_auth=require_auth,
|
||||
require_project=require_project,
|
||||
require_network_ctx=require_network_ctx,
|
||||
require_username_ctx=require_username_ctx,
|
||||
)
|
||||
emit_success(
|
||||
summary=summary,
|
||||
data=data,
|
||||
ctx=runtime,
|
||||
duration_ms=duration_ms,
|
||||
next_commands=next_commands,
|
||||
)
|
||||
@@ -1,638 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
import requests
|
||||
import typer
|
||||
|
||||
SCHEMA_VERSION = "tjwater-cli/v1"
|
||||
CLI_NAME = "tjwater-cli"
|
||||
DEFAULT_TIMEOUT = 180
|
||||
DEFAULT_SERVER = "http://192.168.1.114:8000"
|
||||
|
||||
|
||||
class CLIError(Exception):
|
||||
def __init__(
|
||||
self,
|
||||
summary: str,
|
||||
*,
|
||||
code: str,
|
||||
message: str,
|
||||
exit_code: int,
|
||||
retryable: bool = False,
|
||||
next_commands: list[str] | None = None,
|
||||
data: Any = None,
|
||||
) -> None:
|
||||
super().__init__(message)
|
||||
self.summary = summary
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.exit_code = exit_code
|
||||
self.retryable = retryable
|
||||
self.next_commands = next_commands or []
|
||||
self.data = data
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AuthContext:
|
||||
server: str | None = None
|
||||
access_token: str | None = None
|
||||
project_id: str | None = None
|
||||
username: str | None = None
|
||||
network: str | None = None
|
||||
headers: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RuntimeContext:
|
||||
server: str | None
|
||||
auth: AuthContext
|
||||
scheme: str | None
|
||||
timeout: int
|
||||
request_id: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CommandOptionDoc:
|
||||
name: str
|
||||
description: str
|
||||
required: bool = False
|
||||
repeated: bool = False
|
||||
default: Any = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CommandDoc:
|
||||
path: tuple[str, ...]
|
||||
summary: str
|
||||
description: str
|
||||
options: tuple[CommandOptionDoc, ...] = ()
|
||||
examples: tuple[str, ...] = ()
|
||||
next_commands: tuple[str, ...] = ()
|
||||
output: str = "标准 JSON 输出"
|
||||
|
||||
|
||||
def _pick(mapping: Mapping[str, Any], *keys: str) -> Any:
|
||||
for key in keys:
|
||||
value = mapping.get(key)
|
||||
if value not in (None, ""):
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def load_auth_context(auth_stdin: bool = False) -> AuthContext:
|
||||
if auth_stdin:
|
||||
raw = json.loads(sys.stdin.read())
|
||||
else:
|
||||
extra_headers = os.getenv("TJWATER_EXTRA_HEADERS")
|
||||
raw = {
|
||||
"server": os.getenv("TJWATER_SERVER"),
|
||||
"access_token": os.getenv("TJWATER_ACCESS_TOKEN"),
|
||||
"project_id": os.getenv("TJWATER_PROJECT_ID"),
|
||||
"username": os.getenv("TJWATER_USERNAME"),
|
||||
"network": os.getenv("TJWATER_NETWORK"),
|
||||
"headers": json.loads(extra_headers) if extra_headers else {},
|
||||
}
|
||||
|
||||
headers = raw.get("headers") or {}
|
||||
if not isinstance(headers, dict):
|
||||
raise CLIError(
|
||||
"认证失败",
|
||||
code="AUTH_CONTEXT_INVALID",
|
||||
message="auth context headers must be a JSON object",
|
||||
exit_code=3,
|
||||
)
|
||||
|
||||
return AuthContext(
|
||||
server=_pick(raw, "server", "base_url"),
|
||||
access_token=_pick(raw, "access_token", "token", "accessToken"),
|
||||
project_id=_pick(raw, "project_id", "projectId", "x_project_id"),
|
||||
username=_pick(raw, "username", "preferred_username"),
|
||||
network=_pick(raw, "network", "project_code", "projectCode", "project"),
|
||||
headers={str(key): str(value) for key, value in headers.items()},
|
||||
)
|
||||
|
||||
|
||||
def build_runtime_context(
|
||||
*,
|
||||
server: str | None,
|
||||
auth_stdin: bool = False,
|
||||
scheme: str | None,
|
||||
timeout: int,
|
||||
request_id: str | None,
|
||||
) -> RuntimeContext:
|
||||
auth = load_auth_context(auth_stdin=auth_stdin)
|
||||
resolved_request_id = request_id or str(uuid.uuid4())
|
||||
return RuntimeContext(
|
||||
server=server or auth.server or DEFAULT_SERVER,
|
||||
auth=auth,
|
||||
scheme=scheme,
|
||||
timeout=timeout,
|
||||
request_id=resolved_request_id,
|
||||
)
|
||||
|
||||
|
||||
def require_server(ctx: RuntimeContext) -> str:
|
||||
if ctx.server:
|
||||
return ctx.server.rstrip("/")
|
||||
raise CLIError(
|
||||
"认证失败",
|
||||
code="SERVER_REQUIRED",
|
||||
message="missing server URL; use --server or include server in auth context",
|
||||
exit_code=3,
|
||||
)
|
||||
|
||||
|
||||
def require_access_token(ctx: RuntimeContext) -> str:
|
||||
if ctx.auth.access_token:
|
||||
return ctx.auth.access_token
|
||||
raise CLIError(
|
||||
"认证失败",
|
||||
code="UNAUTHENTICATED",
|
||||
message="missing access token for agent context",
|
||||
exit_code=3,
|
||||
next_commands=["provide access_token via --auth-stdin or TJWATER_ACCESS_TOKEN env var"],
|
||||
)
|
||||
|
||||
|
||||
def require_project_id(ctx: RuntimeContext) -> str:
|
||||
if ctx.auth.project_id:
|
||||
return ctx.auth.project_id
|
||||
raise CLIError(
|
||||
"认证失败",
|
||||
code="PROJECT_CONTEXT_REQUIRED",
|
||||
message="missing project_id for agent context",
|
||||
exit_code=3,
|
||||
next_commands=["add project_id to auth context"],
|
||||
)
|
||||
|
||||
|
||||
def require_network(ctx: RuntimeContext) -> str:
|
||||
if ctx.auth.network:
|
||||
return ctx.auth.network
|
||||
raise CLIError(
|
||||
"认证失败",
|
||||
code="NETWORK_CONTEXT_REQUIRED",
|
||||
message="missing network in auth context for legacy network-based endpoints",
|
||||
exit_code=3,
|
||||
next_commands=["add network to auth context"],
|
||||
)
|
||||
|
||||
|
||||
def require_username(ctx: RuntimeContext) -> str:
|
||||
if ctx.auth.username:
|
||||
return ctx.auth.username
|
||||
raise CLIError(
|
||||
"认证失败",
|
||||
code="USERNAME_CONTEXT_REQUIRED",
|
||||
message="missing username in auth context",
|
||||
exit_code=3,
|
||||
next_commands=["add username to auth context"],
|
||||
)
|
||||
|
||||
|
||||
def resolve_scheme(ctx: RuntimeContext, explicit_scheme: str | None, *, required: bool = False) -> str | None:
|
||||
scheme = explicit_scheme or ctx.scheme
|
||||
if required and not scheme:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="SCHEME_REQUIRED",
|
||||
message="missing scheme; use --scheme",
|
||||
exit_code=2,
|
||||
)
|
||||
return scheme
|
||||
|
||||
|
||||
def parse_time_with_timezone(value: str, *, option_name: str) -> datetime:
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value)
|
||||
except ValueError as exc:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="INVALID_TIME",
|
||||
message=f"{option_name} must be a valid ISO 8601 / RFC 3339 timestamp",
|
||||
exit_code=2,
|
||||
) from exc
|
||||
if parsed.tzinfo is None:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="TIMEZONE_REQUIRED",
|
||||
message=f"{option_name} must include an explicit timezone offset",
|
||||
exit_code=2,
|
||||
)
|
||||
return parsed
|
||||
|
||||
|
||||
def read_json_input(path: Path, *, label: str) -> Any:
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
except FileNotFoundError as exc:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="INPUT_NOT_FOUND",
|
||||
message=f"{label} file not found: {path}",
|
||||
exit_code=2,
|
||||
) from exc
|
||||
except json.JSONDecodeError as exc:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="INPUT_INVALID_JSON",
|
||||
message=f"{label} file must be valid JSON: {path}",
|
||||
exit_code=2,
|
||||
) from exc
|
||||
|
||||
|
||||
def parse_burst_file(path: Path) -> tuple[list[str], list[float]]:
|
||||
raw = read_json_input(path, label="burst")
|
||||
if isinstance(raw, dict) and "bursts" in raw:
|
||||
raw = raw["bursts"]
|
||||
if isinstance(raw, dict) and "burst_ID" in raw and "burst_size" in raw:
|
||||
ids = [str(item) for item in raw["burst_ID"]]
|
||||
sizes = [float(item) for item in raw["burst_size"]]
|
||||
if len(ids) != len(sizes):
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="BURST_FILE_INVALID",
|
||||
message="burst file burst_ID and burst_size must have the same length",
|
||||
exit_code=2,
|
||||
)
|
||||
return ids, sizes
|
||||
if isinstance(raw, list):
|
||||
ids: list[str] = []
|
||||
sizes: list[float] = []
|
||||
for item in raw:
|
||||
if not isinstance(item, dict) or "id" not in item or "size" not in item:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="BURST_FILE_INVALID",
|
||||
message="burst file items must contain id and size",
|
||||
exit_code=2,
|
||||
)
|
||||
ids.append(str(item["id"]))
|
||||
sizes.append(float(item["size"]))
|
||||
return ids, sizes
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="BURST_FILE_INVALID",
|
||||
message="burst file must be a JSON array or object with burst_ID/burst_size",
|
||||
exit_code=2,
|
||||
)
|
||||
|
||||
|
||||
def parse_valve_setting_file(path: Path) -> tuple[list[str], list[float]]:
|
||||
raw = read_json_input(path, label="valve-setting")
|
||||
if isinstance(raw, dict) and "valves" in raw and "valves_k" in raw:
|
||||
valves = [str(item) for item in raw["valves"]]
|
||||
openings = [float(item) for item in raw["valves_k"]]
|
||||
if len(valves) != len(openings):
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="VALVE_SETTING_INVALID",
|
||||
message="valves and valves_k must have the same length",
|
||||
exit_code=2,
|
||||
)
|
||||
return valves, openings
|
||||
if isinstance(raw, list):
|
||||
valves: list[str] = []
|
||||
openings: list[float] = []
|
||||
for item in raw:
|
||||
if not isinstance(item, dict) or "valve" not in item or "opening" not in item:
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="VALVE_SETTING_INVALID",
|
||||
message="valve-setting items must contain valve and opening",
|
||||
exit_code=2,
|
||||
)
|
||||
valves.append(str(item["valve"]))
|
||||
openings.append(float(item["opening"]))
|
||||
return valves, openings
|
||||
raise CLIError(
|
||||
"CLI 参数错误",
|
||||
code="VALVE_SETTING_INVALID",
|
||||
message="valve-setting file must be a JSON array or object with valves/valves_k",
|
||||
exit_code=2,
|
||||
)
|
||||
|
||||
|
||||
def parse_optional_dataset_file(path: Path | None, *, label: str) -> Any:
|
||||
if path is None:
|
||||
return None
|
||||
return read_json_input(path, label=label)
|
||||
|
||||
|
||||
def build_headers(
|
||||
ctx: RuntimeContext,
|
||||
*,
|
||||
require_auth: bool,
|
||||
require_project: bool,
|
||||
) -> dict[str, str]:
|
||||
headers = {
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"X-Request-Id": ctx.request_id,
|
||||
}
|
||||
headers.update(ctx.auth.headers)
|
||||
if require_auth:
|
||||
headers["Authorization"] = f"Bearer {require_access_token(ctx)}"
|
||||
elif ctx.auth.access_token:
|
||||
headers["Authorization"] = f"Bearer {ctx.auth.access_token}"
|
||||
if require_project:
|
||||
headers["X-Project-Id"] = require_project_id(ctx)
|
||||
elif ctx.auth.project_id:
|
||||
headers["X-Project-Id"] = ctx.auth.project_id
|
||||
return headers
|
||||
|
||||
|
||||
def _extract_error_message(response: requests.Response) -> str:
|
||||
try:
|
||||
payload = response.json()
|
||||
except ValueError:
|
||||
text = response.text.strip()
|
||||
return text or f"http {response.status_code}"
|
||||
|
||||
if isinstance(payload, dict):
|
||||
detail = payload.get("detail")
|
||||
if isinstance(detail, str):
|
||||
return detail
|
||||
if isinstance(detail, list):
|
||||
return "; ".join(json.dumps(item, ensure_ascii=False) for item in detail)
|
||||
message = payload.get("message")
|
||||
if isinstance(message, str):
|
||||
return message
|
||||
return json.dumps(payload, ensure_ascii=False)
|
||||
|
||||
|
||||
def map_http_status_to_exit_code(status_code: int) -> int:
|
||||
if status_code in (400, 422):
|
||||
return 2
|
||||
if status_code == 401:
|
||||
return 3
|
||||
if status_code == 403:
|
||||
return 4
|
||||
if status_code == 404:
|
||||
return 5
|
||||
if status_code in (409, 412):
|
||||
return 6
|
||||
return 7
|
||||
|
||||
|
||||
def _parse_response_body(response: requests.Response) -> Any:
|
||||
if response.status_code == 204 or not response.content:
|
||||
return {}
|
||||
content_type = response.headers.get("content-type", "").lower()
|
||||
if "application/json" in content_type:
|
||||
payload = response.json()
|
||||
if isinstance(payload, dict) and payload.get("status") == "error":
|
||||
raise CLIError(
|
||||
"服务端错误",
|
||||
code="SERVER_ERROR",
|
||||
message=str(payload.get("message") or "server returned error status"),
|
||||
exit_code=7,
|
||||
data=payload,
|
||||
)
|
||||
return payload
|
||||
text = response.text
|
||||
if text:
|
||||
return {"report": text}
|
||||
return {}
|
||||
|
||||
|
||||
def _with_network_param(params: dict[str, Any] | None, network: str) -> dict[str, Any]:
|
||||
params = dict(params or {})
|
||||
if "network" in params or "network_name" in params or "name" in params:
|
||||
return params
|
||||
params["network"] = network
|
||||
return params
|
||||
|
||||
|
||||
def request_json(
|
||||
ctx: RuntimeContext,
|
||||
*,
|
||||
method: str,
|
||||
path: str,
|
||||
params: dict[str, Any] | None = None,
|
||||
json_body: Any = None,
|
||||
require_auth: bool = True,
|
||||
require_project: bool = False,
|
||||
require_network_ctx: bool = False,
|
||||
require_username_ctx: bool = False,
|
||||
) -> tuple[Any, int]:
|
||||
require_server(ctx)
|
||||
network = None
|
||||
if require_network_ctx:
|
||||
network = require_network(ctx)
|
||||
if require_username_ctx:
|
||||
require_username(ctx)
|
||||
if network and (params is not None or json_body is None):
|
||||
params = _with_network_param(params, network)
|
||||
|
||||
url = f"{require_server(ctx)}/api/v1{path}"
|
||||
headers = build_headers(ctx, require_auth=require_auth, require_project=require_project)
|
||||
started = time.monotonic()
|
||||
try:
|
||||
response = requests.request(
|
||||
method=method.upper(),
|
||||
url=url,
|
||||
params=params,
|
||||
json=json_body,
|
||||
headers=headers,
|
||||
timeout=ctx.timeout,
|
||||
)
|
||||
except requests.Timeout as exc:
|
||||
raise CLIError(
|
||||
"请求超时",
|
||||
code="REQUEST_TIMEOUT",
|
||||
message=f"request timed out after {ctx.timeout} seconds",
|
||||
exit_code=7,
|
||||
retryable=True,
|
||||
) from exc
|
||||
except requests.RequestException as exc:
|
||||
raise CLIError(
|
||||
"连接失败",
|
||||
code="REQUEST_FAILED",
|
||||
message=str(exc),
|
||||
exit_code=7,
|
||||
retryable=True,
|
||||
) from exc
|
||||
duration_ms = int((time.monotonic() - started) * 1000)
|
||||
|
||||
if not response.ok:
|
||||
raise CLIError(
|
||||
"请求失败",
|
||||
code=f"HTTP_{response.status_code}",
|
||||
message=_extract_error_message(response),
|
||||
exit_code=map_http_status_to_exit_code(response.status_code),
|
||||
retryable=response.status_code >= 500,
|
||||
)
|
||||
return _parse_response_body(response), duration_ms
|
||||
|
||||
|
||||
def request_bytes(
|
||||
ctx: RuntimeContext,
|
||||
*,
|
||||
method: str,
|
||||
path: str,
|
||||
params: dict[str, Any] | None = None,
|
||||
require_auth: bool = True,
|
||||
require_project: bool = False,
|
||||
require_network_ctx: bool = False,
|
||||
) -> tuple[bytes, int]:
|
||||
require_server(ctx)
|
||||
network = None
|
||||
if require_network_ctx:
|
||||
network = require_network(ctx)
|
||||
if network:
|
||||
params = _with_network_param(params, network)
|
||||
|
||||
url = f"{require_server(ctx)}/api/v1{path}"
|
||||
headers = build_headers(ctx, require_auth=require_auth, require_project=require_project)
|
||||
started = time.monotonic()
|
||||
try:
|
||||
response = requests.request(
|
||||
method=method.upper(),
|
||||
url=url,
|
||||
params=params,
|
||||
headers=headers,
|
||||
timeout=ctx.timeout,
|
||||
)
|
||||
except requests.Timeout as exc:
|
||||
raise CLIError(
|
||||
"请求超时",
|
||||
code="REQUEST_TIMEOUT",
|
||||
message=f"request timed out after {ctx.timeout} seconds",
|
||||
exit_code=7,
|
||||
retryable=True,
|
||||
) from exc
|
||||
except requests.RequestException as exc:
|
||||
raise CLIError(
|
||||
"连接失败",
|
||||
code="REQUEST_FAILED",
|
||||
message=str(exc),
|
||||
exit_code=7,
|
||||
retryable=True,
|
||||
) from exc
|
||||
duration_ms = int((time.monotonic() - started) * 1000)
|
||||
|
||||
if not response.ok:
|
||||
raise CLIError(
|
||||
"请求失败",
|
||||
code=f"HTTP_{response.status_code}",
|
||||
message=_extract_error_message(response),
|
||||
exit_code=map_http_status_to_exit_code(response.status_code),
|
||||
retryable=response.status_code >= 500,
|
||||
)
|
||||
return response.content, duration_ms
|
||||
|
||||
|
||||
def build_success_payload(
|
||||
*,
|
||||
summary: str,
|
||||
data: Any,
|
||||
server: str | None,
|
||||
request_id: str,
|
||||
duration_ms: int,
|
||||
next_commands: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"ok": True,
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"summary": summary,
|
||||
"data": data,
|
||||
"metadata": {
|
||||
"request_id": request_id,
|
||||
"server": server,
|
||||
"duration_ms": duration_ms,
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z"),
|
||||
},
|
||||
"next_commands": next_commands or [],
|
||||
}
|
||||
|
||||
|
||||
def build_failure_payload(
|
||||
*,
|
||||
summary: str,
|
||||
code: str,
|
||||
message: str,
|
||||
retryable: bool,
|
||||
server: str | None,
|
||||
request_id: str | None,
|
||||
next_commands: list[str] | None = None,
|
||||
data: Any = None,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"ok": False,
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"summary": summary,
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message,
|
||||
"retryable": retryable,
|
||||
},
|
||||
"data": data,
|
||||
"metadata": {
|
||||
"request_id": request_id,
|
||||
"server": server,
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z"),
|
||||
},
|
||||
"next_commands": next_commands or [],
|
||||
}
|
||||
|
||||
|
||||
def emit_success(
|
||||
*,
|
||||
summary: str,
|
||||
data: Any,
|
||||
ctx: RuntimeContext,
|
||||
duration_ms: int,
|
||||
next_commands: list[str] | None = None,
|
||||
) -> None:
|
||||
typer.echo(
|
||||
json.dumps(
|
||||
build_success_payload(
|
||||
summary=summary,
|
||||
data=data,
|
||||
server=ctx.server,
|
||||
request_id=ctx.request_id,
|
||||
duration_ms=duration_ms,
|
||||
next_commands=next_commands,
|
||||
),
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def emit_failure(
|
||||
*,
|
||||
summary: str,
|
||||
code: str,
|
||||
message: str,
|
||||
exit_code: int,
|
||||
retryable: bool,
|
||||
server: str | None,
|
||||
request_id: str | None,
|
||||
next_commands: list[str] | None = None,
|
||||
data: Any = None,
|
||||
) -> int:
|
||||
typer.echo(
|
||||
json.dumps(
|
||||
build_failure_payload(
|
||||
summary=summary,
|
||||
code=code,
|
||||
message=message,
|
||||
retryable=retryable,
|
||||
server=server,
|
||||
request_id=request_id,
|
||||
next_commands=next_commands,
|
||||
data=data,
|
||||
),
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
return exit_code
|
||||
@@ -1,15 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import click
|
||||
import typer.core
|
||||
|
||||
|
||||
class TJWaterGroup(typer.core.TyperGroup):
|
||||
def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
||||
super().format_help(ctx, formatter)
|
||||
from .helping import build_group_help_appendix
|
||||
|
||||
appendix = build_group_help_appendix(ctx)
|
||||
if appendix:
|
||||
formatter.write_paragraph()
|
||||
formatter.write_text(appendix)
|
||||
@@ -1,414 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Annotated, Any
|
||||
|
||||
import click
|
||||
import typer
|
||||
|
||||
from .apps import GROUP_HELP_APPS, TOP_LEVEL_COMMANDS, app
|
||||
from .core import CLIError
|
||||
from .registry import (
|
||||
get_command_doc,
|
||||
get_group_summary,
|
||||
has_subcommands,
|
||||
is_hidden_path,
|
||||
list_capabilities,
|
||||
list_subcommands,
|
||||
)
|
||||
|
||||
|
||||
def _click_root_command() -> click.Command:
|
||||
# Must stay lazy: the click tree is only complete after command modules import.
|
||||
return typer.main.get_command(app)
|
||||
|
||||
|
||||
def _normalize_command_path(tokens: list[str]) -> tuple[str, ...]:
|
||||
while tokens and tokens[0] not in TOP_LEVEL_COMMANDS:
|
||||
tokens = tokens[1:]
|
||||
return tuple(tokens)
|
||||
|
||||
|
||||
def context_command_path(click_ctx: click.Context | None) -> tuple[str, ...]:
|
||||
if click_ctx is None:
|
||||
return ()
|
||||
return _normalize_command_path(click_ctx.command_path.split())
|
||||
|
||||
|
||||
def _build_click_context(path: tuple[str, ...]) -> click.Context | None:
|
||||
root = _click_root_command()
|
||||
ctx: click.Context = click.Context(root, info_name="tjwater-cli")
|
||||
command: click.Command = root
|
||||
for token in path:
|
||||
if not isinstance(command, click.Group):
|
||||
return None
|
||||
next_command = command.commands.get(token)
|
||||
if next_command is None:
|
||||
return None
|
||||
ctx = click.Context(next_command, info_name=token, parent=ctx)
|
||||
command = next_command
|
||||
return ctx
|
||||
|
||||
|
||||
def build_usage(path: tuple[str, ...]) -> str | None:
|
||||
ctx = _build_click_context(path)
|
||||
if ctx is None:
|
||||
return None
|
||||
parts = ["tjwater-cli", *path]
|
||||
for parameter in ctx.command.params:
|
||||
if not isinstance(parameter, click.Option):
|
||||
continue
|
||||
if "--help" in parameter.opts:
|
||||
continue
|
||||
option_name = next((opt.lstrip("-") for opt in reversed(parameter.opts) if opt.startswith("--")), parameter.name or "")
|
||||
if parameter.is_flag:
|
||||
parts.append(f"--{option_name}" if parameter.required else f"[--{option_name}]")
|
||||
continue
|
||||
placeholder = option_name.upper().replace("-", "_")
|
||||
if parameter.required:
|
||||
parts.extend([f"--{option_name}", f"<{placeholder}>"])
|
||||
else:
|
||||
parts.append(f"[--{option_name} <{placeholder}>]")
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def _click_option_docs(path: tuple[str, ...]) -> list[dict[str, Any]]:
|
||||
ctx = _build_click_context(path)
|
||||
if ctx is None:
|
||||
return []
|
||||
options: list[dict[str, Any]] = []
|
||||
for parameter in ctx.command.params:
|
||||
if not isinstance(parameter, click.Option):
|
||||
continue
|
||||
if "--help" in parameter.opts:
|
||||
continue
|
||||
cli_name = next((opt.lstrip("-") for opt in reversed(parameter.opts) if opt.startswith("--")), parameter.name or "")
|
||||
options.append(
|
||||
{
|
||||
"name": cli_name,
|
||||
"description": parameter.help or "",
|
||||
"required": parameter.required,
|
||||
"repeated": parameter.multiple,
|
||||
"default": parameter.default,
|
||||
}
|
||||
)
|
||||
return options
|
||||
|
||||
|
||||
def _sample_option_value(path: tuple[str, ...], option_name: str) -> str:
|
||||
path_specific_samples: dict[tuple[tuple[str, ...], str], str] = {
|
||||
(("component", "option", "schema"), "kind"): "time",
|
||||
(("component", "option", "get"), "kind"): "time",
|
||||
(("data", "timeseries", "composite"), "kind"): "scada-simulation",
|
||||
(("data", "scada", "get"), "kind"): "info",
|
||||
(("data", "scada", "list"), "kind"): "info",
|
||||
}
|
||||
if (path, option_name) in path_specific_samples:
|
||||
return path_specific_samples[(path, option_name)]
|
||||
if option_name == "start-time":
|
||||
return "2025-01-02T03:04:05+08:00"
|
||||
if option_name == "end-time":
|
||||
return "2025-01-02T04:04:05+08:00"
|
||||
if option_name == "date":
|
||||
return "2025-01-02"
|
||||
if option_name == "duration":
|
||||
return "30"
|
||||
if option_name == "kind":
|
||||
return "time"
|
||||
if option_name == "mode":
|
||||
return "close"
|
||||
if option_name == "scheme":
|
||||
return "baseline"
|
||||
if option_name == "output":
|
||||
return "./demo.inp" if "export-inp" in path else "./output.json"
|
||||
if option_name == "pump":
|
||||
return "PUMP-1"
|
||||
if option_name == "node":
|
||||
return "J1"
|
||||
if option_name == "source-node":
|
||||
return "J1"
|
||||
if option_name == "drainage-node":
|
||||
return "J2"
|
||||
if option_name in {"link", "pipe", "pipe-id", "element-id", "element"}:
|
||||
return "P1"
|
||||
if option_name == "flow":
|
||||
return "120.5"
|
||||
if option_name == "concentration":
|
||||
return "0.8"
|
||||
if option_name == "device-id":
|
||||
return "SCADA-001"
|
||||
if option_name == "burst-file":
|
||||
return "./burst.json"
|
||||
if option_name == "valve-setting-file":
|
||||
return "./valves.json"
|
||||
if option_name.endswith("-file"):
|
||||
return "./input.json"
|
||||
if option_name.endswith("-id"):
|
||||
return "demo-id"
|
||||
return "demo"
|
||||
|
||||
|
||||
def _build_example(path: tuple[str, ...], *, existing_examples: list[str] | None = None) -> str:
|
||||
ctx = _build_click_context(path)
|
||||
required_option_names: list[str] = []
|
||||
if ctx is not None:
|
||||
required_option_names = [
|
||||
next((opt.lstrip("-") for opt in reversed(parameter.opts) if opt.startswith("--")), parameter.name or "")
|
||||
for parameter in ctx.command.params
|
||||
if isinstance(parameter, click.Option) and "--help" not in parameter.opts and parameter.required
|
||||
]
|
||||
if existing_examples:
|
||||
for example in existing_examples:
|
||||
has_required_options = all(f"--{option_name}" in example for option_name in required_option_names)
|
||||
if has_required_options:
|
||||
return example
|
||||
parts = ["tjwater-cli", *path]
|
||||
if ctx is None:
|
||||
return " ".join(parts)
|
||||
for parameter in ctx.command.params:
|
||||
if not isinstance(parameter, click.Option):
|
||||
continue
|
||||
if "--help" in parameter.opts or not parameter.required:
|
||||
continue
|
||||
option_name = next((opt.lstrip("-") for opt in reversed(parameter.opts) if opt.startswith("--")), parameter.name or "")
|
||||
parts.extend([f"--{option_name}", _sample_option_value(path, option_name)])
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def _enrich_leaf_payload(payload: dict[str, Any], path: tuple[str, ...]) -> dict[str, Any]:
|
||||
enriched = dict(payload)
|
||||
enriched["usage"] = build_usage(path) or payload.get("usage")
|
||||
click_options = _click_option_docs(path)
|
||||
if click_options:
|
||||
enriched["options"] = click_options
|
||||
enriched["examples"] = payload.get("examples") or []
|
||||
if not enriched["examples"] or all("<" in example and ">" in example for example in enriched["examples"]):
|
||||
enriched["examples"] = [_build_example(path, existing_examples=enriched["examples"])]
|
||||
return enriched
|
||||
|
||||
|
||||
def _enrich_index_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
enriched = dict(payload)
|
||||
commands: list[dict[str, Any]] = []
|
||||
for command in payload.get("commands", []):
|
||||
command_item = dict(command)
|
||||
path = tuple(command_item["command"].split())
|
||||
doc = get_command_doc(path)
|
||||
if doc is None and has_subcommands(path):
|
||||
command_item["usage"] = f"tjwater-cli {' '.join(path)} help"
|
||||
command_item["example"] = f"tjwater-cli {' '.join(path)} help"
|
||||
else:
|
||||
existing_examples = [] if doc is None else list(doc.get("examples", []))
|
||||
command_item["usage"] = build_usage(path) or command_item.get("usage")
|
||||
command_item["example"] = _build_example(path, existing_examples=existing_examples)
|
||||
commands.append(command_item)
|
||||
enriched["commands"] = commands
|
||||
return enriched
|
||||
|
||||
|
||||
def resolve_help_payload(path: tuple[str, ...]) -> tuple[dict[str, Any] | None, bool]:
|
||||
if not path:
|
||||
return list_capabilities(), True
|
||||
payload = get_command_doc(path)
|
||||
if payload is not None:
|
||||
return _enrich_leaf_payload(payload, path), False
|
||||
if has_subcommands(path):
|
||||
return _enrich_index_payload(list_subcommands(path, get_group_summary(path))), True
|
||||
return None, False
|
||||
|
||||
|
||||
def emit_help_payload(payload: dict[str, Any]) -> None:
|
||||
typer.echo(json.dumps(payload, ensure_ascii=False))
|
||||
|
||||
|
||||
def merge_next_commands(*groups: list[str] | None) -> list[str]:
|
||||
merged: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for group in groups:
|
||||
for command in group or []:
|
||||
if command in seen:
|
||||
continue
|
||||
seen.add(command)
|
||||
merged.append(command)
|
||||
return merged
|
||||
|
||||
|
||||
def merge_error_data(primary: Any, secondary: Any) -> Any:
|
||||
if primary is None:
|
||||
return secondary
|
||||
if secondary is None:
|
||||
return primary
|
||||
if isinstance(primary, dict) and isinstance(secondary, dict):
|
||||
return {**secondary, **primary}
|
||||
return primary
|
||||
|
||||
|
||||
def build_error_guidance(click_ctx: click.Context | None) -> tuple[Any, list[str]]:
|
||||
command_path = context_command_path(click_ctx)
|
||||
usage = build_usage(command_path) if command_path else None
|
||||
if command_path:
|
||||
if command_path[-1] == "help":
|
||||
group_path = command_path[:-1]
|
||||
if group_path:
|
||||
return (
|
||||
{
|
||||
"command_group": " ".join(group_path),
|
||||
"usage": f"tjwater-cli {' '.join(group_path)} help",
|
||||
"examples": [f"tjwater-cli {' '.join(group_path)} help", f"tjwater-cli help {' '.join(group_path)}"],
|
||||
},
|
||||
merge_next_commands(
|
||||
[f"tjwater-cli {' '.join(group_path)} help", f"tjwater-cli help {' '.join(group_path)}"],
|
||||
["tjwater-cli help"],
|
||||
),
|
||||
)
|
||||
payload, is_index = resolve_help_payload(command_path)
|
||||
if payload is not None and not is_index:
|
||||
return (
|
||||
{
|
||||
"command": payload["command"],
|
||||
"usage": payload.get("usage") or usage,
|
||||
"examples": payload.get("examples", []),
|
||||
},
|
||||
merge_next_commands([f"tjwater-cli help {' '.join(command_path)}"], ["tjwater-cli help"]),
|
||||
)
|
||||
if payload is not None and is_index:
|
||||
return (
|
||||
{
|
||||
"command_group": " ".join(command_path),
|
||||
"usage": f"tjwater-cli {' '.join(command_path)} help",
|
||||
"examples": [f"tjwater-cli {' '.join(command_path)} help", f"tjwater-cli help {' '.join(command_path)}"],
|
||||
},
|
||||
merge_next_commands(
|
||||
[f"tjwater-cli {' '.join(command_path)} help", f"tjwater-cli help {' '.join(command_path)}"],
|
||||
["tjwater-cli help"],
|
||||
),
|
||||
)
|
||||
return ({"usage": usage} if usage else None, ["tjwater-cli help"])
|
||||
|
||||
|
||||
def classify_click_error(exc: click.ClickException) -> tuple[str, str]:
|
||||
if isinstance(exc, click.NoSuchOption):
|
||||
return "未知选项", "UNKNOWN_OPTION"
|
||||
if isinstance(exc, click.MissingParameter):
|
||||
return "缺少参数", "MISSING_PARAMETER"
|
||||
if isinstance(exc, click.BadParameter):
|
||||
return "参数无效", "INVALID_PARAMETER"
|
||||
message = exc.format_message()
|
||||
if "No such command" in message:
|
||||
return "未找到命令", "COMMAND_NOT_FOUND"
|
||||
return "CLI 参数错误", "USAGE_ERROR"
|
||||
|
||||
|
||||
def _build_root_help_epilog() -> str:
|
||||
return "\n".join(
|
||||
[
|
||||
"\b",
|
||||
"Examples:",
|
||||
" tjwater-cli help",
|
||||
" tjwater-cli help simulation run",
|
||||
" tjwater-cli simulation run --help",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _build_leaf_help_epilog(path: tuple[str, ...], payload: dict[str, Any]) -> str:
|
||||
lines = ["\b"]
|
||||
description = payload.get("description")
|
||||
usage = payload.get("usage")
|
||||
examples = payload.get("examples", [])
|
||||
next_commands = payload.get("next_commands", [])
|
||||
if description:
|
||||
lines.extend([f"Description: {description}", ""])
|
||||
if usage:
|
||||
lines.extend([f"Usage example: {usage}", ""])
|
||||
if examples:
|
||||
lines.append("Examples:")
|
||||
lines.extend(f" {example}" for example in examples)
|
||||
lines.append("")
|
||||
if next_commands:
|
||||
lines.append("Next steps:")
|
||||
lines.extend(f" {command}" for command in next_commands)
|
||||
lines.append("")
|
||||
lines.extend(["Structured JSON:", f" tjwater-cli help {' '.join(path)}"])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _build_group_help_epilog(path: tuple[str, ...], payload: dict[str, Any]) -> str:
|
||||
lines = ["\b", "Examples:", f" tjwater-cli help {' '.join(path)}"]
|
||||
for command in payload.get("commands", [])[:2]:
|
||||
example = command.get("example")
|
||||
if example:
|
||||
lines.append(f" {example}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def build_group_help_appendix(click_ctx: click.Context | None) -> str | None:
|
||||
path = context_command_path(click_ctx)
|
||||
if not path:
|
||||
return _build_root_help_epilog()
|
||||
payload, is_index = resolve_help_payload(path)
|
||||
if payload is None or not is_index:
|
||||
return None
|
||||
return _build_group_help_epilog(path, payload)
|
||||
|
||||
|
||||
def make_group_help_handler(path_prefix: tuple[str, ...]):
|
||||
def group_help() -> None:
|
||||
payload, is_index = resolve_help_payload(path_prefix)
|
||||
if payload is None:
|
||||
raise CLIError(
|
||||
"未找到命令",
|
||||
code="COMMAND_NOT_FOUND",
|
||||
message=f"unknown command path: {' '.join(path_prefix)}",
|
||||
exit_code=2,
|
||||
next_commands=["tjwater-cli help"],
|
||||
)
|
||||
emit_help_payload(payload)
|
||||
|
||||
group_help.__name__ = f"{'_'.join(path_prefix)}_help"
|
||||
return group_help
|
||||
|
||||
|
||||
def register_group_help_commands() -> None:
|
||||
for group_app, path_prefix in GROUP_HELP_APPS:
|
||||
group_app.command("help")(make_group_help_handler(path_prefix))
|
||||
|
||||
|
||||
def apply_typer_help_metadata() -> None:
|
||||
app.help = "\n".join(
|
||||
[
|
||||
"TJWater agent CLI",
|
||||
"",
|
||||
"Examples:",
|
||||
" tjwater-cli help",
|
||||
" tjwater-cli help simulation run",
|
||||
" tjwater-cli simulation run --help",
|
||||
]
|
||||
)
|
||||
app.short_help = "TJWater agent CLI"
|
||||
for group_app, path_prefix in GROUP_HELP_APPS:
|
||||
for command_info in group_app.registered_commands:
|
||||
command_path = (*path_prefix, command_info.name)
|
||||
if command_info.name == "help":
|
||||
command_info.help = f"输出 {' '.join(path_prefix)} 的 JSON 帮助信息。"
|
||||
command_info.short_help = command_info.help
|
||||
command_info.epilog = "\n".join(["\b", "Example:", f" tjwater-cli help {' '.join(path_prefix)}"])
|
||||
command_info.hidden = False
|
||||
continue
|
||||
payload = get_command_doc(command_path)
|
||||
command_info.help = None if payload is None else str(payload.get("summary", ""))
|
||||
command_info.short_help = command_info.help
|
||||
command_info.epilog = None if payload is None else _build_leaf_help_epilog(command_path, payload)
|
||||
command_info.hidden = is_hidden_path(command_path)
|
||||
for group_info in group_app.registered_groups:
|
||||
group_path = (*path_prefix, group_info.name)
|
||||
summary = get_group_summary(group_path)
|
||||
group_info.help = summary
|
||||
group_info.short_help = summary
|
||||
group_info.hidden = is_hidden_path(group_path)
|
||||
for group_info in app.registered_groups:
|
||||
group_path = (group_info.name,)
|
||||
summary = get_group_summary(group_path)
|
||||
group_info.help = summary
|
||||
group_info.short_help = summary
|
||||
group_info.hidden = is_hidden_path(group_path)
|
||||
@@ -1,112 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
import click
|
||||
import typer
|
||||
from click.exceptions import NoArgsIsHelpError
|
||||
|
||||
from . import commands_analysis, commands_data, commands_readonly # noqa: F401
|
||||
from .apps import app
|
||||
from .core import CLIError, DEFAULT_SERVER, DEFAULT_TIMEOUT, emit_failure
|
||||
from .helping import (
|
||||
apply_typer_help_metadata,
|
||||
build_error_guidance,
|
||||
classify_click_error,
|
||||
emit_help_payload,
|
||||
merge_error_data,
|
||||
merge_next_commands,
|
||||
register_group_help_commands,
|
||||
resolve_help_payload,
|
||||
)
|
||||
|
||||
|
||||
@app.callback()
|
||||
def root_callback(
|
||||
ctx: typer.Context,
|
||||
server: Annotated[str | None, typer.Option("--server", help=f"服务端地址,默认 {DEFAULT_SERVER}")] = None,
|
||||
auth_stdin: Annotated[bool, typer.Option("--auth-stdin", help="从标准输入读取认证上下文 JSON")] = False,
|
||||
scheme: Annotated[str | None, typer.Option("--scheme", help="全局方案标识")] = None,
|
||||
timeout: Annotated[int, typer.Option("--timeout", help="请求超时秒数")] = DEFAULT_TIMEOUT,
|
||||
request_id: Annotated[str | None, typer.Option("--request-id", help="显式请求 ID")] = None,
|
||||
) -> None:
|
||||
ctx.obj = {
|
||||
"server": server,
|
||||
"auth_stdin": auth_stdin,
|
||||
"scheme": scheme,
|
||||
"timeout": timeout,
|
||||
"request_id": request_id,
|
||||
}
|
||||
|
||||
|
||||
register_group_help_commands()
|
||||
|
||||
|
||||
@app.command("help", context_settings={"allow_extra_args": True})
|
||||
def help_command(ctx: typer.Context) -> None:
|
||||
command_path = list(ctx.args)
|
||||
payload, is_index = resolve_help_payload(tuple(command_path))
|
||||
if payload is None:
|
||||
emit_failure(
|
||||
summary="未找到命令",
|
||||
code="COMMAND_NOT_FOUND",
|
||||
message=f"unknown command path: {' '.join(command_path)}",
|
||||
exit_code=2,
|
||||
retryable=False,
|
||||
server=None,
|
||||
request_id=None,
|
||||
data={
|
||||
"usage": "tjwater-cli help <command-path>",
|
||||
"examples": ["tjwater-cli help simulation run", "tjwater-cli simulation help"],
|
||||
},
|
||||
next_commands=["tjwater-cli help", "tjwater-cli help simulation"],
|
||||
)
|
||||
raise typer.Exit(code=2)
|
||||
emit_help_payload(payload)
|
||||
|
||||
|
||||
# Must run at import time because tests call runner.invoke(app, ...) directly.
|
||||
apply_typer_help_metadata()
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
try:
|
||||
app(args=argv if argv is not None else sys.argv[1:], prog_name="tjwater-cli", standalone_mode=False)
|
||||
return 0
|
||||
except CLIError as exc:
|
||||
click_ctx = click.get_current_context(silent=True)
|
||||
error_data, next_commands = build_error_guidance(click_ctx)
|
||||
return emit_failure(
|
||||
summary=exc.summary,
|
||||
code=exc.code,
|
||||
message=exc.message,
|
||||
exit_code=exc.exit_code,
|
||||
retryable=exc.retryable,
|
||||
server=None,
|
||||
request_id=None,
|
||||
next_commands=merge_next_commands(exc.next_commands, next_commands),
|
||||
data=merge_error_data(exc.data, error_data),
|
||||
)
|
||||
except NoArgsIsHelpError:
|
||||
return 0
|
||||
except click.ClickException as exc:
|
||||
click_ctx = click.get_current_context(silent=True) or exc.ctx
|
||||
error_data, next_commands = build_error_guidance(click_ctx)
|
||||
summary, code = classify_click_error(exc)
|
||||
return emit_failure(
|
||||
summary=summary,
|
||||
code=code,
|
||||
message=exc.format_message(),
|
||||
exit_code=2,
|
||||
retryable=False,
|
||||
server=None,
|
||||
request_id=None,
|
||||
next_commands=next_commands,
|
||||
data=error_data,
|
||||
)
|
||||
|
||||
|
||||
def console_entry() -> None:
|
||||
raise SystemExit(main())
|
||||
@@ -1,72 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ElementType(str, Enum):
|
||||
PIPE = "pipe"
|
||||
JUNCTION = "junction"
|
||||
|
||||
|
||||
class SimulationQuery(str, Enum):
|
||||
BY_ID_TIME = "by-id-time"
|
||||
BY_SCHEME_TIME_PROPERTY = "by-scheme-time-property"
|
||||
|
||||
|
||||
class CompositeKind(str, Enum):
|
||||
SCADA_SIMULATION = "scada-simulation"
|
||||
ELEMENT_SIMULATION = "element-simulation"
|
||||
ELEMENT_SCADA = "element-scada"
|
||||
|
||||
|
||||
class ComponentOptionKind(str, Enum):
|
||||
TIME = "time"
|
||||
ENERGY = "energy"
|
||||
PUMP_ENERGY = "pump-energy"
|
||||
NETWORK = "network"
|
||||
|
||||
|
||||
class ValveMode(str, Enum):
|
||||
CLOSE = "close"
|
||||
ISOLATION = "isolation"
|
||||
|
||||
|
||||
class DataSource(str, Enum):
|
||||
MONITORING = "monitoring"
|
||||
SIMULATION = "simulation"
|
||||
|
||||
|
||||
class ScadaListKind(str, Enum):
|
||||
INFO = "info"
|
||||
|
||||
|
||||
PIPE_TIMESERIES_FIELDS: tuple[str, ...] = (
|
||||
"flow",
|
||||
"friction",
|
||||
"headloss",
|
||||
"quality",
|
||||
"reaction",
|
||||
"setting",
|
||||
"status",
|
||||
"velocity",
|
||||
)
|
||||
|
||||
JUNCTION_TIMESERIES_FIELDS: tuple[str, ...] = (
|
||||
"actual_demand",
|
||||
"total_head",
|
||||
"pressure",
|
||||
"quality",
|
||||
)
|
||||
|
||||
SCADA_TIMESERIES_FIELDS: tuple[str, ...] = (
|
||||
"monitored_value",
|
||||
"cleaned_value",
|
||||
)
|
||||
|
||||
|
||||
def timeseries_fields_for_element_type(element_type: ElementType) -> tuple[str, ...]:
|
||||
if element_type == ElementType.PIPE:
|
||||
return PIPE_TIMESERIES_FIELDS
|
||||
if element_type == ElementType.JUNCTION:
|
||||
return JUNCTION_TIMESERIES_FIELDS
|
||||
raise AssertionError(f"unreachable element type: {element_type}")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user