234 lines
8.3 KiB
Python
234 lines
8.3 KiB
Python
"""Idempotently seed synthetic peripheral demo resources for one explicit project."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from uuid import NAMESPACE_URL, UUID, uuid5
|
|
|
|
from psycopg.types.json import Jsonb
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from app.services.content_digest import digest
|
|
from samples.peripheral.auth import Identity
|
|
from samples.peripheral.config import SampleSettings
|
|
from samples.peripheral.demo_data import build_demo_content
|
|
from samples.peripheral.schemas import CreateTestDataRecord, CreateWorkOrder
|
|
from samples.peripheral.store import PostgresStore
|
|
|
|
|
|
SEED_NAMESPACE = uuid5(NAMESPACE_URL, "tjwater/peripheral-sample/seed/v1")
|
|
ACTOR_ID = uuid5(SEED_NAMESPACE, "actor")
|
|
|
|
|
|
def request_id(kind: str) -> UUID:
|
|
return uuid5(SEED_NAMESPACE, kind)
|
|
|
|
|
|
WORK_ORDERS = (
|
|
{
|
|
"key": "work-order/demo-communication",
|
|
"title": "合成通信核查",
|
|
"summary": "检查 DEMO-P001 的预置通信中断情节,仅用于外围样板联调。",
|
|
"status": "open",
|
|
"priority": "high",
|
|
"assignee": "外围样板班组",
|
|
"scheduled_at": "2026-09-17T03:00:00Z",
|
|
},
|
|
{
|
|
"key": "work-order/demo-pressure",
|
|
"title": "合成压力偏差复核",
|
|
"summary": "复核 DEMO-P001、DEMO-P002 相邻测试点的固定合成读数,不代表现场异常。",
|
|
"status": "in_progress",
|
|
"priority": "urgent",
|
|
"assignee": "演示核查班组",
|
|
"scheduled_at": "2026-09-17T04:00:00Z",
|
|
},
|
|
{
|
|
"key": "work-order/flow-zero",
|
|
"title": "合成零流量样本确认",
|
|
"summary": "确认 DEMO-F001 中零值与缺测值可被界面明确区分。",
|
|
"status": "completed",
|
|
"priority": "normal",
|
|
"assignee": "外围样板班组",
|
|
"scheduled_at": "2026-09-17T06:00:00Z",
|
|
},
|
|
)
|
|
|
|
|
|
TEST_DATA = (
|
|
("DEMO-P001", "合成压力 1 · 00:00", "2026-09-17T00:00:00Z", 24.37, "m", "valid"),
|
|
("DEMO-P001", "合成压力 1 · 00:15", "2026-09-17T00:15:00Z", 24.18, "m", "valid"),
|
|
("DEMO-P001", "合成压力 1 · 00:30 缺测", "2026-09-17T00:30:00Z", None, "m", "missing"),
|
|
("DEMO-P001", "合成压力 1 · 00:45", "2026-09-17T00:45:00Z", 24.42, "m", "valid"),
|
|
("DEMO-P002", "合成压力 2 · 00:00", "2026-09-17T00:00:00Z", 31.26, "m", "valid"),
|
|
("DEMO-P002", "合成压力 2 · 00:15", "2026-09-17T00:15:00Z", 31.31, "m", "valid"),
|
|
("DEMO-P002", "合成压力 2 · 00:30", "2026-09-17T00:30:00Z", 31.18, "m", "valid"),
|
|
("DEMO-P002", "合成压力 2 · 00:45", "2026-09-17T00:45:00Z", 31.36, "m", "valid"),
|
|
("DEMO-F001", "合成流量 00:00", "2026-09-17T00:00:00Z", 186.43, "m3/h", "valid"),
|
|
("DEMO-F001", "合成流量 00:15", "2026-09-17T00:15:00Z", 185.90, "m3/h", "valid"),
|
|
("DEMO-F001", "合成零流量 00:30", "2026-09-17T00:30:00Z", 0.0, "m3/h", "valid"),
|
|
("DEMO-F001", "合成流量 00:45", "2026-09-17T00:45:00Z", 187.12, "m3/h", "valid"),
|
|
)
|
|
|
|
|
|
def previous_seed_result(
|
|
store: PostgresStore,
|
|
project_id: str,
|
|
*,
|
|
seed_request_id: UUID,
|
|
command_key: str,
|
|
) -> dict | None:
|
|
"""Read this script's earlier receipt without weakening API idempotency checks."""
|
|
with store.connection(readonly=True) as connection:
|
|
row = connection.execute(
|
|
"""SELECT command_key,result
|
|
FROM peripheral_sample.requests
|
|
WHERE project_id=%s AND actor_id=%s AND request_id=%s""",
|
|
(project_id, str(ACTOR_ID), seed_request_id),
|
|
).fetchone()
|
|
if row is None:
|
|
return None
|
|
if row["command_key"] != command_key:
|
|
raise RuntimeError("seed request id is already used by another command")
|
|
return row["result"]
|
|
|
|
|
|
def restore_seeded_resources(
|
|
store: PostgresStore,
|
|
project_id: str,
|
|
*,
|
|
table: str,
|
|
id_column: str,
|
|
resource_ids: list[str],
|
|
) -> None:
|
|
"""Restore deterministic seed rows that were soft-deleted after an earlier run."""
|
|
allowed_resources = {
|
|
("work_orders", "work_order_id"),
|
|
("test_data_records", "record_id"),
|
|
}
|
|
if (table, id_column) not in allowed_resources:
|
|
raise ValueError("unsupported seeded resource")
|
|
if not resource_ids:
|
|
return
|
|
|
|
with store.connection() as connection:
|
|
connection.execute(
|
|
f"""UPDATE peripheral_sample.{table}
|
|
SET document=document - 'deleted_at' - 'deleted_by',
|
|
deleted_at=NULL, deleted_by=NULL
|
|
WHERE project_id=%s AND {id_column}=ANY(%s::uuid[])""",
|
|
(project_id, resource_ids),
|
|
)
|
|
|
|
|
|
def ensure_demo_dataset(store: PostgresStore, project_id: str) -> str:
|
|
content = build_demo_content()
|
|
content_digest = digest(content)
|
|
with store.connection() as connection:
|
|
row = connection.execute(
|
|
"""SELECT digest FROM peripheral_sample.demo_datasets
|
|
WHERE project_id=%s AND dataset_id='water-demo-v1'""",
|
|
(project_id,),
|
|
).fetchone()
|
|
if row is None:
|
|
connection.execute(
|
|
"""INSERT INTO peripheral_sample.demo_datasets
|
|
(project_id,dataset_id,digest,document)
|
|
VALUES (%s,'water-demo-v1',%s,%s)""",
|
|
(project_id, content_digest, Jsonb(content)),
|
|
)
|
|
elif row["digest"] != content_digest:
|
|
connection.execute(
|
|
"""UPDATE peripheral_sample.demo_datasets
|
|
SET digest=%s, document=%s
|
|
WHERE project_id=%s AND dataset_id='water-demo-v1'""",
|
|
(content_digest, Jsonb(content), project_id),
|
|
)
|
|
return content_digest
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--project-id", required=True, type=UUID)
|
|
args = parser.parse_args()
|
|
project_id = str(args.project_id)
|
|
store = PostgresStore(SampleSettings(db_name="tjwater_v2", enable_faults=False))
|
|
who = Identity(project_id, str(ACTOR_ID), True)
|
|
|
|
dataset_digest = ensure_demo_dataset(store, project_id)
|
|
order_ids = []
|
|
for item in WORK_ORDERS:
|
|
seed_request_id = request_id(item["key"])
|
|
command = CreateWorkOrder(
|
|
request_id=seed_request_id,
|
|
title=item["title"],
|
|
summary=item["summary"],
|
|
status=item["status"],
|
|
priority=item["priority"],
|
|
assignee=item["assignee"],
|
|
scheduled_at=item["scheduled_at"],
|
|
)
|
|
previous = previous_seed_result(
|
|
store,
|
|
project_id,
|
|
seed_request_id=seed_request_id,
|
|
command_key="create_work_order",
|
|
)
|
|
result = previous or store.create_work_order(who, command)
|
|
order_ids.append(result["work_order_id"])
|
|
restore_seeded_resources(
|
|
store,
|
|
project_id,
|
|
table="work_orders",
|
|
id_column="work_order_id",
|
|
resource_ids=order_ids,
|
|
)
|
|
|
|
record_ids = []
|
|
for point_id, label, observed_at, value, unit, quality in TEST_DATA:
|
|
seed_request_id = request_id(f"test-data/{point_id}/{observed_at}")
|
|
command = CreateTestDataRecord(
|
|
request_id=seed_request_id,
|
|
label=label,
|
|
point_id=point_id,
|
|
observed_at=observed_at,
|
|
value=value,
|
|
unit=unit,
|
|
quality=quality,
|
|
notes="外围样板固定合成数据,不是现场监测记录。",
|
|
)
|
|
previous = previous_seed_result(
|
|
store,
|
|
project_id,
|
|
seed_request_id=seed_request_id,
|
|
command_key="create_test_data",
|
|
)
|
|
result = previous or store.create_test_data(who, command)
|
|
record_ids.append(result["record_id"])
|
|
restore_seeded_resources(
|
|
store,
|
|
project_id,
|
|
table="test_data_records",
|
|
id_column="record_id",
|
|
resource_ids=record_ids,
|
|
)
|
|
|
|
result = {
|
|
"project_id": project_id,
|
|
"actor_id": str(ACTOR_ID),
|
|
"origin": "synthetic",
|
|
"demo_dataset_digest": dataset_digest,
|
|
"work_orders": {"count": len(order_ids), "ids": order_ids},
|
|
"test_data_records": {"count": len(record_ids), "ids": record_ids},
|
|
}
|
|
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|