ForcePilot/backend/server/routers/external_systems/__init__.py

202 lines
9.7 KiB
Python
Raw Normal View History

"""外部系统限界上下文 Router 聚合层与共享装配层。
本模块承担两个职责
1. **共享装配层**为各子 Router 提供请求级独立 session UseCases / 轻量
service 装配依赖``get_use_cases`` / ``get_dashboard_service`` /
``get_adapter_stats_service`` Router 通过 ``Depends(get_use_cases)``
注入已装配的 ``UseCases``不再各自调用 ``create_use_cases_from_db(db)``
2. **Router 聚合层** 23 个子 router 通过 include_router 注册统一挂载到
/system/external-systems 前缀下 router 只需声明相对前缀 /systems
/tools根前缀 /system/external-systems 由本聚合 router 统一追加
独立 session 设计说明参照 channels 模块
external_systems 模块 **不复用** 鉴权依赖``get_db``持有的 ``AsyncSession``
而是通过 ``pg_manager.get_async_session_context()`` 开启独立的请求级会话原因
鉴权阶段执行的 ``SELECT`` 会触发 SQLAlchemy 2.0 autobegin 语义开启隐式事务
若复用同一 session控制面 ``TransactionPort.begin()`` 会在已存在的事务上再次
``begin()``且异常路径下底层 asyncpg 连接会残留 "manually started transaction"
连接归还连接池后被下个请求复用时触发
``cannot use Connection.transaction() in a manually started transaction``
独立 session 使事务边界由 external_systems 应用层独占控制与鉴权会话隔离
避免事务状态互相污染会话生命周期限制在本次请求内请求结束自动
commit无异常 rollback有异常后关闭
"""
from __future__ import annotations
from datetime import datetime
from fastapi import APIRouter
from fastapi.exceptions import RequestValidationError
from yuxi.external_systems.infrastructure.container import (
UseCases,
create_adapter_stats_service_from_db,
create_dashboard_service_from_db,
create_use_cases_from_db,
)
from yuxi.external_systems.use_cases.ports import (
AdapterStatsServicePort,
DashboardServicePort,
)
from yuxi.storage.postgres.manager import pg_manager
from yuxi.utils.datetime_utils import coerce_any_to_utc_datetime, to_naive_utc
async def get_use_cases() -> UseCases:
"""装配 external_systems UseCases 集合(请求级独立 session
通过 ``pg_manager.get_async_session_context()`` 开启独立请求级 session
调用 ``create_use_cases_from_db(db)`` 装配完整 24-service ``UseCases`` 容器
yield 完成后自动关闭 session不复用 ``get_db`` session 的原因见模块
docstring
Yields:
已装配的 ``UseCases`` 实例
"""
async with pg_manager.get_async_session_context() as db:
yield create_use_cases_from_db(db)
async def get_dashboard_service() -> DashboardServicePort:
"""装配轻量级 DashboardService请求级独立 session
Dashboard 是纯聚合查询无需完整 UseCases 容器使用
``create_dashboard_service_from_db(db)`` 轻量工厂仅构造
``DashboardService``仅注入 Repositories降低延迟与资源消耗
不复用 ``get_db`` session 的原因见模块 docstring
Yields:
已装配的 ``DashboardServicePort`` 实例
"""
async with pg_manager.get_async_session_context() as db:
yield create_dashboard_service_from_db(db)
async def get_adapter_stats_service() -> AdapterStatsServicePort:
"""装配轻量级 AdapterStatsService请求级独立 session
AdapterStatsService 仅依赖 Repositories纯查询用例无副作用无需
完整 UseCases 容器使用 ``create_adapter_stats_service_from_db(db)``
轻量工厂避免每次统计/健康查询都构造完整 24-service 容器不复用
``get_db`` session 的原因见模块 docstring
Yields:
已装配的 ``AdapterStatsServicePort`` 实例
"""
async with pg_manager.get_async_session_context() as db:
yield create_adapter_stats_service_from_db(db)
# ------------------------------------------------------------------
# 统一时间参数处理(对齐 channels 模块 parse_datetime 模式)
#
# 所有子 Router 的时间查询参数统一使用 str 类型ISO 8601
# 通过 parse_datetime 翻译为 aware UTC datetime再用 to_naive_utc
# 剥离为 naive UTC 以适配 TIMESTAMP WITHOUT TIME ZONE 列。
# ------------------------------------------------------------------
def parse_datetime(field: str, raw: str | None) -> datetime | None:
"""ISO 8601 string → aware UTC datetime失败抛 RequestValidationError (422)。
对齐 channels 模块的 parse_datetime 模式naive 字符串视为 Asia/Shanghai
aware 字符串统一转 UTCraw None 时返回 None
"""
if raw is None:
return None
try:
return coerce_any_to_utc_datetime(raw)
except (ValueError, TypeError) as exc:
raise RequestValidationError(
errors=[
{
"loc": ("query", field),
"msg": f"invalid {field} format: {raw} (expected ISO 8601)",
"type": "value_error",
"input": raw,
}
]
) from exc
def validate_time_range(start: datetime | None, end: datetime | None) -> None:
"""校验时间范围语义start 不晚于 end。失败抛 RequestValidationError (422)。"""
if start is not None and end is not None and start > end:
raise RequestValidationError(
errors=[
{
"loc": ("query", "start_time"),
"msg": "start_time 不能晚于 end_time",
"type": "value_error",
"input": {"start_time": start, "end_time": end},
}
]
)
def parse_and_naive(field: str, raw: str | None) -> datetime | None:
"""一步完成 parse_datetime + to_naive_utc返回 naive UTC datetime 或 None。"""
return to_naive_utc(parse_datetime(field, raw))
external_systems_router = APIRouter(
prefix="/system/external-systems",
tags=["external-systems"],
)
# 子 router 导入置于共享依赖定义之后,避免循环导入
# (子 router 通过 ``from server.routers.external_systems import get_use_cases``
# 反向引用本模块依赖,须保证此处已定义完成)
from server.routers.external_systems.access_rule_router import access_rule_router # noqa: E402
from server.routers.external_systems.adapter_router import adapter_router # noqa: E402
from server.routers.external_systems.alert_router import alert_router # noqa: E402
from server.routers.external_systems.asset_router import asset_router # noqa: E402
from server.routers.external_systems.audit_log_router import audit_log_router # noqa: E402
from server.routers.external_systems.auth_plugin_router import auth_plugin_router # noqa: E402
from server.routers.external_systems.dashboard_router import dashboard_router # noqa: E402
from server.routers.external_systems.environment_router import environment_router # noqa: E402
from server.routers.external_systems.execution_router import execution_router # noqa: E402
from server.routers.external_systems.health_check_router import health_check_router # noqa: E402
from server.routers.external_systems.import_router import import_router # noqa: E402
from server.routers.external_systems.integration_router import integration_router # noqa: E402
from server.routers.external_systems.integration_tool_router import integration_tool_router # noqa: E402
from server.routers.external_systems.metric_router import metric_router # noqa: E402
from server.routers.external_systems.notification_channel_router import notification_channel_router # noqa: E402
from server.routers.external_systems.quota_router import quota_router # noqa: E402
from server.routers.external_systems.secret_rotation_router import secret_rotation_router # noqa: E402
from server.routers.external_systems.system_router import system_router # noqa: E402
from server.routers.external_systems.test_case_router import test_case_router # noqa: E402
from server.routers.external_systems.token_router import token_router # noqa: E402
from server.routers.external_systems.tool_router import tool_router # noqa: E402
from server.routers.external_systems.trash_router import trash_router # noqa: E402
from server.routers.external_systems.webhook_router import webhook_router # noqa: E402
# 注册 23 个子 router
external_systems_router.include_router(system_router)
external_systems_router.include_router(tool_router)
external_systems_router.include_router(environment_router)
external_systems_router.include_router(execution_router)
external_systems_router.include_router(token_router)
external_systems_router.include_router(audit_log_router)
external_systems_router.include_router(asset_router)
external_systems_router.include_router(adapter_router)
external_systems_router.include_router(import_router)
external_systems_router.include_router(integration_router)
external_systems_router.include_router(integration_tool_router)
external_systems_router.include_router(webhook_router)
external_systems_router.include_router(health_check_router)
external_systems_router.include_router(metric_router)
external_systems_router.include_router(quota_router)
external_systems_router.include_router(alert_router)
external_systems_router.include_router(secret_rotation_router)
external_systems_router.include_router(access_rule_router)
external_systems_router.include_router(test_case_router)
external_systems_router.include_router(dashboard_router)
external_systems_router.include_router(auth_plugin_router)
external_systems_router.include_router(notification_channel_router)
external_systems_router.include_router(trash_router)