ForcePilot/backend/package/yuxi/external_systems/integrations/registry.py

138 lines
5.5 KiB
Python
Raw Normal View History

"""IntegrationRegistry厂商集成目录注册表。
实现 ``core/contracts.py::IntegrationRegistry`` 端口结构化子类型无需显式继承
维护 ``key -> IntegrationMetadata`` 映射应用启动时由各集成包 ``__init__.py``
通过 ``register()`` 自注册运行时只读元数据为内存态不持久化到数据库
``clear()`` 仅供测试隔离使用
键冲突策略同一 ``key`` 重复注册且元数据实例不同时抛 ``DomainValidationError``
framework ``register_protocol_auth_plugin`` / ``http_error_extractor_registry``
等注册表策略一致避免静默覆盖
"""
from __future__ import annotations
from yuxi.external_systems.exceptions import DomainValidationError, EntityNotFoundError
from yuxi.external_systems.integrations.schemas import IntegrationMetadata
class IntegrationRegistry:
"""厂商集成目录注册表,实现 core/contracts.py::IntegrationRegistry 端口。
实现 Protocol 3 个查询方法list_integrations / get_integration /
list_adapter_types并额外提供 register / clear 供框架内部使用
framework/registry/adapter_registry.py::AdapterRegistry 模式一致
"""
_integrations: dict[str, IntegrationMetadata] = {}
@classmethod
def register(cls, metadata: IntegrationMetadata) -> None:
"""注册厂商集成元数据。键冲突抛 DomainValidationError。"""
existing = cls._integrations.get(metadata.key)
if existing is not None and existing is not metadata:
raise DomainValidationError(
f"厂商集成元数据冲突: key={metadata.key} "
f"已注册 {existing.display_name},尝试注册 {metadata.display_name}"
)
cls._integrations[metadata.key] = metadata
@classmethod
def filter_integrations(
cls,
*,
adapter_type: str | None = None,
tags: list[str] | None = None,
keyword: str | None = None,
) -> list[IntegrationMetadata]:
"""按条件过滤并稳定排序厂商集成(不分页)。
list_integrations count_integrations 的共用底层方法
保证过滤逻辑单一来源避免两处实现漂移overview / categories
端点需要全量过滤结果时也直接调用本方法
- adapter_type: 精确匹配 metadata.adapter_type
- tags: OR 关系命中任一标签即返回
- keyword: 模糊匹配 display_name / description大小写不敏感
- key 升序稳定排序保证分页结果可重现
"""
items = list(cls._integrations.values())
if adapter_type is not None:
items = [it for it in items if it.adapter_type == adapter_type]
if tags:
tag_set = set(tags)
items = [it for it in items if tag_set.intersection(it.tags)]
if keyword:
kw = keyword.lower()
items = [it for it in items if kw in it.display_name.lower() or kw in it.description.lower()]
items.sort(key=lambda it: it.key)
return items
@classmethod
def list_integrations(
cls,
*,
adapter_type: str | None = None,
tags: list[str] | None = None,
keyword: str | None = None,
limit: int = 50,
offset: int = 0,
) -> list[IntegrationMetadata]:
"""查询厂商集成目录,支持按适配器类型/标签/关键词过滤与分页。
分页基于 filter_integrations 的全量结果切片全量总数请用
count_integrations 获取list 返回的 len(items) 仅为当前页条数
"""
filtered = cls.filter_integrations(adapter_type=adapter_type, tags=tags, keyword=keyword)
if offset < 0:
offset = 0
if limit < 0:
limit = 0
return filtered[offset : offset + limit]
@classmethod
def count_integrations(
cls,
*,
adapter_type: str | None = None,
tags: list[str] | None = None,
keyword: str | None = None,
) -> int:
"""统计满足筛选条件的厂商集成全量总数(不分页)。
list_integrations 共用 filter_integrations保证 total
分页结果一致 Router 层返回真实全量 total避免前端分页页数错误
"""
return len(cls.filter_integrations(adapter_type=adapter_type, tags=tags, keyword=keyword))
@classmethod
def get_integration(cls, key: str) -> IntegrationMetadata | None:
"""按 key 获取厂商集成元数据,未找到返回 None。"""
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
def list_adapter_types(cls) -> list[str]:
"""列出所有已注册厂商集成涉及的适配器类型(去重)。"""
seen: dict[str, None] = {}
for it in cls._integrations.values():
if it.adapter_type not in seen:
seen[it.adapter_type] = None
return list(seen.keys())
@classmethod
def clear(cls) -> None:
"""清空注册表,仅供测试使用。"""
cls._integrations.clear()