Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18253f2fe0 | ||
|
|
02a8686222 | ||
|
|
50d823ca58 | ||
|
|
9f225374de | ||
|
|
9b16e4e0a5 | ||
|
|
810a39a1dc | ||
|
|
e49d21cd1b | ||
|
|
0a2ce81753 | ||
|
|
b8f2f70013 | ||
|
|
6f9c94a4dd | ||
|
|
9f7f5536d7 | ||
|
|
d33619d0d5 | ||
|
|
6a16ea44b2 | ||
|
|
e862e6c500 | ||
|
|
87824f3b3c | ||
|
|
18943314f8 | ||
|
|
c7947a7481 | ||
|
|
4b02118286 | ||
|
|
dc4e3de85e | ||
|
|
bb5d339dbe | ||
|
|
33615cdcfc | ||
|
|
d6dda51008 | ||
|
|
60c8fc4948 | ||
|
|
3de620a2ae | ||
|
|
8e07d580af | ||
|
|
9be0cd34cf | ||
|
|
f61be3685f | ||
|
|
78af7ecfb3 | ||
|
|
bf8e4fb040 | ||
|
|
67ba9c2ac6 | ||
|
|
21929b44fc | ||
|
|
24169bd277 | ||
|
|
a787327ca2 | ||
|
|
c676b55b70 | ||
|
|
322e8156ee | ||
|
|
1ec82971ca | ||
|
|
2d3fd353b4 | ||
|
|
4191f2e508 | ||
|
|
3fd152e033 | ||
|
|
f6361a9eca | ||
|
|
431cf38aaf | ||
|
|
220e641c0e | ||
|
|
ccfc78bb1c | ||
|
|
f90c5ab6bb | ||
|
|
67648587c5 | ||
|
|
42cac53319 | ||
|
|
c5f97da5c0 | ||
|
|
bf088f691b | ||
|
|
ceea6e7456 | ||
|
|
8338e11837 | ||
|
|
59e44eb1e8 | ||
|
|
cbad19e3bb | ||
|
|
41173d61c8 | ||
|
|
b58f075344 | ||
|
|
90a9bb62c6 | ||
|
|
a310522812 |
+1
-1
@@ -13,7 +13,7 @@ temp/
|
|||||||
data/
|
data/
|
||||||
# db_inp/
|
# db_inp/
|
||||||
inp/
|
inp/
|
||||||
# .env
|
.env
|
||||||
*.pyc
|
*.pyc
|
||||||
*.dump
|
*.dump
|
||||||
app/algorithms/health/model/my_survival_forest_model_quxi.joblib
|
app/algorithms/health/model/my_survival_forest_model_quxi.joblib
|
||||||
|
|||||||
@@ -1,262 +0,0 @@
|
|||||||
name: Server CI/CD
|
|
||||||
|
|
||||||
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
|
|
||||||
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:
|
|
||||||
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."
|
|
||||||
@@ -7,4 +7,3 @@ build/
|
|||||||
*.dump
|
*.dump
|
||||||
.vscode/
|
.vscode/
|
||||||
app/algorithms/health/model/my_survival_forest_model_quxi.joblib
|
app/algorithms/health/model/my_survival_forest_model_quxi.joblib
|
||||||
inp/
|
|
||||||
|
|||||||
@@ -1,38 +1,73 @@
|
|||||||
# Repository Guidelines
|
# Repository Guidelines
|
||||||
|
|
||||||
## Project Structure & Module Organization
|
## Project Purpose
|
||||||
|
|
||||||
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`.
|
This repository is the customer-delivery edition of the TJWater backend. Treat it as a deployable delivery package, not as the primary internal development repository. Changes should be limited to customer-facing fixes, deployment compatibility, configuration templates, packaging, and delivery documentation.
|
||||||
|
|
||||||
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.
|
Do not introduce internal-only experiments, debug utilities, local data, or source material that is not required for customer operation.
|
||||||
|
|
||||||
## Build, Test, and Development Commands
|
## Source Encapsulation Requirement
|
||||||
|
|
||||||
Use the existing conda environment when available:
|
Core backend source code must be encapsulated before delivery. The sensitive implementation areas include business services, native integrations, hydraulic/network algorithms, and EPANET-related logic, especially:
|
||||||
|
|
||||||
|
- `app/services`
|
||||||
|
- `app/native`
|
||||||
|
- `app/algorithms`
|
||||||
|
- `app/infra/epanet`
|
||||||
|
|
||||||
|
Use the existing Cython packaging flow in `scripts/compile.py` for these areas. Do not ship uncompiled core `.py` files in the final customer package when compiled extension modules are expected. Keep public entry points, configuration loading, route wiring, and minimal package files readable only where required for runtime and operations.
|
||||||
|
|
||||||
|
Before removing source files with `--delete-source`, verify the build from a clean working tree or disposable copy. The delete mode is destructive by design.
|
||||||
|
|
||||||
|
## Workspace Structure
|
||||||
|
|
||||||
|
- `app/main.py` is the FastAPI entry point.
|
||||||
|
- `app/api` contains HTTP route handlers.
|
||||||
|
- `app/services` contains core business orchestration and must be protected in delivery builds.
|
||||||
|
- `app/native`, `app/algorithms`, and `app/infra/epanet` contain specialized computation and integration code that must be protected in delivery builds.
|
||||||
|
- `infra/` and `Dockerfile` contain deployment assets.
|
||||||
|
- `scripts/` contains operational and packaging helpers.
|
||||||
|
- `tests/` contains backend tests.
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
Run commands from this repository root:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
conda run -n server python -m pytest tests/unit tests/auth -q
|
conda run -n server python -m pytest tests -q
|
||||||
conda run -n server uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
conda run -n server python scripts/run_server.py
|
||||||
docker build -t tjwater-server:local .
|
conda run -n server python scripts/compile.py
|
||||||
docker compose -f infra/docker/docker-compose.yml config
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`pytest` runs backend tests. `uvicorn` starts the FastAPI app locally. `docker build` verifies the container image. `docker compose config` validates compose syntax and variable expansion.
|
Clean compiled extensions when needed:
|
||||||
|
|
||||||
## Coding Style & Naming Conventions
|
```bash
|
||||||
|
conda run -n server python scripts/compile.py --clean
|
||||||
|
```
|
||||||
|
|
||||||
Use Python 3.12, four-space indentation, type hints for new public functions, and explicit imports. Keep API endpoint modules grouped by domain under `app/api/v1/endpoints`. Use `snake_case` for files, functions, and variables; `PascalCase` for classes and Pydantic models. Prefer existing repository/service patterns in `app/infra/db` and `app/services` over introducing new abstractions.
|
Only use source deletion in a prepared delivery copy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
conda run -n server python scripts/compile.py --delete-source
|
||||||
|
```
|
||||||
|
|
||||||
|
## Change Policy
|
||||||
|
|
||||||
|
Keep changes scoped and conservative. Prefer compatibility patches, configuration adjustments, packaging fixes, and customer deployment hardening. If a change requires substantial business logic updates, make it first in the internal backend repository and then port the reviewed result here.
|
||||||
|
|
||||||
|
Do not weaken the encapsulation process to make debugging easier. If debugging requires readable source, do it in the internal repository or a non-delivery branch/copy.
|
||||||
|
|
||||||
## Testing Guidelines
|
## Testing Guidelines
|
||||||
|
|
||||||
The project uses `pytest`. Name test files `test_*.py` and test functions `test_*`. Keep unit tests isolated with fakes or monkeypatching from `tests/conftest.py`. Some existing tests depend on local data outside the repository; avoid adding new tests that require untracked files. For API changes, add or update tests in `tests/api`.
|
Run the narrowest useful test command for the affected area. For delivery packaging changes, verify both:
|
||||||
|
|
||||||
## Commit & Pull Request Guidelines
|
- tests still pass before packaging;
|
||||||
|
- the packaged/compiled runtime can import and start the FastAPI app.
|
||||||
|
|
||||||
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.
|
Do not add tests that depend on untracked customer data, local database dumps, or machine-specific files.
|
||||||
|
|
||||||
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.
|
## Security & Delivery Rules
|
||||||
|
|
||||||
## Security & Configuration Tips
|
Never commit `.env`, production credentials, customer data, logs, generated caches, `node_modules/`, database dumps, or temporary delivery archives. Use `.env.example` or deployment documentation for required configuration.
|
||||||
|
|
||||||
Do not commit `.env`, database dumps, generated caches, or local project data. Use `.env.example` as the configuration template. Secrets for CI/CD belong in Gitea repository secrets such as `REGISTRY_USERNAME`, `REGISTRY_PASSWORD`, and deploy webhook credentials.
|
Review Docker and deployment files carefully before delivery. Customer packages should contain only the files needed to run, operate, and diagnose the deployed service.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Backend Naming Audit
|
# Backend Naming Audit
|
||||||
|
|
||||||
DOC-003 audit for the internal `TJWaterServerBinary` backend.
|
DOC-003 audit for the customer-delivery `TJWaterServerCustomer` backend.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
@@ -12,15 +12,14 @@ The backend is mounted only under `/api/v1` from `app/main.py`; the old no-prefi
|
|||||||
|
|
||||||
These newer routes already follow the naming rule for public HTTP paths:
|
These newer routes already follow the naming rule for public HTTP paths:
|
||||||
|
|
||||||
- Metadata/admin: `/api/v1/admin/projects`, `/api/v1/admin/users/sync`, `/api/v1/admin/projects/{project_id}/members`
|
|
||||||
- Audit: `/api/v1/audit/logs`, `/api/v1/audit/logs/count`
|
- Audit: `/api/v1/audit/logs`, `/api/v1/audit/logs/count`
|
||||||
- Agent auth: `/api/v1/agent/auth/context`
|
- Metadata: `/api/v1/meta/project`, `/api/v1/meta/projects`, `/api/v1/meta/db/health`
|
||||||
- Business APIs: `/api/v1/burst-detection/detect`, `/api/v1/burst-location/locate`, `/api/v1/leakage/identify`
|
- Business APIs: `/api/v1/burst-detection/detect`, `/api/v1/burst-location/locate`, `/api/v1/leakage/identify`
|
||||||
- Time-series APIs: `/api/v1/scada/by-ids-time-range`, `/api/v1/scada/by-ids-field-time-range`, `/api/v1/composite/clean-scada`
|
- Time-series APIs: `/api/v1/scada/by-ids-time-range`, `/api/v1/scada/by-ids-field-time-range`, `/api/v1/composite/clean-scada`
|
||||||
- Project data APIs: `/api/v1/scada-info`, `/api/v1/scheme-list`, `/api/v1/burst-locate-result`
|
- Project data APIs: `/api/v1/scada-info`, `/api/v1/scheme-list`, `/api/v1/burst-locate-result`
|
||||||
- Web integrations: `/api/v1/web-search`, `/api/v1/geocode`
|
- Web integrations: `/api/v1/web-search`, `/api/v1/geocode`
|
||||||
|
|
||||||
Path template parameters such as `{project_id}`, `{user_id}`, `{device_id}`, `{scheme_name}`, and `{link_id}` intentionally remain `snake_case`.
|
Path template parameters such as `{user_id}`, `{device_id}`, `{scheme_name}`, and `{link_id}` intentionally remain `snake_case`.
|
||||||
|
|
||||||
## Legacy URL Categories
|
## Legacy URL Categories
|
||||||
|
|
||||||
@@ -62,7 +61,7 @@ These are likely safe only after confirming no caller uses them:
|
|||||||
|
|
||||||
## Field Naming
|
## Field Naming
|
||||||
|
|
||||||
Most public JSON, query, and SSE fields are already `snake_case`, including `project_id`, `user_id`, `scheme_name`, `scheme_type`, `start_time`, `end_time`, `device_ids`, `session_id`, and `request_id`.
|
Most public JSON, query, and SSE fields are already `snake_case`, including `user_id`, `scheme_name`, `scheme_type`, `start_time`, `end_time`, `device_ids`, `session_id`, and `request_id`.
|
||||||
|
|
||||||
Known legacy exception:
|
Known legacy exception:
|
||||||
|
|
||||||
@@ -72,6 +71,10 @@ Headers keep standard HTTP casing:
|
|||||||
|
|
||||||
- `X-Project-Id`
|
- `X-Project-Id`
|
||||||
|
|
||||||
|
## Internal vs Customer Difference
|
||||||
|
|
||||||
|
The customer backend retains local auth/user-management routes under `/api/v1/auth` and `/api/v1/users`; the internal backend has migrated to Keycloak/metadata admin routes. Treat those Customer-only auth routes as delivery compatibility surface, not a source for new internal API naming.
|
||||||
|
|
||||||
## Recommendation
|
## Recommendation
|
||||||
|
|
||||||
Do not rename existing legacy routes in place. For each active legacy route, keep the new `kebab-case` alias as the documented path, keep the old route marked deprecated, migrate remaining Agent/customer/script callers, then remove only after a documented compatibility window.
|
Do not rename existing legacy routes in place. For each active legacy route, keep the new `kebab-case` alias as the documented path, keep the old route marked deprecated, migrate remaining Agent/customer/script callers, then remove only after a documented compatibility window.
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# tjwater-server Customer Image Packaging Notes
|
||||||
|
|
||||||
|
This repository is the customer-delivery backend package. Build delivery images from this repository root.
|
||||||
|
|
||||||
|
## Image Build
|
||||||
|
|
||||||
|
Build the customer backend image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t tjwater-server:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
The `Dockerfile` already performs source encapsulation in the builder stage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/compile.py
|
||||||
|
python scripts/compile.py --delete-source
|
||||||
|
```
|
||||||
|
|
||||||
|
The compiled and source-deleted `app/` directory is then copied into the final runner stage. The source deletion happens inside the Docker builder layer only; it does not delete local workspace files.
|
||||||
|
|
||||||
|
## Encapsulation Scope
|
||||||
|
|
||||||
|
`scripts/compile.py` defaults to compiling these sensitive areas:
|
||||||
|
|
||||||
|
- `app/services`
|
||||||
|
- `app/native/wndb`
|
||||||
|
- `app/algorithms`
|
||||||
|
- `app/infra/epanet/epanet.py`
|
||||||
|
|
||||||
|
These areas should not contain uncompiled `.py` source files in the delivered image. Public entry points, API route wiring, schemas, configuration, and operational files may remain readable when required for runtime.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
After building, verify the image tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker image ls tjwater-server
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify that core source files were removed and compiled extensions exist:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm --entrypoint sh tjwater-server:latest -c "\
|
||||||
|
printf 'core_py_count='; \
|
||||||
|
find /app/app/services /app/app/native/wndb /app/app/algorithms /app/app/infra/epanet/epanet.py -name '*.py' 2>/dev/null | wc -l; \
|
||||||
|
printf 'core_so_count='; \
|
||||||
|
find /app/app/services /app/app/native/wndb /app/app/algorithms /app/app/infra/epanet -name '*.so' 2>/dev/null | wc -l; \
|
||||||
|
python -c 'import app.main; print(\"import_app_main=ok\")'"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected delivery result:
|
||||||
|
|
||||||
|
```text
|
||||||
|
core_py_count=0
|
||||||
|
core_so_count=<non-zero>
|
||||||
|
import_app_main=ok
|
||||||
|
```
|
||||||
|
|
||||||
|
Warnings from third-party packages during import are not necessarily build failures. Treat non-zero exit codes, failed imports, or leftover core `.py` files as blockers.
|
||||||
|
|
||||||
|
## Export For Windows Delivery
|
||||||
|
|
||||||
|
Export the image tarball to the Windows desktop from WSL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker save -o /mnt/c/Users/admin/Desktop/tjwater-server-latest.tar tjwater-server:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Adjust the Windows username if needed. To find available desktop paths:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
find /mnt/c/Users -maxdepth 2 -type d \( -name Desktop -o -name 桌面 \) 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
On the target machine, import the image with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker load -i tjwater-server-latest.tar
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment Caution
|
||||||
|
|
||||||
|
The development `infra/docker/docker-compose.yml` bind-mounts local source:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- ../../app:/app/app
|
||||||
|
- ../../resources:/app/resources
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not use that source mount for customer delivery of the encapsulated image. Mounting `../../app` over `/app/app` replaces the compiled code inside the image with local source files and defeats the encapsulation. For delivery compose files, use the built image directly and mount only required runtime data/config paths.
|
||||||
|
|
||||||
|
## Local Safety
|
||||||
|
|
||||||
|
Do not run this destructive command in the normal working tree:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/compile.py --delete-source
|
||||||
|
```
|
||||||
|
|
||||||
|
Only use `--delete-source` in Docker builder stages or in a disposable delivery copy. The normal delivery image build already handles this safely.
|
||||||
+15
-3
@@ -1,4 +1,4 @@
|
|||||||
FROM condaforge/miniforge3:latest
|
FROM condaforge/miniforge3:latest AS runtime-base
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -14,13 +14,25 @@ COPY requirements.txt .
|
|||||||
RUN pip install --no-cache-dir uv
|
RUN pip install --no-cache-dir uv
|
||||||
RUN uv pip install --system --no-cache-dir -r requirements.txt
|
RUN uv pip install --system --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
FROM runtime-base AS builder
|
||||||
|
|
||||||
|
RUN mamba install -y c-compiler cxx-compiler && \
|
||||||
|
mamba clean -afy
|
||||||
|
|
||||||
|
COPY app ./app
|
||||||
|
COPY scripts ./scripts
|
||||||
|
|
||||||
|
RUN python scripts/compile.py && \
|
||||||
|
python scripts/compile.py --delete-source
|
||||||
|
|
||||||
|
FROM runtime-base AS runner
|
||||||
|
|
||||||
# 将代码放入子目录 'app',将数据放入子目录 'db_inp'
|
# 将代码放入子目录 'app',将数据放入子目录 'db_inp'
|
||||||
# 这样临时文件默认会生成在 /app 下,而代码在 /app/app 下,实现了分离
|
# 这样临时文件默认会生成在 /app 下,而代码在 /app/app 下,实现了分离
|
||||||
COPY app ./app
|
COPY --from=builder /app/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}'" && \
|
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
|
rm -f app/algorithms/health/model/my_survival_forest_model_quxi.zip
|
||||||
# COPY db_inp ./db_inp
|
# COPY db_inp ./db_inp
|
||||||
COPY .env .
|
|
||||||
RUN mkdir -p db_inp temp data inp
|
RUN mkdir -p db_inp temp data inp
|
||||||
|
|
||||||
# 设置 PYTHONPATH 以便 uvicorn 找到 app 模块
|
# 设置 PYTHONPATH 以便 uvicorn 找到 app 模块
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# TJWaterServerCustomer 客户版后端
|
||||||
|
|
||||||
|
`TJWaterServerCustomer` 是 TJWater 客户交付版 Python 后端。该仓库应被视为可部署交付包,只保留客户运行、配置、部署、诊断和交付说明所需内容。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- 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/ 本地管网数据读写与转换,交付镜像中必须封装
|
||||||
|
scripts/compile.py Cython 封装脚本
|
||||||
|
infra/docker/ Docker Compose 编排
|
||||||
|
tests/ 后端测试
|
||||||
|
```
|
||||||
|
|
||||||
|
## 本地开发
|
||||||
|
|
||||||
|
推荐使用已有 conda 环境:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
conda run -n server python -m pytest tests -q
|
||||||
|
conda run -n server python scripts/run_server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
本地调试不要在正常工作树执行源码删除命令。
|
||||||
|
|
||||||
|
## 客户版镜像打包
|
||||||
|
|
||||||
|
交付镜像标签通常为:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t tjwater-server:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
`Dockerfile` 会在 builder 阶段执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/compile.py
|
||||||
|
python scripts/compile.py --delete-source
|
||||||
|
```
|
||||||
|
|
||||||
|
源码删除只发生在 Docker 构建层内,不会删除本地工作树源码。详细封装、验证和 Windows 导出说明见:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DELIVERY_PACKAGING_NOTES.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 封装范围
|
||||||
|
|
||||||
|
`scripts/compile.py` 默认封装:
|
||||||
|
|
||||||
|
- `app/services`
|
||||||
|
- `app/native/wndb`
|
||||||
|
- `app/algorithms`
|
||||||
|
- `app/infra/epanet/epanet.py`
|
||||||
|
|
||||||
|
交付镜像中这些核心目录不应残留未编译的 `.py` 源码,应以 `.so` 扩展模块运行。
|
||||||
|
|
||||||
|
## 验证命令
|
||||||
|
|
||||||
|
构建后检查镜像:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker image ls tjwater-server
|
||||||
|
```
|
||||||
|
|
||||||
|
检查核心源码是否已删除、FastAPI 入口是否可导入:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm --entrypoint sh tjwater-server:latest -c "\
|
||||||
|
printf 'core_py_count='; \
|
||||||
|
find /app/app/services /app/app/native/wndb /app/app/algorithms /app/app/infra/epanet/epanet.py -name '*.py' 2>/dev/null | wc -l; \
|
||||||
|
printf 'core_so_count='; \
|
||||||
|
find /app/app/services /app/app/native/wndb /app/app/algorithms /app/app/infra/epanet -name '*.so' 2>/dev/null | wc -l; \
|
||||||
|
python -c 'import app.main; print(\"import_app_main=ok\")'"
|
||||||
|
```
|
||||||
|
|
||||||
|
期望结果:
|
||||||
|
|
||||||
|
```text
|
||||||
|
core_py_count=0
|
||||||
|
core_so_count=<非零>
|
||||||
|
import_app_main=ok
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署注意
|
||||||
|
|
||||||
|
客户交付时不要把本地 `../../app` 挂载到容器 `/app/app`,否则会覆盖镜像内已封装代码。交付 compose 文件应使用构建好的镜像,只挂载必要的运行数据和配置。
|
||||||
|
|
||||||
|
## 安全规则
|
||||||
|
|
||||||
|
不要提交 `.env`、生产凭据、客户数据、数据库 dump、日志、生成缓存、临时交付压缩包或本地运行目录。客户版仓库不应加入内部实验、调试工具或非交付源码材料。
|
||||||
@@ -72,6 +72,7 @@ def burst_analysis(
|
|||||||
modify_variable_pump_pattern: dict[str, list] = None,
|
modify_variable_pump_pattern: dict[str, list] = None,
|
||||||
modify_valve_opening: dict[str, float] = None,
|
modify_valve_opening: dict[str, float] = None,
|
||||||
scheme_name: str = None,
|
scheme_name: str = None,
|
||||||
|
username: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
爆管模拟
|
爆管模拟
|
||||||
@@ -86,6 +87,9 @@ def burst_analysis(
|
|||||||
:param scheme_name: 方案名称
|
:param scheme_name: 方案名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
if not username:
|
||||||
|
raise ValueError("username is required when storing burst analysis scheme")
|
||||||
|
|
||||||
scheme_detail: dict = {
|
scheme_detail: dict = {
|
||||||
"burst_ID": burst_ID,
|
"burst_ID": burst_ID,
|
||||||
"burst_size": burst_size,
|
"burst_size": burst_size,
|
||||||
@@ -211,7 +215,7 @@ def burst_analysis(
|
|||||||
name=name,
|
name=name,
|
||||||
scheme_name=scheme_name,
|
scheme_name=scheme_name,
|
||||||
scheme_type="burst_analysis",
|
scheme_type="burst_analysis",
|
||||||
username="admin",
|
username=username,
|
||||||
scheme_start_time=modify_pattern_start_time,
|
scheme_start_time=modify_pattern_start_time,
|
||||||
scheme_detail=scheme_detail,
|
scheme_detail=scheme_detail,
|
||||||
)
|
)
|
||||||
@@ -311,6 +315,7 @@ def flushing_analysis(
|
|||||||
drainage_node_ID: str = None,
|
drainage_node_ID: str = None,
|
||||||
flushing_flow: float = 0,
|
flushing_flow: float = 0,
|
||||||
scheme_name: str = None,
|
scheme_name: str = None,
|
||||||
|
username: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
管道冲洗模拟
|
管道冲洗模拟
|
||||||
@@ -323,6 +328,9 @@ def flushing_analysis(
|
|||||||
:param scheme_name: 方案名称
|
:param scheme_name: 方案名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
if not username:
|
||||||
|
raise ValueError("username is required when storing flushing analysis scheme")
|
||||||
|
|
||||||
scheme_detail: dict = {
|
scheme_detail: dict = {
|
||||||
"duration": modify_total_duration,
|
"duration": modify_total_duration,
|
||||||
"valve_opening": modify_valve_opening,
|
"valve_opening": modify_valve_opening,
|
||||||
@@ -455,7 +463,7 @@ def flushing_analysis(
|
|||||||
name=name,
|
name=name,
|
||||||
scheme_name=scheme_name,
|
scheme_name=scheme_name,
|
||||||
scheme_type="flushing_analysis",
|
scheme_type="flushing_analysis",
|
||||||
username="admin",
|
username=username,
|
||||||
scheme_start_time=modify_pattern_start_time,
|
scheme_start_time=modify_pattern_start_time,
|
||||||
scheme_detail=scheme_detail,
|
scheme_detail=scheme_detail,
|
||||||
)
|
)
|
||||||
@@ -473,6 +481,7 @@ def contaminant_simulation(
|
|||||||
concentration: float, # 污染源浓度,单位mg/L
|
concentration: float, # 污染源浓度,单位mg/L
|
||||||
scheme_name: str = None,
|
scheme_name: str = None,
|
||||||
source_pattern: str = None, # 污染源时间变化模式名称
|
source_pattern: str = None, # 污染源时间变化模式名称
|
||||||
|
username: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
污染模拟
|
污染模拟
|
||||||
@@ -486,6 +495,9 @@ def contaminant_simulation(
|
|||||||
:param scheme_name: 方案名称
|
:param scheme_name: 方案名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
if not username:
|
||||||
|
raise ValueError("username is required when storing contaminant analysis scheme")
|
||||||
|
|
||||||
scheme_detail: dict = {
|
scheme_detail: dict = {
|
||||||
"source": source,
|
"source": source,
|
||||||
"concentration": concentration,
|
"concentration": concentration,
|
||||||
@@ -608,7 +620,7 @@ def contaminant_simulation(
|
|||||||
name=name,
|
name=name,
|
||||||
scheme_name=scheme_name,
|
scheme_name=scheme_name,
|
||||||
scheme_type="contaminant_analysis",
|
scheme_type="contaminant_analysis",
|
||||||
username="admin",
|
username=username,
|
||||||
scheme_start_time=modify_pattern_start_time,
|
scheme_start_time=modify_pattern_start_time,
|
||||||
scheme_detail=scheme_detail,
|
scheme_detail=scheme_detail,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -30,16 +30,6 @@ class BurstDetectionRequest(BaseModel):
|
|||||||
points_per_day: int = Field(1440, description="每天的数据点数")
|
points_per_day: int = Field(1440, description="每天的数据点数")
|
||||||
mu: int = Field(100, description="异常值检测的参数")
|
mu: int = Field(100, description="异常值检测的参数")
|
||||||
iforest_params: dict[str, Any] | None = Field(None, description="隔离森林算法参数")
|
iforest_params: dict[str, Any] | None = Field(None, description="隔离森林算法参数")
|
||||||
target_time: datetime | None = Field(
|
|
||||||
None,
|
|
||||||
description="目标侦测时刻;为空时自动使用最近一个完整的监测时刻",
|
|
||||||
)
|
|
||||||
sampling_interval_minutes: int | None = Field(
|
|
||||||
None,
|
|
||||||
ge=1,
|
|
||||||
le=1440,
|
|
||||||
description="采样间隔(分钟);为空时根据压力 SCADA 传输频率自动推断",
|
|
||||||
)
|
|
||||||
scada_start: datetime | None = Field(None, description="SCADA数据起始时间")
|
scada_start: datetime | None = Field(None, description="SCADA数据起始时间")
|
||||||
scada_end: datetime | None = Field(None, description="SCADA数据结束时间")
|
scada_end: datetime | None = Field(None, description="SCADA数据结束时间")
|
||||||
sensor_nodes: list[str] | None = Field(None, description="传感器节点列表")
|
sensor_nodes: list[str] | None = Field(None, description="传感器节点列表")
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ async def get_project_metadata(
|
|||||||
"""
|
"""
|
||||||
获取项目元数据
|
获取项目元数据
|
||||||
|
|
||||||
返回当前项目的完整元数据,包括项目基本信息和项目权限
|
返回当前项目的完整元数据
|
||||||
"""
|
"""
|
||||||
project = await metadata_repo.get_project_by_id(ctx.project_id)
|
project = await metadata_repo.get_project_by_id(ctx.project_id)
|
||||||
if not project:
|
if not project:
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ async def get_project_info_endpoint(
|
|||||||
project_detail = await metadata_repo.get_project_detail_by_code(network)
|
project_detail = await metadata_repo.get_project_detail_by_code(network)
|
||||||
if not project_detail:
|
if not project_detail:
|
||||||
raise HTTPException(status_code=404, detail=f"Project {network} not found")
|
raise HTTPException(status_code=404, detail=f"Project {network} not found")
|
||||||
|
|
||||||
return ProjectMetaResponse(
|
return ProjectMetaResponse(
|
||||||
project_id=project_detail.project_id,
|
project_id=project_detail.project_id,
|
||||||
name=project_detail.name,
|
name=project_detail.name,
|
||||||
@@ -64,7 +65,7 @@ async def get_project_info_endpoint(
|
|||||||
gs_workspace=project_detail.gs_workspace,
|
gs_workspace=project_detail.gs_workspace,
|
||||||
map_extent=project_detail.map_extent,
|
map_extent=project_detail.map_extent,
|
||||||
status=project_detail.status,
|
status=project_detail.status,
|
||||||
project_role="viewer", # Default role for public access
|
project_role="viewer",
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.get("/listprojects/", summary="获取项目列表", description="获取服务器上所有可用的供水管网项目名称列表。")
|
@router.get("/listprojects/", summary="获取项目列表", description="获取服务器上所有可用的供水管网项目名称列表。")
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import json
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import threading
|
import threading
|
||||||
from fastapi import APIRouter, HTTPException, File, UploadFile, Query, Path, Body
|
from fastapi import APIRouter, Depends, HTTPException, File, UploadFile, Query, Path, Body
|
||||||
from fastapi.responses import PlainTextResponse
|
from fastapi.responses import PlainTextResponse
|
||||||
|
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
||||||
import app.services.simulation as simulation
|
import app.services.simulation as simulation
|
||||||
import app.services.globals as globals
|
import app.services.globals as globals
|
||||||
from app.services.tjnetwork import (
|
from app.services.tjnetwork import (
|
||||||
@@ -209,6 +210,7 @@ async def fastapi_burst_analysis(
|
|||||||
burst_size: list[float] = Query(..., description="对应各爆管点的爆管流量大小列表(L/s)"),
|
burst_size: list[float] = Query(..., description="对应各爆管点的爆管流量大小列表(L/s)"),
|
||||||
modify_total_duration: int = Query(..., description="模拟总时长(秒)"),
|
modify_total_duration: int = Query(..., description="模拟总时长(秒)"),
|
||||||
scheme_name: str = Query(..., description="分析方案名称"),
|
scheme_name: str = Query(..., description="分析方案名称"),
|
||||||
|
username: str = Depends(get_current_keycloak_username),
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
爆管分析(高级版本)
|
爆管分析(高级版本)
|
||||||
@@ -229,6 +231,7 @@ async def fastapi_burst_analysis(
|
|||||||
burst_size=burst_size,
|
burst_size=burst_size,
|
||||||
modify_total_duration=modify_total_duration,
|
modify_total_duration=modify_total_duration,
|
||||||
scheme_name=scheme_name,
|
scheme_name=scheme_name,
|
||||||
|
username=username,
|
||||||
)
|
)
|
||||||
return "success"
|
return "success"
|
||||||
|
|
||||||
@@ -314,6 +317,7 @@ async def fastapi_flushing_analysis(
|
|||||||
flush_flow: float = Query(0, description="冲洗流量(L/s),0表示自动计算"),
|
flush_flow: float = Query(0, description="冲洗流量(L/s),0表示自动计算"),
|
||||||
duration: int | None = Query(None, description="模拟持续时间(秒),默认900秒"),
|
duration: int | None = Query(None, description="模拟持续时间(秒),默认900秒"),
|
||||||
scheme_name: str = Query(..., description="冲洗方案名称"),
|
scheme_name: str = Query(..., description="冲洗方案名称"),
|
||||||
|
username: str = Depends(get_current_keycloak_username),
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
冲洗分析(高级版本)
|
冲洗分析(高级版本)
|
||||||
@@ -340,6 +344,7 @@ async def fastapi_flushing_analysis(
|
|||||||
drainage_node_ID=drainage_node_ID,
|
drainage_node_ID=drainage_node_ID,
|
||||||
flushing_flow=flush_flow,
|
flushing_flow=flush_flow,
|
||||||
scheme_name=scheme_name,
|
scheme_name=scheme_name,
|
||||||
|
username=username,
|
||||||
)
|
)
|
||||||
return result or "success"
|
return result or "success"
|
||||||
|
|
||||||
@@ -354,6 +359,7 @@ async def fastapi_contaminant_simulation(
|
|||||||
duration: int = Query(..., description="模拟持续时间(秒)"),
|
duration: int = Query(..., description="模拟持续时间(秒)"),
|
||||||
scheme_name: str = Query(..., description="模拟方案名称"),
|
scheme_name: str = Query(..., description="模拟方案名称"),
|
||||||
pattern: str | None = Query(None, description="污染源模式ID(可选)"),
|
pattern: str | None = Query(None, description="污染源模式ID(可选)"),
|
||||||
|
username: str = Depends(get_current_keycloak_username),
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
污染物模拟
|
污染物模拟
|
||||||
@@ -376,6 +382,7 @@ async def fastapi_contaminant_simulation(
|
|||||||
source=source,
|
source=source,
|
||||||
concentration=concentration,
|
concentration=concentration,
|
||||||
source_pattern=pattern,
|
source_pattern=pattern,
|
||||||
|
username=username,
|
||||||
)
|
)
|
||||||
return result or "success"
|
return result or "success"
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from app.api.v1.endpoints import (
|
|||||||
scada,
|
scada,
|
||||||
extension,
|
extension,
|
||||||
snapshots,
|
snapshots,
|
||||||
# data_query,
|
|
||||||
users,
|
users,
|
||||||
schemes,
|
schemes,
|
||||||
misc,
|
misc,
|
||||||
@@ -87,7 +86,6 @@ api_router.include_router(visuals.router, tags=["Visuals"])
|
|||||||
|
|
||||||
# Simulation & Data
|
# Simulation & Data
|
||||||
api_router.include_router(simulation.router, tags=["Simulation Control"])
|
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(scada.router)
|
||||||
api_router.include_router(snapshots.router, tags=["Snapshots"])
|
api_router.include_router(snapshots.router, tags=["Snapshots"])
|
||||||
api_router.include_router(users.router, tags=["Users"])
|
api_router.include_router(users.router, tags=["Users"])
|
||||||
|
|||||||
@@ -27,11 +27,6 @@ class Settings(BaseSettings):
|
|||||||
TIMESCALEDB_DB_PORT: str = "5433"
|
TIMESCALEDB_DB_PORT: str = "5433"
|
||||||
TIMESCALEDB_DB_USER: str = "postgres"
|
TIMESCALEDB_DB_USER: str = "postgres"
|
||||||
TIMESCALEDB_DB_PASSWORD: str = "password"
|
TIMESCALEDB_DB_PASSWORD: str = "password"
|
||||||
# InfluxDB
|
|
||||||
INFLUXDB_URL: str = "http://localhost:8086"
|
|
||||||
INFLUXDB_TOKEN: str = "token"
|
|
||||||
INFLUXDB_ORG: str = "org"
|
|
||||||
INFLUXDB_BUCKET: str = "bucket"
|
|
||||||
|
|
||||||
# Metadata Database Config (PostgreSQL)
|
# Metadata Database Config (PostgreSQL)
|
||||||
METADATA_DB_NAME: str = "system_hub"
|
METADATA_DB_NAME: str = "system_hub"
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
This module is reserved for future implementation of advanced cryptographic operations.
|
This module is reserved for future implementation of advanced cryptographic operations.
|
||||||
|
|
||||||
Current basic encryption (Fernet) and password hashing are implemented in `app.core.encryption` and `app.core.security`.
|
Current Fernet encryption helpers are implemented in `app.core.encryption`.
|
||||||
|
Login credentials are owned by Keycloak; this backend does not hash or store
|
||||||
|
local passwords.
|
||||||
Future expansion may include:
|
Future expansion may include:
|
||||||
- Asymmetric encryption (RSA/ECC) for secure communication
|
- Asymmetric encryption (RSA/ECC) for secure communication
|
||||||
- Key management and rotation services
|
- Key management and rotation services
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
|||||||
# influxdb数据库连接信息
|
|
||||||
url = "http://127.0.0.1:8086" # 替换为你的InfluxDB实例地址
|
|
||||||
token = "kMPX2V5HsbzPpUT2B9HPBu1sTG1Emf-lPlT2UjxYnGAuocpXq_f_0lK4HHs-TbbKyjsZpICkMsyXG_V2D7P7yQ==" # 替换为你的InfluxDB Token
|
|
||||||
# _ENCODED_TOKEN = "eEdETTVSWnFSSkF1ekFHUy1vdFhVZEMyTkZkWTc1cUpBalJMcUFCNHA1V2NJSUFsSVVwT3BUOF95QTE2QU9IbUpXZXJ3UV8wOGd3Yjg0c3k0MmpuWlE9PQ=="
|
|
||||||
# token = base64.b64decode(_ENCODED_TOKEN).decode("utf-8")
|
|
||||||
org = "TJWATERORG" # 替换为你的Organization名称
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
from influxdb_client import InfluxDBClient, Point, WriteOptions
|
|
||||||
from influxdb_client.client.query_api import QueryApi
|
|
||||||
import influxdb_info
|
|
||||||
|
|
||||||
# 配置 InfluxDB 连接
|
|
||||||
url = influxdb_info.url
|
|
||||||
token = influxdb_info.token
|
|
||||||
org = influxdb_info.org
|
|
||||||
bucket = "SCADA_data"
|
|
||||||
|
|
||||||
# 创建 InfluxDB 客户端
|
|
||||||
client = InfluxDBClient(url=url, token=token, org=org)
|
|
||||||
|
|
||||||
# 创建查询 API 对象
|
|
||||||
query_api = client.query_api()
|
|
||||||
|
|
||||||
# 构建查询语句
|
|
||||||
query = f'''
|
|
||||||
from(bucket: "{bucket}")
|
|
||||||
|> range(start: -1h)
|
|
||||||
'''
|
|
||||||
|
|
||||||
# 执行查询
|
|
||||||
result = query_api.query(query)
|
|
||||||
print(result)
|
|
||||||
|
|
||||||
# 处理查询结果
|
|
||||||
for table in result:
|
|
||||||
for record in table.records:
|
|
||||||
print(f"Time: {record.get_time()}, Value: {record.get_value()}, Measurement: {record.get_measurement()}, Field: {record.get_field()}")
|
|
||||||
|
|
||||||
# 关闭客户端连接
|
|
||||||
client.close()
|
|
||||||
@@ -169,38 +169,6 @@ class InternalQueries:
|
|||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def query_latest_scada_time(
|
|
||||||
device_ids: List[str],
|
|
||||||
before_time: str | datetime | None = None,
|
|
||||||
db_name: str = None,
|
|
||||||
max_retries: int = 3,
|
|
||||||
) -> datetime | None:
|
|
||||||
"""Return the latest SCADA timestamp for the selected devices."""
|
|
||||||
before_dt = (
|
|
||||||
parse_utc_time(before_time, field_name="before_time")
|
|
||||||
if before_time is not None
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
for attempt in range(max_retries):
|
|
||||||
try:
|
|
||||||
conn_string = (
|
|
||||||
get_timescaledb_pgconn_string(db_name=db_name)
|
|
||||||
if db_name
|
|
||||||
else get_timescaledb_pgconn_string()
|
|
||||||
)
|
|
||||||
with psycopg.Connection.connect(conn_string) as conn:
|
|
||||||
return ScadaRepository.get_latest_scada_time_sync(
|
|
||||||
conn,
|
|
||||||
device_ids,
|
|
||||||
before_dt,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
if attempt < max_retries - 1:
|
|
||||||
time.sleep(1)
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def query_realtime_simulation_by_ids_timerange(
|
def query_realtime_simulation_by_ids_timerange(
|
||||||
element_ids: List[str],
|
element_ids: List[str],
|
||||||
|
|||||||
@@ -54,27 +54,6 @@ class ScadaRepository:
|
|||||||
)
|
)
|
||||||
return cur.fetchall()
|
return cur.fetchall()
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_latest_scada_time_sync(
|
|
||||||
conn: Connection,
|
|
||||||
device_ids: List[str],
|
|
||||||
before_time: datetime | None = None,
|
|
||||||
) -> datetime | None:
|
|
||||||
with conn.cursor(row_factory=dict_row) as cur:
|
|
||||||
if before_time is None:
|
|
||||||
cur.execute(
|
|
||||||
"SELECT max(time) AS time FROM scada.scada_data WHERE device_id = ANY(%s)",
|
|
||||||
(device_ids,),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
cur.execute(
|
|
||||||
"SELECT max(time) AS time FROM scada.scada_data "
|
|
||||||
"WHERE device_id = ANY(%s) AND time <= %s",
|
|
||||||
(device_ids, before_time),
|
|
||||||
)
|
|
||||||
row = cur.fetchone()
|
|
||||||
return row["time"] if row else None
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_scada_field_by_id_time_range(
|
async def get_scada_field_by_id_time_range(
|
||||||
conn: AsyncConnection,
|
conn: AsyncConnection,
|
||||||
|
|||||||
@@ -20,6 +20,17 @@ def _close_connection(connection: pg.Connection) -> None:
|
|||||||
connection.close()
|
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:
|
def _get_project_lock(name: str) -> RLock:
|
||||||
with _registry_lock:
|
with _registry_lock:
|
||||||
lock = _project_locks.get(name)
|
lock = _project_locks.get(name)
|
||||||
@@ -32,7 +43,7 @@ def _get_project_lock(name: str) -> RLock:
|
|||||||
def open_connection(name: str) -> pg.Connection:
|
def open_connection(name: str) -> pg.Connection:
|
||||||
with _get_project_lock(name):
|
with _get_project_lock(name):
|
||||||
connection = g_conn_dict.get(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:
|
if connection is not None:
|
||||||
_close_connection(connection)
|
_close_connection(connection)
|
||||||
connection = pg.connect(
|
connection = pg.connect(
|
||||||
@@ -47,8 +58,9 @@ def is_connection_open(name: str) -> bool:
|
|||||||
connection = g_conn_dict.get(name)
|
connection = g_conn_dict.get(name)
|
||||||
if connection is None:
|
if connection is None:
|
||||||
return False
|
return False
|
||||||
if _is_closed(connection):
|
if not _is_healthy(connection):
|
||||||
del g_conn_dict[name]
|
del g_conn_dict[name]
|
||||||
|
_close_connection(connection)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
+29
-363
@@ -1,10 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import Counter
|
from datetime import datetime
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from app.algorithms.burst_detection.burst_detector import BurstDetector
|
from app.algorithms.burst_detection.burst_detector import BurstDetector
|
||||||
@@ -19,15 +17,6 @@ from app.services.tjnetwork import get_all_scada_info
|
|||||||
from app.services.time_api import extract_date, parse_utc_time, utc_now
|
from app.services.time_api import extract_date, parse_utc_time, utc_now
|
||||||
|
|
||||||
|
|
||||||
TARGET_DAY_COUNT = 15
|
|
||||||
DEFAULT_SAMPLE_INTERVAL_MINUTES = 15
|
|
||||||
TARGET_MU = 1
|
|
||||||
TARGET_N_ESTIMATORS = 50
|
|
||||||
TARGET_RANDOM_STATE = 42
|
|
||||||
TARGET_SCORE_THRESHOLD = -0.04
|
|
||||||
MIN_COMPLETE_SENSORS = 5
|
|
||||||
|
|
||||||
|
|
||||||
def run_burst_detection(
|
def run_burst_detection(
|
||||||
*,
|
*,
|
||||||
network: str,
|
network: str,
|
||||||
@@ -42,8 +31,6 @@ def run_burst_detection(
|
|||||||
points_per_day: int = 1440,
|
points_per_day: int = 1440,
|
||||||
mu: int = 100,
|
mu: int = 100,
|
||||||
iforest_params: dict[str, Any] | None = None,
|
iforest_params: dict[str, Any] | None = None,
|
||||||
target_time: datetime | str | None = None,
|
|
||||||
sampling_interval_minutes: int | None = None,
|
|
||||||
scada_start: datetime | str | None = None,
|
scada_start: datetime | str | None = None,
|
||||||
scada_end: datetime | str | None = None,
|
scada_end: datetime | str | None = None,
|
||||||
sensor_nodes: list[str] | None = None,
|
sensor_nodes: list[str] | None = None,
|
||||||
@@ -55,8 +42,7 @@ def run_burst_detection(
|
|||||||
"""
|
"""
|
||||||
运行爆管侦测服务入口。
|
运行爆管侦测服务入口。
|
||||||
|
|
||||||
调用方式三选一:
|
调用方式二选一:
|
||||||
- 不传数据时间窗,自动侦测最近完整时刻;可用 `target_time` 回放历史时刻
|
|
||||||
- 直接传 `observed_pressure_data`
|
- 直接传 `observed_pressure_data`
|
||||||
- 或传 `scada_start/scada_end` 让后端自动查询 SCADA 压力数据
|
- 或传 `scada_start/scada_end` 让后端自动查询 SCADA 压力数据
|
||||||
|
|
||||||
@@ -88,65 +74,8 @@ def run_burst_detection(
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
use_scada_source = scada_start is not None or scada_end is not None
|
use_scada_source = scada_start is not None or scada_end is not None
|
||||||
use_target_mode = (
|
|
||||||
observed_pressure_data is None
|
|
||||||
and not use_scada_source
|
|
||||||
and data_source != "simulation"
|
|
||||||
) or target_time is not None
|
|
||||||
|
|
||||||
resolved_target_time: datetime | None = None
|
if use_scada_source:
|
||||||
requested_target_time: datetime | None = None
|
|
||||||
excluded_sensors: list[dict[str, str]] = []
|
|
||||||
daily_times: list[datetime] | None = None
|
|
||||||
resolved_sampling_interval_minutes: int | None = None
|
|
||||||
|
|
||||||
if use_target_mode:
|
|
||||||
if observed_pressure_data is not None or use_scada_source:
|
|
||||||
raise ValueError(
|
|
||||||
"target_time 不能与 observed_pressure_data 或 scada_start/scada_end 同时使用。"
|
|
||||||
)
|
|
||||||
scada_sensor_nodes = (
|
|
||||||
selected_sensor_nodes
|
|
||||||
if selected_sensor_nodes is not None
|
|
||||||
else _get_pressure_sensor_nodes(network)
|
|
||||||
)
|
|
||||||
requested_target_time = (
|
|
||||||
_to_datetime(target_time) if target_time is not None else None
|
|
||||||
)
|
|
||||||
resolved_sampling_interval_minutes = _resolve_sampling_interval_minutes(
|
|
||||||
network=network,
|
|
||||||
sensor_nodes=scada_sensor_nodes,
|
|
||||||
requested_interval=sampling_interval_minutes,
|
|
||||||
)
|
|
||||||
target_points_per_day = 1440 // resolved_sampling_interval_minutes
|
|
||||||
(
|
|
||||||
observed_input,
|
|
||||||
resolved_target_time,
|
|
||||||
excluded_sensors,
|
|
||||||
) = _build_target_pressure_from_scada(
|
|
||||||
network=network,
|
|
||||||
sensor_nodes=scada_sensor_nodes,
|
|
||||||
requested_target_time=requested_target_time,
|
|
||||||
sampling_interval_minutes=resolved_sampling_interval_minutes,
|
|
||||||
points_per_day=target_points_per_day,
|
|
||||||
)
|
|
||||||
selected_sensor_nodes = list(observed_input.columns)
|
|
||||||
observed_source = (
|
|
||||||
"latest_monitoring" if target_time is None else "historical_monitoring"
|
|
||||||
)
|
|
||||||
points_per_day = target_points_per_day
|
|
||||||
mu = TARGET_MU
|
|
||||||
iforest_params = {
|
|
||||||
"n_estimators": TARGET_N_ESTIMATORS,
|
|
||||||
"random_state": TARGET_RANDOM_STATE,
|
|
||||||
"contamination": "auto",
|
|
||||||
}
|
|
||||||
daily_times = [
|
|
||||||
resolved_target_time - timedelta(days=offset)
|
|
||||||
for offset in range(TARGET_DAY_COUNT - 1, -1, -1)
|
|
||||||
]
|
|
||||||
|
|
||||||
elif use_scada_source:
|
|
||||||
scada_sensor_nodes = (
|
scada_sensor_nodes = (
|
||||||
selected_sensor_nodes
|
selected_sensor_nodes
|
||||||
if selected_sensor_nodes is not None
|
if selected_sensor_nodes is not None
|
||||||
@@ -192,16 +121,7 @@ def run_burst_detection(
|
|||||||
sensor_nodes=selected_sensor_nodes,
|
sensor_nodes=selected_sensor_nodes,
|
||||||
)
|
)
|
||||||
resolved_sensor_nodes = list(result_df.attrs.get("sensor_nodes", []))
|
resolved_sensor_nodes = list(result_df.attrs.get("sensor_nodes", []))
|
||||||
rows = _serialize_result_rows(
|
rows = _serialize_result_rows(result_df)
|
||||||
result_df,
|
|
||||||
daily_times=daily_times,
|
|
||||||
target_only=use_target_mode,
|
|
||||||
)
|
|
||||||
summary = _build_detection_summary(
|
|
||||||
result_df,
|
|
||||||
daily_times=daily_times,
|
|
||||||
target_only=use_target_mode,
|
|
||||||
)
|
|
||||||
payload: dict[str, Any] = {
|
payload: dict[str, Any] = {
|
||||||
"network": network,
|
"network": network,
|
||||||
"sensor_nodes": resolved_sensor_nodes,
|
"sensor_nodes": resolved_sensor_nodes,
|
||||||
@@ -210,17 +130,7 @@ def run_burst_detection(
|
|||||||
"points_per_day": int(result_df.attrs.get("points_per_day", points_per_day)),
|
"points_per_day": int(result_df.attrs.get("points_per_day", points_per_day)),
|
||||||
"day_count": int(result_df.attrs.get("day_count", len(result_df))),
|
"day_count": int(result_df.attrs.get("day_count", len(result_df))),
|
||||||
"rows": rows,
|
"rows": rows,
|
||||||
"summary": summary,
|
"summary": _build_detection_summary(result_df),
|
||||||
"algorithm_params": {
|
|
||||||
"mu": mu,
|
|
||||||
"points_per_day": points_per_day,
|
|
||||||
"iforest_params": detector.iforest_params,
|
|
||||||
**(
|
|
||||||
{"score_threshold": TARGET_SCORE_THRESHOLD}
|
|
||||||
if use_target_mode
|
|
||||||
else {}
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
if data_source == "simulation":
|
if data_source == "simulation":
|
||||||
payload["data_source"] = "simulation"
|
payload["data_source"] = "simulation"
|
||||||
@@ -231,50 +141,7 @@ def run_burst_detection(
|
|||||||
else:
|
else:
|
||||||
payload["data_source"] = "monitoring"
|
payload["data_source"] = "monitoring"
|
||||||
|
|
||||||
if (
|
if use_scada_source:
|
||||||
use_target_mode
|
|
||||||
and resolved_target_time is not None
|
|
||||||
and resolved_sampling_interval_minutes is not None
|
|
||||||
):
|
|
||||||
sample_start = resolved_target_time - timedelta(
|
|
||||||
days=TARGET_DAY_COUNT,
|
|
||||||
minutes=-resolved_sampling_interval_minutes,
|
|
||||||
)
|
|
||||||
payload.update(
|
|
||||||
{
|
|
||||||
"requested_target_time": (
|
|
||||||
requested_target_time.isoformat()
|
|
||||||
if requested_target_time is not None
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
"target_time": resolved_target_time.isoformat(),
|
|
||||||
"reference_window": {
|
|
||||||
"start": (resolved_target_time - timedelta(days=14)).isoformat(),
|
|
||||||
"end": (resolved_target_time - timedelta(days=1)).isoformat(),
|
|
||||||
"day_count": 14,
|
|
||||||
},
|
|
||||||
"sampling_interval_minutes": resolved_sampling_interval_minutes,
|
|
||||||
"daily_scores": [
|
|
||||||
{
|
|
||||||
"timestamp": row["Timestamp"],
|
|
||||||
"role": row["Role"],
|
|
||||||
"score": row["Score"],
|
|
||||||
"raw_prediction": row["Prediction"],
|
|
||||||
}
|
|
||||||
for row in rows
|
|
||||||
],
|
|
||||||
"data_quality": {
|
|
||||||
"included_sensors": resolved_sensor_nodes,
|
|
||||||
"excluded_sensors": excluded_sensors,
|
|
||||||
"minimum_required_sensors": MIN_COMPLETE_SENSORS,
|
|
||||||
},
|
|
||||||
"scada_window": {
|
|
||||||
"start": sample_start.isoformat(),
|
|
||||||
"end": resolved_target_time.isoformat(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
elif use_scada_source:
|
|
||||||
payload["scada_window"] = {
|
payload["scada_window"] = {
|
||||||
"start": _to_datetime(scada_start).isoformat(),
|
"start": _to_datetime(scada_start).isoformat(),
|
||||||
"end": _to_datetime(scada_end).isoformat(),
|
"end": _to_datetime(scada_end).isoformat(),
|
||||||
@@ -427,49 +294,22 @@ def _store_burst_detection_scheme(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _serialize_result_rows(
|
def _serialize_result_rows(result_df: pd.DataFrame) -> list[dict[str, Any]]:
|
||||||
result_df: pd.DataFrame,
|
|
||||||
*,
|
|
||||||
daily_times: list[datetime] | None = None,
|
|
||||||
target_only: bool = False,
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
rows: list[dict[str, Any]] = []
|
rows: list[dict[str, Any]] = []
|
||||||
raw_rows = result_df.to_dict(orient="records")
|
for row in result_df.to_dict(orient="records"):
|
||||||
for index, row in enumerate(raw_rows):
|
|
||||||
is_target = index == len(raw_rows) - 1
|
|
||||||
is_burst = bool(row["IsBurst"])
|
|
||||||
if target_only:
|
|
||||||
is_burst = is_target and float(row["Score"]) <= TARGET_SCORE_THRESHOLD
|
|
||||||
rows.append(
|
rows.append(
|
||||||
{
|
{
|
||||||
"Day": int(row["Day"]),
|
"Day": int(row["Day"]),
|
||||||
"Score": float(row["Score"]),
|
"Score": float(row["Score"]),
|
||||||
"Prediction": int(row["Prediction"]),
|
"Prediction": int(row["Prediction"]),
|
||||||
"IsBurst": is_burst,
|
"IsBurst": bool(row["IsBurst"]),
|
||||||
**(
|
|
||||||
{
|
|
||||||
"Timestamp": daily_times[index].isoformat(),
|
|
||||||
"Role": "target" if is_target else "reference",
|
|
||||||
}
|
|
||||||
if daily_times is not None
|
|
||||||
else {}
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
def _build_detection_summary(
|
def _build_detection_summary(result_df: pd.DataFrame) -> dict[str, Any]:
|
||||||
result_df: pd.DataFrame,
|
rows = _serialize_result_rows(result_df)
|
||||||
*,
|
|
||||||
daily_times: list[datetime] | None = None,
|
|
||||||
target_only: bool = False,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
rows = _serialize_result_rows(
|
|
||||||
result_df,
|
|
||||||
daily_times=daily_times,
|
|
||||||
target_only=target_only,
|
|
||||||
)
|
|
||||||
if not rows:
|
if not rows:
|
||||||
raise ValueError("爆管侦测结果为空。")
|
raise ValueError("爆管侦测结果为空。")
|
||||||
|
|
||||||
@@ -478,7 +318,7 @@ def _build_detection_summary(
|
|||||||
latest_row = rows[-1]
|
latest_row = rows[-1]
|
||||||
anomaly_days = [row["Day"] for row in rows if row["IsBurst"]]
|
anomaly_days = [row["Day"] for row in rows if row["IsBurst"]]
|
||||||
|
|
||||||
summary = {
|
return {
|
||||||
"burst_detected": bool(latest_row["IsBurst"]),
|
"burst_detected": bool(latest_row["IsBurst"]),
|
||||||
"latest_day": latest_row,
|
"latest_day": latest_row,
|
||||||
"most_anomalous_day": int(result_df.iloc[most_anomalous_index]["Day"]),
|
"most_anomalous_day": int(result_df.iloc[most_anomalous_index]["Day"]),
|
||||||
@@ -486,18 +326,6 @@ def _build_detection_summary(
|
|||||||
"anomaly_day_count": len(anomaly_days),
|
"anomaly_day_count": len(anomaly_days),
|
||||||
"latest_sensor_rankings": _build_latest_sensor_rankings(result_df),
|
"latest_sensor_rankings": _build_latest_sensor_rankings(result_df),
|
||||||
}
|
}
|
||||||
if target_only:
|
|
||||||
target_score = float(latest_row["Score"])
|
|
||||||
summary.update(
|
|
||||||
{
|
|
||||||
"target_score": target_score,
|
|
||||||
"score_threshold": TARGET_SCORE_THRESHOLD,
|
|
||||||
"target_rank": int(result_df["Score"].rank(method="min").iloc[-1]),
|
|
||||||
"target_time": latest_row.get("Timestamp"),
|
|
||||||
"reference_day_count": TARGET_DAY_COUNT - 1,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return summary
|
|
||||||
|
|
||||||
|
|
||||||
def _build_latest_sensor_rankings(result_df: pd.DataFrame) -> list[dict[str, Any]]:
|
def _build_latest_sensor_rankings(result_df: pd.DataFrame) -> list[dict[str, Any]]:
|
||||||
@@ -506,192 +334,18 @@ def _build_latest_sensor_rankings(result_df: pd.DataFrame) -> list[dict[str, Any
|
|||||||
if feature_matrix is None or len(sensor_nodes) == 0:
|
if feature_matrix is None or len(sensor_nodes) == 0:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
latest_values = np.asarray(feature_matrix[-1], dtype=float)
|
latest_values = feature_matrix[-1]
|
||||||
history = np.asarray(feature_matrix[:-1], dtype=float)
|
|
||||||
history_means = history.mean(axis=0)
|
|
||||||
history_stds = history.std(axis=0)
|
|
||||||
safe_stds = np.where(history_stds > 1e-9, history_stds, 1e-9)
|
|
||||||
deviations = (latest_values - history_means) / safe_stds
|
|
||||||
ranking = sorted(
|
ranking = sorted(
|
||||||
zip(
|
zip(sensor_nodes, latest_values, strict=False),
|
||||||
sensor_nodes,
|
key=lambda item: item[1],
|
||||||
latest_values,
|
|
||||||
history_means,
|
|
||||||
history_stds,
|
|
||||||
deviations,
|
|
||||||
strict=False,
|
|
||||||
),
|
|
||||||
key=lambda item: item[4],
|
|
||||||
)
|
)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"sensor_node": sensor_id,
|
"sensor_node": sensor_id,
|
||||||
"latest_high_frequency_value": float(value),
|
"latest_high_frequency_value": float(value),
|
||||||
"historical_mean": float(history_mean),
|
|
||||||
"historical_std": float(history_std),
|
|
||||||
"standardized_deviation": float(deviation),
|
|
||||||
}
|
}
|
||||||
for sensor_id, value, history_mean, history_std, deviation in ranking[
|
for sensor_id, value in ranking[: min(10, len(ranking))]
|
||||||
: min(10, len(ranking))
|
|
||||||
]
|
]
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _build_target_pressure_from_scada(
|
|
||||||
*,
|
|
||||||
network: str,
|
|
||||||
sensor_nodes: list[str],
|
|
||||||
requested_target_time: datetime | None,
|
|
||||||
sampling_interval_minutes: int,
|
|
||||||
points_per_day: int,
|
|
||||||
) -> tuple[pd.DataFrame, datetime, list[dict[str, str]]]:
|
|
||||||
node_query_id = _get_pressure_sensor_mapping(network)
|
|
||||||
mapped_nodes = [node for node in sensor_nodes if node in node_query_id]
|
|
||||||
excluded_without_mapping = [
|
|
||||||
{"sensor_node": node, "reason": "missing_api_query_id"}
|
|
||||||
for node in sensor_nodes
|
|
||||||
if node not in node_query_id
|
|
||||||
]
|
|
||||||
if len(mapped_nodes) < MIN_COMPLETE_SENSORS:
|
|
||||||
raise ValueError(
|
|
||||||
f"可查询的压力测点少于 {MIN_COMPLETE_SENSORS} 个,无法执行爆管侦测。"
|
|
||||||
)
|
|
||||||
|
|
||||||
query_ids = [node_query_id[node] for node in mapped_nodes]
|
|
||||||
candidate_before = requested_target_time
|
|
||||||
last_excluded: list[dict[str, str]] = excluded_without_mapping
|
|
||||||
|
|
||||||
for _ in range(4):
|
|
||||||
resolved_target = InternalQueries.query_latest_scada_time(
|
|
||||||
db_name=network,
|
|
||||||
device_ids=query_ids,
|
|
||||||
before_time=candidate_before,
|
|
||||||
)
|
|
||||||
if resolved_target is None:
|
|
||||||
break
|
|
||||||
|
|
||||||
sample_start = resolved_target - timedelta(
|
|
||||||
days=TARGET_DAY_COUNT,
|
|
||||||
minutes=-sampling_interval_minutes,
|
|
||||||
)
|
|
||||||
expected_index = pd.date_range(
|
|
||||||
start=sample_start,
|
|
||||||
end=resolved_target,
|
|
||||||
freq=f"{sampling_interval_minutes}min",
|
|
||||||
)
|
|
||||||
scada_data = InternalQueries.query_scada_by_ids_timerange(
|
|
||||||
db_name=network,
|
|
||||||
device_ids=query_ids,
|
|
||||||
start_time=sample_start,
|
|
||||||
end_time=resolved_target,
|
|
||||||
)
|
|
||||||
|
|
||||||
complete_columns: dict[str, pd.Series] = {}
|
|
||||||
excluded = list(excluded_without_mapping)
|
|
||||||
for node_id in mapped_nodes:
|
|
||||||
query_id = node_query_id[node_id]
|
|
||||||
records = scada_data.get(query_id, [])
|
|
||||||
if not records:
|
|
||||||
excluded.append({"sensor_node": node_id, "reason": "no_data"})
|
|
||||||
continue
|
|
||||||
|
|
||||||
record_frame = pd.DataFrame.from_records(records)
|
|
||||||
record_frame["time"] = pd.to_datetime(record_frame["time"], utc=True)
|
|
||||||
record_frame["value"] = pd.to_numeric(
|
|
||||||
record_frame["value"], errors="coerce"
|
|
||||||
)
|
|
||||||
series = (
|
|
||||||
record_frame.drop_duplicates(subset="time", keep="last")
|
|
||||||
.set_index("time")["value"]
|
|
||||||
.reindex(expected_index)
|
|
||||||
)
|
|
||||||
if len(series) != TARGET_DAY_COUNT * points_per_day:
|
|
||||||
excluded.append(
|
|
||||||
{"sensor_node": node_id, "reason": "unexpected_sample_count"}
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
if series.isna().any():
|
|
||||||
excluded.append(
|
|
||||||
{"sensor_node": node_id, "reason": "missing_or_invalid_samples"}
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
complete_columns[node_id] = series
|
|
||||||
|
|
||||||
if len(complete_columns) >= MIN_COMPLETE_SENSORS:
|
|
||||||
observation_df = pd.DataFrame(complete_columns, index=expected_index)
|
|
||||||
return observation_df, resolved_target, excluded
|
|
||||||
|
|
||||||
last_excluded = excluded
|
|
||||||
candidate_before = resolved_target - timedelta(microseconds=1)
|
|
||||||
|
|
||||||
excluded_preview = ", ".join(
|
|
||||||
item["sensor_node"] for item in last_excluded[:10]
|
|
||||||
)
|
|
||||||
raise ValueError(
|
|
||||||
f"最近数据中完整压力测点少于 {MIN_COMPLETE_SENSORS} 个;"
|
|
||||||
f"请检查 15 天数据完整性。排除测点: {excluded_preview or '无'}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_sampling_interval_minutes(
|
|
||||||
*,
|
|
||||||
network: str,
|
|
||||||
sensor_nodes: list[str],
|
|
||||||
requested_interval: int | None,
|
|
||||||
) -> int:
|
|
||||||
if requested_interval is not None:
|
|
||||||
interval = int(requested_interval)
|
|
||||||
else:
|
|
||||||
selected_nodes = set(sensor_nodes)
|
|
||||||
inferred_intervals = [
|
|
||||||
parsed
|
|
||||||
for item in get_all_scada_info(network)
|
|
||||||
if str(item.get("type", "")).lower() == "pressure"
|
|
||||||
and str(item.get("associated_element_id", "")) in selected_nodes
|
|
||||||
and (
|
|
||||||
parsed := _parse_sampling_interval_minutes(
|
|
||||||
item.get("transmission_frequency")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
is not None
|
|
||||||
]
|
|
||||||
interval = (
|
|
||||||
Counter(inferred_intervals).most_common(1)[0][0]
|
|
||||||
if inferred_intervals
|
|
||||||
else DEFAULT_SAMPLE_INTERVAL_MINUTES
|
|
||||||
)
|
|
||||||
|
|
||||||
if interval <= 0 or 1440 % interval != 0:
|
|
||||||
raise ValueError("采样间隔必须是能整除 1440 分钟的正整数。")
|
|
||||||
return interval
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_sampling_interval_minutes(value: Any) -> int | None:
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
if isinstance(value, (int, float)):
|
|
||||||
minutes = float(value)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
minutes = pd.to_timedelta(str(value)).total_seconds() / 60
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return None
|
|
||||||
rounded = round(minutes)
|
|
||||||
if minutes <= 0 or abs(minutes - rounded) > 1e-6:
|
|
||||||
return None
|
|
||||||
return int(rounded)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_pressure_sensor_mapping(network: str) -> dict[str, str]:
|
|
||||||
node_query_id: dict[str, str] = {}
|
|
||||||
for item in get_all_scada_info(network):
|
|
||||||
if str(item.get("type", "")).lower() != "pressure":
|
|
||||||
continue
|
|
||||||
node_id = item.get("associated_element_id")
|
|
||||||
query_id = item.get("api_query_id")
|
|
||||||
if node_id and query_id is not None:
|
|
||||||
node_query_id[str(node_id)] = str(query_id)
|
|
||||||
return node_query_id
|
|
||||||
|
|
||||||
|
|
||||||
def _get_pressure_sensor_nodes(network: str) -> list[str]:
|
def _get_pressure_sensor_nodes(network: str) -> list[str]:
|
||||||
@@ -723,7 +377,19 @@ def _build_observed_pressure_from_scada(
|
|||||||
if start_dt >= end_dt:
|
if start_dt >= end_dt:
|
||||||
raise ValueError("SCADA 时间窗非法:scada_start 必须早于 scada_end。")
|
raise ValueError("SCADA 时间窗非法:scada_start 必须早于 scada_end。")
|
||||||
|
|
||||||
node_query_id = _get_pressure_sensor_mapping(network)
|
node_query_id: dict[str, str] = {}
|
||||||
|
for item in get_all_scada_info(network):
|
||||||
|
if str(item.get("type", "")).lower() != "pressure":
|
||||||
|
continue
|
||||||
|
node_id = item.get("associated_element_id")
|
||||||
|
query_id = item.get("api_query_id")
|
||||||
|
if (
|
||||||
|
isinstance(node_id, str)
|
||||||
|
and node_id
|
||||||
|
and isinstance(query_id, str)
|
||||||
|
and query_id
|
||||||
|
):
|
||||||
|
node_query_id[node_id] = query_id
|
||||||
|
|
||||||
missing_nodes = [node_id for node_id in sensor_nodes if node_id not in node_query_id]
|
missing_nodes = [node_id for node_id in sensor_nodes if node_id not in node_query_id]
|
||||||
if missing_nodes:
|
if missing_nodes:
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ non_realtime_region_patterns = {} # 基于source_outflow_region进行区分
|
|||||||
realtime_region_pipe_flow_and_demand_id = {} # 基于source_outflow_region搜索该分区中的实时pipe_flow和demand的api_query_id,后续用region的流量 - 实时流量计的流量
|
realtime_region_pipe_flow_and_demand_id = {} # 基于source_outflow_region搜索该分区中的实时pipe_flow和demand的api_query_id,后续用region的流量 - 实时流量计的流量
|
||||||
realtime_region_pipe_flow_and_demand_patterns = {} # 基于source_outflow_region搜索该分区中的实时pipe_flow和demand的associated_pattern,后续用region的流量 - 实时流量计的流量
|
realtime_region_pipe_flow_and_demand_patterns = {} # 基于source_outflow_region搜索该分区中的实时pipe_flow和demand的associated_pattern,后续用region的流量 - 实时流量计的流量
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# influxdb_api.py中的全局变量
|
# 历史数据访问相关全局变量
|
||||||
# 全局变量,用于存储不同类型的 realtime api_query_id
|
# 全局变量,用于存储不同类型的 realtime api_query_id
|
||||||
reservoir_liquid_level_realtime_ids = []
|
reservoir_liquid_level_realtime_ids = []
|
||||||
tank_liquid_level_realtime_ids = []
|
tank_liquid_level_realtime_ids = []
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import pytz
|
|||||||
import requests
|
import requests
|
||||||
import time
|
import time
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
import app.infra.db.influxdb.api as influxdb_api
|
|
||||||
import typing
|
import typing
|
||||||
import psycopg
|
import psycopg
|
||||||
import logging
|
import logging
|
||||||
|
|||||||
@@ -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}")
|
|
||||||
@@ -1,626 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .core import CommandDoc, CommandOptionDoc, SCHEMA_VERSION
|
|
||||||
|
|
||||||
GROUP_SUMMARIES: dict[tuple[str, ...], str] = {
|
|
||||||
("network",): "管网节点、管线等基础属性查询命令。",
|
|
||||||
("component",): "组件选项与配置读取命令。",
|
|
||||||
("component", "option"): "组件选项查询命令。",
|
|
||||||
("simulation",): "模拟运行与调度相关命令。",
|
|
||||||
("analysis",): "分析计算与诊断相关命令。",
|
|
||||||
("analysis", "leakage"): "漏损分析相关命令。",
|
|
||||||
("analysis", "leakage", "schemes"): "漏损方案查询命令。",
|
|
||||||
("analysis", "burst-detection"): "爆管检测相关命令。",
|
|
||||||
("analysis", "burst-detection", "schemes"): "爆管检测方案查询命令。",
|
|
||||||
("analysis", "burst-location"): "爆管定位相关命令。",
|
|
||||||
("analysis", "burst-location", "schemes"): "爆管定位方案查询命令。",
|
|
||||||
("analysis", "risk"): "风险分析相关命令。",
|
|
||||||
("analysis", "sensor-placement"): "传感器选址相关命令。",
|
|
||||||
("data",): "时序、SCADA 和方案数据查询命令。",
|
|
||||||
("data", "timeseries"): "时序数据查询命令。",
|
|
||||||
("data", "timeseries", "realtime"): "实时模拟时序查询命令。",
|
|
||||||
("data", "timeseries", "scheme"): "方案时序查询命令。",
|
|
||||||
("data", "timeseries", "scada"): "SCADA 时序查询命令。",
|
|
||||||
("data", "timeseries", "composite"): "复合时序查询命令。",
|
|
||||||
("data", "scada"): "SCADA 元数据查询命令。",
|
|
||||||
("data", "scheme"): "方案数据查询命令。",
|
|
||||||
}
|
|
||||||
|
|
||||||
HIDDEN_PATH_PREFIXES: tuple[tuple[str, ...], ...] = (
|
|
||||||
("analysis", "burst-location"),
|
|
||||||
("analysis", "risk"),
|
|
||||||
)
|
|
||||||
|
|
||||||
COMMAND_DOCS: dict[tuple[str, ...], CommandDoc] = {
|
|
||||||
("network", "get-junction-properties"): CommandDoc(
|
|
||||||
path=("network", "get-junction-properties"),
|
|
||||||
summary="读取节点属性",
|
|
||||||
description="调用 /getjunctionproperties/。",
|
|
||||||
options=(CommandOptionDoc("junction", "节点 ID", required=True),),
|
|
||||||
examples=("tjwater-cli network get-junction-properties --junction J1",),
|
|
||||||
),
|
|
||||||
("network", "get-pipe-properties"): CommandDoc(
|
|
||||||
path=("network", "get-pipe-properties"),
|
|
||||||
summary="读取管道属性",
|
|
||||||
description="调用 /getpipeproperties/。",
|
|
||||||
options=(CommandOptionDoc("pipe", "管道 ID", required=True),),
|
|
||||||
examples=("tjwater-cli network get-pipe-properties --pipe P1",),
|
|
||||||
),
|
|
||||||
("network", "get-all-pipes-properties"): CommandDoc(
|
|
||||||
path=("network", "get-all-pipes-properties"),
|
|
||||||
summary="读取全部管道属性",
|
|
||||||
description="调用 /getallpipeproperties/。",
|
|
||||||
examples=("tjwater-cli network get-all-pipes-properties",),
|
|
||||||
),
|
|
||||||
("network", "get-reservoir-properties"): CommandDoc(
|
|
||||||
path=("network", "get-reservoir-properties"),
|
|
||||||
summary="读取水库属性",
|
|
||||||
description="调用 /getreservoirproperties/。",
|
|
||||||
options=(CommandOptionDoc("reservoir", "水库 ID", required=True),),
|
|
||||||
examples=("tjwater-cli network get-reservoir-properties --reservoir R1",),
|
|
||||||
),
|
|
||||||
("network", "get-all-reservoirs-properties"): CommandDoc(
|
|
||||||
path=("network", "get-all-reservoirs-properties"),
|
|
||||||
summary="读取全部水库属性",
|
|
||||||
description="调用 /getallreservoirproperties/。",
|
|
||||||
examples=("tjwater-cli network get-all-reservoirs-properties",),
|
|
||||||
),
|
|
||||||
("network", "get-tank-properties"): CommandDoc(
|
|
||||||
path=("network", "get-tank-properties"),
|
|
||||||
summary="读取水箱属性",
|
|
||||||
description="调用 /gettankproperties/。",
|
|
||||||
options=(CommandOptionDoc("tank", "水箱 ID", required=True),),
|
|
||||||
examples=("tjwater-cli network get-tank-properties --tank T1",),
|
|
||||||
),
|
|
||||||
("network", "get-all-tanks-properties"): CommandDoc(
|
|
||||||
path=("network", "get-all-tanks-properties"),
|
|
||||||
summary="读取全部水箱属性",
|
|
||||||
description="调用 /getalltankproperties/。",
|
|
||||||
examples=("tjwater-cli network get-all-tanks-properties",),
|
|
||||||
),
|
|
||||||
("network", "get-pump-properties"): CommandDoc(
|
|
||||||
path=("network", "get-pump-properties"),
|
|
||||||
summary="读取水泵属性",
|
|
||||||
description="调用 /getpumpproperties/。",
|
|
||||||
options=(CommandOptionDoc("pump", "水泵 ID", required=True),),
|
|
||||||
examples=("tjwater-cli network get-pump-properties --pump PU1",),
|
|
||||||
),
|
|
||||||
("network", "get-all-pumps-properties"): CommandDoc(
|
|
||||||
path=("network", "get-all-pumps-properties"),
|
|
||||||
summary="读取全部水泵属性",
|
|
||||||
description="调用 /getallpumpproperties/。",
|
|
||||||
examples=("tjwater-cli network get-all-pumps-properties",),
|
|
||||||
),
|
|
||||||
("network", "get-valve-properties"): CommandDoc(
|
|
||||||
path=("network", "get-valve-properties"),
|
|
||||||
summary="读取阀门属性",
|
|
||||||
description="调用 /getvalveproperties/。",
|
|
||||||
options=(CommandOptionDoc("valve", "阀门 ID", required=True),),
|
|
||||||
examples=("tjwater-cli network get-valve-properties --valve V1",),
|
|
||||||
),
|
|
||||||
("network", "get-all-valves-properties"): CommandDoc(
|
|
||||||
path=("network", "get-all-valves-properties"),
|
|
||||||
summary="读取全部阀门属性",
|
|
||||||
description="调用 /getallvalveproperties/。",
|
|
||||||
examples=("tjwater-cli network get-all-valves-properties",),
|
|
||||||
),
|
|
||||||
("component", "option", "schema"): CommandDoc(
|
|
||||||
path=("component", "option", "schema"),
|
|
||||||
summary="读取选项 schema",
|
|
||||||
description="kind 支持 time、energy、pump-energy、network。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("kind", "选项类型", required=True),
|
|
||||||
CommandOptionDoc("pump", "pump-energy 时需要的泵 ID"),
|
|
||||||
),
|
|
||||||
examples=(
|
|
||||||
"tjwater-cli component option schema --kind time",
|
|
||||||
"tjwater-cli component option schema --kind energy",
|
|
||||||
"tjwater-cli component option schema --kind pump-energy --pump PUMP1",
|
|
||||||
"tjwater-cli component option schema --kind network",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("component", "option", "get"): CommandDoc(
|
|
||||||
path=("component", "option", "get"),
|
|
||||||
summary="读取选项属性",
|
|
||||||
description="kind 支持 time、energy、pump-energy、network。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("kind", "选项类型", required=True),
|
|
||||||
CommandOptionDoc("pump", "pump-energy 时需要的泵 ID"),
|
|
||||||
),
|
|
||||||
examples=(
|
|
||||||
"tjwater-cli component option get --kind time",
|
|
||||||
"tjwater-cli component option get --kind energy",
|
|
||||||
"tjwater-cli component option get --kind pump-energy --pump PUMP1",
|
|
||||||
"tjwater-cli component option get --kind network",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("simulation", "run"): CommandDoc(
|
|
||||||
path=("simulation", "run"),
|
|
||||||
summary="触发指定绝对时间的模拟运行",
|
|
||||||
description="把显式带时区的 RFC3339 start-time 直接传给 /simulations/run-by-date;服务端按带时区时间处理并统一按 UTC 存储结果,实时数据需后续通过 data timeseries 在对应时间段查询。duration 单位为分钟。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
|
||||||
CommandOptionDoc("duration", "持续分钟数", required=True),
|
|
||||||
),
|
|
||||||
examples=("tjwater-cli simulation run --start-time 2025-01-02T03:04:05+08:00 --duration 30",),
|
|
||||||
next_commands=(
|
|
||||||
"tjwater-cli data timeseries realtime links --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00",
|
|
||||||
"tjwater-cli data timeseries realtime nodes --start-time 2025-01-02T03:04:05+08:00 --end-time 2025-01-02T03:34:05+08:00",
|
|
||||||
),
|
|
||||||
output="模拟触发结果;实时数据需通过 data timeseries 命令按时间段查询",
|
|
||||||
),
|
|
||||||
("analysis", "burst"): CommandDoc(
|
|
||||||
path=("analysis", "burst"),
|
|
||||||
summary="执行爆管分析",
|
|
||||||
description="读取 burst-file 并转换为 burst_ID[] / burst_size[];接口本身只返回分析执行结果,方案数据需后续通过 data scheme 命令获取。duration 单位为秒。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
|
||||||
CommandOptionDoc("duration", "持续秒数", required=True),
|
|
||||||
CommandOptionDoc("burst-file", "爆管输入 JSON 文件", required=True),
|
|
||||||
CommandOptionDoc("scheme", "方案名称"),
|
|
||||||
),
|
|
||||||
examples=(
|
|
||||||
"tjwater-cli analysis burst --start-time 2025-01-02T03:04:05+08:00 --duration 900 --burst-file ./burst.json --scheme burst_case_01",
|
|
||||||
"tjwater-cli data scheme get --name burst_case_01",
|
|
||||||
"tjwater-cli data scheme list",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("analysis", "valve"): CommandDoc(
|
|
||||||
path=("analysis", "valve"),
|
|
||||||
summary="阀门工况分析。",
|
|
||||||
description="close 模式按指定阀门关闭执行定时长模拟;isolation 模式按指定事故元素计算关阀隔离方案。duration 单位为秒。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc(name="mode", description="阀门操作模式:'close' 或 'isolation'", required=True),
|
|
||||||
CommandOptionDoc(name="start-time", description="close 模式需要的起始绝对时间,必须显式带时区偏移"),
|
|
||||||
CommandOptionDoc(name="valve", description="close 模式下需关闭的阀门 ID(可多次指定)", repeated=True),
|
|
||||||
CommandOptionDoc(name="element", description="isolation 模式下的事故元素 ID(可多次指定)", repeated=True),
|
|
||||||
CommandOptionDoc(name="disabled-valve", description="isolation 模式下需排除的故障阀门 ID(可多次指定)", repeated=True),
|
|
||||||
CommandOptionDoc(name="duration", description="close 模式持续秒数,默认 900"),
|
|
||||||
CommandOptionDoc(name="scheme", description="close 模式方案名称"),
|
|
||||||
),
|
|
||||||
examples=(
|
|
||||||
"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",
|
|
||||||
"tjwater-cli analysis valve --mode isolation --element E1 --element E2",
|
|
||||||
"tjwater-cli analysis valve --mode isolation --element E1 --disabled-valve V3",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("analysis", "flushing"): CommandDoc(
|
|
||||||
path=("analysis", "flushing"),
|
|
||||||
summary="执行冲洗分析",
|
|
||||||
description="读取 valve-setting-file 并转换为 valves[] / valves_k[]。duration 单位为秒,默认 900。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
|
||||||
CommandOptionDoc("valve-setting-file", "阀门开度 JSON 文件", required=True),
|
|
||||||
CommandOptionDoc("drainage-node", "排污节点 ID", required=True),
|
|
||||||
CommandOptionDoc("flow", "冲洗流量", required=True),
|
|
||||||
CommandOptionDoc("duration", "持续秒数,默认 900"),
|
|
||||||
CommandOptionDoc("scheme", "方案名称", required=True),
|
|
||||||
),
|
|
||||||
examples=("tjwater-cli analysis flushing --start-time 2025-01-02T03:04:05+08:00 --valve-setting-file ./valve.json --drainage-node N1 --flow 100.0 --duration 900 --scheme flush_case_01",),
|
|
||||||
),
|
|
||||||
("analysis", "age"): CommandDoc(
|
|
||||||
path=("analysis", "age"),
|
|
||||||
summary="执行水龄分析",
|
|
||||||
description="调用 /age_analysis/。duration 单位为秒。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
|
||||||
CommandOptionDoc("duration", "持续秒数", required=True),
|
|
||||||
),
|
|
||||||
examples=("tjwater-cli analysis age --start-time 2025-01-02T03:04:05+08:00 --duration 900",),
|
|
||||||
),
|
|
||||||
("analysis", "contaminant"): CommandDoc(
|
|
||||||
path=("analysis", "contaminant"),
|
|
||||||
summary="执行污染物模拟",
|
|
||||||
description="调用 /contaminant-simulation。duration 单位为秒。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
|
||||||
CommandOptionDoc("duration", "持续秒数", required=True),
|
|
||||||
CommandOptionDoc("source-node", "污染源节点 ID", required=True),
|
|
||||||
CommandOptionDoc("concentration", "浓度值", required=True),
|
|
||||||
CommandOptionDoc("pattern", "模式 ID"),
|
|
||||||
CommandOptionDoc("scheme", "方案名称", required=True),
|
|
||||||
),
|
|
||||||
examples=("tjwater-cli analysis contaminant --start-time 2025-01-02T03:04:05+08:00 --duration 900 --source-node N1 --concentration 10.0 --scheme contam_case_01",),
|
|
||||||
),
|
|
||||||
("analysis", "sensor-placement", "kmeans"): CommandDoc(
|
|
||||||
path=("analysis", "sensor-placement", "kmeans"),
|
|
||||||
summary="执行 KMeans 传感器选址",
|
|
||||||
description="使用 POST /pressure_sensor_placement_kmeans/,补齐 username 和 min_diameter。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("count", "传感器数量", required=True),
|
|
||||||
CommandOptionDoc("min-diameter", "最小管径,默认 0"),
|
|
||||||
CommandOptionDoc("scheme", "方案名称"),
|
|
||||||
),
|
|
||||||
examples=("tjwater-cli analysis sensor-placement kmeans --count 5 --min-diameter 100 --scheme placement_case_01",),
|
|
||||||
),
|
|
||||||
("analysis", "leakage", "identify"): CommandDoc(
|
|
||||||
path=("analysis", "leakage", "identify"),
|
|
||||||
summary="执行漏损识别",
|
|
||||||
description="把 CLI 时间映射到 scada_start / scada_end。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("start-time", "显式带时区的 SCADA 开始时间", required=True),
|
|
||||||
CommandOptionDoc("end-time", "显式带时区的 SCADA 结束时间", required=True),
|
|
||||||
CommandOptionDoc("scheme", "方案名称"),
|
|
||||||
),
|
|
||||||
examples=("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",),
|
|
||||||
),
|
|
||||||
("analysis", "leakage", "schemes", "list"): CommandDoc(
|
|
||||||
path=("analysis", "leakage", "schemes", "list"),
|
|
||||||
summary="列出漏损方案",
|
|
||||||
description="调用 /leakage/schemes/。",
|
|
||||||
examples=("tjwater-cli analysis leakage schemes list",),
|
|
||||||
),
|
|
||||||
("analysis", "leakage", "schemes", "get"): CommandDoc(
|
|
||||||
path=("analysis", "leakage", "schemes", "get"),
|
|
||||||
summary="读取漏损方案详情",
|
|
||||||
description="调用 /leakage/schemes/{scheme_name}。",
|
|
||||||
examples=("tjwater-cli analysis leakage schemes get my_scheme",),
|
|
||||||
),
|
|
||||||
("analysis", "burst-detection", "detect"): CommandDoc(
|
|
||||||
path=("analysis", "burst-detection", "detect"),
|
|
||||||
summary="执行爆管检测",
|
|
||||||
description="调用 /burst-detection/detect/。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("start-time", "显式带时区的 SCADA 开始时间", required=True),
|
|
||||||
CommandOptionDoc("end-time", "显式带时区的 SCADA 结束时间", required=True),
|
|
||||||
CommandOptionDoc("scheme", "方案名称"),
|
|
||||||
),
|
|
||||||
examples=("tjwater-cli analysis burst-detection detect --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --scheme detect_case_01",),
|
|
||||||
),
|
|
||||||
("analysis", "burst-detection", "schemes", "list"): CommandDoc(
|
|
||||||
path=("analysis", "burst-detection", "schemes", "list"),
|
|
||||||
summary="列出爆管检测方案",
|
|
||||||
description="调用 /burst-detection/schemes/。",
|
|
||||||
examples=("tjwater-cli analysis burst-detection schemes list",),
|
|
||||||
),
|
|
||||||
("analysis", "burst-detection", "schemes", "get"): CommandDoc(
|
|
||||||
path=("analysis", "burst-detection", "schemes", "get"),
|
|
||||||
summary="读取爆管检测方案详情",
|
|
||||||
description="调用 /burst-detection/schemes/{scheme_name}。",
|
|
||||||
examples=("tjwater-cli analysis burst-detection schemes get my_scheme",),
|
|
||||||
),
|
|
||||||
("analysis", "burst-location", "locate"): CommandDoc(
|
|
||||||
path=("analysis", "burst-location", "locate"),
|
|
||||||
summary="执行爆管定位",
|
|
||||||
description="调用 /burst-location/locate/;需要 burst-leakage。支持 monitoring 和 simulation 两种数据源。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("start-time", "显式带时区的 SCADA 开始时间", required=True),
|
|
||||||
CommandOptionDoc("end-time", "显式带时区的 SCADA 结束时间", required=True),
|
|
||||||
CommandOptionDoc("burst-leakage", "爆管漏水量", required=True),
|
|
||||||
CommandOptionDoc("scheme", "方案名称"),
|
|
||||||
CommandOptionDoc("data-source", "数据源:monitoring(默认)或 simulation"),
|
|
||||||
CommandOptionDoc("pressure-scada-id", "压力 SCADA ID(可多次指定)", repeated=True),
|
|
||||||
CommandOptionDoc("flow-scada-id", "流量 SCADA ID(可多次指定)", repeated=True),
|
|
||||||
CommandOptionDoc("pressure-file", "包含 burst_pressure/normal_pressure 的 JSON 文件"),
|
|
||||||
CommandOptionDoc("flow-file", "包含 burst_flow/normal_flow 的 JSON 文件"),
|
|
||||||
CommandOptionDoc("use-scada-flow", "启用 SCADA 流量"),
|
|
||||||
),
|
|
||||||
examples=(
|
|
||||||
"tjwater-cli analysis burst-location locate --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --burst-leakage 100.0 --scheme locate_case_01",
|
|
||||||
"tjwater-cli analysis burst-location locate --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --burst-leakage 50.0 --scheme locate_case_01 --data-source simulation --pressure-file ./pressure.json --flow-file ./flow.json",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("analysis", "burst-location", "schemes", "list"): CommandDoc(
|
|
||||||
path=("analysis", "burst-location", "schemes", "list"),
|
|
||||||
summary="列出爆管定位方案",
|
|
||||||
description="调用 /burst-location/schemes/。",
|
|
||||||
examples=("tjwater-cli analysis burst-location schemes list",),
|
|
||||||
),
|
|
||||||
("analysis", "burst-location", "schemes", "get"): CommandDoc(
|
|
||||||
path=("analysis", "burst-location", "schemes", "get"),
|
|
||||||
summary="读取爆管定位方案详情",
|
|
||||||
description="调用 /burst-location/schemes/{scheme_name}。",
|
|
||||||
examples=("tjwater-cli analysis burst-location schemes get my_scheme",),
|
|
||||||
),
|
|
||||||
("analysis", "risk", "pipe-now"): CommandDoc(
|
|
||||||
path=("analysis", "risk", "pipe-now"),
|
|
||||||
summary="读取单条管道当前风险",
|
|
||||||
description="调用 /getpiperiskprobabilitynow/。",
|
|
||||||
options=(CommandOptionDoc("pipe", "管道 ID", required=True),),
|
|
||||||
examples=("tjwater-cli analysis risk pipe-now --pipe P1",),
|
|
||||||
),
|
|
||||||
("analysis", "risk", "pipe-history"): CommandDoc(
|
|
||||||
path=("analysis", "risk", "pipe-history"),
|
|
||||||
summary="读取单条管道历史风险",
|
|
||||||
description="调用 /getpiperiskprobability/。",
|
|
||||||
options=(CommandOptionDoc("pipe", "管道 ID", required=True),),
|
|
||||||
examples=("tjwater-cli analysis risk pipe-history --pipe P1",),
|
|
||||||
),
|
|
||||||
("analysis", "risk", "network"): CommandDoc(
|
|
||||||
path=("analysis", "risk", "network"),
|
|
||||||
summary="读取全网风险",
|
|
||||||
description="组合 /getnetworkpiperiskprobabilitynow/ 与 /getpiperiskprobabilitygeometries/。",
|
|
||||||
examples=("tjwater-cli analysis risk network",),
|
|
||||||
),
|
|
||||||
("data", "timeseries", "realtime", "links"): CommandDoc(
|
|
||||||
path=("data", "timeseries", "realtime", "links"),
|
|
||||||
summary="查询实时管道时序",
|
|
||||||
description="调用 /realtime/links。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
|
||||||
CommandOptionDoc("end-time", "显式带时区的结束时间", required=True),
|
|
||||||
),
|
|
||||||
examples=("tjwater-cli data timeseries realtime links --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00",),
|
|
||||||
),
|
|
||||||
("data", "timeseries", "realtime", "nodes"): CommandDoc(
|
|
||||||
path=("data", "timeseries", "realtime", "nodes"),
|
|
||||||
summary="查询实时节点时序",
|
|
||||||
description="调用 /realtime/nodes。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
|
||||||
CommandOptionDoc("end-time", "显式带时区的结束时间", required=True),
|
|
||||||
),
|
|
||||||
examples=("tjwater-cli data timeseries realtime nodes --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00",),
|
|
||||||
),
|
|
||||||
("data", "timeseries", "realtime", "simulation-by-id-time"): CommandDoc(
|
|
||||||
path=("data", "timeseries", "realtime", "simulation-by-id-time"),
|
|
||||||
summary="按元素和时间查询实时模拟结果",
|
|
||||||
description="调用 /realtime/query/by-id-time。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("id", "元素 ID", required=True),
|
|
||||||
CommandOptionDoc("type", "元素类型:pipe 或 junction;links/nodes 是独立子命令,不是 type 取值", required=True),
|
|
||||||
CommandOptionDoc("time", "显式带时区的查询时间", required=True),
|
|
||||||
),
|
|
||||||
examples=(
|
|
||||||
"tjwater-cli data timeseries realtime simulation-by-id-time --id J1 --type junction --time 2025-01-02T03:30:00+08:00",
|
|
||||||
"tjwater-cli data timeseries realtime simulation-by-id-time --id P1 --type pipe --time 2025-01-02T03:30:00+08:00",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("data", "timeseries", "realtime", "simulation-by-time-property"): CommandDoc(
|
|
||||||
path=("data", "timeseries", "realtime", "simulation-by-time-property"),
|
|
||||||
summary="按时间和属性查询实时模拟结果",
|
|
||||||
description="调用 /realtime/query/by-time-property。pipe 属性:flow、friction、headloss、quality、reaction、setting、status、velocity;junction 属性:actual_demand、total_head、pressure、quality。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("type", "元素类型:pipe 或 junction;links/nodes 是独立子命令,不是 type 取值", required=True),
|
|
||||||
CommandOptionDoc("time", "显式带时区的查询时间", required=True),
|
|
||||||
CommandOptionDoc("property", "属性名;会按 type 校验可选值", required=True),
|
|
||||||
),
|
|
||||||
examples=("tjwater-cli data timeseries realtime simulation-by-time-property --type pipe --time 2025-01-02T03:30:00+08:00 --property flow",),
|
|
||||||
),
|
|
||||||
("data", "timeseries", "scheme", "links"): CommandDoc(
|
|
||||||
path=("data", "timeseries", "scheme", "links"),
|
|
||||||
summary="查询方案管道时序",
|
|
||||||
description="调用 /scheme/links。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
|
||||||
CommandOptionDoc("end-time", "显式带时区的结束时间", required=True),
|
|
||||||
CommandOptionDoc("scheme", "方案名称"),
|
|
||||||
CommandOptionDoc("scheme-type", "方案类型"),
|
|
||||||
),
|
|
||||||
examples=("tjwater-cli data timeseries scheme links --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --scheme my_scheme",),
|
|
||||||
),
|
|
||||||
("data", "timeseries", "scheme", "node-field"): CommandDoc(
|
|
||||||
path=("data", "timeseries", "scheme", "node-field"),
|
|
||||||
summary="查询方案节点字段时序",
|
|
||||||
description="调用 /scheme/nodes/{node_id}/field。field 仅支持 actual_demand、total_head、pressure、quality。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("node", "节点 ID", required=True),
|
|
||||||
CommandOptionDoc("field", "字段名:actual_demand、total_head、pressure、quality", required=True),
|
|
||||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
|
||||||
CommandOptionDoc("end-time", "显式带时区的结束时间", required=True),
|
|
||||||
CommandOptionDoc("scheme", "方案名称"),
|
|
||||||
CommandOptionDoc("scheme-type", "方案类型"),
|
|
||||||
),
|
|
||||||
examples=("tjwater-cli data timeseries scheme node-field --node J1 --field pressure --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --scheme my_scheme",),
|
|
||||||
),
|
|
||||||
("data", "timeseries", "scheme", "simulation"): CommandDoc(
|
|
||||||
path=("data", "timeseries", "scheme", "simulation"),
|
|
||||||
summary="查询方案模拟数据",
|
|
||||||
description="支持 by-id-time 与 by-scheme-time-property 两种查询。pipe 属性:flow、friction、headloss、quality、reaction、setting、status、velocity;junction 属性:actual_demand、total_head、pressure、quality。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("query", "查询模式:by-id-time 或 by-scheme-time-property", required=True),
|
|
||||||
CommandOptionDoc("scheme", "方案名称"),
|
|
||||||
CommandOptionDoc("scheme-type", "方案类型"),
|
|
||||||
CommandOptionDoc("id", "元素 ID(by-id-time 时必需)"),
|
|
||||||
CommandOptionDoc("time", "显式带时区的查询时间", required=True),
|
|
||||||
CommandOptionDoc("type", "元素类型:pipe 或 junction;links/nodes 是独立子命令,不是 type 取值"),
|
|
||||||
CommandOptionDoc("property", "属性名(by-scheme-time-property 时必需;会按 type 校验可选值)"),
|
|
||||||
),
|
|
||||||
examples=(
|
|
||||||
"tjwater-cli data timeseries scheme simulation --query by-id-time --id J1 --time 2025-01-02T03:30:00+08:00 --type junction --scheme my_scheme",
|
|
||||||
"tjwater-cli data timeseries scheme simulation --query by-scheme-time-property --time 2025-01-02T03:30:00+08:00 --type pipe --property flow --scheme my_scheme",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("data", "timeseries", "scada", "query"): CommandDoc(
|
|
||||||
path=("data", "timeseries", "scada", "query"),
|
|
||||||
summary="查询 SCADA 时序",
|
|
||||||
description="device-id 会被转换成后端逗号分隔参数。field 仅支持 monitored_value、cleaned_value。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("device-id", "设备 ID(可多次指定)", required=True, repeated=True),
|
|
||||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
|
||||||
CommandOptionDoc("end-time", "显式带时区的结束时间", required=True),
|
|
||||||
CommandOptionDoc("field", "字段名:monitored_value、cleaned_value"),
|
|
||||||
),
|
|
||||||
examples=(
|
|
||||||
"tjwater-cli data timeseries scada query --device-id D1 --device-id D2 --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00",
|
|
||||||
"tjwater-cli 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 monitored_value",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("data", "timeseries", "composite"): CommandDoc(
|
|
||||||
path=("data", "timeseries", "composite"),
|
|
||||||
summary="执行复合时序查询",
|
|
||||||
description="kind 支持 scada-simulation、element-simulation、element-scada。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("kind", "复合查询类型", required=True),
|
|
||||||
CommandOptionDoc("feature", "特征值(可多次指定,scada-simulation 为 device_id,element-simulation 为 element_id:property,element-scada 为 element_id)", repeated=True),
|
|
||||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
|
||||||
CommandOptionDoc("end-time", "显式带时区的结束时间", required=True),
|
|
||||||
CommandOptionDoc("scheme", "方案名称"),
|
|
||||||
CommandOptionDoc("scheme-type", "方案类型"),
|
|
||||||
CommandOptionDoc("use-cleaned", "element-scada 使用清洗值"),
|
|
||||||
),
|
|
||||||
examples=(
|
|
||||||
"tjwater-cli data timeseries composite --kind scada-simulation --feature D1 --feature D2 --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --scheme my_scheme",
|
|
||||||
"tjwater-cli data timeseries composite --kind element-simulation --feature J1:pressure --feature P1:flow --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --scheme my_scheme",
|
|
||||||
"tjwater-cli data timeseries composite --kind element-scada --feature J1 --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --use-cleaned",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("data", "timeseries", "composite", "pipeline-health"): CommandDoc(
|
|
||||||
path=("data", "timeseries", "composite", "pipeline-health"),
|
|
||||||
summary="查询管道健康预测",
|
|
||||||
description="调用 /composite/pipeline-health-prediction。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("pipe", "管道 ID", required=True),
|
|
||||||
CommandOptionDoc("start-time", "显式带时区的开始时间", required=True),
|
|
||||||
CommandOptionDoc("end-time", "显式带时区的结束时间", required=True),
|
|
||||||
),
|
|
||||||
examples=("tjwater-cli data timeseries composite pipeline-health --pipe P1 --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00",),
|
|
||||||
),
|
|
||||||
("data", "scada", "get"): CommandDoc(
|
|
||||||
path=("data", "scada", "get"),
|
|
||||||
summary="读取单条 SCADA 元数据",
|
|
||||||
description="kind 仅支持 info。",
|
|
||||||
options=(
|
|
||||||
CommandOptionDoc("kind", "SCADA 数据类型", required=True),
|
|
||||||
CommandOptionDoc("id", "记录 ID", required=True),
|
|
||||||
),
|
|
||||||
examples=("tjwater-cli data scada get --kind info --id SCADA-001",),
|
|
||||||
),
|
|
||||||
("data", "scada", "list"): CommandDoc(
|
|
||||||
path=("data", "scada", "list"),
|
|
||||||
summary="列出 SCADA 元数据",
|
|
||||||
description="kind 仅支持 info。",
|
|
||||||
options=(CommandOptionDoc("kind", "SCADA 数据类型", required=True),),
|
|
||||||
examples=("tjwater-cli data scada list --kind info",),
|
|
||||||
),
|
|
||||||
("data", "scheme", "schema"): CommandDoc(
|
|
||||||
path=("data", "scheme", "schema"),
|
|
||||||
summary="读取方案 schema",
|
|
||||||
description="调用 /getschemeschema/。",
|
|
||||||
examples=("tjwater-cli data scheme schema",),
|
|
||||||
),
|
|
||||||
("data", "scheme", "get"): CommandDoc(
|
|
||||||
path=("data", "scheme", "get"),
|
|
||||||
summary="读取单条方案",
|
|
||||||
description="调用 /getscheme/。",
|
|
||||||
options=(CommandOptionDoc("name", "方案名称", required=True),),
|
|
||||||
examples=("tjwater-cli data scheme get --name my_scheme",),
|
|
||||||
),
|
|
||||||
("data", "scheme", "list"): CommandDoc(
|
|
||||||
path=("data", "scheme", "list"),
|
|
||||||
summary="列出方案",
|
|
||||||
description="调用 /schemes。",
|
|
||||||
examples=("tjwater-cli data scheme list",),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _build_examples(doc: CommandDoc) -> list[str]:
|
|
||||||
return list(doc.examples) if doc.examples else [_build_usage(doc)]
|
|
||||||
|
|
||||||
|
|
||||||
def _is_hidden_path(path: tuple[str, ...]) -> bool:
|
|
||||||
return any(path[: len(prefix)] == prefix for prefix in HIDDEN_PATH_PREFIXES)
|
|
||||||
|
|
||||||
|
|
||||||
def is_hidden_path(path: tuple[str, ...]) -> bool:
|
|
||||||
return _is_hidden_path(path)
|
|
||||||
|
|
||||||
|
|
||||||
def has_subcommands(path_prefix: tuple[str, ...]) -> bool:
|
|
||||||
return any(
|
|
||||||
not _is_hidden_path(doc.path)
|
|
||||||
and doc.path[: len(path_prefix)] == path_prefix
|
|
||||||
and len(doc.path) > len(path_prefix)
|
|
||||||
for doc in COMMAND_DOCS.values()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_group_summary(path_prefix: tuple[str, ...]) -> str:
|
|
||||||
return GROUP_SUMMARIES.get(path_prefix, f"{' '.join(path_prefix)} 可用子命令")
|
|
||||||
|
|
||||||
|
|
||||||
def list_capabilities() -> dict[str, object]:
|
|
||||||
seen: set[tuple[str, ...]] = set()
|
|
||||||
commands: list[dict[str, str]] = []
|
|
||||||
for doc in sorted(COMMAND_DOCS.values(), key=lambda item: item.path):
|
|
||||||
if _is_hidden_path(doc.path):
|
|
||||||
continue
|
|
||||||
prefix = doc.path[:1]
|
|
||||||
if prefix in seen:
|
|
||||||
continue
|
|
||||||
seen.add(prefix)
|
|
||||||
commands.append(
|
|
||||||
{
|
|
||||||
"command": " ".join(prefix),
|
|
||||||
"summary": get_group_summary(prefix),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"ok": True,
|
|
||||||
"schema_version": SCHEMA_VERSION,
|
|
||||||
"summary": "可用一级菜单",
|
|
||||||
"menu_level": 1,
|
|
||||||
"commands": commands,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_command_doc(path: tuple[str, ...]) -> dict[str, object] | None:
|
|
||||||
if _is_hidden_path(path):
|
|
||||||
return None
|
|
||||||
doc = COMMAND_DOCS.get(path)
|
|
||||||
if doc is None:
|
|
||||||
return None
|
|
||||||
return {
|
|
||||||
"ok": True,
|
|
||||||
"schema_version": SCHEMA_VERSION,
|
|
||||||
"summary": doc.summary,
|
|
||||||
"command": " ".join(doc.path),
|
|
||||||
"description": doc.description,
|
|
||||||
"usage": _build_usage(doc),
|
|
||||||
"options": [
|
|
||||||
{
|
|
||||||
"name": option.name,
|
|
||||||
"description": option.description,
|
|
||||||
"required": option.required,
|
|
||||||
"repeated": option.repeated,
|
|
||||||
"default": option.default,
|
|
||||||
}
|
|
||||||
for option in doc.options
|
|
||||||
],
|
|
||||||
"examples": _build_examples(doc),
|
|
||||||
"next_commands": list(doc.next_commands),
|
|
||||||
"output": doc.output,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def list_subcommands(path_prefix: tuple[str, ...], summary: str | None = None) -> dict[str, object]:
|
|
||||||
seen: set[str] = set()
|
|
||||||
commands: list[dict[str, str]] = []
|
|
||||||
for doc in sorted(COMMAND_DOCS.values(), key=lambda item: item.path):
|
|
||||||
if _is_hidden_path(doc.path):
|
|
||||||
continue
|
|
||||||
if doc.path[: len(path_prefix)] != path_prefix or len(doc.path) <= len(path_prefix):
|
|
||||||
continue
|
|
||||||
subcommand = doc.path[len(path_prefix)]
|
|
||||||
if subcommand in seen:
|
|
||||||
continue
|
|
||||||
seen.add(subcommand)
|
|
||||||
current_path = (*path_prefix, subcommand)
|
|
||||||
is_group = has_subcommands(current_path)
|
|
||||||
usage = f"tjwater-cli {' '.join(current_path)} help" if is_group else (doc.examples[0] if doc.examples else _build_usage(doc))
|
|
||||||
commands.append(
|
|
||||||
{
|
|
||||||
"command": " ".join(current_path),
|
|
||||||
"summary": get_group_summary(current_path) if is_group else doc.summary,
|
|
||||||
"usage": usage,
|
|
||||||
"example": f"tjwater-cli {' '.join(current_path)} help" if is_group else _build_examples(doc)[0],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"ok": True,
|
|
||||||
"schema_version": SCHEMA_VERSION,
|
|
||||||
"summary": summary or get_group_summary(path_prefix),
|
|
||||||
"commands": commands,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _build_usage(doc: CommandDoc) -> str:
|
|
||||||
parts = ["tjwater-cli", *doc.path]
|
|
||||||
for option in doc.options:
|
|
||||||
placeholder = option.name.upper().replace("-", "_")
|
|
||||||
if option.required:
|
|
||||||
parts.extend([f"--{option.name}", f"<{placeholder}>"])
|
|
||||||
else:
|
|
||||||
parts.append(f"[--{option.name} <{placeholder}>]")
|
|
||||||
return " ".join(parts)
|
|
||||||
@@ -1,427 +0,0 @@
|
|||||||
# Agent CLI 接口范围确认
|
|
||||||
|
|
||||||
本文档确认 `app/api/v1/endpoints/` 面向 Agent CLI 的首批封装范围。
|
|
||||||
|
|
||||||
## 结论
|
|
||||||
|
|
||||||
首批 CLI 采用 **少量顶层入口 + 业务域二级分组 + 只读/分析优先** 的设计。
|
|
||||||
|
|
||||||
```text
|
|
||||||
tjwater-cli network
|
|
||||||
tjwater-cli component
|
|
||||||
tjwater-cli simulation
|
|
||||||
tjwater-cli analysis
|
|
||||||
tjwater-cli data
|
|
||||||
tjwater-cli help
|
|
||||||
```
|
|
||||||
|
|
||||||
首批默认不暴露:
|
|
||||||
|
|
||||||
- 会修改 network 的接口:`add*`、`set*`、`delete*`、`generate*`
|
|
||||||
- 项目生命周期接口:创建、删除、导入、打开、关闭、锁定、解锁、复制
|
|
||||||
- 数据写入/清理接口:insert、update、delete、clean、clear、batch store
|
|
||||||
- 用户管理接口:创建、更新、删除、激活、停用
|
|
||||||
- 快照回滚和批量命令执行接口:undo、redo、pick、batch
|
|
||||||
|
|
||||||
## 设计原则
|
|
||||||
|
|
||||||
- CLI 不按 HTTP endpoint 一比一映射,而按 Agent 任务组织。
|
|
||||||
- 首批只暴露 `schema`、`list`、`get`、`exists`、只读计算和分析类能力。
|
|
||||||
- CLI 输入优先使用显式选项、可重复选项、枚举值和文件路径,尽量不要求用户直接输入 JSON。
|
|
||||||
- CLI 输出统一使用 JSON;首批默认直接在 stdout 返回结构化结果,不再额外设计 `result_ref` / `--out-ref` 输出层。
|
|
||||||
- 首批 CLI 只保留 **Non-interactive / Agent** 认证模式:必须显式注入认证上下文,不隐式复用本机默认登录态,也不设计本地 `login`。
|
|
||||||
- stdout/stderr、退出码、输出 schema version 视为 CLI 契约的一部分,需要独立于 HTTP body 明确定义。
|
|
||||||
- 现有 HTTP 路径的拼写错误、双斜杠、错误方法不继承到 CLI。
|
|
||||||
- 高频命令可以提供 alias,但文档和 skill 只写规范命令。
|
|
||||||
|
|
||||||
## 分级约束
|
|
||||||
|
|
||||||
| 顶层命令 | 二级范围 | 说明 |
|
|
||||||
|---|---|---|
|
|
||||||
| `network` | `get-node-properties`、`get-link-properties` | 管网节点/管线属性查询,只读 |
|
|
||||||
| `component` | `option` | EPANET 选项设置,只读 |
|
|
||||||
| `simulation` | `run` | 模拟运行 |
|
|
||||||
| `analysis` | `burst`、`valve`、`flushing`、`age`、`contaminant`、`sensor-placement`、`leakage`、`burst-detection`、`burst-location`、`risk` | 任务级分析 |
|
|
||||||
| `data` | `timeseries`、`scada`、`scheme`、`extension`、`misc` | 数据查询 |
|
|
||||||
| `help` | `COMMAND` | Agent 能力发现和命令说明 |
|
|
||||||
|
|
||||||
命令深度建议:
|
|
||||||
|
|
||||||
- 常规命令不超过 3 层:`tjwater-cli component option get`
|
|
||||||
- 时序数据允许 4 层:`tjwater-cli data timeseries realtime links`
|
|
||||||
- `risk` 归入 `analysis risk`
|
|
||||||
- `scada`、`scheme`、`extension` 归入 `data`
|
|
||||||
|
|
||||||
## 全局上下文与通用参数
|
|
||||||
|
|
||||||
首批 CLI 建议统一支持以下全局参数:
|
|
||||||
|
|
||||||
```text
|
|
||||||
--server URL
|
|
||||||
--auth-context PATH
|
|
||||||
--scheme SCHEME
|
|
||||||
--timeout SEC
|
|
||||||
--request-id ID
|
|
||||||
```
|
|
||||||
|
|
||||||
参数含义:
|
|
||||||
|
|
||||||
| 参数 | 含义 | 作用域 | 说明 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `--server URL` | 指定 CLI 要连接的服务端地址 | 连接上下文 | 例如 `https://api.example.com`。用于覆盖环境变量或 `auth-context` 中的默认 base URL,便于在 dev / test / prod 间切换。 |
|
|
||||||
| `--auth-context PATH` | 指定一份显式的隔离认证上下文文件 | 认证上下文 | 面向 agent / 自动化调用。该文件可包含 access token、server、project、user 等字段;不得隐式回退到本机默认状态。 |
|
|
||||||
| `--scheme SCHEME` | 指定当前命令使用的方案 / 工况 / 配置集标识 | 业务资源上下文 | 适用于时序方案、检测方案、定位方案等场景。用于区分当前 project 下的不同分析配置。 |
|
|
||||||
| `--timeout SEC` | 指定本次命令等待响应的超时时间 | 执行控制 | 对同步请求表示请求超时上限,超过后 CLI 直接返回超时错误。 |
|
|
||||||
| `--request-id ID` | 为本次调用显式指定链路追踪 ID | 追踪与观测 | 便于跨前端、CLI、服务端串联日志与审计记录。若未提供,CLI 可自动生成,并应在输出 metadata 中回显。 |
|
|
||||||
|
|
||||||
约束:
|
|
||||||
|
|
||||||
- project 属于认证上下文的一部分,默认从 `auth-context` 或前端传入的 `X-Project-Id` 解析,不作为常规全局参数要求重复传入。
|
|
||||||
- 首批 CLI 不提供 Interactive / Human 登录态;所有命令都按 Agent 模式处理,不得依赖隐式默认认证状态。
|
|
||||||
- `--server`、`--auth-context` 属于连接与认证上下文;`--scheme` 属于业务资源上下文,两者需要分开建模。
|
|
||||||
- `--request-id` 用于链路追踪;若未显式传入,CLI 可以自动生成,但必须在输出 metadata 中回显。
|
|
||||||
|
|
||||||
参数表达建议:
|
|
||||||
|
|
||||||
- 用户输入的业务时间默认按 **UTC+8** 理解;若命令直接接收完整时间戳,应使用 ISO 8601 / RFC 3339 并显式包含时区。CLI 可直接传 `+08:00`,也可传其他时区的绝对时间,由服务端统一归一化。
|
|
||||||
- 范围参数优先拆成 `--start-time` / `--end-time`,不再引入模糊的 `--time-range ...` 写法。
|
|
||||||
- 复合输入优先使用可重复显式选项或 `--input FILE`,避免把多个语义字段压进 `ID:SIZE`、`NODE:VALUE`、`VALVE:OPENING` 这类 shell 内联 DSL。
|
|
||||||
- 若必须传大批量复合参数,优先支持 `--input FILE`,文件格式由 `help` 给出 schema。
|
|
||||||
|
|
||||||
## 首批 CLI 范围
|
|
||||||
|
|
||||||
### Network
|
|
||||||
|
|
||||||
来源:
|
|
||||||
|
|
||||||
```text
|
|
||||||
app/api/v1/endpoints/network/*.py
|
|
||||||
```
|
|
||||||
|
|
||||||
| 命令 | 覆盖接口 | 说明 |
|
|
||||||
|---|---|---|
|
|
||||||
| `tjwater-cli network get-node-properties --node NODE` | `GET /getnodeproperties/` | 读取当前 project 中指定节点的属性 |
|
|
||||||
| `tjwater-cli network get-link-properties --link LINK` | `GET /getlinkproperties/` | 读取当前 project 中指定管线的属性 |
|
|
||||||
| `tjwater-cli network get-all-junction-properties` | `GET /getalljunctionproperties/` | 读取当前 project 中所有节点属性 |
|
|
||||||
| `tjwater-cli network get-all-pipe-properties` | `GET /getallpipeproperties/` | 读取当前 project 中所有管道属性 |
|
|
||||||
|
|
||||||
暂不暴露:
|
|
||||||
|
|
||||||
```text
|
|
||||||
add*
|
|
||||||
set*
|
|
||||||
delete*
|
|
||||||
generate*
|
|
||||||
POST /generatedistrictmeteringarea/
|
|
||||||
POST /generatesubdistrictmeteringarea/
|
|
||||||
POST /generateservicearea/
|
|
||||||
POST /generatevirtualdistrict/
|
|
||||||
```
|
|
||||||
|
|
||||||
备注:`GET /settitle/` 语义是修改标题,首批不暴露。
|
|
||||||
|
|
||||||
### Component
|
|
||||||
|
|
||||||
来源:
|
|
||||||
|
|
||||||
```text
|
|
||||||
app/api/v1/endpoints/components/*.py
|
|
||||||
```
|
|
||||||
|
|
||||||
| 命令 | 覆盖接口 | 说明 |
|
|
||||||
|---|---|---|
|
|
||||||
| `tjwater-cli component option schema --kind time` | `GET /gettimeschema` | 时间选项 schema |
|
|
||||||
| `tjwater-cli component option get --kind time` | `GET /gettimeproperties/` | 时间选项属性 |
|
|
||||||
| `tjwater-cli component option schema --kind energy` | `GET /getenergyschema/` | 全局能耗选项 schema |
|
|
||||||
| `tjwater-cli component option get --kind energy` | `GET /getenergyproperties/` | 全局能耗选项属性 |
|
|
||||||
| `tjwater-cli component option schema --kind pump-energy` | `GET /getpumpenergyschema/` | 泵能耗选项 schema |
|
|
||||||
| `tjwater-cli component option get --kind pump-energy --pump PUMP` | `GET /getpumpenergyproperties//` | 指定泵的能耗选项属性 |
|
|
||||||
| `tjwater-cli component option schema --kind network` | `GET /getoptionschema/` | 管网选项 schema |
|
|
||||||
| `tjwater-cli component option get --kind network` | `GET /getoptionproperties/` | 管网选项属性 |
|
|
||||||
|
|
||||||
暂不暴露:
|
|
||||||
|
|
||||||
```text
|
|
||||||
POST /addcurve/
|
|
||||||
POST /setcurveproperties/
|
|
||||||
POST /deletecurve/
|
|
||||||
POST /addpattern/
|
|
||||||
POST /setpatternproperties/
|
|
||||||
POST /deletepattern/
|
|
||||||
POST /settimeproperties/
|
|
||||||
POST /setenergyproperties/
|
|
||||||
GET /setpumpenergyproperties//
|
|
||||||
POST /setoptionproperties/
|
|
||||||
POST /setcontrolproperties/
|
|
||||||
POST /setruleproperties/
|
|
||||||
POST /setqualityproperties/
|
|
||||||
POST /setemitterproperties/
|
|
||||||
POST /setsource/
|
|
||||||
POST /addsource/
|
|
||||||
POST /deletesource/
|
|
||||||
POST /setreaction/
|
|
||||||
POST /setpipereaction/
|
|
||||||
POST /settankreaction/
|
|
||||||
POST /setmixing/
|
|
||||||
POST /addmixing/
|
|
||||||
POST /deletemixing/
|
|
||||||
POST /setvertexproperties/
|
|
||||||
POST /addvertex/
|
|
||||||
POST /deletevertex/
|
|
||||||
POST /setlabelproperties/
|
|
||||||
POST /addlabel/
|
|
||||||
POST /deletelabel/
|
|
||||||
POST /setbackdropproperties/
|
|
||||||
```
|
|
||||||
|
|
||||||
备注:
|
|
||||||
|
|
||||||
- `options` 当前实际只读接口分为 4 组:`time`、`energy`、`pump-energy`、`network`。
|
|
||||||
- `pump-energy` 是唯一需要额外资源标识的读取接口,必须带 `--pump PUMP`。
|
|
||||||
- 后端现有路径 `GET /getpumpenergyproperties//` 和 `GET /setpumpenergyproperties//` 存在双斜杠 / 方法异常,CLI 不继承这些路径细节,只保留语义化命令。
|
|
||||||
|
|
||||||
### Simulation / Analysis / Risk
|
|
||||||
|
|
||||||
来源:
|
|
||||||
|
|
||||||
```text
|
|
||||||
app/api/v1/endpoints/simulation.py
|
|
||||||
app/api/v1/endpoints/leakage.py
|
|
||||||
app/api/v1/endpoints/burst_detection.py
|
|
||||||
app/api/v1/endpoints/burst_location.py
|
|
||||||
app/api/v1/endpoints/risk.py
|
|
||||||
```
|
|
||||||
|
|
||||||
| 命令 | 覆盖接口 | 说明 |
|
|
||||||
|---|---|---|
|
|
||||||
| `tjwater-cli simulation run --start-time RFC3339 --duration MINUTES` | `POST /simulations/run-by-date` | 按指定绝对开始时间触发当前 project 的实时模拟;`start-time` 必须显式带时区,结果写入服务端时序库,后续通过 `tjwater-cli data timeseries realtime *` 查询 |
|
|
||||||
| `tjwater-cli analysis burst --start-time TIME --duration SEC --scheme SCHEME --burst-file FILE` | `GET /burst-analysis` | 爆管分析;`FILE` 提供爆管点与流量列表,CLI 负责转换为 `burst_ID[]` / `burst_size[]` |
|
|
||||||
| `tjwater-cli analysis valve --mode close\|isolation --start-time TIME --valve VALVE [--scheme SCHEME]` | `GET /valve_close_analysis/`、`GET /valve-isolation-analysis` | 阀门分析;close 模式需要 `--scheme`,`--valve` 可重复 |
|
|
||||||
| `tjwater-cli analysis flushing --start-time TIME --valve-setting-file FILE --drainage-node NODE --flow FLOW --scheme SCHEME [--duration SEC]` | `GET /flushing-analysis` | 冲洗分析;`FILE` 提供阀门与开度列表,CLI 负责转换为 `valves[]` / `valves_k[]` |
|
|
||||||
| `tjwater-cli analysis age --start-time TIME --duration SEC` | `GET /age_analysis/` | 水龄分析 |
|
|
||||||
| `tjwater-cli analysis contaminant --start-time TIME --duration SEC --source-node NODE --concentration VALUE --scheme SCHEME [--pattern PATTERN]` | `GET /contaminant-simulation` | 污染物模拟 |
|
|
||||||
| `tjwater-cli analysis sensor-placement kmeans --count N` | `GET /pressuresensorplacementkmeans/` | 基于 kmeans 的传感器放置分析;不包含创建方案 |
|
|
||||||
| `tjwater-cli analysis leakage identify --scheme SCHEME --start-time TIME --end-time TIME` | `POST /leakage/identify/` | 漏损识别 |
|
|
||||||
| `tjwater-cli analysis leakage schemes list\|get` | `GET /leakage/schemes/`、`GET /leakage/schemes/{scheme_name}` | 漏损方案查询 |
|
|
||||||
| `tjwater-cli analysis burst-detection detect --scheme SCHEME --start-time TIME --end-time TIME` | `POST /burst-detection/detect/` | 爆管检测 |
|
|
||||||
| `tjwater-cli analysis burst-detection schemes list\|get` | `GET /burst-detection/schemes/`、`GET /burst-detection/schemes/{scheme_name}` | 爆管检测方案查询 |
|
|
||||||
| `tjwater-cli analysis burst-location locate --scheme SCHEME --start-time TIME --end-time TIME` | `POST /burst-location/locate/` | 爆管定位 |
|
|
||||||
| `tjwater-cli analysis burst-location schemes list\|get` | `GET /burst-location/schemes/`、`GET /burst-location/schemes/{scheme_name}` | 爆管定位方案查询 |
|
|
||||||
| `tjwater-cli analysis risk pipe-now --pipe PIPE` | `GET /getpiperiskprobabilitynow/` | 单条管道当前风险 |
|
|
||||||
| `tjwater-cli analysis risk pipe-history --pipe PIPE` | `GET /getpiperiskprobability/` | 单条管道历史风险 |
|
|
||||||
| `tjwater-cli analysis risk network` | `GET /getnetworkpiperiskprobabilitynow/`、`GET /getpiperiskprobabilitygeometries/` | 当前 project 全网风险 |
|
|
||||||
|
|
||||||
暂缓或暂不暴露:
|
|
||||||
|
|
||||||
```text
|
|
||||||
POST /network_project/
|
|
||||||
GET /runproject/
|
|
||||||
POST /network_update/
|
|
||||||
POST /project_management/
|
|
||||||
POST /sensorplacementscheme/create
|
|
||||||
POST /pump_failure/
|
|
||||||
POST /pressure_regulation/
|
|
||||||
POST /scheduling_analysis/
|
|
||||||
POST /daily_scheduling_analysis/
|
|
||||||
```
|
|
||||||
|
|
||||||
执行模型:
|
|
||||||
|
|
||||||
- 首批 CLI 统一按同步命令设计,避免引入额外的异步轮询协议。
|
|
||||||
- `simulation run` 不直接回传全量模拟结果;它负责触发服务端模拟,并返回执行摘要、时间窗口和后续查询提示。
|
|
||||||
- 当前 `simulations/run-by-date` 接口会从 `start_time` 指定的绝对时间开始,按 15 分钟步长运行直到达到 `duration`,结果持久化到服务端时序存储。
|
|
||||||
- `start_time` 必须显式带时区;CLI 推荐直接传 **UTC+8** 时间,服务端统一转换后执行和落库。CLI 文档与帮助信息需要把这条规则写成显式契约,不能把数据库存储时间直接暴露成用户输入语义。
|
|
||||||
- 模拟结果读取统一走 `tjwater-cli data timeseries realtime *`,而不是再单独设计 `simulation output`。
|
|
||||||
- `analysis` 相关命令首批也按同步请求处理;若后续服务端真的引入任务队列,再单独设计 `job` 类基础设施能力。
|
|
||||||
|
|
||||||
### Data
|
|
||||||
|
|
||||||
来源:
|
|
||||||
|
|
||||||
```text
|
|
||||||
app/api/v1/endpoints/timeseries/*.py
|
|
||||||
app/api/v1/endpoints/scada.py
|
|
||||||
app/api/v1/endpoints/schemes.py
|
|
||||||
app/api/v1/endpoints/extension.py
|
|
||||||
app/api/v1/endpoints/misc.py
|
|
||||||
app/api/v1/endpoints/project_data.py
|
|
||||||
```
|
|
||||||
|
|
||||||
| 命令 | 覆盖接口 | 说明 |
|
|
||||||
|---|---|---|
|
|
||||||
| `tjwater-cli data timeseries realtime links --start-time TIME --end-time TIME` | `GET /realtime/links` | 查询指定时间范围内的实时/模拟管道数据 |
|
|
||||||
| `tjwater-cli data timeseries realtime nodes --start-time TIME --end-time TIME` | `GET /realtime/nodes` | 查询指定时间范围内的实时/模拟节点数据 |
|
|
||||||
| `tjwater-cli data timeseries realtime simulation-by-id-time --id ID --type pipe\|junction --time TIME` | `GET /realtime/query/by-id-time` | 查询指定元素在指定时间点的模拟结果 |
|
|
||||||
| `tjwater-cli data timeseries realtime simulation-by-time-property --type pipe\|junction --time TIME --property PROPERTY` | `GET /realtime/query/by-time-property` | 查询指定时间点某类元素某属性的聚合模拟结果 |
|
|
||||||
| `tjwater-cli data timeseries scheme links --scheme SCHEME --start-time TIME --end-time TIME` | `GET /scheme/links`、`GET /scheme/links/{link_id}/field` | 方案管道数据 |
|
|
||||||
| `tjwater-cli data timeseries scheme node-field --node NODE --field FIELD` | `GET /scheme/nodes/{node_id}/field` | 方案节点字段 |
|
|
||||||
| `tjwater-cli data timeseries scheme simulation --query by-id-time\|by-scheme-time-property --scheme SCHEME --id ID --time TIME --property PROPERTY` | `GET /scheme/query/*` | 方案模拟查询 |
|
|
||||||
| `tjwater-cli data timeseries scada query --device-id ID --start-time TIME --end-time TIME [--device-id ID ...] [--field FIELD]` | `GET /scada/by-ids-time-range`、`GET /scada/by-ids-field-time-range` | SCADA 时序;CLI 把重复 `--device-id` 转换为后端逗号分隔参数 |
|
|
||||||
| `tjwater-cli data timeseries composite --kind scada-simulation\|element-simulation\|element-scada --feature FEATURE --start-time TIME --end-time TIME` | `GET /composite/*` | 复合查询,`--feature` 可重复 |
|
|
||||||
| `tjwater-cli data timeseries composite pipeline-health --pipe PIPE --start-time TIME --end-time TIME` | `GET /composite/pipeline-health-prediction` | 管道健康预测 |
|
|
||||||
| `tjwater-cli data scada get\|list --kind info` | `GET /getscadainfo/`、`GET /getallscadainfo/` | `SCADA info` 元数据 |
|
|
||||||
| `tjwater-cli data scheme schema\|get\|list` | `schemes.py` 下 `GET` 接口 | 当前 project 方案查询 |
|
|
||||||
|
|
||||||
- `realtime` 是首批 simulation 结果的主读取域;CLI 可以按任务语义组合 `links`、`nodes`、`simulation-by-id-time`、`simulation-by-time-property`,但底层数据源仍以 `realtime.py` 为准。
|
|
||||||
- `realtime`、`scheme`、`composite` 等时间查询命令面向用户时仍按 **UTC+8** 输入;CLI/服务端负责转换为后端使用的 **UTC0** 条件进行检索。若返回结果直接包含时间戳,必须显式带时区,避免把存储时间和展示时间混淆。
|
|
||||||
|
|
||||||
暂不暴露:
|
|
||||||
|
|
||||||
```text
|
|
||||||
POST /realtime/*/batch
|
|
||||||
DELETE /realtime/*
|
|
||||||
PATCH /realtime/*
|
|
||||||
POST /realtime/simulation/store
|
|
||||||
POST /scheme/*/batch
|
|
||||||
PATCH /scheme/*
|
|
||||||
DELETE /scheme/*
|
|
||||||
POST /scheme/simulation/store
|
|
||||||
POST /scada/batch
|
|
||||||
PATCH /scada/{device_id}/field
|
|
||||||
DELETE /scada/by-id-time-range
|
|
||||||
POST /composite/clean-scada
|
|
||||||
POST /setscadadevice/
|
|
||||||
POST /addscadadevice/
|
|
||||||
POST /deletescadadevice/
|
|
||||||
POST /cleanscadadevice/
|
|
||||||
POST /setscadadevicedata/
|
|
||||||
POST /addscadadevicedata/
|
|
||||||
POST /deletescadadevicedata/
|
|
||||||
POST /cleanscadadevicedata/
|
|
||||||
POST /setscadaelement/
|
|
||||||
POST /addscadaelement/
|
|
||||||
POST /deletescadaelement/
|
|
||||||
POST /cleanscadaelement/
|
|
||||||
POST /setextensiondata/
|
|
||||||
POST /test_dict/
|
|
||||||
GET /getjson/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 不纳入首批 CLI 的运维接口
|
|
||||||
|
|
||||||
来源:
|
|
||||||
|
|
||||||
```text
|
|
||||||
app/api/v1/endpoints/snapshots.py
|
|
||||||
app/api/v1/endpoints/cache.py
|
|
||||||
app/api/v1/endpoints/audit.py
|
|
||||||
app/api/v1/endpoints/users.py
|
|
||||||
```
|
|
||||||
|
|
||||||
这些接口不纳入首批 Agent CLI。原因是它们更偏运维、审计或状态回滚,不属于 Agent 面向水务业务分析的核心调用范围。
|
|
||||||
|
|
||||||
暂不暴露:
|
|
||||||
|
|
||||||
```text
|
|
||||||
GET /getcurrentoperationid/
|
|
||||||
GET /getsnapshots/
|
|
||||||
GET /havesnapshot/
|
|
||||||
GET /havesnapshotforoperation/
|
|
||||||
GET /havesnapshotforcurrentoperation/
|
|
||||||
GET /getrestoreoperation/
|
|
||||||
POST /undo/
|
|
||||||
POST /redo/
|
|
||||||
POST /takesnapshot*/
|
|
||||||
POST /picksnapshot/
|
|
||||||
POST /pickoperation/
|
|
||||||
GET /syncwithserver/
|
|
||||||
POST /batch/
|
|
||||||
POST /compressedbatch/
|
|
||||||
POST /setrestoreoperation/
|
|
||||||
GET /queryredis/
|
|
||||||
POST /clearrediskey/
|
|
||||||
POST /clearrediskeys/
|
|
||||||
POST /clearallredis/
|
|
||||||
GET /audit/logs
|
|
||||||
GET /audit/logs/my
|
|
||||||
GET /audit/logs/count
|
|
||||||
GET /getuserschema/
|
|
||||||
GET /getuser/
|
|
||||||
GET /getallusers/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Help
|
|
||||||
|
|
||||||
`help` 不直接对应现有 endpoint,但建议作为 Agent CLI 的基础设施。能力发现更适合复用 CLI 的 `help` 语义,而不是新增一个偏内部化的 `capability` 顶层命令。
|
|
||||||
|
|
||||||
| 命令 | 说明 |
|
|
||||||
|---|---|
|
|
||||||
| `tjwater-cli help` | 返回当前 CLI 能力清单,供 Agent 发现可用命令 |
|
|
||||||
| `tjwater-cli help COMMAND` | 返回某个命令的参数、输出、示例和推荐后续命令 |
|
|
||||||
|
|
||||||
输出补充约束:
|
|
||||||
|
|
||||||
- 首批 CLI 不再设计通用 `result_ref` / `--out-ref` 机制。
|
|
||||||
- 若某业务命令确实需要落本地文件,应由所属命令显式提供 `--output PATH`。
|
|
||||||
- 若后续出现超大结果集、必须脱离 stdout 传输时,再单独设计结果引用机制,而不是在首批 CLI 中预埋未闭环能力。
|
|
||||||
|
|
||||||
## 输出规范
|
|
||||||
|
|
||||||
进程级契约:
|
|
||||||
|
|
||||||
- `stdout`:默认只输出一个 JSON 对象,供 agent / 脚本稳定解析。
|
|
||||||
- `stderr`:输出进度、警告和诊断信息;不得混入结构化结果 JSON。
|
|
||||||
- 退出码必须稳定,不能简单透传底层 HTTP status。
|
|
||||||
|
|
||||||
建议退出码:
|
|
||||||
|
|
||||||
| 退出码 | 含义 |
|
|
||||||
|---|---|
|
|
||||||
| `0` | 成功 |
|
|
||||||
| `2` | CLI 参数错误 / 用法错误 |
|
|
||||||
| `3` | 认证失败 |
|
|
||||||
| `4` | 权限不足 |
|
|
||||||
| `5` | 资源不存在 |
|
|
||||||
| `6` | 冲突、前置条件不满足或非法状态 |
|
|
||||||
| `7` | 服务端错误 |
|
|
||||||
|
|
||||||
成功:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"ok": true,
|
|
||||||
"schema_version": "tjwater-cli/v1",
|
|
||||||
"summary": "读取成功",
|
|
||||||
"data": {},
|
|
||||||
"metadata": {},
|
|
||||||
"next_commands": []
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
失败:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"ok": false,
|
|
||||||
"schema_version": "tjwater-cli/v1",
|
|
||||||
"summary": "认证失败",
|
|
||||||
"error": {
|
|
||||||
"code": "UNAUTHENTICATED",
|
|
||||||
"message": "missing access token for agent context",
|
|
||||||
"retryable": false
|
|
||||||
},
|
|
||||||
"data": null,
|
|
||||||
"metadata": {},
|
|
||||||
"next_commands": [
|
|
||||||
"tjwater-cli <command> --auth-context /path/to/auth-context.json"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
补充约束:
|
|
||||||
|
|
||||||
- `metadata` 至少建议包含:`request_id`、`server`、`duration_ms`、`generated_at`。
|
|
||||||
- `next_commands` 是面向 agent 的推荐后续动作,不影响退出码和主结果语义。
|
|
||||||
- 所有 `help` 输出也应带 `schema_version`,便于 agent 做能力协商。
|
|
||||||
|
|
||||||
## 后续开放条件
|
|
||||||
|
|
||||||
如后续要开放写操作,需要单独设计:
|
|
||||||
|
|
||||||
- 权限校验
|
|
||||||
- dry-run / preview
|
|
||||||
- 显式确认机制
|
|
||||||
- 审计日志
|
|
||||||
- 变更快照
|
|
||||||
- 回滚策略
|
|
||||||
- Agent 可读的错误恢复建议
|
|
||||||
-105205
File diff suppressed because it is too large
Load Diff
-26498
File diff suppressed because it is too large
Load Diff
-30973
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
-111322
File diff suppressed because it is too large
Load Diff
-328198
File diff suppressed because it is too large
Load Diff
@@ -1,213 +0,0 @@
|
|||||||
[TITLE]
|
|
||||||
Hanoi example by Fujiwara and Khang, Water Resources Research, 1990
|
|
||||||
|
|
||||||
[JUNCTIONS]
|
|
||||||
;ID Elev Demand Pattern
|
|
||||||
2 0 890 ;
|
|
||||||
3 0 850 ;
|
|
||||||
4 0 130 ;
|
|
||||||
5 0 725 ;
|
|
||||||
6 0 1005 ;
|
|
||||||
7 0 1350 ;
|
|
||||||
8 0 550 ;
|
|
||||||
9 0 525 ;
|
|
||||||
10 0 525 ;
|
|
||||||
11 0 500 ;
|
|
||||||
12 0 560 ;
|
|
||||||
13 0 940 ;
|
|
||||||
14 0 615 ;
|
|
||||||
15 0 280 ;
|
|
||||||
16 0 310 ;
|
|
||||||
17 0 865 ;
|
|
||||||
18 0 1345 ;
|
|
||||||
19 0 60 ;
|
|
||||||
20 0 1275 ;
|
|
||||||
21 0 930 ;
|
|
||||||
22 0 485 ;
|
|
||||||
23 0 1045 ;
|
|
||||||
24 0 820 ;
|
|
||||||
25 0 170 ;
|
|
||||||
26 0 900 ;
|
|
||||||
27 0 370 ;
|
|
||||||
28 0 290 ;
|
|
||||||
29 0 360 ;
|
|
||||||
30 0 360 ;
|
|
||||||
31 0 105 ;
|
|
||||||
32 0 805 ;
|
|
||||||
|
|
||||||
[RESERVOIRS]
|
|
||||||
;ID Head Pattern
|
|
||||||
1 100.0 ;
|
|
||||||
|
|
||||||
[TANKS]
|
|
||||||
;ID Elevation InitLevel MinLevel MaxLevel Diameter MinVol VolCurve
|
|
||||||
|
|
||||||
[PIPES]
|
|
||||||
;ID Node1 Node2 Length Diameter Roughness MinorLoss Status
|
|
||||||
1 1 2 100 0.0001 130 0 open ;
|
|
||||||
2 2 3 1350 0.0001 130 0 open ;
|
|
||||||
3 3 4 900 0.0001 130 0 open ;
|
|
||||||
4 4 5 1150 0.0001 130 0 open ;
|
|
||||||
5 5 6 1450 0.0001 130 0 open ;
|
|
||||||
6 6 7 450 0.0001 130 0 open ;
|
|
||||||
7 7 8 850 0.0001 130 0 open ;
|
|
||||||
8 8 9 850 0.0001 130 0 open ;
|
|
||||||
9 9 10 800 0.0001 130 0 open ;
|
|
||||||
10 10 11 950 0.0001 130 0 open ;
|
|
||||||
11 11 12 1200 0.0001 130 0 open ;
|
|
||||||
12 12 13 3500 0.0001 130 0 open ;
|
|
||||||
13 10 14 800 0.0001 130 0 open ;
|
|
||||||
14 14 15 500 0.0001 130 0 open ;
|
|
||||||
15 15 16 550 0.0001 130 0 open ;
|
|
||||||
16 17 16 2730 0.0001 130 0 open ;
|
|
||||||
17 18 17 1750 0.0001 130 0 open ;
|
|
||||||
18 19 18 800 0.0001 130 0 open ;
|
|
||||||
19 3 19 400 0.0001 130 0 open ;
|
|
||||||
20 3 20 2200 0.0001 130 0 open ;
|
|
||||||
21 20 21 1500 0.0001 130 0 open ;
|
|
||||||
22 21 22 500 0.0001 130 0 open ;
|
|
||||||
23 20 23 2650 0.0001 130 0 open ;
|
|
||||||
24 23 24 1230 0.0001 130 0 open ;
|
|
||||||
25 24 25 1300 0.0001 130 0 open ;
|
|
||||||
26 26 25 850 0.0001 130 0 open ;
|
|
||||||
27 27 26 300 0.0001 130 0 open ;
|
|
||||||
28 16 27 750 0.0001 130 0 open ;
|
|
||||||
29 23 28 1500 0.0001 130 0 open ;
|
|
||||||
30 28 29 2000 0.0001 130 0 open ;
|
|
||||||
31 29 30 1600 0.0001 130 0 open ;
|
|
||||||
32 30 31 150 0.0001 130 0 open ;
|
|
||||||
33 32 31 860 0.0001 130 0 open ;
|
|
||||||
34 25 32 950 0.0001 130 0 open ;
|
|
||||||
|
|
||||||
[PUMPS]
|
|
||||||
;ID Node1 Node2 Parameters
|
|
||||||
|
|
||||||
[VALVES]
|
|
||||||
;ID Node1 Node2 Diameter Type Setting MinorLoss
|
|
||||||
|
|
||||||
[TAGS]
|
|
||||||
|
|
||||||
[DEMANDS]
|
|
||||||
;Junction Demand Pattern Category
|
|
||||||
|
|
||||||
[STATUS]
|
|
||||||
;ID Status/Setting
|
|
||||||
|
|
||||||
[PATTERNS]
|
|
||||||
;ID Multipliers
|
|
||||||
|
|
||||||
[CURVES]
|
|
||||||
;ID X-Value Y-Value
|
|
||||||
|
|
||||||
[CONTROLS]
|
|
||||||
|
|
||||||
[RULES]
|
|
||||||
|
|
||||||
[ENERGY]
|
|
||||||
Global Efficiency 75
|
|
||||||
Global Price 0
|
|
||||||
Demand Charge 0
|
|
||||||
|
|
||||||
[EMITTERS]
|
|
||||||
;Junction Coefficient
|
|
||||||
|
|
||||||
[QUALITY]
|
|
||||||
;Node InitQual
|
|
||||||
|
|
||||||
[SOURCES]
|
|
||||||
;Node Type Quality Pattern
|
|
||||||
|
|
||||||
[REACTIONS]
|
|
||||||
;Type Pipe/Tank Coefficient
|
|
||||||
|
|
||||||
|
|
||||||
[REACTIONS]
|
|
||||||
Order Bulk 1
|
|
||||||
Order Wall 1
|
|
||||||
Global Bulk 0
|
|
||||||
Global Wall 0
|
|
||||||
Limiting Potential 0
|
|
||||||
Roughness Correlation 0
|
|
||||||
|
|
||||||
[MIXING]
|
|
||||||
;Tank Model
|
|
||||||
|
|
||||||
[TIMES]
|
|
||||||
Duration 0
|
|
||||||
Hydraulic Timestep 1:00
|
|
||||||
Quality Timestep 0:05
|
|
||||||
Pattern Timestep 1:00
|
|
||||||
Pattern Start 0:00
|
|
||||||
Report Timestep 1:00
|
|
||||||
Report Start 0:00
|
|
||||||
Start ClockTime 12 am
|
|
||||||
Statistic None
|
|
||||||
|
|
||||||
[REPORT]
|
|
||||||
Status No
|
|
||||||
Summary No
|
|
||||||
Page 0
|
|
||||||
|
|
||||||
[OPTIONS]
|
|
||||||
Units CMH
|
|
||||||
Headloss H-W
|
|
||||||
Specific Gravity 1
|
|
||||||
Viscosity 1
|
|
||||||
Trials 40
|
|
||||||
Accuracy 0.001
|
|
||||||
Unbalanced Continue 10
|
|
||||||
Pattern 1
|
|
||||||
Demand Multiplier 1.0
|
|
||||||
Emitter Exponent 0.5
|
|
||||||
Quality NONE mg/L
|
|
||||||
Diffusivity 1
|
|
||||||
Tolerance 0.01
|
|
||||||
|
|
||||||
[COORDINATES]
|
|
||||||
;Node X-Coord Y-Coord
|
|
||||||
2 5021.20 1582.17
|
|
||||||
3 5025.20 2585.42
|
|
||||||
4 5874.22 2588.30
|
|
||||||
5 6873.11 2588.30
|
|
||||||
6 8103.51 2585.42
|
|
||||||
7 8103.51 3234.67
|
|
||||||
8 8106.66 4179.28
|
|
||||||
9 8106.66 5133.78
|
|
||||||
10 7318.64 5133.78
|
|
||||||
11 7319.94 5831.65
|
|
||||||
12 7319.94 6671.19
|
|
||||||
13 5636.76 6676.24
|
|
||||||
14 6530.63 5133.78
|
|
||||||
15 5676.02 5133.78
|
|
||||||
16 5021.20 5133.78
|
|
||||||
17 5021.20 4412.36
|
|
||||||
18 5021.20 3868.52
|
|
||||||
19 5021.20 3191.49
|
|
||||||
20 3587.87 2588.30
|
|
||||||
21 3587.87 1300.84
|
|
||||||
22 3587.87 901.29
|
|
||||||
23 1978.55 2588.30
|
|
||||||
24 1975.58 4084.35
|
|
||||||
25 1980.46 5137.63
|
|
||||||
26 3077.46 5137.63
|
|
||||||
27 3933.52 5133.78
|
|
||||||
28 846.04 2588.20
|
|
||||||
29 -552.41 2588.20
|
|
||||||
30 -552.38 4369.06
|
|
||||||
31 -549.36 5137.63
|
|
||||||
32 536.45 5137.63
|
|
||||||
1 5360.71 1354.05
|
|
||||||
|
|
||||||
[VERTICES]
|
|
||||||
;Link X-Coord Y-Coord
|
|
||||||
|
|
||||||
[LABELS]
|
|
||||||
;X-Coord Y-Coord Label & Anchor Node
|
|
||||||
|
|
||||||
[BACKDROP]
|
|
||||||
DIMENSIONS -985.92 612.54 8551.27 6964.99
|
|
||||||
UNITS None
|
|
||||||
FILE
|
|
||||||
OFFSET 0.00 0.00
|
|
||||||
|
|
||||||
[END]
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
[TITLE]
|
|
||||||
|
|
||||||
|
|
||||||
[JUNCTIONS]
|
|
||||||
;ID Elev Demand Pattern
|
|
||||||
1 0 0 ;
|
|
||||||
2 0 0 ;
|
|
||||||
3 0 0 ;
|
|
||||||
4 0 0 ;
|
|
||||||
5 0 0 ;
|
|
||||||
|
|
||||||
[RESERVOIRS]
|
|
||||||
;ID Head Pattern
|
|
||||||
|
|
||||||
[TANKS]
|
|
||||||
;ID Elevation InitLevel MinLevel MaxLevel Diameter MinVol VolCurve
|
|
||||||
|
|
||||||
[PIPES]
|
|
||||||
;ID Node1 Node2 Length Diameter Roughness MinorLoss Status
|
|
||||||
1 3 2 1000 12 100 0 Open ;
|
|
||||||
|
|
||||||
[PUMPS]
|
|
||||||
;ID Node1 Node2 Parameters
|
|
||||||
|
|
||||||
[VALVES]
|
|
||||||
;ID Node1 Node2 Diameter Type Setting MinorLoss
|
|
||||||
|
|
||||||
[TAGS]
|
|
||||||
|
|
||||||
[DEMANDS]
|
|
||||||
;Junction Demand Pattern Category
|
|
||||||
|
|
||||||
[STATUS]
|
|
||||||
;ID Status/Setting
|
|
||||||
|
|
||||||
[PATTERNS]
|
|
||||||
;ID Multipliers
|
|
||||||
|
|
||||||
[CURVES]
|
|
||||||
;ID X-Value Y-Value
|
|
||||||
|
|
||||||
[CONTROLS]
|
|
||||||
|
|
||||||
|
|
||||||
[RULES]
|
|
||||||
|
|
||||||
|
|
||||||
[ENERGY]
|
|
||||||
Global Efficiency 75
|
|
||||||
Global Price 0
|
|
||||||
Demand Charge 0
|
|
||||||
|
|
||||||
[EMITTERS]
|
|
||||||
;Junction Coefficient
|
|
||||||
|
|
||||||
[QUALITY]
|
|
||||||
;Node InitQual
|
|
||||||
|
|
||||||
[SOURCES]
|
|
||||||
;Node Type Quality Pattern
|
|
||||||
|
|
||||||
[REACTIONS]
|
|
||||||
;Type Pipe/Tank Coefficient
|
|
||||||
|
|
||||||
|
|
||||||
[REACTIONS]
|
|
||||||
Order Bulk 1
|
|
||||||
Order Tank 1
|
|
||||||
Order Wall 1
|
|
||||||
Global Bulk 0
|
|
||||||
Global Wall 0
|
|
||||||
Limiting Potential 0
|
|
||||||
Roughness Correlation 0
|
|
||||||
|
|
||||||
[MIXING]
|
|
||||||
;Tank Model
|
|
||||||
|
|
||||||
[TIMES]
|
|
||||||
Duration 0:00
|
|
||||||
Hydraulic Timestep 1:00
|
|
||||||
Quality Timestep 0:05
|
|
||||||
Pattern Timestep 1:00
|
|
||||||
Pattern Start 0:00
|
|
||||||
Report Timestep 1:00
|
|
||||||
Report Start 0:00
|
|
||||||
Start ClockTime 12 am
|
|
||||||
Statistic NONE
|
|
||||||
|
|
||||||
[REPORT]
|
|
||||||
Status No
|
|
||||||
Summary No
|
|
||||||
Page 0
|
|
||||||
|
|
||||||
[OPTIONS]
|
|
||||||
Units GPM
|
|
||||||
Headloss H-W
|
|
||||||
Specific Gravity 1
|
|
||||||
Viscosity 1
|
|
||||||
Trials 40
|
|
||||||
Accuracy 0.001
|
|
||||||
CHECKFREQ 2
|
|
||||||
MAXCHECK 10
|
|
||||||
DAMPLIMIT 0
|
|
||||||
Unbalanced Continue 10
|
|
||||||
Pattern 1
|
|
||||||
Demand Multiplier 1.0
|
|
||||||
Emitter Exponent 0.5
|
|
||||||
Quality None mg/L
|
|
||||||
Diffusivity 1
|
|
||||||
Tolerance 0.01
|
|
||||||
|
|
||||||
[COORDINATES]
|
|
||||||
;Node X-Coord Y-Coord
|
|
||||||
1 455.97 6698.11
|
|
||||||
2 6022.01 3616.35
|
|
||||||
3 7374.21 6509.43
|
|
||||||
4 3128.93 5786.16
|
|
||||||
5 2122.64 2358.49
|
|
||||||
|
|
||||||
[VERTICES]
|
|
||||||
;Link X-Coord Y-Coord
|
|
||||||
|
|
||||||
[LABELS]
|
|
||||||
;X-Coord Y-Coord Label & Anchor Node
|
|
||||||
|
|
||||||
[BACKDROP]
|
|
||||||
DIMENSIONS 0.00 0.00 10000.00 10000.00
|
|
||||||
UNITS None
|
|
||||||
FILE
|
|
||||||
OFFSET 0.00 0.00
|
|
||||||
|
|
||||||
[END]
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
[TITLE]
|
|
||||||
|
|
||||||
|
|
||||||
[JUNCTIONS]
|
|
||||||
;ID Elev Demand Pattern
|
|
||||||
N23 0 0 ;
|
|
||||||
N1 18 5 ;
|
|
||||||
N2 18 10 ;
|
|
||||||
N3 14 0 ;
|
|
||||||
N4 12 5 ;
|
|
||||||
N5 14 30 ;
|
|
||||||
N24 0 0 ;
|
|
||||||
N25 0 0 ;
|
|
||||||
N14 20 5 ;
|
|
||||||
N13 23 0 ;
|
|
||||||
N16 10 0 ;
|
|
||||||
N17 7 0 ;
|
|
||||||
N19 10 5 ;
|
|
||||||
N20 7 0 ;
|
|
||||||
N21 10 0 ;
|
|
||||||
N22 15 20 ;
|
|
||||||
N18 8 5 ;
|
|
||||||
N6 15 10 ;
|
|
||||||
N7 14.5 0 ;
|
|
||||||
N8 14 20 ;
|
|
||||||
N9 14 0 ;
|
|
||||||
N10 15 5 ;
|
|
||||||
N11 12 10 ;
|
|
||||||
N12 15 0 ;
|
|
||||||
N15 8 20 ;
|
|
||||||
|
|
||||||
[RESERVOIRS]
|
|
||||||
;ID Head Pattern
|
|
||||||
|
|
||||||
[TANKS]
|
|
||||||
;ID Elevation InitLevel MinLevel MaxLevel Diameter MinVol VolCurve
|
|
||||||
1 0 54.66 54.5 56 1000000 0 ;
|
|
||||||
2 0 54.60 54.5 55.5 1000000 0 ;
|
|
||||||
3 0 54.50 54 55.5 1000000 0 ;
|
|
||||||
|
|
||||||
[PIPES]
|
|
||||||
;ID Node1 Node2 Length Diameter Roughness MinorLoss Status
|
|
||||||
P1 N23 N1 606 457 110 0 Open ;
|
|
||||||
P9 N1 N2 1930 457 110 0 Open ;
|
|
||||||
P10 N2 N3 5150 305 10 0 Open ;
|
|
||||||
P30 N3 N4 326 0.152 100 0 Open ;
|
|
||||||
P31 N4 N5 844 229 110 0 Open ;
|
|
||||||
P35 N5 N22 1408 152 100 0 Open ;
|
|
||||||
P36 N5 N7 500 381 110 0 Open ;
|
|
||||||
P34 N7 N6 615 381 110 0 Open ;
|
|
||||||
P32 N6 N3 1274 152 100 0 Open ;
|
|
||||||
P27 N6 N8 743 381 110 0 Open ;
|
|
||||||
P26 N8 N9 443 229 90 0 Open ;
|
|
||||||
P37 N9 N6 300 229 90 0 Open ;
|
|
||||||
P25 N8 N10 249 305 105 0 Open ;
|
|
||||||
P5 N24 N10 3383 305 100 0 Open ;
|
|
||||||
P2 N23 N24 454 457 110 0 Open ;
|
|
||||||
P3 N24 N14 2782 229 105 0 Open ;
|
|
||||||
P4 N14 N25 304 381 135 0 Open ;
|
|
||||||
P6 N24 N13 1767 475 110 0 Open ;
|
|
||||||
P7 N13 N14 1014 381 135 0 Open ;
|
|
||||||
P23 N10 N11 542 229 90 0 Open ;
|
|
||||||
P22 N11 N12 777 229 90 0 Open ;
|
|
||||||
P24 N8 N12 1600 457 110 0 Open ;
|
|
||||||
P8 N25 N16 1014 381 6 0 Open ;
|
|
||||||
P13 N16 N17 822 305 140 0 Open ;
|
|
||||||
P16 N17 N19 1072 229 90 0 Open ;
|
|
||||||
P17 N19 N20 864 152 90 0 Open ;
|
|
||||||
P18 N20 N21 711 152 90 0 Open ;
|
|
||||||
P14 N17 N18 411 152 100 0 Open ;
|
|
||||||
P15 N20 N18 701 229 110 0 Open ;
|
|
||||||
P20 N15 N22 2334 152 100 0 Open ;
|
|
||||||
P19 N15 N21 832 152 90 0 Open ;
|
|
||||||
P12 N15 N16 914 229 125 0 Open ;
|
|
||||||
0 1 N23 1 1000 110 0 Open ;
|
|
||||||
16 2 N24 1 1000 100 0 Open ;
|
|
||||||
20 3 N25 1 1000 100 0 Open ;
|
|
||||||
P11 N13 N12 762 457 110 0 Open ;
|
|
||||||
P21 N12 N15 1996 0.229 95 0 Open ;
|
|
||||||
P28 N8 N22 931 229 125 0 Open ;
|
|
||||||
P29 N21 N22 2689 152 100 0 Open ;
|
|
||||||
P33 N5 N6 1115 229 90 0 Open ;
|
|
||||||
|
|
||||||
[PUMPS]
|
|
||||||
;ID Node1 Node2 Parameters
|
|
||||||
|
|
||||||
[VALVES]
|
|
||||||
;ID Node1 Node2 Diameter Type Setting MinorLoss
|
|
||||||
|
|
||||||
[TAGS]
|
|
||||||
|
|
||||||
[DEMANDS]
|
|
||||||
;Junction Demand Pattern Category
|
|
||||||
|
|
||||||
[STATUS]
|
|
||||||
;ID Status/Setting
|
|
||||||
|
|
||||||
[PATTERNS]
|
|
||||||
;ID Multipliers
|
|
||||||
|
|
||||||
[CURVES]
|
|
||||||
;ID X-Value Y-Value
|
|
||||||
|
|
||||||
[CONTROLS]
|
|
||||||
|
|
||||||
[RULES]
|
|
||||||
|
|
||||||
[ENERGY]
|
|
||||||
Global Efficiency 75
|
|
||||||
Global Price 0
|
|
||||||
Demand Charge 0
|
|
||||||
|
|
||||||
[EMITTERS]
|
|
||||||
;Junction Coefficient
|
|
||||||
|
|
||||||
[QUALITY]
|
|
||||||
;Node InitQual
|
|
||||||
|
|
||||||
[SOURCES]
|
|
||||||
;Node Type Quality Pattern
|
|
||||||
|
|
||||||
[REACTIONS]
|
|
||||||
;Type Pipe/Tank Coefficient
|
|
||||||
|
|
||||||
|
|
||||||
[REACTIONS]
|
|
||||||
Order Bulk 1
|
|
||||||
Order Tank 1
|
|
||||||
Order Wall 1
|
|
||||||
Global Bulk 0
|
|
||||||
Global Wall 0
|
|
||||||
Limiting Potential 0
|
|
||||||
Roughness Correlation 0
|
|
||||||
|
|
||||||
[MIXING]
|
|
||||||
;Tank Model
|
|
||||||
|
|
||||||
[TIMES]
|
|
||||||
Duration 0
|
|
||||||
Hydraulic Timestep 1:00
|
|
||||||
Quality Timestep 0:05
|
|
||||||
Pattern Timestep 1:00
|
|
||||||
Pattern Start 0:00
|
|
||||||
Report Timestep 1:00
|
|
||||||
Report Start 0:00
|
|
||||||
Start ClockTime 12 am
|
|
||||||
Statistic None
|
|
||||||
|
|
||||||
[REPORT]
|
|
||||||
Status No
|
|
||||||
Summary No
|
|
||||||
Page 0
|
|
||||||
|
|
||||||
[OPTIONS]
|
|
||||||
Units LPS
|
|
||||||
Headloss H-W
|
|
||||||
Specific Gravity 1
|
|
||||||
Viscosity 1
|
|
||||||
Trials 40
|
|
||||||
Accuracy 0.001
|
|
||||||
Unbalanced Continue 10
|
|
||||||
Pattern 1
|
|
||||||
Demand Multiplier 1.0
|
|
||||||
Emitter Exponent 0.5
|
|
||||||
Quality None mg/L
|
|
||||||
Diffusivity 1
|
|
||||||
Tolerance 0.01
|
|
||||||
|
|
||||||
[COORDINATES]
|
|
||||||
;Node X-Coord Y-Coord
|
|
||||||
N23 2146.34 8439.02
|
|
||||||
N1 3268.29 8471.54
|
|
||||||
N2 4585.37 8455.28
|
|
||||||
N3 6081.30 8422.76
|
|
||||||
N4 7121.95 8504.07
|
|
||||||
N5 8260.16 8520.33
|
|
||||||
N24 2048.78 6536.59
|
|
||||||
N25 1918.70 3821.14
|
|
||||||
N14 2048.78 4796.75
|
|
||||||
N13 2845.53 5235.77
|
|
||||||
N16 3154.47 3739.84
|
|
||||||
N17 4455.28 3804.88
|
|
||||||
N19 5560.98 3837.40
|
|
||||||
N20 6601.63 3788.62
|
|
||||||
N21 7804.88 3788.62
|
|
||||||
N22 9203.25 3788.62
|
|
||||||
N18 6162.60 3317.07
|
|
||||||
N6 6845.53 7268.29
|
|
||||||
N7 7918.70 7235.77
|
|
||||||
N8 6357.72 6552.85
|
|
||||||
N9 6975.61 6552.85
|
|
||||||
N10 4065.04 6439.02
|
|
||||||
N11 4048.78 5723.58
|
|
||||||
N12 5056.91 5040.65
|
|
||||||
N15 5073.17 4552.85
|
|
||||||
1 1674.80 8520.33
|
|
||||||
2 1170.73 6536.59
|
|
||||||
3 1170.73 4016.26
|
|
||||||
|
|
||||||
[VERTICES]
|
|
||||||
;Link X-Coord Y-Coord
|
|
||||||
16 1170.73 6308.94
|
|
||||||
|
|
||||||
[LABELS]
|
|
||||||
;X-Coord Y-Coord Label & Anchor Node
|
|
||||||
|
|
||||||
[BACKDROP]
|
|
||||||
DIMENSIONS 0.00 0.00 10000.00 10000.00
|
|
||||||
UNITS None
|
|
||||||
FILE
|
|
||||||
OFFSET 0.00 0.00
|
|
||||||
|
|
||||||
[END]
|
|
||||||
-14197
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,178 +0,0 @@
|
|||||||
[TITLE]
|
|
||||||
EPANET Example Network 1
|
|
||||||
A simple example of modeling chlorine decay. Both bulk and
|
|
||||||
wall reactions are included.
|
|
||||||
|
|
||||||
[JUNCTIONS]
|
|
||||||
;ID Elev Demand Pattern
|
|
||||||
10 710 0 ;
|
|
||||||
11 710 150 ;
|
|
||||||
12 700 150 ;
|
|
||||||
13 695 100 ;
|
|
||||||
21 700 150 ;
|
|
||||||
22 695 200 ;
|
|
||||||
23 690 150 ;
|
|
||||||
31 700 100 ;
|
|
||||||
32 710 100 ;
|
|
||||||
|
|
||||||
[RESERVOIRS]
|
|
||||||
;ID Head Pattern
|
|
||||||
9 800 ;
|
|
||||||
|
|
||||||
[TANKS]
|
|
||||||
;ID Elevation InitLevel MinLevel MaxLevel Diameter MinVol VolCurve
|
|
||||||
2 850 120 100 150 50.5 0 ;
|
|
||||||
|
|
||||||
[PIPES]
|
|
||||||
;ID Node1 Node2 Length Diameter Roughness MinorLoss Status
|
|
||||||
10 10 11 10530 18 100 0 Open ;
|
|
||||||
11 11 12 5280 14 100 0 Open ;
|
|
||||||
12 12 13 5280 10 100 0 Open ;
|
|
||||||
21 21 22 5280 10 100 0 Open ;
|
|
||||||
22 22 23 5280 12 100 0 Open ;
|
|
||||||
31 31 32 5280 6 100 0 Open ;
|
|
||||||
110 2 12 200 18 100 0 Open ;
|
|
||||||
111 11 21 5280 10 100 0 Open ;
|
|
||||||
112 12 22 5280 12 100 0 Open ;
|
|
||||||
113 13 23 5280 8 100 0 Open ;
|
|
||||||
121 21 31 5280 8 100 0 Open ;
|
|
||||||
122 22 32 5280 6 100 0 Open ;
|
|
||||||
|
|
||||||
[PUMPS]
|
|
||||||
;ID Node1 Node2 Parameters
|
|
||||||
9 9 10 HEAD 1 ;
|
|
||||||
|
|
||||||
[VALVES]
|
|
||||||
;ID Node1 Node2 Diameter Type Setting MinorLoss
|
|
||||||
|
|
||||||
[TAGS]
|
|
||||||
|
|
||||||
[DEMANDS]
|
|
||||||
;Junction Demand Pattern Category
|
|
||||||
|
|
||||||
[STATUS]
|
|
||||||
;ID Status/Setting
|
|
||||||
|
|
||||||
[PATTERNS]
|
|
||||||
;ID Multipliers
|
|
||||||
;Demand Pattern
|
|
||||||
1 1.0 1.2 1.4 1.6 1.4 1.2
|
|
||||||
1 1.0 0.8 0.6 0.4 0.6 0.8
|
|
||||||
|
|
||||||
[CURVES]
|
|
||||||
;ID X-Value Y-Value
|
|
||||||
;PUMP: Pump Curve for Pump 9
|
|
||||||
1 1500 250
|
|
||||||
|
|
||||||
[CONTROLS]
|
|
||||||
LINK 9 OPEN IF NODE 2 BELOW 110
|
|
||||||
LINK 9 CLOSED IF NODE 2 ABOVE 140
|
|
||||||
|
|
||||||
|
|
||||||
[RULES]
|
|
||||||
|
|
||||||
[ENERGY]
|
|
||||||
Global Efficiency 75
|
|
||||||
Global Price 0.0
|
|
||||||
Demand Charge 0.0
|
|
||||||
|
|
||||||
[EMITTERS]
|
|
||||||
;Junction Coefficient
|
|
||||||
|
|
||||||
[QUALITY]
|
|
||||||
;Node InitQual
|
|
||||||
10 0.5
|
|
||||||
11 0.5
|
|
||||||
12 0.5
|
|
||||||
13 0.5
|
|
||||||
21 0.5
|
|
||||||
22 0.5
|
|
||||||
23 0.5
|
|
||||||
31 0.5
|
|
||||||
32 0.5
|
|
||||||
9 1.0
|
|
||||||
2 1.0
|
|
||||||
|
|
||||||
[SOURCES]
|
|
||||||
;Node Type Quality Pattern
|
|
||||||
|
|
||||||
[REACTIONS]
|
|
||||||
;Type Pipe/Tank Coefficient
|
|
||||||
|
|
||||||
|
|
||||||
[REACTIONS]
|
|
||||||
Order Bulk 1
|
|
||||||
Order Tank 1
|
|
||||||
Order Wall 1
|
|
||||||
Global Bulk -.5
|
|
||||||
Global Wall -1
|
|
||||||
Limiting Potential 0.0
|
|
||||||
Roughness Correlation 0.0
|
|
||||||
|
|
||||||
[MIXING]
|
|
||||||
;Tank Model
|
|
||||||
|
|
||||||
[TIMES]
|
|
||||||
Duration 24:00
|
|
||||||
Hydraulic Timestep 1:00
|
|
||||||
Quality Timestep 0:05
|
|
||||||
Pattern Timestep 2:00
|
|
||||||
Pattern Start 0:00
|
|
||||||
Report Timestep 1:00
|
|
||||||
Report Start 0:00
|
|
||||||
Start ClockTime 12 am
|
|
||||||
Statistic None
|
|
||||||
|
|
||||||
[REPORT]
|
|
||||||
Status Yes
|
|
||||||
Summary No
|
|
||||||
Page 0
|
|
||||||
|
|
||||||
[OPTIONS]
|
|
||||||
Units GPM
|
|
||||||
Headloss H-W
|
|
||||||
Specific Gravity 1.0
|
|
||||||
Viscosity 1.0
|
|
||||||
Trials 40
|
|
||||||
Accuracy 0.001
|
|
||||||
CHECKFREQ 2
|
|
||||||
MAXCHECK 10
|
|
||||||
DAMPLIMIT 0
|
|
||||||
Unbalanced Continue 10
|
|
||||||
Pattern 1
|
|
||||||
Demand Multiplier 1.0
|
|
||||||
Emitter Exponent 0.5
|
|
||||||
Quality Chlorine mg/L
|
|
||||||
Diffusivity 1.0
|
|
||||||
Tolerance 0.01
|
|
||||||
|
|
||||||
[COORDINATES]
|
|
||||||
;Node X-Coord Y-Coord
|
|
||||||
10 20.00 70.00
|
|
||||||
11 30.00 70.00
|
|
||||||
12 50.00 70.00
|
|
||||||
13 70.00 70.00
|
|
||||||
21 30.00 40.00
|
|
||||||
22 50.00 40.00
|
|
||||||
23 70.00 40.00
|
|
||||||
31 30.00 10.00
|
|
||||||
32 50.00 10.00
|
|
||||||
9 10.00 70.00
|
|
||||||
2 50.00 90.00
|
|
||||||
|
|
||||||
[VERTICES]
|
|
||||||
;Link X-Coord Y-Coord
|
|
||||||
|
|
||||||
[LABELS]
|
|
||||||
;X-Coord Y-Coord Label & Anchor Node
|
|
||||||
6.99 73.63 "Source"
|
|
||||||
13.48 68.13 "Pump"
|
|
||||||
43.85 91.21 "Tank"
|
|
||||||
|
|
||||||
[BACKDROP]
|
|
||||||
DIMENSIONS 7.00 6.00 73.00 94.00
|
|
||||||
UNITS None
|
|
||||||
FILE
|
|
||||||
OFFSET 0.00 0.00
|
|
||||||
|
|
||||||
[END]
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
[TITLE]
|
|
||||||
EPANET Example Network 2
|
|
||||||
Example of modeling a 55-hour fluoride tracer study.
|
|
||||||
Measured fluoride data is contained in the file Net2-FL.dat
|
|
||||||
and should be registered with the project to produce a
|
|
||||||
Calibration Report (select Calibration Data from the Project
|
|
||||||
menu).
|
|
||||||
|
|
||||||
[JUNCTIONS]
|
|
||||||
;ID Elev Demand Pattern
|
|
||||||
1 50 -694.4 2 ;
|
|
||||||
2 100 8 ;
|
|
||||||
3 60 14 ;
|
|
||||||
4 60 8 ;
|
|
||||||
5 100 8 ;
|
|
||||||
6 125 5 ;
|
|
||||||
7 160 4 ;
|
|
||||||
8 110 9 ;
|
|
||||||
9 180 14 ;
|
|
||||||
10 130 5 ;
|
|
||||||
11 185 34.78 ;
|
|
||||||
12 210 16 ;
|
|
||||||
13 210 2 ;
|
|
||||||
14 200 2 ;
|
|
||||||
15 190 2 ;
|
|
||||||
16 150 20 ;
|
|
||||||
17 180 20 ;
|
|
||||||
18 100 20 ;
|
|
||||||
19 150 5 ;
|
|
||||||
20 170 19 ;
|
|
||||||
21 150 16 ;
|
|
||||||
22 200 10 ;
|
|
||||||
23 230 8 ;
|
|
||||||
24 190 11 ;
|
|
||||||
25 230 6 ;
|
|
||||||
27 130 8 ;
|
|
||||||
28 110 0 ;
|
|
||||||
29 110 7 ;
|
|
||||||
30 130 3 ;
|
|
||||||
31 190 17 ;
|
|
||||||
32 110 17 ;
|
|
||||||
33 180 1.5 ;
|
|
||||||
34 190 1.5 ;
|
|
||||||
35 110 0 ;
|
|
||||||
36 110 1 ;
|
|
||||||
|
|
||||||
[RESERVOIRS]
|
|
||||||
;ID Head Pattern
|
|
||||||
|
|
||||||
[TANKS]
|
|
||||||
;ID Elevation InitLevel MinLevel MaxLevel Diameter MinVol VolCurve
|
|
||||||
26 235 56.7 50 70 50 0 ;
|
|
||||||
|
|
||||||
[PIPES]
|
|
||||||
;ID Node1 Node2 Length Diameter Roughness MinorLoss Status
|
|
||||||
1 1 2 2400 12 100 0 Open ;
|
|
||||||
2 2 5 800 12 100 0 Open ;
|
|
||||||
3 2 3 1300 8 100 0 Open ;
|
|
||||||
4 3 4 1200 8 100 0 Open ;
|
|
||||||
5 4 5 1000 12 100 0 Open ;
|
|
||||||
6 5 6 1200 12 100 0 Open ;
|
|
||||||
7 6 7 2700 12 100 0 Open ;
|
|
||||||
8 7 8 1200 12 140 0 Open ;
|
|
||||||
9 7 9 400 12 100 0 Open ;
|
|
||||||
10 8 10 1000 8 140 0 Open ;
|
|
||||||
11 9 11 700 12 100 0 Open ;
|
|
||||||
12 11 12 1900 12 100 0 Open ;
|
|
||||||
13 12 13 600 12 100 0 Open ;
|
|
||||||
14 13 14 400 12 100 0 Open ;
|
|
||||||
15 14 15 300 12 100 0 Open ;
|
|
||||||
16 13 16 1500 8 100 0 Open ;
|
|
||||||
17 15 17 1500 8 100 0 Open ;
|
|
||||||
18 16 17 600 8 100 0 Open ;
|
|
||||||
19 17 18 700 12 100 0 Open ;
|
|
||||||
20 18 32 350 12 100 0 Open ;
|
|
||||||
21 16 19 1400 8 100 0 Open ;
|
|
||||||
22 14 20 1100 12 100 0 Open ;
|
|
||||||
23 20 21 1300 8 100 0 Open ;
|
|
||||||
24 21 22 1300 8 100 0 Open ;
|
|
||||||
25 20 22 1300 8 100 0 Open ;
|
|
||||||
26 24 23 600 12 100 0 Open ;
|
|
||||||
27 15 24 250 12 100 0 Open ;
|
|
||||||
28 23 25 300 12 100 0 Open ;
|
|
||||||
29 25 26 200 12 100 0 Open ;
|
|
||||||
30 25 31 600 12 100 0 Open ;
|
|
||||||
31 31 27 400 8 100 0 Open ;
|
|
||||||
32 27 29 400 8 100 0 Open ;
|
|
||||||
34 29 28 700 8 100 0 Open ;
|
|
||||||
35 22 33 1000 8 100 0 Open ;
|
|
||||||
36 33 34 400 8 100 0 Open ;
|
|
||||||
37 32 19 500 8 100 0 Open ;
|
|
||||||
38 29 35 500 8 100 0 Open ;
|
|
||||||
39 35 30 1000 8 100 0 Open ;
|
|
||||||
40 28 35 700 8 100 0 Open ;
|
|
||||||
41 28 36 300 8 100 0 Open ;
|
|
||||||
|
|
||||||
[PUMPS]
|
|
||||||
;ID Node1 Node2 Parameters
|
|
||||||
|
|
||||||
[VALVES]
|
|
||||||
;ID Node1 Node2 Diameter Type Setting MinorLoss
|
|
||||||
|
|
||||||
[TAGS]
|
|
||||||
|
|
||||||
[DEMANDS]
|
|
||||||
;Junction Demand Pattern Category
|
|
||||||
|
|
||||||
[STATUS]
|
|
||||||
;ID Status/Setting
|
|
||||||
|
|
||||||
[PATTERNS]
|
|
||||||
;ID Multipliers
|
|
||||||
;Demand Pattern
|
|
||||||
1 1.26 1.04 .97 .97 .89 1.19
|
|
||||||
1 1.28 .67 .67 1.34 2.46 .97
|
|
||||||
1 .92 .68 1.43 .61 .31 .78
|
|
||||||
1 .37 .67 1.26 1.56 1.19 1.26
|
|
||||||
1 .6 1.1 1.03 .73 .88 1.06
|
|
||||||
1 .99 1.72 1.12 1.34 1.12 .97
|
|
||||||
1 1.04 1.15 .91 .61 .68 .46
|
|
||||||
1 .51 .74 1.12 1.34 1.26 .97
|
|
||||||
1 .82 1.37 1.03 .81 .88 .81
|
|
||||||
1 .81
|
|
||||||
;Pump Station Outflow Pattern
|
|
||||||
2 .96 .96 .96 .96 .96 .96
|
|
||||||
2 .62 0 0 0 0 0
|
|
||||||
2 .8 1 1 1 1 .15
|
|
||||||
2 0 0 0 0 0 0
|
|
||||||
2 .55 .92 .92 .92 .92 .9
|
|
||||||
2 .9 .45 0 0 0 0
|
|
||||||
2 0 .7 1 1 1 1
|
|
||||||
2 .2 0 0 0 0 0
|
|
||||||
2 0 .74 .92 .92 .92 .92
|
|
||||||
2 .92
|
|
||||||
;Pump Station Fluoride Pattern
|
|
||||||
3 .98 1.02 1.05 .99 .64 .46
|
|
||||||
3 .35 .35 .35 .35 .35 .35
|
|
||||||
3 .17 .17 .13 .13 .13 .15
|
|
||||||
3 .15 .15 .15 .15 .15 .15
|
|
||||||
3 .15 .12 .1 .08 .11 .09
|
|
||||||
3 .09 .08 .08 .08 .08 .08
|
|
||||||
3 .08 .09 .07 .07 .09 .09
|
|
||||||
3 .09 .09 .09 .09 .09 .09
|
|
||||||
3 .09 .08 .35 .72 .82 .92
|
|
||||||
3 1
|
|
||||||
|
|
||||||
[CURVES]
|
|
||||||
;ID X-Value Y-Value
|
|
||||||
|
|
||||||
[CONTROLS]
|
|
||||||
|
|
||||||
[RULES]
|
|
||||||
|
|
||||||
[ENERGY]
|
|
||||||
Global Efficiency 75
|
|
||||||
Global Price 0.0
|
|
||||||
Demand Charge 0.0
|
|
||||||
|
|
||||||
[EMITTERS]
|
|
||||||
;Junction Coefficient
|
|
||||||
|
|
||||||
[QUALITY]
|
|
||||||
;Node InitQual
|
|
||||||
1 1.0
|
|
||||||
2 1.0
|
|
||||||
3 1.0
|
|
||||||
4 1.0
|
|
||||||
5 1.0
|
|
||||||
6 1.0
|
|
||||||
7 1.0
|
|
||||||
8 1.0
|
|
||||||
9 1.0
|
|
||||||
10 1.0
|
|
||||||
11 1.0
|
|
||||||
12 1.0
|
|
||||||
13 1.0
|
|
||||||
14 1.0
|
|
||||||
15 1.0
|
|
||||||
16 1.0
|
|
||||||
17 1.0
|
|
||||||
18 1.0
|
|
||||||
19 1.0
|
|
||||||
20 1.0
|
|
||||||
21 1.0
|
|
||||||
22 1.0
|
|
||||||
23 1.0
|
|
||||||
24 1.0
|
|
||||||
25 1.0
|
|
||||||
27 1.0
|
|
||||||
28 1.0
|
|
||||||
29 1.0
|
|
||||||
30 1.0
|
|
||||||
31 1.0
|
|
||||||
32 1.0
|
|
||||||
33 1.0
|
|
||||||
34 1.0
|
|
||||||
35 1.0
|
|
||||||
36 1.0
|
|
||||||
26 1.0
|
|
||||||
|
|
||||||
[SOURCES]
|
|
||||||
;Node Type Quality Pattern
|
|
||||||
1 CONCEN 1.0 3
|
|
||||||
|
|
||||||
[REACTIONS]
|
|
||||||
;Type Pipe/Tank Coefficient
|
|
||||||
|
|
||||||
|
|
||||||
[REACTIONS]
|
|
||||||
Order Bulk 1
|
|
||||||
Order Tank 1
|
|
||||||
Order Wall 1
|
|
||||||
Global Bulk 0.0
|
|
||||||
Global Wall 0.0
|
|
||||||
Limiting Potential 0.0
|
|
||||||
Roughness Correlation 0.0
|
|
||||||
|
|
||||||
[MIXING]
|
|
||||||
;Tank Model
|
|
||||||
|
|
||||||
[TIMES]
|
|
||||||
Duration 55:00
|
|
||||||
Hydraulic Timestep 1:00
|
|
||||||
Quality Timestep 0:05
|
|
||||||
Pattern Timestep 1:00
|
|
||||||
Pattern Start 0:00
|
|
||||||
Report Timestep 1:00
|
|
||||||
Report Start 0:00
|
|
||||||
Start ClockTime 8 am
|
|
||||||
Statistic None
|
|
||||||
|
|
||||||
[REPORT]
|
|
||||||
Status No
|
|
||||||
Summary No
|
|
||||||
Page 0
|
|
||||||
|
|
||||||
[OPTIONS]
|
|
||||||
Units GPM
|
|
||||||
Headloss H-W
|
|
||||||
Specific Gravity 1.0
|
|
||||||
Viscosity 1.0
|
|
||||||
Trials 40
|
|
||||||
Accuracy 0.001
|
|
||||||
CHECKFREQ 2
|
|
||||||
MAXCHECK 10
|
|
||||||
DAMPLIMIT 0
|
|
||||||
Unbalanced Continue 10
|
|
||||||
Pattern 1
|
|
||||||
Demand Multiplier 1.0
|
|
||||||
Emitter Exponent 0.5
|
|
||||||
Quality Fluoride mg/L
|
|
||||||
Diffusivity 1.0
|
|
||||||
Tolerance 0.01
|
|
||||||
|
|
||||||
[COORDINATES]
|
|
||||||
;Node X-Coord Y-Coord
|
|
||||||
1 21.00 4.00
|
|
||||||
2 19.00 20.00
|
|
||||||
3 11.00 21.00
|
|
||||||
4 14.00 28.00
|
|
||||||
5 19.00 25.00
|
|
||||||
6 28.00 23.00
|
|
||||||
7 36.00 39.00
|
|
||||||
8 38.00 30.00
|
|
||||||
9 36.00 42.00
|
|
||||||
10 37.00 23.00
|
|
||||||
11 37.00 49.00
|
|
||||||
12 39.00 60.00
|
|
||||||
13 38.00 64.00
|
|
||||||
14 38.00 66.00
|
|
||||||
15 37.00 69.00
|
|
||||||
16 27.00 65.00
|
|
||||||
17 27.00 69.00
|
|
||||||
18 23.00 68.00
|
|
||||||
19 21.00 59.00
|
|
||||||
20 45.00 68.00
|
|
||||||
21 51.00 62.00
|
|
||||||
22 54.00 69.00
|
|
||||||
23 35.00 74.00
|
|
||||||
24 37.00 71.00
|
|
||||||
25 35.00 76.00
|
|
||||||
27 39.00 87.00
|
|
||||||
28 49.00 85.00
|
|
||||||
29 42.00 86.00
|
|
||||||
30 47.00 80.00
|
|
||||||
31 37.00 80.00
|
|
||||||
32 23.00 64.00
|
|
||||||
33 56.00 73.00
|
|
||||||
34 56.00 77.00
|
|
||||||
35 43.00 81.00
|
|
||||||
36 53.00 87.00
|
|
||||||
26 33.00 76.00
|
|
||||||
|
|
||||||
[VERTICES]
|
|
||||||
;Link X-Coord Y-Coord
|
|
||||||
|
|
||||||
[LABELS]
|
|
||||||
;X-Coord Y-Coord Label & Anchor Node
|
|
||||||
24.00 7.00 "Pump"
|
|
||||||
24.00 4.00 "Station"
|
|
||||||
26.76 77.42 "Tank"
|
|
||||||
|
|
||||||
[BACKDROP]
|
|
||||||
DIMENSIONS 8.75 -0.15 58.25 91.15
|
|
||||||
UNITS None
|
|
||||||
FILE
|
|
||||||
OFFSET 0.00 0.00
|
|
||||||
|
|
||||||
[END]
|
|
||||||
-147940
File diff suppressed because it is too large
Load Diff
-60024
File diff suppressed because it is too large
Load Diff
Binary file not shown.
-1304498
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,252 +0,0 @@
|
|||||||
[TITLE]
|
|
||||||
|
|
||||||
|
|
||||||
[JUNCTIONS]
|
|
||||||
;ID Elev Demand Pattern
|
|
||||||
2 5.80 19.49 1 ;
|
|
||||||
3 5.80 19.49 1 ;
|
|
||||||
4 5.80 19.50 1 ;
|
|
||||||
5 5.80 19.49 1 ;
|
|
||||||
6 5.80 19.49 1 ;
|
|
||||||
7 5.80 19.50 1 ;
|
|
||||||
8 5.80 19.49 1 ;
|
|
||||||
9 5.80 19.50 1 ;
|
|
||||||
10 5.80 19.49 1 ;
|
|
||||||
11 5.80 19.49 1 ;
|
|
||||||
12 5.80 19.50 1 ;
|
|
||||||
13 5.80 19.49 1 ;
|
|
||||||
15 5.80 19.49 1 ;
|
|
||||||
16 5.80 19.50 1 ;
|
|
||||||
17 5.80 19.49 1 ;
|
|
||||||
18 5.80 19.49 1 ;
|
|
||||||
19 5.80 19.50 1 ;
|
|
||||||
20 5.80 19.49 1 ;
|
|
||||||
21 5.80 19.49 1 ;
|
|
||||||
22 0 136.45 1 ;
|
|
||||||
23 0 0 1 ;
|
|
||||||
24 0 0 1 ;
|
|
||||||
|
|
||||||
[RESERVOIRS]
|
|
||||||
;ID Head Pattern
|
|
||||||
1 3.19 ;
|
|
||||||
14 7.40 ;
|
|
||||||
|
|
||||||
[TANKS]
|
|
||||||
;ID Elevation InitLevel MinLevel MaxLevel Diameter MinVol VolCurve
|
|
||||||
|
|
||||||
[PIPES]
|
|
||||||
;ID Node1 Node2 Length Diameter Roughness MinorLoss Status
|
|
||||||
5 2 6 1976 315 120 0 Open ;
|
|
||||||
6 6 7 1300 250 120 0 Open ;
|
|
||||||
7 7 8 1051 160 120 0 Open ;
|
|
||||||
8 3 9 6210 250 120 0 Open ;
|
|
||||||
9 5 10 2173 200 120 0 Open ;
|
|
||||||
10 4 11 2984 500 120 0 Open ;
|
|
||||||
11 11 12 2100 500 120 0 Open ;
|
|
||||||
12 11 13 2199 250 120 0 Open ;
|
|
||||||
14 15 16 3100 315 120 0 Open ;
|
|
||||||
15 15 17 6485 400 120 0 Open ;
|
|
||||||
16 16 18 2688 200 120 0 Open ;
|
|
||||||
17 17 19 2306 200 120 0 Open ;
|
|
||||||
18 17 20 4161 250 120 0 Open ;
|
|
||||||
19 20 21 2841 200 120 0 Open ;
|
|
||||||
20 12 22 1 1000 120 0 Open ;
|
|
||||||
1 23 2 3024 355 100 0 Open ;
|
|
||||||
2 23 3 1400 400 120 0 Open ;
|
|
||||||
3 23 4 3149 500 120 0 Open ;
|
|
||||||
4 23 5 5400 250 120 0 Open ;
|
|
||||||
13 24 15 3554 400 120 0 Open ;
|
|
||||||
|
|
||||||
[PUMPS]
|
|
||||||
;ID Node1 Node2 Parameters
|
|
||||||
23 1 23 HEAD 1 ;
|
|
||||||
22 1 23 HEAD 1 ;
|
|
||||||
21 1 23 HEAD 1 ;
|
|
||||||
24 14 24 HEAD 1 ;
|
|
||||||
25 14 24 HEAD 1 ;
|
|
||||||
|
|
||||||
[VALVES]
|
|
||||||
;ID Node1 Node2 Diameter Type Setting MinorLoss
|
|
||||||
|
|
||||||
[TAGS]
|
|
||||||
|
|
||||||
[DEMANDS]
|
|
||||||
;Junction Demand Pattern Category
|
|
||||||
|
|
||||||
[STATUS]
|
|
||||||
;ID Status/Setting
|
|
||||||
|
|
||||||
[PATTERNS]
|
|
||||||
;ID Multipliers
|
|
||||||
;
|
|
||||||
1 1
|
|
||||||
|
|
||||||
[CURVES]
|
|
||||||
;ID X-Value Y-Value
|
|
||||||
;Ë®±Ã:
|
|
||||||
1 0 56.99
|
|
||||||
1 33.33 56.74
|
|
||||||
1 50 56.49
|
|
||||||
1 66.67 55.74
|
|
||||||
1 83.33 54.24
|
|
||||||
1 100 51.98
|
|
||||||
1 116.67 48.72
|
|
||||||
1 133.33 43.95
|
|
||||||
1 150 38.44
|
|
||||||
|
|
||||||
[CONTROLS]
|
|
||||||
|
|
||||||
[RULES]
|
|
||||||
|
|
||||||
[ENERGY]
|
|
||||||
Global Efficiency 75
|
|
||||||
Global Price 0
|
|
||||||
Demand Charge 0
|
|
||||||
|
|
||||||
[EMITTERS]
|
|
||||||
;Junction Coefficient
|
|
||||||
|
|
||||||
[QUALITY]
|
|
||||||
;Node InitQual
|
|
||||||
1 0.3
|
|
||||||
14 0.3
|
|
||||||
|
|
||||||
[SOURCES]
|
|
||||||
;Node Type Quality Pattern
|
|
||||||
|
|
||||||
[REACTIONS]
|
|
||||||
;Type Pipe/Tank Coefficient
|
|
||||||
|
|
||||||
|
|
||||||
[REACTIONS]
|
|
||||||
Order Bulk 1
|
|
||||||
Order Tank 1
|
|
||||||
Order Wall 1
|
|
||||||
Global Bulk -1
|
|
||||||
Global Wall -0.5
|
|
||||||
Limiting Potential 0
|
|
||||||
Roughness Correlation 0
|
|
||||||
|
|
||||||
[MIXING]
|
|
||||||
;Tank Model
|
|
||||||
|
|
||||||
[TIMES]
|
|
||||||
Duration 48:00
|
|
||||||
Hydraulic Timestep 0:05
|
|
||||||
Quality Timestep 0:05
|
|
||||||
Pattern Timestep 0:05
|
|
||||||
Pattern Start 0:00
|
|
||||||
Report Timestep 0:05
|
|
||||||
Report Start 0:00
|
|
||||||
Start ClockTime 12 am
|
|
||||||
Statistic None
|
|
||||||
|
|
||||||
[REPORT]
|
|
||||||
Status No
|
|
||||||
Summary No
|
|
||||||
Page 0
|
|
||||||
|
|
||||||
[OPTIONS]
|
|
||||||
Units LPS
|
|
||||||
Headloss H-W
|
|
||||||
Specific Gravity 1
|
|
||||||
Viscosity 1
|
|
||||||
Trials 100
|
|
||||||
Accuracy 0.01
|
|
||||||
CHECKFREQ 2
|
|
||||||
MAXCHECK 10
|
|
||||||
DAMPLIMIT 0
|
|
||||||
Unbalanced Continue 10
|
|
||||||
Pattern 1
|
|
||||||
Demand Multiplier 1.0
|
|
||||||
Emitter Exponent 0.5
|
|
||||||
Quality »¯Ñ§³É·Ö mg/L
|
|
||||||
Diffusivity 1
|
|
||||||
Tolerance 0.01
|
|
||||||
|
|
||||||
[COORDINATES]
|
|
||||||
;Node X-Coord Y-Coord
|
|
||||||
2 2054.14 6695.86
|
|
||||||
3 2249.20 7663.22
|
|
||||||
4 3523.09 7328.82
|
|
||||||
5 3136.94 9211.78
|
|
||||||
6 1409.24 6787.42
|
|
||||||
7 1122.61 6819.27
|
|
||||||
8 800.16 6950.64
|
|
||||||
9 1357.48 8809.71
|
|
||||||
10 3531.05 9996.02
|
|
||||||
11 3654.46 8308.12
|
|
||||||
12 4353.11 8268.31
|
|
||||||
13 3710.19 9060.51
|
|
||||||
15 5467.75 8264.33
|
|
||||||
16 5467.75 9327.23
|
|
||||||
17 6769.51 9243.63
|
|
||||||
18 5220.94 9765.13
|
|
||||||
19 6359.47 9390.92
|
|
||||||
20 8055.33 8853.50
|
|
||||||
21 8970.94 8901.27
|
|
||||||
22 4353.60 8338.97
|
|
||||||
23 2590.07 7540.80
|
|
||||||
24 4405.35 8378.78
|
|
||||||
1 2900.58 7676.15
|
|
||||||
14 4309.81 8420.58
|
|
||||||
|
|
||||||
[VERTICES]
|
|
||||||
;Link X-Coord Y-Coord
|
|
||||||
5 1970.54 6707.80
|
|
||||||
5 1791.40 6719.75
|
|
||||||
6 1373.41 6689.89
|
|
||||||
6 1242.04 6769.51
|
|
||||||
8 1817.28 7750.80
|
|
||||||
8 1875.00 8134.95
|
|
||||||
8 1968.55 8190.68
|
|
||||||
8 1849.12 8529.06
|
|
||||||
8 1833.20 8833.60
|
|
||||||
10 3642.52 7852.31
|
|
||||||
14 5481.69 8612.66
|
|
||||||
15 6072.85 8238.46
|
|
||||||
15 6076.83 8349.92
|
|
||||||
15 6106.69 8481.29
|
|
||||||
15 6156.45 8610.67
|
|
||||||
15 6385.35 8616.64
|
|
||||||
15 6357.48 8865.45
|
|
||||||
15 6666.00 8887.34
|
|
||||||
15 6753.58 9203.82
|
|
||||||
16 5467.75 9460.59
|
|
||||||
16 5254.78 9486.46
|
|
||||||
16 5095.54 9488.46
|
|
||||||
16 5127.39 9767.12
|
|
||||||
17 6803.34 9317.28
|
|
||||||
17 6596.34 9402.87
|
|
||||||
17 6486.86 9464.57
|
|
||||||
17 6445.06 9355.10
|
|
||||||
18 7131.77 9080.41
|
|
||||||
18 7689.09 8875.40
|
|
||||||
19 8292.20 8837.58
|
|
||||||
19 8682.32 8909.24
|
|
||||||
19 8769.90 8895.30
|
|
||||||
2 2502.49 7568.67
|
|
||||||
2 2243.73 7600.52
|
|
||||||
3 2781.15 7461.19
|
|
||||||
3 2880.67 7433.32
|
|
||||||
4 2613.95 7636.35
|
|
||||||
4 2757.27 7839.37
|
|
||||||
4 2785.13 8508.16
|
|
||||||
13 5010.45 8384.75
|
|
||||||
13 5040.31 8289.21
|
|
||||||
13 5229.40 8271.30
|
|
||||||
23 2789.11 7711.98
|
|
||||||
21 2900.58 7564.69
|
|
||||||
24 4345.64 8378.78
|
|
||||||
25 4363.55 8410.63
|
|
||||||
|
|
||||||
[LABELS]
|
|
||||||
;X-Coord Y-Coord Label & Anchor Node
|
|
||||||
|
|
||||||
[BACKDROP]
|
|
||||||
DIMENSIONS 0.00 0.00 10000.00 10000.00
|
|
||||||
UNITS None
|
|
||||||
FILE
|
|
||||||
OFFSET 0.00 0.00
|
|
||||||
|
|
||||||
[END]
|
|
||||||
File diff suppressed because it is too large
Load Diff
-109
@@ -1,109 +0,0 @@
|
|||||||
[TITLE]
|
|
||||||
|
|
||||||
[JUNCTIONS]
|
|
||||||
|
|
||||||
[RESERVOIRS]
|
|
||||||
|
|
||||||
[TANKS]
|
|
||||||
|
|
||||||
[PIPES]
|
|
||||||
|
|
||||||
[PUMPS]
|
|
||||||
|
|
||||||
[VALVES]
|
|
||||||
|
|
||||||
[DEMANDS]
|
|
||||||
|
|
||||||
[EMITTERS]
|
|
||||||
|
|
||||||
[STATUS]
|
|
||||||
|
|
||||||
[PATTERNS]
|
|
||||||
|
|
||||||
[CURVES]
|
|
||||||
|
|
||||||
[CONTROLS]
|
|
||||||
|
|
||||||
[ENERGY]
|
|
||||||
GLOBAL EFFICIENCY 0.7500
|
|
||||||
GLOBAL PRICE 0.0000
|
|
||||||
DEMAND CHARGE 0.0000
|
|
||||||
|
|
||||||
[QUALITY]
|
|
||||||
|
|
||||||
[SOURCES]
|
|
||||||
|
|
||||||
[MIXING]
|
|
||||||
|
|
||||||
[REACTIONS]
|
|
||||||
ORDER BULK 1.0000
|
|
||||||
ORDER WALL 1.0000
|
|
||||||
ORDER TANK 1.0000
|
|
||||||
GLOBAL BULK 0.0000
|
|
||||||
GLOBAL WALL 0.0000
|
|
||||||
LIMITING POTENTIAL 0.0000
|
|
||||||
ROUGHNESS CORRELATION 0.0000
|
|
||||||
|
|
||||||
[OPTIONS]
|
|
||||||
FLOW_UNITS GPM
|
|
||||||
PRESSURE_UNITS PSI
|
|
||||||
HEADLOSS_MODEL H-W
|
|
||||||
SPECIFIC_GRAVITY 1.0000
|
|
||||||
SPECIFIC_VISCOSITY 1.0000
|
|
||||||
|
|
||||||
MAXIMUM_TRIALS 100
|
|
||||||
HEAD_TOLERANCE 0.0000
|
|
||||||
FLOW_TOLERANCE 0.0000
|
|
||||||
FLOW_CHANGE_LIMIT 0.0000
|
|
||||||
TIME_WEIGHT 0.0000
|
|
||||||
STEP_SIZING FULL
|
|
||||||
IF_UNBALANCED STOP
|
|
||||||
|
|
||||||
|
|
||||||
DEMAND_MODEL FIXED
|
|
||||||
DEMAND_PATTERN
|
|
||||||
DEMAND_MULTIPLIER 1.0000
|
|
||||||
MINIMUM_PRESSURE 0.0000
|
|
||||||
SERVICE_PRESSURE 0.0000
|
|
||||||
PRESSURE_EXPONENT 0.5000
|
|
||||||
|
|
||||||
LEAKAGE_MODEL NONE
|
|
||||||
LEAKAGE_COEFF1 0.0000
|
|
||||||
LEAKAGE_COEFF2 0.0000
|
|
||||||
EMITTER_EXPONENT 0.5000
|
|
||||||
|
|
||||||
QUALITY_MODEL NONE
|
|
||||||
SPECIFIC_DIFFUSIVITY 1.0000
|
|
||||||
QUALITY_TOLERANCE 0.0100
|
|
||||||
|
|
||||||
[TIMES]
|
|
||||||
TOTAL DURATION 0:00:00
|
|
||||||
HYDRAULIC TIMESTEP 1:00:00
|
|
||||||
QUALITY TIMESTEP 0:05:00
|
|
||||||
RULE TIMESTEP 0:05:00
|
|
||||||
PATTERN TIMESTEP 1:00:00
|
|
||||||
PATTERN START 0:00:00
|
|
||||||
REPORT TIMESTEP 1:00:00
|
|
||||||
REPORT START 0:00:00
|
|
||||||
START CLOCKTIME 0:00:00
|
|
||||||
|
|
||||||
[REPORT]
|
|
||||||
SUMMARY YES
|
|
||||||
|
|
||||||
[COORDINATES]
|
|
||||||
|
|
||||||
[VERTICES]
|
|
||||||
|
|
||||||
[REGION]
|
|
||||||
|
|
||||||
[BOUND]
|
|
||||||
|
|
||||||
[DATA_NODE_OF_REGION]
|
|
||||||
|
|
||||||
[DMA]
|
|
||||||
|
|
||||||
[VD]
|
|
||||||
|
|
||||||
[SA]
|
|
||||||
|
|
||||||
[DATA_REGION_OF_NODE]
|
|
||||||
-38821
File diff suppressed because it is too large
Load Diff
-37670
File diff suppressed because it is too large
Load Diff
-40290
File diff suppressed because it is too large
Load Diff
-73400
File diff suppressed because it is too large
Load Diff
-72248
File diff suppressed because it is too large
Load Diff
-40183
File diff suppressed because it is too large
Load Diff
-41673
File diff suppressed because it is too large
Load Diff
-6383
File diff suppressed because it is too large
Load Diff
-1645
File diff suppressed because it is too large
Load Diff
-486141
File diff suppressed because it is too large
Load Diff
-40289
File diff suppressed because it is too large
Load Diff
-30972
File diff suppressed because it is too large
Load Diff
-481
@@ -1,481 +0,0 @@
|
|||||||
[TITLE]
|
|
||||||
EPANET Example Network 3
|
|
||||||
Example showing how the percent of Lake water in a dual-source
|
|
||||||
system changes over time.
|
|
||||||
|
|
||||||
[JUNCTIONS]
|
|
||||||
;ID Elev Demand Pattern
|
|
||||||
10 147 0 ;
|
|
||||||
15 32 1 3 ;
|
|
||||||
20 129 0 ;
|
|
||||||
35 12.5 1 4 ;
|
|
||||||
40 131.9 0 ;
|
|
||||||
50 116.5 0 ;
|
|
||||||
60 0 0 ;
|
|
||||||
601 0 0 ;
|
|
||||||
61 0 0 ;
|
|
||||||
101 42 189.95 ;
|
|
||||||
103 43 133.2 ;
|
|
||||||
105 28.5 135.37 ;
|
|
||||||
107 22 54.64 ;
|
|
||||||
109 20.3 231.4 ;
|
|
||||||
111 10 141.94 ;
|
|
||||||
113 2 20.01 ;
|
|
||||||
115 14 52.1 ;
|
|
||||||
117 13.6 117.71 ;
|
|
||||||
119 2 176.13 ;
|
|
||||||
120 0 0 ;
|
|
||||||
121 -2 41.63 ;
|
|
||||||
123 11 1 2 ;
|
|
||||||
125 11 45.6 ;
|
|
||||||
127 56 17.66 ;
|
|
||||||
129 51 0 ;
|
|
||||||
131 6 42.75 ;
|
|
||||||
139 31 5.89 ;
|
|
||||||
141 4 9.85 ;
|
|
||||||
143 -4.5 6.2 ;
|
|
||||||
145 1 27.63 ;
|
|
||||||
147 18.5 8.55 ;
|
|
||||||
149 16 27.07 ;
|
|
||||||
151 33.5 144.48 ;
|
|
||||||
153 66.2 44.17 ;
|
|
||||||
157 13.1 51.79 ;
|
|
||||||
159 6 41.32 ;
|
|
||||||
161 4 15.8 ;
|
|
||||||
163 5 9.42 ;
|
|
||||||
164 5 0 ;
|
|
||||||
166 -2 2.6 ;
|
|
||||||
167 -5 14.56 ;
|
|
||||||
169 -5 0 ;
|
|
||||||
171 -4 39.34 ;
|
|
||||||
173 -4 0 ;
|
|
||||||
177 8 58.17 ;
|
|
||||||
179 8 0 ;
|
|
||||||
181 8 0 ;
|
|
||||||
183 11 0 ;
|
|
||||||
184 16 0 ;
|
|
||||||
185 16 25.65 ;
|
|
||||||
187 12.5 0 ;
|
|
||||||
189 4 107.92 ;
|
|
||||||
191 25 81.9 ;
|
|
||||||
193 18 71.31 ;
|
|
||||||
195 15.5 0 ;
|
|
||||||
197 23 17.04 ;
|
|
||||||
199 -2 119.32 ;
|
|
||||||
201 0.1 44.61 ;
|
|
||||||
203 2 1 5 ;
|
|
||||||
204 21 0 ;
|
|
||||||
205 21 65.36 ;
|
|
||||||
206 1 0 ;
|
|
||||||
207 9 69.39 ;
|
|
||||||
208 16 0 ;
|
|
||||||
209 -2 0.87 ;
|
|
||||||
211 7 8.67 ;
|
|
||||||
213 7 13.94 ;
|
|
||||||
215 7 92.19 ;
|
|
||||||
217 6 24.22 ;
|
|
||||||
219 4 41.32 ;
|
|
||||||
225 8 22.8 ;
|
|
||||||
229 10.5 64.18 ;
|
|
||||||
231 5 16.48 ;
|
|
||||||
237 14 15.61 ;
|
|
||||||
239 13 44.61 ;
|
|
||||||
241 13 0 ;
|
|
||||||
243 14 4.34 ;
|
|
||||||
247 18 70.38 ;
|
|
||||||
249 18 0 ;
|
|
||||||
251 30 24.16 ;
|
|
||||||
253 36 54.52 ;
|
|
||||||
255 27 40.39 ;
|
|
||||||
257 17 0 ;
|
|
||||||
259 25 0 ;
|
|
||||||
261 0 0 ;
|
|
||||||
263 0 0 ;
|
|
||||||
265 0 0 ;
|
|
||||||
267 21 0 ;
|
|
||||||
269 0 0 ;
|
|
||||||
271 6 0 ;
|
|
||||||
273 8 0 ;
|
|
||||||
275 10 0 ;
|
|
||||||
|
|
||||||
[RESERVOIRS]
|
|
||||||
;ID Head Pattern
|
|
||||||
River 220.0 ;
|
|
||||||
Lake 167.0 ;
|
|
||||||
|
|
||||||
[TANKS]
|
|
||||||
;ID Elevation InitLevel MinLevel MaxLevel Diameter MinVol VolCurve
|
|
||||||
1 131.9 13.1 .1 32.1 85 0 ;
|
|
||||||
2 116.5 23.5 6.5 40.3 50 0 ;
|
|
||||||
3 129.0 29.0 4.0 35.5 164 0 ;
|
|
||||||
|
|
||||||
[PIPES]
|
|
||||||
;ID Node1 Node2 Length Diameter Roughness MinorLoss Status
|
|
||||||
20 3 20 99 99 199 0 Open ;
|
|
||||||
40 1 40 99 99 199 0 Open ;
|
|
||||||
50 2 50 99 99 199 0 Open ;
|
|
||||||
60 River 60 1231 24 140 0 Open ;
|
|
||||||
101 10 101 14200 18 110 0 Open ;
|
|
||||||
103 101 103 1350 16 130 0 Open ;
|
|
||||||
105 101 105 2540 12 130 0 Open ;
|
|
||||||
107 105 107 1470 12 130 0 Open ;
|
|
||||||
109 103 109 3940 16 130 0 Open ;
|
|
||||||
111 109 111 2000 12 130 0 Open ;
|
|
||||||
112 115 111 1160 12 130 0 Open ;
|
|
||||||
113 111 113 1680 12 130 0 Open ;
|
|
||||||
114 115 113 2000 8 130 0 Open ;
|
|
||||||
115 107 115 1950 8 130 0 Open ;
|
|
||||||
116 113 193 1660 12 130 0 Open ;
|
|
||||||
117 263 105 2725 12 130 0 Open ;
|
|
||||||
119 115 117 2180 12 130 0 Open ;
|
|
||||||
120 119 120 730 12 130 0 Open ;
|
|
||||||
121 120 117 1870 12 130 0 Open ;
|
|
||||||
122 121 120 2050 8 130 0 Open ;
|
|
||||||
123 121 119 2000 30 141 0 Open ;
|
|
||||||
125 123 121 1500 30 141 0 Open ;
|
|
||||||
129 121 125 930 24 130 0 Open ;
|
|
||||||
131 125 127 3240 24 130 0 Open ;
|
|
||||||
133 20 127 785 20 130 0 Open ;
|
|
||||||
135 127 129 900 24 130 0 Open ;
|
|
||||||
137 129 131 6480 16 130 0 Open ;
|
|
||||||
145 129 139 2750 8 130 0 Open ;
|
|
||||||
147 139 141 2050 8 130 0 Open ;
|
|
||||||
149 143 141 1400 8 130 0 Open ;
|
|
||||||
151 15 143 1650 8 130 0 Open ;
|
|
||||||
153 145 141 3510 12 130 0 Open ;
|
|
||||||
155 147 145 2200 12 130 0 Open ;
|
|
||||||
159 147 149 880 12 130 0 Open ;
|
|
||||||
161 149 151 1020 8 130 0 Open ;
|
|
||||||
163 151 153 1170 12 130 0 Open ;
|
|
||||||
169 125 153 4560 8 130 0 Open ;
|
|
||||||
171 119 151 3460 12 130 0 Open ;
|
|
||||||
173 119 157 2080 30 141 0 Open ;
|
|
||||||
175 157 159 2910 30 141 0 Open ;
|
|
||||||
177 159 161 2000 30 141 0 Open ;
|
|
||||||
179 161 163 430 30 141 0 Open ;
|
|
||||||
180 163 164 150 14 130 0 Open ;
|
|
||||||
181 164 166 490 14 130 0 Open ;
|
|
||||||
183 265 169 590 30 141 0 Open ;
|
|
||||||
185 167 169 60 8 130 0 Open ;
|
|
||||||
186 187 204 99.9 8 130 0 Open ;
|
|
||||||
187 169 171 1270 30 141 0 Open ;
|
|
||||||
189 171 173 50 30 141 0 Open ;
|
|
||||||
191 271 171 760 24 130 0 Open ;
|
|
||||||
193 35 181 30 24 130 0 Open ;
|
|
||||||
195 181 177 30 12 130 0 Open ;
|
|
||||||
197 177 179 30 12 130 0 Open ;
|
|
||||||
199 179 183 210 12 130 0 Open ;
|
|
||||||
201 40 179 1190 12 130 0 Open ;
|
|
||||||
202 185 184 99.9 8 130 0 Open ;
|
|
||||||
203 183 185 510 8 130 0 Open ;
|
|
||||||
204 184 205 4530. 12 130 0 Open ;
|
|
||||||
205 204 185 1325. 12 130 0 Open ;
|
|
||||||
207 189 183 1350 12 130 0 Open ;
|
|
||||||
209 189 187 500 8 130 0 Open ;
|
|
||||||
211 169 269 646 12 130 0 Open ;
|
|
||||||
213 191 187 2560 12 130 0 Open ;
|
|
||||||
215 267 189 1230 12 130 0 Open ;
|
|
||||||
217 191 193 520 12 130 0 Open ;
|
|
||||||
219 193 195 360 12 130 0 Open ;
|
|
||||||
221 161 195 2300 8 130 0 Open ;
|
|
||||||
223 197 191 1150 12 130 0 Open ;
|
|
||||||
225 111 197 2790 12 130 0 Open ;
|
|
||||||
229 173 199 4000 24 141 0 Open ;
|
|
||||||
231 199 201 630 24 141 0 Open ;
|
|
||||||
233 201 203 120 24 130 0 Open ;
|
|
||||||
235 199 273 725 12 130 0 Open ;
|
|
||||||
237 205 207 1200 12 130 0 Open ;
|
|
||||||
238 207 206 450 12 130 0 Open ;
|
|
||||||
239 275 207 1430 12 130 0 Open ;
|
|
||||||
240 206 208 510 12 130 0 Open ;
|
|
||||||
241 208 209 885 12 130 0 Open ;
|
|
||||||
243 209 211 1210 16 130 0 Open ;
|
|
||||||
245 211 213 990 16 130 0 Open ;
|
|
||||||
247 213 215 4285 16 130 0 Open ;
|
|
||||||
249 215 217 1660 16 130 0 Open ;
|
|
||||||
251 217 219 2050 14 130 0 Open ;
|
|
||||||
257 217 225 1560 12 130 0 Open ;
|
|
||||||
261 213 229 2200 8 130 0 Open ;
|
|
||||||
263 229 231 1960 12 130 0 Open ;
|
|
||||||
269 211 237 2080 12 130 0 Open ;
|
|
||||||
271 237 229 790 8 130 0 Open ;
|
|
||||||
273 237 239 510 12 130 0 Open ;
|
|
||||||
275 239 241 35 12 130 0 Open ;
|
|
||||||
277 241 243 2200 12 130 0 Open ;
|
|
||||||
281 241 247 445 10 130 0 Open ;
|
|
||||||
283 239 249 430 12 130 0 Open ;
|
|
||||||
285 247 249 10 12 130 0 Open ;
|
|
||||||
287 247 255 1390 10 130 0 Open ;
|
|
||||||
289 50 255 925 10 130 0 Open ;
|
|
||||||
291 255 253 1100 10 130 0 Open ;
|
|
||||||
293 255 251 1100 8 130 0 Open ;
|
|
||||||
295 249 251 1450 12 130 0 Open ;
|
|
||||||
297 120 257 645 8 130 0 Open ;
|
|
||||||
299 257 259 350 8 130 0 Open ;
|
|
||||||
301 259 263 1400 8 130 0 Open ;
|
|
||||||
303 257 261 1400 8 130 0 Open ;
|
|
||||||
305 117 261 645 12 130 0 Open ;
|
|
||||||
307 261 263 350 12 130 0 Open ;
|
|
||||||
309 265 267 1580 8 130 0 Open ;
|
|
||||||
311 193 267 1170 12 130 0 Open ;
|
|
||||||
313 269 189 646 12 130 0 Open ;
|
|
||||||
315 181 271 260 24 130 0 Open ;
|
|
||||||
317 273 275 2230 8 130 0 Open ;
|
|
||||||
319 273 205 645 12 130 0 Open ;
|
|
||||||
321 163 265 1200 30 141 0 Open ;
|
|
||||||
323 201 275 300 12 130 0 Open ;
|
|
||||||
325 269 271 1290 8 130 0 Open ;
|
|
||||||
329 61 123 45500 30 140 0 Open ;
|
|
||||||
330 60 601 1 30 140 0 Closed ;
|
|
||||||
333 601 61 1 30 140 0 Open ;
|
|
||||||
|
|
||||||
[PUMPS]
|
|
||||||
;ID Node1 Node2 Parameters
|
|
||||||
10 Lake 10 HEAD 1 ;
|
|
||||||
335 60 61 HEAD 2 ;
|
|
||||||
|
|
||||||
[VALVES]
|
|
||||||
;ID Node1 Node2 Diameter Type Setting MinorLoss
|
|
||||||
|
|
||||||
[TAGS]
|
|
||||||
|
|
||||||
[DEMANDS]
|
|
||||||
;Junction Demand Pattern Category
|
|
||||||
|
|
||||||
[STATUS]
|
|
||||||
;ID Status/Setting
|
|
||||||
10 Closed
|
|
||||||
|
|
||||||
[PATTERNS]
|
|
||||||
;ID Multipliers
|
|
||||||
;General Default Demand Pattern
|
|
||||||
1 1.34 1.94 1.46 1.44 .76 .92
|
|
||||||
1 .85 1.07 .96 1.1 1.08 1.19
|
|
||||||
1 1.16 1.08 .96 .83 .79 .74
|
|
||||||
1 .64 .64 .85 .96 1.24 1.67
|
|
||||||
;Demand Pattern for Node 123
|
|
||||||
2 0 0 0 0 0 1219
|
|
||||||
2 0 0 0 1866 1836 1818
|
|
||||||
2 1818 1822 1822 1817 1824 1816
|
|
||||||
2 1833 1817 1830 1814 1840 1859
|
|
||||||
;Demand Pattern for Node 15
|
|
||||||
3 620 620 620 620 620 360
|
|
||||||
3 360 0 0 0 0 360
|
|
||||||
3 360 360 360 360 0 0
|
|
||||||
3 0 0 0 0 360 360
|
|
||||||
;Demand Pattern for Node 35
|
|
||||||
4 1637 1706 1719 1719 1791 1819
|
|
||||||
4 1777 1842 1815 1825 1856 1801
|
|
||||||
4 1819 1733 1664 1620 1613 1620
|
|
||||||
4 1616 1647 1627 1627 1671 1668
|
|
||||||
;Demand Pattern for Node 203
|
|
||||||
5 4439 4531 4511 4582 4531 4582
|
|
||||||
5 4572 4613 4643 4643 4592 4613
|
|
||||||
5 4531 4521 4449 4439 4449 4460
|
|
||||||
5 4439 4419 4368 4399 4470 4480
|
|
||||||
|
|
||||||
[CURVES]
|
|
||||||
;ID X-Value Y-Value
|
|
||||||
;PUMP: Pump Curve for Pump 10 (Lake Source)
|
|
||||||
1 0 104.
|
|
||||||
1 2000. 92.
|
|
||||||
1 4000. 63.
|
|
||||||
;PUMP: Pump Curve for Pump 335 (River Source)
|
|
||||||
2 0 200.
|
|
||||||
2 8000. 138.
|
|
||||||
2 14000. 86.
|
|
||||||
|
|
||||||
[CONTROLS]
|
|
||||||
;Lake source operates only part of the day
|
|
||||||
Link 10 OPEN AT TIME 1
|
|
||||||
Link 10 CLOSED AT TIME 15
|
|
||||||
|
|
||||||
;Pump 335 controlled by level in Tank 1
|
|
||||||
;When pump is closed, bypass pipe is opened
|
|
||||||
Link 335 OPEN IF Node 1 BELOW 17.1
|
|
||||||
Link 335 CLOSED IF Node 1 ABOVE 19.1
|
|
||||||
Link 330 CLOSED IF Node 1 BELOW 17.1
|
|
||||||
Link 330 OPEN IF Node 1 ABOVE 19.1
|
|
||||||
|
|
||||||
|
|
||||||
[RULES]
|
|
||||||
|
|
||||||
[ENERGY]
|
|
||||||
Global Efficiency 75
|
|
||||||
Global Price 0.0
|
|
||||||
Demand Charge 0.0
|
|
||||||
|
|
||||||
[EMITTERS]
|
|
||||||
;Junction Coefficient
|
|
||||||
|
|
||||||
[QUALITY]
|
|
||||||
;Node InitQual
|
|
||||||
|
|
||||||
[SOURCES]
|
|
||||||
;Node Type Quality Pattern
|
|
||||||
|
|
||||||
[REACTIONS]
|
|
||||||
;Type Pipe/Tank Coefficient
|
|
||||||
|
|
||||||
|
|
||||||
[REACTIONS]
|
|
||||||
Order Bulk 1
|
|
||||||
Order Tank 1
|
|
||||||
Order Wall 1
|
|
||||||
Global Bulk 0.0
|
|
||||||
Global Wall 0.0
|
|
||||||
Limiting Potential 0.0
|
|
||||||
Roughness Correlation 0.0
|
|
||||||
|
|
||||||
[MIXING]
|
|
||||||
;Tank Model
|
|
||||||
|
|
||||||
[TIMES]
|
|
||||||
Duration 24:00
|
|
||||||
Hydraulic Timestep 1:00
|
|
||||||
Quality Timestep 0:05
|
|
||||||
Pattern Timestep 1:00
|
|
||||||
Pattern Start 0:00
|
|
||||||
Report Timestep 1:00
|
|
||||||
Report Start 0:00
|
|
||||||
Start ClockTime 12 am
|
|
||||||
Statistic None
|
|
||||||
|
|
||||||
[REPORT]
|
|
||||||
Status Yes
|
|
||||||
Summary No
|
|
||||||
Page 0
|
|
||||||
|
|
||||||
[OPTIONS]
|
|
||||||
Units GPM
|
|
||||||
Headloss H-W
|
|
||||||
Specific Gravity 1.0
|
|
||||||
Viscosity 1.0
|
|
||||||
Trials 40
|
|
||||||
Accuracy 0.001
|
|
||||||
CHECKFREQ 2
|
|
||||||
MAXCHECK 10
|
|
||||||
DAMPLIMIT 0
|
|
||||||
Unbalanced Continue 10
|
|
||||||
Pattern 1
|
|
||||||
Demand Multiplier 1.0
|
|
||||||
Emitter Exponent 0.5
|
|
||||||
Quality Trace Lake
|
|
||||||
Diffusivity 1.0
|
|
||||||
Tolerance 0.01
|
|
||||||
|
|
||||||
[COORDINATES]
|
|
||||||
;Node X-Coord Y-Coord
|
|
||||||
10 9.00 27.85
|
|
||||||
15 38.68 23.76
|
|
||||||
20 29.44 26.91
|
|
||||||
35 25.46 10.52
|
|
||||||
40 27.02 9.81
|
|
||||||
50 33.01 3.01
|
|
||||||
60 23.90 29.94
|
|
||||||
601 23.00 29.49
|
|
||||||
61 23.71 29.03
|
|
||||||
101 13.81 22.94
|
|
||||||
103 12.96 21.31
|
|
||||||
105 16.97 21.28
|
|
||||||
107 18.45 20.46
|
|
||||||
109 17.64 18.92
|
|
||||||
111 20.21 17.53
|
|
||||||
113 22.04 16.61
|
|
||||||
115 20.98 19.18
|
|
||||||
117 21.69 21.28
|
|
||||||
119 23.70 22.76
|
|
||||||
120 22.08 23.10
|
|
||||||
121 23.54 25.50
|
|
||||||
123 23.37 27.31
|
|
||||||
125 24.59 25.64
|
|
||||||
127 29.29 26.40
|
|
||||||
129 30.32 26.39
|
|
||||||
131 37.89 29.55
|
|
||||||
139 33.28 24.54
|
|
||||||
141 35.68 23.08
|
|
||||||
143 37.47 21.97
|
|
||||||
145 33.02 19.29
|
|
||||||
147 30.24 20.38
|
|
||||||
149 29.62 20.74
|
|
||||||
151 28.29 21.39
|
|
||||||
153 28.13 22.63
|
|
||||||
157 24.85 20.16
|
|
||||||
159 23.12 17.50
|
|
||||||
161 25.10 15.28
|
|
||||||
163 25.39 14.98
|
|
||||||
164 25.98 15.14
|
|
||||||
166 26.48 15.13
|
|
||||||
167 25.88 12.98
|
|
||||||
169 25.68 12.74
|
|
||||||
171 26.65 11.80
|
|
||||||
173 26.87 11.59
|
|
||||||
177 26.00 10.50
|
|
||||||
179 25.71 10.40
|
|
||||||
181 25.72 10.74
|
|
||||||
183 25.45 10.18
|
|
||||||
184 25.15 9.52
|
|
||||||
185 25.01 9.67
|
|
||||||
187 23.64 11.04
|
|
||||||
189 24.15 11.37
|
|
||||||
191 22.10 14.07
|
|
||||||
193 22.88 14.35
|
|
||||||
195 23.18 14.72
|
|
||||||
197 20.97 15.18
|
|
||||||
199 29.42 8.44
|
|
||||||
201 30.89 8.57
|
|
||||||
203 31.14 8.89
|
|
||||||
204 23.80 10.90
|
|
||||||
205 29.20 6.46
|
|
||||||
206 31.66 6.64
|
|
||||||
207 31.00 6.61
|
|
||||||
208 32.54 6.81
|
|
||||||
209 33.76 6.59
|
|
||||||
211 34.20 5.54
|
|
||||||
213 35.26 6.16
|
|
||||||
215 39.95 8.73
|
|
||||||
217 42.11 8.67
|
|
||||||
219 44.86 9.32
|
|
||||||
225 43.53 7.38
|
|
||||||
229 36.16 3.49
|
|
||||||
231 38.38 2.54
|
|
||||||
237 35.37 3.08
|
|
||||||
239 35.76 2.31
|
|
||||||
241 35.87 2.11
|
|
||||||
243 37.04 0.00
|
|
||||||
247 35.02 2.05
|
|
||||||
249 35.02 1.81
|
|
||||||
251 34.15 1.10
|
|
||||||
253 32.17 1.88
|
|
||||||
255 33.51 2.45
|
|
||||||
257 21.17 23.32
|
|
||||||
259 20.80 23.40
|
|
||||||
261 20.79 21.45
|
|
||||||
263 20.32 21.57
|
|
||||||
265 25.39 13.60
|
|
||||||
267 23.38 12.95
|
|
||||||
269 25.03 12.14
|
|
||||||
271 25.97 11.00
|
|
||||||
273 29.16 7.38
|
|
||||||
275 31.07 8.29
|
|
||||||
River 24.15 31.06
|
|
||||||
Lake 8.00 27.53
|
|
||||||
1 27.46 9.84
|
|
||||||
2 32.99 3.45
|
|
||||||
3 29.41 27.27
|
|
||||||
|
|
||||||
[VERTICES]
|
|
||||||
;Link X-Coord Y-Coord
|
|
||||||
|
|
||||||
[LABELS]
|
|
||||||
;X-Coord Y-Coord Label & Anchor Node
|
|
||||||
8.00 29.42 "LAKE"
|
|
||||||
25.00 31.10 "RIVER"
|
|
||||||
|
|
||||||
[BACKDROP]
|
|
||||||
DIMENSIONS 6.16 -1.55 46.70 32.61
|
|
||||||
UNITS None
|
|
||||||
FILE
|
|
||||||
OFFSET 0.00 0.00
|
|
||||||
|
|
||||||
[END]
|
|
||||||
Binary file not shown.
Binary file not shown.
-77766
File diff suppressed because it is too large
Load Diff
+1
-2
@@ -43,7 +43,6 @@ httpx==0.28.1
|
|||||||
httpx-sse==0.4.3
|
httpx-sse==0.4.3
|
||||||
idna==3.10
|
idna==3.10
|
||||||
importlib_metadata==8.7.1
|
importlib_metadata==8.7.1
|
||||||
influxdb-client==1.48.0
|
|
||||||
iniconfig==2.0.0
|
iniconfig==2.0.0
|
||||||
jaraco.classes==3.4.0
|
jaraco.classes==3.4.0
|
||||||
jaraco.context==6.1.0
|
jaraco.context==6.1.0
|
||||||
@@ -103,6 +102,7 @@ pyarmor==9.0.7
|
|||||||
pyarmor.cli.core==7.6.4
|
pyarmor.cli.core==7.6.4
|
||||||
pybind11-stubgen==2.5.1
|
pybind11-stubgen==2.5.1
|
||||||
pycparser==2.23
|
pycparser==2.23
|
||||||
|
pyclipper==1.4.0
|
||||||
pydantic==2.10.6
|
pydantic==2.10.6
|
||||||
pydantic-settings==2.12.0
|
pydantic-settings==2.12.0
|
||||||
pydantic_core==2.27.2
|
pydantic_core==2.27.2
|
||||||
@@ -168,4 +168,3 @@ zmq==0.0.0
|
|||||||
pymoo==0.6.1.6
|
pymoo==0.6.1.6
|
||||||
scikit-learn==1.6.1
|
scikit-learn==1.6.1
|
||||||
scipy==1.15.2
|
scipy==1.15.2
|
||||||
pyclipper==1.4.0
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,138 +0,0 @@
|
|||||||
id,type,associated_element_id,associated_pattern,associated_pipe_flow_id,associated_source_outflow_id1,associated_source_outflow_id2,associated_source_outflow_id3,associated_source_outflow_id4,associated_source_outflow_id5,API_query_id,transmission_mode,transmission_frequency,reliability,X_coor,Y_coor
|
|
||||||
ZBBDJSCP000002,reservoir_liquid_level,ZBBDJSCP000002,HighPressure,,,,,,,2497,realtime,0:01:00,1,106.414403,29.83252977
|
|
||||||
R00003,reservoir_liquid_level,R00003,LowPressure,,,,,,,2571,realtime,0:01:00,1,106.414403,29.83252977
|
|
||||||
ZBBDTJSC000002,tank_liquid_level,ZBBDTJSC000002,,,,,,,,4780,realtime,0:01:00,1,106.377934,29.80097177
|
|
||||||
ZBBDTJSC000001,tank_liquid_level,ZBBDTJSC000001,,,,,,,,9774,realtime,0:01:00,1,106.414257,29.82031872
|
|
||||||
PU00000,fixed_pump,PU00000,1#,,,,,,,2747,realtime,0:01:00,1,106.414403,29.83252977
|
|
||||||
PU00001,fixed_pump,PU00001,2#,,,,,,,2776,realtime,0:01:00,1,106.414403,29.83252977
|
|
||||||
PU00002,fixed_pump,PU00002,3#,,,,,,,2730,realtime,0:01:00,1,106.414403,29.83252977
|
|
||||||
PU00003,fixed_pump,PU00003,4#,,,,,,,2787,realtime,0:01:00,1,106.414403,29.83252977
|
|
||||||
PU00004,variable_pump,PU00004,5#,,,,,,,2500,realtime,0:01:00,1,106.414403,29.83252977
|
|
||||||
PU00005,variable_pump,PU00005,6#,,,,,,,2502,realtime,0:01:00,1,106.414403,29.83252977
|
|
||||||
PU00006,variable_pump,PU00006,7#,,,,,,,2504,realtime,0:01:00,1,106.414403,29.83252977
|
|
||||||
ZBBGXSZW000377,source_outflow,ZBBGXSZW000377,DN900,,ZBBGXSZW000377,,,,,2498,realtime,0:01:00,1,106.4194993,29.83772834
|
|
||||||
P17021,source_outflow,P17021,DN500,,P17021,,,,,3854,realtime,0:01:00,1,106.4194939,29.83810336
|
|
||||||
P16504,source_outflow,P16504,DN1000,,P16504,,,,,3853,realtime,0:01:00,1,106.4195338,29.83781925
|
|
||||||
ZBBGXSZK001071,pipe_flow,ZBBGXSZK001071,BianDianZhan,,ZBBGXSZW000377,P17021,,,,9521,non_realtime,6:00:00,1,106.4003826,29.81743895
|
|
||||||
ZBBGXSZK001163,pipe_flow,ZBBGXSZK001163,BeiNanDaDao,,ZBBGXSZW000377,P17021,,,,9478,non_realtime,6:00:00,1,106.3994217,29.80947624
|
|
||||||
ZBBGXSZK001105,pipe_flow,ZBBGXSZK001105,TianShengLiJie,,ZBBGXSZW000377,P17021,,,,9506,non_realtime,6:00:00,1,106.4180443,29.81625954
|
|
||||||
P10755,pipe_flow,P10755,XueYuanXiaoQu,,ZBBGXSZW000377,P17021,,,,9497,non_realtime,6:00:00,1,106.4131703,29.81316079
|
|
||||||
ZBBGXSZK001128,pipe_flow,ZBBGXSZK001128,YunHuaLu,,ZBBGXSZW000377,P17021,,,,9480,non_realtime,6:00:00,1,106.3948247,29.80702128
|
|
||||||
P10839,pipe_flow,P10839,GaoJiaQiao,,ZBBGXSZW000377,P17021,,,,9507,non_realtime,6:00:00,1,106.4079379,29.82356299
|
|
||||||
ZBBGXSZK001079,pipe_flow,ZBBGXSZK001079,LuZuoFuLuXiaDuan,,ZBBGXSZW000377,P17021,,,,9487,non_realtime,6:00:00,1,106.3922472,29.80343211
|
|
||||||
ZBBGXSZK001096(L),pipe_flow,ZBBGXSZK001096(L),TianRunCheng,,ZBBGXSZW000377,P17021,,,,9485,non_realtime,6:00:00,1,106.3907546,29.80015062
|
|
||||||
P15959,pipe_flow,P15959,CaoJiaBa,,ZBBGXSZW000377,P17021,,,,9505,non_realtime,6:00:00,1,106.3664514,29.76674381
|
|
||||||
ZBBGXSZK001031,pipe_flow,ZBBGXSZK001031,PuLingChang,,ZBBGXSZW000377,P17021,,,,9481,non_realtime,6:00:00,1,106.3807941,29.75983691
|
|
||||||
P12544,pipe_flow,P12544,QiLongXiaoQu,,ZBBGXSZW000377,P17021,,,,9524,non_realtime,6:00:00,1,106.367557,29.76592301
|
|
||||||
ZBBGXTYK003616,pipe_flow,ZBBGXTYK003616,TuanXiao,,ZBBGXSZW000377,P17021,,,,9498,non_realtime,6:00:00,1,106.3741092,29.77647684
|
|
||||||
ZBBGXTYK003427,pipe_flow,ZBBGXTYK003427,ChengBeiCaiShiKou,,P16504,,,,,9488,non_realtime,6:00:00,1,106.4211904,29.8337277
|
|
||||||
ZBBGXSZK001775,pipe_flow,ZBBGXSZK001775,WenXingShe,,P16504,,,,,9482,non_realtime,6:00:00,1,106.4296384,29.83322552
|
|
||||||
ZBBGXTYK003392,pipe_flow,ZBBGXTYK003392,YueLiangTianBBGJCZ,,P16504,,,,,9510,non_realtime,6:00:00,1,106.4338386,29.82316156
|
|
||||||
ZBBGXSZK001166,pipe_flow,ZBBGXSZK001166,YueLiangTian,,P16504,,,,,9486,non_realtime,6:00:00,1,106.4324165,29.82465186
|
|
||||||
P13961,pipe_flow,P13961,YueLiangTian200,,P16504,,,,,9520,non_realtime,6:00:00,1,106.4256,29.8222
|
|
||||||
ZBBGXTYK003069,pipe_flow,ZBBGXTYK003069,ChengTaoChang,,P16504,,,,,9479,non_realtime,6:00:00,1,106.431,29.8153
|
|
||||||
ZBBGXTYK003355,pipe_flow,ZBBGXTYK003355,HuoCheZhan,,P16504,,,,,9518,non_realtime,6:00:00,1,106.4284178,29.80932951
|
|
||||||
ZBBGXSZW000335,pipe_flow,ZBBGXSZW000335,LiangKu,,P16504,,,,,9512,non_realtime,6:00:00,1,106.4241088,29.80023184
|
|
||||||
ZBBGXSZK001957,pipe_flow,ZBBGXSZK001957,QunXingLu,,P16504,,,,,9489,non_realtime,6:00:00,1,106.4302597,29.81089267
|
|
||||||
P05940,pipe_flow,P05940,TuanShanBaoZhongShiHua,,ZBBGXSZW000377,P17021,,,,9535,non_realtime,6:00:00,1,106.4168586,29.83510228
|
|
||||||
ZBBGXSZK002419,pipe_flow,ZBBGXSZK002419,XieMa,,ZBBGXSZW000377,P17021,,,,7423,non_realtime,6:00:00,1,106.3893687,29.79893615
|
|
||||||
P16512,pipe_flow,P16512,BeiWenQuanJiuHaoErQi,,ZBBGXSZW000377,P17021,,,,,non_realtime,6:00:00,1,106.4077965,29.82305305
|
|
||||||
GSD230117133902A0620C9E4A2F,pipe_flow,GSD230117133902A0620C9E4A2F,LaiYinHuSiQi,,ZBBGXSZW000377,P17021,,,,,non_realtime,6:00:00,1,106.3793446,29.80116239
|
|
||||||
ZBBGXSZK000647,pipe_flow,ZBBGXSZK000647,JiuYuanErTongYiYuan,,P16504,,,,,,non_realtime,6:00:00,1,106.4256984,29.82186294
|
|
||||||
ZBBGXSZK000247,pipe_flow,ZBBGXSZK000247,TangDouHua,,P16504,,,,,,non_realtime,6:00:00,1,106.4351488,29.83505307
|
|
||||||
P16773,pipe_flow,P16773,TaiJiBinJiangErQi(SanJi),,P16504,,,,,9531,non_realtime,6:00:00,1,106.4453773,29.82620736
|
|
||||||
ZBBGXSZK002373,pipe_flow,ZBBGXSZK002373,ZhangDouHua,,P16504,,,,,,non_realtime,6:00:00,1,106.4404323,29.82998935
|
|
||||||
P12942,pipe_flow,P12942,JinYunXiaoQuDN400,,P16504,,,,,,non_realtime,6:00:00,1,106.3865726,29.80903033
|
|
||||||
P16769,pipe_flow,P16769,ShiYanCun,,P16504,,,,,9492,non_realtime,6:00:00,1,106.4307834,29.82932991
|
|
||||||
ZBBDTZDP004690,demand,ZBBDTZDP004690,ChuanYiJiXiao,,ZBBGXSZW000377,P17021,,,,9501,non_realtime,6:00:00,1,106.4092,29.8601
|
|
||||||
ZBBDFQJL000025,demand,ZBBDFQJL000025,BeiQuanHuaYuan,,ZBBGXSZW000377,P17021,,,,9405,non_realtime,6:00:00,1,106.4121,29.8275
|
|
||||||
ZBBDFQJL000030,demand,ZBBDFQJL000030,ZhuangYuanFuDi,,ZBBGXSZW000377,P17021,,,,9509,non_realtime,6:00:00,1,106.4021,29.8021
|
|
||||||
ZBBDFQJL000036,demand,ZBBDFQJL000036,JingNingJiaYuan,,ZBBGXSZW000377,P17021,,,,9494,non_realtime,6:00:00,1,106.3987,29.8095
|
|
||||||
ZBBDFQJL000034,demand,ZBBDFQJL000034,308,,ZBBGXSZW000377,P17021,,,,9504,non_realtime,6:00:00,1,106.4008,29.8166
|
|
||||||
ZBBDFQJL000032,demand,ZBBDFQJL000032,JiaYinYuan,,ZBBGXSZW000377,P17021,,,,9513,non_realtime,6:00:00,1,106.3889,29.8048
|
|
||||||
ZBBDFQJL000031,demand,ZBBDFQJL000031,XinChengGuoJi,,ZBBGXSZW000377,P17021,,,,9517,non_realtime,6:00:00,1,106.389,29.805
|
|
||||||
ZBBDFQJL000033,demand,ZBBDFQJL000033,YiJingBeiChen,,ZBBGXSZW000377,P17021,,,,9519,non_realtime,6:00:00,1,106.3906,29.8041
|
|
||||||
ZBBDFQJL000035,demand,ZBBDFQJL000035,ZhongYangXinDu,,ZBBGXSZW000377,P17021,,,,9522,non_realtime,6:00:00,1,106.3937,29.8044
|
|
||||||
J06186,demand,J06186,XinHaiJiaYuan,,ZBBGXSZW000377,P17021,,,,9525,non_realtime,6:00:00,1,106.4067,29.8278
|
|
||||||
ZBBDFQJL000052,demand,ZBBDFQJL000052,DongFengJie,,ZBBGXSZW000377,P17021,,,,9526,non_realtime,6:00:00,1,106.3678,29.766
|
|
||||||
ZBBDFQJL000049,demand,ZBBDFQJL000049,DingYaXinYu,,ZBBGXSZW000377,P17021,,,,9500,non_realtime,6:00:00,1,106.3674,29.7654
|
|
||||||
GSD2302160925534D50CD17540D_E,demand,GSD2302160925534D50CD17540D_End,ZiYunTai,,ZBBGXSZW000377,P17021,,,,9529,non_realtime,6:00:00,1,106.387,29.7884
|
|
||||||
ZBBDFQJL000050,demand,ZBBDFQJL000050,XieMaGuangChang,,ZBBGXSZW000377,P17021,,,,9477,non_realtime,6:00:00,1,106.3652,29.7657
|
|
||||||
GSD2307192058578BCE265C6EA8,demand,GSD2307192058578BCE265C6EA8,YongJinFu,,ZBBGXSZW000377,P17021,,,,9534,non_realtime,6:00:00,1,106.3831,29.781
|
|
||||||
ZBBDFQJL000028,demand,ZBBDFQJL000028,PanXiMingDu,,P16504,,,,,9514,non_realtime,6:00:00,1,106.4223,29.821
|
|
||||||
GSD230113095617514F4A2066D7_E,demand,GSD230113095617514F4A2066D7_End,WanKeJinYuHuaFuGaoCeng,,P16504,,,,,9528,non_realtime,6:00:00,1,106.4228,29.8113
|
|
||||||
ZBBDFQJL000014,demand,ZBBDFQJL000014,KeJiXiao,,P16504,,,,,9503,non_realtime,6:00:00,1,106.4332,29.8258
|
|
||||||
ZBBDFQJL000016,demand,ZBBDFQJL000016,LuGouQiao,,P16504,,,,,9527,non_realtime,6:00:00,1,106.437,29.8285
|
|
||||||
ZBBDFQJL000057,demand,ZBBDFQJL000057,LongJiangHuaYuan,,P16504,,,,,9495,non_realtime,6:00:00,1,106.4316,29.8309
|
|
||||||
ZBBDFQJL000012,demand,ZBBDFQJL000012,LaoQiZhongDui,,P16504,,,,,9496,non_realtime,6:00:00,1,106.433,29.8239
|
|
||||||
ZBBDFQJL000007,demand,ZBBDFQJL000007,TianQiDaSha,,P16504,,,,,9502,non_realtime,6:00:00,1,106.4346,29.8319
|
|
||||||
ZBBDFQJL000013,demand,ZBBDFQJL000013,TianShengPaiChuSuo,,P16504,,,,,9499,non_realtime,6:00:00,1,106.4253,29.8222
|
|
||||||
ZBBDFQJL000037,demand,ZBBDFQJL000037,TianShengShangPin,,P16504,,,,,9493,non_realtime,6:00:00,1,106.4305,29.8264
|
|
||||||
ZBBDFQJL000009,demand,ZBBDFQJL000009,JiaoTang,,P16504,,,,,9511,non_realtime,6:00:00,1,106.4402,29.826
|
|
||||||
ZBBDFQJL000011,demand,ZBBDFQJL000011,RenMinHuaYuan,,P16504,,,,,9508,non_realtime,6:00:00,1,106.4359,29.8238
|
|
||||||
ZBBDFQJL000005,demand,ZBBDFQJL000005,TaiJiBinJiangYiQi,,P16504,,,,,9484,non_realtime,6:00:00,1,106.4443,29.8245
|
|
||||||
ZBBDFQJL000006,demand,ZBBDFQJL000006,TianQiHuaYuan,,P16504,,,,,9523,non_realtime,6:00:00,1,106.4361,29.8189
|
|
||||||
ZBBDFQJL000004,demand,ZBBDFQJL000004,TaiJiBinJiangErQi,,P16504,,,,,9515,non_realtime,6:00:00,1,106.4443,29.8297
|
|
||||||
ZBBDFQJL000039,demand,ZBBDFQJL000039,122Zhong,,P16504,,,,,9516,non_realtime,6:00:00,1,106.4284,29.8109
|
|
||||||
GSD230112144241F42EF6065148_E,demand,GSD230112144241F42EF6065148_End,WanKeJinYuHuaFuYangFang,,P16504,,,,,9530,non_realtime,6:00:00,1,106.4263,29.8154
|
|
||||||
J06194,pressure,J06194,,,,,,,,2514,realtime,0:01:00,1,106.4195183,29.83782213
|
|
||||||
J06190,pressure,J06190,,,,,,,,2510,realtime,0:01:00,1,106.4194644,29.83781489
|
|
||||||
ZBBDFQJL000025_p,pressure,ZBBDFQJL000025,,,,,,,,9536,non_realtime,6:00:00,1,106.4120554,29.82753087
|
|
||||||
ZBBDFQJL000050_p,pressure,ZBBDFQJL000050,,,,,,,,9537,non_realtime,6:00:00,1,106.3651228,29.76558811
|
|
||||||
ZBBDFQJL000018_p,pressure,ZBBDFQJL000018,,,,,,,,9538,non_realtime,6:00:00,1,106.3994298,29.809484
|
|
||||||
ZBBDFQJL000002_p,pressure,ZBBDFQJL000002,,,,,,,,9539,non_realtime,6:00:00,1,106.4309932,29.81534215
|
|
||||||
ZBBDFQJL000019_p,pressure,ZBBDFQJL000019,,,,,,,,9540,non_realtime,6:00:00,1,106.3948196,29.80700511
|
|
||||||
ZBBDFQJL000047_p,pressure,ZBBDFQJL000047,,,,,,,,9541,non_realtime,6:00:00,1,106.3807979,29.75983831
|
|
||||||
ZBBDFQJL000001_p,pressure,ZBBDFQJL000001,,,,,,,,9542,non_realtime,6:00:00,1,106.4296304,29.83324097
|
|
||||||
ZBBDTZDP006510_p,pressure,ZBBDTZDP006510,,,,,,,,9543,non_realtime,6:00:00,1,106.4143,29.8204
|
|
||||||
ZBBDFQJL000005_p,pressure,ZBBDFQJL000005,,,,,,,,9544,non_realtime,6:00:00,1,106.4442613,29.824473
|
|
||||||
ZBBDFQJL000023_p,pressure,ZBBDFQJL000023,,,,,,,,9545,non_realtime,6:00:00,1,106.3907545,29.80014626
|
|
||||||
ZBBDPQFM000144_p,pressure,ZBBDPQFM000144,,,,,,,,9546,non_realtime,6:00:00,1,106.4324087,29.82466081
|
|
||||||
ZBBDFQJL000022_p,pressure,ZBBDFQJL000022,,,,,,,,9547,non_realtime,6:00:00,1,106.3922466,29.80343116
|
|
||||||
ZBBDFQJL000026_p,pressure,ZBBDFQJL000026,,,,,,,,9548,non_realtime,6:00:00,1,106.4211902,29.83372698
|
|
||||||
ZBBDFQJL000027_p,pressure,ZBBDFQJL000027,,,,,,,,9549,non_realtime,6:00:00,1,106.4302659,29.81087884
|
|
||||||
ZBBDFQJL000020_p,pressure,ZBBDFQJL000020,,,,,,,,9551,non_realtime,6:00:00,1,106.4069939,29.81230584
|
|
||||||
ZBBDFQJL000015_p,pressure,ZBBDFQJL000015,,,,,,,,9552,non_realtime,6:00:00,1,106.4307918,29.82933519
|
|
||||||
ZBBDFQJL000037_p,pressure,ZBBDFQJL000037,,,,,,,,9553,non_realtime,6:00:00,1,106.4305041,29.82636702
|
|
||||||
ZBBDFQJL000036_p,pressure,ZBBDFQJL000036,,,,,,,,9554,non_realtime,6:00:00,1,106.3987322,29.80947672
|
|
||||||
ZBBDFQJL000057_p,pressure,ZBBDFQJL000057,,,,,,,,9555,non_realtime,6:00:00,1,106.4315801,29.83086849
|
|
||||||
ZBBDFQJL000012_p,pressure,ZBBDFQJL000012,,,,,,,,9556,non_realtime,6:00:00,1,106.4330174,29.82386587
|
|
||||||
ZBBDFQJL000021_p,pressure,ZBBDFQJL000021,,,,,,,,9557,non_realtime,6:00:00,1,106.413164,29.81315876
|
|
||||||
ZBBDFQJL000045_p,pressure,ZBBDFQJL000045,,,,,,,,9558,non_realtime,6:00:00,1,106.3741065,29.77646525
|
|
||||||
ZBBDFQJL000013_p,pressure,ZBBDFQJL000013,,,,,,,,9559,non_realtime,6:00:00,1,106.4253422,29.82216074
|
|
||||||
ZBBDFQJL000049_p,pressure,ZBBDFQJL000049,,,,,,,,9560,non_realtime,6:00:00,1,106.3673675,29.76535923
|
|
||||||
ZBBDFQJL000053_p,pressure,ZBBDFQJL000053,,,,,,,,9561,non_realtime,6:00:00,1,106.4091385,29.86001467
|
|
||||||
ZBBDFQJL000007_p,pressure,ZBBDFQJL000007,,,,,,,,9562,non_realtime,6:00:00,1,106.4345765,29.83187159
|
|
||||||
ZBBDFQJL000014_p,pressure,ZBBDFQJL000014,,,,,,,,9563,non_realtime,6:00:00,1,106.4332467,29.82578845
|
|
||||||
ZBBDFQJL000034_p,pressure,ZBBDFQJL000034,,,,,,,,9564,non_realtime,6:00:00,1,106.4008362,29.81662925
|
|
||||||
ZBBDFQJL000051_p,pressure,ZBBDFQJL000051,,,,,,,,9565,non_realtime,6:00:00,1,106.3664647,29.76674002
|
|
||||||
ZBBDFQJL000029_p,pressure,ZBBDFQJL000029,,,,,,,,9566,non_realtime,6:00:00,1,106.4180536,29.81625021
|
|
||||||
ZBBDFQJL000038_p,pressure,ZBBDFQJL000038,,,,,,,,9567,non_realtime,6:00:00,1,106.4079197,29.82356534
|
|
||||||
ZBBDFQJL000011_p,pressure,ZBBDFQJL000011,,,,,,,,9568,non_realtime,6:00:00,1,106.4359412,29.82375869
|
|
||||||
ZBBDFQJL000030_p,pressure,ZBBDFQJL000030,,,,,,,,9569,non_realtime,6:00:00,1,106.4021179,29.80210907
|
|
||||||
ZBBDFQJL000024_p,pressure,ZBBDFQJL000024,,,,,,,,9570,non_realtime,6:00:00,1,106.4338355,29.82316474
|
|
||||||
ZBBDFQJL000009_p,pressure,ZBBDFQJL000009,,,,,,,,9571,non_realtime,6:00:00,1,106.4402264,29.825971
|
|
||||||
ZBBDFQJL000042_p,pressure,ZBBDFQJL000042,,,,,,,,9572,non_realtime,6:00:00,1,106.4241609,29.80026986
|
|
||||||
ZBBDFQJL000032_p,pressure,ZBBDFQJL000032,,,,,,,,9573,non_realtime,6:00:00,1,106.3889269,29.80481683
|
|
||||||
ZBBDFQJL000028_p,pressure,ZBBDFQJL000028,,,,,,,,9574,non_realtime,6:00:00,1,106.4222702,29.82103146
|
|
||||||
ZBBDFQJL000004_p,pressure,ZBBDFQJL000004,,,,,,,,9575,non_realtime,6:00:00,1,106.4442663,29.82966834
|
|
||||||
ZBBDFQJL000039_p,pressure,ZBBDFQJL000039,,,,,,,,9576,non_realtime,6:00:00,1,106.4284664,29.81088051
|
|
||||||
ZBBDFQJL000031_p,pressure,ZBBDFQJL000031,,,,,,,,9577,non_realtime,6:00:00,1,106.3890122,29.80504763
|
|
||||||
ZBBDFQJL000043_p,pressure,ZBBDFQJL000043,,,,,,,,9578,non_realtime,6:00:00,1,106.4284118,29.80929921
|
|
||||||
ZBBDFQJL000033_p,pressure,ZBBDFQJL000033,,,,,,,,9579,non_realtime,6:00:00,1,106.3906102,29.80408746
|
|
||||||
ZBBDFQJL000010_p,pressure,ZBBDFQJL000010,,,,,,,,9580,non_realtime,6:00:00,1,106.4256312,29.82218279
|
|
||||||
ZBBDFQJL000041_p,pressure,ZBBDFQJL000041,,,,,,,,9581,non_realtime,6:00:00,1,106.4003806,29.81743804
|
|
||||||
ZBBDFQJL000035_p,pressure,ZBBDFQJL000035,,,,,,,,9582,non_realtime,6:00:00,1,106.3937231,29.80441954
|
|
||||||
ZBBDFQJL000006_p,pressure,ZBBDFQJL000006,,,,,,,,9583,non_realtime,6:00:00,1,106.4361409,29.81886211
|
|
||||||
ZBBDFQJL000048_p,pressure,ZBBDFQJL000048,,,,,,,,9584,non_realtime,6:00:00,1,106.367557,29.76592299
|
|
||||||
J06186_p,pressure,J06186,,,,,,,,9585,non_realtime,6:00:00,1,106.4069218,29.82794812
|
|
||||||
ZBBDFQJL000052_p,pressure,ZBBDFQJL000052,,,,,,,,9586,non_realtime,6:00:00,1,106.3678407,29.76602511
|
|
||||||
ZBBDFQJL000016_p,pressure,ZBBDFQJL000016,,,,,,,,9587,non_realtime,6:00:00,1,106.4369887,29.82851071
|
|
||||||
GSD230113095617514F4A2066D7_E_p,pressure,GSD230113095617514F4A2066D7_End,,,,,,,,9588,non_realtime,6:00:00,1,106.4227365,29.811344
|
|
||||||
GSD2302160925534D50CD17540D_E_p,pressure,GSD2302160925534D50CD17540D_End,,,,,,,,9589,non_realtime,6:00:00,1,106.3870281,29.78840589
|
|
||||||
GSD230112144241F42EF6065148_E_p,pressure,GSD230112144241F42EF6065148_End,,,,,,,,9590,non_realtime,6:00:00,1,106.4262502,29.81534482
|
|
||||||
J06198_p,pressure,J06198,,,,,,,,9591,non_realtime,6:00:00,1,106.4454034,29.82619719
|
|
||||||
J06197_p,pressure,J06197,,,,,,,,9592,non_realtime,6:00:00,1,106.4380272,29.82741219
|
|
||||||
ZBBDYJGP000127_p,pressure,ZBBDYJGP000127,,,,,,,,9593,non_realtime,6:00:00,1,106.4006775,29.81822706
|
|
||||||
GSD2307192058578BCE265C6EA8_p,pressure,GSD2307192058578BCE265C6EA8,,,,,,,,9594,non_realtime,6:00:00,1,106.383014,29.78097074
|
|
||||||
ZBBDTZDP008185_p,pressure,ZBBDTZDP008185,,,,,,,,9595,non_realtime,6:00:00,1,106.4168547,29.83510138
|
|
||||||
|
@@ -1,67 +0,0 @@
|
|||||||
-- ============================================
|
|
||||||
-- TJWater Server 用户系统数据库迁移脚本
|
|
||||||
-- ============================================
|
|
||||||
|
|
||||||
-- 创建用户表
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
username VARCHAR(50) UNIQUE NOT NULL,
|
|
||||||
email VARCHAR(100) UNIQUE NOT NULL,
|
|
||||||
hashed_password VARCHAR(255) NOT NULL,
|
|
||||||
role VARCHAR(20) DEFAULT 'USER' NOT NULL,
|
|
||||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
|
||||||
is_superuser BOOLEAN DEFAULT FALSE NOT NULL,
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
||||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT users_role_check CHECK (role IN ('ADMIN', 'OPERATOR', 'USER', 'VIEWER'))
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 创建索引
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_is_active ON users(is_active);
|
|
||||||
|
|
||||||
-- 创建触发器自动更新 updated_at
|
|
||||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
|
||||||
RETURNS TRIGGER AS $$
|
|
||||||
BEGIN
|
|
||||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
|
||||||
RETURN NEW;
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
|
|
||||||
DROP TRIGGER IF EXISTS update_users_updated_at ON users;
|
|
||||||
CREATE TRIGGER update_users_updated_at
|
|
||||||
BEFORE UPDATE ON users
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE FUNCTION update_updated_at_column();
|
|
||||||
|
|
||||||
-- 创建默认管理员账号 (密码: admin123)
|
|
||||||
INSERT INTO users (username, email, hashed_password, role, is_superuser)
|
|
||||||
VALUES (
|
|
||||||
'admin',
|
|
||||||
'admin@tjwater.com',
|
|
||||||
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5aeAJK.1tYKAW',
|
|
||||||
'ADMIN',
|
|
||||||
TRUE
|
|
||||||
) ON CONFLICT (username) DO NOTHING;
|
|
||||||
|
|
||||||
-- 迁移现有硬编码用户 (tjwater/tjwater@123)
|
|
||||||
INSERT INTO users (username, email, hashed_password, role, is_superuser)
|
|
||||||
VALUES (
|
|
||||||
'tjwater',
|
|
||||||
'tjwater@tjwater.com',
|
|
||||||
'$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW',
|
|
||||||
'ADMIN',
|
|
||||||
TRUE
|
|
||||||
) ON CONFLICT (username) DO NOTHING;
|
|
||||||
|
|
||||||
-- 添加注释
|
|
||||||
COMMENT ON TABLE users IS '用户表 - 存储系统用户信息';
|
|
||||||
COMMENT ON COLUMN users.id IS '用户ID(主键)';
|
|
||||||
COMMENT ON COLUMN users.username IS '用户名(唯一)';
|
|
||||||
COMMENT ON COLUMN users.email IS '邮箱地址(唯一)';
|
|
||||||
COMMENT ON COLUMN users.hashed_password IS 'bcrypt 密码哈希';
|
|
||||||
COMMENT ON COLUMN users.role IS '用户角色: ADMIN, OPERATOR, USER, VIEWER';
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
-- ============================================
|
|
||||||
-- TJWater Server 审计日志表迁移脚本
|
|
||||||
-- ============================================
|
|
||||||
|
|
||||||
-- 创建审计日志表
|
|
||||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
|
||||||
username VARCHAR(50),
|
|
||||||
action VARCHAR(50) NOT NULL,
|
|
||||||
resource_type VARCHAR(50),
|
|
||||||
resource_id VARCHAR(100),
|
|
||||||
ip_address VARCHAR(45),
|
|
||||||
user_agent TEXT,
|
|
||||||
request_method VARCHAR(10),
|
|
||||||
request_path TEXT,
|
|
||||||
request_data JSONB,
|
|
||||||
response_status INTEGER,
|
|
||||||
error_message TEXT,
|
|
||||||
timestamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 创建索引以提高查询性能
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_audit_logs_username ON audit_logs(username);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_audit_logs_timestamp ON audit_logs(timestamp DESC);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit_logs(action);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_audit_logs_resource ON audit_logs(resource_type, resource_id);
|
|
||||||
|
|
||||||
-- 添加注释
|
|
||||||
COMMENT ON TABLE audit_logs IS '审计日志表 - 记录所有关键操作';
|
|
||||||
COMMENT ON COLUMN audit_logs.id IS '日志ID(主键)';
|
|
||||||
COMMENT ON COLUMN audit_logs.user_id IS '用户ID(外键)';
|
|
||||||
COMMENT ON COLUMN audit_logs.username IS '用户名(冗余字段,用于用户删除后仍可查询)';
|
|
||||||
COMMENT ON COLUMN audit_logs.action IS '操作类型(如:LOGIN, LOGOUT, CREATE, UPDATE, DELETE)';
|
|
||||||
COMMENT ON COLUMN audit_logs.resource_type IS '资源类型(如:user, project, network)';
|
|
||||||
COMMENT ON COLUMN audit_logs.resource_id IS '资源ID';
|
|
||||||
COMMENT ON COLUMN audit_logs.ip_address IS '客户端IP地址';
|
|
||||||
COMMENT ON COLUMN audit_logs.user_agent IS '客户端User-Agent';
|
|
||||||
COMMENT ON COLUMN audit_logs.request_method IS 'HTTP请求方法';
|
|
||||||
COMMENT ON COLUMN audit_logs.request_path IS '请求路径';
|
|
||||||
COMMENT ON COLUMN audit_logs.request_data IS '请求数据(JSON格式,敏感信息已脱敏)';
|
|
||||||
COMMENT ON COLUMN audit_logs.response_status IS 'HTTP响应状态码';
|
|
||||||
COMMENT ON COLUMN audit_logs.error_message IS '错误消息(如果有)';
|
|
||||||
COMMENT ON COLUMN audit_logs.timestamp IS '操作时间';
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
-- ============================================
|
|
||||||
-- TJWater Server 时区统一迁移脚本
|
|
||||||
-- 将历史无时区时间列升级为 TIMESTAMP WITH TIME ZONE
|
|
||||||
-- 约定:历史无时区值按 UTC 解释
|
|
||||||
-- ============================================
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'public'
|
|
||||||
AND table_name = 'users'
|
|
||||||
AND column_name = 'created_at'
|
|
||||||
AND data_type = 'timestamp without time zone'
|
|
||||||
) THEN
|
|
||||||
EXECUTE 'ALTER TABLE public.users
|
|
||||||
ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE
|
|
||||||
USING created_at AT TIME ZONE ''UTC''';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'public'
|
|
||||||
AND table_name = 'users'
|
|
||||||
AND column_name = 'updated_at'
|
|
||||||
AND data_type = 'timestamp without time zone'
|
|
||||||
) THEN
|
|
||||||
EXECUTE 'ALTER TABLE public.users
|
|
||||||
ALTER COLUMN updated_at TYPE TIMESTAMP WITH TIME ZONE
|
|
||||||
USING updated_at AT TIME ZONE ''UTC''';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'public'
|
|
||||||
AND table_name = 'audit_logs'
|
|
||||||
AND column_name = 'timestamp'
|
|
||||||
AND data_type = 'timestamp without time zone'
|
|
||||||
) THEN
|
|
||||||
EXECUTE 'ALTER TABLE public.audit_logs
|
|
||||||
ALTER COLUMN timestamp TYPE TIMESTAMP WITH TIME ZONE
|
|
||||||
USING "timestamp" AT TIME ZONE ''UTC''';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'public'
|
|
||||||
AND table_name = 'scheme_list'
|
|
||||||
AND column_name = 'scheme_start_time'
|
|
||||||
AND data_type IN ('character varying', 'text')
|
|
||||||
) THEN
|
|
||||||
EXECUTE 'ALTER TABLE public.scheme_list
|
|
||||||
ALTER COLUMN scheme_start_time TYPE TIMESTAMP WITH TIME ZONE
|
|
||||||
USING CASE
|
|
||||||
WHEN scheme_start_time ~ ''(Z|[+-][0-9]{2}:[0-9]{2})$'' THEN scheme_start_time::timestamptz
|
|
||||||
ELSE scheme_start_time::timestamp AT TIME ZONE ''UTC''
|
|
||||||
END';
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
create type _node_type as enum ('junction', 'reservoir', 'tank');
|
|
||||||
|
|
||||||
create type _link_type as enum ('pipe', 'pump', 'valve');
|
|
||||||
|
|
||||||
create type _curve_type as enum ('PUMP', 'EFFICIENCY', 'VOLUME', 'HEADLOSS');
|
|
||||||
|
|
||||||
create type _region_type as enum ('NONE', 'DMA', 'SA', 'VD', 'WDA');
|
|
||||||
|
|
||||||
create table _node
|
|
||||||
(
|
|
||||||
id varchar(32) primary key
|
|
||||||
, type _node_type not null
|
|
||||||
);
|
|
||||||
|
|
||||||
create table _link
|
|
||||||
(
|
|
||||||
id varchar(32) primary key
|
|
||||||
, type _link_type not null
|
|
||||||
);
|
|
||||||
|
|
||||||
create table _curve
|
|
||||||
(
|
|
||||||
id varchar(32) primary key
|
|
||||||
, type _curve_type not null
|
|
||||||
);
|
|
||||||
|
|
||||||
create table _pattern
|
|
||||||
(
|
|
||||||
id varchar(32) primary key
|
|
||||||
);
|
|
||||||
|
|
||||||
create table _region
|
|
||||||
(
|
|
||||||
id varchar(32) primary key
|
|
||||||
, type _region_type not null
|
|
||||||
);
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
-- [TITLE]
|
|
||||||
|
|
||||||
create table title
|
|
||||||
(
|
|
||||||
value text
|
|
||||||
);
|
|
||||||
|
|
||||||
insert into title (value) values ('');
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user