refactor(external_systems): 优化时间参数处理与新增异常能力
1. 将工具、webhook的时间DTO字段从字符串改为datetime类型 2. 移除工具服务中的手动ISO时间解析逻辑 3. 新增CapabilityNotSupportedError异常与get_integration_or_raise方法 4. 格式化代码行内表达式简化写法
This commit is contained in:
parent
00092c818e
commit
1965b9f742
@ -33,7 +33,8 @@ adapters),放在根目录便于统一导入路径。
|
|||||||
├── SecretResolutionError (400) # 密钥解析失败
|
├── SecretResolutionError (400) # 密钥解析失败
|
||||||
├── AuthError (403) # 认证失败
|
├── AuthError (403) # 认证失败
|
||||||
├── AccessDeniedError (403) # 访问规则拒绝
|
├── AccessDeniedError (403) # 访问规则拒绝
|
||||||
└── WebhookError (400) # Webhook 验签/处理失败
|
├── WebhookError (400) # Webhook 验签/处理失败
|
||||||
|
└── CapabilityNotSupportedError (501) # 适配器未实现可选能力
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@ -448,3 +449,46 @@ class WebhookError(FrameworkError):
|
|||||||
|
|
||||||
status_code = 400
|
status_code = 400
|
||||||
error_code = "WEBHOOK_ERROR"
|
error_code = "WEBHOOK_ERROR"
|
||||||
|
|
||||||
|
|
||||||
|
class CapabilityNotSupportedError(FrameworkError):
|
||||||
|
"""适配器未实现某项可选能力。
|
||||||
|
|
||||||
|
当适配器存在但不实现某项可选能力(如 ``validate_config``)时抛出,
|
||||||
|
由 Router 捕获 Python 内置 ``NotImplementedError`` 后翻译为本异常,
|
||||||
|
交由 ``unified_error_handler`` 统一映射为 HTTP 501,避免原生异常
|
||||||
|
被 ``unhandled_exception_handler`` 兜底为 500。
|
||||||
|
|
||||||
|
与 ``IntegrationOperationNotRegisteredError`` 的区别:本异常表示
|
||||||
|
适配器实例不支持该能力方法;后者表示厂商集成的操作未注册。
|
||||||
|
"""
|
||||||
|
|
||||||
|
status_code = 501
|
||||||
|
error_code = "CAPABILITY_NOT_SUPPORTED"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
capability: str,
|
||||||
|
*,
|
||||||
|
adapter_type: str | None = None,
|
||||||
|
details: dict[str, Any] | None = None,
|
||||||
|
trace_id: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""初始化能力未支持异常。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
capability: 未实现的能力名称(如 ``validate_config``)。
|
||||||
|
adapter_type: 适配器类型,写入 ``details`` 供客户端参考。
|
||||||
|
details: 业务字段字典,与上述参数合并。
|
||||||
|
trace_id: 调用链路追踪 ID,用于跨链路关联。
|
||||||
|
"""
|
||||||
|
merged: dict[str, Any] = dict(details or {})
|
||||||
|
if adapter_type is not None:
|
||||||
|
merged["adapter_type"] = adapter_type
|
||||||
|
merged["capability"] = capability
|
||||||
|
adapter_desc = f" {adapter_type}" if adapter_type else ""
|
||||||
|
super().__init__(
|
||||||
|
f"适配器{adapter_desc}未实现能力: {capability}",
|
||||||
|
details=merged,
|
||||||
|
trace_id=trace_id,
|
||||||
|
)
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from yuxi.external_systems.exceptions import DomainValidationError
|
from yuxi.external_systems.exceptions import DomainValidationError, EntityNotFoundError
|
||||||
from yuxi.external_systems.integrations.schemas import IntegrationMetadata
|
from yuxi.external_systems.integrations.schemas import IntegrationMetadata
|
||||||
|
|
||||||
|
|
||||||
@ -84,9 +84,7 @@ class IntegrationRegistry:
|
|||||||
分页基于 filter_integrations 的全量结果切片,全量总数请用
|
分页基于 filter_integrations 的全量结果切片,全量总数请用
|
||||||
count_integrations 获取(list 返回的 len(items) 仅为当前页条数)。
|
count_integrations 获取(list 返回的 len(items) 仅为当前页条数)。
|
||||||
"""
|
"""
|
||||||
filtered = cls.filter_integrations(
|
filtered = cls.filter_integrations(adapter_type=adapter_type, tags=tags, keyword=keyword)
|
||||||
adapter_type=adapter_type, tags=tags, keyword=keyword
|
|
||||||
)
|
|
||||||
if offset < 0:
|
if offset < 0:
|
||||||
offset = 0
|
offset = 0
|
||||||
if limit < 0:
|
if limit < 0:
|
||||||
@ -106,17 +104,24 @@ class IntegrationRegistry:
|
|||||||
与 list_integrations 共用 filter_integrations,保证 total 与
|
与 list_integrations 共用 filter_integrations,保证 total 与
|
||||||
分页结果一致。供 Router 层返回真实全量 total,避免前端分页页数错误。
|
分页结果一致。供 Router 层返回真实全量 total,避免前端分页页数错误。
|
||||||
"""
|
"""
|
||||||
return len(
|
return len(cls.filter_integrations(adapter_type=adapter_type, tags=tags, keyword=keyword))
|
||||||
cls.filter_integrations(
|
|
||||||
adapter_type=adapter_type, tags=tags, keyword=keyword
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_integration(cls, key: str) -> IntegrationMetadata | None:
|
def get_integration(cls, key: str) -> IntegrationMetadata | None:
|
||||||
"""按 key 获取厂商集成元数据,未找到返回 None。"""
|
"""按 key 获取厂商集成元数据,未找到返回 None。"""
|
||||||
return cls._integrations.get(key)
|
return cls._integrations.get(key)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_integration_or_raise(cls, key: str) -> IntegrationMetadata:
|
||||||
|
"""按 key 获取厂商集成元数据,未找到抛 ``EntityNotFoundError`` (404)。
|
||||||
|
|
||||||
|
供 Router 的详情类端点使用,避免每个端点重复 None 判断与抛异常逻辑。
|
||||||
|
"""
|
||||||
|
metadata = cls._integrations.get(key)
|
||||||
|
if metadata is None:
|
||||||
|
raise EntityNotFoundError(f"厂商集成 {key} 不存在")
|
||||||
|
return metadata
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def list_adapter_types(cls) -> list[str]:
|
def list_adapter_types(cls) -> list[str]:
|
||||||
"""列出所有已注册厂商集成涉及的适配器类型(去重)。"""
|
"""列出所有已注册厂商集成涉及的适配器类型(去重)。"""
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
@ -506,8 +507,8 @@ class GetToolExecutionTrendInput(BaseModel):
|
|||||||
model_config = ConfigDict(frozen=True)
|
model_config = ConfigDict(frozen=True)
|
||||||
|
|
||||||
tool_id: int
|
tool_id: int
|
||||||
start_time: str | None = None
|
start_time: datetime | None = None
|
||||||
end_time: str | None = None
|
end_time: datetime | None = None
|
||||||
interval: str = "day"
|
interval: str = "day"
|
||||||
env_key: str | None = None
|
env_key: str | None = None
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ dataclass 对齐。
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
@ -124,8 +125,8 @@ class ListWebhookEventsInput(BaseModel):
|
|||||||
triggered_execution_id: str | None = None
|
triggered_execution_id: str | None = None
|
||||||
signature_verified: bool | None = None
|
signature_verified: bool | None = None
|
||||||
source_ip: str | None = None
|
source_ip: str | None = None
|
||||||
start: Any | None = None
|
start: datetime | None = None
|
||||||
end: Any | None = None
|
end: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
class GetEventInput(BaseModel):
|
class GetEventInput(BaseModel):
|
||||||
|
|||||||
@ -25,7 +25,6 @@ tool_router 扩展端点(版本对比 / 版本回滚 / 参数模板 / 批量
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from yuxi.external_systems.core.contracts import (
|
from yuxi.external_systems.core.contracts import (
|
||||||
@ -384,11 +383,7 @@ class ToolService(ToolServicePort):
|
|||||||
adapter = self._registry.get_adapter(tool.adapter_type)
|
adapter = self._registry.get_adapter(tool.adapter_type)
|
||||||
# 系统级工具预解析 context 供 adapter 构造 schema(如 gRPC asset 加载 proto);
|
# 系统级工具预解析 context 供 adapter 构造 schema(如 gRPC asset 加载 proto);
|
||||||
# 工具级工具 context 为 None(契约允许)。
|
# 工具级工具 context 为 None(契约允许)。
|
||||||
context = (
|
context = await self._context_builder.build(tool, env) if tool.system_id is not None else None
|
||||||
await self._context_builder.build(tool, env)
|
|
||||||
if tool.system_id is not None
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
schema_tool = await adapter.build_tool(tool, context)
|
schema_tool = await adapter.build_tool(tool, context)
|
||||||
env_dict = env or {}
|
env_dict = env or {}
|
||||||
|
|
||||||
@ -994,7 +989,8 @@ class ToolService(ToolServicePort):
|
|||||||
async with self._uow:
|
async with self._uow:
|
||||||
# 2. 回滚前创建当前配置的新版本快照(记录回滚前的状态)
|
# 2. 回滚前创建当前配置的新版本快照(记录回滚前的状态)
|
||||||
new_version_id = await self._record_version_before_update(
|
new_version_id = await self._record_version_before_update(
|
||||||
tool, input_dto.updated_by,
|
tool,
|
||||||
|
input_dto.updated_by,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3. 将目标版本明文 snapshot 写回 ext_tool_configs
|
# 3. 将目标版本明文 snapshot 写回 ext_tool_configs
|
||||||
@ -1198,12 +1194,12 @@ class ToolService(ToolServicePort):
|
|||||||
details={"interval": input_dto.interval},
|
details={"interval": input_dto.interval},
|
||||||
)
|
)
|
||||||
|
|
||||||
start = self._parse_iso_datetime(input_dto.start_time)
|
start = input_dto.start_time
|
||||||
end = self._parse_iso_datetime(input_dto.end_time)
|
end = input_dto.end_time
|
||||||
if start is not None and end is not None and start > end:
|
if start is not None and end is not None and start > end:
|
||||||
raise DomainValidationError(
|
raise DomainValidationError(
|
||||||
"start_time 不能晚于 end_time",
|
"start_time 不能晚于 end_time",
|
||||||
details={"start_time": input_dto.start_time, "end_time": input_dto.end_time},
|
details={"start_time": start.isoformat(), "end_time": end.isoformat()},
|
||||||
)
|
)
|
||||||
|
|
||||||
rows = await self._repos.execution.aggregate_trend_by_tool(
|
rows = await self._repos.execution.aggregate_trend_by_tool(
|
||||||
@ -1269,21 +1265,3 @@ class ToolService(ToolServicePort):
|
|||||||
|
|
||||||
items = [to_asset_output(a) for a in assets]
|
items = [to_asset_output(a) for a in assets]
|
||||||
return GetToolAssetsOutput(items=items)
|
return GetToolAssetsOutput(items=items)
|
||||||
|
|
||||||
# ========== 内部辅助(扩展端点) ==========
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _parse_iso_datetime(value: str | None) -> datetime | None:
|
|
||||||
"""解析 ISO 8601 时间字符串,返回 ``datetime`` 或 ``None``。
|
|
||||||
|
|
||||||
解析失败时抛 ``DomainValidationError``,不静默回退。
|
|
||||||
"""
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
||||||
except ValueError as exc:
|
|
||||||
raise DomainValidationError(
|
|
||||||
f"时间格式非法: {value}(需 ISO 8601 格式)",
|
|
||||||
details={"value": value},
|
|
||||||
) from exc
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user