feat(scada): serve project devices through pooled API
This commit is contained in:
@@ -3,7 +3,6 @@ from uuid import UUID
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from psycopg import AsyncConnection
|
from psycopg import AsyncConnection
|
||||||
|
|
||||||
from app.infra.db.postgresql.scada import ScadaInfoRepository
|
|
||||||
from app.infra.db.postgresql.analysis import AnalysisRepository
|
from app.infra.db.postgresql.analysis import AnalysisRepository
|
||||||
from app.auth.project_dependencies import get_project_pg_connection
|
from app.auth.project_dependencies import get_project_pg_connection
|
||||||
|
|
||||||
@@ -17,24 +16,6 @@ async def get_database_connection(
|
|||||||
yield conn
|
yield conn
|
||||||
|
|
||||||
|
|
||||||
@router.get("/scada-info/database-view", summary="获取SCADA信息", description="使用连接池查询所有SCADA信息")
|
|
||||||
async def get_scada_info_with_connection(
|
|
||||||
conn: AsyncConnection = Depends(get_database_connection),
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
获取所有SCADA信息
|
|
||||||
|
|
||||||
返回项目中所有的SCADA设备信息
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
scada_data = await ScadaInfoRepository.get_scadas(conn)
|
|
||||||
return {"success": True, "data": scada_data, "count": len(scada_data)}
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=500, detail=f"查询SCADA信息时发生错误: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/analysis/runs", summary="获取分析运行列表")
|
@router.get("/analysis/runs", summary="获取分析运行列表")
|
||||||
async def get_analysis_runs(
|
async def get_analysis_runs(
|
||||||
conn: AsyncConnection = Depends(get_database_connection),
|
conn: AsyncConnection = Depends(get_database_connection),
|
||||||
|
|||||||
@@ -1,34 +1,42 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Query
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from psycopg import AsyncConnection
|
||||||
|
|
||||||
from app.services.tjnetwork import (
|
from app.auth.project_dependencies import get_project_pg_connection
|
||||||
get_all_scada_info,
|
from app.domain.schemas.scada import ScadaDeviceResponse
|
||||||
get_scada_info,
|
from app.infra.db.postgresql.scada import ScadaInfoRepository, get_scada_info_schema
|
||||||
get_scada_info_schema,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/network-schemas/scada-device", summary="获取 SCADA 设备结构")
|
@router.get("/network-schemas/scada-device", summary="获取 SCADA 设备结构")
|
||||||
def get_scada_device_schema(
|
def get_scada_device_schema() -> dict[str, dict[str, Any]]:
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
return get_scada_info_schema("")
|
||||||
) -> dict[str, dict[str, Any]]:
|
|
||||||
return get_scada_info_schema(network)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/scada-devices", summary="获取 SCADA 设备列表")
|
@router.get(
|
||||||
def get_scada_devices(
|
"/scada-devices",
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
summary="获取 SCADA 设备列表",
|
||||||
|
response_model=list[ScadaDeviceResponse],
|
||||||
|
)
|
||||||
|
async def get_scada_devices(
|
||||||
|
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
return get_all_scada_info(network)
|
return await ScadaInfoRepository.get_scadas(conn)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/scada-devices/detail", summary="获取 SCADA 设备")
|
@router.get(
|
||||||
def get_scada_device(
|
"/scada-devices/{device_id}",
|
||||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
summary="获取 SCADA 设备",
|
||||||
device_id: str = Query(..., description="SCADA 设备 ID"),
|
response_model=ScadaDeviceResponse,
|
||||||
|
)
|
||||||
|
async def get_scada_device(
|
||||||
|
device_id: str,
|
||||||
|
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return get_scada_info(network, device_id)
|
device = await ScadaInfoRepository.get_scada(conn, device_id)
|
||||||
|
if device is None:
|
||||||
|
raise HTTPException(status_code=404, detail="SCADA 设备不存在")
|
||||||
|
return device
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class ScadaDeviceResponse(BaseModel):
|
||||||
|
"""Project SCADA metadata keyed by the canonical device identifier."""
|
||||||
|
|
||||||
|
device_id: str
|
||||||
|
device_type: str
|
||||||
|
node_id: str | None = None
|
||||||
|
link_id: str | None = None
|
||||||
|
api_query_id: str | None = None
|
||||||
|
transmission_mode: str
|
||||||
|
transmission_frequency: str
|
||||||
|
reliability: int | None = None
|
||||||
|
x: float | None = None
|
||||||
|
y: float | None = None
|
||||||
|
longitude: float | None = None
|
||||||
|
latitude: float | None = None
|
||||||
@@ -9,7 +9,9 @@ from app.native.wndb.core.database import read_all, try_read
|
|||||||
|
|
||||||
_SCADA_VIEW_SELECT = """
|
_SCADA_VIEW_SELECT = """
|
||||||
SELECT id AS device_id, device_type, node_id, link_id, api_query_id,
|
SELECT id AS device_id, device_type, node_id, link_id, api_query_id,
|
||||||
transmission_mode, transmission_frequency, reliability, x, y
|
transmission_mode, transmission_frequency, reliability, x, y,
|
||||||
|
ST_X(ST_Transform(geom, 4326)) AS longitude,
|
||||||
|
ST_Y(ST_Transform(geom, 4326)) AS latitude
|
||||||
FROM gis.scada_devices
|
FROM gis.scada_devices
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -60,6 +62,8 @@ def _device(record: dict[str, Any]) -> dict[str, Any]:
|
|||||||
"reliability": _optional_int(record["reliability"]),
|
"reliability": _optional_int(record["reliability"]),
|
||||||
"x": _optional_float(record["x"]),
|
"x": _optional_float(record["x"]),
|
||||||
"y": _optional_float(record["y"]),
|
"y": _optional_float(record["y"]),
|
||||||
|
"longitude": _optional_float(record["longitude"]),
|
||||||
|
"latitude": _optional_float(record["latitude"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -76,6 +80,18 @@ class ScadaInfoRepository:
|
|||||||
|
|
||||||
return [_device(record) for record in records]
|
return [_device(record) for record in records]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_scada(
|
||||||
|
conn: AsyncConnection, device_id: str
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
async with conn.cursor() as cur:
|
||||||
|
await cur.execute(
|
||||||
|
_SCADA_VIEW_SELECT + " WHERE id = %s",
|
||||||
|
(device_id,),
|
||||||
|
)
|
||||||
|
record = await cur.fetchone()
|
||||||
|
return _device(record) if record else None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_existing_device_ids(
|
async def get_existing_device_ids(
|
||||||
conn: AsyncConnection, device_ids: list[str]
|
conn: AsyncConnection, device_ids: list[str]
|
||||||
@@ -102,6 +118,8 @@ def get_scada_info_schema(name: str) -> dict[str, dict[str, Any]]:
|
|||||||
"reliability": {"type": "int", "optional": False, "readonly": True},
|
"reliability": {"type": "int", "optional": False, "readonly": True},
|
||||||
"x": {"type": "float", "optional": True, "readonly": True},
|
"x": {"type": "float", "optional": True, "readonly": True},
|
||||||
"y": {"type": "float", "optional": True, "readonly": True},
|
"y": {"type": "float", "optional": True, "readonly": True},
|
||||||
|
"longitude": {"type": "float", "optional": True, "readonly": True},
|
||||||
|
"latitude": {"type": "float", "optional": True, "readonly": True},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"contracts": {
|
"contracts": {
|
||||||
"server": {
|
"server": {
|
||||||
"file": "server-v1.openapi.json",
|
"file": "server-v1.openapi.json",
|
||||||
"sha256": "b6640fa87909c9a8a8747e9003ace35ea6a209f99f6e1dc15cb0bd5c313dc7c3"
|
"sha256": "f07ecfa4843955bd63a4cdf0d7d336d593f41725a8abc1b0da6cce7ff4fff982"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+153
-106
@@ -1502,6 +1502,37 @@
|
|||||||
"title": "Page[ProjectSummaryResponse]",
|
"title": "Page[ProjectSummaryResponse]",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"Page_ScadaDeviceResponse_": {
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/ScadaDeviceResponse"
|
||||||
|
},
|
||||||
|
"title": "Items",
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"title": "Limit",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"offset": {
|
||||||
|
"title": "Offset",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"title": "Total",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"items",
|
||||||
|
"total",
|
||||||
|
"limit",
|
||||||
|
"offset"
|
||||||
|
],
|
||||||
|
"title": "Page[ScadaDeviceResponse]",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"Page_SensorPlacementSchemeResponse_": {
|
"Page_SensorPlacementSchemeResponse_": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"items": {
|
"items": {
|
||||||
@@ -2224,6 +2255,123 @@
|
|||||||
"title": "RunSimulationManuallyByDateRest",
|
"title": "RunSimulationManuallyByDateRest",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"ScadaDeviceResponse": {
|
||||||
|
"description": "Project SCADA metadata keyed by the canonical device identifier.",
|
||||||
|
"properties": {
|
||||||
|
"api_query_id": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Api Query Id"
|
||||||
|
},
|
||||||
|
"device_id": {
|
||||||
|
"title": "Device Id",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"device_type": {
|
||||||
|
"title": "Device Type",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"latitude": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Latitude"
|
||||||
|
},
|
||||||
|
"link_id": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Link Id"
|
||||||
|
},
|
||||||
|
"longitude": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Longitude"
|
||||||
|
},
|
||||||
|
"node_id": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Node Id"
|
||||||
|
},
|
||||||
|
"reliability": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Reliability"
|
||||||
|
},
|
||||||
|
"transmission_frequency": {
|
||||||
|
"title": "Transmission Frequency",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"transmission_mode": {
|
||||||
|
"title": "Transmission Mode",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"x": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "X"
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Y"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"device_id",
|
||||||
|
"device_type",
|
||||||
|
"transmission_mode",
|
||||||
|
"transmission_frequency"
|
||||||
|
],
|
||||||
|
"title": "ScadaDeviceResponse",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ScadaReadingBatchItem": {
|
"ScadaReadingBatchItem": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"cleaned_value": {
|
"cleaned_value": {
|
||||||
@@ -26515,7 +26663,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/Page_dict_str__Any__"
|
"$ref": "#/components/schemas/Page_ScadaDeviceResponse_"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -26593,17 +26741,15 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/scada-devices/detail": {
|
"/api/v1/scada-devices/{device_id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "get_scada_devices_detail",
|
"operationId": "get_scada_devices_device_id",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "SCADA 设备 ID",
|
"in": "path",
|
||||||
"in": "query",
|
|
||||||
"name": "device_id",
|
"name": "device_id",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"description": "SCADA 设备 ID",
|
|
||||||
"title": "Device Id",
|
"title": "Device Id",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -26623,8 +26769,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"title": "Response Get Scada Devices Detail",
|
"$ref": "#/components/schemas/ScadaDeviceResponse"
|
||||||
"type": "object"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -26702,104 +26847,6 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/scada-info/database-view": {
|
|
||||||
"get": {
|
|
||||||
"description": "使用连接池查询所有SCADA信息",
|
|
||||||
"operationId": "get_scada_info_database_view",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "header",
|
|
||||||
"name": "X-Project-Id",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "X-Project-Id",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/JsonValue"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Successful Response"
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ProblemDetails"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Authentication required"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ProblemDetails"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Insufficient permission"
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ProblemDetails"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Resource not found"
|
|
||||||
},
|
|
||||||
"409": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ProblemDetails"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Resource conflict"
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ProblemDetails"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Validation error"
|
|
||||||
},
|
|
||||||
"503": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ProblemDetails"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Dependency unavailable"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"OAuth2PasswordBearer": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"summary": "获取SCADA信息",
|
|
||||||
"tags": [
|
|
||||||
"Project Data"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/scada-properties": {
|
"/api/v1/scada-properties": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "获取指定SCADA点的属性信息",
|
"description": "获取指定SCADA点的属性信息",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from psycopg import connect
|
|||||||
|
|
||||||
from app.core.config import get_pgconn_string
|
from app.core.config import get_pgconn_string
|
||||||
from app.infra.db.dynamic_manager import ProjectConnectionManager
|
from app.infra.db.dynamic_manager import ProjectConnectionManager
|
||||||
|
from app.infra.db.postgresql.scada import ScadaInfoRepository
|
||||||
from app.infra.db.timescaledb.sync_pool import timescale_connection
|
from app.infra.db.timescaledb.sync_pool import timescale_connection
|
||||||
from app.native.wndb.commands.api import delete_pattern_cascade
|
from app.native.wndb.commands.api import delete_pattern_cascade
|
||||||
from app.native.wndb.core.connection import project_connection, project_transaction
|
from app.native.wndb.core.connection import project_connection, project_transaction
|
||||||
@@ -105,6 +106,29 @@ def test_dynamic_pool_replaces_terminated_idle_connection() -> None:
|
|||||||
asyncio.run(exercise_pool())
|
asyncio.run(exercise_pool())
|
||||||
|
|
||||||
|
|
||||||
|
def test_scada_api_repository_reads_device_ids_through_dynamic_pool() -> None:
|
||||||
|
async def read_scada_devices() -> list[dict]:
|
||||||
|
manager = ProjectConnectionManager()
|
||||||
|
try:
|
||||||
|
async with manager.pg_connection(
|
||||||
|
uuid4(), "biz_data", get_pgconn_string(db_name=PROJECT), 1, 4
|
||||||
|
) as conn:
|
||||||
|
return await ScadaInfoRepository.get_scadas(conn)
|
||||||
|
finally:
|
||||||
|
await manager.close_all()
|
||||||
|
|
||||||
|
devices = asyncio.run(read_scada_devices())
|
||||||
|
|
||||||
|
assert devices
|
||||||
|
device_ids = [device["device_id"] for device in devices]
|
||||||
|
assert len(device_ids) == len(set(device_ids))
|
||||||
|
assert all(device_ids)
|
||||||
|
assert all(
|
||||||
|
device["longitude"] is not None and device["latitude"] is not None
|
||||||
|
for device in devices
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_nested_wndb_writes_roll_back_as_one_transaction() -> None:
|
def test_nested_wndb_writes_roll_back_as_one_transaction() -> None:
|
||||||
with pytest.raises(RuntimeError, match="force rollback"):
|
with pytest.raises(RuntimeError, match="force rollback"):
|
||||||
with project_transaction(PROJECT) as conn:
|
with project_transaction(PROJECT) as conn:
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ class _FakeCursor:
|
|||||||
async def __aexit__(self, exc_type, exc, tb):
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def execute(self, query):
|
async def execute(self, query, params=None):
|
||||||
self.query = query
|
self.query = query
|
||||||
|
self.params = params
|
||||||
|
|
||||||
async def fetchall(self):
|
async def fetchall(self):
|
||||||
return [
|
return [
|
||||||
@@ -33,9 +34,14 @@ class _FakeCursor:
|
|||||||
"reliability": "95",
|
"reliability": "95",
|
||||||
"x": "117.1",
|
"x": "117.1",
|
||||||
"y": "32.9",
|
"y": "32.9",
|
||||||
|
"longitude": "121.5",
|
||||||
|
"latitude": "30.9",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
async def fetchone(self):
|
||||||
|
return (await self.fetchall())[0]
|
||||||
|
|
||||||
|
|
||||||
class _FakeConnection:
|
class _FakeConnection:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -62,11 +68,25 @@ def test_get_scadas_normalizes_id_and_type():
|
|||||||
"reliability": 95,
|
"reliability": 95,
|
||||||
"x": 117.1,
|
"x": 117.1,
|
||||||
"y": 32.9,
|
"y": 32.9,
|
||||||
|
"longitude": 121.5,
|
||||||
|
"latitude": 30.9,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
assert "node_id" in conn.cursor_instance.query
|
assert "node_id" in conn.cursor_instance.query
|
||||||
assert "link_id" in conn.cursor_instance.query
|
assert "link_id" in conn.cursor_instance.query
|
||||||
assert "FROM gis.scada_devices" in conn.cursor_instance.query
|
assert "FROM gis.scada_devices" in conn.cursor_instance.query
|
||||||
|
assert "ST_Transform(geom, 4326)" in conn.cursor_instance.query
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_scada_filters_the_project_view_by_device_id():
|
||||||
|
conn = _FakeConnection()
|
||||||
|
|
||||||
|
result = asyncio.run(ScadaInfoRepository.get_scada(conn, "25470001"))
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert result["device_id"] == "25470001"
|
||||||
|
assert "WHERE id = %s" in conn.cursor_instance.query
|
||||||
|
assert conn.cursor_instance.params == ("25470001",)
|
||||||
|
|
||||||
|
|
||||||
def test_realtime_element_mappings_are_project_local_and_immutable(monkeypatch):
|
def test_realtime_element_mappings_are_project_local_and_immutable(monkeypatch):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
|
|||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from app.api.v1.endpoints import project_data
|
from app.api.v1.endpoints import scada as scada_endpoint
|
||||||
from app.services import timeseries_analysis as composite_queries
|
from app.services import timeseries_analysis as composite_queries
|
||||||
|
|
||||||
|
|
||||||
@@ -18,6 +18,8 @@ PROJECT_SCADA = {
|
|||||||
"reliability": 1.0,
|
"reliability": 1.0,
|
||||||
"x": 117.1,
|
"x": 117.1,
|
||||||
"y": 32.9,
|
"y": 32.9,
|
||||||
|
"longitude": 121.5,
|
||||||
|
"latitude": 30.9,
|
||||||
}
|
}
|
||||||
START_TIME = datetime(2026, 6, 1, tzinfo=timezone.utc)
|
START_TIME = datetime(2026, 6, 1, tzinfo=timezone.utc)
|
||||||
END_TIME = datetime(2026, 6, 2, tzinfo=timezone.utc)
|
END_TIME = datetime(2026, 6, 2, tzinfo=timezone.utc)
|
||||||
@@ -128,11 +130,11 @@ def test_element_scada_query_uses_current_project_metadata(monkeypatch):
|
|||||||
|
|
||||||
def test_scada_info_endpoint_uses_current_project_connection(monkeypatch):
|
def test_scada_info_endpoint_uses_current_project_connection(monkeypatch):
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
project_data.ScadaInfoRepository,
|
scada_endpoint.ScadaInfoRepository,
|
||||||
"get_scadas",
|
"get_scadas",
|
||||||
AsyncMock(return_value=[PROJECT_SCADA.copy()]),
|
AsyncMock(return_value=[PROJECT_SCADA.copy()]),
|
||||||
)
|
)
|
||||||
|
|
||||||
result = asyncio.run(project_data.get_scada_info_with_connection(object()))
|
result = asyncio.run(scada_endpoint.get_scada_devices(object()))
|
||||||
|
|
||||||
assert result == {"success": True, "data": [PROJECT_SCADA], "count": 1}
|
assert result == [PROJECT_SCADA]
|
||||||
|
|||||||
Reference in New Issue
Block a user