贯通项目 DSN 并移除 Redis/InfluxDB 遗留 #2
@@ -49,7 +49,7 @@ These route groups expose many command-style concatenated paths. They should not
|
|||||||
- Network object CRUD: `addjunction`, `getjunctionelevation`, `setpipediameter`, `getvalvesetting`, and similar junction/pipe/pump/tank/reservoir/valve routes
|
- Network object CRUD: `addjunction`, `getjunctionelevation`, `setpipediameter`, `getvalvesetting`, and similar junction/pipe/pump/tank/reservoir/valve routes
|
||||||
- Region/DMA/VD commands: `calculatedistrictmeteringareaforregion`, `getdistrictmeteringarea`, `generatevirtualdistrict`, and related routes
|
- Region/DMA/VD commands: `calculatedistrictmeteringareaforregion`, `getdistrictmeteringarea`, `generatevirtualdistrict`, and related routes
|
||||||
- SCADA native CRUD: `getscadadevice`, `setscadadevicedata`, `cleanscadaelement`, and related routes
|
- SCADA native CRUD: `getscadadevice`, `setscadadevicedata`, `cleanscadaelement`, and related routes
|
||||||
- Snapshot/cache utilities: `takesnapshotforoperation`, `syncwithserver`, `clearrediskey`, `queryredis`
|
- Snapshot/synchronization utilities: `takesnapshotforoperation`, `syncwithserver`
|
||||||
- Advanced simulation endpoints with underscore paths: `pressure_regulation`, `daily_scheduling_analysis`, `network_update`, `pressure_sensor_placement_kmeans`
|
- Advanced simulation endpoints with underscore paths: `pressure_regulation`, `daily_scheduling_analysis`, `network_update`, `pressure_sensor_placement_kmeans`
|
||||||
|
|
||||||
### Direct Cleanup Candidates
|
### Direct Cleanup Candidates
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
- Python 3.12
|
- Python 3.12
|
||||||
- FastAPI / Uvicorn
|
- FastAPI / Uvicorn
|
||||||
- Pydantic / SQLAlchemy / psycopg
|
- Pydantic / SQLAlchemy / psycopg
|
||||||
- Redis、PostgreSQL、PostGIS、TimescaleDB
|
- PostgreSQL、PostGIS、TimescaleDB
|
||||||
- WNTR、EPANET、Cython、科学计算与空间分析依赖
|
- WNTR、EPANET、Cython、科学计算与空间分析依赖
|
||||||
- pytest
|
- pytest
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ app/api/ HTTP API 路由
|
|||||||
app/auth/ 认证和权限上下文
|
app/auth/ 认证和权限上下文
|
||||||
app/core/ 配置、日志和基础设施初始化
|
app/core/ 配置、日志和基础设施初始化
|
||||||
app/domain/ 领域模型和 Pydantic schema
|
app/domain/ 领域模型和 Pydantic schema
|
||||||
app/infra/ 数据库、缓存、EPANET 和外部集成
|
app/infra/ 数据库、EPANET 和外部集成
|
||||||
app/services/ 业务服务编排
|
app/services/ 业务服务编排
|
||||||
app/algorithms/ 管网算法、模拟、爆管、漏损、清洗和健康分析
|
app/algorithms/ 管网算法、模拟、爆管、漏损、清洗和健康分析
|
||||||
app/native/ 本地管网数据读写与转换
|
app/native/ 本地管网数据读写与转换
|
||||||
@@ -65,6 +65,16 @@ docker compose -f infra/docker/docker-compose.yml config
|
|||||||
- 优先复用现有 FastAPI/service/repository 边界。
|
- 优先复用现有 FastAPI/service/repository 边界。
|
||||||
- 不要把临时数据、数据库 dump、日志或本地运行产物纳入提交。
|
- 不要把临时数据、数据库 dump、日志或本地运行产物纳入提交。
|
||||||
|
|
||||||
|
## 项目数据库路由
|
||||||
|
|
||||||
|
项目级 REST 请求通过 `X-Project-Id` 解析元数据中的数据库配置:
|
||||||
|
|
||||||
|
- `biz_data` DSN 用于管网业务数据;`{project_code}_template` 和模拟临时库沿用该 DSN 的主机、端口与凭据,仅替换数据库名。
|
||||||
|
- `iot_data` DSN 用于 TimescaleDB,始终使用元数据配置的完整 DSN,不再从项目代码推导数据库名。
|
||||||
|
- 元数据、业务库和 TimescaleDB 可以部署在同一主机,也可以分别部署。
|
||||||
|
|
||||||
|
使用模板复制或临时方案库的模拟功能时,`biz_data` 账号必须具备现有数据库创建、删除和连接终止操作所需的 PostgreSQL 权限。
|
||||||
|
|
||||||
## 测试与发布
|
## 测试与发布
|
||||||
|
|
||||||
提交前根据改动范围运行最小有效测试:
|
提交前根据改动范围运行最小有效测试:
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
from fastapi import APIRouter, Query
|
|
||||||
from app.infra.cache.redis_client import redis_client
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
@router.delete("/redis-keys/detail", summary="清除单个缓存键", description="根据键名清除单个Redis缓存")
|
|
||||||
async def fastapi_clear_redis_key(key: str = Query(..., description="缓存键名")):
|
|
||||||
"""
|
|
||||||
清除单个缓存键
|
|
||||||
|
|
||||||
根据指定的键名删除Redis中对应的缓存
|
|
||||||
"""
|
|
||||||
redis_client.delete(key)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/redis-keys", summary="清除匹配的缓存键", description="根据模式清除匹配的Redis缓存键")
|
|
||||||
async def fastapi_clear_redis_keys(keys: str = Query(..., description="缓存键模式(支持通配符)")):
|
|
||||||
"""
|
|
||||||
清除匹配的缓存键
|
|
||||||
|
|
||||||
根据指定的模式删除Redis中所有匹配的缓存键
|
|
||||||
"""
|
|
||||||
# delete keys contains the key
|
|
||||||
matched_keys = redis_client.keys(f"*{keys}*")
|
|
||||||
if matched_keys:
|
|
||||||
redis_client.delete(*matched_keys)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/all-redis", summary="清除所有缓存", description="清空整个Redis数据库的所有缓存")
|
|
||||||
async def fastapi_clear_all_redis():
|
|
||||||
"""
|
|
||||||
清除所有缓存
|
|
||||||
|
|
||||||
清空Redis数据库中的所有缓存键值对
|
|
||||||
"""
|
|
||||||
redis_client.flushdb()
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/redis", summary="查询缓存键列表", description="获取Redis中所有的缓存键")
|
|
||||||
async def fastapi_query_redis():
|
|
||||||
"""
|
|
||||||
查询缓存键列表
|
|
||||||
|
|
||||||
获取Redis数据库中所有的缓存键列表
|
|
||||||
"""
|
|
||||||
# Helper to decode bytes to str for JSON response if needed,
|
|
||||||
# but original just returned keys (which might be bytes in redis-py unless decode_responses=True)
|
|
||||||
# create_redis_client usually sets decode_responses=False by default.
|
|
||||||
# We will assume user handles bytes or we should decode.
|
|
||||||
# Original just returned redis_client.keys("*")
|
|
||||||
keys = redis_client.keys("*")
|
|
||||||
# Clean output for API
|
|
||||||
return [k.decode('utf-8') if isinstance(k, bytes) else k for k in keys]
|
|
||||||
@@ -17,8 +17,13 @@ from app.auth.metadata_dependencies import (
|
|||||||
get_current_metadata_admin,
|
get_current_metadata_admin,
|
||||||
get_metadata_repository,
|
get_metadata_repository,
|
||||||
)
|
)
|
||||||
|
from app.auth.project_dependencies import (
|
||||||
|
ProjectContext,
|
||||||
|
resolve_project_business_routing,
|
||||||
|
)
|
||||||
from app.core.audit import AuditAction, log_audit_event
|
from app.core.audit import AuditAction, log_audit_event
|
||||||
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
||||||
|
from app.infra.db.project_routing import activate_project_routing
|
||||||
from app.services.network_import import network_update
|
from app.services.network_import import network_update
|
||||||
from app.services.tjnetwork import run_inp
|
from app.services.tjnetwork import run_inp
|
||||||
|
|
||||||
@@ -118,21 +123,21 @@ async def _run_uploaded_inp(content: bytes) -> str:
|
|||||||
return run_inp(model_name)
|
return run_inp(model_name)
|
||||||
|
|
||||||
|
|
||||||
async def _update_from_inp(content: bytes) -> None:
|
async def _update_from_inp(content: bytes, project_code: str) -> None:
|
||||||
temp_path: Path | None = None
|
temp_path: Path | None = None
|
||||||
try:
|
try:
|
||||||
with NamedTemporaryFile(suffix=".inp", delete=False) as temp_file:
|
with NamedTemporaryFile(suffix=".inp", delete=False) as temp_file:
|
||||||
temp_file.write(content)
|
temp_file.write(content)
|
||||||
temp_path = Path(temp_file.name)
|
temp_path = Path(temp_file.name)
|
||||||
network_update(str(temp_path))
|
network_update(str(temp_path), project_code)
|
||||||
finally:
|
finally:
|
||||||
if temp_path is not None:
|
if temp_path is not None:
|
||||||
temp_path.unlink(missing_ok=True)
|
temp_path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
async def _apply_model_update(content: bytes) -> None:
|
async def _apply_model_update(content: bytes, project_code: str) -> None:
|
||||||
try:
|
try:
|
||||||
await _update_from_inp(content)
|
await _update_from_inp(content, project_code)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
@@ -177,7 +182,19 @@ async def update_project_model(
|
|||||||
) -> dict:
|
) -> dict:
|
||||||
project = await _get_active_project(project_id, metadata_repo)
|
project = await _get_active_project(project_id, metadata_repo)
|
||||||
content, filename = await _read_upload(file)
|
content, filename = await _read_upload(file)
|
||||||
await _apply_model_update(content)
|
routing = await resolve_project_business_routing(
|
||||||
|
ProjectContext(
|
||||||
|
project_id=project.id,
|
||||||
|
project_code=project.code,
|
||||||
|
user_id=current_user.id,
|
||||||
|
project_role="owner",
|
||||||
|
system_role=current_user.role,
|
||||||
|
is_superuser=current_user.is_superuser,
|
||||||
|
),
|
||||||
|
metadata_repo,
|
||||||
|
)
|
||||||
|
with activate_project_routing(routing):
|
||||||
|
await _apply_model_update(content, project.code)
|
||||||
await _audit_model_change(
|
await _audit_model_change(
|
||||||
request=request,
|
request=request,
|
||||||
current_user=current_user,
|
current_user=current_user,
|
||||||
|
|||||||
@@ -333,7 +333,6 @@ async def fastapi_get_all_junction_properties(
|
|||||||
list: 包含所有节点属性的列表
|
list: 包含所有节点属性的列表
|
||||||
"""
|
"""
|
||||||
# 缓存查询结果提高性能
|
# 缓存查询结果提高性能
|
||||||
# global redis_client # Redis logic removed for clean split, can be re-added if needed or imported
|
|
||||||
results = get_all_junctions(network)
|
results = get_all_junctions(network)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|||||||
@@ -385,7 +385,6 @@ async def fastapi_get_all_pipe_properties(
|
|||||||
包含所有管道属性的字典列表
|
包含所有管道属性的字典列表
|
||||||
"""
|
"""
|
||||||
# 缓存查询结果提高性能
|
# 缓存查询结果提高性能
|
||||||
# global redis_client
|
|
||||||
results = get_all_pipes(network)
|
results = get_all_pipes(network)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|||||||
@@ -177,7 +177,6 @@ async def fastapi_get_all_pump_properties(
|
|||||||
包含所有水泵属性的字典列表
|
包含所有水泵属性的字典列表
|
||||||
"""
|
"""
|
||||||
# 缓存查询结果提高性能
|
# 缓存查询结果提高性能
|
||||||
# global redis_client
|
|
||||||
results = get_all_pumps(network)
|
results = get_all_pumps(network)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|||||||
@@ -540,7 +540,6 @@ async def fastapi_get_all_tank_properties(
|
|||||||
包含所有水箱属性的字典列表
|
包含所有水箱属性的字典列表
|
||||||
"""
|
"""
|
||||||
# 缓存查询结果提高性能
|
# 缓存查询结果提高性能
|
||||||
# global redis_client
|
|
||||||
results = get_all_tanks(network)
|
results = get_all_tanks(network)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|||||||
@@ -307,7 +307,6 @@ async def fastapi_get_all_valve_properties(
|
|||||||
返回指定水网中所有阀门的完整属性列表。
|
返回指定水网中所有阀门的完整属性列表。
|
||||||
"""
|
"""
|
||||||
# 缓存查询结果提高性能
|
# 缓存查询结果提高性能
|
||||||
# global redis_client
|
|
||||||
results = get_all_valves(network)
|
results = get_all_valves(network)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,16 @@ from app.api.problem_details import ProblemDetails
|
|||||||
from app.api.pagination import PaginatedList
|
from app.api.pagination import PaginatedList
|
||||||
from app.api.v1.router import api_router as handler_api_router
|
from app.api.v1.router import api_router as handler_api_router
|
||||||
from app.auth.metadata_dependencies import get_current_metadata_user
|
from app.auth.metadata_dependencies import get_current_metadata_user
|
||||||
from app.auth.project_dependencies import ProjectContext, get_project_context
|
from app.auth.project_dependencies import (
|
||||||
|
ProjectContext,
|
||||||
|
get_project_business_routing,
|
||||||
|
get_project_context,
|
||||||
|
get_project_simulation_routing,
|
||||||
|
)
|
||||||
|
from app.infra.db.project_routing import (
|
||||||
|
ActiveProjectRouting,
|
||||||
|
activate_project_routing,
|
||||||
|
)
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
@@ -44,6 +53,13 @@ _PUBLIC_PARAMETER_RENAMES = {
|
|||||||
}
|
}
|
||||||
_MODEL_NAME_IS_NETWORK = {"RunSimulationManuallyByDate", "PressureSensorPlacement"}
|
_MODEL_NAME_IS_NETWORK = {"RunSimulationManuallyByDate", "PressureSensorPlacement"}
|
||||||
_MODEL_USERNAME_FROM_AUTH = {"PressureSensorPlacement"}
|
_MODEL_USERNAME_FROM_AUTH = {"PressureSensorPlacement"}
|
||||||
|
_TIMESCALE_ROUTED_ENDPOINT_MODULES = {
|
||||||
|
"app.api.v1.endpoints.burst_detection",
|
||||||
|
"app.api.v1.endpoints.burst_location",
|
||||||
|
"app.api.v1.endpoints.leakage",
|
||||||
|
"app.api.v1.endpoints.simulation",
|
||||||
|
}
|
||||||
|
_TIMESCALE_ROUTED_ENDPOINT_NAMES = {"open_project_endpoint"}
|
||||||
|
|
||||||
|
|
||||||
def _clean_name(name: str) -> str:
|
def _clean_name(name: str) -> str:
|
||||||
@@ -128,10 +144,14 @@ def _with_header_project_context(endpoint, route_name: str):
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
injected_context_name = existing_context_parameter or "_rest_project_context"
|
injected_context_name = existing_context_parameter or "_rest_project_context"
|
||||||
|
injected_routing_name = "_rest_project_routing"
|
||||||
injected_user_name = "_rest_current_user"
|
injected_user_name = "_rest_current_user"
|
||||||
|
|
||||||
@wraps(endpoint)
|
@wraps(endpoint)
|
||||||
async def wrapper(*args, **kwargs):
|
async def wrapper(*args, **kwargs):
|
||||||
|
project_routing = kwargs.pop(injected_routing_name, None)
|
||||||
|
if not isinstance(project_routing, ActiveProjectRouting):
|
||||||
|
raise RuntimeError("REST project database routing was not resolved")
|
||||||
project_context = kwargs.get(injected_context_name)
|
project_context = kwargs.get(injected_context_name)
|
||||||
if not isinstance(project_context, ProjectContext):
|
if not isinstance(project_context, ProjectContext):
|
||||||
raise RuntimeError("REST project context was not resolved")
|
raise RuntimeError("REST project context was not resolved")
|
||||||
@@ -162,10 +182,11 @@ def _with_header_project_context(endpoint, route_name: str):
|
|||||||
kwargs[parameter_name] = original_model.model_validate(data)
|
kwargs[parameter_name] = original_model.model_validate(data)
|
||||||
if model_has_username:
|
if model_has_username:
|
||||||
kwargs.pop(injected_user_name, None)
|
kwargs.pop(injected_user_name, None)
|
||||||
result = endpoint(*args, **kwargs)
|
with activate_project_routing(project_routing):
|
||||||
if inspect.isawaitable(result):
|
result = endpoint(*args, **kwargs)
|
||||||
return await result
|
if inspect.isawaitable(result):
|
||||||
return result
|
return await result
|
||||||
|
return result
|
||||||
|
|
||||||
parameters = []
|
parameters = []
|
||||||
for name, parameter in signature.parameters.items():
|
for name, parameter in signature.parameters.items():
|
||||||
@@ -181,6 +202,14 @@ def _with_header_project_context(endpoint, route_name: str):
|
|||||||
if name in body_models:
|
if name in body_models:
|
||||||
parameter = parameter.replace(annotation=body_models[name][1])
|
parameter = parameter.replace(annotation=body_models[name][1])
|
||||||
parameters.append(parameter)
|
parameters.append(parameter)
|
||||||
|
routing_dependency = (
|
||||||
|
get_project_simulation_routing
|
||||||
|
if (
|
||||||
|
endpoint.__module__ in _TIMESCALE_ROUTED_ENDPOINT_MODULES
|
||||||
|
or endpoint.__name__ in _TIMESCALE_ROUTED_ENDPOINT_NAMES
|
||||||
|
)
|
||||||
|
else get_project_business_routing
|
||||||
|
)
|
||||||
if not existing_context_parameter:
|
if not existing_context_parameter:
|
||||||
parameters.append(
|
parameters.append(
|
||||||
inspect.Parameter(
|
inspect.Parameter(
|
||||||
@@ -190,6 +219,14 @@ def _with_header_project_context(endpoint, route_name: str):
|
|||||||
default=Depends(get_project_context),
|
default=Depends(get_project_context),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
parameters.append(
|
||||||
|
inspect.Parameter(
|
||||||
|
injected_routing_name,
|
||||||
|
kind=inspect.Parameter.KEYWORD_ONLY,
|
||||||
|
annotation=ActiveProjectRouting,
|
||||||
|
default=Depends(routing_dependency),
|
||||||
|
)
|
||||||
|
)
|
||||||
if username_parameter or model_has_username:
|
if username_parameter or model_has_username:
|
||||||
parameters.append(
|
parameters.append(
|
||||||
inspect.Parameter(
|
inspect.Parameter(
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from app.api.v1.endpoints import (
|
|||||||
audit,
|
audit,
|
||||||
burst_detection,
|
burst_detection,
|
||||||
burst_location,
|
burst_location,
|
||||||
cache,
|
|
||||||
extension,
|
extension,
|
||||||
geocoding,
|
geocoding,
|
||||||
leakage,
|
leakage,
|
||||||
@@ -53,7 +52,6 @@ from app.api.v1.endpoints.timeseries import (
|
|||||||
)
|
)
|
||||||
from app.auth.permissions import (
|
from app.auth.permissions import (
|
||||||
BURST_RUN,
|
BURST_RUN,
|
||||||
ENVIRONMENT_MANAGE,
|
|
||||||
OPTIMIZATION_RUN,
|
OPTIMIZATION_RUN,
|
||||||
RISK_RUN,
|
RISK_RUN,
|
||||||
SCADA_CLEAN,
|
SCADA_CLEAN,
|
||||||
@@ -89,7 +87,6 @@ simulation_access = Depends(
|
|||||||
|
|
||||||
webgis_view_access = Depends(require_permission(WEBGIS_VIEW))
|
webgis_view_access = Depends(require_permission(WEBGIS_VIEW))
|
||||||
simulation_run_access = Depends(require_permission(SIMULATION_RUN))
|
simulation_run_access = Depends(require_permission(SIMULATION_RUN))
|
||||||
environment_manage_access = Depends(require_permission(ENVIRONMENT_MANAGE))
|
|
||||||
burst_run_access = Depends(require_permission(BURST_RUN))
|
burst_run_access = Depends(require_permission(BURST_RUN))
|
||||||
risk_run_access = Depends(require_permission(RISK_RUN))
|
risk_run_access = Depends(require_permission(RISK_RUN))
|
||||||
optimization_run_access = Depends(require_permission(OPTIMIZATION_RUN))
|
optimization_run_access = Depends(require_permission(OPTIMIZATION_RUN))
|
||||||
@@ -168,11 +165,6 @@ api_router.include_router(
|
|||||||
tags=["Risk"],
|
tags=["Risk"],
|
||||||
dependencies=[risk_run_access],
|
dependencies=[risk_run_access],
|
||||||
)
|
)
|
||||||
api_router.include_router(
|
|
||||||
cache.router,
|
|
||||||
tags=["Cache"],
|
|
||||||
dependencies=[environment_manage_access],
|
|
||||||
)
|
|
||||||
api_router.include_router(
|
api_router.include_router(
|
||||||
web_search.router,
|
web_search.router,
|
||||||
tags=["Web Search"],
|
tags=["Web Search"],
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from app.infra.db.metadb.repositories.metadata_repository import (
|
|||||||
MetadataRepository,
|
MetadataRepository,
|
||||||
ProjectDbRouting,
|
ProjectDbRouting,
|
||||||
)
|
)
|
||||||
|
from app.infra.db.project_routing import ActiveProjectRouting
|
||||||
|
|
||||||
DB_ROLE_BIZ_DATA = "biz_data"
|
DB_ROLE_BIZ_DATA = "biz_data"
|
||||||
DB_ROLE_IOT_DATA = "iot_data"
|
DB_ROLE_IOT_DATA = "iot_data"
|
||||||
@@ -99,6 +100,62 @@ async def get_project_context(
|
|||||||
return await resolve_project_context(x_project_id, current_user, metadata_repo)
|
return await resolve_project_context(x_project_id, current_user, metadata_repo)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_project_business_routing(
|
||||||
|
ctx: ProjectContext = Depends(get_project_context),
|
||||||
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||||
|
) -> ActiveProjectRouting:
|
||||||
|
return await resolve_project_business_routing(ctx, metadata_repo)
|
||||||
|
|
||||||
|
|
||||||
|
async def resolve_project_business_routing(
|
||||||
|
ctx: ProjectContext,
|
||||||
|
metadata_repo: MetadataRepository,
|
||||||
|
) -> ActiveProjectRouting:
|
||||||
|
business = await _get_project_routing(
|
||||||
|
metadata_repo,
|
||||||
|
ctx.project_id,
|
||||||
|
DB_ROLE_BIZ_DATA,
|
||||||
|
DB_TYPE_POSTGRES,
|
||||||
|
"PostgreSQL",
|
||||||
|
)
|
||||||
|
return ActiveProjectRouting(
|
||||||
|
project_code=ctx.project_code,
|
||||||
|
business_dsn=business.dsn,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_project_simulation_routing(
|
||||||
|
ctx: ProjectContext = Depends(get_project_context),
|
||||||
|
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
|
||||||
|
) -> ActiveProjectRouting:
|
||||||
|
return await resolve_project_simulation_routing(ctx, metadata_repo)
|
||||||
|
|
||||||
|
|
||||||
|
async def resolve_project_simulation_routing(
|
||||||
|
ctx: ProjectContext,
|
||||||
|
metadata_repo: MetadataRepository,
|
||||||
|
) -> ActiveProjectRouting:
|
||||||
|
business = await _get_project_routing(
|
||||||
|
metadata_repo,
|
||||||
|
ctx.project_id,
|
||||||
|
DB_ROLE_BIZ_DATA,
|
||||||
|
DB_TYPE_POSTGRES,
|
||||||
|
"PostgreSQL",
|
||||||
|
)
|
||||||
|
timescale = await _get_project_routing(
|
||||||
|
metadata_repo,
|
||||||
|
ctx.project_id,
|
||||||
|
DB_ROLE_IOT_DATA,
|
||||||
|
DB_TYPE_TIMESCALE,
|
||||||
|
"TimescaleDB",
|
||||||
|
)
|
||||||
|
return ActiveProjectRouting(
|
||||||
|
project_code=ctx.project_code,
|
||||||
|
business_dsn=business.dsn,
|
||||||
|
timescale_dsn=timescale.dsn,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _get_project_routing(
|
async def _get_project_routing(
|
||||||
metadata_repo: MetadataRepository,
|
metadata_repo: MetadataRepository,
|
||||||
project_id: UUID,
|
project_id: UUID,
|
||||||
|
|||||||
@@ -26,12 +26,6 @@ class Settings(BaseSettings):
|
|||||||
TIMESCALEDB_DB_PORT: str = "5433"
|
TIMESCALEDB_DB_PORT: str = "5433"
|
||||||
TIMESCALEDB_DB_USER: str = "postgres"
|
TIMESCALEDB_DB_USER: str = "postgres"
|
||||||
TIMESCALEDB_DB_PASSWORD: str = "password"
|
TIMESCALEDB_DB_PASSWORD: str = "password"
|
||||||
# InfluxDB
|
|
||||||
INFLUXDB_URL: str = "http://localhost:8086"
|
|
||||||
INFLUXDB_TOKEN: str = "token"
|
|
||||||
INFLUXDB_ORG: str = "org"
|
|
||||||
INFLUXDB_BUCKET: str = "bucket"
|
|
||||||
|
|
||||||
# Metadata Database Config (PostgreSQL)
|
# Metadata Database Config (PostgreSQL)
|
||||||
METADATA_DB_NAME: str = "system_hub"
|
METADATA_DB_NAME: str = "system_hub"
|
||||||
METADATA_DB_HOST: str = "localhost"
|
METADATA_DB_HOST: str = "localhost"
|
||||||
|
|||||||
Vendored
Vendored
-19
@@ -1,19 +0,0 @@
|
|||||||
import redis
|
|
||||||
import msgpack
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
# Initialize Redis connection
|
|
||||||
redis_client = redis.Redis(host="127.0.0.1", port=6379, db=0)
|
|
||||||
|
|
||||||
def encode_datetime(obj: Any) -> Any:
|
|
||||||
"""Serialize datetime objects to dictionary format."""
|
|
||||||
if isinstance(obj, datetime):
|
|
||||||
return {"__datetime__": True, "as_str": obj.strftime("%Y%m%dT%H:%M:%S.%f")}
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def decode_datetime(obj: Any) -> Any:
|
|
||||||
"""Deserialize dictionary format to datetime objects."""
|
|
||||||
if "__datetime__" in obj:
|
|
||||||
return datetime.strptime(obj["as_str"], "%Y%m%dT%H:%M:%S.%f")
|
|
||||||
return obj
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
|||||||
from app.core.config import settings
|
|
||||||
|
|
||||||
url = settings.INFLUXDB_URL
|
|
||||||
token = settings.INFLUXDB_TOKEN
|
|
||||||
org = settings.INFLUXDB_ORG
|
|
||||||
@@ -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()
|
|
||||||
@@ -20,14 +20,17 @@ def _normalize_postgres_dsn(dsn: str) -> str:
|
|||||||
scheme, rest = dsn.split("://", 1)
|
scheme, rest = dsn.split("://", 1)
|
||||||
if scheme not in ("postgresql", "postgres", "postgresql+psycopg"):
|
if scheme not in ("postgresql", "postgres", "postgresql+psycopg"):
|
||||||
return dsn
|
return dsn
|
||||||
|
if scheme == "postgresql+psycopg":
|
||||||
|
scheme = "postgresql"
|
||||||
|
normalized_dsn = f"{scheme}://{rest}"
|
||||||
if "@" not in rest:
|
if "@" not in rest:
|
||||||
return dsn
|
return normalized_dsn
|
||||||
userinfo, hostinfo = rest.rsplit("@", 1)
|
userinfo, hostinfo = rest.rsplit("@", 1)
|
||||||
if ":" not in userinfo:
|
if ":" not in userinfo:
|
||||||
return dsn
|
return normalized_dsn
|
||||||
username, password = userinfo.split(":", 1)
|
username, password = userinfo.split(":", 1)
|
||||||
if "@" not in password:
|
if "@" not in password:
|
||||||
return dsn
|
return normalized_dsn
|
||||||
password = password.replace("@", "%40")
|
password = password.replace("@", "%40")
|
||||||
return f"{scheme}://{username}:{password}@{hostinfo}"
|
return f"{scheme}://{username}:{password}@{hostinfo}"
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from contextlib import asynccontextmanager
|
|||||||
from typing import AsyncGenerator, Dict, Optional
|
from typing import AsyncGenerator, Dict, Optional
|
||||||
import psycopg_pool
|
import psycopg_pool
|
||||||
from psycopg.rows import dict_row
|
from psycopg.rows import dict_row
|
||||||
import app.core.config as postgresql_info
|
from app.infra.db.project_routing import get_project_pgconn_string
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -13,6 +13,7 @@ class Database:
|
|||||||
def __init__(self, db_name=None):
|
def __init__(self, db_name=None):
|
||||||
self.pool = None
|
self.pool = None
|
||||||
self.db_name = db_name
|
self.db_name = db_name
|
||||||
|
self.conninfo = None
|
||||||
|
|
||||||
def init_pool(self, db_name=None):
|
def init_pool(self, db_name=None):
|
||||||
"""Initialize the connection pool."""
|
"""Initialize the connection pool."""
|
||||||
@@ -21,9 +22,10 @@ class Database:
|
|||||||
|
|
||||||
# Get connection string, handling default case where target_db_name might be None
|
# Get connection string, handling default case where target_db_name might be None
|
||||||
if target_db_name:
|
if target_db_name:
|
||||||
conn_string = postgresql_info.get_pgconn_string(db_name=target_db_name)
|
conn_string = get_project_pgconn_string(db_name=target_db_name)
|
||||||
else:
|
else:
|
||||||
conn_string = postgresql_info.get_pgconn_string()
|
conn_string = get_project_pgconn_string()
|
||||||
|
self.conninfo = conn_string
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.pool = psycopg_pool.AsyncConnectionPool(
|
self.pool = psycopg_pool.AsyncConnectionPool(
|
||||||
@@ -75,6 +77,12 @@ async def get_database_instance(db_name: Optional[str] = None) -> Database:
|
|||||||
if not db_name:
|
if not db_name:
|
||||||
return db # 返回默认数据库实例
|
return db # 返回默认数据库实例
|
||||||
|
|
||||||
|
expected_conninfo = get_project_pgconn_string(db_name=db_name)
|
||||||
|
existing = _database_instances.get(db_name)
|
||||||
|
if existing is not None and existing.conninfo != expected_conninfo:
|
||||||
|
await existing.close()
|
||||||
|
del _database_instances[db_name]
|
||||||
|
|
||||||
if db_name not in _database_instances:
|
if db_name not in _database_instances:
|
||||||
# 创建新的数据库实例
|
# 创建新的数据库实例
|
||||||
instance = create_database_instance(db_name)
|
instance = create_database_instance(db_name)
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from contextvars import ContextVar, Token
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Iterator
|
||||||
|
|
||||||
|
from psycopg.conninfo import make_conninfo
|
||||||
|
|
||||||
|
from app.core.config import get_pgconn_string, get_timescaledb_pgconn_string
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ActiveProjectRouting:
|
||||||
|
project_code: str
|
||||||
|
business_dsn: str
|
||||||
|
timescale_dsn: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
_active_project_routing: ContextVar[ActiveProjectRouting | None] = ContextVar(
|
||||||
|
"active_project_routing",
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_project_routing() -> ActiveProjectRouting | None:
|
||||||
|
return _active_project_routing.get()
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def activate_project_routing(
|
||||||
|
routing: ActiveProjectRouting,
|
||||||
|
) -> Iterator[ActiveProjectRouting]:
|
||||||
|
token: Token[ActiveProjectRouting | None] = _active_project_routing.set(routing)
|
||||||
|
try:
|
||||||
|
yield routing
|
||||||
|
finally:
|
||||||
|
_active_project_routing.reset(token)
|
||||||
|
|
||||||
|
|
||||||
|
def _dsn_for_database(dsn: str, database_name: str) -> str:
|
||||||
|
return make_conninfo(dsn, dbname=database_name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_project_pgconn_string(db_name: str | None = None) -> str:
|
||||||
|
routing = get_active_project_routing()
|
||||||
|
if routing is None:
|
||||||
|
return get_pgconn_string(db_name=db_name)
|
||||||
|
if db_name is None or db_name == routing.project_code:
|
||||||
|
return routing.business_dsn
|
||||||
|
return _dsn_for_database(routing.business_dsn, db_name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_project_timescale_pgconn_string(db_name: str | None = None) -> str:
|
||||||
|
routing = get_active_project_routing()
|
||||||
|
if routing is None:
|
||||||
|
return get_timescaledb_pgconn_string(db_name=db_name)
|
||||||
|
if routing.timescale_dsn is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"TimescaleDB routing is not configured for project {routing.project_code}"
|
||||||
|
)
|
||||||
|
# Legacy simulation code used to derive the Timescale database name from
|
||||||
|
# the project code. Project-scoped requests must instead use the complete
|
||||||
|
# iot_data DSN selected by metadata routing.
|
||||||
|
return routing.timescale_dsn
|
||||||
@@ -3,7 +3,7 @@ from contextlib import asynccontextmanager
|
|||||||
from typing import AsyncGenerator, Dict, Optional
|
from typing import AsyncGenerator, Dict, Optional
|
||||||
import psycopg_pool
|
import psycopg_pool
|
||||||
from psycopg.rows import dict_row
|
from psycopg.rows import dict_row
|
||||||
from app.core.config import get_timescaledb_pgconn_string
|
from app.infra.db.project_routing import get_project_timescale_pgconn_string
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -13,6 +13,7 @@ class Database:
|
|||||||
def __init__(self, db_name=None):
|
def __init__(self, db_name=None):
|
||||||
self.pool = None
|
self.pool = None
|
||||||
self.db_name = db_name
|
self.db_name = db_name
|
||||||
|
self.conninfo = None
|
||||||
|
|
||||||
def init_pool(self, db_name=None):
|
def init_pool(self, db_name=None):
|
||||||
"""Initialize the connection pool."""
|
"""Initialize the connection pool."""
|
||||||
@@ -21,9 +22,10 @@ class Database:
|
|||||||
|
|
||||||
# Get connection string, handling default case where target_db_name might be None
|
# Get connection string, handling default case where target_db_name might be None
|
||||||
if target_db_name:
|
if target_db_name:
|
||||||
conn_string = get_timescaledb_pgconn_string(db_name=target_db_name)
|
conn_string = get_project_timescale_pgconn_string(db_name=target_db_name)
|
||||||
else:
|
else:
|
||||||
conn_string = get_timescaledb_pgconn_string()
|
conn_string = get_project_timescale_pgconn_string()
|
||||||
|
self.conninfo = conn_string
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.pool = psycopg_pool.AsyncConnectionPool(
|
self.pool = psycopg_pool.AsyncConnectionPool(
|
||||||
@@ -54,8 +56,8 @@ class Database:
|
|||||||
"""Get the TimescaleDB connection string."""
|
"""Get the TimescaleDB connection string."""
|
||||||
target_db_name = db_name or self.db_name
|
target_db_name = db_name or self.db_name
|
||||||
if target_db_name:
|
if target_db_name:
|
||||||
return get_timescaledb_pgconn_string(db_name=target_db_name)
|
return get_project_timescale_pgconn_string(db_name=target_db_name)
|
||||||
return get_timescaledb_pgconn_string()
|
return get_project_timescale_pgconn_string()
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def get_connection(self) -> AsyncGenerator:
|
async def get_connection(self) -> AsyncGenerator:
|
||||||
@@ -84,6 +86,12 @@ async def get_database_instance(db_name: Optional[str] = None) -> Database:
|
|||||||
if not db_name:
|
if not db_name:
|
||||||
return db # 返回默认数据库实例
|
return db # 返回默认数据库实例
|
||||||
|
|
||||||
|
expected_conninfo = get_project_timescale_pgconn_string(db_name=db_name)
|
||||||
|
existing = _database_instances.get(db_name)
|
||||||
|
if existing is not None and existing.conninfo != expected_conninfo:
|
||||||
|
await existing.close()
|
||||||
|
del _database_instances[db_name]
|
||||||
|
|
||||||
if db_name not in _database_instances:
|
if db_name not in _database_instances:
|
||||||
# 创建新的数据库实例
|
# 创建新的数据库实例
|
||||||
instance = create_database_instance(db_name)
|
instance = create_database_instance(db_name)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import psycopg
|
|||||||
from psycopg import sql
|
from psycopg import sql
|
||||||
from psycopg.rows import dict_row
|
from psycopg.rows import dict_row
|
||||||
import time
|
import time
|
||||||
from app.core.config import get_timescaledb_pgconn_string
|
from app.infra.db.project_routing import get_project_timescale_pgconn_string
|
||||||
from app.infra.db.timescaledb.repositories.scheme import SchemeRepository
|
from app.infra.db.timescaledb.repositories.scheme import SchemeRepository
|
||||||
from app.infra.db.timescaledb.repositories.realtime import RealtimeRepository
|
from app.infra.db.timescaledb.repositories.realtime import RealtimeRepository
|
||||||
from app.infra.db.timescaledb.repositories.scada import ScadaRepository
|
from app.infra.db.timescaledb.repositories.scada import ScadaRepository
|
||||||
@@ -26,9 +26,9 @@ class InternalStorage:
|
|||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
try:
|
try:
|
||||||
conn_string = (
|
conn_string = (
|
||||||
get_timescaledb_pgconn_string(db_name=db_name)
|
get_project_timescale_pgconn_string(db_name=db_name)
|
||||||
if db_name
|
if db_name
|
||||||
else get_timescaledb_pgconn_string()
|
else get_project_timescale_pgconn_string()
|
||||||
)
|
)
|
||||||
with psycopg.Connection.connect(conn_string) as conn:
|
with psycopg.Connection.connect(conn_string) as conn:
|
||||||
RealtimeRepository.store_realtime_simulation_result_sync(
|
RealtimeRepository.store_realtime_simulation_result_sync(
|
||||||
@@ -58,9 +58,9 @@ class InternalStorage:
|
|||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
try:
|
try:
|
||||||
conn_string = (
|
conn_string = (
|
||||||
get_timescaledb_pgconn_string(db_name=db_name)
|
get_project_timescale_pgconn_string(db_name=db_name)
|
||||||
if db_name
|
if db_name
|
||||||
else get_timescaledb_pgconn_string()
|
else get_project_timescale_pgconn_string()
|
||||||
)
|
)
|
||||||
with psycopg.Connection.connect(conn_string) as conn:
|
with psycopg.Connection.connect(conn_string) as conn:
|
||||||
SchemeRepository.store_scheme_simulation_result_sync(
|
SchemeRepository.store_scheme_simulation_result_sync(
|
||||||
@@ -99,9 +99,9 @@ class InternalQueries:
|
|||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
try:
|
try:
|
||||||
conn_string = (
|
conn_string = (
|
||||||
get_timescaledb_pgconn_string(db_name=db_name)
|
get_project_timescale_pgconn_string(db_name=db_name)
|
||||||
if db_name
|
if db_name
|
||||||
else get_timescaledb_pgconn_string()
|
else get_project_timescale_pgconn_string()
|
||||||
)
|
)
|
||||||
with psycopg.Connection.connect(conn_string) as conn:
|
with psycopg.Connection.connect(conn_string) as conn:
|
||||||
rows = ScadaRepository.get_scada_by_ids_time_range_sync(
|
rows = ScadaRepository.get_scada_by_ids_time_range_sync(
|
||||||
@@ -140,9 +140,9 @@ class InternalQueries:
|
|||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
try:
|
try:
|
||||||
conn_string = (
|
conn_string = (
|
||||||
get_timescaledb_pgconn_string(db_name=db_name)
|
get_project_timescale_pgconn_string(db_name=db_name)
|
||||||
if db_name
|
if db_name
|
||||||
else get_timescaledb_pgconn_string()
|
else get_project_timescale_pgconn_string()
|
||||||
)
|
)
|
||||||
with psycopg.Connection.connect(conn_string) as conn:
|
with psycopg.Connection.connect(conn_string) as conn:
|
||||||
rows = ScadaRepository.get_scada_by_ids_time_range_sync(
|
rows = ScadaRepository.get_scada_by_ids_time_range_sync(
|
||||||
@@ -185,9 +185,9 @@ class InternalQueries:
|
|||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
try:
|
try:
|
||||||
conn_string = (
|
conn_string = (
|
||||||
get_timescaledb_pgconn_string(db_name=db_name)
|
get_project_timescale_pgconn_string(db_name=db_name)
|
||||||
if db_name
|
if db_name
|
||||||
else get_timescaledb_pgconn_string()
|
else get_project_timescale_pgconn_string()
|
||||||
)
|
)
|
||||||
with psycopg.Connection.connect(conn_string) as conn:
|
with psycopg.Connection.connect(conn_string) as conn:
|
||||||
return ScadaRepository.get_latest_scada_time_sync(
|
return ScadaRepository.get_latest_scada_time_sync(
|
||||||
@@ -286,9 +286,9 @@ class InternalQueries:
|
|||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
try:
|
try:
|
||||||
conn_string = (
|
conn_string = (
|
||||||
get_timescaledb_pgconn_string(db_name=db_name)
|
get_project_timescale_pgconn_string(db_name=db_name)
|
||||||
if db_name
|
if db_name
|
||||||
else get_timescaledb_pgconn_string()
|
else get_project_timescale_pgconn_string()
|
||||||
)
|
)
|
||||||
with psycopg.Connection.connect(conn_string) as conn:
|
with psycopg.Connection.connect(conn_string) as conn:
|
||||||
with conn.cursor(row_factory=dict_row) as cur:
|
with conn.cursor(row_factory=dict_row) as cur:
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ from threading import RLock
|
|||||||
|
|
||||||
import psycopg as pg
|
import psycopg as pg
|
||||||
|
|
||||||
from app.core.config import get_pgconn_string
|
from app.infra.db.project_routing import get_project_pgconn_string
|
||||||
|
|
||||||
g_conn_dict: dict[str, pg.Connection] = {}
|
g_conn_dict: dict[str, pg.Connection] = {}
|
||||||
|
g_conninfo_dict: dict[str, str] = {}
|
||||||
_registry_lock = RLock()
|
_registry_lock = RLock()
|
||||||
_project_locks: dict[str, RLock] = {}
|
_project_locks: dict[str, RLock] = {}
|
||||||
|
|
||||||
@@ -42,14 +43,18 @@ 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):
|
||||||
|
conninfo = get_project_pgconn_string(db_name=name)
|
||||||
connection = g_conn_dict.get(name)
|
connection = g_conn_dict.get(name)
|
||||||
if connection is None or not _is_healthy(connection):
|
if (
|
||||||
|
connection is None
|
||||||
|
or g_conninfo_dict.get(name) != conninfo
|
||||||
|
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(conninfo=conninfo, autocommit=True)
|
||||||
conninfo=get_pgconn_string(db_name=name), autocommit=True
|
|
||||||
)
|
|
||||||
g_conn_dict[name] = connection
|
g_conn_dict[name] = connection
|
||||||
|
g_conninfo_dict[name] = conninfo
|
||||||
return connection
|
return connection
|
||||||
|
|
||||||
|
|
||||||
@@ -60,6 +65,12 @@ def is_connection_open(name: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
if not _is_healthy(connection):
|
if not _is_healthy(connection):
|
||||||
del g_conn_dict[name]
|
del g_conn_dict[name]
|
||||||
|
g_conninfo_dict.pop(name, None)
|
||||||
|
_close_connection(connection)
|
||||||
|
return False
|
||||||
|
if g_conninfo_dict.get(name) != get_project_pgconn_string(db_name=name):
|
||||||
|
del g_conn_dict[name]
|
||||||
|
g_conninfo_dict.pop(name, None)
|
||||||
_close_connection(connection)
|
_close_connection(connection)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
@@ -68,6 +79,7 @@ def is_connection_open(name: str) -> bool:
|
|||||||
def close_connection(name: str) -> None:
|
def close_connection(name: str) -> None:
|
||||||
with _get_project_lock(name):
|
with _get_project_lock(name):
|
||||||
connection = g_conn_dict.pop(name, None)
|
connection = g_conn_dict.pop(name, None)
|
||||||
|
g_conninfo_dict.pop(name, None)
|
||||||
if connection is not None:
|
if connection is not None:
|
||||||
_close_connection(connection)
|
_close_connection(connection)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ from .connection import (
|
|||||||
is_connection_open,
|
is_connection_open,
|
||||||
open_connection,
|
open_connection,
|
||||||
)
|
)
|
||||||
from app.core.config import get_pgconn_string, get_pg_config, get_pg_password
|
from app.core.config import get_pg_config, get_pg_password
|
||||||
|
from app.infra.db.project_routing import get_project_pgconn_string
|
||||||
|
|
||||||
# no undo/redo
|
# no undo/redo
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ _server_databases = ["template0", "template1", "postgres", "project"]
|
|||||||
|
|
||||||
def list_project() -> list[str]:
|
def list_project() -> list[str]:
|
||||||
ps = []
|
ps = []
|
||||||
with pg.connect(conninfo=get_pgconn_string(), autocommit=True) as conn:
|
with pg.connect(conninfo=get_project_pgconn_string(), autocommit=True) as conn:
|
||||||
with conn.cursor(row_factory=dict_row) as cur:
|
with conn.cursor(row_factory=dict_row) as cur:
|
||||||
for p in cur.execute(
|
for p in cur.execute(
|
||||||
f"select datname from pg_database where datname <> 'postgres' and datname <> 'template0' and datname <> 'template1' and datname <> 'project'"
|
f"select datname from pg_database where datname <> 'postgres' and datname <> 'template0' and datname <> 'template1' and datname <> 'project'"
|
||||||
@@ -27,7 +28,7 @@ def list_project() -> list[str]:
|
|||||||
|
|
||||||
def have_project(name: str) -> bool:
|
def have_project(name: str) -> bool:
|
||||||
with pg.connect(
|
with pg.connect(
|
||||||
conninfo=get_pgconn_string(db_name="postgres"), autocommit=True
|
conninfo=get_project_pgconn_string(db_name="postgres"), autocommit=True
|
||||||
) as conn:
|
) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("select 1 from pg_database where datname = %s", (name,))
|
cur.execute("select 1 from pg_database where datname = %s", (name,))
|
||||||
@@ -38,7 +39,7 @@ def copy_project(source: str, new: str) -> None:
|
|||||||
close_connection(source)
|
close_connection(source)
|
||||||
|
|
||||||
with pg.connect(
|
with pg.connect(
|
||||||
conninfo=get_pgconn_string(db_name="postgres"), autocommit=True
|
conninfo=get_project_pgconn_string(db_name="postgres"), autocommit=True
|
||||||
) as admin_conn:
|
) as admin_conn:
|
||||||
with admin_conn.cursor() as cur:
|
with admin_conn.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
@@ -131,7 +132,9 @@ class CopyProjectEx:
|
|||||||
connection.commit()
|
connection.commit()
|
||||||
|
|
||||||
def __call__(self, source: str, new_db: str, excluded_tables: [str] = None) -> None:
|
def __call__(self, source: str, new_db: str, excluded_tables: [str] = None) -> None:
|
||||||
source_connection = pg.connect(conninfo=get_pgconn_string(), autocommit=True)
|
source_connection = pg.connect(
|
||||||
|
conninfo=get_project_pgconn_string(), autocommit=True
|
||||||
|
)
|
||||||
|
|
||||||
self.create_database(source_connection, new_db)
|
self.create_database(source_connection, new_db)
|
||||||
|
|
||||||
@@ -140,7 +143,7 @@ class CopyProjectEx:
|
|||||||
source_connection.close()
|
source_connection.close()
|
||||||
|
|
||||||
new_db_connection = pg.connect(
|
new_db_connection = pg.connect(
|
||||||
conninfo=get_pgconn_string(db_name=new_db), autocommit=True
|
conninfo=get_project_pgconn_string(db_name=new_db), autocommit=True
|
||||||
)
|
)
|
||||||
self.init_operation_table(new_db_connection, excluded_tables)
|
self.init_operation_table(new_db_connection, excluded_tables)
|
||||||
new_db_connection.close()
|
new_db_connection.close()
|
||||||
@@ -151,7 +154,7 @@ def create_project(name: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def delete_project(name: str) -> None:
|
def delete_project(name: str) -> None:
|
||||||
with pg.connect(conninfo=get_pgconn_string(), autocommit=True) as conn:
|
with pg.connect(conninfo=get_project_pgconn_string(), autocommit=True) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
f"select pg_terminate_backend(pid) from pg_stat_activity where datname = '{name}'"
|
f"select pg_terminate_backend(pid) from pg_stat_activity where datname = '{name}'"
|
||||||
@@ -161,7 +164,7 @@ def delete_project(name: str) -> None:
|
|||||||
|
|
||||||
def clean_project(excluded: list[str] = []) -> None:
|
def clean_project(excluded: list[str] = []) -> None:
|
||||||
projects = list_project()
|
projects = list_project()
|
||||||
with pg.connect(conninfo=get_pgconn_string(), autocommit=True) as conn:
|
with pg.connect(conninfo=get_project_pgconn_string(), autocommit=True) as conn:
|
||||||
with conn.cursor(row_factory=dict_row) as cur:
|
with conn.cursor(row_factory=dict_row) as cur:
|
||||||
row = cur.execute(f"select current_database()").fetchone()
|
row = cur.execute(f"select current_database()").fetchone()
|
||||||
if row != None:
|
if row != None:
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ 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 = []
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import chardet
|
|||||||
import psycopg
|
import psycopg
|
||||||
from psycopg import sql
|
from psycopg import sql
|
||||||
|
|
||||||
import app.services.project_info as project_info
|
from app.infra.db.project_routing import get_project_pgconn_string
|
||||||
from app.core.config import get_pgconn_string
|
|
||||||
from app.services.tjnetwork import read_inp
|
from app.services.tjnetwork import read_inp
|
||||||
|
|
||||||
|
|
||||||
@@ -15,13 +14,14 @@ from app.services.tjnetwork import read_inp
|
|||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
|
|
||||||
def network_update(file_path: str) -> None:
|
def network_update(file_path: str, project_code: str) -> None:
|
||||||
"""
|
"""
|
||||||
更新pg数据库中的inp文件
|
更新pg数据库中的inp文件
|
||||||
:param file_path: inp文件
|
:param file_path: inp文件
|
||||||
|
:param project_code: 元数据项目代码
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
read_inp("szh", file_path)
|
read_inp(project_code, file_path)
|
||||||
|
|
||||||
csv_path = "./history_pattern_flow.csv"
|
csv_path = "./history_pattern_flow.csv"
|
||||||
|
|
||||||
@@ -51,8 +51,7 @@ def network_update(file_path: str) -> None:
|
|||||||
if os.path.exists(csv_path):
|
if os.path.exists(csv_path):
|
||||||
print(f"history_patterns_flows文件存在,开始处理...")
|
print(f"history_patterns_flows文件存在,开始处理...")
|
||||||
|
|
||||||
# 连接到 PostgreSQL 数据库(这里是数据库 "bb")
|
with psycopg.connect(get_project_pgconn_string(project_code)) as conn:
|
||||||
with psycopg.connect(f"dbname={project_info.name} host=127.0.0.1") as conn:
|
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
with open(csv_path, newline="", encoding="utf-8-sig") as csvfile:
|
with open(csv_path, newline="", encoding="utf-8-sig") as csvfile:
|
||||||
reader = csv.DictReader(csvfile)
|
reader = csv.DictReader(csvfile)
|
||||||
@@ -92,7 +91,7 @@ def submit_scada_info(name: str, coord_id: str) -> None:
|
|||||||
print(f"检测到的文件编码:{file_encoding}")
|
print(f"检测到的文件编码:{file_encoding}")
|
||||||
try:
|
try:
|
||||||
# 动态替换数据库名称
|
# 动态替换数据库名称
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
|
|
||||||
# 连接到 PostgreSQL 数据库(这里是数据库 "bb")
|
# 连接到 PostgreSQL 数据库(这里是数据库 "bb")
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import pandas as pd
|
|||||||
import psycopg
|
import psycopg
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
from app.core.config import get_pgconn_string
|
from app.infra.db.project_routing import get_project_pgconn_string
|
||||||
from app.services.time_api import parse_utc_time
|
from app.services.time_api import parse_utc_time
|
||||||
|
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ def scheme_name_exists(name: str, scheme_name: str) -> bool:
|
|||||||
:return: 如果存在返回 True,否则返回 False
|
:return: 如果存在返回 True,否则返回 False
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
@@ -57,7 +57,7 @@ def store_scheme_info(
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
sql = """
|
sql = """
|
||||||
@@ -93,7 +93,7 @@ def delete_scheme_info(name: str, scheme_name: str) -> None:
|
|||||||
:param scheme_name: 要删除的方案名称
|
:param scheme_name: 要删除的方案名称
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
# 使用参数化查询删除方案记录
|
# 使用参数化查询删除方案记录
|
||||||
@@ -121,7 +121,7 @@ def query_scheme_list(
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 动态替换数据库名称
|
# 动态替换数据库名称
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
# 连接到 PostgreSQL 数据库(这里是数据库 "bb")
|
# 连接到 PostgreSQL 数据库(这里是数据库 "bb")
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
@@ -203,7 +203,7 @@ def query_scheme_detail(
|
|||||||
scheme_type,
|
scheme_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
if scheme_type:
|
if scheme_type:
|
||||||
@@ -255,7 +255,7 @@ def store_leakage_identify_result(
|
|||||||
run_status: str = "completed",
|
run_status: str = "completed",
|
||||||
error_message: str | None = None,
|
error_message: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
@@ -299,7 +299,7 @@ def query_leakage_identify_schemes(
|
|||||||
scheme_type: str = "dma_leak_identification",
|
scheme_type: str = "dma_leak_identification",
|
||||||
query_date: date | None = None,
|
query_date: date | None = None,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
if query_date is None:
|
if query_date is None:
|
||||||
@@ -343,7 +343,7 @@ def query_leakage_identify_schemes(
|
|||||||
|
|
||||||
|
|
||||||
def query_leakage_identify_scheme_detail(name: str, scheme_name: str) -> dict:
|
def query_leakage_identify_scheme_detail(name: str, scheme_name: str) -> dict:
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
@@ -400,7 +400,7 @@ def query_burst_location_schemes(
|
|||||||
scheme_type: str = "burst_location",
|
scheme_type: str = "burst_location",
|
||||||
query_date: date | None = None,
|
query_date: date | None = None,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
if query_date is None:
|
if query_date is None:
|
||||||
@@ -444,7 +444,7 @@ def query_burst_location_schemes(
|
|||||||
|
|
||||||
|
|
||||||
def query_burst_location_scheme_detail(name: str, scheme_name: str) -> dict:
|
def query_burst_location_scheme_detail(name: str, scheme_name: str) -> dict:
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
@@ -479,7 +479,7 @@ def query_burst_detection_schemes(
|
|||||||
scheme_type: str = "burst_detection",
|
scheme_type: str = "burst_detection",
|
||||||
query_date: date | None = None,
|
query_date: date | None = None,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
if query_date is None:
|
if query_date is None:
|
||||||
@@ -523,7 +523,7 @@ def query_burst_detection_schemes(
|
|||||||
|
|
||||||
|
|
||||||
def query_burst_detection_scheme_detail(name: str, scheme_name: str) -> dict:
|
def query_burst_detection_scheme_detail(name: str, scheme_name: str) -> dict:
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
@@ -564,7 +564,7 @@ def upload_shp_to_pg(name: str, table_name: str, role: str, shp_file_path: str):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 动态连接到指定的数据库
|
# 动态连接到指定的数据库
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
# 读取 Shapefile 文件
|
# 读取 Shapefile 文件
|
||||||
gdf = gpd.read_file(shp_file_path)
|
gdf = gpd.read_file(shp_file_path)
|
||||||
@@ -604,7 +604,7 @@ def submit_risk_probability_result(name: str, result_file_path: str) -> None:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 动态替换数据库名称
|
# 动态替换数据库名称
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
|
|
||||||
# 连接到 PostgreSQL 数据库
|
# 连接到 PostgreSQL 数据库
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
|
|||||||
+10
-11
@@ -28,14 +28,13 @@ 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
|
||||||
import app.services.globals as globals
|
import app.services.globals as globals
|
||||||
import app.services.project_info as project_info
|
import app.services.project_info as project_info
|
||||||
from app.services.time_api import parse_beijing_time, parse_clock_duration_seconds
|
from app.services.time_api import parse_beijing_time, parse_clock_duration_seconds
|
||||||
from app.core.config import get_pgconn_string
|
from app.infra.db.project_routing import get_project_pgconn_string
|
||||||
from app.infra.db.timescaledb.internal_queries import (
|
from app.infra.db.timescaledb.internal_queries import (
|
||||||
InternalQueries as TimescaleInternalQueries,
|
InternalQueries as TimescaleInternalQueries,
|
||||||
)
|
)
|
||||||
@@ -55,7 +54,7 @@ def query_corresponding_element_id_and_query_id(name: str) -> None:
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
# 连接数据库
|
# 连接数据库
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
try:
|
try:
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
@@ -100,7 +99,7 @@ def query_corresponding_pattern_id_and_query_id(name: str) -> None:
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
# 连接数据库
|
# 连接数据库
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
try:
|
try:
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
@@ -140,7 +139,7 @@ def query_non_realtime_region(name: str) -> dict:
|
|||||||
"""
|
"""
|
||||||
source_outflow_regions = [] # 用于存储所有 region(包含重复的)
|
source_outflow_regions = [] # 用于存储所有 region(包含重复的)
|
||||||
# 构建连接字符串
|
# 构建连接字符串
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
try:
|
try:
|
||||||
# 连接到数据库
|
# 连接到数据库
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
@@ -217,7 +216,7 @@ def query_non_realtime_region_patterns(
|
|||||||
region_tuple_to_key = {
|
region_tuple_to_key = {
|
||||||
frozenset(ids): region for region, ids in globals.source_outflow_region.items()
|
frozenset(ids): region for region, ids in globals.source_outflow_region.items()
|
||||||
}
|
}
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
try:
|
try:
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
@@ -305,7 +304,7 @@ def query_realtime_region_pipe_flow_and_demand_id(
|
|||||||
region_tuple_to_key = {
|
region_tuple_to_key = {
|
||||||
frozenset(ids): region for region, ids in globals.source_outflow_region.items()
|
frozenset(ids): region for region, ids in globals.source_outflow_region.items()
|
||||||
}
|
}
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
try:
|
try:
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
@@ -377,7 +376,7 @@ def query_pipe_flow_region_patterns(
|
|||||||
:param column_prefix: 需要提取的列的前缀
|
:param column_prefix: 需要提取的列的前缀
|
||||||
:return: pipe_flow_region_patterns 字典
|
:return: pipe_flow_region_patterns 字典
|
||||||
"""
|
"""
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
try:
|
try:
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
@@ -440,7 +439,7 @@ def query_SCADA_ID_corresponding_info(name: str, SCADA_ID: str) -> dict:
|
|||||||
:param SCADA_ID: SCADA设备的ID
|
:param SCADA_ID: SCADA设备的ID
|
||||||
:return: 包含associated_element_id和api_query_id的字典
|
:return: 包含associated_element_id和api_query_id的字典
|
||||||
"""
|
"""
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
try:
|
try:
|
||||||
# 使用 psycopg.connect 创建连接
|
# 使用 psycopg.connect 创建连接
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
@@ -496,7 +495,7 @@ def get_source_outflow_region_id(
|
|||||||
"No associated_source_outflow_id found in source_outflow_region."
|
"No associated_source_outflow_id found in source_outflow_region."
|
||||||
)
|
)
|
||||||
return globals.source_outflow_region_id
|
return globals.source_outflow_region_id
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
try:
|
try:
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
@@ -553,7 +552,7 @@ def get_realtime_region_patterns(
|
|||||||
globals.realtime_region_pipe_flow_and_demand_patterns = {
|
globals.realtime_region_pipe_flow_and_demand_patterns = {
|
||||||
region: [] for region in globals.realtime_region_pipe_flow_and_demand_id.keys()
|
region: [] for region in globals.realtime_region_pipe_flow_and_demand_id.keys()
|
||||||
}
|
}
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_project_pgconn_string(db_name=name)
|
||||||
try:
|
try:
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"contracts": {
|
"contracts": {
|
||||||
"server": {
|
"server": {
|
||||||
"file": "server-v1.openapi.json",
|
"file": "server-v1.openapi.json",
|
||||||
"sha256": "df7ae927dcf5ae32c3c1ad9be3245b1b78b984ce1902dd91e6313770860e0d48"
|
"sha256": "ac9b6fac185dfd999f1791cba51eb482df17a427b361963250aafa5fb1a276b4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5227,97 +5227,6 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/all-redis": {
|
|
||||||
"delete": {
|
|
||||||
"description": "清空整个Redis数据库的所有缓存",
|
|
||||||
"operationId": "delete_all_redis",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "header",
|
|
||||||
"name": "X-Project-Id",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "X-Project-Id",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"204": {
|
|
||||||
"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": "清除所有缓存",
|
|
||||||
"tags": [
|
|
||||||
"Cache"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/all-scada-properties": {
|
"/api/v1/all-scada-properties": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "获取指定水网中所有SCADA点的属性信息",
|
"description": "获取指定水网中所有SCADA点的属性信息",
|
||||||
@@ -29452,308 +29361,6 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/redis": {
|
|
||||||
"get": {
|
|
||||||
"description": "获取Redis中所有的缓存键",
|
|
||||||
"operationId": "get_redis",
|
|
||||||
"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": "查询缓存键列表",
|
|
||||||
"tags": [
|
|
||||||
"Cache"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/redis-keys": {
|
|
||||||
"delete": {
|
|
||||||
"description": "根据模式清除匹配的Redis缓存键",
|
|
||||||
"operationId": "delete_redis_keys",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "缓存键模式(支持通配符)",
|
|
||||||
"in": "query",
|
|
||||||
"name": "keys",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"description": "缓存键模式(支持通配符)",
|
|
||||||
"title": "Keys",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"in": "header",
|
|
||||||
"name": "X-Project-Id",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "X-Project-Id",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"204": {
|
|
||||||
"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": "清除匹配的缓存键",
|
|
||||||
"tags": [
|
|
||||||
"Cache"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/redis-keys/detail": {
|
|
||||||
"delete": {
|
|
||||||
"description": "根据键名清除单个Redis缓存",
|
|
||||||
"operationId": "delete_redis_keys_detail",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "缓存键名",
|
|
||||||
"in": "query",
|
|
||||||
"name": "key",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"description": "缓存键名",
|
|
||||||
"title": "Key",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"in": "header",
|
|
||||||
"name": "X-Project-Id",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "X-Project-Id",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"204": {
|
|
||||||
"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": "清除单个缓存键",
|
|
||||||
"tags": [
|
|
||||||
"Cache"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/redos": {
|
"/api/v1/redos": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "重做网络上被撤销的操作",
|
"description": "重做网络上被撤销的操作",
|
||||||
|
|||||||
@@ -16,12 +16,8 @@ services:
|
|||||||
- ../../resources:/app/resources
|
- ../../resources:/app/resources
|
||||||
environment:
|
environment:
|
||||||
- PYTHONPATH=/app
|
- PYTHONPATH=/app
|
||||||
- REDIS_HOST=redis
|
|
||||||
- REDIS_PORT=${REDIS_PORT}
|
|
||||||
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
|
||||||
# Add other DB connections here as needed by your app
|
# Add other DB connections here as needed by your app
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
|
||||||
- timescaledb
|
- timescaledb
|
||||||
- postgis
|
- postgis
|
||||||
|
|
||||||
@@ -29,17 +25,6 @@ services:
|
|||||||
# Infrastructure Services
|
# Infrastructure Services
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
# --- Redis ---
|
|
||||||
redis:
|
|
||||||
image: redis:latest
|
|
||||||
container_name: redis
|
|
||||||
restart: always
|
|
||||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
|
||||||
ports:
|
|
||||||
- "${REDIS_PORT}:6379"
|
|
||||||
volumes:
|
|
||||||
- ./redis/data:/data
|
|
||||||
|
|
||||||
# --- Keycloak ---
|
# --- Keycloak ---
|
||||||
keycloakDB:
|
keycloakDB:
|
||||||
image: postgis/postgis:14-3.5
|
image: postgis/postgis:14-3.5
|
||||||
|
|||||||
+1
-5
@@ -29,7 +29,6 @@ email-validator==2.3.0
|
|||||||
esda==2.7.0
|
esda==2.7.0
|
||||||
et_xmlfile==2.0.0
|
et_xmlfile==2.0.0
|
||||||
exceptiongroup==1.3.1
|
exceptiongroup==1.3.1
|
||||||
fakeredis==2.33.0
|
|
||||||
fastapi==0.128.0
|
fastapi==0.128.0
|
||||||
fastmcp==2.9.2
|
fastmcp==2.9.2
|
||||||
fonttools==4.58.0
|
fonttools==4.58.0
|
||||||
@@ -43,7 +42,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
|
||||||
@@ -107,7 +105,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
|
||||||
pydevd-pycharm==243.16718.36
|
pydevd-pycharm==243.16718.36
|
||||||
pydocket==0.16.6
|
|
||||||
Pygments==2.18.0
|
Pygments==2.18.0
|
||||||
PyJWT==2.10.1
|
PyJWT==2.10.1
|
||||||
pykalman==0.10.2
|
pykalman==0.10.2
|
||||||
@@ -127,7 +124,6 @@ pytz==2025.2
|
|||||||
PyYAML==6.0.3
|
PyYAML==6.0.3
|
||||||
pyzmq==26.2.1
|
pyzmq==26.2.1
|
||||||
reactivex==4.0.4
|
reactivex==4.0.4
|
||||||
redis==5.2.1
|
|
||||||
referencing==0.36.2
|
referencing==0.36.2
|
||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
rich==14.2.0
|
rich==14.2.0
|
||||||
@@ -168,4 +164,4 @@ 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
|
pyclipper==1.4.0
|
||||||
|
|||||||
Binary file not shown.
@@ -1,25 +0,0 @@
|
|||||||
import auto_realtime
|
|
||||||
import auto_store_non_realtime_SCADA_data
|
|
||||||
import asyncio
|
|
||||||
import influxdb_api
|
|
||||||
import influxdb_info
|
|
||||||
import project_info
|
|
||||||
|
|
||||||
# 为了让多个任务并发运行,我们可以用 asyncio.to_thread 分别启动它们
|
|
||||||
async def main():
|
|
||||||
task1 = asyncio.to_thread(auto_realtime.realtime_task)
|
|
||||||
task2 = asyncio.to_thread(auto_store_non_realtime_SCADA_data.store_non_realtime_SCADA_data_task)
|
|
||||||
await asyncio.gather(task1, task2)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
url = influxdb_info.url
|
|
||||||
token = influxdb_info.token
|
|
||||||
org_name = influxdb_info.org
|
|
||||||
|
|
||||||
influxdb_api.query_pg_scada_info_realtime(project_info.name)
|
|
||||||
influxdb_api.query_pg_scada_info_non_realtime(project_info.name)
|
|
||||||
|
|
||||||
# 用 asyncio 并发启动两个任务
|
|
||||||
asyncio.run(main())
|
|
||||||
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import schedule
|
|
||||||
import time
|
|
||||||
import datetime
|
|
||||||
import shutil
|
|
||||||
import redis
|
|
||||||
import urllib.request
|
|
||||||
import influxdb_api
|
|
||||||
import msgpack
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
# 将 Query的信息 序列号到 redis/json, 默认不支持datetime,需要自定义
|
|
||||||
# 自定义序列化函数
|
|
||||||
# 序列化处理器
|
|
||||||
def encode_datetime(obj):
|
|
||||||
"""将datetime转换为可序列化的字典结构"""
|
|
||||||
if isinstance(obj, datetime.datetime):
|
|
||||||
return {
|
|
||||||
'__datetime__': True,
|
|
||||||
'as_str': obj.strftime("%Y%m%dT%H:%M:%S.%f")
|
|
||||||
}
|
|
||||||
return obj
|
|
||||||
|
|
||||||
# 反序列化处理器
|
|
||||||
def decode_datetime(obj):
|
|
||||||
"""将字典还原为datetime对象"""
|
|
||||||
if '__datetime__' in obj:
|
|
||||||
return datetime.datetime.strptime(
|
|
||||||
obj['as_str'], "%Y%m%dT%H:%M:%S.%f"
|
|
||||||
)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
##########################
|
|
||||||
# 需要用Python 3.12 来运行才能提高performance
|
|
||||||
##########################
|
|
||||||
|
|
||||||
def queryallrecordsbydate(querydate: str, redis_client: redis.Redis):
|
|
||||||
cache_key = f"queryallrecordsbydate_{querydate}"
|
|
||||||
exists = redis_client.exists(cache_key)
|
|
||||||
|
|
||||||
if not exists:
|
|
||||||
nodes_links: tuple = influxdb_api.query_all_records_by_date(query_date=querydate)
|
|
||||||
redis_client.set(cache_key, msgpack.packb(nodes_links, default=encode_datetime))
|
|
||||||
|
|
||||||
def queryallrecordsbydate_by_url(querydate: str):
|
|
||||||
print(f'queryallrecordsbydate: {querydate}')
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = urllib.request.urlopen(
|
|
||||||
f"http://127.0.0.1/queryallrecordsbydate/?querydate={querydate}"
|
|
||||||
)
|
|
||||||
html = response.read().decode("utf-8")
|
|
||||||
|
|
||||||
except urllib.error.URLError as e:
|
|
||||||
print("Error")
|
|
||||||
|
|
||||||
def queryallscadarecordsbydate(querydate: str, redis_client: redis.Redis):
|
|
||||||
cache_key = f"queryallscadarecordsbydate_{querydate}"
|
|
||||||
exists = redis_client.exists(cache_key)
|
|
||||||
|
|
||||||
if not exists:
|
|
||||||
result_dict = influxdb_api.query_all_SCADA_records_by_date(query_date=querydate)
|
|
||||||
redis_client.set(cache_key, msgpack.packb(result_dict, default=encode_datetime))
|
|
||||||
|
|
||||||
def queryallscadarecordsbydate_by_url(querydate: str):
|
|
||||||
print(f'queryallscadarecordsbydate: {querydate}')
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = urllib.request.urlopen(
|
|
||||||
f"http://127.0.0.1/queryallscadarecordsbydate/?querydate={querydate}"
|
|
||||||
)
|
|
||||||
html = response.read().decode("utf-8")
|
|
||||||
|
|
||||||
except urllib.error.URLError as e:
|
|
||||||
print("Error")
|
|
||||||
|
|
||||||
|
|
||||||
def auto_cache_data():
|
|
||||||
# 初始化 Redis 连接
|
|
||||||
# 用redis 限制并发访u
|
|
||||||
redis_client = redis.Redis(host="127.0.0.1", port=6379, db=0)
|
|
||||||
|
|
||||||
# auto cache data for the last 3 days
|
|
||||||
today = datetime.date.today()
|
|
||||||
for i in range(1, 4):
|
|
||||||
prev_day = today - datetime.timedelta(days=i)
|
|
||||||
str_prev_day = prev_day.strftime('%Y-%m-%d')
|
|
||||||
print(str_prev_day)
|
|
||||||
|
|
||||||
queryallrecordsbydate(str_prev_day, redis_client)
|
|
||||||
queryallscadarecordsbydate(str_prev_day, redis_client)
|
|
||||||
|
|
||||||
redis_client.close()
|
|
||||||
|
|
||||||
def auto_cache_data_by_url():
|
|
||||||
# auto cache data for the last 3 days
|
|
||||||
today = datetime.date.today()
|
|
||||||
for i in range(1, 4):
|
|
||||||
prev_day = today - datetime.timedelta(days=i)
|
|
||||||
str_prev_day = prev_day.strftime('%Y-%m-%d')
|
|
||||||
print(str_prev_day)
|
|
||||||
|
|
||||||
queryallrecordsbydate_by_url(str_prev_day)
|
|
||||||
queryallscadarecordsbydate_by_url(str_prev_day)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
|
|
||||||
auto_cache_data_by_url()
|
|
||||||
|
|
||||||
# auto run in the midnight
|
|
||||||
schedule.every().day.at("03:00").do(auto_cache_data_by_url)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
schedule.run_pending()
|
|
||||||
time.sleep(1)
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
from logging.handlers import TimedRotatingFileHandler
|
|
||||||
import influxdb_api
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
import globals
|
|
||||||
from datetime import datetime, timedelta, timezone
|
|
||||||
import schedule
|
|
||||||
import time
|
|
||||||
import shutil
|
|
||||||
from influxdb_client import InfluxDBClient, BucketsApi, WriteApi, OrganizationsApi, Point, QueryApi
|
|
||||||
import simulation
|
|
||||||
import influxdb_info
|
|
||||||
import project_info
|
|
||||||
|
|
||||||
def setup_logger():
|
|
||||||
# 创建日志目录
|
|
||||||
log_dir = "logs"
|
|
||||||
os.makedirs(log_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# 配置基础日志格式
|
|
||||||
log_format = "%(asctime)s - %(levelname)s - %(message)s"
|
|
||||||
formatter = logging.Formatter(log_format)
|
|
||||||
|
|
||||||
# 创建主 Logger
|
|
||||||
logger = logging.getLogger()
|
|
||||||
logger.setLevel(logging.INFO) # 全局日志级别
|
|
||||||
|
|
||||||
# --- 1. 按日期分割的日志文件 Handler ---
|
|
||||||
log_file = os.path.join(log_dir, "simulation.log")
|
|
||||||
file_handler = TimedRotatingFileHandler(
|
|
||||||
filename=log_file,
|
|
||||||
when="midnight", # 每天午夜轮转
|
|
||||||
interval=1,
|
|
||||||
backupCount=7,
|
|
||||||
encoding="utf-8"
|
|
||||||
)
|
|
||||||
file_handler.suffix = "simulation-%Y-%m-%d.log" # 文件名格式
|
|
||||||
file_handler.setFormatter(formatter)
|
|
||||||
file_handler.setLevel(logging.INFO) # 文件记录所有级别日志
|
|
||||||
|
|
||||||
# --- 2. 控制台实时输出 Handler ---
|
|
||||||
console_handler = logging.StreamHandler() # 默认输出到 sys.stderr (控制台)
|
|
||||||
console_handler.setFormatter(formatter)
|
|
||||||
console_handler.setLevel(logging.INFO) # 控制台仅显示 INFO 及以上级别
|
|
||||||
|
|
||||||
# 将 Handler 添加到 Logger
|
|
||||||
logger.addHandler(file_handler)
|
|
||||||
#logger.addHandler(console_handler)
|
|
||||||
|
|
||||||
return logger
|
|
||||||
|
|
||||||
logger = setup_logger()
|
|
||||||
|
|
||||||
# 2025/02/01
|
|
||||||
def get_next_time() -> str:
|
|
||||||
"""
|
|
||||||
获取下一个1分钟时间点,返回格式为字符串'YYYY-MM-DDTHH:MM:00+08:00'
|
|
||||||
:return: 返回字符串格式的时间,表示下一个1分钟的时间点
|
|
||||||
"""
|
|
||||||
# 获取当前时间,并设定为北京时间
|
|
||||||
now = datetime.now() # now 类型为 datetime,表示当前本地时间
|
|
||||||
# 获取当前的分钟,并且将秒和微秒置为零
|
|
||||||
current_time = now.replace(second=0, microsecond=0) # current_time 类型为 datetime,时间的秒和微秒部分被清除
|
|
||||||
return current_time.strftime('%Y-%m-%dT%H:%M:%S+08:00')
|
|
||||||
|
|
||||||
|
|
||||||
# 2025/02/06
|
|
||||||
def store_realtime_SCADA_data_job() -> None:
|
|
||||||
"""
|
|
||||||
定义的任务1,每分钟执行1次,每次执行时,更新get_real_value_time并调用store_realtime_SCADA_data_to_influxdb函数
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
# 获取当前时间并更新get_real_value_time,转换为字符串格式
|
|
||||||
get_real_value_time: str = get_next_time() # get_real_value_time 类型为 str,格式为'2025-02-01T18:45:00+08:00'
|
|
||||||
# 调用函数执行任务
|
|
||||||
influxdb_api.store_realtime_SCADA_data_to_influxdb(get_real_value_time)
|
|
||||||
logger.info('{} -- Successfully store realtime SCADA data.'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
|
|
||||||
|
|
||||||
|
|
||||||
# 2025/02/06
|
|
||||||
def get_next_15minute_time() -> str:
|
|
||||||
"""
|
|
||||||
获取下一个15分钟的时间点,返回格式为字符串'YYYY-MM-DDTHH:MM:00+08:00'
|
|
||||||
:return: 返回字符串格式的时间,表示下一个15分钟执行时间点
|
|
||||||
"""
|
|
||||||
now = datetime.now()
|
|
||||||
# 向上舍入到下一个15分钟
|
|
||||||
next_15minute = (now.minute // 15 + 1) * 15 - 15
|
|
||||||
if next_15minute == 60:
|
|
||||||
next_15minute = 0
|
|
||||||
now = now + timedelta(hours=1)
|
|
||||||
next_time = now.replace(minute=next_15minute, second=0, microsecond=0)
|
|
||||||
return next_time.strftime('%Y-%m-%dT%H:%M:%S+08:00')
|
|
||||||
|
|
||||||
|
|
||||||
# 2025/02/07
|
|
||||||
def run_simulation_job() -> None:
|
|
||||||
"""
|
|
||||||
定义的任务3,每15分钟执行一次在store_realtime_SCADA_data_to_influxdb之后执行run_simulation。
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
# 获取当前时间,并检查是否是整点15分钟
|
|
||||||
current_time = datetime.now()
|
|
||||||
if current_time.minute % 15 == 0:
|
|
||||||
print(f"{current_time.strftime('%Y-%m-%d %H:%M:%S')} -- Start simulation task.")
|
|
||||||
# 计算前,获取scada_info中的信息,按照设定的方法修改pg数据库
|
|
||||||
simulation.query_corresponding_element_id_and_query_id(project_info.name)
|
|
||||||
simulation.query_corresponding_pattern_id_and_query_id(project_info.name)
|
|
||||||
region_result = simulation.query_non_realtime_region(project_info.name)
|
|
||||||
globals.source_outflow_region_id = simulation.get_source_outflow_region_id(project_info.name, region_result)
|
|
||||||
globals.realtime_region_pipe_flow_and_demand_id = simulation.query_realtime_region_pipe_flow_and_demand_id(project_info.name, region_result)
|
|
||||||
globals.pipe_flow_region_patterns = simulation.query_pipe_flow_region_patterns(project_info.name)
|
|
||||||
globals.non_realtime_region_patterns = simulation.query_non_realtime_region_patterns(project_info.name, region_result)
|
|
||||||
globals.source_outflow_region_patterns, realtime_region_pipe_flow_and_demand_patterns = simulation.get_realtime_region_patterns(project_info.name,
|
|
||||||
globals.source_outflow_region_id,
|
|
||||||
globals.realtime_region_pipe_flow_and_demand_id)
|
|
||||||
modify_pattern_start_time: str = get_next_15minute_time() # 获取下一个15分钟时间点
|
|
||||||
# print(modify_pattern_start_time)
|
|
||||||
simulation.run_simulation(name=project_info.name, simulation_type="realtime", modify_pattern_start_time=modify_pattern_start_time)
|
|
||||||
|
|
||||||
logger.info('{} -- Successfully run simulation and store realtime simulation result.'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
|
|
||||||
else:
|
|
||||||
logger.info(f"{current_time.strftime('%Y-%m-%d %H:%M:%S')} -- Skipping the simulation task.")
|
|
||||||
|
|
||||||
|
|
||||||
# 2025/02/06
|
|
||||||
def realtime_task() -> None:
|
|
||||||
"""
|
|
||||||
定时执行任务1和,使用schedule库每1分钟执行一次store_realtime_SCADA_data_job函数。
|
|
||||||
该任务会一直运行,定期调用store_realtime_SCADA_data_job获取SCADA数据。
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
# 等待到整分对齐
|
|
||||||
now = datetime.now()
|
|
||||||
wait_seconds = 60 - now.second
|
|
||||||
time.sleep(wait_seconds)
|
|
||||||
# 使用 .at(":00") 指定在每分钟的第0秒执行
|
|
||||||
schedule.every(1).minute.at(":00").do(store_realtime_SCADA_data_job)
|
|
||||||
# 每15分钟执行一次run_simulation_job
|
|
||||||
schedule.every(1).minute.at(":00").do(run_simulation_job)
|
|
||||||
# 持续执行任务,检查是否有待执行的任务
|
|
||||||
while True:
|
|
||||||
schedule.run_pending() # 执行所有待处理的定时任务
|
|
||||||
time.sleep(1) # 暂停1秒,避免过于频繁的任务检查
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
url = influxdb_info.url
|
|
||||||
token = influxdb_info.token
|
|
||||||
org_name = influxdb_info.org
|
|
||||||
|
|
||||||
client = InfluxDBClient(url=url, token=token)
|
|
||||||
# step2: 先查询pg数据库中scada_info的信息,然后存储SCADA数据到SCADA_data这个bucket里
|
|
||||||
influxdb_api.query_pg_scada_info_realtime(project_info.name)
|
|
||||||
# 自动执行
|
|
||||||
realtime_task()
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import influxdb_api
|
|
||||||
import globals
|
|
||||||
from datetime import datetime, timedelta, timezone
|
|
||||||
import schedule
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
|
||||||
import time
|
|
||||||
from influxdb_client import InfluxDBClient, BucketsApi, WriteApi, OrganizationsApi, Point, QueryApi
|
|
||||||
import influxdb_info
|
|
||||||
import project_info
|
|
||||||
|
|
||||||
def setup_logger():
|
|
||||||
# 创建日志目录
|
|
||||||
log_dir = "logs"
|
|
||||||
os.makedirs(log_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# 配置基础日志格式
|
|
||||||
log_format = "%(asctime)s - %(levelname)s - %(message)s"
|
|
||||||
formatter = logging.Formatter(log_format)
|
|
||||||
|
|
||||||
# 创建主 Logger
|
|
||||||
logger = logging.getLogger()
|
|
||||||
logger.setLevel(logging.INFO) # 全局日志级别
|
|
||||||
|
|
||||||
# --- 1. 按日期分割的日志文件 Handler ---
|
|
||||||
log_file = os.path.join(log_dir, "scada.log")
|
|
||||||
file_handler = TimedRotatingFileHandler(
|
|
||||||
filename=log_file,
|
|
||||||
when="midnight", # 每天午夜轮转
|
|
||||||
interval=1,
|
|
||||||
backupCount=7,
|
|
||||||
encoding="utf-8"
|
|
||||||
)
|
|
||||||
file_handler.suffix = "scada-%Y-%m-%d.log" # 文件名格式
|
|
||||||
file_handler.setFormatter(formatter)
|
|
||||||
file_handler.setLevel(logging.INFO) # 文件记录 INFO 及以上级别
|
|
||||||
|
|
||||||
# --- 2. 控制台实时输出 Handler ---
|
|
||||||
console_handler = logging.StreamHandler() # 默认输出到 sys.stderr (控制台)
|
|
||||||
console_handler.setFormatter(formatter)
|
|
||||||
console_handler.setLevel(logging.INFO) # 控制台仅显示 INFO 及以上级别
|
|
||||||
|
|
||||||
# 将 Handler 添加到 Logger
|
|
||||||
logger.addHandler(file_handler)
|
|
||||||
# logger.addHandler(console_handler)
|
|
||||||
|
|
||||||
return logger
|
|
||||||
|
|
||||||
|
|
||||||
logger = setup_logger()
|
|
||||||
|
|
||||||
# 2025/02/01
|
|
||||||
def get_next_time() -> str:
|
|
||||||
"""
|
|
||||||
获取下一个1分钟时间点,返回格式为字符串'YYYY-MM-DDTHH:MM:00+08:00'
|
|
||||||
:return: 返回字符串格式的时间,表示下一个1分钟的时间点
|
|
||||||
"""
|
|
||||||
# 获取当前时间,并设定为北京时间
|
|
||||||
now = datetime.now() # now 类型为 datetime,表示当前本地时间
|
|
||||||
# 获取当前的分钟,并且将秒和微秒置为零
|
|
||||||
current_time = now.replace(second=0, microsecond=0) # current_time 类型为 datetime,时间的秒和微秒部分被清除
|
|
||||||
return current_time.strftime('%Y-%m-%dT%H:%M:%S+08:00')
|
|
||||||
|
|
||||||
|
|
||||||
# 2025/02/06
|
|
||||||
def get_next_period_time() -> str:
|
|
||||||
"""
|
|
||||||
获取下一个6小时时间点,返回格式为字符串'YYYY-MM-DDTHH:00:00+08:00'
|
|
||||||
:return: 返回字符串格式的时间,表示下一个6小时执行时间点
|
|
||||||
"""
|
|
||||||
# 获取当前时间,并设定为北京时间
|
|
||||||
now = datetime.now() # now 类型为 datetime,表示当前本地时间
|
|
||||||
# 获取当前的小时数并计算下一个6小时时间点
|
|
||||||
next_period_hour = (now.hour // 6 + 1) * 6 - 6 # next_period_hour 类型为 int,表示下一个6小时时间点的小时部分
|
|
||||||
# 如果计算的小时大于23,表示进入第二天,调整为00:00
|
|
||||||
if next_period_hour >= 24:
|
|
||||||
next_period_hour = 0
|
|
||||||
now = now + timedelta(days=1) # 如果超过24小时,日期增加1天
|
|
||||||
# 将秒和微秒部分清除,构建出下一个6小时点的datetime对象
|
|
||||||
next_period_time = now.replace(hour=next_period_hour, minute=0, second=0, microsecond=0)
|
|
||||||
return next_period_time.strftime('%Y-%m-%dT%H:%M:%S+08:00') # 格式化为指定的字符串格式并返回
|
|
||||||
|
|
||||||
|
|
||||||
# 2025/02/06
|
|
||||||
def store_non_realtime_SCADA_data_job() -> None:
|
|
||||||
"""
|
|
||||||
定义的任务2,每6小时执行一次,在0点、6点、12点、18点执行,执行时,更新get_history_data_end_time并调用store_non_realtime_SCADA_data_to_influxdb函数
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
# 获取当前时间
|
|
||||||
current_time = datetime.now()
|
|
||||||
# 只在0点、6点、12点、18点执行任务
|
|
||||||
# if current_time.hour % 6 == 0 and current_time.minute == 0:
|
|
||||||
if current_time.minute % 10 == 0:
|
|
||||||
logger.info(f"{current_time.strftime('%Y-%m-%d %H:%M:%S')} -- Start store non realtime SCADA data task.")
|
|
||||||
# 获取下一个6小时的时间点,并更新get_history_data_end_time
|
|
||||||
get_history_data_end_time: str = get_next_time() # get_history_data_end_time 类型为 str,格式为'2025-02-06T12:00:00+08:00'
|
|
||||||
# print(get_next_time)
|
|
||||||
# 调用函数执行任务
|
|
||||||
influxdb_api.store_non_realtime_SCADA_data_to_influxdb(get_history_data_end_time)
|
|
||||||
logger.info('{} -- Successfully store non realtime SCADA data.'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
|
|
||||||
else:
|
|
||||||
logger.info(f"{current_time.strftime('%Y-%m-%d %H:%M:%S')} -- Skipping store non realtime SCADA data task.")
|
|
||||||
|
|
||||||
|
|
||||||
# 2025/02/06
|
|
||||||
def store_non_realtime_SCADA_data_task() -> None:
|
|
||||||
"""
|
|
||||||
定时执行6小时的任务,使用schedule库每分钟执行一次store_non_realtime_SCADA_data_job函数。
|
|
||||||
该任务会一直运行,定期调用store_non_realtime_SCADA_data_job获取SCADA数据。
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
# 等待到整分对齐
|
|
||||||
now = datetime.now()
|
|
||||||
wait_seconds = 60 - now.second
|
|
||||||
time.sleep(wait_seconds)
|
|
||||||
try:
|
|
||||||
# 每分钟检查一次,执行store_non_realtime_SCADA_data_job
|
|
||||||
schedule.every(1).minute.at(":00").do(store_non_realtime_SCADA_data_job)
|
|
||||||
# 持续执行任务,检查是否有待执行的任务
|
|
||||||
while True:
|
|
||||||
schedule.run_pending() # 执行所有待处理的定时任务
|
|
||||||
time.sleep(1) # 暂停1秒,避免过于频繁的任务检查
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error occurred in store_non_realtime_SCADA_data_task: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
url = influxdb_info.url
|
|
||||||
token = influxdb_info.token
|
|
||||||
org_name = influxdb_info.org
|
|
||||||
|
|
||||||
client = InfluxDBClient(url=url, token=token)
|
|
||||||
# step2: 先查询pg数据库中scada_info的信息,然后存储SCADA数据到SCADA_data这个bucket里
|
|
||||||
influxdb_api.query_pg_scada_info_non_realtime(project_info.name)
|
|
||||||
# 自动执行
|
|
||||||
store_non_realtime_SCADA_data_task()
|
|
||||||
@@ -2,9 +2,6 @@ from distutils.core import setup
|
|||||||
from Cython.Build import cythonize
|
from Cython.Build import cythonize
|
||||||
|
|
||||||
setup(ext_modules=cythonize([
|
setup(ext_modules=cythonize([
|
||||||
"main.py",
|
|
||||||
"auto_realtime.py",
|
|
||||||
"auto_store_non_realtime_SCADA_data.py",
|
|
||||||
"tjnetwork.py",
|
"tjnetwork.py",
|
||||||
"online_Analysis.py",
|
"online_Analysis.py",
|
||||||
"sensitivity.py",
|
"sensitivity.py",
|
||||||
@@ -15,10 +12,8 @@ setup(ext_modules=cythonize([
|
|||||||
"get_data.py",
|
"get_data.py",
|
||||||
"get_current_total_Q.py",
|
"get_current_total_Q.py",
|
||||||
"get_current_status.py",
|
"get_current_status.py",
|
||||||
"influxdb_api.py",
|
|
||||||
"influxdb_query_SCADA_data.py",
|
|
||||||
"simulation.py",
|
"simulation.py",
|
||||||
"time_api.py",
|
"time_api.py",
|
||||||
"api/*.py",
|
"api/*.py",
|
||||||
"epanet/*.py"
|
"epanet/*.py"
|
||||||
]))
|
]))
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ def get_history_data(
|
|||||||
# print(data)
|
# print(data)
|
||||||
|
|
||||||
# # 定义 CSV 文件的路径
|
# # 定义 CSV 文件的路径
|
||||||
# csv_file_path = './influxdb_data_4984.csv'
|
|
||||||
# # 将数据写入 CSV 文件
|
# # 将数据写入 CSV 文件
|
||||||
# # with open(csv_file_path, mode='w', newline='') as file:
|
# # with open(csv_file_path, mode='w', newline='') as file:
|
||||||
# # writer = csv.writer(file)
|
# # writer = csv.writer(file)
|
||||||
@@ -143,7 +142,6 @@ def get_history_data(
|
|||||||
# #
|
# #
|
||||||
# # print(f"数据已保存到 {csv_file_path}")
|
# # print(f"数据已保存到 {csv_file_path}")
|
||||||
#
|
#
|
||||||
# filtered_csv_file_path = './filtered_influxdb_data_4984.csv'
|
|
||||||
# #
|
# #
|
||||||
# # # # 读取并筛选数据
|
# # # # 读取并筛选数据
|
||||||
# data_list1 = []
|
# data_list1 = []
|
||||||
|
|||||||
@@ -18,14 +18,12 @@ def install():
|
|||||||
packages = [
|
packages = [
|
||||||
'"psycopg[binary]"',
|
'"psycopg[binary]"',
|
||||||
'pytest',
|
'pytest',
|
||||||
'influxdb_client',
|
|
||||||
'numpy',
|
'numpy',
|
||||||
'fastapi',
|
'fastapi',
|
||||||
"msgpack",
|
"msgpack",
|
||||||
'schedule',
|
'schedule',
|
||||||
'pandas',
|
'pandas',
|
||||||
'openpyxl',
|
'openpyxl',
|
||||||
'redis',
|
|
||||||
'pydantic',
|
'pydantic',
|
||||||
'python-dateutil',
|
'python-dateutil',
|
||||||
'starlette',
|
'starlette',
|
||||||
|
|||||||
-4481
File diff suppressed because it is too large
Load Diff
@@ -1,395 +0,0 @@
|
|||||||
# API Endpoints (scripts/main.py)
|
|
||||||
|
|
||||||
Non-commented FastAPI routes defined in `scripts/main.py`.
|
|
||||||
|
|
||||||
- `POST /login/`
|
|
||||||
- `GET /getallextensiondatakeys/`
|
|
||||||
- `GET /getallextensiondata/`
|
|
||||||
- `GET /getextensiondata/`
|
|
||||||
- `POST /setextensiondata`
|
|
||||||
- `GET /listprojects/`
|
|
||||||
- `GET /haveproject/`
|
|
||||||
- `POST /createproject/`
|
|
||||||
- `POST /deleteproject/`
|
|
||||||
- `GET /isprojectopen/`
|
|
||||||
- `POST /openproject/`
|
|
||||||
- `POST /closeproject/`
|
|
||||||
- `POST /copyproject/`
|
|
||||||
- `POST /importinp/`
|
|
||||||
- `GET /exportinp/`
|
|
||||||
- `POST /readinp/`
|
|
||||||
- `GET /dumpinp/`
|
|
||||||
- `GET /runproject/`
|
|
||||||
- `GET /runprojectreturndict/`
|
|
||||||
- `GET /runinp/`
|
|
||||||
- `GET /dumpoutput/`
|
|
||||||
- `GET /isprojectlocked/`
|
|
||||||
- `GET /isprojectlockedbyme/`
|
|
||||||
- `POST /lockproject/`
|
|
||||||
- `POST /unlockproject/`
|
|
||||||
- `GET /getcurrentoperationid/`
|
|
||||||
- `POST /undo/`
|
|
||||||
- `POST /redo/`
|
|
||||||
- `GET /getsnapshots/`
|
|
||||||
- `GET /havesnapshot/`
|
|
||||||
- `GET /havesnapshotforoperation/`
|
|
||||||
- `GET /havesnapshotforcurrentoperation/`
|
|
||||||
- `POST /takesnapshotforoperation/`
|
|
||||||
- `POST takenapshotforcurrentoperation`
|
|
||||||
- `POST /takesnapshot/`
|
|
||||||
- `POST /picksnapshot/`
|
|
||||||
- `POST /pickoperation/`
|
|
||||||
- `GET /syncwithserver/`
|
|
||||||
- `POST /batch/`
|
|
||||||
- `POST /compressedbatch/`
|
|
||||||
- `GET /getrestoreoperation/`
|
|
||||||
- `POST /setrestoreoperation/`
|
|
||||||
- `GET /isnode/`
|
|
||||||
- `GET /isjunction/`
|
|
||||||
- `GET /isreservoir/`
|
|
||||||
- `GET /istank/`
|
|
||||||
- `GET /islink/`
|
|
||||||
- `GET /ispipe/`
|
|
||||||
- `GET /ispump/`
|
|
||||||
- `GET /isvalve/`
|
|
||||||
- `GET /getnodetype/`
|
|
||||||
- `GET /getlinktype/`
|
|
||||||
- `GET /getelementtype/`
|
|
||||||
- `GET /getelementtypevalue/`
|
|
||||||
- `GET /iscurve/`
|
|
||||||
- `GET /ispattern/`
|
|
||||||
- `GET /getnodes/`
|
|
||||||
- `GET /getlinks/`
|
|
||||||
- `GET /getcurves/`
|
|
||||||
- `GET /getpatterns/`
|
|
||||||
- `GET /getnodelinks/`
|
|
||||||
- `GET /getnodeproperties/`
|
|
||||||
- `GET /getlinkproperties/`
|
|
||||||
- `GET /getscadaproperties/`
|
|
||||||
- `GET /getallscadaproperties/`
|
|
||||||
- `GET /getelementpropertieswithtype/`
|
|
||||||
- `GET /getelementproperties/`
|
|
||||||
- `GET /gettitleschema/`
|
|
||||||
- `GET /gettitle/`
|
|
||||||
- `GET /settitle/`
|
|
||||||
- `GET /getjunctionschema`
|
|
||||||
- `POST /addjunction/`
|
|
||||||
- `POST /deletejunction/`
|
|
||||||
- `GET /getjunctionelevation/`
|
|
||||||
- `GET /getjunctionx/`
|
|
||||||
- `GET /getjunctiony/`
|
|
||||||
- `GET /getjunctioncoord/`
|
|
||||||
- `GET /getjunctiondemand/`
|
|
||||||
- `GET /getjunctionpattern/`
|
|
||||||
- `POST /setjunctionelevation/`
|
|
||||||
- `POST /setjunctionx/`
|
|
||||||
- `POST /setjunctiony/`
|
|
||||||
- `POST /setjunctioncoord/`
|
|
||||||
- `POST /setjunctiondemand/`
|
|
||||||
- `POST /setjunctionpattern/`
|
|
||||||
- `GET /getjunctionproperties/`
|
|
||||||
- `GET /getalljunctionproperties/`
|
|
||||||
- `POST /setjunctionproperties/`
|
|
||||||
- `GET /getreservoirschema`
|
|
||||||
- `POST /addreservoir/`
|
|
||||||
- `POST /deletereservoir/`
|
|
||||||
- `GET /getreservoirhead/`
|
|
||||||
- `GET /getreservoirpattern/`
|
|
||||||
- `GET /getreservoirx/`
|
|
||||||
- `GET /getreservoiry/`
|
|
||||||
- `GET /getreservoircoord/`
|
|
||||||
- `POST /setreservoirhead/`
|
|
||||||
- `POST /setreservoirpattern/`
|
|
||||||
- `POST /setreservoirx/`
|
|
||||||
- `POST /setreservoirx/`
|
|
||||||
- `POST /setreservoircoord/`
|
|
||||||
- `GET /getreservoirproperties/`
|
|
||||||
- `GET /getallreservoirproperties/`
|
|
||||||
- `POST /setreservoirproperties/`
|
|
||||||
- `GET /gettankschema`
|
|
||||||
- `POST /addtank/`
|
|
||||||
- `POST /deletetank/`
|
|
||||||
- `GET /gettankelevation/`
|
|
||||||
- `GET /gettankinitlevel/`
|
|
||||||
- `GET /gettankminlevel/`
|
|
||||||
- `GET /gettankmaxlevel/`
|
|
||||||
- `GET /gettankdiameter/`
|
|
||||||
- `GET /gettankminvol/`
|
|
||||||
- `GET /gettankvolcurve/`
|
|
||||||
- `GET /gettankoverflow/`
|
|
||||||
- `GET /gettankx/`
|
|
||||||
- `GET /gettanky/`
|
|
||||||
- `GET /gettankcoord/`
|
|
||||||
- `POST /settankelevation/`
|
|
||||||
- `POST /settankinitlevel/`
|
|
||||||
- `POST /settankminlevel/`
|
|
||||||
- `POST /settankmaxlevel/`
|
|
||||||
- `POST settankdiameter//`
|
|
||||||
- `POST /settankminvol/`
|
|
||||||
- `POST /settankvolcurve/`
|
|
||||||
- `POST /settankoverflow/`
|
|
||||||
- `POST /settankx/`
|
|
||||||
- `POST /settanky/`
|
|
||||||
- `POST /settankcoord/`
|
|
||||||
- `GET /gettankproperties/`
|
|
||||||
- `GET /getalltankproperties/`
|
|
||||||
- `POST /settankproperties/`
|
|
||||||
- `GET /getpipeschema`
|
|
||||||
- `POST /addpipe/`
|
|
||||||
- `POST /deletepipe/`
|
|
||||||
- `GET /getpipenode1/`
|
|
||||||
- `GET /getpipenode2/`
|
|
||||||
- `GET /getpipelength/`
|
|
||||||
- `GET /getpipediameter/`
|
|
||||||
- `GET /getpiperoughness/`
|
|
||||||
- `GET /getpipeminorloss/`
|
|
||||||
- `GET /getpipestatus/`
|
|
||||||
- `POST /setpipenode1/`
|
|
||||||
- `POST /setpipenode2/`
|
|
||||||
- `POST /setpipelength/`
|
|
||||||
- `POST /setpipediameter/`
|
|
||||||
- `POST /setpiperoughness/`
|
|
||||||
- `POST /setpipeminorloss/`
|
|
||||||
- `POST /setpipestatus/`
|
|
||||||
- `GET /getpipeproperties/`
|
|
||||||
- `GET /getallpipeproperties/`
|
|
||||||
- `POST /setpipeproperties/`
|
|
||||||
- `GET /getpumpschema`
|
|
||||||
- `POST /addpump/`
|
|
||||||
- `POST /deletepump/`
|
|
||||||
- `GET /getpumpnode1/`
|
|
||||||
- `GET /getpumpnode2/`
|
|
||||||
- `POST /setpumpnode1/`
|
|
||||||
- `POST /setpumpnode2/`
|
|
||||||
- `GET /getpumpproperties/`
|
|
||||||
- `GET /getallpumpproperties/`
|
|
||||||
- `POST /setpumpproperties/`
|
|
||||||
- `GET /getvalveschema`
|
|
||||||
- `POST /addvalve/`
|
|
||||||
- `POST /deletevalve/`
|
|
||||||
- `GET /getvalvenode1/`
|
|
||||||
- `GET /getvalvenode2/`
|
|
||||||
- `GET /getvalvediameter/`
|
|
||||||
- `GET /getvalvetype/`
|
|
||||||
- `GET /getvalvesetting/`
|
|
||||||
- `GET /getvalveminorloss/`
|
|
||||||
- `POST /setvalvenode1/`
|
|
||||||
- `POST /setvalvenode2/`
|
|
||||||
- `POST /setvalvenodediameter/`
|
|
||||||
- `POST /setvalvetype/`
|
|
||||||
- `POST /setvalvesetting/`
|
|
||||||
- `GET /getvalveproperties/`
|
|
||||||
- `GET /getallvalveproperties/`
|
|
||||||
- `POST /setvalveproperties/`
|
|
||||||
- `POST /deletenode/`
|
|
||||||
- `POST /deletelink/`
|
|
||||||
- `GET /gettagschema/`
|
|
||||||
- `GET /gettag/`
|
|
||||||
- `GET /gettags/`
|
|
||||||
- `POST /settag/`
|
|
||||||
- `GET /getdemandschema`
|
|
||||||
- `GET /getdemandproperties/`
|
|
||||||
- `POST /setdemandproperties/`
|
|
||||||
- `GET /getstatusschema`
|
|
||||||
- `GET /getstatus/`
|
|
||||||
- `POST /setstatus/`
|
|
||||||
- `GET /getpatternschema`
|
|
||||||
- `POST /addpattern/`
|
|
||||||
- `POST /deletepattern/`
|
|
||||||
- `GET /getpatternproperties/`
|
|
||||||
- `POST /setpatternproperties/`
|
|
||||||
- `GET /getcurveschema`
|
|
||||||
- `POST /addcurve/`
|
|
||||||
- `POST /deletecurve/`
|
|
||||||
- `GET /getcurveproperties/`
|
|
||||||
- `POST /setcurveproperties/`
|
|
||||||
- `GET /getcontrolschema/`
|
|
||||||
- `GET /getcontrolproperties/`
|
|
||||||
- `POST /setcontrolproperties/`
|
|
||||||
- `GET /getruleschema/`
|
|
||||||
- `GET /getruleproperties/`
|
|
||||||
- `POST /setruleproperties/`
|
|
||||||
- `GET /getenergyschema/`
|
|
||||||
- `GET /getenergyproperties/`
|
|
||||||
- `POST /setenergyproperties/`
|
|
||||||
- `GET /getpumpenergyschema/`
|
|
||||||
- `GET /getpumpenergyproperties//`
|
|
||||||
- `GET /setpumpenergyproperties//`
|
|
||||||
- `GET /getemitterschema`
|
|
||||||
- `GET /getemitterproperties/`
|
|
||||||
- `POST /setemitterproperties/`
|
|
||||||
- `GET /getqualityschema/`
|
|
||||||
- `GET /getqualityproperties/`
|
|
||||||
- `POST /setqualityproperties/`
|
|
||||||
- `GET /getsourcechema/`
|
|
||||||
- `GET /getsource/`
|
|
||||||
- `POST /setsource/`
|
|
||||||
- `POST /addsource/`
|
|
||||||
- `POST /deletesource/`
|
|
||||||
- `GET /getreactionschema/`
|
|
||||||
- `GET /getreaction/`
|
|
||||||
- `POST /setreaction/`
|
|
||||||
- `GET /getpipereactionschema/`
|
|
||||||
- `GET /getpipereaction/`
|
|
||||||
- `POST /setpipereaction/`
|
|
||||||
- `GET /gettankreactionschema/`
|
|
||||||
- `GET /gettankreaction/`
|
|
||||||
- `POST /settankreaction/`
|
|
||||||
- `GET /getmixingschema/`
|
|
||||||
- `GET /getmixing/`
|
|
||||||
- `POST /setmixing/`
|
|
||||||
- `POST /addmixing/`
|
|
||||||
- `POST /deletemixing/`
|
|
||||||
- `GET /gettimeschema`
|
|
||||||
- `GET /gettimeproperties/`
|
|
||||||
- `POST /settimeproperties/`
|
|
||||||
- `GET /getoptionschema/`
|
|
||||||
- `GET /getoptionproperties/`
|
|
||||||
- `POST /setoptionproperties/`
|
|
||||||
- `GET /getnodecoord/`
|
|
||||||
- `GET /getnetworkgeometries/`
|
|
||||||
- `GET /getmajornodecoords/`
|
|
||||||
- `GET /getnetworkinextent/`
|
|
||||||
- `GET /getnetworklinknodes/`
|
|
||||||
- `GET /getmajorpipenodes/`
|
|
||||||
- `GET /getvertexschema/`
|
|
||||||
- `GET /getvertexproperties/`
|
|
||||||
- `POST /setvertexproperties/`
|
|
||||||
- `POST /addvertex/`
|
|
||||||
- `POST /deletevertex/`
|
|
||||||
- `GET /getallvertexlinks/`
|
|
||||||
- `GET /getallvertices/`
|
|
||||||
- `GET /getlabelschema/`
|
|
||||||
- `GET /getlabelproperties/`
|
|
||||||
- `POST /setlabelproperties/`
|
|
||||||
- `POST /addlabel/`
|
|
||||||
- `POST /deletelabel/`
|
|
||||||
- `GET /getbackdropschema/`
|
|
||||||
- `GET /getbackdropproperties/`
|
|
||||||
- `POST /setbackdropproperties/`
|
|
||||||
- `GET /getscadadeviceschema/`
|
|
||||||
- `GET /getscadadevice/`
|
|
||||||
- `POST /setscadadevice/`
|
|
||||||
- `POST /addscadadevice/`
|
|
||||||
- `POST /deletescadadevice/`
|
|
||||||
- `POST /cleanscadadevice/`
|
|
||||||
- `GET /getallscadadeviceids/`
|
|
||||||
- `GET /getallscadadevices/`
|
|
||||||
- `GET /getscadadevicedataschema/`
|
|
||||||
- `GET /getscadadevicedata/`
|
|
||||||
- `POST /setscadadevicedata/`
|
|
||||||
- `POST /addscadadevicedata/`
|
|
||||||
- `POST /deletescadadevicedata/`
|
|
||||||
- `POST /cleanscadadevicedata/`
|
|
||||||
- `GET /getscadaelementschema/`
|
|
||||||
- `GET /getscadaelements/`
|
|
||||||
- `GET /getscadaelement/`
|
|
||||||
- `POST /setscadaelement/`
|
|
||||||
- `POST /addscadaelement/`
|
|
||||||
- `POST /deletescadaelement/`
|
|
||||||
- `POST /cleanscadaelement/`
|
|
||||||
- `GET /getregionschema/`
|
|
||||||
- `GET /getregion/`
|
|
||||||
- `POST /setregion/`
|
|
||||||
- `POST /addregion/`
|
|
||||||
- `POST /deleteregion/`
|
|
||||||
- `GET /calculatedistrictmeteringareafornodes/`
|
|
||||||
- `GET /calculatedistrictmeteringareaforregion/`
|
|
||||||
- `GET /calculatedistrictmeteringareafornetwork/`
|
|
||||||
- `GET /getdistrictmeteringareaschema/`
|
|
||||||
- `GET /getdistrictmeteringarea/`
|
|
||||||
- `POST /setdistrictmeteringarea/`
|
|
||||||
- `POST /adddistrictmeteringarea/`
|
|
||||||
- `POST /deletedistrictmeteringarea/`
|
|
||||||
- `GET /getalldistrictmeteringareaids/`
|
|
||||||
- `GET /getalldistrictmeteringareas/`
|
|
||||||
- `POST /generatedistrictmeteringarea/`
|
|
||||||
- `POST /generatesubdistrictmeteringarea/`
|
|
||||||
- `GET /calculateservicearea/`
|
|
||||||
- `GET /getserviceareaschema/`
|
|
||||||
- `GET /getservicearea/`
|
|
||||||
- `POST /setservicearea/`
|
|
||||||
- `POST /addservicearea/`
|
|
||||||
- `POST /deleteservicearea/`
|
|
||||||
- `GET /getallserviceareas/`
|
|
||||||
- `POST /generateservicearea/`
|
|
||||||
- `GET /calculatevirtualdistrict/`
|
|
||||||
- `GET /getvirtualdistrictschema/`
|
|
||||||
- `GET /getvirtualdistrict/`
|
|
||||||
- `POST /setvirtualdistrict/`
|
|
||||||
- `POST /addvirtualdistrict/`
|
|
||||||
- `POST /deletevirtualdistrict/`
|
|
||||||
- `GET /getallvirtualdistrict/`
|
|
||||||
- `POST /generatevirtualdistrict/`
|
|
||||||
- `GET /calculatedemandtonodes/`
|
|
||||||
- `GET /calculatedemandtoregion/`
|
|
||||||
- `GET /calculatedemandtonetwork/`
|
|
||||||
- `GET /getscadainfoschema/`
|
|
||||||
- `GET /getscadainfo/`
|
|
||||||
- `GET /getallscadainfo/`
|
|
||||||
- `GET /getschemeschema/`
|
|
||||||
- `GET /getscheme/`
|
|
||||||
- `GET /getallschemes/`
|
|
||||||
- `GET /getpiperiskprobabilitynow/`
|
|
||||||
- `GET /getpiperiskprobability/`
|
|
||||||
- `GET /getpipesriskprobability/`
|
|
||||||
- `GET /getnetworkpiperiskprobabilitynow/`
|
|
||||||
- `GET /getpiperiskprobabilitygeometries/`
|
|
||||||
- `GET /getallsensorplacements/`
|
|
||||||
- `GET /getallburstlocateresults/`
|
|
||||||
- `POST /uploadinp/`
|
|
||||||
- `GET /downloadinp/`
|
|
||||||
- `GET /convertv3tov2/`
|
|
||||||
- `GET /getjson/`
|
|
||||||
- `GET /getrealtimedata/`
|
|
||||||
- `GET /getsimulationresult/`
|
|
||||||
- `GET /querynodelatestrecordbyid/`
|
|
||||||
- `GET /querylinklatestrecordbyid/`
|
|
||||||
- `GET /queryscadalatestrecordbyid/`
|
|
||||||
- `GET /queryallrecordsbytime/`
|
|
||||||
- `GET /queryallrecordsbytimeproperty/`
|
|
||||||
- `GET /queryallschemerecordsbytimeproperty/`
|
|
||||||
- `GET /querysimulationrecordsbyidtime/`
|
|
||||||
- `GET /queryschemesimulationrecordsbyidtime/`
|
|
||||||
- `GET /queryallrecordsbydate/`
|
|
||||||
- `GET /queryallrecordsbytimerange/`
|
|
||||||
- `GET /queryallrecordsbydatewithtype/`
|
|
||||||
- `GET /queryallrecordsbyidsdatetype/`
|
|
||||||
- `GET /queryallrecordsbydateproperty/`
|
|
||||||
- `GET /querynodecurvebyidpropertydaterange/`
|
|
||||||
- `GET /querylinkcurvebyidpropertydaterange/`
|
|
||||||
- `GET /queryscadadatabydeviceidandtime/`
|
|
||||||
- `GET /queryscadadatabydeviceidandtimerange/`
|
|
||||||
- `GET /queryfillingscadadatabydeviceidandtimerange/`
|
|
||||||
- `GET /querycleaningscadadatabydeviceidandtimerange/`
|
|
||||||
- `GET /querysimulationscadadatabydeviceidandtimerange/`
|
|
||||||
- `GET /querycleanedscadadatabydeviceidandtimerange/`
|
|
||||||
- `GET /queryscadadatabydeviceidanddate/`
|
|
||||||
- `GET /queryallscadarecordsbydate/`
|
|
||||||
- `GET /queryallschemeallrecords/`
|
|
||||||
- `GET /queryschemeallrecordsproperty/`
|
|
||||||
- `POST /clearrediskey/`
|
|
||||||
- `POST /clearrediskeys/`
|
|
||||||
- `POST /clearallredis/`
|
|
||||||
- `GET /queryredis/`
|
|
||||||
- `GET /queryinfluxdbbuckets/`
|
|
||||||
- `GET /queryinfluxdbbucketmeasurements/`
|
|
||||||
- `POST /download_history_data_manually/`
|
|
||||||
- `POST /runsimulationmanuallybydate/`
|
|
||||||
- `POST /burst_analysis/`
|
|
||||||
- `GET /valve_close_analysis/`
|
|
||||||
- `GET /flushing_analysis/`
|
|
||||||
- `GET /contaminant_simulation/`
|
|
||||||
- `GET /age_analysis/`
|
|
||||||
- `POST /scheduling_analysis/`
|
|
||||||
- `POST /pressure_regulation/`
|
|
||||||
- `POST /project_management/`
|
|
||||||
- `POST /network_project/`
|
|
||||||
- `POST /daily_scheduling_analysis/`
|
|
||||||
- `POST /network_update/`
|
|
||||||
- `POST /pump_failure/`
|
|
||||||
- `POST /pressure_sensor_placement_sensitivity/`
|
|
||||||
- `POST /pressure_sensor_placement_kmeans/`
|
|
||||||
- `POST /sensorplacementscheme/create`
|
|
||||||
- `POST /scadadevicedatacleaning/`
|
|
||||||
- `POST /test_dict/`
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import redis
|
|
||||||
|
|
||||||
redis_client = redis.Redis(host="127.0.0.1", port=6379, db=0)
|
|
||||||
matched_keys = redis_client.keys(f"**")
|
|
||||||
redis_client.delete(*matched_keys)
|
|
||||||
@@ -10,6 +10,8 @@ from app.auth.metadata_dependencies import (
|
|||||||
get_current_metadata_admin,
|
get_current_metadata_admin,
|
||||||
get_metadata_repository,
|
get_metadata_repository,
|
||||||
)
|
)
|
||||||
|
from app.infra.db.metadb.repositories.metadata_repository import ProjectDbRouting
|
||||||
|
from app.infra.db.project_routing import get_project_pgconn_string
|
||||||
from tests.conftest import build_test_app
|
from tests.conftest import build_test_app
|
||||||
|
|
||||||
|
|
||||||
@@ -98,3 +100,47 @@ def test_model_import_rejects_non_inp_file(monkeypatch):
|
|||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert response.json()["detail"] == "Only .inp model files are accepted"
|
assert response.json()["detail"] == "Only .inp model files are accepted"
|
||||||
model_import.log_audit_event.assert_not_awaited()
|
model_import.log_audit_event.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_update_uses_project_business_routing(monkeypatch):
|
||||||
|
project_id = uuid4()
|
||||||
|
project = SimpleNamespace(id=project_id, code="demo", status="active")
|
||||||
|
repo = SimpleNamespace(
|
||||||
|
session=object(),
|
||||||
|
get_project_by_id=AsyncMock(return_value=project),
|
||||||
|
get_project_db_routing=AsyncMock(
|
||||||
|
return_value=ProjectDbRouting(
|
||||||
|
project_id=project_id,
|
||||||
|
db_role="biz_data",
|
||||||
|
db_type="postgresql",
|
||||||
|
dsn="postgresql://user:password@biz.example/routed_business",
|
||||||
|
pool_min_size=1,
|
||||||
|
pool_max_size=5,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
captured: dict[str, str] = {}
|
||||||
|
|
||||||
|
async def fake_apply_model_update(content: bytes, project_code: str) -> None:
|
||||||
|
assert content == VALID_INP
|
||||||
|
captured["project_code"] = project_code
|
||||||
|
captured["dsn"] = get_project_pgconn_string(project_code)
|
||||||
|
|
||||||
|
monkeypatch.setattr(model_import, "_apply_model_update", fake_apply_model_update)
|
||||||
|
monkeypatch.setattr(model_import, "log_audit_event", AsyncMock())
|
||||||
|
client = _client(
|
||||||
|
admin=SimpleNamespace(id=uuid4(), role="admin", is_superuser=False),
|
||||||
|
repo=repo,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.patch(
|
||||||
|
f"/api/v1/admin/projects/{project_id}/model-imports",
|
||||||
|
files={"file": ("desktop-model.inp", VALID_INP)},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert captured == {
|
||||||
|
"project_code": "demo",
|
||||||
|
"dsn": "postgresql://user:password@biz.example/routed_business",
|
||||||
|
}
|
||||||
|
repo.get_project_db_routing.assert_awaited_once_with(project_id, "biz_data")
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import inspect
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import Mock
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -12,15 +12,44 @@ from fastapi.testclient import TestClient
|
|||||||
|
|
||||||
from app.api.v1.endpoints import schemes as schemes_endpoint
|
from app.api.v1.endpoints import schemes as schemes_endpoint
|
||||||
from app.api.v1.endpoints import simulation as simulation_endpoint
|
from app.api.v1.endpoints import simulation as simulation_endpoint
|
||||||
from app.api.v1.endpoints import cache as cache_endpoint
|
|
||||||
from app.api.pagination import PaginatedList
|
from app.api.pagination import PaginatedList
|
||||||
from app.api.v1.rest_router import api_router, build_rest_router
|
from app.api.v1.rest_router import api_router, build_rest_router
|
||||||
from app.api.v1.router import api_router as source_api_router
|
from app.api.v1.router import api_router as source_api_router
|
||||||
from app.auth.metadata_dependencies import get_current_metadata_user
|
from app.auth.metadata_dependencies import get_current_metadata_user
|
||||||
from app.auth.project_dependencies import ProjectContext, get_project_context
|
from app.auth.project_dependencies import (
|
||||||
|
ProjectContext,
|
||||||
|
get_project_business_routing,
|
||||||
|
get_project_context,
|
||||||
|
get_project_simulation_routing,
|
||||||
|
)
|
||||||
|
from app.infra.db.project_routing import (
|
||||||
|
ActiveProjectRouting,
|
||||||
|
get_project_pgconn_string,
|
||||||
|
get_project_timescale_pgconn_string,
|
||||||
|
)
|
||||||
from scripts.check_openapi import current_contract_bytes, validate
|
from scripts.check_openapi import current_contract_bytes, validate
|
||||||
|
|
||||||
|
|
||||||
|
def _override_project_routing(
|
||||||
|
app: FastAPI,
|
||||||
|
project_context: ProjectContext,
|
||||||
|
) -> None:
|
||||||
|
app.dependency_overrides[get_project_context] = lambda: project_context
|
||||||
|
business = ActiveProjectRouting(
|
||||||
|
project_code=project_context.project_code,
|
||||||
|
business_dsn=f"postgresql://user:password@biz/{project_context.project_code}",
|
||||||
|
)
|
||||||
|
simulation = ActiveProjectRouting(
|
||||||
|
project_code=project_context.project_code,
|
||||||
|
business_dsn=business.business_dsn,
|
||||||
|
timescale_dsn=(
|
||||||
|
f"postgresql://user:password@timescale/{project_context.project_code}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
app.dependency_overrides[get_project_business_routing] = lambda: business
|
||||||
|
app.dependency_overrides[get_project_simulation_routing] = lambda: simulation
|
||||||
|
|
||||||
|
|
||||||
def test_rest_router_preserves_every_distinct_source_operation() -> None:
|
def test_rest_router_preserves_every_distinct_source_operation() -> None:
|
||||||
skipped_names = {"fastapi_get_json", "fastapi_test_dict"}
|
skipped_names = {"fastapi_get_json", "fastapi_test_dict"}
|
||||||
source_names = {
|
source_names = {
|
||||||
@@ -44,6 +73,16 @@ def test_rest_router_has_unique_method_path_pairs() -> None:
|
|||||||
assert len(pairs) == len(set(pairs))
|
assert len(pairs) == len(set(pairs))
|
||||||
|
|
||||||
|
|
||||||
|
def test_removed_redis_management_routes_are_not_published() -> None:
|
||||||
|
published_paths = {
|
||||||
|
route.path for route in api_router.routes if isinstance(route, APIRoute)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert published_paths.isdisjoint(
|
||||||
|
{"/redis-keys/detail", "/redis-keys", "/all-redis", "/redis"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_rest_router_rejects_duplicate_method_path_pairs() -> None:
|
def test_rest_router_rejects_duplicate_method_path_pairs() -> None:
|
||||||
first = APIRoute(
|
first = APIRoute(
|
||||||
"/duplicate",
|
"/duplicate",
|
||||||
@@ -176,6 +215,21 @@ def test_valve_isolation_route_uses_the_isolation_handler() -> None:
|
|||||||
assert route.name == "valve_isolation_endpoint"
|
assert route.name == "valve_isolation_endpoint"
|
||||||
|
|
||||||
|
|
||||||
|
def test_open_project_route_requires_business_and_timescale_routing() -> None:
|
||||||
|
route = next(
|
||||||
|
route
|
||||||
|
for route in api_router.routes
|
||||||
|
if isinstance(route, APIRoute)
|
||||||
|
and route.path == "/projects/current"
|
||||||
|
and route.methods == {"POST"}
|
||||||
|
)
|
||||||
|
routing_parameter = inspect.signature(route.endpoint).parameters[
|
||||||
|
"_rest_project_routing"
|
||||||
|
]
|
||||||
|
|
||||||
|
assert routing_parameter.default.dependency is get_project_simulation_routing
|
||||||
|
|
||||||
|
|
||||||
def test_valve_isolation_runtime_accepts_frontend_query(monkeypatch) -> None:
|
def test_valve_isolation_runtime_accepts_frontend_query(monkeypatch) -> None:
|
||||||
captured: dict[str, object] = {}
|
captured: dict[str, object] = {}
|
||||||
|
|
||||||
@@ -184,6 +238,8 @@ def test_valve_isolation_runtime_accepts_frontend_query(monkeypatch) -> None:
|
|||||||
network=network,
|
network=network,
|
||||||
accident_element=accident_element,
|
accident_element=accident_element,
|
||||||
disabled_valves=disabled_valves,
|
disabled_valves=disabled_valves,
|
||||||
|
business_dsn=get_project_pgconn_string(network),
|
||||||
|
timescale_dsn=get_project_timescale_pgconn_string(network),
|
||||||
)
|
)
|
||||||
return {"isolatable": True, "must_close_valves": ["V-1"]}
|
return {"isolatable": True, "must_close_valves": ["V-1"]}
|
||||||
|
|
||||||
@@ -194,12 +250,13 @@ def test_valve_isolation_runtime_accepts_frontend_query(monkeypatch) -> None:
|
|||||||
)
|
)
|
||||||
app = FastAPI(redirect_slashes=False)
|
app = FastAPI(redirect_slashes=False)
|
||||||
app.include_router(api_router, prefix="/api/v1")
|
app.include_router(api_router, prefix="/api/v1")
|
||||||
app.dependency_overrides[get_project_context] = lambda: ProjectContext(
|
project_context = ProjectContext(
|
||||||
project_id=uuid4(),
|
project_id=uuid4(),
|
||||||
project_code="fengyang",
|
project_code="fengyang",
|
||||||
user_id=uuid4(),
|
user_id=uuid4(),
|
||||||
project_role="member",
|
project_role="member",
|
||||||
)
|
)
|
||||||
|
_override_project_routing(app, project_context)
|
||||||
|
|
||||||
response = TestClient(app, raise_server_exceptions=False).post(
|
response = TestClient(app, raise_server_exceptions=False).post(
|
||||||
"/api/v1/valve-isolation-analyses",
|
"/api/v1/valve-isolation-analyses",
|
||||||
@@ -216,6 +273,8 @@ def test_valve_isolation_runtime_accepts_frontend_query(monkeypatch) -> None:
|
|||||||
"network": "fengyang",
|
"network": "fengyang",
|
||||||
"accident_element": ["P-1", "P-2"],
|
"accident_element": ["P-1", "P-2"],
|
||||||
"disabled_valves": ["V-9"],
|
"disabled_valves": ["V-9"],
|
||||||
|
"business_dsn": "postgresql://user:password@biz/fengyang",
|
||||||
|
"timescale_dsn": "postgresql://user:password@timescale/fengyang",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -247,6 +306,7 @@ def test_rest_runtime_consumes_injected_project_context(monkeypatch) -> None:
|
|||||||
network=network,
|
network=network,
|
||||||
scheme_type=scheme_type,
|
scheme_type=scheme_type,
|
||||||
query_date=query_date,
|
query_date=query_date,
|
||||||
|
business_dsn=get_project_pgconn_string(network),
|
||||||
)
|
)
|
||||||
return [{"scheme_name": "burst_case", "scheme_type": scheme_type}]
|
return [{"scheme_name": "burst_case", "scheme_type": scheme_type}]
|
||||||
|
|
||||||
@@ -263,7 +323,7 @@ def test_rest_runtime_consumes_injected_project_context(monkeypatch) -> None:
|
|||||||
user_id=uuid4(),
|
user_id=uuid4(),
|
||||||
project_role="viewer",
|
project_role="viewer",
|
||||||
)
|
)
|
||||||
app.dependency_overrides[get_project_context] = lambda: project_context
|
_override_project_routing(app, project_context)
|
||||||
|
|
||||||
response = TestClient(app, raise_server_exceptions=False).get(
|
response = TestClient(app, raise_server_exceptions=False).get(
|
||||||
"/api/v1/schemes",
|
"/api/v1/schemes",
|
||||||
@@ -275,6 +335,7 @@ def test_rest_runtime_consumes_injected_project_context(monkeypatch) -> None:
|
|||||||
"network": "fengyang",
|
"network": "fengyang",
|
||||||
"scheme_type": "burst_analysis",
|
"scheme_type": "burst_analysis",
|
||||||
"query_date": None,
|
"query_date": None,
|
||||||
|
"business_dsn": "postgresql://user:password@biz/fengyang",
|
||||||
}
|
}
|
||||||
assert response.json()["items"] == [
|
assert response.json()["items"] == [
|
||||||
{"scheme_name": "burst_case", "scheme_type": "burst_analysis"}
|
{"scheme_name": "burst_case", "scheme_type": "burst_analysis"}
|
||||||
@@ -324,12 +385,13 @@ def test_sensor_placement_body_uses_authenticated_project_and_user(
|
|||||||
)
|
)
|
||||||
app = FastAPI(redirect_slashes=False)
|
app = FastAPI(redirect_slashes=False)
|
||||||
app.include_router(api_router, prefix="/api/v1")
|
app.include_router(api_router, prefix="/api/v1")
|
||||||
app.dependency_overrides[get_project_context] = lambda: ProjectContext(
|
project_context = ProjectContext(
|
||||||
project_id=uuid4(),
|
project_id=uuid4(),
|
||||||
project_code="project_a",
|
project_code="project_a",
|
||||||
user_id=uuid4(),
|
user_id=uuid4(),
|
||||||
project_role="member",
|
project_role="member",
|
||||||
)
|
)
|
||||||
|
_override_project_routing(app, project_context)
|
||||||
app.dependency_overrides[get_current_metadata_user] = lambda: type(
|
app.dependency_overrides[get_current_metadata_user] = lambda: type(
|
||||||
"User", (), {"username": "alice"}
|
"User", (), {"username": "alice"}
|
||||||
)()
|
)()
|
||||||
@@ -353,26 +415,6 @@ def test_sensor_placement_body_uses_authenticated_project_and_user(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_cache_management_requires_environment_permission(monkeypatch) -> None:
|
|
||||||
flushdb = Mock(return_value=True)
|
|
||||||
monkeypatch.setattr(cache_endpoint.redis_client, "flushdb", flushdb)
|
|
||||||
app = FastAPI(redirect_slashes=False)
|
|
||||||
app.include_router(api_router, prefix="/api/v1")
|
|
||||||
app.dependency_overrides[get_project_context] = lambda: ProjectContext(
|
|
||||||
project_id=uuid4(),
|
|
||||||
project_code="project_a",
|
|
||||||
user_id=uuid4(),
|
|
||||||
project_role="member",
|
|
||||||
)
|
|
||||||
|
|
||||||
response = TestClient(app, raise_server_exceptions=False).delete(
|
|
||||||
"/api/v1/all-redis"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 403
|
|
||||||
flushdb.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
def test_rest_runtime_json_encodes_untyped_datetime_response() -> None:
|
def test_rest_runtime_json_encodes_untyped_datetime_response() -> None:
|
||||||
source_router = APIRouter()
|
source_router = APIRouter()
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ from uuid import uuid4
|
|||||||
import pytest
|
import pytest
|
||||||
from cryptography.fernet import InvalidToken
|
from cryptography.fernet import InvalidToken
|
||||||
|
|
||||||
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
from app.infra.db.metadb.repositories.metadata_repository import (
|
||||||
|
MetadataRepository,
|
||||||
|
_normalize_postgres_dsn,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class _DummyResult:
|
class _DummyResult:
|
||||||
@@ -124,6 +127,12 @@ def test_encrypted_dsn_decrypts_without_migration(monkeypatch):
|
|||||||
session.commit.assert_not_awaited()
|
session.commit.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
|
def test_psycopg_sqlalchemy_dsn_is_normalized_for_direct_psycopg_clients():
|
||||||
|
assert _normalize_postgres_dsn(
|
||||||
|
"postgresql+psycopg://user:secret@db.example/project"
|
||||||
|
) == "postgresql://user:secret@db.example/project"
|
||||||
|
|
||||||
|
|
||||||
def test_upsert_project_database_config_encrypts_plaintext_dsn(monkeypatch):
|
def test_upsert_project_database_config_encrypts_plaintext_dsn(monkeypatch):
|
||||||
project_id = uuid4()
|
project_id = uuid4()
|
||||||
session = SimpleNamespace(
|
session = SimpleNamespace(
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import pytest
|
||||||
|
from psycopg.conninfo import conninfo_to_dict
|
||||||
|
|
||||||
|
from app.infra.db.project_routing import (
|
||||||
|
ActiveProjectRouting,
|
||||||
|
activate_project_routing,
|
||||||
|
get_active_project_routing,
|
||||||
|
get_project_pgconn_string,
|
||||||
|
get_project_timescale_pgconn_string,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _routing(project_code: str = "project_a") -> ActiveProjectRouting:
|
||||||
|
return ActiveProjectRouting(
|
||||||
|
project_code=project_code,
|
||||||
|
business_dsn=(
|
||||||
|
"postgresql://biz_user:biz_password@biz.example:5432/biz_database"
|
||||||
|
"?sslmode=require"
|
||||||
|
),
|
||||||
|
timescale_dsn=(
|
||||||
|
"postgresql://ts_user:ts_password@timescale.example:5433/ts_database"
|
||||||
|
"?sslmode=require"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_database_uses_exact_routing_dsn_for_project_code() -> None:
|
||||||
|
routing = _routing()
|
||||||
|
|
||||||
|
with activate_project_routing(routing):
|
||||||
|
assert get_project_pgconn_string("project_a") == routing.business_dsn
|
||||||
|
assert (
|
||||||
|
get_project_timescale_pgconn_string("project_a")
|
||||||
|
== routing.timescale_dsn
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_business_template_keeps_server_and_timescale_ignores_legacy_db_name() -> None:
|
||||||
|
with activate_project_routing(_routing()):
|
||||||
|
business = conninfo_to_dict(get_project_pgconn_string("project_a_template"))
|
||||||
|
timescale = conninfo_to_dict(
|
||||||
|
get_project_timescale_pgconn_string("temporary_scheme")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert business == {
|
||||||
|
"user": "biz_user",
|
||||||
|
"password": "biz_password",
|
||||||
|
"dbname": "project_a_template",
|
||||||
|
"host": "biz.example",
|
||||||
|
"port": "5432",
|
||||||
|
"sslmode": "require",
|
||||||
|
}
|
||||||
|
assert timescale == {
|
||||||
|
"user": "ts_user",
|
||||||
|
"password": "ts_password",
|
||||||
|
"dbname": "ts_database",
|
||||||
|
"host": "timescale.example",
|
||||||
|
"port": "5433",
|
||||||
|
"sslmode": "require",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_routing_is_nested_and_request_local() -> None:
|
||||||
|
first = _routing("project_a")
|
||||||
|
second = _routing("project_b")
|
||||||
|
|
||||||
|
assert get_active_project_routing() is None
|
||||||
|
with activate_project_routing(first):
|
||||||
|
assert get_active_project_routing() is first
|
||||||
|
with activate_project_routing(second):
|
||||||
|
assert get_active_project_routing() is second
|
||||||
|
assert get_active_project_routing() is first
|
||||||
|
assert get_active_project_routing() is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_timescale_access_requires_iot_routing_in_project_request() -> None:
|
||||||
|
business_only = _routing()
|
||||||
|
business_only = ActiveProjectRouting(
|
||||||
|
project_code=business_only.project_code,
|
||||||
|
business_dsn=business_only.business_dsn,
|
||||||
|
)
|
||||||
|
|
||||||
|
with activate_project_routing(business_only):
|
||||||
|
with pytest.raises(RuntimeError, match="TimescaleDB routing is not configured"):
|
||||||
|
get_project_timescale_pgconn_string()
|
||||||
@@ -35,7 +35,9 @@ class _FakeConnection:
|
|||||||
def test_query_scheme_list_pushes_scheme_type_into_sql(monkeypatch):
|
def test_query_scheme_list_pushes_scheme_type_into_sql(monkeypatch):
|
||||||
cursor = _FakeCursor()
|
cursor = _FakeCursor()
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
scheme_management, "get_pgconn_string", lambda db_name=None: "postgres://test"
|
scheme_management,
|
||||||
|
"get_project_pgconn_string",
|
||||||
|
lambda db_name=None: "postgres://test",
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
scheme_management.psycopg, "connect", lambda _conn_string: _FakeConnection(cursor)
|
scheme_management.psycopg, "connect", lambda _conn_string: _FakeConnection(cursor)
|
||||||
|
|||||||
@@ -45,9 +45,11 @@ class _FakeConnection:
|
|||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def clear_native_connections():
|
def clear_native_connections():
|
||||||
connection.g_conn_dict.clear()
|
connection.g_conn_dict.clear()
|
||||||
|
connection.g_conninfo_dict.clear()
|
||||||
connection._project_locks.clear()
|
connection._project_locks.clear()
|
||||||
yield
|
yield
|
||||||
connection.g_conn_dict.clear()
|
connection.g_conn_dict.clear()
|
||||||
|
connection.g_conninfo_dict.clear()
|
||||||
connection._project_locks.clear()
|
connection._project_locks.clear()
|
||||||
|
|
||||||
|
|
||||||
@@ -61,6 +63,10 @@ def test_is_project_open_drops_closed_cached_connection():
|
|||||||
def test_open_connection_reuses_healthy_cached_connection(monkeypatch):
|
def test_open_connection_reuses_healthy_cached_connection(monkeypatch):
|
||||||
cached = _FakeConnection()
|
cached = _FakeConnection()
|
||||||
connection.g_conn_dict["fengyang"] = cached
|
connection.g_conn_dict["fengyang"] = cached
|
||||||
|
connection.g_conninfo_dict["fengyang"] = "dbname=fengyang"
|
||||||
|
monkeypatch.setattr(
|
||||||
|
connection, "get_project_pgconn_string", lambda db_name: f"dbname={db_name}"
|
||||||
|
)
|
||||||
|
|
||||||
def fail_connect(*, conninfo, autocommit):
|
def fail_connect(*, conninfo, autocommit):
|
||||||
raise AssertionError("cached connection should be reused")
|
raise AssertionError("cached connection should be reused")
|
||||||
@@ -84,7 +90,7 @@ def test_read_all_reopens_closed_cached_connection(monkeypatch):
|
|||||||
|
|
||||||
monkeypatch.setattr(connection.pg, "connect", fake_connect)
|
monkeypatch.setattr(connection.pg, "connect", fake_connect)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
connection, "get_pgconn_string", lambda db_name: f"dbname={db_name}"
|
connection, "get_project_pgconn_string", lambda db_name: f"dbname={db_name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
rows = database.read_all("fengyang", "select * from times")
|
rows = database.read_all("fengyang", "select * from times")
|
||||||
@@ -99,6 +105,7 @@ def test_read_all_reopens_cached_connection_when_health_check_fails(monkeypatch)
|
|||||||
stale = _FakeConnection(fail_ping=True)
|
stale = _FakeConnection(fail_ping=True)
|
||||||
fresh = _FakeConnection(rows=[{"scheme_name": "base"}])
|
fresh = _FakeConnection(rows=[{"scheme_name": "base"}])
|
||||||
connection.g_conn_dict["fengyang"] = stale
|
connection.g_conn_dict["fengyang"] = stale
|
||||||
|
connection.g_conninfo_dict["fengyang"] = "dbname=fengyang"
|
||||||
|
|
||||||
opened = []
|
opened = []
|
||||||
|
|
||||||
@@ -108,7 +115,7 @@ def test_read_all_reopens_cached_connection_when_health_check_fails(monkeypatch)
|
|||||||
|
|
||||||
monkeypatch.setattr(connection.pg, "connect", fake_connect)
|
monkeypatch.setattr(connection.pg, "connect", fake_connect)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
connection, "get_pgconn_string", lambda db_name: f"dbname={db_name}"
|
connection, "get_project_pgconn_string", lambda db_name: f"dbname={db_name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
rows = database.read_all("fengyang", "select * from scheme_list")
|
rows = database.read_all("fengyang", "select * from scheme_list")
|
||||||
@@ -119,3 +126,20 @@ def test_read_all_reopens_cached_connection_when_health_check_fails(monkeypatch)
|
|||||||
assert opened == [("dbname=fengyang", True)]
|
assert opened == [("dbname=fengyang", True)]
|
||||||
assert connection.g_conn_dict["fengyang"] is fresh
|
assert connection.g_conn_dict["fengyang"] is fresh
|
||||||
assert fresh.executed == ["select * from scheme_list"]
|
assert fresh.executed == ["select * from scheme_list"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_open_connection_replaces_cache_when_project_dsn_changes(monkeypatch):
|
||||||
|
cached = _FakeConnection()
|
||||||
|
fresh = _FakeConnection()
|
||||||
|
connection.g_conn_dict["fengyang"] = cached
|
||||||
|
connection.g_conninfo_dict["fengyang"] = "host=old dbname=fengyang"
|
||||||
|
monkeypatch.setattr(
|
||||||
|
connection,
|
||||||
|
"get_project_pgconn_string",
|
||||||
|
lambda db_name: f"host=new dbname={db_name}",
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(connection.pg, "connect", lambda **_kwargs: fresh)
|
||||||
|
|
||||||
|
assert connection.open_connection("fengyang") is fresh
|
||||||
|
assert cached.close_calls == 1
|
||||||
|
assert connection.g_conninfo_dict["fengyang"] == "host=new dbname=fengyang"
|
||||||
|
|||||||
Reference in New Issue
Block a user